Skip to content

Commit b80e1fc

Browse files
committed
feat(examples/iam-roles): add IAM roles sample with critical policies and batch migrations
- Added IamRoles example project to Polygon solution - Implemented IAM role mutations: - GrantUserRoleMutation (critical security mutation) - RevokeUserRoleMutation - Introduced domain policies: - PreventLastAdminRemovalPolicy (guards against removing final Admin) - RequireTwoManApprovalPolicy for critical role changes - Added execution scenarios: - GrantAdminScenario (single critical mutation with approval) - RevokeAdminScenario (policy-guarded admin revocation) - BatchRoleMigrationScenario (batch role grants during org restructure) - Demonstrated: - Critical vs high-risk mutation classification - System vs user MutationContext usage - Metadata-driven approvals (approvedBy) - Policy blocking and decision reporting - Batch execution with partial failures - Added immutable UserPermissionsState domain model - Registered IamRoles project in solution structure
1 parent 663c4b1 commit b80e1fc

11 files changed

Lines changed: 505 additions & 0 deletions

ModularityKit.Mutator.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
<Folder Name="/Polygon/">
33
<Project Path="Polygon/BillingQuotas/BillingQuotas.csproj" />
44
<Project Path="Polygon/FeatureFlags/FeatureFlags.csproj" />
5+
<Project Path="Polygon/IamRoles/IamRoles.csproj" />
56
</Folder>
67
<Project Path="ModularityKit.Mutator/ModularityKit.Mutator.csproj" />
78
</Solution>

Polygon/IamRoles/IamRoles.csproj

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net10.0</TargetFramework>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<ProjectReference Include="..\..\ModularityKit.Mutator\ModularityKit.Mutator.csproj" />
12+
</ItemGroup>
13+
14+
<ItemGroup>
15+
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.2" />
16+
</ItemGroup>
17+
18+
</Project>
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
using IamRoles.State;
2+
using ModularityKit.Mutator.Abstractions.Changes;
3+
using ModularityKit.Mutator.Abstractions.Context;
4+
using ModularityKit.Mutator.Abstractions.Engine;
5+
using ModularityKit.Mutator.Abstractions.Intent;
6+
using ModularityKit.Mutator.Abstractions.Results;
7+
8+
namespace IamRoles.Mutations;
9+
10+
/// <summary>
11+
/// Mutation that grants role to user in current <see cref="UserPermissionsState"/>.
12+
/// </summary>
13+
internal sealed record GrantUserRoleMutation(
14+
string UserId,
15+
string Role,
16+
MutationContext Context
17+
) : IMutation<UserPermissionsState>
18+
{
19+
public MutationIntent Intent { get; } = new()
20+
{
21+
OperationName = "GrantUserRole",
22+
Category = "Security",
23+
RiskLevel = MutationRiskLevel.Critical,
24+
Description = "Grants role to a user"
25+
};
26+
27+
public ValidationResult Validate(UserPermissionsState state)
28+
{
29+
var result = new ValidationResult();
30+
31+
if (string.IsNullOrWhiteSpace(UserId))
32+
result.AddError("UserId", "UserId cannot be empty");
33+
34+
if (string.IsNullOrWhiteSpace(Role))
35+
result.AddError("Role", "Role cannot be empty");
36+
37+
if (state.RolesByUser.TryGetValue(UserId, out var roles) &&
38+
roles.Contains(Role))
39+
result.AddError("Role", "User already has this role");
40+
41+
return result;
42+
}
43+
44+
public MutationResult<UserPermissionsState> Apply(UserPermissionsState state)
45+
{
46+
var rolesByUser = state.RolesByUser
47+
.ToDictionary(kv => kv.Key, kv => new HashSet<string>(kv.Value));
48+
49+
if (!rolesByUser.TryGetValue(UserId, out var roles))
50+
{
51+
roles = [];
52+
rolesByUser[UserId] = roles;
53+
}
54+
55+
roles.Add(Role);
56+
57+
var newState = state with { RolesByUser = rolesByUser };
58+
59+
var changes = ChangeSet.Single(
60+
StateChange.Added($"RolesByUser.{UserId}", Role)
61+
);
62+
63+
return MutationResult<UserPermissionsState>.Success(newState, changes);
64+
}
65+
66+
public MutationResult<UserPermissionsState> Simulate(UserPermissionsState state)
67+
=> Apply(state);
68+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
using IamRoles.State;
2+
using ModularityKit.Mutator.Abstractions.Changes;
3+
using ModularityKit.Mutator.Abstractions.Context;
4+
using ModularityKit.Mutator.Abstractions.Engine;
5+
using ModularityKit.Mutator.Abstractions.Intent;
6+
using ModularityKit.Mutator.Abstractions.Results;
7+
8+
namespace IamRoles.Mutations;
9+
10+
/// <summary>
11+
/// Mutation that revokes role from user in the current <see cref="UserPermissionsState"/>.
12+
/// </summary>
13+
internal sealed record RevokeUserRoleMutation(
14+
string UserId,
15+
string Role,
16+
MutationContext Context
17+
) : IMutation<UserPermissionsState>
18+
{
19+
public MutationIntent Intent { get; } = new()
20+
{
21+
OperationName = "RevokeUserRole",
22+
Category = "Security",
23+
RiskLevel = MutationRiskLevel.High,
24+
Description = "Revokes a role from a user"
25+
};
26+
27+
public ValidationResult Validate(UserPermissionsState state)
28+
{
29+
var result = new ValidationResult();
30+
31+
if (!state.RolesByUser.TryGetValue(UserId, out var roles) ||
32+
!roles.Contains(Role))
33+
result.AddError("Role", "User does not have this role");
34+
35+
return result;
36+
}
37+
38+
public MutationResult<UserPermissionsState> Apply(UserPermissionsState state)
39+
{
40+
var rolesByUser = state.RolesByUser
41+
.ToDictionary(kv => kv.Key, kv => new HashSet<string>(kv.Value));
42+
43+
rolesByUser[UserId].Remove(Role);
44+
45+
var newState = state with { RolesByUser = rolesByUser };
46+
47+
var changes = ChangeSet.Single(
48+
StateChange.Removed($"RolesByUser.{UserId}", Role)
49+
);
50+
51+
return MutationResult<UserPermissionsState>.Success(newState, changes);
52+
}
53+
54+
public MutationResult<UserPermissionsState> Simulate(UserPermissionsState state)
55+
=> Apply(state);
56+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
using IamRoles.Mutations;
2+
using IamRoles.State;
3+
using ModularityKit.Mutator.Abstractions.Engine;
4+
using ModularityKit.Mutator.Abstractions.Policies;
5+
6+
namespace IamRoles.Policies;
7+
8+
/// <summary>
9+
/// Policy that prevents the removal of last user with "Admin" role.
10+
/// </summary>
11+
internal sealed class PreventLastAdminRemovalPolicy
12+
: IMutationPolicy<UserPermissionsState>
13+
{
14+
public string Name => "PreventLastAdminRemoval";
15+
public int Priority => 100;
16+
public string Description => "Prevents removal of the last Admin user.";
17+
18+
/// <summary>
19+
/// Evaluates mutation against the policy.
20+
/// Only RevokeUserRoleMutation for the "Admin" role is subject to this policy.
21+
/// </summary>
22+
/// <param name="mutation">The mutation being evaluated.</param>
23+
/// <param name="state">The current user permissions state.</param>
24+
/// <returns>A <see cref="PolicyDecision"/> indicating whether the mutation is allowed or denied.</returns>
25+
public PolicyDecision Evaluate(
26+
IMutation<UserPermissionsState> mutation,
27+
UserPermissionsState state)
28+
{
29+
if (mutation is not RevokeUserRoleMutation { Role: "Admin" })
30+
return PolicyDecision.Allow();
31+
32+
var adminCount = state.RolesByUser
33+
.SelectMany(kv => kv.Value)
34+
.Count(r => r == "Admin");
35+
36+
if (adminCount <= 1)
37+
{
38+
return PolicyDecision.Deny(
39+
"Cannot remove the last Admin user");
40+
}
41+
42+
return PolicyDecision.Allow();
43+
}
44+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
using IamRoles.State;
2+
using ModularityKit.Mutator.Abstractions.Engine;
3+
using ModularityKit.Mutator.Abstractions.Intent;
4+
using ModularityKit.Mutator.Abstractions.Policies;
5+
6+
namespace IamRoles.Policies;
7+
8+
/// <summary>
9+
/// Policy that requires second user approval for critical mutations in <see cref="UserPermissionsState"/>.
10+
/// </summary>
11+
public sealed class RequireTwoManApprovalPolicy
12+
: IMutationPolicy<UserPermissionsState>
13+
{
14+
public string Name => "RequireTwoManApproval";
15+
public int Priority => 100;
16+
public string Description => "Requires second user approval for critical mutations.";
17+
18+
/// <summary>
19+
/// Evaluates mutation against the policy.
20+
/// Critical mutations must have a second approver that is different from the actor.
21+
/// </summary>
22+
/// <param name="mutation">The mutation being evaluated.</param>
23+
/// <param name="state">The current user permissions state.</param>
24+
/// <returns>A <see cref="PolicyDecision"/> indicating whether the mutation is allowed or denied.</returns>
25+
public PolicyDecision Evaluate(
26+
IMutation<UserPermissionsState> mutation,
27+
UserPermissionsState state)
28+
{
29+
if (mutation.Intent.RiskLevel != MutationRiskLevel.Critical)
30+
return PolicyDecision.Allow();
31+
32+
var context = mutation.Context;
33+
34+
if (!context.Metadata.TryGetValue("approvedBy", out var approvedObj))
35+
{
36+
return PolicyDecision.Deny(
37+
"Critical mutation requires second approval (approvedBy missing)");
38+
}
39+
40+
var approvers = approvedObj switch
41+
{
42+
string s => s.Split(',').Select(x => x.Trim()).Where(x => !string.IsNullOrEmpty(x)),
43+
IEnumerable<string> arr => arr,
44+
_ => [approvedObj?.ToString() ?? string.Empty]
45+
};
46+
47+
if (approvers.Any(a => string.Equals(a, context.ActorId, StringComparison.OrdinalIgnoreCase)))
48+
{
49+
return PolicyDecision.Deny(
50+
"Second approval must come from a different user");
51+
}
52+
53+
return PolicyDecision.Allow();
54+
}
55+
}

Polygon/IamRoles/Program.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
using IamRoles.Policies;
2+
using Microsoft.Extensions.DependencyInjection;
3+
using ModularityKit.Mutator.Abstractions;
4+
using ModularityKit.Mutator.Abstractions.Engine;
5+
using ModularityKit.Mutator.Runtime;
6+
7+
namespace IamRoles;
8+
9+
internal static class Program
10+
{
11+
private static async Task Main()
12+
{
13+
var services = new ServiceCollection();
14+
services.AddMutators(MutationEngineOptions.Strict, addDefaultLoggingInterceptor: true);
15+
16+
var provider = services.BuildServiceProvider();
17+
var engine = provider.GetRequiredService<IMutationEngine>();
18+
19+
engine.RegisterPolicy(new PreventLastAdminRemovalPolicy());
20+
engine.RegisterPolicy(new RequireTwoManApprovalPolicy());
21+
22+
Console.WriteLine("=== ModularityKit.Mutators - Complete Example ===\n");
23+
24+
await Scenarios.GrantAdminScenario.Run(engine);
25+
await Scenarios.RevokeAdminScenario.Run(engine);
26+
await Scenarios.BatchRoleMigrationScenario.Run(engine);
27+
28+
Console.WriteLine("\n METRICS & STATISTICS");
29+
30+
var stats = await engine.GetStatisticsAsync();
31+
32+
Console.WriteLine($"\n Mutation Statistics:");
33+
Console.WriteLine($" Total executed: {stats.TotalExecuted}");
34+
35+
Console.WriteLine($"\n Performance Metrics:");
36+
Console.WriteLine($" Average execution time: {stats.AverageExecutionTime.TotalMilliseconds:F2} ms");
37+
Console.WriteLine($" Median execution time: {stats.MedianExecutionTime.TotalMilliseconds:F2} ms");
38+
Console.WriteLine($" P95 execution time: {stats.P95ExecutionTime.TotalMilliseconds:F2} ms");
39+
}
40+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
using IamRoles.Mutations;
2+
using IamRoles.State;
3+
using ModularityKit.Mutator.Abstractions.Context;
4+
using ModularityKit.Mutator.Abstractions.Engine;
5+
6+
namespace IamRoles.Scenarios;
7+
8+
/// <summary>
9+
/// BatchRoleMigrationScenario
10+
///
11+
/// Demonstrates batch migration of IAM roles for multiple users using <see cref="IMutation{TState}"/>
12+
/// and <see cref="GrantUserRoleMutation"/> in a single operation.
13+
///
14+
/// Scenario Details:
15+
/// - Executes multiple <see cref="IMutationEngine.ExecuteBatchAsync{TState}"/> instances in a batch via <see cref="IMutationEngine"/>.
16+
/// - Provides a <see cref="UserPermissionsState"/> with system metadata, including approval info for auditability.
17+
/// - Logs success and failure statistics for each mutation.
18+
/// - Handles policy decisions for each mutation, such as restrictions on role assignments.
19+
/// - Works with <see cref="IMutation{TState}"/> which tracks roles assigned to users.
20+
///
21+
/// Key Steps:
22+
/// 1. Initialize <see cref="IMutationEngine.ExecuteBatchAsync{TState}"/> with initial roles for each user.
23+
/// 2. Construct an array of <see cref="IMutationEngine"/> representing role grants.
24+
/// 3. Execute the batch with <see cref="IMutationEngine"/>.
25+
/// 4. Count successes and failures, and display any policy decisions that blocked mutations.
26+
///
27+
/// Example Use Case:
28+
/// - Performing organizational role restructuring
29+
/// - Batch granting of elevated permissions to multiple users
30+
/// - Ensuring compliance and auditability via system metadata and policy evaluation
31+
///
32+
/// Notes:
33+
/// - Individual mutations in the batch may fail if any policy denies the role change.
34+
/// - Correlation ID or metadata can be used to trace this batch operation in logs.
35+
/// </summary>
36+
internal static class BatchRoleMigrationScenario
37+
{
38+
internal static async Task Run(IMutationEngine engine)
39+
{
40+
Console.WriteLine("\n=== Batch Role Migration Scenario ===");
41+
42+
var state = new UserPermissionsState
43+
{
44+
RolesByUser = new Dictionary<string, HashSet<string>>
45+
{
46+
["alice"] = ["User"],
47+
["bob"] = ["User"],
48+
["carol"] = ["User"]
49+
}
50+
};
51+
52+
var ctx = MutationContext.System(reason: "Org restructure")
53+
with
54+
{
55+
Metadata = new Dictionary<string, object>
56+
{
57+
["approvedBy"] = "security-team"
58+
}
59+
};
60+
61+
var mutations = new IMutation<UserPermissionsState>[]
62+
{
63+
new GrantUserRoleMutation("alice", "Manager", ctx),
64+
new GrantUserRoleMutation("bob", "Admin", ctx),
65+
new GrantUserRoleMutation("carol", "Manager", ctx)
66+
};
67+
68+
var result = await engine.ExecuteBatchAsync(mutations, state);
69+
70+
Console.WriteLine($"Executed: {result.Results.Count}");
71+
Console.WriteLine($"Success: {result.Results.Count(r => r.IsSuccess)}");
72+
Console.WriteLine($"Failed: {result.Results.Count(r => !r.IsSuccess)}");
73+
74+
foreach (var failure in result.Results.Where(r => !r.IsSuccess))
75+
{
76+
Console.WriteLine("✗ Mutation failed:");
77+
foreach (var decision in failure.PolicyDecisions)
78+
Console.WriteLine($" Policy: {decision.PolicyName}{decision.Reason}");
79+
}
80+
}
81+
}

0 commit comments

Comments
 (0)