Skip to content

feat: people_admin role with group management permissions#144

Merged
zunkelty merged 2 commits into
mainfrom
soenke/people-admin-br6za
May 28, 2026
Merged

feat: people_admin role with group management permissions#144
zunkelty merged 2 commits into
mainfrom
soenke/people-admin-br6za

Conversation

@zunkelty

@zunkelty zunkelty commented May 28, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Introduces the people_admin grant with permissions to create groups and manage members, managers, and exports across all groups
  • Removes users.create/users.import from people_admin (admin-only)
  • Replaces the groups.view_all global action with a derived gate check using group.members.manage || group.managers.manage || group.export
  • Adds group.export to legal officers so they retain access to the admin groups page
  • Adds an optional-scope pattern to group-scoped permissions: calling without a group ID returns a grant-based gate check (useful for nav/layout guards without a specific group in context)

Test plan

  • Verify a people_admin user can access the admin groups page and sees the nav item
  • Verify a people_admin user can create a group, add/remove members, appoint/remove managers, and export
  • Verify a people_admin user cannot create or import users
  • Verify a legal officer (president/VP/head of finance) can access the admin groups page and export groups
  • Verify a plain member/department head cannot access the admin groups page
  • Run npm test — all 89 permission tests pass

Summary by CodeRabbit

  • Refactor

    • Streamlined authorization: access to admin group features now requires any of several specific group permissions rather than a single legacy permission.
    • Permission checks now support evaluating group-scoped actions without explicit group context.
  • Tests

    • Updated authorization tests and type-check assertions to reflect new permission rules and expected outcomes.

Review Change Stack

People admins can now create groups and manage members, managers, and
exports across all groups. Legal officers gain group.export to retain
access to the admin groups page. groups.view_all is replaced by a
derived gate check across the three group-scoped actions, with
group-scoped permissions now supporting an optional-scope pattern for
gate checks without a specific group ID.
@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 9d71e776-3938-4f23-8333-5e89974afcb2

📥 Commits

Reviewing files that changed from the base of the PR and between 8a35ca0 and 433dc88.

📒 Files selected for processing (1)
  • src/lib/permissions/permissions.typecheck.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/lib/permissions/permissions.typecheck.ts

📝 Walkthrough

Walkthrough

The PR removes the global groups.view_all permission, enables unscoped checks for group-scoped actions, updates evaluation logic (including group.members.manage and group.export), and updates UI gates and tests to use the new scoped permission model.

Changes

Permission system: unscoped group authorization and groups.view_all removal

Layer / File(s) Summary
Unscoped group authorization API
src/components/can.tsx, src/lib/permissions/server.ts
CanCheck/useCan/Can accept GroupScopedAction without a resource; can gains an overload with no resource and treats missing resource as non-member/non-manager for evaluation.
Authorization evaluation rules
src/lib/permissions/evaluate.ts
Removes groups.view_all from global actions; group.members.manage now allows people-admin; group.export includes isGroupManager in its OR; groups.create now allows people-admins as well.
Application authorization gates
src/components/nav-main.tsx, src/app/(authenticated)/(app)/(default)/admin/groups/layout.tsx, src/app/(authenticated)/(app)/(default)/admin/people/[id]/groups-card.tsx
Layout and components now check three group.* permissions (group.members.manage, group.managers.manage, group.export) with OR logic. NavMain adds CanAdminGroups which aggregates useCan checks and reports visibility to HidableGroupContext.
Test and type coverage updates
src/lib/permissions/permissions.test.ts, src/lib/permissions/permissions.typecheck.ts
Tests remove groups.view_all coverage, add/adjust people_admin and legal_officer assertions for group.* and groups.create, deny users.create/users.import for people_admin where applicable, and update typechecks to assert unscoped group permission checks and new user-creation scoped checks.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • startberlin/cockpit#101: Both PRs modify overlapping group-scoped authorization logic including evaluateGroupScopedAction, isGroupManager conditions, and group.managers.manage / group.export permission evaluation.

Poem

🐰 A permission garden grows anew,
Three little gates replace the view—
Scoped checks hop lightly through the grass,
Unscoped calls now let rabbits pass.
A carrot for the auth-refresh crew.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat: people_admin role with group management permissions' accurately describes the main change: introducing a new people_admin role with expanded group management capabilities. The title clearly summarizes the primary feature addition.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch soenke/people-admin-br6za

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/app/(authenticated)/(app)/(default)/admin/groups/layout.tsx (1)

11-15: ⚡ Quick win

Parallelize independent permission checks in the layout gate.

Line 11–15 performs three independent async calls sequentially, which adds unnecessary server latency for this route.

Proposed change
-  if (
-    !(await can("group.members.manage")) &&
-    !(await can("group.managers.manage")) &&
-    !(await can("group.export"))
-  ) {
+  const [canManageMembers, canManageManagers, canExport] = await Promise.all([
+    can("group.members.manage"),
+    can("group.managers.manage"),
+    can("group.export"),
+  ]);
+  if (!canManageMembers && !canManageManagers && !canExport) {
     redirect("/membership");
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`(authenticated)/(app)/(default)/admin/groups/layout.tsx around lines
11 - 15, The three independent permission checks using
can("group.members.manage"), can("group.managers.manage"), and
can("group.export") are executed sequentially, causing extra latency; change the
logic in layout.tsx to run these calls in parallel (e.g., via Promise.all or
Promise.allSettled), await them together, destructure the boolean results, and
then use the combined values in the existing conditional to decide access.
src/app/(authenticated)/(app)/(default)/admin/people/[id]/groups-card.tsx (1)

17-20: ⚡ Quick win

Run permission checks concurrently before loading groups.

Line 17–20 awaits independent checks one-by-one; use Promise.all to reduce TTFB on this server component path.

Proposed change
-  const canViewGroups =
-    (await can("group.members.manage")) ||
-    (await can("group.managers.manage")) ||
-    (await can("group.export"));
+  const [canManageMembers, canManageManagers, canExport] = await Promise.all([
+    can("group.members.manage"),
+    can("group.managers.manage"),
+    can("group.export"),
+  ]);
+  const canViewGroups = canManageMembers || canManageManagers || canExport;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`(authenticated)/(app)/(default)/admin/people/[id]/groups-card.tsx
around lines 17 - 20, The three sequential awaits when computing canViewGroups
(await can("group.members.manage"), await can("group.managers.manage"), await
can("group.export")) should be run concurrently; replace them with a Promise.all
call to execute can(...) in parallel, then compute canViewGroups from the
resulting boolean array (e.g., const [members, managers, exportPerm] = await
Promise.all([...]) and set canViewGroups = members || managers || exportPerm) so
the server component in groups-card.tsx performs permission checks concurrently.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/lib/permissions/permissions.typecheck.ts`:
- Around line 30-32: The assertion calls evaluateAuth(authority,
"group.members.manage") without the required GroupScope even though evaluateAuth
currently requires a GroupScope for group-scoped actions; fix by either (A)
updating the call site to pass a proper GroupScope object (e.g.,
evaluateAuth(authority, "group.members.manage", { groupId: "<id>" } as
GroupScope)) so the runtime and types align, or (B) change evaluateAuth's
signature/implementation to accept an optional scope for group.* actions (adjust
the parameter type, add runtime handling when scope is undefined, and update any
type guards) so unscoped group permissions are permitted—choose one approach and
make the corresponding type and runtime changes around evaluateAuth and
GroupScope usage.

---

Nitpick comments:
In `@src/app/`(authenticated)/(app)/(default)/admin/groups/layout.tsx:
- Around line 11-15: The three independent permission checks using
can("group.members.manage"), can("group.managers.manage"), and
can("group.export") are executed sequentially, causing extra latency; change the
logic in layout.tsx to run these calls in parallel (e.g., via Promise.all or
Promise.allSettled), await them together, destructure the boolean results, and
then use the combined values in the existing conditional to decide access.

In `@src/app/`(authenticated)/(app)/(default)/admin/people/[id]/groups-card.tsx:
- Around line 17-20: The three sequential awaits when computing canViewGroups
(await can("group.members.manage"), await can("group.managers.manage"), await
can("group.export")) should be run concurrently; replace them with a Promise.all
call to execute can(...) in parallel, then compute canViewGroups from the
resulting boolean array (e.g., const [members, managers, exportPerm] = await
Promise.all([...]) and set canViewGroups = members || managers || exportPerm) so
the server component in groups-card.tsx performs permission checks concurrently.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 69569e39-2726-4a68-bca4-c4ccd1716d55

📥 Commits

Reviewing files that changed from the base of the PR and between f7c1c31 and 8a35ca0.

📒 Files selected for processing (8)
  • src/app/(authenticated)/(app)/(default)/admin/groups/layout.tsx
  • src/app/(authenticated)/(app)/(default)/admin/people/[id]/groups-card.tsx
  • src/components/can.tsx
  • src/components/nav-main.tsx
  • src/lib/permissions/evaluate.ts
  • src/lib/permissions/permissions.test.ts
  • src/lib/permissions/permissions.typecheck.ts
  • src/lib/permissions/server.ts

Comment thread src/lib/permissions/permissions.typecheck.ts Outdated
@zunkelty

Copy link
Copy Markdown
Contributor Author

Fixes Applied Successfully

Fixed 1 file based on 1 CodeRabbit feedback item.

Files modified:

  • src/lib/permissions/permissions.typecheck.ts

Commit: 433dc88

The fix corrects a typecheck assertion that called evaluateAuth on a group-scoped action without the required GroupScope. The gate check pattern belongs in can()/useCan(), which explicitly pass { isGroupMember: false, isGroupManager: false } when called without a group ID.

@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor

Actionable comments posted: 0

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant