Skip to content

Commit e81234d

Browse files
committed
Feat: Harden governance approval workflow
1 parent 82549e9 commit e81234d

17 files changed

Lines changed: 1135 additions & 251 deletions

Examples/Governance/ApprovalWorkflow/Scenarios/GovernanceApprovalWorkflowScenario.cs

Lines changed: 80 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using ModularityKit.Mutator.Abstractions.Context;
22
using ModularityKit.Mutator.Abstractions.Intent;
33
using ModularityKit.Mutator.Abstractions.Policies;
4+
using ModularityKit.Mutator.Governance.Abstractions.Approval.Model;
45
using ModularityKit.Mutator.Governance.Abstractions.Requests.Factory;
56
using ModularityKit.Mutator.Governance.Abstractions.Requests.Model;
67
using 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

Comments
 (0)