Skip to content

Security: Source group CRUD endpoints lack authentication/authorization checks - #127

Open
tuanaiseo wants to merge 1 commit into
GACWR:masterfrom
tuanaiseo:contribai/fix/security/source-group-crud-endpoints-lack-authent
Open

Security: Source group CRUD endpoints lack authentication/authorization checks#127
tuanaiseo wants to merge 1 commit into
GACWR:masterfrom
tuanaiseo:contribai/fix/security/source-group-crud-endpoints-lack-authent

Conversation

@tuanaiseo

Copy link
Copy Markdown

Problem

The source_groups router exposes list/get/create/update operations with only a DB dependency and no get_current_user or role-based authorization dependency. This allows unauthenticated or unauthorized callers to read and modify source group configuration, which may include sensitive ingestion settings.

Severity: high
File: core/api_routers/source_groups.py

Solution

Add authentication (Depends(get_current_user)) and enforce role-based access control (e.g., admin/editor) for create/update/delete. Restrict list/get access as appropriate and add audit logging for configuration changes.

Changes

  • core/api_routers/source_groups.py (modified)

Testing

  • Existing tests pass
  • Manual review completed
  • No new warnings/errors introduced

The source_groups router exposes list/get/create/update operations with only a DB dependency and no `get_current_user` or role-based authorization dependency. This allows unauthenticated or unauthorized callers to read and modify source group configuration, which may include sensitive ingestion settings.

Affected files: source_groups.py

Signed-off-by: tuanaiseo <221258316+tuanaiseo@users.noreply.github.com>
@Jovonni
Jovonni requested a review from Copilot April 11, 2026 14:17
@Jovonni Jovonni added the Roadmap Item - TODO This issue is already on our roadmap label Apr 11, 2026
@Jovonni

Jovonni commented Apr 11, 2026

Copy link
Copy Markdown
Collaborator

🚀 nice! was on our todo 🙏🏽

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds authentication/authorization enforcement to the source_groups API router to prevent unauthenticated access to source group configuration and to introduce basic audit logging for configuration changes.

Changes:

  • Require authentication for GET /source_groups and GET /source_groups/{group_id}.
  • Add a write-access dependency for POST/PUT endpoints.
  • Emit audit logs on source group create/update.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +34 to +48
def require_source_group_write_access(current_user: Any = Depends(get_current_user)):
role = getattr(current_user, "role", None)
roles = getattr(current_user, "roles", None)

if isinstance(current_user, dict):
role = role or current_user.get("role")
roles = roles or current_user.get("roles")

if role in {"admin", "editor"}:
return current_user

if isinstance(roles, (list, set, tuple)) and any(r in {"admin", "editor"} for r in roles):
return current_user

raise HTTPException(status_code=403, detail="Insufficient permissions")

Copilot AI Apr 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

require_source_group_write_access checks for roles {"admin", "editor"}, but the codebase’s defined roles are ["admin", "manager", "triage", "analyst"] (see core/api_routers/auth.py VALID_ROLES). As written, no user can ever have the "editor" role and non-admin roles will be blocked from writes even if they have permissions via role_permissions. Consider reusing the existing core.auth.require_permission(page, "write") mechanism (and adding a page entry for source groups if needed) or updating the role list/permission model consistently across the app.

Copilot uses AI. Check for mistakes.
db.add(db_sg)
db.commit()
db.refresh(db_sg)
logger.info("source_group_created", extra={"source_group_id": str(db_sg.id), "actor": str(getattr(current_user, "id", None) or current_user.get("id") if isinstance(current_user, dict) else None)})

Copilot AI Apr 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The audit log actor field is always None because get_current_user() returns a dict with user_id, not id. Use current_user.get("user_id") (or username) and consider extracting it into a local variable for readability.

Copilot uses AI. Check for mistakes.
db_sg.config = sg.config
db.commit()
db.refresh(db_sg)
logger.info("source_group_updated", extra={"source_group_id": str(db_sg.id), "actor": str(getattr(current_user, "id", None) or current_user.get("id") if isinstance(current_user, dict) else None)})

Copilot AI Apr 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as above: get_current_user() provides user_id, not id, so this audit log records actor=None. Use current_user.get("user_id") (or username) to make the audit trail reliable.

Copilot uses AI. Check for mistakes.
Comment on lines 50 to +54
@router.get("/", response_model=List[SourceGroupRead])
def list_source_groups(db: Session = Depends(get_db)):
def list_source_groups(
db: Session = Depends(get_db),
current_user: Any = Depends(get_current_user)
):

Copilot AI Apr 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Auth/authorization behavior changed for these endpoints (now requires authentication, and writes require elevated access), but there are no API router tests for source_groups. Add tests similar to core/tests/test_api_routers/test_jobs.py that cover: unauthenticated requests => 401, authenticated but insufficient role => 403 on POST/PUT, and permitted role => success.

Copilot uses AI. Check for mistakes.

@Jovonni Jovonni left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for catching this — the missing auth on this router is a real gap and I want it closed. A few changes before we merge so it lines up with how the rest of the API does authz:

  1. Reuse require_permission instead of a bespoke check. Every other write router (models, cases, settings, data) gates on Depends(require_permission("<page>", "write")) from core/auth.py, which honors the role_permissions table and the admin bypass. The custom require_source_group_write_access introduces an editor role that isn't part of our RBAC model, so it'd behave differently from the rest of the app. Suggest: reads → require_permission("data", "read"), writes → require_permission("data", "write"). That drops the whole helper.

  2. Dead code: get_current_user always returns a User model, never a dict, so the isinstance(current_user, dict) branches (and the inline actor extraction in the log lines) never execute. Once you switch to require_permission the audit-log lines simplify a lot — current_user.id is enough.

  3. Not blocking, just noting: this router still has no DELETE, and separately require_permission currently fails open on exception (core/auth.py). I'll track the fail-open hardening on its own — out of scope here.

Swap to require_permission and I'll get this in right after the batch of feature work I've got in flight. Appreciate the contribution.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Roadmap Item - TODO This issue is already on our roadmap

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants