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(orvalid_fromis the default)valid_until > now(orvalid_untilis None)
The Access Resolver automatically filters by temporal validity when computing effective access.
Revoking access¶
To revoke access immediately, set valid_until to now:
Or delete the assignment entirely:
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.