From c0f437e393eedc3159577ae30197aea89ad8e268 Mon Sep 17 00:00:00 2001 From: Andreas Frisch Date: Tue, 28 Jul 2026 16:23:15 +0200 Subject: [PATCH 1/3] Show only Capability roles by default --- db/seed/RbacRoleGrants.csv | 1 + .../Infrastructure/Api/RBAC/RbacController.cs | 35 ++++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/db/seed/RbacRoleGrants.csv b/db/seed/RbacRoleGrants.csv index 2cf67820..ebd2e128 100644 --- a/db/seed/RbacRoleGrants.csv +++ b/db/seed/RbacRoleGrants.csv @@ -5,3 +5,4 @@ E02D91E6-DE3C-4C55-BC0A-EDABD1492197;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;2026-0 F1F3A53C-3D8E-4D54-BF66-7FB67209B701;36202DFB-D106-440D-8B99-F11BC8D77C9C;2026-07-23T09:15:35.803840;User;owner@bar.com;Capability;bar 706A6EB8-4378-4F66-9D37-BC84601D57F0;2C561A6D-90F4-4649-80B3-76A854A64EA2;2026-07-23T09:15:35.803843;User;contributor@bar.com;Capability;bar 8D0D4C50-2336-4C1C-91D8-7654577F52B3;22DAB91B-C2D8-4840-A173-1416EF1B882D;2026-07-23T09:15:35.803846;User;reader@bar.com;Capability;bar +F1F3A53C-3D8E-4D54-BF66-7FB67209B702;36202DFB-D106-440D-8B99-F11BC8D77C9C;2026-07-23T09:15:35.803840;User;andfris@dfds.com;Capability;cloudengineering-xxx diff --git a/src/SelfService/Infrastructure/Api/RBAC/RbacController.cs b/src/SelfService/Infrastructure/Api/RBAC/RbacController.cs index fe865386..83a1ec78 100644 --- a/src/SelfService/Infrastructure/Api/RBAC/RbacController.cs +++ b/src/SelfService/Infrastructure/Api/RBAC/RbacController.cs @@ -87,9 +87,42 @@ public IActionResult GetAssignablePermissions() [HttpGet("get-assignable-roles")] [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] - public async Task GetAssignableRoles() + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest, "application/problem+json")] + public async Task GetAssignableRoles([FromQuery] string? scope) { + var capabilityRoleNames = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "Owner", + "Contributor", + "Reader", + }; + var roles = await _rbacApplicationService.GetAssignableRoles(); + switch (scope?.Trim().ToLowerInvariant()) + { + case null: + case "": + case "capability": + roles = roles + .Where(r => r.Type == RbacAccessType.Capability || capabilityRoleNames.Contains(r.Name)) + .ToList(); + break; + case "system": + case "global": + roles = roles + .Where(r => r.Type != RbacAccessType.Capability && !capabilityRoleNames.Contains(r.Name)) + .ToList(); + break; + default: + return BadRequest( + new ProblemDetails + { + Title = "Invalid scope", + Detail = "Valid values are capability, system, global, or omitted.", + } + ); + } + List toRbacDTO(List roles) { return roles.Select(role => RbacRoleDTO.FromRbacRole(role)).ToList(); From fe3642b5b6be96932510a01a9b57987de17fe958 Mon Sep 17 00:00:00 2001 From: Andreas Frisch Date: Mon, 3 Aug 2026 15:17:32 +0200 Subject: [PATCH 2/3] Legacy endpoint provide information of users access to third party access per capability --- .../Api/System/IAadAwsSyncCapabilityQuery.cs | 4 +++ .../Queries/AadAwsSyncCapabilityQuery.cs | 34 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/src/SelfService/Infrastructure/Api/System/IAadAwsSyncCapabilityQuery.cs b/src/SelfService/Infrastructure/Api/System/IAadAwsSyncCapabilityQuery.cs index 57096143..e4f6e33a 100644 --- a/src/SelfService/Infrastructure/Api/System/IAadAwsSyncCapabilityQuery.cs +++ b/src/SelfService/Infrastructure/Api/System/IAadAwsSyncCapabilityQuery.cs @@ -24,6 +24,10 @@ public class MemberDto // aad-aws-sync look the user up in Azure AD directly instead of guessing via // email — correct even when the user's UPN differs from their email address. public required string UserId { get; set; } + + // Whether the member has access to modify and interact with third-party services. + // True if their role is Owner or Contributor, false otherwise. + public required bool HasAccessToThirdParty { get; set; } } public class ContextDto diff --git a/src/SelfService/Infrastructure/Persistence/Queries/AadAwsSyncCapabilityQuery.cs b/src/SelfService/Infrastructure/Persistence/Queries/AadAwsSyncCapabilityQuery.cs index f229a09b..2182da96 100644 --- a/src/SelfService/Infrastructure/Persistence/Queries/AadAwsSyncCapabilityQuery.cs +++ b/src/SelfService/Infrastructure/Persistence/Queries/AadAwsSyncCapabilityQuery.cs @@ -19,6 +19,7 @@ public async Task> GetCapabilities() var allMemberships = await GetAllMembershipByCapability(); var emailByUserId = await GetEmailByUserId(); var allAwsAccounts = await GetAllAwsAccountsByCapability(); + var rolesByCapabilityAndUserId = await GetRolesByCapabilityAndUserId(); return from capability in allCapabilities let memberships = allMemberships[capability.Id] @@ -40,6 +41,10 @@ public async Task> GetCapabilities() // 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(), Contexts = awsAccounts @@ -77,4 +82,33 @@ private async Task> GetAllAwsAccountsByCapabil var awsAccounts = await _context.AwsAccounts.ToListAsync(); return awsAccounts.ToLookup(x => x.CapabilityId); } + + private async Task> GetRolesByCapabilityAndUserId() + { + // Fetch all role grants for users with capability-scoped roles + var roleGrants = await _context + .RbacRoleGrants.Where(x => + x.Type == RbacAccessType.Capability && x.AssignedEntityType == AssignedEntityType.User + ) + .ToListAsync(); + + // Fetch all roles + var roles = await _context.RbacRoles.ToListAsync(); + var rolesById = roles.ToDictionary(x => x.Id); + + // Map (capabilityId, userId) -> roleName + var result = new Dictionary<(CapabilityId, UserId), string>(); + + foreach (var grant in roleGrants) + { + if (grant.Resource != null && rolesById.TryGetValue(grant.RoleId, out var role)) + { + var capabilityId = new CapabilityId(grant.Resource); + var userId = new UserId(grant.AssignedEntityId); + result[(capabilityId, userId)] = role.Name; + } + } + + return result; + } } From 8b5a1120fa6af3b45695756c43601ed7e1d8d5e5 Mon Sep 17 00:00:00 2001 From: Andreas Frisch Date: Tue, 4 Aug 2026 10:24:55 +0200 Subject: [PATCH 3/3] parse IDs correctly --- .../Persistence/Queries/AadAwsSyncCapabilityQuery.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/SelfService/Infrastructure/Persistence/Queries/AadAwsSyncCapabilityQuery.cs b/src/SelfService/Infrastructure/Persistence/Queries/AadAwsSyncCapabilityQuery.cs index 2182da96..ef3867e5 100644 --- a/src/SelfService/Infrastructure/Persistence/Queries/AadAwsSyncCapabilityQuery.cs +++ b/src/SelfService/Infrastructure/Persistence/Queries/AadAwsSyncCapabilityQuery.cs @@ -103,8 +103,8 @@ private async Task> GetAllAwsAccountsByCapabil { if (grant.Resource != null && rolesById.TryGetValue(grant.RoleId, out var role)) { - var capabilityId = new CapabilityId(grant.Resource); - var userId = new UserId(grant.AssignedEntityId); + var capabilityId = CapabilityId.CreateFrom(grant.Resource); + var userId = UserId.Parse(grant.AssignedEntityId); result[(capabilityId, userId)] = role.Name; } }