Version: 1.0 Date: 2026-05-14 Epic: EP-08 (Post-MVP) User Stories: US-031, US-032 (EXPANDED to 5-6 stories) Functional Story: FS-12 (Role Promotion & Maturity)
Identity Governance & Administration is the practice of:
In UMS: This means defining a model where roles and permissions evolve over time, and transitions are governed, audited, and auditable.
Each role has a maturity level reflecting responsibility and seniority:
public enum RoleMaturityLevel
{
JUNIOR = 1, // Apprentice (0-6 months)
INTERMEDIATE = 2, // Contributor (6-18 months)
SENIOR = 3, // Expert (18+ months)
LEAD = 4, // Team leader
PRINCIPAL = 5 // Architect/Strategist
}
public record RoleMaturityStatus
{
public Guid UserId { get; init; }
public Guid RoleId { get; init; }
public RoleMaturityLevel CurrentLevel { get; init; }
public RoleMaturityLevel EligibleNextLevel { get; init; }
// Timeline
public DateTime AssignedAt { get; init; }
public DateTime CurrentLevelSince { get; init; }
public DateTime? EligibleForPromotionAt { get; init; }
// Compliance
public int CompletedCertifications { get; init; }
public int CompletedTrainings { get; init; }
public decimal PerformanceScore { get; init; } // 0.0 to 5.0
public bool HasNoComplianceIssues { get; init; }
public string? BlockingFactor { get; init; }
}
FS-12 manages complete promotion lifecycle:
As a: Senior user in role for 2+ years I want: Request promotion to Lead So that: Compensation and responsibilities align
As a: Security Administrator I want: See promotion impact So that: I don’t approve risky changes
Acceptance:
As a: Manager I want: Approve or reject promotion So that: Team aligned
As a: IGA Admin I want: Execute approved promotion So that: New permissions applied
As a: HR Analytics I want: See promotion metrics So that: I identify bottlenecks
As a: IGA System I want: Auto-calculate eligibility So that: Notifications automatic
public class RolePromotionImpactAnalysis
{
public Guid UserId { get; set; }
public Guid CurrentRoleId { get; set; }
public Guid TargetRoleId { get; set; }
// Permissions
public List<Permission> CurrentPermissions { get; set; }
public List<Permission> TargetPermissions { get; set; }
public List<Permission> PermissionsAdded { get; set; }
public List<Permission> PermissionsRemoved { get; set; }
public List<Permission> ConflictingPermissions { get; set; }
// Affected systems
public List<SystemImpact> AffectedSystems { get; set; }
// Risk
public decimal RiskScore { get; set; } // 0-100
public List<string> RiskFactors { get; set; }
public DateTime AnalyzedAt { get; set; }
public string AnalyzedBy { get; set; }
}
public class PromotionImpactAnalysisService
{
public async Task<RolePromotionImpactAnalysis> AnalyzeAsync(
User user,
Role currentRole,
Role targetRole)
{
// 1. Get current permissions
var currentPermissions = await _authorizationService
.GetEffectivePermissionsAsync(user.Id);
// 2. Get target role permissions
var targetPermissions = await _authorizationService
.GetPermissionsByRoleAsync(targetRole.Id);
// 3. Calculate differences
var added = targetPermissions.Except(currentPermissions).ToList();
var removed = currentPermissions.Except(targetPermissions).ToList();
// 4. Detect conflicts (e.g., create + delete = risky)
var conflicting = DetectConflictingPermissions(added);
// 5. Identify affected systems
var affectedSystems = added
.GroupBy(p => p.System)
.Select(g => new SystemImpact
{
SystemName = g.Key,
NewPermissionsCount = g.Count(),
ImpactLevel = CalculateImpactLevel(g)
})
.ToList();
// 6. Calculate risk score
var riskScore = CalculateRiskScore(added, removed, targetRole, user);
return new RolePromotionImpactAnalysis
{
UserId = user.Id,
CurrentRoleId = currentRole.Id,
TargetRoleId = targetRole.Id,
PermissionsAdded = added,
PermissionsRemoved = removed,
ConflictingPermissions = conflicting,
AffectedSystems = affectedSystems,
RiskScore = riskScore,
AnalyzedAt = DateTime.UtcNow
};
}
}
stateDiagram-v2
[*] --> DRAFT
DRAFT --> PENDING_MANAGER_APPROVAL: Submit
PENDING_MANAGER_APPROVAL --> PENDING_SECURITY_REVIEW: Manager approves
PENDING_MANAGER_APPROVAL --> REJECTED: Manager rejects
PENDING_SECURITY_REVIEW --> PENDING_SECURITY_APPROVAL: HIGH_RISK
PENDING_SECURITY_REVIEW --> APPROVED_READY_TO_EXECUTE: LOW_RISK
PENDING_SECURITY_APPROVAL --> APPROVED_READY_TO_EXECUTE: Security approves
PENDING_SECURITY_APPROVAL --> REJECTED: Security rejects
APPROVED_READY_TO_EXECUTE --> EXECUTED: Admin executes
EXECUTED --> VERIFIED: Verification ok
EXECUTED --> VERIFICATION_FAILED: Verification failed
REJECTED --> [*]
VERIFIED --> [*]
VERIFICATION_FAILED --> [*]
flowchart TB
subgraph IGA[IGA BOUNDED CONTEXT]
direction TB
subgraph AG[Aggregates]
A1[RoleMaturityStatus]
A2[PromotionRequest]
A3[PromotionImpactAnalysis]
end
subgraph PO[Ports]
P1[IPromotionApprovalService]
P2[IPromotionImpactAnalyzer]
P3[IEligibilityCalculator]
end
subgraph AD[Adapters]
AD1[SqlServerIGARepository]
AD2[PromotionApprovalAdapter]
end
subgraph EV[Events]
E1[PromotionRequestedEvent]
E2[PromotionEligibilityCalculatedEvent]
E3[PromotionApprovedEvent]
E4[PromotionRejectedEvent]
E5[PromotionExecutedEvent]
E6[PromotionVerifiedEvent]
end
end
-- ============================================
-- IGA CONTEXT TABLES
-- ============================================
CREATE TABLE [iga].[role_maturity_levels] (
[id] UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWID(),
[root_tenant_id] UNIQUEIDENTIFIER NOT NULL,
[user_id] UNIQUEIDENTIFIER NOT NULL,
[role_id] UNIQUEIDENTIFIER NOT NULL,
[current_maturity_level] VARCHAR(32) NOT NULL,
[next_eligible_maturity_level] VARCHAR(32),
[assigned_at] DATETIME2 NOT NULL,
[current_level_since] DATETIME2 NOT NULL,
[eligible_for_promotion_at] DATETIME2,
[completed_certifications_count] INT DEFAULT 0,
[completed_trainings_count] INT DEFAULT 0,
[performance_score] DECIMAL(3,2),
[has_no_compliance_issues] BIT DEFAULT 1,
[blocking_factor] NVARCHAR(MAX),
[last_reviewed_at] DATETIME2,
CONSTRAINT pk_role_maturity_levels PRIMARY KEY (id, root_tenant_id),
CONSTRAINT fk_role_maturity_user FOREIGN KEY (user_id, root_tenant_id) REFERENCES [identity].[users](id, root_tenant_id)
);
CREATE TABLE [iga].[promotion_requests] (
[id] UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWID(),
[root_tenant_id] UNIQUEIDENTIFIER NOT NULL,
[user_id] UNIQUEIDENTIFIER NOT NULL,
[current_role_id] UNIQUEIDENTIFIER NOT NULL,
[target_role_id] UNIQUEIDENTIFIER NOT NULL,
[requested_at] DATETIME2 NOT NULL DEFAULT GETUTCDATE(),
[requested_by] UNIQUEIDENTIFIER NOT NULL,
[request_reason] NVARCHAR(MAX),
[manager_id] UNIQUEIDENTIFIER NOT NULL,
[manager_approval_status] VARCHAR(32),
[manager_decision_at] DATETIME2,
[security_approval_status] VARCHAR(32),
[security_decision_at] DATETIME2,
[status] VARCHAR(32) NOT NULL DEFAULT 'DRAFT',
[final_status] VARCHAR(32),
[executed_at] DATETIME2,
[executed_by] UNIQUEIDENTIFIER,
[verified_at] DATETIME2,
CONSTRAINT pk_promotion_requests PRIMARY KEY (id, root_tenant_id),
CONSTRAINT fk_promotion_requests_user FOREIGN KEY (user_id, root_tenant_id) REFERENCES [identity].[users](id, root_tenant_id)
);
CREATE TABLE [iga].[promotion_impact_analysis] (
[id] UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWID(),
[root_tenant_id] UNIQUEIDENTIFIER NOT NULL,
[promotion_request_id] UNIQUEIDENTIFIER NOT NULL,
[risk_score] DECIMAL(5,2),
[risk_level] VARCHAR(32),
[new_permissions_count] INT,
[removed_permissions_count] INT,
[affected_systems_count] INT,
[conflicting_permissions] NVARCHAR(MAX),
[risk_factors] NVARCHAR(MAX),
[suggested_mitigations] NVARCHAR(MAX),
[analyzed_at] DATETIME2 NOT NULL DEFAULT GETUTCDATE(),
[analyzed_by] VARCHAR(255),
CONSTRAINT pk_promotion_impact_analysis PRIMARY KEY (id, root_tenant_id),
CONSTRAINT fk_promotion_impact_request FOREIGN KEY (promotion_request_id, root_tenant_id) REFERENCES [iga].[promotion_requests](id, root_tenant_id)
);
CREATE TABLE [iga].[promotion_eligible_notifications] (
[id] UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWID(),
[root_tenant_id] UNIQUEIDENTIFIER NOT NULL,
[user_id] UNIQUEIDENTIFIER NOT NULL,
[eligible_for_next_level] VARCHAR(32),
[eligible_at] DATETIME2 NOT NULL,
[notification_sent_at] DATETIME2,
[user_acknowledged_at] DATETIME2,
CONSTRAINT pk_promotion_eligible_notifications PRIMARY KEY (id, root_tenant_id)
);
-- Indices
CREATE INDEX idx_role_maturity_user ON [iga].[role_maturity_levels] (user_id, root_tenant_id);
CREATE INDEX idx_promotion_requests_user ON [iga].[promotion_requests] (user_id, root_tenant_id)
WHERE status IN ('PENDING_MANAGER_APPROVAL', 'PENDING_SECURITY_REVIEW');
Promotion requests may require formal approval if target role is sensitive:
if (targetRole.RiskLevel == "CRITICAL")
{
var approvalRequest = await _approvalService.CreateApprovalRequestAsync(
workflow: "ROLE_PROMOTION_APPROVAL",
requester: user.Id,
linkedEntity: promotionRequest.Id);
}
When promotion executes, user permissions updated:
await _authorizationService.RevokePermissionsAsync(user.Id, currentRole.Id);
await _authorizationService.AssignPermissionsAsync(user.Id, targetRole.Id);
All events audited:
await _auditService.LogAsync(new AuditEvent
{
EventType = "PROMOTION_EXECUTED",
UserId = user.Id,
Details = new
{
FromRole = currentRole.Name,
ToRole = targetRole.Name,
RiskScore = impactAnalysis.RiskScore
}
});
Approved by: Principal Architect Date: 2026-05-14