Skip to content

feat(api): add group management surface - #545

Open
abdulrafey1 wants to merge 7 commits into
rafey/feat/permission-groups-enginefrom
rafey/feat/permission-groups-api
Open

feat(api): add group management surface#545
abdulrafey1 wants to merge 7 commits into
rafey/feat/permission-groups-enginefrom
rafey/feat/permission-groups-api

Conversation

@abdulrafey1

Copy link
Copy Markdown
Contributor

Closes: Permission system needs groups to grant roles to users in bulk

Part 2 of 3 stacked PRs — stacked on #544 (engine). Management surface for the group tables the engine PR introduced.

What

Adds the management surface for permission groups: REST CRUD for group structure, CLI for membership and group-role grants, and the group.* permissions gating it — following the split already established for roles (structure via REST, principal/grant edges via CLI).

Changes

  • feat(core): declare group.create / group.read / group.update / group.delete permissions and export the group engine surface through sparkth.lib.permissions
  • feat(migrations): data migration 5c6ccf073bb7 grants the four group.* permissions to the seeded admin role (no-op when the role isn't seeded)
  • feat(api): group CRUD under /api/v1/permissions/groups (list / create / get / patch / delete), gated at the global scope; GroupNotFound → 404, GroupAlreadyExists / GroupInUse → 409 via the central exception registry
  • feat(cli): groups add-member <user> <group> and groups assign-role-to-group <group> <role> [--scope] [--scope-object-id]
  • docs: group.* rows in the permissions guide's shipped-permissions table
  • chore(frontend): regenerated frontend/lib/api/generated.ts from the OpenAPI schema

How to Test

  1. make test.backend.pytest — full suite green (1512 passed); new coverage in tests/api/v1/test_permission_groups.py and tests/cli/test_groups.py.
  2. make migrations — applies 5c6ccf073bb7 (verified up and down against PostgreSQL).
  3. As an admin, POST /api/v1/permissions/groups {"name": "cs-staff"} → 201; duplicate name → 409 with exact detail body.
  4. make cli -- groups add-member <username> cs-staff then make cli -- groups assign-role-to-group cs-staff <role> — every member now passes can() checks for that role's permissions at the granted scope.

Notes

This PR description was written with the assistance of an LLM (Claude).

@abdulrafey1 abdulrafey1 self-assigned this Jul 27, 2026
@abdulrafey1 abdulrafey1 changed the title feat(api): add group management surface (#519) feat(api): add group management surface Jul 27, 2026
@abdulrafey1 abdulrafey1 added the enhancement New feature or request label Jul 27, 2026
abdulrafey1 and others added 3 commits July 27, 2026 17:20
group.create/read/update/delete gate the group-management surface; granted to the
seeded admin role by data migration so existing admins can manage groups. The group
engine functions join the sparkth.lib.permissions facade for the API and CLI.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CRUD for user groups under /api/v1/permissions/groups, gated by the group.*
permissions at the global scope; domain exceptions mapped centrally (404/409).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
groups add-member and groups assign-role-to-group manage the principal/grant edges,
mirroring the CLI-only posture of user-role assignment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@abdulrafey1
abdulrafey1 force-pushed the rafey/feat/permission-groups-api branch from bd2a2b0 to 2a54f17 Compare July 27, 2026 12:20

@hamza-56 hamza-56 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

API layer is clean. Comments are mostly about the CLI and docs:

Comment thread sparkth/cli/groups.py
Comment thread sparkth/cli/groups.py Outdated
Comment thread sparkth/api/v1/permissions/schemas.py
Comment thread docs/guides/permissions.md
@abdulrafey1 abdulrafey1 reopened this Jul 29, 2026
abdulrafey1 and others added 2 commits July 29, 2026 19:13
…rmission-groups-api

# Conflicts:
#	frontend/lib/api/generated.ts
#	sparkth/api/v1/permissions/routes/__init__.py
- add "groups remove-member" and "groups revoke-role-from-group" so
  membership and group-role grants are no longer one-way doors (the
  engine functions existed but nothing was wired to them)
- extract _resolve_user/_resolve_group_id helpers, removing the
  copy-pasted lookup/not-found/exit blocks and the dead id-None branch
- note the known PATCH limitation on GroupUpdate (a description cannot
  be cleared back to null because None means "unchanged")

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@abdulrafey1
abdulrafey1 requested a review from hamza-56 July 29, 2026 14:32

@regisb regisb left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Super clean PR. Just one minor comment (feel free to ignore) and question.

)
async def create_group(payload: GroupCreate, session: AsyncSession = Depends(get_async_session)) -> GroupResponse:
"""Create a group. Returns the created GroupResponse (id, name, description)."""
return _group_to_response(await group_engine.create_group(payload.name, payload.description, session))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Are we not catching exceptions thrown by group_engine.create_group in these endpoints? E.g: I believe we should raise 40x if a group already exists?

@@ -0,0 +1,82 @@
"""Group sub-router (mounted at ``/groups``): CRUD for user groups (issue #519).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I like how this router is really clean.

Comment thread sparkth/cli/groups.py
permission_scope = get_permission_scope(scope)
except PermissionScopeNotFound:
typer.secho(f"Unknown scope kind: '{scope}'", fg=typer.colors.RED)
raise typer.Exit(code=1) from None

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: I'm not too confortable running typer.Exit in non-CLI functions. I understand this specific function is only called in CLI, but it might be a future footgun during a refactoring. Feel free to ignore this comment.

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

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Permission system needs groups to grant roles to users in bulk

3 participants