Skip to content

Authorization and Root Entity Scope

Purpose

Root entity scope provides a reusable authorization boundary.

It answers:

text
Is this user allowed to operate on this entity or object?

The same concept can be used across:

  • entities,
  • assets,
  • properties,
  • tags,
  • groups,
  • credentials,
  • secret metadata,
  • remote sessions,
  • audit queries,
  • future PostgreSQL RLS.

Root Entity

A root entity is a top-level scope boundary.

Examples:

text
Customer A
Customer B
Internal Operations
Demo Tenant

A user's access is represented as one or more allowed root entities.

Core Rule

A user may operate on a target entity only when:

text
target.path <@ allowed_root.path

where allowed_root is one of the user's active root scopes.

Capabilities

Root scope answers where a user may act.

Capabilities answer what a user may do.

Examples:

text
entity.read
entity.create
entity.update
entity.delete
entity.move
property.read
property.update
tag.assign
group.manage
vault.secret.reveal
audit.read

An operation normally requires both:

text
inside allowed root scope
AND
has required capability

Table

Recommended table:

text
user_root_entity_scopes

Fields:

text
id
user_id
root_entity_id
capabilities jsonb
created_at
updated_at
deleted_at

Service Utility

Create a reusable authorization function early.

Example TypeScript shape:

ts
type AssertEntityInUserScopeInput = {
  userId: string;
  targetEntityId: string;
  capability: string;
};

async function assertEntityInUserScope(input: AssertEntityInUserScopeInput) {
  // 1. Load target entity path.
  // 2. Load user's allowed root paths and capabilities.
  // 3. Check at least one root path contains target path.
  // 4. Check required capability.
  // 5. Throw a typed authorization error if denied.
}

Query Pattern

sql
SELECT 1
FROM entities target
JOIN user_root_entity_scopes scope ON scope.user_id = $user_id
JOIN entities root ON root.id = scope.root_entity_id
WHERE target.id = $target_entity_id
  AND target.path <@ root.path
  AND target.deleted_at IS NULL
  AND root.deleted_at IS NULL
  AND scope.deleted_at IS NULL;

Capability checks can be performed in TypeScript or SQL depending on how capabilities are stored.

Multi-Root Users

A user may have access to multiple roots.

Examples:

  • national manager,
  • support engineer,
  • integration service account,
  • admin user.

Queries should support multiple allowed roots without loading the entire database.

Pattern:

sql
WHERE EXISTS (
  SELECT 1
  FROM user_root_entity_scopes scope
  JOIN entities root ON root.id = scope.root_entity_id
  WHERE scope.user_id = $user_id
    AND entities.path <@ root.path
    AND scope.deleted_at IS NULL
)

Reparenting and Root Scope

Moving an entity may change its root scope.

Default rule:

text
Disallow cross-root moves.

If cross-root moves are required, they must be explicit privileged operations because they can affect:

  • permissions,
  • encryption key scope,
  • audit visibility,
  • integration ownership,
  • external references,
  • reporting.

Authorization By Object Type

Some operations target records that are not directly entities rows.

Examples:

  • property value,
  • tag assignment,
  • group membership,
  • relationship,
  • secret version,
  • audit event.

These records should carry or resolve to an entity id.

Example:

text
entity_property_values.entity_id -> entities.id
entity_tags.entity_id -> entities.id
entity_group_members.group_entity_id -> entities.id
entity_group_members.member_entity_id -> entities.id

Authorization should resolve the relevant entity and apply root scope.

Future RLS

The same model can support PostgreSQL RLS.

Conceptual policy:

sql
USING (
  EXISTS (
    SELECT 1
    FROM app_session_allowed_roots ar
    WHERE entities.path <@ ar.root_path
  )
)

Do not rush RLS before service authorization is clear. Add it later as defense in depth.

Common Mistakes

Mistake: Checking only entity type

Bad:

text
User can edit assets, therefore allow edit.

Correct:

text
User can edit assets AND asset is inside allowed root scope.

Mistake: Checking only parent

A deeply nested asset may be many levels below the root. Use ltree path containment.

Mistake: Forgetting secondary entities

Relationships and group memberships often involve two entities. Check both where required.

Mistake: Mixing tenant and role

Tenant/root scope is not a role. A user may have the same role in one root and no access to another.