11using ModularityKit . Mutator . Abstractions . Context ;
22using ModularityKit . Mutator . Abstractions . Intent ;
33using ModularityKit . Mutator . Abstractions . Policies ;
4+ using ModularityKit . Mutator . Governance . Abstractions . Approval . Model ;
45using ModularityKit . Mutator . Governance . Abstractions . Requests . Factory ;
56using ModularityKit . Mutator . Governance . Abstractions . Requests . Model ;
67using ModularityKit . Mutator . Governance . Runtime . Approval . Execution ;
@@ -27,21 +28,44 @@ public static async Task Run()
2728 MutationContext . User ( "alice" , "Alice" , "Manager approved" ) ) ;
2829 PrintRequest ( afterAlice ) ;
2930
30- PrintSection ( "Approve Step 1 - Second Actor " ) ;
31+ PrintSection ( "Approve Step 2 - Quorum Approval 1/2 " ) ;
3132 var bobApproval = afterAlice . ApprovalRequirements . Single ( requirement => requirement . ApproverId == "bob" ) ;
3233 var afterBob = await manager . ApproveRequirement (
3334 request . RequestId ,
3435 bobApproval . ApprovalId ,
3536 MutationContext . User ( "bob" , "Bob" , "Security approved" ) ) ;
3637 PrintRequest ( afterBob ) ;
3738
38- PrintSection ( "Approve Step 2" ) ;
39+ PrintSection ( "Approve Step 2 - Quorum Approval 2/2 " ) ;
3940 var carolApproval = afterBob . ApprovalRequirements . Single ( requirement => requirement . ApproverId == "carol" ) ;
4041 var afterCarol = await manager . ApproveRequirement (
4142 request . RequestId ,
4243 carolApproval . ApprovalId ,
4344 MutationContext . User ( "carol" , "Carol" , "Finance approved" ) ) ;
4445 PrintRequest ( afterCarol ) ;
46+
47+ PrintSection ( "Approve Step 3 - Role Target" ) ;
48+ var financeApproval = afterCarol . ApprovalRequirements . Single ( requirement => requirement . ApproverRole == "finance-approver" ) ;
49+ var afterFinance = await manager . ApproveRequirement (
50+ request . RequestId ,
51+ financeApproval . ApprovalId ,
52+ MutationContext . User ( "frank" , "Frank" , "Finance role approved" ) with
53+ {
54+ Metadata = new Dictionary < string , object >
55+ {
56+ [ "ActorRoles" ] = new [ ] { "finance-approver" }
57+ }
58+ } ) ;
59+ PrintRequest ( afterFinance ) ;
60+
61+ PrintSection ( "Expire A Separate Pending Approval Request" ) ;
62+ var expiringRequest = await store . Create ( CreateExpiringApprovalRequest ( ) ) ;
63+ var expired = await manager . ExpirePendingApprovals (
64+ DateTimeOffset . UtcNow ,
65+ MutationContext . Service ( "approval-timeout-monitor" , "Expire stale approvals" ) ) ;
66+
67+ var expiredRequest = expired . Single ( candidate => candidate . RequestId == expiringRequest . RequestId ) ;
68+ PrintRequest ( expiredRequest ) ;
4569 }
4670
4771 private static MutationRequest CreateApprovalRequest ( )
@@ -63,29 +87,62 @@ private static MutationRequest CreateApprovalRequest()
6387 new PolicyRequirement
6488 {
6589 Type = "Approval" ,
66- Description = "Security review " ,
90+ Description = "Security quorum " ,
6791 Data = new
6892 {
69- Approver = "bob" ,
70- StepOrder = 1 ,
93+ Approvers = new [ ] { "bob" , "carol" , "dave" } ,
94+ StepOrder = 2 ,
95+ ApprovalGroupId = "security-quorum" ,
96+ Quorum = 2 ,
7197 Reason = "Security sign-off"
7298 }
7399 } ,
74100 new PolicyRequirement
75101 {
76102 Type = "Approval" ,
77- Description = "Finance review" ,
103+ Description = "Finance role review" ,
78104 Data = new
79105 {
80- Approver = "carol " ,
81- StepOrder = 2 ,
106+ ApproverRole = "finance-approver " ,
107+ StepOrder = 3 ,
82108 Reason = "Budget sign-off"
83109 }
84110 }
85111 ] ,
86112 expectedStateVersion : "v10" ) ;
87113 }
88114
115+ private static MutationRequest CreateExpiringApprovalRequest ( )
116+ {
117+ return MutationRequestFactory . PendingApproval (
118+ stateId : "tenant-42:deploy" ,
119+ stateType : "DeploymentState" ,
120+ mutationType : "ApproveDeploymentMutation" ,
121+ intent : new MutationIntent
122+ {
123+ OperationName = "ApproveDeployment" ,
124+ Category = "Operations" ,
125+ Description = "Approval request that will expire"
126+ } ,
127+ context : MutationContext . User ( "requester" , "Requester" , "Need emergency deployment approval" ) ,
128+ requirements :
129+ [
130+ new PolicyRequirement
131+ {
132+ Type = "Approval" ,
133+ Description = "Operations group approval" ,
134+ Data = new
135+ {
136+ ApproverGroup = "ops-oncall" ,
137+ StepOrder = 1 ,
138+ ExpiresAt = DateTimeOffset . UtcNow . AddMinutes ( - 1 ) ,
139+ Reason = "Operational readiness"
140+ }
141+ }
142+ ] ,
143+ expectedStateVersion : "v11" ) ;
144+ }
145+
89146 private static void PrintSection ( string title )
90147 {
91148 Console . WriteLine ( ) ;
@@ -102,11 +159,25 @@ private static void PrintRequest(MutationRequest request)
102159 foreach ( var requirement in request . ApprovalRequirements . OrderBy ( requirement => requirement . StepOrder ) . ThenBy ( requirement => requirement . ApproverId ) )
103160 {
104161 Console . WriteLine (
105- $ " - Step { requirement . StepOrder } : { requirement . ApproverId } => { requirement . Status } ") ;
162+ $ " - Step { requirement . StepOrder } : { DescribeTarget ( requirement ) } => { requirement . Status } (group: { requirement . ApprovalGroupId ?? "-" } , quorum: { requirement . RequiredApprovals } , expires: { requirement . ExpiresAt ? . ToString ( "O" ) ?? "-" } ) ") ;
106163 }
107164
108165 var lastDecision = request . Decisions [ ^ 1 ] ;
109166 Console . WriteLine ( $ "Last decision: { lastDecision . Type } by { lastDecision . Context . ActorId ?? "system" } ") ;
110167 Console . WriteLine ( $ "Reason: { lastDecision . Reason ?? "-" } ") ;
111168 }
169+
170+ private static string DescribeTarget ( MutationApprovalRequirement requirement )
171+ {
172+ if ( ! string . IsNullOrWhiteSpace ( requirement . ApproverId ) )
173+ return requirement . ApproverId ;
174+
175+ if ( ! string . IsNullOrWhiteSpace ( requirement . ApproverRole ) )
176+ return $ "role:{ requirement . ApproverRole } ";
177+
178+ if ( ! string . IsNullOrWhiteSpace ( requirement . ApproverGroup ) )
179+ return $ "group:{ requirement . ApproverGroup } ";
180+
181+ return "unknown" ;
182+ }
112183}
0 commit comments