8ea4e20f36
AEG-VS-01-03: Identity & Role Assignment State Machines Implementation: 1. IdentityState.cs - 7 states: UNDEFINED → ACTIVE → REQUIRES_MFA_SETUP → MFA_CONFIGURED → MFA_SUSPENDED → INACTIVE → REVOKED - Immutable value object with typed transitions - State queries (IsActive, IsMfaRequired, CanReceiveRoles) - No infrastructure dependencies (pure domain logic) 2. RoleAssignmentState.cs - Maker-Checker workflow: PENDING_APPROVAL → APPROVED_BY_1 → APPROVED_BY_2 → ACTIVE → EXPIRED/REVOKED/REJECTED - Approval count constraints enforced at state level - Immutable state transitions 3. IdentityStateTests.cs - 9 unit tests covering all transitions - Boundary testing (invalid transitions throw) - State query tests - Value object equality Principles: - 정공법: State machine encoded in domain, not middleware - SOLID: Single responsibility (state transitions) - 과유불액: Only what contract requires - 안정성: Immutable value objects, exception-based validation - 재현성: Pure C# logic, no DB/external dependencies All tests PASSING (9/9) Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
107 lines
3.5 KiB
C#
107 lines
3.5 KiB
C#
namespace KArtSell.Modules.IdentityAccess.ManageIdentityAndRoles.Domain;
|
|
|
|
/// <summary>
|
|
/// Role Assignment workflow state (Maker-Checker pattern)
|
|
/// AEG-VS-01-03: Immutable value object for approval workflow
|
|
/// </summary>
|
|
public sealed record RoleAssignmentState
|
|
{
|
|
public const string PendingApproval = "PENDING_APPROVAL";
|
|
public const string ApprovedBy1 = "APPROVED_BY_1";
|
|
public const string ApprovedBy2 = "APPROVED_BY_2";
|
|
public const string Active = "ACTIVE";
|
|
public const string Expired = "EXPIRED";
|
|
public const string Revoked = "REVOKED";
|
|
public const string Rejected = "REJECTED";
|
|
|
|
private static readonly HashSet<string> ValidStates =
|
|
[
|
|
PendingApproval, ApprovedBy1, ApprovedBy2, Active, Expired, Revoked, Rejected
|
|
];
|
|
|
|
public string Value { get; }
|
|
|
|
private RoleAssignmentState(string value)
|
|
{
|
|
if (!ValidStates.Contains(value))
|
|
throw new ArgumentException($"Invalid role assignment state: {value}", nameof(value));
|
|
Value = value;
|
|
}
|
|
|
|
// Factory methods
|
|
public static RoleAssignmentState CreatePending() => new(PendingApproval);
|
|
public static RoleAssignmentState Activate() => new(Active);
|
|
public static RoleAssignmentState Expire() => new(Expired);
|
|
public static RoleAssignmentState Revoke() => new(Revoked);
|
|
public static RoleAssignmentState Reject() => new(Rejected);
|
|
public static RoleAssignmentState Parse(string value) => new(value);
|
|
|
|
// State transitions
|
|
public RoleAssignmentState ApproveByFirst()
|
|
{
|
|
return Value switch
|
|
{
|
|
PendingApproval => new(ApprovedBy1),
|
|
_ => throw new InvalidOperationException($"Cannot approve from {Value}")
|
|
};
|
|
}
|
|
|
|
public RoleAssignmentState ApproveBySecond()
|
|
{
|
|
return Value switch
|
|
{
|
|
ApprovedBy1 => new(ApprovedBy2),
|
|
_ => throw new InvalidOperationException($"Cannot approve second from {Value}")
|
|
};
|
|
}
|
|
|
|
public RoleAssignmentState ActivateAfterApproval()
|
|
{
|
|
return Value switch
|
|
{
|
|
ApprovedBy2 => new(Active),
|
|
_ => throw new InvalidOperationException($"Cannot activate from {Value}")
|
|
};
|
|
}
|
|
|
|
public RoleAssignmentState ExpireTimebound()
|
|
{
|
|
return Value switch
|
|
{
|
|
Active => new(Expired),
|
|
_ => throw new InvalidOperationException($"Cannot expire from {Value}")
|
|
};
|
|
}
|
|
|
|
public RoleAssignmentState RevokeActive()
|
|
{
|
|
return Value switch
|
|
{
|
|
Active or Expired => new(Revoked),
|
|
_ => throw new InvalidOperationException($"Cannot revoke from {Value}")
|
|
};
|
|
}
|
|
|
|
public RoleAssignmentState RejectRequest()
|
|
{
|
|
return Value switch
|
|
{
|
|
PendingApproval or ApprovedBy1 => new(Rejected),
|
|
_ => throw new InvalidOperationException($"Cannot reject from {Value}")
|
|
};
|
|
}
|
|
|
|
// State queries
|
|
public bool IsPending() => Value == PendingApproval;
|
|
public bool IsAwaitingSecondApproval() => Value == ApprovedBy1;
|
|
public bool IsApproved() => Value == ApprovedBy2;
|
|
public bool IsActive() => Value == Active;
|
|
public bool IsExpired() => Value == Expired;
|
|
public bool IsRevoked() => Value == Revoked;
|
|
public bool IsRejected() => Value == Rejected;
|
|
public bool CanApprove() => Value is PendingApproval or ApprovedBy1;
|
|
public bool RequiresSecondApproval() => Value == ApprovedBy1;
|
|
|
|
public override string ToString() => Value;
|
|
}
|