Bounded Context: Authorization
Aggregate Root: Profile
Module: Ums.Domain.Authorization.Profile
Status: Production
The Profile aggregate represents an effective authorization assignment for a user inside a tenant. It binds a UserId to a RoleId and optionally to a BranchId, then materializes effective permissions from published PermissionTemplate definitions. It is the parent container for ProfilePermission owned entities and the operational source used by downstream access checks.
- Represent the active authorization footprint of a user in a tenant.
- Enforce scope boundaries between organization-wide and branch-scoped access.
- Materialize published template items into effective
ProfilePermissionentries. - Allow controlled per-permission overrides without mutating the source template.
- Control lifecycle state (
Active/Inactive) for the whole profile.
Profile is the aggregate root. Template linkage, permission overrides, permission activation/deactivation, and aggregate status changes must go through Profile.
TenantId,UserId, andRoleIdare mandatory for everyProfile.Scopeis derived fromBranchId: no branch meansOrgWide; a branch meansBranchScoped.- A
Profilecan only linkPermissionTemplateinstances from the same tenant. - A
Profilecan only link templates that are alreadyPublished. - A
Profilecannot link the same template twice. - Permission overrides and permission state changes are only valid while the parent
Profileis active. ProfilePermissionidentity is materialized per template item and keeps source lineage throughTemplateId.
| Entity / VO | Type | Ownership | Description |
|---|---|---|---|
ProfilePermission |
Entity | Owned | Effective permission materialized from a template item |
ProfileScope |
Enumeration | - | OrgWide or BranchScoped |
TenantId |
Value Object | - | Tenant ownership boundary |
UserId |
Value Object | - | User receiving the effective profile |
RoleId |
Value Object | - | Role source for template selection |
BranchId |
Value Object | - | Optional branch scoping |
TemplateId |
Value Object | - | Source template lineage for materialized permissions |
| Event | Trigger |
|---|---|
ProfileCreatedEvent |
New profile created |
TemplateLinkedToProfileEvent |
Published template linked and materialized into permissions |
PermissionOverriddenEvent |
Allow / deny / neutral / activate / deactivate applied to a permission |
ProfileDeactivatedEvent |
Profile deactivated |
ProfileActivatedEvent |
Profile reactivated |
Profile (Aggregate Root)
├── Props: ProfileProps
│ ├── Id: IdValueObject
│ ├── TenantId: TenantId
│ ├── UserId: UserId
│ ├── RoleId: RoleId
│ ├── BranchId?: BranchId
│ ├── Scope: ProfileScope
│ ├── IsActive: bool
│ └── Audit: AuditValueObject
└── Children
└── IReadOnlyCollection<ProfilePermission>
└── Props: ProfilePermissionProps
├── Id: IdValueObject
├── ProfileId: ProfileId
├── TemplateId: TemplateId
├── TargetType: ExclusiveArcTarget
├── TargetId: IdValueObject
├── ActionId: ActionId
├── IsAllowed: bool
├── IsDenied: bool
├── IsActive: bool
├── IsOverride: bool
└── Audit: AuditValueObject
classDiagram
direction TB
class Profile {
+Guid Id
+Guid TenantId
+Guid UserId
+Guid RoleId
+Guid? BranchId
+ProfileScope Scope
+bool IsActive
+List~ProfilePermission~ Permissions
+Create(tenantId, userId, roleId, branchId, actor)
+AssignTemplate(template, actor)
+OverridePermissionAllow(permissionId, actor)
+OverridePermissionDeny(permissionId, actor)
+OverridePermissionNeutral(permissionId, actor)
+ActivatePermission(permissionId, actor)
+DeactivatePermission(permissionId, actor)
+Activate(actor)
+Deactivate(actor)
}
class ProfilePermission {
+Guid Id
+Guid ProfileId
+Guid TemplateId
+ExclusiveArcTarget TargetType
+Guid TargetId
+Guid ActionId
+bool IsAllowed
+bool IsDenied
+bool IsActive
+bool IsOverride
}
Profile "1" *-- "0..*" ProfilePermission
sequenceDiagram
participant C as Client
participant H as Handler
participant R as IProfileRepository
participant P as Profile (AR)
participant T as PermissionTemplate
C->>H: CreateProfileCommand(tenantId, userId, roleId, branchId)
H->>P: Profile.Create(tenantId, userId, roleId, branchId, actor)
P->>P: Derive Scope from BranchId
P->>P: Raise ProfileCreatedEvent
H->>R: Add(profile)
R-->>H: ok
H->>T: Load published template for tenant/role
H->>P: profile.AssignTemplate(template, actor)
P->>P: Validate tenant and published status
P->>P: Materialize ProfilePermission items
P->>P: Raise TemplateLinkedToProfileEvent
H->>R: Update(profile)
R-->>H: ok
H-->>C: ProfileId
sequenceDiagram
participant C as Client
participant H as Handler
participant R as IProfileRepository
participant P as Profile (AR)
C->>H: OverridePermissionAllow(profileId, permissionId)
H->>R: GetById(profileId)
R-->>H: Profile
H->>P: OverridePermissionAllow(permissionId, actor)
P->>P: Validate active profile
P->>P: Mark permission as override
P->>P: Raise PermissionOverriddenEvent
H->>R: Update(profile)
R-->>H: ok
H-->>C: Success
erDiagram
PROFILE ||--o{ PROFILE_PERMISSION : "contains"
TENANT ||--o{ PROFILE : "owns"
USER_ACCOUNT ||--o{ PROFILE : "receives"
ROLE ||--o{ PROFILE : "sources"
BRANCH ||--o{ PROFILE : "scopes"
PERMISSION_TEMPLATE ||--o{ PROFILE_PERMISSION : "materializes"
ACTION ||--o{ PROFILE_PERMISSION : "enforces"
PROFILE {
uniqueidentifier Id PK
uniqueidentifier TenantId FK
uniqueidentifier UserId FK
uniqueidentifier RoleId FK
uniqueidentifier BranchId FK "Nullable"
int ScopeId "1=OrgWide, 2=BranchScoped"
bit IsActive
nvarchar CreatedBy
datetime2 CreatedAtUtc
nvarchar UpdatedBy "Nullable"
datetime2 UpdatedAtUtc "Nullable"
nvarchar AuditTimeSpan
}
PROFILE_PERMISSION {
uniqueidentifier Id PK
uniqueidentifier ProfileId FK
uniqueidentifier TemplateId FK
int TargetTypeId
uniqueidentifier TargetId
uniqueidentifier ActionId FK
bit IsAllowed
bit IsDenied
bit IsActive
bit IsOverride
nvarchar CreatedBy
datetime2 CreatedAtUtc
nvarchar UpdatedBy "Nullable"
datetime2 UpdatedAtUtc "Nullable"
nvarchar AuditTimeSpan
}
Profileis always tenant-owned in the current implementation;TenantIdis required.- Organization-wide behavior is modeled through
ScopeId = OrgWide, not through a nullTenantId. PROFILE_PERMISSIONinherits isolation scope from its parentProfile.
- Upstream: Consumes
TenantId,UserId, andBranchIdfrom the Identity Bounded Context. - Consumes
RoleIdand publishedPermissionTemplatedefinitions from the Authorization Context. - Consumes
ActionIdand target topology fromSystemSuite. - Is consumed by Approvals, IGA, and runtime authorization evaluators.
CreateProfileCommand-> Inputs:TenantId, UserId, RoleId, BranchId?-> Returns:Guid- Template assignment, permission overrides, and permission status changes are exposed through REST endpoints alongside the core profile lifecycle commands.
- Saved as part of the
Profiletransaction boundary. - Current SQL Server table:
[ums_authorization].[Profiles] - Current SQL Server child table:
[ums_authorization].[ProfilePermissions] - Current indexes for
Profile:TenantId,UserId,(TenantId, UserId, RoleId, BranchId) - Current indexes for
ProfilePermission:ProfileId,(ProfileId, TemplateId, ActionId, TargetId) - Audit metadata is persisted on both the aggregate root and the owned permissions.
- Profile mutation is a security-sensitive operation and updates aggregate audit metadata on every state change.
- Effective authorization can be tightened through per-permission deny or neutral overrides without changing the source template.
- Downstream access evaluators should treat inactive profiles and inactive permissions as non-effective.
Profileis an effective authorization assignment, not a named catalog role.Scopeis persisted as an enumeration identifier (ScopeId) and derived fromBranchIdat creation time.- Effective permissions preserve lineage through
TemplateId, allowing re-evaluation, auditing, and future rebuild workflows. - Manual changes are expressed through
IsOverrideand state toggles inProfilePermission, instead of mutatingPermissionTemplate.