Skip to content

Role Assignments

A role assignment connects a user to a role within a typed scope, with optional temporal validity. This is the record that determines what a user can do, where, and for how long.

The RoleAssignment model

RoleAssignment is a Beanie document stored in the role_assignments collection:

from craft_easy.models.role_assignment import RoleAssignment
from datetime import datetime, timezone, timedelta

assignment = RoleAssignment(
    user_id=user.id,
    role_name="operator",
    scope_type="tenant",
    scope_id=tenant.id,
    valid_from=datetime.now(timezone.utc),
    valid_until=datetime.now(timezone.utc) + timedelta(days=90),
    assigned_by=admin_user.id,
    reason="Temporary operator access for Q2 project",
)
await assignment.insert()

Fields

Field Type Description
user_id ObjectId (indexed) The user receiving the role
role_name str (indexed) Name of the role to assign
scope_type ScopeType Where the role applies: "system", "tenant", "partner", "parking_area", "org_node"
scope_id ObjectId (optional) ID of the scope boundary (None for system scope)
valid_from datetime When the assignment becomes active (defaults to now)
valid_until datetime (optional) When the assignment expires (None = no expiry)
assigned_by ObjectId (optional) User who created the assignment
reason str (optional) Why the assignment was made

Indexes

(user_id, 1, valid_until, 1)       — fast lookup of active assignments per user
(role_name, 1)                      — find all holders of a role
(scope_type, 1, scope_id, 1)       — find assignments within a scope

Scope types

The scope_type field determines the boundary within which the role applies:

Scope type Meaning Example
system Full platform access Platform administrators
tenant Access within one tenant Tenant operators
partner Access across a partner's tenants Reseller administrators
parking_area Access within one parking area Site-specific wardens
org_node Access within an org subtree Department managers

A user can have multiple assignments with different scopes. The Access Resolver merges them at request time.

System scope

System-scoped assignments have scope_id=None and grant access across the entire platform:

system_assignment = RoleAssignment(
    user_id=admin_user.id,
    role_name="admin",
    scope_type="system",
    scope_id=None,
)

Tenant scope

Tenant-scoped assignments restrict the role to a single tenant:

tenant_assignment = RoleAssignment(
    user_id=operator_user.id,
    role_name="operator",
    scope_type="tenant",
    scope_id=tenant.id,
)

Parking area scope

For fine-grained access, assign a role to a specific parking area:

area_assignment = RoleAssignment(
    user_id=warden_user.id,
    role_name="warden",
    scope_type="parking_area",
    scope_id=parking_area.id,
)

Temporal validity

Assignments support time-bounded access through valid_from and valid_until:

# 90-day temporary access
temp = RoleAssignment(
    user_id=contractor.id,
    role_name="operator",
    scope_type="tenant",
    scope_id=tenant.id,
    valid_from=datetime(2026, 4, 1, tzinfo=timezone.utc),
    valid_until=datetime(2026, 7, 1, tzinfo=timezone.utc),
    reason="Summer season contractor",
)
await temp.insert()

The is_active() method checks whether an assignment is currently valid:

assignment.is_active()                          # Check against current time
assignment.is_active(at=some_future_datetime)   # Check against specific time

An assignment is active when:

  • valid_from <= now (or valid_from is the default)
  • valid_until > now (or valid_until is None)

The Access Resolver automatically filters by temporal validity when computing effective access.

Revoking access

To revoke access immediately, set valid_until to now:

assignment.valid_until = datetime.now(timezone.utc)
await assignment.save()

Or delete the assignment entirely:

await assignment.delete()

Admin endpoints

Role assignments are managed via standard CRUD:

# List all role assignments
curl http://localhost:5001/role-assignments

# List assignments for a specific user
curl "http://localhost:5001/role-assignments?user_id={user_id}"

# Create a role assignment
curl -X POST http://localhost:5001/role-assignments \
  -H "Content-Type: application/json" \
  -d '{
    "user_id": "664f1a2b3c4d5e6f7a8b9c0d",
    "role_name": "operator",
    "scope_type": "tenant",
    "scope_id": "664f1a2b3c4d5e6f7a8b9c0e",
    "valid_until": "2026-07-01T00:00:00Z",
    "reason": "Summer season access"
  }'

# Update (ETag required)
curl -X PATCH http://localhost:5001/role-assignments/{id} \
  -H "Content-Type: application/json" \
  -H "If-Match: \"etag-value\"" \
  -d '{"valid_until": "2026-10-01T00:00:00Z"}'

# Delete (ETag required)
curl -X DELETE http://localhost:5001/role-assignments/{id} \
  -H "If-Match: \"etag-value\""

Multiple assignments

A user can hold multiple role assignments simultaneously. The resolver unions all capabilities from all active assignments:

# User is an operator in Tenant A and a viewer in Tenant B
await RoleAssignment(user_id=user.id, role_name="operator", scope_type="tenant", scope_id=tenant_a.id).insert()
await RoleAssignment(user_id=user.id, role_name="viewer", scope_type="tenant", scope_id=tenant_b.id).insert()

When this user makes a request in Tenant A, they get operator capabilities. In Tenant B, they get viewer capabilities. The Scope Filters system ensures they only see data within their assigned scopes.