Skip to content

Groups: share workspaces and registries with groups (native + OIDC)#346

Merged
aktech merged 42 commits into
nebari-dev:mainfrom
tylerpotts:feature/groups-rbac
Jun 1, 2026
Merged

Groups: share workspaces and registries with groups (native + OIDC)#346
aktech merged 42 commits into
nebari-dev:mainfrom
tylerpotts:feature/groups-rbac

Conversation

@tylerpotts

@tylerpotts tylerpotts commented May 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

Closes #293.
Closes #72

  • New Group primitive with native (admin-managed) and OIDC-synced sources.
  • Workspace share, registry access, and the admin role can now be granted to groups.
  • Casbin matcher switched from r.sub == p.sub to g(r.sub, p.sub) so existing direct policies still match and group memberships resolve transitively. Regression test included.
  • OIDC groups claim drives JIT membership reconciliation on every login. Refuses to merge claims into existing native groups to prevent permanent untracked grants (regression test included).
  • Admin Groups page, ShareDialog gains a User/Group tabbed selector, user-management shows each user's groups.
  • Full Swagger godoc on the new handlers; ui.md and server-setup.md updated.

Deviations from the issue

  • Matcher rewrite is required (issue claimed it wasn't). Backward compatible because Casbin treats g(x, x) as true.
  • DB + Casbin writes are sequential, not transactional (mirrors existing per-user pattern at `workspace_permissions.go:73`).
  • Group-as-admin policy uses (\"admin\", \"admin\") to match existing admin scheme (issue said `"*"`).
  • Collaborators API returns a flat list with kind discriminator instead of separate `users[]`/`groups[]`.
  • OIDC claims do not merge into native groups — silently joining an admin-managed native group via an IdP claim would create permanent untracked grants (phase-2 reconcile only considers `source=oidc`).

Bugs caught during manual walkthrough (already fixed in this branch)

  • `WorkspaceService.List` did not consider group-mediated permissions — workspaces shared with a group did not appear in members' workspace lists. Fixed in `081cace`.
  • ShareDialog group picker hid all groups from admins not in those groups. Fixed by using the existing `useIsAdmin` hook to source from `/admin/groups` for admins, `/groups/me` for non-admins (`2ffc541`).

Test plan

  • `make test` passes (`./internal/...` only — `cmd/nebi` E2E failures are pre-existing pixi-on-macOS-arm64 issues).
  • `cd frontend && npm run test && npm run lint && npx tsc -b` passes (146/146 tests).
  • Manual: native group sharing flow — group creation, member add/remove, share workspace with group, member sees workspace, remove share.
  • Manual: admin-bypass — admin can share with groups they're not in.
  • Manual: non-admin owner can only share with groups they're a member of.
  • Manual: OIDC sync — login with `groups` claim creates groups + memberships; subsequent login without a claim removes the membership; group row preserved.
  • Manual: OIDC claim naming an existing native group does NOT add the user to the native group (server logs warn).
  • Manual: `/admin/users` shows the new Groups column with native/OIDC badge styling.

Known follow-ups (not blocking)

  • Extract a `CollaboratorRow` subcomponent in ShareDialog (currently a ~55-line inline ternary).
  • Add a happy-path test for the group-share submit flow in `ShareDialog.test.tsx`.
  • Collaborators tab (`WorkspaceDetail.tsx` line ~691) currently only renders user collaborators — could render groups inline as well.

🤖 Implemented with Claude Code

tylerpotts added 30 commits May 11, 2026 14:37
WorkspaceService.List() was only querying the per-user permissions table,
so workspaces shared with a group never appeared in the user's listing
even though Casbin's CanReadWorkspace correctly returned true.

Now also unions in workspaces whose group_permissions row references any
group the user belongs to (resolved via Casbin grouping rules).
@netlify

netlify Bot commented May 11, 2026

Copy link
Copy Markdown

Deploy Preview for nebi-docs ready!

Name Link
🔨 Latest commit 13e4713
🔍 Latest deploy log https://app.netlify.com/projects/nebi-docs/deploys/6a1d23551a729e0009d9a9d1
😎 Deploy Preview https://deploy-preview-346--nebi-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

Task 9 instrumented HandleCallback (browser OAuth) and ExchangeIDToken (device
flow), but missed the proxy-cookie flow used by gateway-proxied deployments —
SessionFromProxy and the Middleware fallback path. Those call
findOrCreateProxyUser + syncRolesFromGroups but never reconciled group
memberships, so OIDC groups in the ID token were silently dropped on the wire.

Caught during in-cluster testing against tyler-hetzner-dev.
The unique index on groups.name doesn't honour gorm.DeletedAt — a
soft-deleted row keeps the name reserved at the DB level. The OIDC
sync's create-after-delete path hit ERROR: duplicate key value
violates unique constraint "idx_groups_name" whenever an admin
deleted a native group that an IdP claim later re-introduced.

DeleteGroup already hard-removes group_members and Casbin rules; the
group row was the only soft-deleted leftover and served no purpose
given the cascade. Switched to Unscoped() for the final delete.
Also unscoped the GroupPermission delete for consistency (same
unique-constraint risk if a future schema adds one).

Updated TestDeleteGroup_HardRemovesCasbinRules and added
TestDeleteGroup_FreesNameForRecreate as a regression test for the
OIDC sync's create-after-delete sequence.
The 'switches to group mode' test passed when the picker source was
/groups/me but broke after commit 2ffc541 switched the admin code path
to /admin/groups (via useIsAdmin). The default test handlers expose a
working /admin/users endpoint so useIsAdmin returns true in tests, and
the dialog correctly queries /admin/groups — which was unmocked.

Added a default /admin/groups handler returning [] (silences MSW
unhandled-request warnings across the suite) and updated the test to
mock both endpoints so it works regardless of which one the dialog
ends up calling.
@tylerpotts tylerpotts marked this pull request as ready for review May 15, 2026 02:22
@kcpevey kcpevey requested review from Adam-D-Lewis and aktech May 20, 2026 17:35
@aktech

aktech commented May 26, 2026

Copy link
Copy Markdown
Member

I am going to review this.

Comment thread internal/rbac/rbac.go Outdated
Comment thread internal/auth/group_sync.go
Comment thread internal/auth/group_sync.go
Comment thread internal/auth/group_sync.go
Comment thread internal/service/admin.go
Comment thread docs/superpowers/plans/2026-05-11-groups-rbac.md Outdated
tylerpotts and others added 3 commits May 29, 2026 15:14
- rbac: drop redundant enforcer.SavePolicy() from all group functions; the
  GORM adapter has AutoSave so per-row mutations already persist. Capture
  previously-ignored RemovePolicy errors in revoke paths.
- auth: audit-log OIDC group creation, membership add, and stale membership
  removal during SyncOIDCGroups (origin: oidc).
- service: GrantGroupAdmin rejects OIDC-synced groups with a ConflictError.
- docs: remove the groups-rbac plan file.
@aktech aktech merged commit dad6163 into nebari-dev:main Jun 1, 2026
20 of 21 checks passed
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.

Groups: share workspaces/registries with groups (native + OIDC) [User story] - Multi-user/team management use case

2 participants