From 043263515356d0a205a54a1408301b5d5d75dd82 Mon Sep 17 00:00:00 2001 From: Vadym Samoilenko Date: Tue, 3 Mar 2026 13:08:54 +0000 Subject: [PATCH] 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 --- backend/app/api/routes.py | 14 +++++++------- backend/app/dependencies/auth.py | 10 +--------- backend/app/main.py | 6 ------ frontend/App.tsx | 2 +- frontend/contexts/UserContext.tsx | 2 +- 5 files changed, 10 insertions(+), 24 deletions(-) diff --git a/backend/app/api/routes.py b/backend/app/api/routes.py index 61db577..79ffef5 100755 --- a/backend/app/api/routes.py +++ b/backend/app/api/routes.py @@ -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) diff --git a/backend/app/dependencies/auth.py b/backend/app/dependencies/auth.py index 585927a..063c18b 100755 --- a/backend/app/dependencies/auth.py +++ b/backend/app/dependencies/auth.py @@ -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 diff --git a/backend/app/main.py b/backend/app/main.py index eae0f69..cd3ca04 100755 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -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}") diff --git a/frontend/App.tsx b/frontend/App.tsx index eb70143..a1dbd34 100755 --- a/frontend/App.tsx +++ b/frontend/App.tsx @@ -855,7 +855,7 @@ const AppContent: React.FC<{ msalInstance: any }> = ({ msalInstance }) => { } }; - const readOnly = !canWrite; + const readOnly = !canWrite || (isOversightAdmin && !user?.agencyId); const renderContent = () => { switch (currentView) { diff --git a/frontend/contexts/UserContext.tsx b/frontend/contexts/UserContext.tsx index 454cd07..128836b 100644 --- a/frontend/contexts/UserContext.tsx +++ b/frontend/contexts/UserContext.tsx @@ -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',