Grant oversight_admin write access to campaigns and proofs

Oversight admins can now create campaigns, upload proofs, and
flag/resolve issues when they have an agency assigned. They retain
all existing cross-agency read access for analytics, auditing, and
user management. Oversight admins without an agency see a read-only
campaigns view.

Changes:
- Add oversight_admin to canWrite permission in UserContext
- Guard readOnly for oversight_admin without agency in App.tsx
- Remove oversight_admin block from require_write_access dependency
- Remove WebSocket oversight_admin upload block in main.py
- Require agency for oversight_admin campaign creation in routes.py

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Vadym Samoilenko 2026-03-03 13:08:54 +00:00
parent 0348693ebd
commit 0432635153
5 changed files with 10 additions and 24 deletions

View file

@ -149,8 +149,8 @@ async def create_campaign(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_write_access),
):
"""Create a new campaign. Blocked for oversight_admin."""
if current_user.agency_id is None and current_user.role not in ("super_admin", "oversight_admin"):
"""Create a new campaign."""
if current_user.agency_id is None and current_user.role != "super_admin":
raise HTTPException(status_code=403, detail="You must be assigned to an agency before creating campaigns.")
repo = CampaignRepository(db)
@ -222,7 +222,7 @@ async def update_campaign(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_write_access),
):
"""Update a campaign. Blocked for oversight_admin."""
"""Update a campaign. """
repo = CampaignRepository(db)
# Verify campaign exists and user has access
@ -265,7 +265,7 @@ async def delete_campaign(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_write_access),
):
"""Delete a campaign and all associated files. Blocked for oversight_admin."""
"""Delete a campaign and all associated files. """
repo = CampaignRepository(db)
campaign = await repo.get_by_id(campaign_id)
@ -375,7 +375,7 @@ async def delete_proof(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_write_access),
):
"""Delete a proof and its associated files. Blocked for oversight_admin."""
"""Delete a proof and its associated files. """
repo = ProofRepository(db)
proof = await repo.get_by_id(proof_id)
@ -402,7 +402,7 @@ async def flag_proof_version(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_write_access),
):
"""Flag an issue on a proof version. Blocked for oversight_admin."""
"""Flag an issue on a proof version. """
proof_repo = ProofRepository(db)
audit_repo = AuditRepository(db)
@ -441,7 +441,7 @@ async def resolve_proof_version(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_write_access),
):
"""Resolve an issue on a proof version. Blocked for oversight_admin."""
"""Resolve an issue on a proof version. """
proof_repo = ProofRepository(db)
audit_repo = AuditRepository(db)

View file

@ -138,13 +138,5 @@ def require_role(*allowed_roles: str):
async def require_write_access(
current_user: User = Depends(get_current_db_user),
) -> User:
"""
Dependency that blocks oversight_admin from write/mutation operations.
All other roles are allowed through.
"""
if current_user.role == "oversight_admin":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Oversight Admin has read-only access",
)
"""Dependency for write/mutation operations."""
return current_user

View file

@ -201,12 +201,6 @@ async def websocket_analyze(websocket: WebSocket):
ws_user_repo = UserRepository(ws_session)
azure_oid = user_claims.get("oid") or user_claims.get("sub")
ws_user = await ws_user_repo.get_by_azure_oid(azure_oid) if azure_oid else None
if ws_user and ws_user.role == "oversight_admin":
await manager.send_message(client_id, {
"type": "error",
"message": "Oversight Admin has read-only access and cannot analyze proofs."
})
continue
current_user_id = ws_user.id if ws_user else None
except Exception as role_err:
logger.warning(f"[MAIN] Role check failed for client {client_id}: {role_err}")

View file

@ -855,7 +855,7 @@ const AppContent: React.FC<{ msalInstance: any }> = ({ msalInstance }) => {
}
};
const readOnly = !canWrite;
const readOnly = !canWrite || (isOversightAdmin && !user?.agencyId);
const renderContent = () => {
switch (currentView) {

View file

@ -73,7 +73,7 @@ export const UserProvider: React.FC<{ children: React.ReactNode }> = ({ children
isLoading,
isSuperAdmin: role === 'super_admin',
isOversightAdmin: role === 'oversight_admin',
canWrite: role === 'super_admin' || role === 'agency_admin' || role === 'basic_user',
canWrite: role === 'super_admin' || role === 'oversight_admin' || role === 'agency_admin' || role === 'basic_user',
canSeeAnalytics: role === 'super_admin' || role === 'oversight_admin' || role === 'agency_admin',
canSeeAuditing: role === 'super_admin' || role === 'oversight_admin',
canSeeKnowledgeBase: role === 'super_admin',