Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions db/seed/RbacRoleGrants.csv
Original file line number Diff line number Diff line change
Expand Up @@ -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
35 changes: 34 additions & 1 deletion src/SelfService/Infrastructure/Api/RBAC/RbacController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,42 @@ public IActionResult GetAssignablePermissions()

[HttpGet("get-assignable-roles")]
[ProducesResponseType(typeof(List<RbacRoleDTO>), StatusCodes.Status200OK)]
public async Task<IActionResult> GetAssignableRoles()
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest, "application/problem+json")]
public async Task<IActionResult> GetAssignableRoles([FromQuery] string? scope)
{
var capabilityRoleNames = new HashSet<string>(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<RbacRoleDTO> toRbacDTO(List<RbacRole> roles)
{
return roles.Select(role => RbacRoleDTO.FromRbacRole(role)).ToList();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public async Task<IEnumerable<CapabilityDto>> 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]
Expand All @@ -40,6 +41,10 @@ public async Task<IEnumerable<CapabilityDto>> 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
Expand Down Expand Up @@ -77,4 +82,33 @@ private async Task<ILookup<CapabilityId, AwsAccount>> GetAllAwsAccountsByCapabil
var awsAccounts = await _context.AwsAccounts.ToListAsync();
return awsAccounts.ToLookup(x => x.CapabilityId);
}

private async Task<Dictionary<(CapabilityId, UserId), string>> 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 = CapabilityId.CreateFrom(grant.Resource);
var userId = UserId.Parse(grant.AssignedEntityId);
result[(capabilityId, userId)] = role.Name;
}
}

return result;
}
}
Loading