From e1c538c526ee2471d772acd66c7ce04ebb2047e1 Mon Sep 17 00:00:00 2001 From: Andreas Frisch Date: Wed, 5 Aug 2026 16:36:32 +0200 Subject: [PATCH 1/3] mark noncompliant capabilities for complete EntraID cleanup --- .../Queries/TestAadAwsSyncCapabilityQuery.cs | 52 +++++++++++++++++++ .../Api/System/IAadAwsSyncCapabilityQuery.cs | 1 + .../Queries/AadAwsSyncCapabilityQuery.cs | 2 + 3 files changed, 55 insertions(+) create mode 100644 src/SelfService.Tests/Infrastructure/Queries/TestAadAwsSyncCapabilityQuery.cs diff --git a/src/SelfService.Tests/Infrastructure/Queries/TestAadAwsSyncCapabilityQuery.cs b/src/SelfService.Tests/Infrastructure/Queries/TestAadAwsSyncCapabilityQuery.cs new file mode 100644 index 00000000..ba1298d9 --- /dev/null +++ b/src/SelfService.Tests/Infrastructure/Queries/TestAadAwsSyncCapabilityQuery.cs @@ -0,0 +1,52 @@ +using SelfService.Domain.Models; +using SelfService.Infrastructure.Persistence.Queries; + +namespace SelfService.Tests.Infrastructure.Queries; + +public class TestAadAwsSyncCapabilityQuery +{ + [Fact] + [Trait("Category", "InMemoryDatabase")] + public async Task sets_remove_users_from_group_based_on_tag_compliance() + { + using var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + + await using var databaseFactory = new InMemoryDatabaseFactory(); + var dbContext = await databaseFactory.CreateSelfServiceDbContext(); + + var compliantCapability = A.Capability + .WithId(CapabilityId.CreateFrom("compliant-capability")) + .WithName("compliant-capability") + .WithJsonMetadata( + """ + { + "dfds.cost.centre": "1234", + "dfds.businessCapability": "platform", + "dfds.env": "prod", + "dfds.data.classification": "internal", + "dfds.service.criticality": "high", + "dfds.service.availability": "24x7" + } + """ + ) + .Build(); + + var nonCompliantCapability = A.Capability + .WithId(CapabilityId.CreateFrom("non-compliant-capability")) + .WithName("non-compliant-capability") + .WithJsonMetadata("{}") + .Build(); + + await dbContext.Capabilities.AddRangeAsync( + new[] { compliantCapability, nonCompliantCapability }, + cancellationTokenSource.Token + ); + await dbContext.SaveChangesAsync(cancellationTokenSource.Token); + + var sut = new AadAwsSyncCapabilityQuery(dbContext); + var result = (await sut.GetCapabilities()).ToDictionary(x => x.Id); + + Assert.False(result[compliantCapability.Id].RemoveUsersFromGroup); + Assert.True(result[nonCompliantCapability.Id].RemoveUsersFromGroup); + } +} diff --git a/src/SelfService/Infrastructure/Api/System/IAadAwsSyncCapabilityQuery.cs b/src/SelfService/Infrastructure/Api/System/IAadAwsSyncCapabilityQuery.cs index e4f6e33a..8eb46828 100644 --- a/src/SelfService/Infrastructure/Api/System/IAadAwsSyncCapabilityQuery.cs +++ b/src/SelfService/Infrastructure/Api/System/IAadAwsSyncCapabilityQuery.cs @@ -11,6 +11,7 @@ public class CapabilityDto public required string Name { get; set; } public required string RootId { get; set; } public required string Description { get; set; } + public required bool RemoveUsersFromGroup { get; set; } public required MemberDto[] Members { get; set; } public required ContextDto[] Contexts { get; set; } public required string JsonMetadata { get; set; } diff --git a/src/SelfService/Infrastructure/Persistence/Queries/AadAwsSyncCapabilityQuery.cs b/src/SelfService/Infrastructure/Persistence/Queries/AadAwsSyncCapabilityQuery.cs index ef3867e5..7209e0f5 100644 --- a/src/SelfService/Infrastructure/Persistence/Queries/AadAwsSyncCapabilityQuery.cs +++ b/src/SelfService/Infrastructure/Persistence/Queries/AadAwsSyncCapabilityQuery.cs @@ -1,5 +1,6 @@ using Microsoft.EntityFrameworkCore; using SelfService.Domain.Models; +using SelfService.Domain.Services; using SelfService.Infrastructure.Api.System; namespace SelfService.Infrastructure.Persistence.Queries; @@ -30,6 +31,7 @@ public async Task> GetCapabilities() Name = capability.Name, RootId = capability.Id, Description = capability.Description, + RemoveUsersFromGroup = !TagComplianceEvaluator.Evaluate(capability.JsonMetadata).IsCompliant, JsonMetadata = capability.JsonMetadata, Members = memberships .Select(member => new MemberDto From b8f9604f6a2ec98febcbf975284ad89cb7947fda Mon Sep 17 00:00:00 2001 From: Andreas Frisch Date: Wed, 5 Aug 2026 16:49:30 +0200 Subject: [PATCH 2/3] simply remove members from noncompliant capability syncs --- .../Queries/TestAadAwsSyncCapabilityQuery.cs | 26 ++++++++++++-- .../Api/System/IAadAwsSyncCapabilityQuery.cs | 1 - .../Queries/AadAwsSyncCapabilityQuery.cs | 36 ++++++++++--------- 3 files changed, 42 insertions(+), 21 deletions(-) diff --git a/src/SelfService.Tests/Infrastructure/Queries/TestAadAwsSyncCapabilityQuery.cs b/src/SelfService.Tests/Infrastructure/Queries/TestAadAwsSyncCapabilityQuery.cs index ba1298d9..5455b0bc 100644 --- a/src/SelfService.Tests/Infrastructure/Queries/TestAadAwsSyncCapabilityQuery.cs +++ b/src/SelfService.Tests/Infrastructure/Queries/TestAadAwsSyncCapabilityQuery.cs @@ -7,7 +7,7 @@ public class TestAadAwsSyncCapabilityQuery { [Fact] [Trait("Category", "InMemoryDatabase")] - public async Task sets_remove_users_from_group_based_on_tag_compliance() + public async Task returns_empty_members_for_non_compliant_capabilities() { using var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(5)); @@ -37,16 +37,36 @@ public async Task sets_remove_users_from_group_based_on_tag_compliance() .WithJsonMetadata("{}") .Build(); + var compliantMember = A.Member.WithUserId(UserId.Parse("compliant-user")).Build(); + var nonCompliantMember = A.Member.WithUserId(UserId.Parse("non-compliant-user")).Build(); + + var compliantMembership = A.Membership + .WithCapabilityId(compliantCapability.Id) + .WithUserId(compliantMember.Id) + .Build(); + + var nonCompliantMembership = A.Membership + .WithCapabilityId(nonCompliantCapability.Id) + .WithUserId(nonCompliantMember.Id) + .Build(); + await dbContext.Capabilities.AddRangeAsync( new[] { compliantCapability, nonCompliantCapability }, cancellationTokenSource.Token ); + await dbContext.Members.AddRangeAsync(new[] { compliantMember, nonCompliantMember }, cancellationTokenSource.Token); + await dbContext.Memberships.AddRangeAsync( + new[] { compliantMembership, nonCompliantMembership }, + cancellationTokenSource.Token + ); await dbContext.SaveChangesAsync(cancellationTokenSource.Token); var sut = new AadAwsSyncCapabilityQuery(dbContext); var result = (await sut.GetCapabilities()).ToDictionary(x => x.Id); - Assert.False(result[compliantCapability.Id].RemoveUsersFromGroup); - Assert.True(result[nonCompliantCapability.Id].RemoveUsersFromGroup); + Assert.Single(result[compliantCapability.Id].Members); + Assert.Equal(compliantMember.Id.ToString(), result[compliantCapability.Id].Members[0].UserId); + + Assert.Empty(result[nonCompliantCapability.Id].Members); } } diff --git a/src/SelfService/Infrastructure/Api/System/IAadAwsSyncCapabilityQuery.cs b/src/SelfService/Infrastructure/Api/System/IAadAwsSyncCapabilityQuery.cs index 8eb46828..e4f6e33a 100644 --- a/src/SelfService/Infrastructure/Api/System/IAadAwsSyncCapabilityQuery.cs +++ b/src/SelfService/Infrastructure/Api/System/IAadAwsSyncCapabilityQuery.cs @@ -11,7 +11,6 @@ public class CapabilityDto public required string Name { get; set; } public required string RootId { get; set; } public required string Description { get; set; } - public required bool RemoveUsersFromGroup { get; set; } public required MemberDto[] Members { get; set; } public required ContextDto[] Contexts { get; set; } public required string JsonMetadata { get; set; } diff --git a/src/SelfService/Infrastructure/Persistence/Queries/AadAwsSyncCapabilityQuery.cs b/src/SelfService/Infrastructure/Persistence/Queries/AadAwsSyncCapabilityQuery.cs index 7209e0f5..574e7d9e 100644 --- a/src/SelfService/Infrastructure/Persistence/Queries/AadAwsSyncCapabilityQuery.cs +++ b/src/SelfService/Infrastructure/Persistence/Queries/AadAwsSyncCapabilityQuery.cs @@ -25,30 +25,32 @@ public async Task> GetCapabilities() return from capability in allCapabilities let memberships = allMemberships[capability.Id] let awsAccounts = allAwsAccounts[capability.Id] + let isTagCompliant = TagComplianceEvaluator.Evaluate(capability.JsonMetadata).IsCompliant select new CapabilityDto { Id = capability.Id, Name = capability.Name, RootId = capability.Id, Description = capability.Description, - RemoveUsersFromGroup = !TagComplianceEvaluator.Evaluate(capability.JsonMetadata).IsCompliant, JsonMetadata = capability.JsonMetadata, - Members = memberships - .Select(member => new MemberDto - { - // Membership.UserId is the member's id; for service principals this is the - // Azure object id, so resolve the (synthetic) email from the Member record. - // Fall back to UserId for memberships without a matching Member row. - Email = emailByUserId.GetValueOrDefault(member.UserId, member.UserId), - // UserId is the authoritative identifier (the UPN for regular users); - // aad-aws-sync uses it to resolve the user in Azure AD directly. - UserId = member.UserId.ToString(), - // User has access to third-party services if their role is Owner or Contributor - HasAccessToThirdParty = - rolesByCapabilityAndUserId.TryGetValue((capability.Id, member.UserId), out var role) - && (role == "Owner" || role == "Contributor"), - }) - .ToArray(), + Members = isTagCompliant + ? memberships + .Select(member => new MemberDto + { + // Membership.UserId is the member's id; for service principals this is the + // Azure object id, so resolve the (synthetic) email from the Member record. + // Fall back to UserId for memberships without a matching Member row. + Email = emailByUserId.GetValueOrDefault(member.UserId, member.UserId), + // UserId is the authoritative identifier (the UPN for regular users); + // aad-aws-sync uses it to resolve the user in Azure AD directly. + UserId = member.UserId.ToString(), + // User has access to third-party services if their role is Owner or Contributor + HasAccessToThirdParty = + rolesByCapabilityAndUserId.TryGetValue((capability.Id, member.UserId), out var role) + && (role == "Owner" || role == "Contributor"), + }) + .ToArray() + : Array.Empty(), Contexts = awsAccounts .Select(context => new ContextDto { From a64ed2d9aad5fce9251838835a4578b365380fe0 Mon Sep 17 00:00:00 2001 From: Andreas Frisch Date: Tue, 1 Sep 2026 09:42:34 +0200 Subject: [PATCH 3/3] format --- .../Queries/TestAadAwsSyncCapabilityQuery.cs | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/SelfService.Tests/Infrastructure/Queries/TestAadAwsSyncCapabilityQuery.cs b/src/SelfService.Tests/Infrastructure/Queries/TestAadAwsSyncCapabilityQuery.cs index 5455b0bc..bdd185ec 100644 --- a/src/SelfService.Tests/Infrastructure/Queries/TestAadAwsSyncCapabilityQuery.cs +++ b/src/SelfService.Tests/Infrastructure/Queries/TestAadAwsSyncCapabilityQuery.cs @@ -14,8 +14,8 @@ public async Task returns_empty_members_for_non_compliant_capabilities() await using var databaseFactory = new InMemoryDatabaseFactory(); var dbContext = await databaseFactory.CreateSelfServiceDbContext(); - var compliantCapability = A.Capability - .WithId(CapabilityId.CreateFrom("compliant-capability")) + var compliantCapability = A + .Capability.WithId(CapabilityId.CreateFrom("compliant-capability")) .WithName("compliant-capability") .WithJsonMetadata( """ @@ -31,8 +31,8 @@ public async Task returns_empty_members_for_non_compliant_capabilities() ) .Build(); - var nonCompliantCapability = A.Capability - .WithId(CapabilityId.CreateFrom("non-compliant-capability")) + var nonCompliantCapability = A + .Capability.WithId(CapabilityId.CreateFrom("non-compliant-capability")) .WithName("non-compliant-capability") .WithJsonMetadata("{}") .Build(); @@ -40,13 +40,13 @@ public async Task returns_empty_members_for_non_compliant_capabilities() var compliantMember = A.Member.WithUserId(UserId.Parse("compliant-user")).Build(); var nonCompliantMember = A.Member.WithUserId(UserId.Parse("non-compliant-user")).Build(); - var compliantMembership = A.Membership - .WithCapabilityId(compliantCapability.Id) + var compliantMembership = A + .Membership.WithCapabilityId(compliantCapability.Id) .WithUserId(compliantMember.Id) .Build(); - var nonCompliantMembership = A.Membership - .WithCapabilityId(nonCompliantCapability.Id) + var nonCompliantMembership = A + .Membership.WithCapabilityId(nonCompliantCapability.Id) .WithUserId(nonCompliantMember.Id) .Build(); @@ -54,7 +54,10 @@ await dbContext.Capabilities.AddRangeAsync( new[] { compliantCapability, nonCompliantCapability }, cancellationTokenSource.Token ); - await dbContext.Members.AddRangeAsync(new[] { compliantMember, nonCompliantMember }, cancellationTokenSource.Token); + await dbContext.Members.AddRangeAsync( + new[] { compliantMember, nonCompliantMember }, + cancellationTokenSource.Token + ); await dbContext.Memberships.AddRangeAsync( new[] { compliantMembership, nonCompliantMembership }, cancellationTokenSource.Token