Contexto Acotado: IGA
Raíz del Agregado: Sí
Módulo: Ums.Domain.IGA.PromotionRequest
Estado: Producción
El agregado PromotionRequest coordina los ascensos de acceso, lo que permite a los usuarios solicitar de manera segura transiciones desde su rol actual hacia un rol de destino más privilegiado. Impone una ruta estricta y auditada de verificación que incluye puntajes de riesgo automáticos, aprobación de gerentes, evaluaciones de seguridad, ejecución de roles y verificación posterior a la ejecución.
PromotionImpactAnalysis).PromotionRequest sirve como la raíz del agregado, gestionando el ciclo de vida del proceso de ascenso y albergando a PromotionImpactAnalysis como una entidad de propiedad exclusiva.
Draft.Draft $\rightarrow$ PendingManagerApproval (a través de Submit).PendingManagerApproval $\rightarrow$ PendingSecurityReview (a través de ManagerApprove) O Rejected (a través de ManagerReject).PendingSecurityReview $\rightarrow$ ApprovedReadyToExecute (a través de SecurityReviewLowRisk si la puntuación de riesgo analizada es baja) O PendingSecurityApproval (a través de SecurityReviewHighRisk si la puntuación de riesgo es alta) O Rejected (a través de SecurityReject).PendingSecurityApproval $\rightarrow$ ApprovedReadyToExecute (a través de SecurityApprove) O Rejected (a través de SecurityReject).ApprovedReadyToExecute $\rightarrow$ Executed (a través de Execute).Executed $\rightarrow$ Verified (a través de Verify) O VerificationFailed (a través de MarkVerificationFailed).DomainErrors.IGA.ImpactAnalysisAlreadyExists).RiskScore en el análisis de impacto debe ser un decimal estrictamente entre 0 y 100 inclusive (DomainErrors.IGA.InvalidPerformanceScore).| Entidad / VO | Tipo | Descripción |
|—|—|—|
| PromotionRequestId | Objeto de Valor | Identificador único del agregado |
| TenantId | Objeto de Valor | Identificador de partición asignado al contexto del inquilino |
| UserId | Objeto de Valor | Referencia al usuario objetivo (Contexto de Identity) |
| RoleId | Objeto de Valor | Referencia a los roles actual y objetivo (Contexto de Autorización) |
| PromotionStatus | Enumerado | Enumerado del estado de la FSM (Draft, PendingManagerApproval, etc.) |
| ApprovalDecision | Enumerado | None · Approved · Rejected |
| PromotionImpactAnalysis | Entidad | Entidad hija de propiedad exclusiva que contiene métricas de riesgo |
| PromotionImpactAnalysisId | Objeto de Valor | Identificador único de la entidad hija de análisis de impacto |
| TextValueObject | Objeto de Valor | Propiedades de texto generales para niveles de riesgo, mitigaciones, conflictos, etc. |
PromotionRequest (Aggregate Root)
├── Props: PromotionRequestProps
│ ├── Id: PromotionRequestId
│ ├── TenantId: TenantId
│ ├── UserId: UserId (Ref Externa)
│ ├── CurrentRoleId: RoleId (Ref Externa)
│ ├── TargetRoleId: RoleId (Ref Externa)
│ ├── RequestedAt: DateTime
│ ├── RequestedBy: ActorId
│ ├── RequestReason: TextValueObject?
│ ├── ManagerId: UserId
│ ├── ManagerApprovalStatus: ApprovalDecision
│ ├── ManagerDecisionAt: DateTime?
│ ├── SecurityApprovalStatus: ApprovalDecision
│ ├── SecurityDecisionAt: DateTime?
│ ├── Status: PromotionStatus
│ ├── ExecutedAt: DateTime?
│ ├── ExecutedBy: ActorId?
│ ├── VerifiedAt: DateTime?
│ └── Audit: AuditValueObject
└── ImpactAnalyses: PromotionImpactAnalysis[] (Colección Hija)
└── Props: PromotionImpactAnalysisProps
├── Id: IdValueObject
├── PromotionRequestId: PromotionRequestId
├── RiskScore: decimal
├── RiskLevel: TextValueObject
├── NewPermissionsCount: int
├── RemovedPermissionsCount: int
├── AffectedSystemsCount: int
├── ConflictingPermissions: TextValueObject?
├── RiskFactors: TextValueObject?
├── SuggestedMitigations: TextValueObject?
├── AnalyzedAt: DateTime
└── AnalyzedBy: TextValueObject?
classDiagram
direction TB
class PromotionRequest {
+PromotionRequestProps Props
+IReadOnlyCollection~PromotionImpactAnalysis~ ImpactAnalyses
+Create(TenantId, UserId, RoleId, RoleId, UserId, TextValueObject?, ActorId) Result~PromotionRequest~
+Submit(ActorId) Result
+ManagerApprove(ActorId) Result
+ManagerReject(ActorId) Result
+SecurityReviewLowRisk(ActorId) Result
+SecurityReviewHighRisk(ActorId) Result
+SecurityApprove(ActorId) Result
+SecurityReject(ActorId) Result
+Execute(ActorId) Result
+Verify(ActorId) Result
+MarkVerificationFailed(ActorId) Result
+AddImpactAnalysis(...) Result
}
class PromotionRequestProps {
+IdValueObject Id
+TenantId TenantId
+UserId UserId
+RoleId CurrentRoleId
+RoleId TargetRoleId
+DateTime RequestedAt
+ActorId RequestedBy
+TextValueObject RequestReason
+UserId ManagerId
+ApprovalDecision ManagerApprovalStatus
+ApprovalDecision SecurityApprovalStatus
+PromotionStatus Status
+AuditValueObject Audit
}
class PromotionImpactAnalysis {
+IdValueObject Id
+PromotionRequestId PromotionRequestId
+decimal RiskScore
+TextValueObject RiskLevel
+int NewPermissionsCount
+int RemovedPermissionsCount
+int AffectedSystemsCount
+TextValueObject ConflictingPermissions
+TextValueObject RiskFactors
+TextValueObject SuggestedMitigations
+DateTime AnalyzedAt
+TextValueObject AnalyzedBy
+Create() Result~PromotionImpactAnalysis~
}
class PromotionStatus {
<<enumeration>>
Draft
PendingManagerApproval
PendingSecurityReview
PendingSecurityApproval
ApprovedReadyToExecute
Executed
Verified
Rejected
VerificationFailed
}
PromotionRequest *-- PromotionRequestProps
PromotionRequest "1" *-- "0..1" PromotionImpactAnalysis : posee
PromotionRequestProps --> PromotionStatus
Nota: Las secuencias de creación y validación para el análisis de impacto se coordinan exclusivamente a través del agregado raíz.
sequenceDiagram
autonumber
actor Mgr as Gerente
actor Sec as Auditor de Seguridad
participant App as Servicio de Aplicación
participant PR as PromotionRequest [Agregado]
participant Repo as PromotionRequestRepository
participant DB as SQL Server
Note over Mgr, PR: La solicitud está en estado PendingManagerApproval
Mgr->>App: ApprovePromotionRequest(RequestId)
App->>Repo: GetByIdAsync(RequestId)
Repo-->>App: PromotionRequest
App->>PR: ManagerApprove(ManagerId)
note over PR: Status = PendingSecurityReview
PR-->>App: Success
App->>Repo: SaveAsync(PR)
Note over Sec, PR: La revisión de seguridad detecta una puntuación de riesgo alto (ej. conflicto de SOD)
Sec->>App: AddImpactAnalysis(RiskScore: 85)
App->>PR: AddImpactAnalysis(...)
PR-->>App: Success
Sec->>App: ReviewHighRisk(SecurityId)
App->>PR: SecurityReviewHighRisk(SecurityId)
note over PR: Status = PendingSecurityApproval
PR-->>App: Success
Note over Sec, PR: Aprobación Ejecutiva de Seguridad
Sec->>App: SecurityApprove(SecurityId)
App->>PR: SecurityApprove(SecurityId)
note over PR: Status = ApprovedReadyToExecute
PR-->>App: Success
App->>Repo: SaveAsync(PR)
Repo->>DB: UPDATE PROMOTION_REQUEST Status = 'ApprovedReadyToExecute'
DB-->>Repo: Acknowledge
erDiagram
PROMOTION_REQUEST ||--o| PROMOTION_IMPACT_ANALYSIS : "posee / evaluado por"
PROMOTION_REQUEST {
uniqueidentifier PromotionRequestId PK
uniqueidentifier TenantId FK
uniqueidentifier UserId FK "Contexto de Identity"
uniqueidentifier CurrentRoleId FK "Contexto de Autorización"
uniqueidentifier TargetRoleId FK "Contexto de Autorización"
datetime2 RequestedAt
nvarchar RequestedBy
nvarchar RequestReason
uniqueidentifier ManagerId
nvarchar ManagerApprovalStatus
datetime2 ManagerDecisionAt
nvarchar SecurityApprovalStatus
datetime2 SecurityDecisionAt
nvarchar Status
datetime2 ExecutedAt
nvarchar ExecutedBy
datetime2 VerifiedAt
nvarchar CreatedBy
datetime2 CreatedAt
}
PROMOTION_IMPACT_ANALYSIS {
uniqueidentifier AnalysisId PK
uniqueidentifier PromotionRequestId FK
decimal RiskScore
nvarchar RiskLevel
int NewPermissionsCount
int RemovedPermissionsCount
int AffectedSystemsCount
nvarchar ConflictingPermissions
nvarchar RiskFactors
nvarchar SuggestedMitigations
datetime2 AnalyzedAt
nvarchar AnalyzedBy
}
TenantId. Los envíos se verifican contra las propiedades de configuración del inquilino para evitar la falsificación de solicitudes entre inquilinos.PromotionImpactAnalysis hereda las reglas de delimitación de su agregado raíz padre PromotionRequest. El acceso entre inquilinos está implícitamente bloqueado.Los motores de seguridad leen los hallazgos de PromotionImpactAnalysis para decidir si bloquear acciones o requerir flujos de aprobación de alto riesgo.
flowchart TD
subgraph IdentityContext [Contexto de Identity]
U[UserAccount]
end
subgraph AuthContext [Contexto de Autorización]
R1[Rol Actual]
R2[Rol Objetivo]
end
subgraph IgaContext [Contexto IGA]
PR[PromotionRequest]
PIA[PromotionImpactAnalysis]
end
PR -.->|hace referencia a UserId| U
PR -.->|hace referencia a| R1
PR -.->|hace referencia a| R2
PR *--|posee| PIA
Draft.PromotionRequest para adjuntar los datos del análisis de impacto.public class PromotionRequestConfiguration : IEntityTypeConfiguration<PromotionRequest>
{
public void Configure(EntityTypeBuilder<PromotionRequest> builder)
{
builder.ToTable("PROMOTION_REQUEST");
builder.HasKey(e => e.Id);
builder.OwnsOne(e => e.Props, props =>
{
props.Property(p => p.Id).HasColumnName("PromotionRequestId");
props.Property(p => p.TenantId).HasColumnName("TenantId");
props.Property(p => p.UserId).HasColumnName("UserId");
props.Property(p => p.CurrentRoleId).HasColumnName("CurrentRoleId");
props.Property(p => p.TargetRoleId).HasColumnName("TargetRoleId");
props.Property(p => p.RequestedAt).HasColumnName("RequestedAt");
props.Property(p => p.RequestedBy).HasConversion(a => a.GetValue(), s => ActorId.Load(s)).HasColumnName("RequestedBy");
props.Property(p => p.RequestReason).HasConversion(p => p.GetValue(), s => TextValueObject.Create(s).Value).HasColumnName("RequestReason");
props.Property(p => p.ManagerId).HasColumnName("ManagerId");
props.Property(p => p.ManagerApprovalStatus).HasConversion<string>().HasColumnName("ManagerApprovalStatus");
props.Property(p => p.ManagerDecisionAt).HasColumnName("ManagerDecisionAt");
props.Property(p => p.SecurityApprovalStatus).HasConversion<string>().HasColumnName("SecurityApprovalStatus");
props.Property(p => p.SecurityDecisionAt).HasColumnName("SecurityDecisionAt");
props.Property(p => p.Status).HasConversion<string>().HasColumnName("Status");
props.Property(p => p.ExecutedAt).HasColumnName("ExecutedAt");
props.Property(p => p.ExecutedBy).HasConversion(a => a == null ? (Guid?)null : a.GetValue(), s => s == null ? null : ActorId.Load(s.Value)).HasColumnName("ExecutedBy");
props.Property(p => p.VerifiedAt).HasColumnName("VerifiedAt");
props.OwnsOne(p => p.Audit);
});
// Mapea las propiedades del esquema dependiente. La eliminación en cascada garantiza la consistencia.
builder.HasMany(e => e.ImpactAnalyses)
.WithOne()
.HasForeignKey("PromotionRequestId")
.OnDelete(DeleteBehavior.Cascade);
}
}
ManagerId) autorizado para aprobar una solicitud de ascenso no puede ser el usuario objetivo (UserId) ni el auditor de seguridad que realiza la evaluación de seguridad.PendingSecurityApproval), evitando adiciones de roles automáticas sin un visto bueno especializado.PromotionImpactAnalysis, en lugar de bloquear síncronamente los flujos de escritura de la capa de aplicación o flujos de ejecución del usuario mientras se ejecutan análisis pesados sobre gráficos de permisos.