Skip to content

Groups: share workspaces/registries with groups (native + OIDC) #293

@aktech

Description

@aktech

Summary

Add a first-class Group concept to nebi so workspaces, registries, and the admin role can be granted to a group of users instead of one user at a time.

Relevant issue #72

Image

Works in both deployment modes:

  • Native (no OIDC): admins create groups and manage members in the nebi UI.
  • OIDC: groups + members come from the IdP groups claim. Auto-created on first login, members synced JIT from each ID token. Read-only in nebi.

Scope

Groups can be granted permission on:

  1. Workspaces (viewer / editor) — same roles as user sharing today.
  2. Registries (read / write).
  3. The admin role (every member becomes an effective admin).

Effective permission for a user = union of direct grants + grants via any group they belong to (standard Casbin g semantics — no matcher changes needed).

Data model

New tables (GORM AutoMigrate):

  • groupsid (uuid), name (unique), description, source (native | oidc), timestamps, soft delete.
  • group_members — composite PK (group_id, user_id), created_at.
  • group_permissionsgroup_id, workspace_id, role_id (reuses existing roles table). Sibling of the existing per-user permissions table; not a discriminator on permissions (keeps existing queries untouched).

Registry-by-group and admin-by-group are expressed purely as Casbin policies (no extra table).

RBAC (Casbin)

internal/rbac/model.conf already has g = _, _ — wire it up, no matcher rewrite.

  • Membership: g(user_id, group_id)
  • Group on workspace: p(group_id, "ws:"+workspace_id, "read"|"write")
  • Group on registry: p(group_id, "reg:"+registry_id, "read"|"write")
  • Group as admin: p(group_id, "admin", "*")

Existing IsAdmin / CanReadWorkspace / CanWriteWorkspace resolve transitively for free.

DB write + Casbin write must happen in a single transaction (matches today's per-user pattern). On group delete, hard-remove all Casbin grouping + policies for that group id (Casbin doesn't honor GORM soft-delete).

API

# Admin group CRUD (native groups only — OIDC groups return 409 on edit)
GET    /api/v1/admin/groups
POST   /api/v1/admin/groups
GET    /api/v1/admin/groups/{id}
PATCH  /api/v1/admin/groups/{id}
DELETE /api/v1/admin/groups/{id}
POST   /api/v1/admin/groups/{id}/members
DELETE /api/v1/admin/groups/{id}/members/{user_id}

# Group as admin / registry grants
POST   /api/v1/admin/groups/{id}/grant-admin
DELETE /api/v1/admin/groups/{id}/grant-admin
POST   /api/v1/admin/registries/{id}/grant-group
DELETE /api/v1/admin/registries/{id}/grant-group/{group_id}

# Picker for non-admins
GET    /api/v1/groups/me     # only groups the caller belongs to

# Workspace sharing (parallel to existing user share endpoints)
POST   /api/v1/workspaces/{id}/share-group           {group_id, role}
DELETE /api/v1/workspaces/{id}/share-group/{group_id}

GET /workspaces/{id}/collaborators extended to return users[] and groups[] side by side, each with a source badge.

Authorization for share-group: caller must be admin OR (workspace owner AND member of group_id). This enforces the rule that owners can only share with groups they belong to.

OIDC sync

  • Add groups to the requested OIDC scopes in internal/auth/oidc.go.
  • On every login, read the groups claim from the ID token.
  • For each name in the claim: upsert a groups row with source=oidc; ensure group_members(user_id, group_id) exists; mirror to Casbin (g(user, group)).
  • For groups the user is currently a member of (source=oidc) but not in this login's claim: remove membership + Casbin grouping rule.
  • OIDC groups with zero members are kept (workspace shares stay valid through churn).

Stale-until-next-login is acceptable; no background poll, no per-IdP admin API.

Frontend

  • New admin page: Groups — list, create (native), edit, delete, manage members. OIDC groups shown read-only with a badge.
  • ShareDialog (frontend/src/components/sharing/ShareDialog.tsx): add a tabbed/segmented selector for User vs Group; group dropdown sourced from GET /groups/me.
  • Collaborators list renders groups inline with users, distinguished by an icon + native/oidc badge.
  • Admin user-management page: show which groups a user belongs to.

Out of scope (first cut)

  • Group-of-groups / nesting.
  • Per-member roles within a group.
  • Background reconcile worker for DB ↔ Casbin drift.
  • IdP admin-API polling.
  • Per-user direct-override semantics ("downgrade Alice even though group says editor").

Files most affected

  • internal/models/ — new group.go, group_member.go, group_permission.go.
  • internal/db/db.go — add models to AutoMigrate.
  • internal/rbac/rbac.go — new helpers for group grouping + group-scoped grants.
  • internal/service/workspace_permissions.go — extend collaborator listing; new ShareWorkspaceWithGroup / UnshareWorkspaceFromGroup.
  • internal/service/admin.go (or new groups.go) — group CRUD + member ops.
  • internal/auth/oidc.go — claim extraction + JIT sync.
  • internal/api/handlers/ — new group.go, additions to workspace.go, registry.go, admin.go.
  • internal/api/router.go — wire new routes.
  • frontend/src/pages/admin/Groups.tsx (new), frontend/src/components/sharing/ShareDialog.tsx, frontend/src/types/models.ts.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions