Entity Memberships
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.
| Status | Grants permissions? |
|---|---|
active (and inside validity window) | Yes |
suspended / revoked | No |
expired / pending | Computed: outside window / not yet started — no |
rejected | Reserved 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)
| Method | Path | Permission | Notes |
|---|---|---|---|
GET | /me | Authenticated | Own memberships; include_inactive. Rows include entity_name, entity_display_name, entity_type and role_names |
POST | / | Tree membership:create + entity and user in tenant scope | Add 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:read | Members of one entity (paginated) |
GET | /entity/{entity_id}/details | Tree membership:read | Same + user/role summaries (bare list) |
GET | /entity/{entity_id}/members | Tree membership:read | Paginated details envelope with total; optional search (email / name) |
GET | /user/{user_id} | membership:read + target in tenant scope | User’s memberships (access reviews). Out-of-scope users → 404 |
PATCH | /{entity_id}/{user_id} | Tree membership:update | Replace 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:delete | Soft 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
}