Permissions Domain (permissions_v1)
The Permissions domain (permissions_v1) is a bounded context responsible for managing fine-grained access control and authorization across the CO2 target asset management system. It implements role-based access control (RBAC) with hierarchical permissions spanning multiple resource types—from estates down to individual features and catalogues. The domain enforces business rules around permission grants, role hierarchies, and multi-admin limitations.
Overview
Section titled “Overview”The Permissions domain implements a comprehensive authorization model where:
- Users receive permissions to access Resources (estates, sites, layers, features, catalogues)
- Roles define what actions a user can perform on each resource type
- Grants are immutable once created; modifications require revocation + re-granting
- Business rules enforce constraints like maximum estate admin roles per user and preventing duplicate permissions
- Audit trails capture who granted/revoked permissions and when through domain events
This is not a session or authentication domain—it focuses purely on authorization: determining what an authenticated user is allowed to do.
Permission Model
Section titled “Permission Model”Resource Types
Section titled “Resource Types”Permissions are granted on five resource types, forming a hierarchy:
| Resource Type | Purpose | Typical Hierarchy |
|---|---|---|
| estate | A workspace/organization | Top-level container |
| site | Location within an estate | Owned by one estate |
| layer | Floor plan or map layer | Owned by one site |
| feature | Individual element on a layer | Owned by one layer |
| catalogue | Asset type catalog | Cross-cutting, estate-scoped |
Roles and Hierarchies
Section titled “Roles and Hierarchies”Each resource type has a specific set of roles, organized hierarchically. Roles at higher levels grant greater permission.
Estate Roles
Section titled “Estate Roles”// Hierarchy: read < write < admin < ownerRole.estateRead // Read-only access to estateRole.estateWrite // Can create and modify assetsRole.estateAdmin // Can grant/revoke permissions, manage usersRole.estateOwner // Full control (rarely used)Site Roles
Section titled “Site Roles”// Hierarchy: read < write < adminRole.siteRead // Read-only access to siteRole.siteWrite // Can create and modify assets within siteRole.siteAdmin // Can grant/revoke site permissionsLayer & Feature Roles
Section titled “Layer & Feature Roles”// Hierarchy: none < read < write < adminRole.layerNone // No accessRole.layerRead // Can view layer/featureRole.layerWrite // Can modify layer/featureRole.layerAdmin // Can grant layer/feature permissions
Role.featureNoneRole.featureReadRole.featureWriteRole.featureAdminCatalogue Roles
Section titled “Catalogue Roles”// Hierarchy: read < write < adminRole.catalogueRead // Can browse catalogueRole.catalogueWrite // Can add/modify catalogue itemsRole.catalogueAdmin // Can manage catalogue accessPermission Status
Section titled “Permission Status”A permission has a lifecycle with three possible states:
class PermissionStatus { static const active = PermissionStatus._('active'); // Currently effective static const revoked = PermissionStatus._('revoked'); // Explicitly disabled static const suspended = PermissionStatus._('suspended'); // Temporarily disabled}Role Hierarchy and Permission Checks
Section titled “Role Hierarchy and Permission Checks”The Role class provides methods for comparing roles within the same type:
// Role comparisonrole.hasMinimumPermission(minimumRole) // true if role >= minimumRole
// Example:if (userRole.hasMinimumPermission(Role.siteAdmin)) { // User can perform admin actions}
// Permission grantingrole.canGrant(targetRole) // true if role can grant targetRole to othersKey Aggregates
Section titled “Key Aggregates”UserPermissionAggregate
Section titled “UserPermissionAggregate”The root aggregate managing all permissions for a specific user.
Identity:
AggregateId: System-generated ID (format:perm-{userId})userId: UserId: The user whose permissions are tracked
Core Attributes:
class UserPermissionAggregate extends Aggregate<UserPermissionAggregate> { final UserId userId; // User identity final Map<String, UserPermission> _permissions; // permissionId -> permission final DateTime createdAt; // Aggregate creation time final DateTime updatedAt; // Last modification time final DateTime? lastViewedAt; // Optional access timestamp}UserPermission Value Object:
Each permission is a value object containing:
class UserPermission { final PermissionId permissionId; // Unique permission identifier final UserId userId; // User receiving the permission final ResourceType resourceType; // Type of resource (estate, site, etc.) final ResourceId resourceId; // ID of the specific resource final Role role; // Role for this resource final UserId grantedBy; // Admin who granted it final DateTime grantedAt; // Grant timestamp final PermissionStatus status; // active, revoked, or suspended
// Revocation tracking final UserId? revokedBy; // Admin who revoked it final DateTime? revokedAt; // Revocation timestamp final PermissionReasonDescription? revocationReason;
// Last modification final UserId? lastModifiedBy; // Admin who last changed role final DateTime? lastModifiedAt; // Modification timestamp
// Metadata for UI display final DateTime? lastViewed; // When user last accessed resource final int? assetCount; // Number of assets (for estates) final DomainText? displayName; // Display name (e.g., estate name)}Key Methods:
// Query methodsList<UserPermission> get activePermissionsbool hasPermissionFor(ResourceType type, ResourceId id, [Role? minimumRole])UserPermission? getPermissionFor(ResourceType type, ResourceId id)int countActivePermissions(ResourceType type)
// Specialized accessors (convenience methods)bool hasEstateAccess(EstateId id, [Role? minimumRole])bool hasSiteAccess(EstateId estateId, SiteId siteId, [Role? minimumRole])bool hasLayerAccess(SiteId siteId, LayerId layerId, [Role? minimumRole])bool hasFeatureAccess(SiteId siteId, ResourceId featureId, [Role? minimumRole])bool hasCatalogueAccess(CatalogueId catalogueId, [Role? minimumRole])
// Access mappingMap<SiteId, Role> siteAccessMap(EstateId estateId)
// Validationbool canGrantPermission(ResourceType type, ResourceId id, Role role)Business Rules
Section titled “Business Rules”The aggregate enforces critical invariants via validate():
- User ID Required: userId cannot be empty or unresolved
- Estate Admin Limit: User cannot have more than 3 active estate admin roles
- No Duplicate Permissions: User cannot have multiple active permissions for the same resource
- DisplayName Not Enforced at Runtime: While required for estate permissions in production, the aggregate doesn’t throw to avoid breaking legacy permissions lacking displayName
These rules are checked when applying events and before persisting aggregates.
Domain Events
Section titled “Domain Events”The permissions domain emits four event types to capture all state changes:
PermissionGrantedEvent
Section titled “PermissionGrantedEvent”Fired when a user is granted access to a resource.
class PermissionGrantedEvent implements Event { final PermissionId permissionId; final UserId userId; final ResourceType resourceType; final ResourceId resourceId; final Role role; final UserId grantedBy; // Admin who performed the grant final DateTime grantedAt; final DomainText? displayName; // Optional display name (e.g., estate name)}Generated by: GrantPermissionDirective Example:
PermissionGrantedEvent( permissionId: PermissionId('perm-user-123-estate-e1'), userId: UserId('user-123'), resourceType: ResourceType.estate, resourceId: ResourceId('estate-e1', ResourceType.estate), role: Role.estateAdmin, grantedBy: UserId('admin-456'), grantedAt: DateTime.now(), displayName: DomainText('Production Estate'),)PermissionRevokedEvent
Section titled “PermissionRevokedEvent”Fired when a user’s permission is revoked.
class PermissionRevokedEvent implements Event { final PermissionId permissionId; final UserId userId; final ResourceType resourceType; final ResourceId resourceId; final Role previousRole; // The role that was revoked final UserId revokedBy; // Admin who performed the revocation final PermissionReasonDescription? reason; final DateTime revokedAt;}Generated by: RevokePermissionDirective
PermissionRoleChangedEvent
Section titled “PermissionRoleChangedEvent”Fired when a user’s role for a resource is modified.
class PermissionRoleChangedEvent implements Event { final PermissionId permissionId; final UserId userId; final ResourceType resourceType; final ResourceId resourceId; final Role previousRole; final Role newRole; final UserId changedBy; // Admin who made the change final PermissionReasonDescription? reason; final DateTime changedAt;}Generated by: ChangePermissionRoleDirective
PermissionMetadataUpdatedEvent
Section titled “PermissionMetadataUpdatedEvent”Fired when metadata (lastViewed, assetCount, displayName) is updated. Used for UI-driven updates that don’t affect core permissions.
class PermissionMetadataUpdatedEvent implements Event { final PermissionId permissionId; final UserId userId; final ResourceType resourceType; final ResourceId resourceId; final DateTime? lastViewed; // When user viewed this resource final int? assetCount; // Number of assets in resource final DomainText? displayName; // Updated display name final DateTime updatedAt;}Generated by: UpdatePermissionMetadataDirective
Directives (Command Handlers)
Section titled “Directives (Command Handlers)”Directives are command handlers that validate business rules and emit events. All directives target the UserPermissionAggregate.
GrantPermissionDirective
Section titled “GrantPermissionDirective”Grants a new permission to a user.
Input Payload:
class GrantPermissionPayload { final PermissionId permissionId; final UserId userId; final ResourceType resourceType; final ResourceId resourceId; final Role role; final UserId grantedBy; // Which admin is granting final DomainText? displayName; // Optional (e.g., estate name)}Business Rules:
- Cannot grant duplicate: user must not already have an active permission for this resource
- If a permission with same role exists (idempotent), no event is emitted
- If a permission with different role exists, throws error (use ChangePermissionRole instead)
- PermissionId is generated deterministically:
perm-{userId}-{resourceType}-{resourceId}
Example:
final directive = GrantPermissionDirective( payload: GrantPermissionPayload( permissionId: PermissionId('perm-u1-estate-e1'), userId: UserId('user-1'), resourceType: ResourceType.estate, resourceId: ResourceId('estate-1', ResourceType.estate), role: Role.estateAdmin, grantedBy: UserId('admin-user'), displayName: DomainText('My Estate'), ),);
final result = await engine.execute<UserPermissionAggregate, GrantPermissionPayload>( directive: directive, timelineId: timelineId, workspaceId: workspace,);
// result.updatedAggregate now has the new permission// result.events contains PermissionGrantedEventRevokePermissionDirective
Section titled “RevokePermissionDirective”Revokes an existing permission.
Input Payload:
class RevokePermissionPayload { final PermissionId permissionId; final UserId userId; final ResourceType resourceType; final ResourceId resourceId; final Role previousRole; // Expected previous role (for validation) final UserId revokedBy; // Which admin is revoking final PermissionReasonDescription? reason;}Business Rules:
- Permission must exist and be active
- Cannot revoke already-revoked or suspended permissions
- Looks up the actual permission by resource (permissionId may be stale)
Example:
final directive = RevokePermissionDirective( payload: RevokePermissionPayload( permissionId: PermissionId('perm-u1-estate-e1'), userId: UserId('user-1'), resourceType: ResourceType.estate, resourceId: ResourceId('estate-1', ResourceType.estate), previousRole: Role.estateAdmin, revokedBy: UserId('admin-user'), reason: PermissionReasonDescription('User left project'), ),);ChangePermissionRoleDirective
Section titled “ChangePermissionRoleDirective”Changes a user’s role for an existing resource (e.g., estate admin → estate read).
Input Payload:
class ChangePermissionRolePayload { final PermissionId permissionId; final UserId userId; final ResourceType resourceType; final ResourceId resourceId; final Role previousRole; final Role newRole; final UserId changedBy; // Which admin is making the change final PermissionReasonDescription? reason;}Business Rules:
- Permission must exist
- Role must be different from current
- ChangedBy user must have permission to grant roles (via canGrant)
Example:
final directive = ChangePermissionRoleDirective( payload: ChangePermissionRolePayload( permissionId: PermissionId('perm-u1-estate-e1'), userId: UserId('user-1'), resourceType: ResourceType.estate, resourceId: ResourceId('estate-1', ResourceType.estate), previousRole: Role.estateAdmin, newRole: Role.estateRead, changedBy: UserId('admin-user'), ),);UpdatePermissionMetadataDirective
Section titled “UpdatePermissionMetadataDirective”Updates metadata without modifying the core permission.
Input Payload:
class UpdatePermissionMetadataPayload { final UserId userId; final ResourceType resourceType; final ResourceId resourceId; final DateTime? lastViewed; // When user last accessed resource final int? assetCount; // Number of assets final DomainText? displayName; // Display name update}Business Rules:
- If no permission exists for this resource, no event is emitted (no-op)
- Only updates existing permissions; doesn’t create new ones
- Used for UI-driven metadata updates (e.g., marking estate as viewed)
Example:
final directive = UpdatePermissionMetadataDirective( payload: UpdatePermissionMetadataPayload( userId: UserId('user-1'), resourceType: ResourceType.estate, resourceId: ResourceId('estate-1', ResourceType.estate), lastViewed: DateTime.now(), assetCount: 42, ),);Permission Checking Across the System
Section titled “Permission Checking Across the System”Within Aggregates (Nomos Selectors)
Section titled “Within Aggregates (Nomos Selectors)”The aggregate provides specialized accessor methods for checking permissions by resource:
final permissionAgg = userPermissions;
// Estate accessif (permissionAgg.hasEstateAccess(estateId, Role.estateAdmin)) { // User can administer this estate}
// Site accessif (permissionAgg.hasSiteAccess(estateId, siteId, Role.siteWrite)) { // User can write to this site}
// Layer accessif (permissionAgg.hasLayerAccess(siteId, layerId)) { // User has some level of access to this layer}
// Feature accessif (permissionAgg.hasFeatureAccess(siteId, featureId, Role.featureAdmin)) { // User can administer this feature}
// Catalogue accessif (permissionAgg.hasCatalogueAccess(catalogueId)) { // User can access this catalogue}
// Get all sites user has access tofinal siteRoles = permissionAgg.siteAccessMap(estateId);siteRoles.forEach((siteId, role) { print('$siteId: $role');});General Permission Query
Section titled “General Permission Query”final perm = permissionAgg.getPermissionFor(ResourceType.estate, resourceId);if (perm != null && perm.isActive) { print('Access granted as: ${perm.role}');}Permission Counts
Section titled “Permission Counts”// Count active permissions by typefinal siteCount = permissionAgg.countActivePermissions(ResourceType.site);print('User has access to $siteCount sites');
// Get all active permissionsfinal active = permissionAgg.activePermissions;Pre-Grant Validation
Section titled “Pre-Grant Validation”Before granting new permissions, check if it’s allowed:
if (permissionAgg.canGrantPermission(ResourceType.estate, resourceId, Role.estateAdmin)) { // Safe to grant; user hasn't hit admin limit} else { throw StateError('Cannot grant: maximum estate admins reached or duplicate permission');}Aggregate ID Format
Section titled “Aggregate ID Format”The UserPermissionAggregate uses a deterministic, prefixed ID to avoid collisions with other aggregate types:
// ID format: perm-{userId}final aggregateId = UserPermissionAggregateId.forUser(UserId('user-123')).value;// Result: "perm-user-123"
// Permissions are stored by permissionId within the aggregatefinal permissionId = 'perm-user-123-estate-e1';This ensures:
- One aggregate per user (all their permissions in one place)
- No collision with UserAggregate IDs
- Deterministic lookup by user
Role Hierarchy and Permission Inheritance
Section titled “Role Hierarchy and Permission Inheritance”Hierarchy Rules
Section titled “Hierarchy Rules”The role hierarchy defines permission inheritance. A user with a higher role implicitly has permissions of all lower roles:
// If user has estateAdmin, they can also act as estateWrite and estateReadif (role.hasMinimumPermission(Role.estateRead)) { // Can read}if (role.hasMinimumPermission(Role.estateWrite)) { // Can write (implies read)}if (role.hasMinimumPermission(Role.estateAdmin)) { // Can admin (implies write and read)}Role Granting Authority
Section titled “Role Granting Authority”Only users with admin or owner roles can grant permissions. They can only grant roles at or below their own level:
// Can this user grant a role?if (myRole.canGrant(targetRole)) { // Safe to grant}
// Example:final adminRole = Role.estateAdmin;final writeRole = Role.estateWrite;final readRole = Role.estateRead;
// Admin can grant write and read but not ownerprint(adminRole.canGrant(writeRole)); // trueprint(adminRole.canGrant(readRole)); // trueprint(adminRole.canGrant(Role.estateOwner)); // falseUsage Examples
Section titled “Usage Examples”Granting Estate Admin Access
Section titled “Granting Estate Admin Access”import 'package:permissions_v1/permissions_v1.dart';import 'package:contracts_v1/contracts_v1.dart';import 'package:nomos_core/nomos_core.dart';
// Initialize the domainregisterPermissionsV1();
// Create and execute a grant directivefinal directive = GrantPermissionDirective( payload: GrantPermissionPayload( permissionId: PermissionId('perm-alice-estate-prod'), userId: UserId('alice-123'), resourceType: ResourceType.estate, resourceId: ResourceId('estate-prod', ResourceType.estate), role: Role.estateAdmin, grantedBy: UserId('admin-system'), displayName: DomainText('Production Estate'), ),);
final result = await engine.execute<UserPermissionAggregate, GrantPermissionPayload>( directive: directive, timelineId: const NomosTimelineId('default'), workspaceId: EstateWorkspace(EstateId('ws-prod')).toNomos,);
// Check resultfinal updatedAgg = result.updatedAggregate;assert(updatedAgg.hasEstateAccess(EstateId('estate-prod'), Role.estateAdmin));
// Events generatedfor (final event in result.events) { if (event is PermissionGrantedEvent) { print('Permission granted: ${event.permissionId}'); }}Promoting a User’s Role
Section titled “Promoting a User’s Role”// First, get current permissionfinal currentPerm = userPermissions.getPermissionFor( ResourceType.estate, ResourceId('estate-prod', ResourceType.estate),);
// Change their role from write to adminfinal directive = ChangePermissionRoleDirective( payload: ChangePermissionRolePayload( permissionId: currentPerm!.permissionId, userId: currentPerm.userId, resourceType: ResourceType.estate, resourceId: ResourceId('estate-prod', ResourceType.estate), previousRole: Role.estateWrite, newRole: Role.estateAdmin, changedBy: UserId('admin-system'), reason: PermissionReasonDescription('Promotion to admin'), ),);
final result = await engine.execute<UserPermissionAggregate, ChangePermissionRolePayload>( directive: directive, timelineId: timelineId, workspaceId: workspace,);
// Updated aggregate now reflects new rolefinal updatedPerm = result.updatedAggregate.getPermissionFor( ResourceType.estate, ResourceId('estate-prod', ResourceType.estate),);assert(updatedPerm!.role == Role.estateAdmin);Revoking Access and Auditing
Section titled “Revoking Access and Auditing”final directive = RevokePermissionDirective( payload: RevokePermissionPayload( permissionId: PermissionId('perm-bob-estate-old'), userId: UserId('bob-456'), resourceType: ResourceType.estate, resourceId: ResourceId('estate-old', ResourceType.estate), previousRole: Role.estateRead, revokedBy: UserId('admin-system'), reason: PermissionReasonDescription('User transferred to different team'), ),);
final result = await engine.execute<UserPermissionAggregate, RevokePermissionPayload>( directive: directive, timelineId: timelineId, workspaceId: workspace,);
// Check resultfinal revokedPerm = result.updatedAggregate.getPermissionFor( ResourceType.estate, ResourceId('estate-old', ResourceType.estate),);assert(revokedPerm == null); // No longer in active permissions
// Audit trail: check event for who, when, whyfor (final event in result.events) { if (event is PermissionRevokedEvent) { print('Revoked by: ${event.revokedBy}'); print('Reason: ${event.reason}'); print('At: ${event.revokedAt}'); }}Checking Multi-Level Access
Section titled “Checking Multi-Level Access”final agg = userPermissions;
// Check cascading permissionsbool canEditSite(EstateId estateId, SiteId siteId) { // Need at least site write on the site return agg.hasSiteAccess(estateId, siteId, Role.siteWrite);}
bool canViewAllSitesInEstate(EstateId estateId) { // Need at least estate read return agg.hasEstateAccess(estateId, Role.estateRead);}
// Get subset of accessible sitesfinal accessibleSites = agg.siteAccessMap(estateId) .entries .where((e) => e.value.hasMinimumPermission(Role.siteAdmin)) .map((e) => e.key) .toList();Serialization and JSON
Section titled “Serialization and JSON”Aggregate Serialization
Section titled “Aggregate Serialization”The aggregate and its contained permissions serialize to JSON for persistence:
final json = aggregate.toJson();// {// "id": "perm-user-123",// "userId": "user-123",// "permissions": {// "perm-user-123-estate-e1": {// "permissionId": "perm-user-123-estate-e1",// "userId": "user-123",// "resourceType": "estate",// "resourceId": "estate-e1",// "role": { "value": "admin", "type": "estate" },// "grantedBy": "admin-456",// "grantedAt": "2024-01-15T10:30:00Z",// "status": "active",// "displayName": "Production Estate"// }// },// "createdAt": "2024-01-01T00:00:00Z",// "updatedAt": "2024-01-15T10:30:00Z"// }
// Deserializefinal restored = UserPermissionAggregate.fromJson(json, AggregateId('perm-user-123'));Defensive Deserialization
Section titled “Defensive Deserialization”The aggregate includes robust deserialization that handles:
- Missing or malformed permission entries (marked as revoked, skipped)
- Flexible date parsing (ISO strings, timestamps, epoch seconds/milliseconds)
- Default fallbacks for missing role data
This allows graceful migration of legacy permissions without breaking.
Domain Registration
Section titled “Domain Registration”Permissioning types must be registered with Nomos before use:
import 'package:permissions_v1/permissions_v1.dart';
void main() { // Register all permissions domain types registerPermissionsV1();
// Or use the DomainModule pattern final module = PermissionsV1(); module.registerDomainTypes();}This registers:
- Aggregate factories and type information
- All four event types and their JSON deserializers
- All four directive types and payload deserializers
Testing
Section titled “Testing”The domain includes comprehensive tests covering:
Unit Tests (permission_aggregate_test.dart):
- Business rule enforcement (estate admin limit, duplicate prevention)
- Permission queries and selectors
- Role hierarchy and granting logic
Integration Tests (permissions_v1_test.dart):
- Grant → Revoke workflows
- Role changes
- Metadata updates
- Multi-user scenarios
Example Test:
test('Business Rules: Estate Admin Limit', () { final userId = UserId('USR-1'); var aggregate = UserPermissionAggregate( userId: userId, createdAt: DateTime.now(), updatedAt: DateTime.now(), );
// Grant 3 estate admin roles (allowed) for (var i = 1; i <= 3; i++) { aggregate = aggregate.apply(PermissionGrantedEvent( permissionId: PermissionId('P-$i'), userId: userId, resourceType: ResourceType.estate, resourceId: ResourceId('E-$i', ResourceType.estate), role: Role.estateAdmin, grantedBy: UserId('ADMIN'), grantedAt: DateTime.now(), )); } expect(() => aggregate.validate(), returnsNormally);
// Grant 4th admin role (not allowed) aggregate = aggregate.apply(PermissionGrantedEvent( permissionId: PermissionId('P-4'), userId: userId, resourceType: ResourceType.estate, resourceId: ResourceId('E-4', ResourceType.estate), role: Role.estateAdmin, grantedBy: UserId('ADMIN'), grantedAt: DateTime.now(), )); expect(() => aggregate.validate(), throwsStateError);});Dependencies
Section titled “Dependencies”The permissions domain depends on:
- nomos_core: Event sourcing framework (aggregates, events, directives)
- contracts_v1: Shared value objects (UserId, Role, ResourceType, PermissionId, etc.)
It has no dependencies on other domains, enabling it to be used across the system without circular dependency risk.
Package Location
Section titled “Package Location”dart_packages/co2/domains/permissions_v1/├── lib/│ ├── permissions_v1.dart # Main export file│ ├── src/│ │ ├── aggregates/│ │ │ └── user_permission_aggregate.dart # Core aggregate and value objects│ │ ├── events/│ │ │ └── permission_events.dart # Four event types│ │ └── directives/│ │ └── permission_directives.dart # Four directive types│ └── [exports]├── test/│ ├── permissions_v1_test.dart # Integration tests│ ├── permission_aggregate_test.dart # Unit tests│ └── coverage_booster_test.dart├── pubspec.yaml└── README.mdKey Concepts Summary
Section titled “Key Concepts Summary”| Concept | Purpose | Example |
|---|---|---|
| UserPermissionAggregate | Root aggregate tracking one user’s permissions | Aggregate for alice-123 contains 5 permissions |
| UserPermission | Individual permission value object | Permission to admin estate-prod |
| PermissionId | Deterministic ID for a permission | perm-alice-123-estate-prod |
| Role | Permission level for a resource type | Role.estateAdmin |
| ResourceType | Type of resource being controlled | ResourceType.estate |
| Event | Immutable record of state change | PermissionGrantedEvent, PermissionRevokedEvent |
| Directive | Command handler enforcing business rules | GrantPermissionDirective |
| PermissionStatus | Current state of a permission | active, revoked, suspended |
Common Patterns
Section titled “Common Patterns”Pattern 1: Bulk Permission Grants
Section titled “Pattern 1: Bulk Permission Grants”To grant multiple permissions to a user across resources:
final userId = UserId('new-user-123');final grantedBy = UserId('admin');
final estateId = EstateId('estate-1');final siteIds = [SiteId('site-1'), SiteId('site-2')];
// Grant estate readawait engine.execute<UserPermissionAggregate, GrantPermissionPayload>( directive: GrantPermissionDirective( payload: GrantPermissionPayload( userId: userId, resourceType: ResourceType.estate, resourceId: ResourceId(estateId.value, ResourceType.estate), role: Role.estateRead, grantedBy: grantedBy, ), ), ...);
// Grant site write for multiple sitesfor (final siteId in siteIds) { await engine.execute<UserPermissionAggregate, GrantPermissionPayload>( directive: GrantPermissionDirective( payload: GrantPermissionPayload( userId: userId, resourceType: ResourceType.site, resourceId: ResourceId(siteId.value, ResourceType.site), role: Role.siteWrite, grantedBy: grantedBy, ), ), ... );}Pattern 2: Permission Audit Trail
Section titled “Pattern 2: Permission Audit Trail”Trace who granted/revoked each permission:
final agg = userPermissions;
for (final perm in agg.activePermissions) { print('${perm.resourceType}:${perm.resourceId} - ${perm.role}'); print(' Granted by: ${perm.grantedBy} at ${perm.grantedAt}'); if (perm.lastModifiedBy != null) { print(' Last modified by: ${perm.lastModifiedBy} at ${perm.lastModifiedAt}'); }}Pattern 3: Role Downgrade with Audit
Section titled “Pattern 3: Role Downgrade with Audit”Safely downgrade a user’s role while recording the reason:
final perm = agg.getPermissionFor(resourceType, resourceId);if (perm != null && perm.role == Role.estateAdmin) { await engine.execute<UserPermissionAggregate, ChangePermissionRolePayload>( directive: ChangePermissionRoleDirective( payload: ChangePermissionRolePayload( permissionId: perm.permissionId, userId: perm.userId, resourceType: resourceType, resourceId: resourceId, previousRole: Role.estateAdmin, newRole: Role.estateRead, changedBy: UserId('compliance-audit'), reason: PermissionReasonDescription('Quarterly audit: privilege reduction'), ), ), ... );}