OutlabsAuth
Enterprise

Entities

Enterprise org tree CRUD, move, children, path.
Enterprise entity hierarchy HTTP API.

How to mount and call the entity tree when enable_entity_hierarchy is on (EnterpriseRBAC).


When this API exists

PresetMount get_entities_router?
SimpleRBACNo — hierarchy is forced off
EnterpriseRBACYes — typical prefix /v1/entities
from outlabs_auth.routers import get_entities_router, get_config_router

app.include_router(get_entities_router(auth, prefix="/v1/entities"))
app.include_router(get_config_router(auth, prefix="/v1/config"))  # root type vocabulary

Runnable: examples/enterprise_rbac.
Catalog: Routers & Prefixes.


Concepts you need

An entity is a node in an org tree:

  • structural — org chart (company → office → team, …)
  • access_group — cross-cutting workspace / group

entity_type is a free-form string (organization, office, team, …). Parent/child rules live on the parent (allowed_child_classes / allowed_child_types). Root types come from system config (below).

Closure table (one idea): OutlabsAuth stores every ancestor→descendant pair (with depth) so “is this under that org?” and “list the whole subtree” stay cheap. Prefer /children, /descendants, /path, and *_tree permissions over querying closure yourself. Deeper write-up: Core Authorization Concepts.

Users get access via memberships on entities, not by appearing on the entity row alone — Entity Memberships.

An entity's status (inactive) and validity window do not suspend the access its members hold through their memberships — membership status and windows do that. They block entity-anchored API keys and integration principals (service accounts). Archiving an entity (DELETE) is different: it revokes the entity's memberships, anchored keys and principals.


Permissions (short)

StyleWhen
require_permission("entity:…")Global check (no entity context)
Tree check (entity:create_tree, …)Target entity present — subtree auth
Entity-scoped checkPermission in a specific entity context

Create under a parent uses tree create on that parent; root create falls back to a global create check.

Tenant scope (DD-061). With enforce_user_scope=True (default) every entity route applies the same tenant scope as the user routes: non-global actors only list and open entities inside their resolved scope (their root tree plus membership subtrees; archived entities stay visible to their own tenant), and out-of-scope entities answer 404 — the scope check runs before the permission check, so this holds on tree-permission routes too. Changes that create, remove or reshape a tenant — creating a root, moving an entity to the root level or under another root, archiving a root — require a global actor (superuser or active system-wide role holder) and answer 403 otherwise. Moving to the root level must also satisfy the configured root entity types.

Root-changing moves fail closed (DD-061 decision 17). A move that changes the subtree's root — under another root, to the root level, or a root under any parent — is refused for every caller, superusers included, while the subtree still carries access: 422 with error code ENTITY_MOVE_CARRIES_ACCESS (details.reason = cross_root_move_carries_access) and details.access counting each category:

CategoryCounted while
membershipsan entity membership in the subtree is not revoked (active, suspended, pending, expired)
pending_invitationsan invited, not yet accepted account has such a membership or is rooted in the subtree
role_assignmentsa role anchored in the subtree (entity-local, or the org roles of a root inside it) is held through a non-revoked direct grant, a membership elsewhere or a non-archived integration principal
api_keysan API key anchored at a subtree entity is not revoked
integration_principalsan integration principal anchored at a subtree entity is not archived
accountsan account (any status) is rooted in the subtree — only when a root is demoted

await auth.entity_service.get_subtree_access(session, entity_id) returns the same counts. To reorganize across tenants: revoke or archive the subtree's access (DELETE /memberships/{entity_id}/{user_id}, revoke direct grants and keys, archive principals), move it, then re-grant in the destination with the destination's own accounts and roles. Accounts never change tenant: a root that holds accounts cannot be demoted — move its children instead. An allowed move re-anchors the subtree's entity-local role definitions at the new root and is audited (entity.moved with changes_root, previous_root_entity_id and reanchored_role_ids). Moves within one root are unchanged.

Revoked memberships stay behind as history and cannot be re-granted across the boundary: reactivating, widening or adding roles to a membership whose holder is rooted in another tree answers 422 (membership_root_mismatch) for every actor.

Subtrees moved on earlier releases. Moves before 0.1.0a35 did not re-root members, so such a subtree can still hold memberships of another tenant's accounts. An account belongs to the tenant whose tree holds its root (DD-061 decision 16): the member's root tenant keeps managing it, while the destination tenant sees it only through its membership and gets 403 on every account change (password, email, status, roles, sessions, keys) and on adding it elsewhere; it can still suspend or revoke the membership. The member cannot grant access in the destination tenant either — inviting into, adding members to, or re-granting memberships in an entity outside its own root's tree answers 403, and so do creating an account rooted there (POST /users) and writing an entity-local role defined there. Revoke such memberships (see the 0.1.0a35 upgrade notes for the audit queries).


HTTP API

Paths relative to /v1/entities.

List / CRUD

MethodPathPermissionNotes
GET/entity:readPaginated active entities. Query: search, entity_class, entity_type, parent_id, root_only, page, limit
POST/Tree entity:create on parent_entity_id (root create: global actor)EntityCreateRequest → EntityResponse (201)
GET/{entity_id}entity:readEntityResponse
PATCH/{entity_id}entity:updatePartial EntityUpdateRequest (identity / parent not renamed here)
DELETE/{entity_id}entity:deleteQuery cascade (default false) → 204. Archiving a root needs a global actor

Tree navigation

MethodPathPermissionNotes
GET/{entity_id}/childrenentity:readDirect active children
GET/{entity_id}/descendantsTree entity:readSubtree; optional entity_type filter
GET/{entity_id}/pathentity:readBreadcrumb root → entity
POST/{entity_id}/moveEntity entity:update; tree create on new parent if setEntityMoveRequest (new_parent_id optional → promote to root: global actor and an allowed root type; under another root: global actor). A root-changing move of a subtree that carries access answers 422 ENTITY_MOVE_CARRIES_ACCESS. Rewrites closure

Create UX helper

MethodPathPermissionNotes
GET/type-suggestionsTree entity:create on parent_idSibling-based entity_type autocomplete. Query: parent_id, entity_class

Members bridge (read-only)

MethodPathPermissionNotes
GET/{entity_id}/membersTree membership:readPaginated convenience list. Full lifecycle: /v1/memberships → 54

Entity-type config API

Mounted separately under /v1/config (get_config_router):

MethodPathAuthPurpose
GET/v1/config/entity-typesPublicAllowed root types + child-type hints
PUT/v1/config/entity-typesSuperuserUpdate vocabulary

Empty allowed_root_types for a class rejects root creates of that class. Per-parent child rules still use the entity’s allowed_child_* fields.


Response cheatsheet

EntityResponse: id, name, display_name, slug, description, entity_class, entity_type, parent_entity_id, status, validity window, allowed_child_classes / allowed_child_types, optional naming governance, created_at / updated_at when loaded.

Create, update (field-level diff), move and archive emit retained audit events (category=entity) — Sessions & Audit.

MemberResponse: user_id, email/names, role_ids, role_names.


Typical host flow

  1. Choose Enterprise — 07
  2. Mount entities + config routers — 02
  3. Set root types via PUT /v1/config/entity-types (or accept defaults)
  4. Create roots → children; use /type-suggestions in admin UI
  5. Attach users with memberships — 54
  6. Protect host routes with entity / tree permission deps — 13