OutlabsAuth
Enterprise

Entity Memberships

Enterprise membership lifecycle.
EnterpriseRBAC membership lifecycle.

A membership connects one user, one entity, and zero or more roles. That is the primary way EnterpriseRBAC grants entity-scoped access. Users can belong to many entities; each membership has its own status and validity window.

from outlabs_auth.routers import get_memberships_router

app.include_router(get_memberships_router(auth, prefix="/v1/memberships"))

SimpleRBAC has no memberships — use direct roles on the users router instead.


Lifecycle

Stored fields include status, valid_from, valid_until, join/revoke metadata, and revocation_reason.

StatusGrants permissions?
active (and inside validity window)Yes
suspended / revokedNo
expired / pendingComputed: outside window / not yet started — no
rejectedReserved for future approval flows

status is what you store. effective_status applies the validity window at read/check time (e.g. stored active + valid_until yesterday → expired).

A membership grants permissions only when status == active and now is inside the validity window.

Limitation: timing is membership-wide — you cannot give different roles on the same membership different windows.


HTTP API (/v1/memberships)

MethodPathPermissionNotes
GET/meAuthenticatedOwn memberships; include_inactive. Rows include entity_name, entity_display_name, entity_type and role_names
POST/Tree membership:create + entity and user in tenant scopeAdd member. Body: user_id, entity_id, role_ids, optional status (active|suspended), validity, reason. Out-of-scope entity or user → 404; a visible user that does not belong to the actor's tenant (rooted elsewhere or unrooted, seen only through a membership) → 403, so adopting such an account needs a global actor; an entity outside the tree of a non-global actor's own root → 403
GET/entity/{entity_id}Tree membership:readMembers of one entity (paginated)
GET/entity/{entity_id}/detailsTree membership:readSame + user/role summaries (bare list)
GET/entity/{entity_id}/membersTree membership:readPaginated details envelope with total; optional search (email / name)
GET/user/{user_id}membership:read + target in tenant scopeUser’s memberships (access reviews). Out-of-scope users → 404
PATCH/{entity_id}/{user_id}Tree membership:updateReplace roles, suspend/reactivate, set/clear windows (null clears). Reactivating or widening the window re-runs role delegation checks. Re-granting or adding roles needs the entity in the tree of a non-global actor's own root (403), and is refused for a holder rooted in another tree (422 membership_root_mismatch, every actor)
DELETE/{entity_id}/{user_id}Tree membership:deleteSoft revoke → revoked; reactivate later with PATCH status=active (only while the holder is rooted in the entity's tree)

List/detail queries: page, limit, include_inactive (default false).

Tenant scope (DD-061): every route keyed by entity_id checks the caller's tenant scope before the permission check, so an entity in another tenant (or one that does not exist) answers 404; an in-scope entity still answers 403 without the permission.

Roles on create/update are validated against the entity context. Role pickers: GET /v1/roles/entity/{entity_id} — 25.

Convenience read on the entities router: GET /v1/entities/{id}/members — 51. History/orphans on users router — 23.


Response shape (typical)

{
  "id": "uuid",
  "entity_id": "uuid",
  "user_id": "uuid",
  "role_ids": ["uuid"],
  "status": "active",
  "effective_status": "expired",
  "joined_at": "2026-03-16T09:00:00Z",
  "valid_from": "2026-03-16T09:00:00Z",
  "valid_until": "2026-03-23T09:00:00Z",
  "is_currently_valid": false,
  "can_grant_permissions": false
}