From a8e201ab22b11666c842d8215549c30ce1235754 Mon Sep 17 00:00:00 2001 From: Andreas Frisch Date: Wed, 22 Jul 2026 15:17:29 +0200 Subject: [PATCH 1/9] Use Global CE permissions from RBAC... and others --- .../Application/TestRbacApplicationService.cs | 1 + .../Application/RbacApplicationService.cs | 123 ++- .../Domain/Models/RbacNamespace.cs | 10 +- .../Domain/Services/AuthorizationService.cs | 832 +++++++----------- .../Domain/Services/ECRRepositoryService.cs | 2 +- .../Domain/Services/IAuthorizationService.cs | 2 +- .../Api/Capabilities/CapabilityController.cs | 4 +- .../Api/Catalog/CatalogController.cs | 6 +- tools/config.json | 271 +++++- tools/config_skeleton.json | 283 +++++- tools/setup-baseline-permissions.go | 253 +++--- 11 files changed, 1069 insertions(+), 718 deletions(-) diff --git a/src/SelfService.Tests/Application/TestRbacApplicationService.cs b/src/SelfService.Tests/Application/TestRbacApplicationService.cs index e38afb49..7bc7b4ed 100644 --- a/src/SelfService.Tests/Application/TestRbacApplicationService.cs +++ b/src/SelfService.Tests/Application/TestRbacApplicationService.cs @@ -758,4 +758,5 @@ await rbacSvc.IsUserPermitted( ); */ } + } diff --git a/src/SelfService/Application/RbacApplicationService.cs b/src/SelfService/Application/RbacApplicationService.cs index 80b0f7cc..bad56ace 100644 --- a/src/SelfService/Application/RbacApplicationService.cs +++ b/src/SelfService/Application/RbacApplicationService.cs @@ -35,12 +35,6 @@ IRbacRoleRepository roleRepository _cache = new RbacCache(); } - /* - [Note 2025-09-18 by andfris] - The permission checks in this service are commented out for now, as they interfere with bootstrapping - and with the Cloud Engineer role which is supposed to have blanket permissions. - */ - public async Task IsUserPermitted(string user, List permissions, string objectId) { var resp = new PermittedResponse(); @@ -838,6 +832,8 @@ public class Permission public RbacNamespace Namespace { get; set; } = RbacNamespace.Default; public RbacAccessType AccessType { get; set; } = RbacAccessType.Capability; + // Canonical catalog of known RBAC permissions exposed by the application. + // The database stores the actual permission grants (who/what has which permission and scope). public static List BootstrapPermissions() { var permissions = new List @@ -938,12 +934,127 @@ public static List BootstrapPermissions() new(RbacNamespace.Rbac, "create", "Manage RBAC", RbacAccessType.Global), new(RbacNamespace.Rbac, "update", "Manage RBAC", RbacAccessType.Global), new(RbacNamespace.Rbac, "delete", "Manage RBAC", RbacAccessType.Global), + new( + RbacNamespace.ServiceCatalogue, + "read", + "Read service catalogue resources", + RbacAccessType.Global + ), new( RbacNamespace.SystemLegacy, "read", "Read legacy system data (e.g. AAD-AWS sync capability list)", RbacAccessType.Global ), + new( + RbacNamespace.SystemAdmin, + "view-deleted-capabilities", + "View deleted capabilities", + RbacAccessType.Global + ), + new( + RbacNamespace.SystemAdmin, + "unset-capability-tags", + "Unset capability tags", + RbacAccessType.Global + ), + new( + RbacNamespace.SystemAdmin, + "create-demo-recording", + "Create demo recordings", + RbacAccessType.Global + ), + new( + RbacNamespace.SystemAdmin, + "update-demo-recording", + "Update demo recordings", + RbacAccessType.Global + ), + new( + RbacNamespace.SystemAdmin, + "delete-demo-recording", + "Delete demo recordings", + RbacAccessType.Global + ), + new( + RbacNamespace.SystemAdmin, + "manage-permission-matrix", + "Manage permission matrix", + RbacAccessType.Global + ), + new( + RbacNamespace.SystemAdmin, + "synchronize-aws-ecr-and-database-ecr", + "Synchronize AWS ECR and database ECR", + RbacAccessType.Global + ), + new( + RbacNamespace.SystemAdmin, + "bypass-membership-approvals", + "Bypass membership approvals", + RbacAccessType.Global + ), + new( + RbacNamespace.SystemAdmin, + "manage-self-assessment-options", + "Manage self-assessment options", + RbacAccessType.Global + ), + new( + RbacNamespace.SystemAdmin, + "create-release-notes", + "Create release notes", + RbacAccessType.Global + ), + new( + RbacNamespace.SystemAdmin, + "update-release-note", + "Update release note", + RbacAccessType.Global + ), + new( + RbacNamespace.SystemAdmin, + "toggle-release-note-is-active", + "Toggle release note active state", + RbacAccessType.Global + ), + new( + RbacNamespace.SystemAdmin, + "list-draft-release-notes", + "List draft release notes", + RbacAccessType.Global + ), + new( + RbacNamespace.SystemAdmin, + "remove-release-note", + "Remove release note", + RbacAccessType.Global + ), + new(RbacNamespace.SystemAdmin, "create-event", "Create events", RbacAccessType.Global), + new(RbacNamespace.SystemAdmin, "update-event", "Update events", RbacAccessType.Global), + new(RbacNamespace.SystemAdmin, "delete-event", "Delete events", RbacAccessType.Global), + new(RbacNamespace.SystemAdmin, "create-news-item", "Create news items", RbacAccessType.Global), + new(RbacNamespace.SystemAdmin, "update-news-item", "Update news items", RbacAccessType.Global), + new(RbacNamespace.SystemAdmin, "delete-news-item", "Delete news items", RbacAccessType.Global), + new(RbacNamespace.SystemAdmin, "get-user-emails", "Get user emails", RbacAccessType.Global), + new( + RbacNamespace.CapabilityManagement, + "batch-create-capabilities", + "Create capabilities in batch as administrator", + RbacAccessType.Global + ), + new( + RbacNamespace.SystemAdmin, + "delete-membership-application-as-admin", + "Delete membership applications as administrator", + RbacAccessType.Global + ), + new( + RbacNamespace.SystemAdmin, + "retry-creating-message-contract", + "Retry failed message contract creation as administrator", + RbacAccessType.Global + ), }; return permissions; diff --git a/src/SelfService/Domain/Models/RbacNamespace.cs b/src/SelfService/Domain/Models/RbacNamespace.cs index 7fd59bc4..d7303654 100644 --- a/src/SelfService/Domain/Models/RbacNamespace.cs +++ b/src/SelfService/Domain/Models/RbacNamespace.cs @@ -6,7 +6,7 @@ namespace SelfService.Domain.Models; [JsonConverter(typeof(RbacNamespaceJsonConverter))] public class RbacNamespace : ValueObject { - // topics, capability-management, capability-membership-management, tags-and-metadata, aws, finout, azure, rbac, system-legacy + // topics, capability-management, capability-membership-management, tags-and-metadata, aws, finout, azure, rbac, service-catalogue, system-admin, system-legacy public static readonly RbacNamespace Topics = new("topics"); public static readonly RbacNamespace TopicsPublic = new("topics-public"); public static readonly RbacNamespace CapabilityManagement = new("capability-management"); @@ -16,6 +16,8 @@ public class RbacNamespace : ValueObject public static readonly RbacNamespace Finout = new("finout"); public static readonly RbacNamespace Azure = new("azure"); public static readonly RbacNamespace Rbac = new("rbac"); + public static readonly RbacNamespace ServiceCatalogue = new("service-catalogue"); + public static readonly RbacNamespace SystemAdmin = new("system-admin"); public static readonly RbacNamespace SystemLegacy = new("system-legacy"); // allow non-optional values. Cannot be created and has no permissions. @@ -79,6 +81,12 @@ public static bool TryParse(string input, out RbacNamespace rbacNamespace) case "rbac": rbacNamespace = Rbac; break; + case "service-catalogue": + rbacNamespace = ServiceCatalogue; + break; + case "system-admin": + rbacNamespace = SystemAdmin; + break; case "system-legacy": rbacNamespace = SystemLegacy; break; diff --git a/src/SelfService/Domain/Services/AuthorizationService.cs b/src/SelfService/Domain/Services/AuthorizationService.cs index f27a140b..3e5900e6 100644 --- a/src/SelfService/Domain/Services/AuthorizationService.cs +++ b/src/SelfService/Domain/Services/AuthorizationService.cs @@ -44,29 +44,17 @@ IRbacApplicationService rbacApplicationService public async Task CanAddTopic(UserId userId, CapabilityId capabilityId, KafkaClusterId clusterId) { - var isMemberOfOwningCapability = await _membershipQuery.HasActiveMembership(userId, capabilityId); - - /* - var canCreateTopics = ( - await _rbacApplicationService.IsUserPermitted( - userId, - new List - { - new() - { - Namespace = RbacNamespace.Topics, - Name = "create", - AccessType = RbacAccessType.Capability, - }, - }, - capabilityId - ) - ).Permitted(); - */ + var canCreateTopics = await HasPermission( + userId, + RbacAccessType.Capability, + RbacNamespace.Topics, + "create", + capabilityId + ); var hasClusterAccess = (await _kafkaClusterAccessRepository.FindBy(capabilityId, clusterId))?.IsAccessGranted ?? false; - return hasClusterAccess && isMemberOfOwningCapability; // canCreateTopics; + return hasClusterAccess && canCreateTopics; } public async Task CanReadTopic(PortalUser portalUser, KafkaTopic kafkaTopic) @@ -76,144 +64,155 @@ public async Task CanReadTopic(PortalUser portalUser, KafkaTopic kafkaTopi return true; } - return ( - await _rbacApplicationService.IsUserPermitted( - portalUser.Id, - new List - { - new() - { - Namespace = RbacNamespace.Topics, - Name = "read-private", - AccessType = RbacAccessType.Capability, - }, - }, - kafkaTopic.CapabilityId - ) - ).Permitted(); + return await HasPermission( + portalUser.Id, + RbacAccessType.Capability, + RbacNamespace.Topics, + "read-private", + kafkaTopic.CapabilityId + ); } public async Task CanModifyTopic(PortalUser portalUser, KafkaTopic kafkaTopic) { - return ( - await _rbacApplicationService.IsUserPermitted( - portalUser.Id, - new List - { - new() - { - Namespace = RbacNamespace.Topics, - Name = "update", - AccessType = RbacAccessType.Capability, - }, - }, - kafkaTopic.CapabilityId - ) - ).Permitted(); + return await HasPermission( + portalUser.Id, + RbacAccessType.Capability, + RbacNamespace.Topics, + "update", + kafkaTopic.CapabilityId + ); } public async Task CanDeleteTopic(PortalUser portalUser, KafkaTopic kafkaTopic) { if (kafkaTopic.IsPublic) { - return ( - await _rbacApplicationService.IsUserPermitted( - portalUser.Id, - new List - { - new() - { - Namespace = RbacNamespace.TopicsPublic, - Name = "delete", - AccessType = RbacAccessType.Capability, - }, - }, - kafkaTopic.CapabilityId - ) - ).Permitted(); - } - return ( - await _rbacApplicationService.IsUserPermitted( + return await HasPermission( portalUser.Id, - new List - { - new() - { - Namespace = RbacNamespace.Topics, - Name = "delete", - AccessType = RbacAccessType.Capability, - }, - }, + RbacAccessType.Capability, + RbacNamespace.TopicsPublic, + "delete", kafkaTopic.CapabilityId - ) - ).Permitted(); + ); + } + + return await HasPermission( + portalUser.Id, + RbacAccessType.Capability, + RbacNamespace.Topics, + "delete", + kafkaTopic.CapabilityId + ); } public bool CanViewDeletedCapabilities(PortalUser portalUser) { - return IsCloudEngineerEnabled(portalUser); + return IsCloudEngineerEnabled(portalUser, "view-deleted-capabilities"); } public bool CanUnsetCapabilityTags(PortalUser portalUser) { - return IsCloudEngineerEnabled(portalUser); + return IsCloudEngineerEnabled(portalUser, "unset-capability-tags"); } public bool CanCreateDemoRecording(PortalUser portalUser) { - return IsCloudEngineerEnabled(portalUser); + return IsCloudEngineerEnabled(portalUser, "create-demo-recording"); } public bool CanUpdateDemoRecording(PortalUser portalUser) { - return IsCloudEngineerEnabled(portalUser); + return IsCloudEngineerEnabled(portalUser, "update-demo-recording"); } public bool CanDeleteDemoRecording(PortalUser portalUser) { - return IsCloudEngineerEnabled(portalUser); + return IsCloudEngineerEnabled(portalUser, "delete-demo-recording"); } - private bool IsCloudEngineerEnabled(PortalUser portalUser) + private bool IsCloudEngineerEnabled(PortalUser portalUser, string permissionName) { - if (portalUser.Roles.Any(role => role == UserRole.CloudEngineer)) + if ( + _httpContextAccessor.HttpContext != null + && _httpContextAccessor.HttpContext.Items.ContainsKey("userPermissions") + ) { - if ( - _httpContextAccessor.HttpContext != null - && !_httpContextAccessor.HttpContext.Items.ContainsKey("userPermissions") - ) - { - return true; - } + return false; } - return false; + return HasPermission(portalUser, RbacAccessType.Global, RbacNamespace.SystemAdmin, permissionName); } - public async Task CanReadConsumers(PortalUser portalUser, KafkaTopic kafkaTopic) + private bool HasPermission( + PortalUser portalUser, + RbacAccessType scope, + RbacNamespace permissionNamespace, + string permissionName, + string resourceId = "" + ) { - if (kafkaTopic.IsPublic) + try { - return true; + return HasPermission(portalUser.Id, scope, permissionNamespace, permissionName, resourceId) + .GetAwaiter() + .GetResult(); + } + catch (Exception ex) + { + _logger.LogWarning( + ex, + "Failed to evaluate permission {Scope} {Namespace}/{PermissionName} for user {UserId}", + scope, + permissionNamespace, + permissionName, + portalUser.Id + ); + return false; } + } + private async Task HasPermission( + string userId, + RbacAccessType scope, + RbacNamespace permissionNamespace, + string permissionName, + string resourceId = "" + ) + { return ( await _rbacApplicationService.IsUserPermitted( - portalUser.Id, + userId, new List { new() { - Namespace = RbacNamespace.Topics, - Name = "read-private", - AccessType = RbacAccessType.Capability, + Namespace = permissionNamespace, + Name = permissionName, + AccessType = scope, }, }, - kafkaTopic.CapabilityId + resourceId ) ).Permitted(); } + public async Task CanReadConsumers(PortalUser portalUser, KafkaTopic kafkaTopic) + { + if (kafkaTopic.IsPublic) + { + return true; + } + + return await HasPermission( + portalUser.Id, + RbacAccessType.Capability, + RbacNamespace.Topics, + "read-private", + kafkaTopic.CapabilityId + ); + } + public async Task CanReadMessageContracts(PortalUser portalUser, KafkaTopic kafkaTopic) { if (kafkaTopic.IsPublic) @@ -221,40 +220,24 @@ public async Task CanReadMessageContracts(PortalUser portalUser, KafkaTopi return true; } - return ( - await _rbacApplicationService.IsUserPermitted( - portalUser.Id, - new List - { - new() - { - Namespace = RbacNamespace.Topics, - Name = "read-private", - AccessType = RbacAccessType.Capability, - }, - }, - kafkaTopic.CapabilityId - ) - ).Permitted(); + return await HasPermission( + portalUser.Id, + RbacAccessType.Capability, + RbacNamespace.Topics, + "read-private", + kafkaTopic.CapabilityId + ); } public async Task CanAddMessageContract(PortalUser portalUser, KafkaTopic kafkaTopic) { - return ( - await _rbacApplicationService.IsUserPermitted( - portalUser.Id, - new List - { - new() - { - Namespace = RbacNamespace.Topics, - Name = "update", - AccessType = RbacAccessType.Capability, - }, - }, - kafkaTopic.CapabilityId - ) - ).Permitted(); + return await HasPermission( + portalUser.Id, + RbacAccessType.Capability, + RbacNamespace.Topics, + "update", + kafkaTopic.CapabilityId + ); } // we don't have information about Kafka clusters in the RBAC system @@ -273,87 +256,49 @@ public async Task HasKafkaClusterAccess(CapabilityId capabilityId, KafkaCl public async Task CanReadMembershipApplications(UserId userId, MembershipApplication application) { - var isMemberOfOwningCapability = await _membershipQuery.HasActiveMembership(userId, application.CapabilityId); var ownsApplication = application.Applicant == userId; - /* - return ( - await _rbacApplicationService.IsUserPermitted( - userId, - new List - { - new() - { - Namespace = RbacNamespace.CapabilityMembershipManagement, - Name = "read-requests", - AccessType = RbacAccessType.Capability, - }, - }, - application.CapabilityId - ) - ).Permitted(); - */ - return isMemberOfOwningCapability || ownsApplication; + var hasReadRequestsPermission = await HasPermission( + userId, + RbacAccessType.Capability, + RbacNamespace.CapabilityMembershipManagement, + "read-requests", + application.CapabilityId + ); + + return ownsApplication || hasReadRequestsPermission; } public async Task CanApproveMembershipApplications(UserId userId, MembershipApplication application) { - return ( - await _rbacApplicationService.IsUserPermitted( - userId, - new List - { - new() - { - Namespace = RbacNamespace.CapabilityMembershipManagement, - Name = "manage-requests", - AccessType = RbacAccessType.Capability, - }, - }, - application.CapabilityId - ) - ).Permitted(); + return await HasPermission( + userId, + RbacAccessType.Capability, + RbacNamespace.CapabilityMembershipManagement, + "manage-requests", + application.CapabilityId + ); } public async Task CanApproveMembershipApplications(UserId userId, CapabilityId capabilityId) { - var isMemberOfOwningCapability = await _membershipQuery.HasActiveMembership(userId, capabilityId); - /* - return ( - await _rbacApplicationService.IsUserPermitted( - userId, - new List - { - new() - { - Namespace = RbacNamespace.CapabilityMembershipManagement, - Name = "manage-requests", - AccessType = RbacAccessType.Capability, - }, - }, - capabilityId - ) - ).Permitted(); - */ - return isMemberOfOwningCapability; + return await HasPermission( + userId, + RbacAccessType.Capability, + RbacNamespace.CapabilityMembershipManagement, + "manage-requests", + capabilityId + ); } public async Task CanViewAwsAccount(UserId userId, CapabilityId capabilityId) { - var canReadAwsAccount = ( - await _rbacApplicationService.IsUserPermitted( - userId, - new List - { - new() - { - Namespace = RbacNamespace.Aws, - Name = "read", - AccessType = RbacAccessType.Capability, - }, - }, - capabilityId - ) - ).Permitted(); + var canReadAwsAccount = await HasPermission( + userId, + RbacAccessType.Capability, + RbacNamespace.Aws, + "read", + capabilityId + ); return (await _awsAccountRepository.Exists(capabilityId)) && canReadAwsAccount; } @@ -365,21 +310,13 @@ public async Task CanViewAwsAccount(UserId userId, AwsAccountId accountId) { return false; } - return ( - await _rbacApplicationService.IsUserPermitted( - userId, - new List - { - new() - { - Namespace = RbacNamespace.Aws, - Name = "read", - AccessType = RbacAccessType.Capability, - }, - }, - account.CapabilityId - ) - ).Permitted(); + return await HasPermission( + userId, + RbacAccessType.Capability, + RbacNamespace.Aws, + "read", + account.CapabilityId + ); } public async Task CanViewAwsAccountInformation(UserId userId, CapabilityId capabilityId) @@ -393,112 +330,53 @@ public async Task CanViewAwsAccountInformation(UserId userId, CapabilityId return false; } - return ( - await _rbacApplicationService.IsUserPermitted( - userId, - new List - { - new() - { - Namespace = RbacNamespace.Aws, - Name = "read", - AccessType = RbacAccessType.Capability, - }, - }, - account.CapabilityId - ) - ).Permitted(); + return await HasPermission( + userId, + RbacAccessType.Capability, + RbacNamespace.Aws, + "read", + account.CapabilityId + ); } public async Task CanRequestAwsAccount(UserId userId, CapabilityId capabilityId) { - var isMemberOfOwningCapability = await _membershipQuery.HasActiveMembership(userId, capabilityId); - var canCreateAwsAccount = isMemberOfOwningCapability; - /* - var canCreateAwsAccount = ( - await _rbacApplicationService.IsUserPermitted( - userId, - new List - { - new() - { - Namespace = RbacNamespace.Aws, - Name = "create", - AccessType = RbacAccessType.Capability, - }, - }, - capabilityId - ) - ).Permitted(); - */ + var canCreateAwsAccount = await HasPermission( + userId, + RbacAccessType.Capability, + RbacNamespace.Aws, + "create", + capabilityId + ); + return (!await _awsAccountRepository.Exists(capabilityId)) && canCreateAwsAccount; } public async Task CanViewAzureResources(UserId userId, CapabilityId capabilityId) { - return ( - await _rbacApplicationService.IsUserPermitted( - userId, - new List - { - new() - { - Namespace = RbacNamespace.Azure, - Name = "read", - AccessType = RbacAccessType.Capability, - }, - }, - capabilityId - ) - ).Permitted(); + return await HasPermission(userId, RbacAccessType.Capability, RbacNamespace.Azure, "read", capabilityId); } public async Task CanRequestAzureResource(UserId userId, CapabilityId capabilityId, string environment) { - // should we use the environment for anything? It is already checked for before calling this function - // as is we could consolidate the two 'identical' CanRequestAzureResource(s) methods - var isMemberOfOwningCapability = await _membershipQuery.HasActiveMembership(userId, capabilityId); - return isMemberOfOwningCapability; - /* - return ( - await _rbacApplicationService.IsUserPermitted( - userId, - new List - { - new() - { - Namespace = RbacNamespace.Azure, - Name = "create", - AccessType = RbacAccessType.Capability, - }, - }, - capabilityId - ) - ).Permitted(); - */ + return await HasPermission( + userId, + RbacAccessType.Capability, + RbacNamespace.Azure, + "create", + capabilityId + ); } public async Task CanRequestAzureResources(UserId userId, CapabilityId capabilityId) { - var isMemberOfOwningCapability = await _membershipQuery.HasActiveMembership(userId, capabilityId); - return isMemberOfOwningCapability; - /* - return ( - await _rbacApplicationService.IsUserPermitted( - userId, - new List - { - new() - { - Namespace = RbacNamespace.Azure, - Name = "create", - AccessType = RbacAccessType.Capability, - }, - }, - capabilityId - ) - ).Permitted(); - */ + return await HasPermission( + userId, + RbacAccessType.Capability, + RbacNamespace.Azure, + "create", + capabilityId + ); } public async Task CanLeave(UserId userId, CapabilityId capabilityId) @@ -538,136 +416,83 @@ public async Task CanApply(UserId userId, CapabilityId capabilityId) public async Task CanViewAllApplications(UserId userId, CapabilityId capabilityId) { - var isMemberOfOwningCapability = await _membershipQuery.HasActiveMembership(userId, capabilityId); - /* - return ( - await _rbacApplicationService.IsUserPermitted( - userId, - new List - { - new() - { - Namespace = RbacNamespace.CapabilityMembershipManagement, - Name = "read-requests", - AccessType = RbacAccessType.Capability, - }, - }, - capabilityId - ) - ).Permitted(); - */ - return isMemberOfOwningCapability; + return await HasPermission( + userId, + RbacAccessType.Capability, + RbacNamespace.CapabilityMembershipManagement, + "read-requests", + capabilityId + ); } public async Task CanDeleteCapability(UserId userId, CapabilityId capabilityId) { - return ( - await _rbacApplicationService.IsUserPermitted( - userId, - new List - { - new() - { - Namespace = RbacNamespace.CapabilityManagement, - Name = "request-deletion", - AccessType = RbacAccessType.Capability, - }, - }, - capabilityId - ) - ).Permitted(); + return await HasPermission( + userId, + RbacAccessType.Capability, + RbacNamespace.CapabilityManagement, + "request-deletion", + capabilityId + ); } public bool CanManagePermissionMatrix(PortalUser portalUser) { - return IsCloudEngineerEnabled(portalUser); + return IsCloudEngineerEnabled(portalUser, "manage-permission-matrix"); } public bool CanSynchronizeAwsECRAndDatabaseECR(PortalUser portalUser) { - return IsCloudEngineerEnabled(portalUser); + return IsCloudEngineerEnabled(portalUser, "synchronize-aws-ecr-and-database-ecr"); } public async Task CanGetCapabilityJsonMetadata(PortalUser portalUser, CapabilityId capabilityId) { - var hasPermission = ( - await _rbacApplicationService.IsUserPermitted( - portalUser.Id, - new List - { - new() - { - Namespace = RbacNamespace.TagsAndMetadata, - Name = "read", - AccessType = RbacAccessType.Capability, - }, - }, - capabilityId - ) - ).Permitted(); + var hasPermission = await HasPermission( + portalUser.Id, + RbacAccessType.Capability, + RbacNamespace.TagsAndMetadata, + "read", + capabilityId + ); return hasPermission; } public async Task CanSetCapabilityJsonMetadata(PortalUser portalUser, CapabilityId capabilityId) { - var hasCreatePermission = ( - await _rbacApplicationService.IsUserPermitted( - portalUser.Id, - new List - { - new() - { - Namespace = RbacNamespace.TagsAndMetadata, - Name = "create", - AccessType = RbacAccessType.Capability, - }, - }, - capabilityId - ) - ).Permitted(); - - var hasUpdatePermission = ( - await _rbacApplicationService.IsUserPermitted( - portalUser.Id, - new List - { - new() - { - Namespace = RbacNamespace.TagsAndMetadata, - Name = "update", - AccessType = RbacAccessType.Capability, - }, - }, - capabilityId - ) - ).Permitted(); + var hasCreatePermission = await HasPermission( + portalUser.Id, + RbacAccessType.Capability, + RbacNamespace.TagsAndMetadata, + "create", + capabilityId + ); + + var hasUpdatePermission = await HasPermission( + portalUser.Id, + RbacAccessType.Capability, + RbacNamespace.TagsAndMetadata, + "update", + capabilityId + ); return hasCreatePermission || hasUpdatePermission; } public bool CanBypassMembershipApprovals(PortalUser portalUser) { - return IsCloudEngineerEnabled(portalUser); + return IsCloudEngineerEnabled(portalUser, "bypass-membership-approvals"); } - /* - * This is a temporary solution to allow certain users to create capabilities in batch until we have a proper RBAC solution in place for this. - * User ids are extracted from Azure AD - */ - private static readonly IReadOnlySet BatchCapabilityAllowList = new HashSet( - StringComparer.OrdinalIgnoreCase - ) + public bool CanBatchCreateCapabilities(PortalUser portalUser) { - "noesimo@dfds.com", - "jakstr.ptr@partner.dfds.com", - "joakkei@dfds.com", - "jonlars@dfds.com", - }; - - public bool CanBatchCreateCapabilities(PortalUser portalUser, UserId userId) - { - return BatchCapabilityAllowList.Contains(userId.ToString()) || IsCloudEngineerEnabled(portalUser); + return HasPermission( + portalUser, + RbacAccessType.Global, + RbacNamespace.CapabilityManagement, + "batch-create-capabilities" + ); } public async Task CanDeleteMembershipApplication( @@ -677,100 +502,62 @@ MembershipApplicationId membershipApplicationId ) { var membershipApp = await _membershipApplicationRepository.Get(membershipApplicationId); - var hasPermission = ( - await _rbacApplicationService.IsUserPermitted( - portalUser.Id, - new List - { - new() - { - Namespace = RbacNamespace.CapabilityMembershipManagement, - Name = "manage-requests", - AccessType = RbacAccessType.Capability, - }, - }, - membershipApp.CapabilityId - ) - ).Permitted(); + var hasPermission = await HasPermission( + portalUser.Id, + RbacAccessType.Capability, + RbacNamespace.CapabilityMembershipManagement, + "manage-requests", + membershipApp.CapabilityId + ); var isApplicant = membershipApp.Applicant == userId; - return isApplicant || hasPermission || IsCloudEngineerEnabled(portalUser); + return isApplicant + || hasPermission + || IsCloudEngineerEnabled(portalUser, "delete-membership-application-as-admin"); } public async Task CanRemoveMember(UserId requesterId, CapabilityId capabilityId) { - return ( - await _rbacApplicationService.IsUserPermitted( - requesterId, - new List - { - new() - { - Namespace = RbacNamespace.CapabilityMembershipManagement, - Name = "delete", - AccessType = RbacAccessType.Capability, - }, - }, - capabilityId - ) - ).Permitted(); + return await HasPermission( + requesterId, + RbacAccessType.Capability, + RbacNamespace.CapabilityMembershipManagement, + "delete", + capabilityId + ); } public async Task CanInviteToCapability(UserId userId, CapabilityId capabilityId) { - return ( - await _rbacApplicationService.IsUserPermitted( - userId, - new List - { - new() - { - Namespace = RbacNamespace.CapabilityMembershipManagement, - Name = "create", - AccessType = RbacAccessType.Capability, - }, - }, - capabilityId - ) - ).Permitted(); + return await HasPermission( + userId, + RbacAccessType.Capability, + RbacNamespace.CapabilityMembershipManagement, + "create", + capabilityId + ); } public async Task CanViewMembershipApplications(UserId userId, CapabilityId capabilityId) { - return ( - await _rbacApplicationService.IsUserPermitted( - userId, - new List - { - new() - { - Namespace = RbacNamespace.CapabilityMembershipManagement, - Name = "read", - AccessType = RbacAccessType.Capability, - }, - }, - capabilityId - ) - ).Permitted(); + return await HasPermission( + userId, + RbacAccessType.Capability, + RbacNamespace.CapabilityMembershipManagement, + "read", + capabilityId + ); } public async Task CanSeeAwsAccountId(PortalUser portalUser, CapabilityId capabilityId) { - return ( - await _rbacApplicationService.IsUserPermitted( - portalUser.Id, - new List - { - new() - { - Namespace = RbacNamespace.Aws, - Name = "read", - AccessType = RbacAccessType.Capability, - }, - }, - capabilityId - ) - ).Permitted(); + return await HasPermission( + portalUser.Id, + RbacAccessType.Capability, + RbacNamespace.Aws, + "read", + capabilityId + ); } public async Task CanRetryCreatingMessageContract(PortalUser portalUser, MessageContractId messageContractId) @@ -781,113 +568,100 @@ public async Task CanRetryCreatingMessageContract(PortalUser portalUser, M return false; } var kafkaTopic = await _kafkaTopicRepository.Get(messageContract.KafkaTopicId); - var canCreateMessageContract = ( - await _rbacApplicationService.IsUserPermitted( - portalUser.Id, - new List - { - new() - { - Namespace = RbacNamespace.Topics, - Name = "update", - AccessType = RbacAccessType.Capability, - }, - }, - kafkaTopic.CapabilityId - ) - ).Permitted(); - bool isCloudEngineer = IsCloudEngineerEnabled(portalUser); + var canCreateMessageContract = await HasPermission( + portalUser.Id, + RbacAccessType.Capability, + RbacNamespace.Topics, + "update", + kafkaTopic.CapabilityId + ); + bool isCloudEngineer = IsCloudEngineerEnabled(portalUser, "retry-creating-message-contract"); return isCloudEngineer || canCreateMessageContract; } public async Task CanSelfAssess(UserId userId, CapabilityId capabilityId) { - return ( - await _rbacApplicationService.IsUserPermitted( - userId, - new List - { - new() - { - Namespace = RbacNamespace.TagsAndMetadata, - Name = "create", - AccessType = RbacAccessType.Capability, - }, - new() - { - Namespace = RbacNamespace.TagsAndMetadata, - Name = "update", - AccessType = RbacAccessType.Capability, - }, - }, - capabilityId - ) - ).Permitted(); + var canCreate = await HasPermission( + userId, + RbacAccessType.Capability, + RbacNamespace.TagsAndMetadata, + "create", + capabilityId + ); + var canUpdate = await HasPermission( + userId, + RbacAccessType.Capability, + RbacNamespace.TagsAndMetadata, + "update", + capabilityId + ); + + return canCreate && canUpdate; } public bool CanManageSelfAssessmentOptions(PortalUser portalUser) { - return IsCloudEngineerEnabled(portalUser); + return IsCloudEngineerEnabled(portalUser, "manage-self-assessment-options"); } public bool IsAuthorizedToCreateReleaseNotes(PortalUser portalUser) { - return IsCloudEngineerEnabled(portalUser); + return IsCloudEngineerEnabled(portalUser, "create-release-notes"); } public bool IsAuthorizedToUpdateReleaseNote(PortalUser portalUser) { - return IsCloudEngineerEnabled(portalUser); + return IsCloudEngineerEnabled(portalUser, "update-release-note"); } public bool IsAuthorizedToToggleReleaseNoteIsActive(PortalUser portalUser) { - return IsCloudEngineerEnabled(portalUser); + return IsCloudEngineerEnabled(portalUser, "toggle-release-note-is-active"); } public bool IsAuthorizedToListDraftReleaseNotes(PortalUser portalUser) { - return IsCloudEngineerEnabled(portalUser); + return IsCloudEngineerEnabled(portalUser, "list-draft-release-notes"); } public bool IsAuthorizedToRemoveReleaseNote(PortalUser portalUser) { - return IsCloudEngineerEnabled(portalUser); + return IsCloudEngineerEnabled(portalUser, "remove-release-note"); } public bool CanCreateEvent(PortalUser portalUser) { - return IsCloudEngineerEnabled(portalUser); + return IsCloudEngineerEnabled(portalUser, "create-event"); } public bool CanUpdateEvent(PortalUser portalUser) { - return IsCloudEngineerEnabled(portalUser); + return IsCloudEngineerEnabled(portalUser, "update-event"); } public bool CanDeleteEvent(PortalUser portalUser) { - return IsCloudEngineerEnabled(portalUser); + return IsCloudEngineerEnabled(portalUser, "delete-event"); } public bool CanCreateNewsItem(PortalUser portalUser) { - return IsCloudEngineerEnabled(portalUser); + return IsCloudEngineerEnabled(portalUser, "create-news-item"); } public bool CanUpdateNewsItem(PortalUser portalUser) { - return IsCloudEngineerEnabled(portalUser); + return IsCloudEngineerEnabled(portalUser, "update-news-item"); } public bool CanDeleteNewsItem(PortalUser portalUser) { - return IsCloudEngineerEnabled(portalUser); + return IsCloudEngineerEnabled(portalUser, "delete-news-item"); } public bool CanGetUserEmails(PortalUser portalUser) { - return IsCloudEngineerEnabled(portalUser); + return IsCloudEngineerEnabled(portalUser, "get-user-emails"); } } diff --git a/src/SelfService/Domain/Services/ECRRepositoryService.cs b/src/SelfService/Domain/Services/ECRRepositoryService.cs index 34830a15..4a2c39f4 100644 --- a/src/SelfService/Domain/Services/ECRRepositoryService.cs +++ b/src/SelfService/Domain/Services/ECRRepositoryService.cs @@ -40,7 +40,7 @@ public Task HasRepository(string repositoryName) { return _ecrRepositoryRepository.HasRepository(repositoryName); } - +cloud public Task> GetAllECRRepositories() { return _ecrRepositoryRepository.GetAll(); diff --git a/src/SelfService/Domain/Services/IAuthorizationService.cs b/src/SelfService/Domain/Services/IAuthorizationService.cs index a338eaed..d8844d0f 100644 --- a/src/SelfService/Domain/Services/IAuthorizationService.cs +++ b/src/SelfService/Domain/Services/IAuthorizationService.cs @@ -36,7 +36,7 @@ public interface IAuthorizationService Task CanGetCapabilityJsonMetadata(PortalUser portalUser, CapabilityId capabilityId); Task CanSetCapabilityJsonMetadata(PortalUser portalUser, CapabilityId capabilityId); bool CanBypassMembershipApprovals(PortalUser portalUser); - bool CanBatchCreateCapabilities(PortalUser portalUser, UserId userId); + bool CanBatchCreateCapabilities(PortalUser portalUser); Task CanDeleteMembershipApplication( PortalUser portalUser, UserId userId, diff --git a/src/SelfService/Infrastructure/Api/Capabilities/CapabilityController.cs b/src/SelfService/Infrastructure/Api/Capabilities/CapabilityController.cs index 36c32e88..a0e6e1f0 100644 --- a/src/SelfService/Infrastructure/Api/Capabilities/CapabilityController.cs +++ b/src/SelfService/Infrastructure/Api/Capabilities/CapabilityController.cs @@ -1774,12 +1774,12 @@ public async Task de([FromBody] BatchCapabilityRequest request) return Unauthorized(); var portalUser = HttpContext.User.ToPortalUser(); - if (!_authorizationService.CanBatchCreateCapabilities(portalUser, userId)) + if (!_authorizationService.CanBatchCreateCapabilities(portalUser)) return Unauthorized( new ProblemDetails { Title = "User unauthorized", - Detail = "Only cloud engineers and selected users can create capabilities in batch.", + Detail = "Missing permission to create capabilities in batch.", } ); diff --git a/src/SelfService/Infrastructure/Api/Catalog/CatalogController.cs b/src/SelfService/Infrastructure/Api/Catalog/CatalogController.cs index e868ef5e..d5524a65 100644 --- a/src/SelfService/Infrastructure/Api/Catalog/CatalogController.cs +++ b/src/SelfService/Infrastructure/Api/Catalog/CatalogController.cs @@ -1,10 +1,11 @@ using Microsoft.AspNetCore.Mvc; using SelfService.Application; +using SelfService.Infrastructure.Api.RBAC; namespace SelfService.Infrastructure.Api.Catalog; -// TODO, add service catalogue specific RBAC scopes [Route("catalog")] +[RbacConfig(nameof(RbacObjectType.Global), "id")] [Produces("application/json")] [ApiController] public class CatalogController : ControllerBase @@ -22,6 +23,7 @@ CatalogApiResourceFactory apiResourceFactory } [HttpGet("applications")] + [RequiresPermission("service-catalogue", "service-catalogue-read")] [ProducesResponseType(typeof(CatalogApplicationsApiResource), StatusCodes.Status200OK)] public async Task GetApplications( [FromQuery] string? capabilityId, @@ -45,6 +47,7 @@ CancellationToken cancellationToken } [HttpGet("namespaces")] + [RequiresPermission("service-catalogue", "service-catalogue-read")] [ProducesResponseType(typeof(CatalogNamespacesApiResource), StatusCodes.Status200OK)] public async Task GetNamespaces(CancellationToken cancellationToken) { @@ -53,6 +56,7 @@ public async Task GetNamespaces(CancellationToken cancellationTok } [HttpGet("dependencies")] + [RequiresPermission("service-catalogue", "service-catalogue-read")] [ProducesResponseType(typeof(CatalogDependenciesApiResource), StatusCodes.Status200OK)] public async Task GetDependencies( [FromQuery] string? @namespace, diff --git a/tools/config.json b/tools/config.json index 9c08c2a8..ab986789 100644 --- a/tools/config.json +++ b/tools/config.json @@ -4,55 +4,266 @@ "cloudengineers": [ "andfris@dfds.com" ], - "cloudengineerRoles": [{ - "roleName": "Owner", - "scope": "Global" - }], + "batchCapabilityCreators": [], + "serviceCatalogueReaders": [], + "cloudengineerRoles": [ + { + "roleName": "CloudEngineer", + "scope": "Global" + } + ], "roles": [ { "name": "Owner", "permissions": { - "topics": ["create", "read-public", "read-private", "update", "delete"], - "capability-management": ["receive-alerts", "receive-cost", "request-deletion", "manage-permissions", "read-self-assess", "create-self-assess"], - "capability-membership-management": ["create", "delete", "read", "read-requests", "manage-requests"], - "tags-and-metadata": ["create", "read", "update", "delete"], - "aws": ["create", "read", "manage-provider", "read-provider"], - "finout": ["read-dashboards", "manage-dashboards", "manage-alerts", "read-alerts"], - "azure": ["create", "read", "read-provider", "manage-provider"], - "rbac": ["create", "read", "update", "delete"] + "topics": [ + "create", + "read-public", + "read-private", + "update", + "delete" + ], + "capability-management": [ + "receive-alerts", + "receive-cost", + "request-deletion", + "manage-permissions", + "read-self-assess", + "create-self-assess" + ], + "capability-membership-management": [ + "create", + "delete", + "read", + "read-requests", + "manage-requests" + ], + "tags-and-metadata": [ + "create", + "read", + "update", + "delete" + ], + "aws": [ + "create", + "read", + "manage-provider", + "read-provider" + ], + "finout": [ + "read-dashboards", + "manage-dashboards", + "manage-alerts", + "read-alerts" + ], + "azure": [ + "create", + "read", + "read-provider", + "manage-provider" + ], + "rbac": [ + "create", + "read", + "update", + "delete" + ] } }, { "name": "Contributor", "permissions": { - "topics": ["create", "read-public", "read-private", "update", "delete"], - "capability-management": ["receive-alerts"], - "capability-membership-management": ["create", "read", "read-requests", "manage-requests"], - "tags-and-metadata": ["create", "read", "update", "delete"], - "aws": ["create", "read", "manage-provider", "read-provider"], - "finout": ["read-dashboards", "manage-dashboards", "manage-alerts", "read-alerts"], - "azure": ["create", "read", "read-provider", "manage-provider"] + "topics": [ + "create", + "read-public", + "read-private", + "update", + "delete" + ], + "capability-management": [ + "receive-alerts" + ], + "capability-membership-management": [ + "create", + "read", + "read-requests", + "manage-requests" + ], + "tags-and-metadata": [ + "create", + "read", + "update", + "delete" + ], + "aws": [ + "create", + "read", + "manage-provider", + "read-provider" + ], + "finout": [ + "read-dashboards", + "manage-dashboards", + "manage-alerts", + "read-alerts" + ], + "azure": [ + "create", + "read", + "read-provider", + "manage-provider" + ] } }, { "name": "Reader", "permissions": { - "topics": ["read-public", "read-private"], - "capability-membership-management": ["read", "read-requests"], - "tags-and-metadata": ["read"], - "aws": ["read", "read-provider"], - "finout": ["read-dashboards", "read-alerts"], - "azure": ["read", "read-provider"] + "topics": [ + "read-public", + "read-private" + ], + "capability-membership-management": [ + "read", + "read-requests" + ], + "tags-and-metadata": [ + "read" + ], + "aws": [ + "read", + "read-provider" + ], + "finout": [ + "read-dashboards", + "read-alerts" + ], + "azure": [ + "read", + "read-provider" + ] } }, { "name": "Guest", "permissions": { - "topics": ["read-public"], - "capability-membership-management": ["read"], - "tags-and-metadata": ["read"], - "finout": ["read-dashboards"] + "topics": [ + "read-public" + ], + "capability-membership-management": [ + "read" + ], + "tags-and-metadata": [ + "read" + ], + "finout": [ + "read-dashboards" + ] + } + }, + { + "name": "CloudEngineer", + "permissions": { + "topics": [ + "create", + "read-public", + "read-private", + "update", + "delete" + ], + "capability-management": [ + "receive-alerts", + "receive-cost", + "request-deletion", + "manage-permissions", + "read-self-assess", + "create-self-assess", + "batch-create-capabilities" + ], + "capability-membership-management": [ + "create", + "delete", + "read", + "read-requests", + "manage-requests" + ], + "tags-and-metadata": [ + "create", + "read", + "update", + "delete" + ], + "aws": [ + "create", + "read", + "manage-provider", + "read-provider" + ], + "finout": [ + "read-dashboards", + "manage-dashboards", + "manage-alerts", + "read-alerts" + ], + "azure": [ + "create", + "read", + "read-provider", + "manage-provider" + ], + "rbac": [ + "create", + "read", + "update", + "delete" + ], + "service-catalogue": [ + "read" + ], + "system-legacy": [ + "read" + ], + "system-admin": [ + "view-deleted-capabilities", + "unset-capability-tags", + "create-demo-recording", + "update-demo-recording", + "delete-demo-recording", + "manage-permission-matrix", + "synchronize-aws-ecr-and-database-ecr", + "bypass-membership-approvals", + "manage-self-assessment-options", + "create-release-notes", + "update-release-note", + "toggle-release-note-is-active", + "list-draft-release-notes", + "remove-release-note", + "create-event", + "update-event", + "delete-event", + "create-news-item", + "update-news-item", + "delete-news-item", + "get-user-emails", + "delete-membership-application-as-admin", + "retry-creating-message-contract" + ] + } + }, + { + "name": "BatchCapabilityCreator", + "permissions": { + "capability-management": [ + "batch-create-capabilities" + ] + } + }, + { + "name": "ServiceCatalogueReader", + "permissions": { + "service-catalogue": [ + "read" + ] } } ] -} +} \ No newline at end of file diff --git a/tools/config_skeleton.json b/tools/config_skeleton.json index df89d2fb..df195ec3 100644 --- a/tools/config_skeleton.json +++ b/tools/config_skeleton.json @@ -2,58 +2,277 @@ "debug": true, "apiUrl": "http://localhost:8080", "cloudengineers": [ - "admin1@mailinator.com", - "someoneelse@dfds.com" + "admin1@mailinator.com", + "someoneelse@dfds.com" + ], + "batchCapabilityCreators": [ + "admin1@mailinator.com" + ], + "serviceCatalogueReaders": [ + "someoneelse@dfds.com" + ], + "cloudengineerRoles": [ + { + "roleName": "Owner", + "scope": "Global" + }, + { + "roleName": "CloudEngineer", + "scope": "Global" + } ], - "cloudengineerRoles": [{ - "roleName": "Owner", - "scope": "Global" - }], "roles": [ { "name": "Owner", "permissions": { - "topics": ["create", "read-public", "read-private", "update", "delete"], - "capability-management": ["receive-alerts", "receive-cost", "request-deletion", "manage-permissions", "read-self-assess", "create-self-assess"], - "capability-membership-management": ["create", "delete", "read", "read-requests", "manage-requests"], - "tags-and-metadata": ["create", "read", "update", "delete"], - "aws": ["create", "read", "manage-provider", "read-provider"], - "finout": ["read-dashboards", "manage-dashboards", "manage-alerts", "read-alerts"], - "azure": ["create", "read", "read-provider", "manage-provider"], - "rbac": ["create", "read", "update", "delete"] + "topics": [ + "create", + "read-public", + "read-private", + "update", + "delete" + ], + "capability-management": [ + "receive-alerts", + "receive-cost", + "request-deletion", + "manage-permissions", + "read-self-assess", + "create-self-assess" + ], + "capability-membership-management": [ + "create", + "delete", + "read", + "read-requests", + "manage-requests" + ], + "tags-and-metadata": [ + "create", + "read", + "update", + "delete" + ], + "aws": [ + "create", + "read", + "manage-provider", + "read-provider" + ], + "finout": [ + "read-dashboards", + "manage-dashboards", + "manage-alerts", + "read-alerts" + ], + "azure": [ + "create", + "read", + "read-provider", + "manage-provider" + ], + "rbac": [ + "create", + "read", + "update", + "delete" + ] } }, { "name": "Contributor", "permissions": { - "topics": ["create", "read-public", "read-private", "update", "delete"], - "capability-management": ["receive-alerts"], - "capability-membership-management": ["create", "read", "read-requests", "manage-requests"], - "tags-and-metadata": ["create", "read", "update", "delete"], - "aws": ["create", "read", "manage-provider", "read-provider"], - "finout": ["read-dashboards", "manage-dashboards", "manage-alerts", "read-alerts"], - "azure": ["create", "read", "read-provider", "manage-provider"] + "topics": [ + "create", + "read-public", + "read-private", + "update", + "delete" + ], + "capability-management": [ + "receive-alerts" + ], + "capability-membership-management": [ + "create", + "read", + "read-requests", + "manage-requests" + ], + "tags-and-metadata": [ + "create", + "read", + "update", + "delete" + ], + "aws": [ + "create", + "read", + "manage-provider", + "read-provider" + ], + "finout": [ + "read-dashboards", + "manage-dashboards", + "manage-alerts", + "read-alerts" + ], + "azure": [ + "create", + "read", + "read-provider", + "manage-provider" + ] } }, { "name": "Reader", "permissions": { - "topics": ["read-public", "read-private"], - "capability-membership-management": ["read", "read-requests"], - "tags-and-metadata": ["read"], - "aws": ["read", "read-provider"], - "finout": ["read-dashboards", "read-alerts"], - "azure": ["read", "read-provider"] + "topics": [ + "read-public", + "read-private" + ], + "capability-membership-management": [ + "read", + "read-requests" + ], + "tags-and-metadata": [ + "read" + ], + "aws": [ + "read", + "read-provider" + ], + "finout": [ + "read-dashboards", + "read-alerts" + ], + "azure": [ + "read", + "read-provider" + ] } }, { "name": "Guest", "permissions": { - "topics": ["read-public"], - "capability-membership-management": ["read"], - "tags-and-metadata": ["read"], - "finout": ["read-dashboards"] + "topics": [ + "read-public" + ], + "capability-membership-management": [ + "read" + ], + "tags-and-metadata": [ + "read" + ], + "finout": [ + "read-dashboards" + ] + } + }, + { + "name": "CloudEngineer", + "permissions": { + "topics": [ + "create", + "read-public", + "read-private", + "update", + "delete" + ], + "capability-management": [ + "receive-alerts", + "receive-cost", + "request-deletion", + "manage-permissions", + "read-self-assess", + "create-self-assess", + "batch-create-capabilities" + ], + "capability-membership-management": [ + "create", + "delete", + "read", + "read-requests", + "manage-requests" + ], + "tags-and-metadata": [ + "create", + "read", + "update", + "delete" + ], + "aws": [ + "create", + "read", + "manage-provider", + "read-provider" + ], + "finout": [ + "read-dashboards", + "manage-dashboards", + "manage-alerts", + "read-alerts" + ], + "azure": [ + "create", + "read", + "read-provider", + "manage-provider" + ], + "rbac": [ + "create", + "read", + "update", + "delete" + ], + "service-catalogue": [ + "read" + ], + "system-legacy": [ + "read" + ], + "system-admin": [ + "view-deleted-capabilities", + "unset-capability-tags", + "create-demo-recording", + "update-demo-recording", + "delete-demo-recording", + "manage-permission-matrix", + "synchronize-aws-ecr-and-database-ecr", + "bypass-membership-approvals", + "manage-self-assessment-options", + "create-release-notes", + "update-release-note", + "toggle-release-note-is-active", + "list-draft-release-notes", + "remove-release-note", + "create-event", + "update-event", + "delete-event", + "create-news-item", + "update-news-item", + "delete-news-item", + "get-user-emails", + "delete-membership-application-as-admin", + "retry-creating-message-contract" + ] + } + }, + { + "name": "BatchCapabilityCreator", + "permissions": { + "capability-management": [ + "batch-create-capabilities" + ] + } + }, + { + "name": "ServiceCatalogueReader", + "permissions": { + "service-catalogue": [ + "read" + ] } } ] -} +} \ No newline at end of file diff --git a/tools/setup-baseline-permissions.go b/tools/setup-baseline-permissions.go index 464fbf89..338e9dd8 100644 --- a/tools/setup-baseline-permissions.go +++ b/tools/setup-baseline-permissions.go @@ -129,30 +129,49 @@ func main() { log.Println("...") } - /* - Fetch all groups and check for "CloudEngineers" group - If it does not exist, create it - */ - if config.Debug { log.Println(">> Consolidating groups...") } + managedGroups := []ManagedGroup{ + { + Name: "CloudEngineers", + Roles: config.CloudEngineerRoles, + Members: config.Cloudengineers, + }, + { + Name: "BatchCapabilityCreators", + Roles: []RoleBinding{ + {RoleName: "BatchCapabilityCreator", Scope: "Global"}, + }, + Members: config.BatchCapabilityCreators, + }, + { + Name: "ServiceCatalogueReaders", + Roles: []RoleBinding{ + {RoleName: "ServiceCatalogueReader", Scope: "Global"}, + }, + Members: config.ServiceCatalogueReaders, + }, + } + availableGroups, err := fetchGroups(config) if err != nil { log.Fatalf("failed to fetch groups: %v", err) } - if _, exists := availableGroups["CloudEngineers"]; !exists { - log.Println("Group 'CloudEngineers' does not exist; creating it...") - createGroup(config, "CloudEngineers") + for _, groupSpec := range managedGroups { + if _, exists := availableGroups[groupSpec.Name]; !exists { + log.Printf("Group '%s' does not exist; creating it...", groupSpec.Name) + createGroup(config, groupSpec.Name) + } } availableGroups, err = fetchGroups(config) if err != nil { log.Fatalf("failed to fetch groups: %v", err) } - // Debug: print all available groups + if config.Debug { log.Println("Now Available groups:") for name, id := range availableGroups { @@ -160,105 +179,9 @@ func main() { } } - if config.Debug { - log.Println("...") - } - - /* - Fetch all role for "CloudEngineers" group - Check for Owner Global permission - If not found, assign it - */ - if config.Debug { - log.Println(">> Ensuring 'CloudEngineers' group has correct roles...") - } - group := availableGroups["CloudEngineers"] - assignedRoles, err := fetchRoleGrantsForGroup(config, group.ID) - if err != nil { - log.Fatalf("failed to fetch role grants for group: %v", err) - } - - if config.Debug { - log.Printf("'CloudEngineers' group currently has %d assigned roles.", len(assignedRoles)) - for _, ra := range assignedRoles { - log.Printf(" - Role ID: %s, Entity Type: %s, Entity ID: %s, Type: %s, Resource: %s", ra.RoleId, ra.AssignedEntityType, ra.AssignedEntityId, ra.Type, ra.Resource) - } - } - - // ensure all roles from config are assigned to the group - for _, r := range config.CloudEngineerRoles { - roleFound := false - for _, ra := range assignedRoles { - if ra.RoleId == availableRoles[strings.ToLower(r.RoleName)] && strings.EqualFold(ra.Type, r.Scope) { - roleFound = true - break - } - } - if !roleFound { - log.Printf("- Assigning %s role to 'CloudEngineers' group...", r.RoleName) - err := assignRole(config, availableRoles[strings.ToLower(r.RoleName)], "Group", group.ID, r.Scope, "") - if err != nil { - log.Fatalf("failed to assign role: %v", err) - } - if config.Debug { - log.Printf("- Assigned %s -- %s role to 'CloudEngineers' group.", r.RoleName, r.Scope) - } - } else { - if config.Debug { - log.Printf("'CloudEngineers' group already has %s -- %s role; no action needed.", r.RoleName, r.Scope) - } - } - } - // ensure that no other roles are assigned to the group - for _, ra := range assignedRoles { - roleDesired := false - for _, r := range config.CloudEngineerRoles { - if ra.RoleId == availableRoles[strings.ToLower(r.RoleName)] && strings.EqualFold(ra.Type, r.Scope) { - roleDesired = true - break - } - } - if !roleDesired { - log.Printf("- WARNING: 'CloudEngineers' group has unexpected role assigned (Role ID: %s, Type: %s, Scope: %s). Please review manually.", ra.RoleId, availableRoles[ra.RoleId], ra.Type) - } - } - - if config.Debug { - log.Println("...") - } - - /* - Fetch all existing members of group "CloudEngineers" - For all cloud engineers in config, check if they are already members - If not, add them - */ - if config.Debug { - log.Println(">> Ensuring all cloud engineers are members of 'CloudEngineers' group...") - } - group = availableGroups["CloudEngineers"] - - missingMembers, unwantedMembers := differences(config.Cloudengineers, extractEmails(group.Members)) - - // add missing members - for _, email := range missingMembers { - log.Printf("- Adding missing member: %s", email) - createMembership(config, group.ID, email) - } - // remove unwanted members - for _, email := range unwantedMembers { - log.Printf("- WARNING: 'CloudEngineers' group has unexpected member: %s. Please review manually.", email) - } - - if config.Debug { - log.Println("Final members of 'CloudEngineers' group:") - availableGroups, err = fetchGroups(config) - if err != nil { - log.Fatalf("failed to fetch groups: %v", err) - } - group = availableGroups["CloudEngineers"] - for _, email := range group.Members { - log.Printf(" - %s", email) - } + for _, groupSpec := range managedGroups { + ensureGroupRoles(config, groupSpec, availableGroups, availableRoles) + ensureGroupMembers(config, groupSpec, availableGroups) } if config.Debug { @@ -287,14 +210,24 @@ type Role struct { Permissions map[string][]string `json:"permissions"` } +type RoleBinding struct { + RoleName string `json:"roleName"` + Scope string `json:"scope"` +} + +type ManagedGroup struct { + Name string + Roles []RoleBinding + Members []string +} + type Config struct { - Debug bool `json:"debug"` - ApiUrl string `json:"apiUrl"` - Cloudengineers []string `json:"cloudengineers"` - CloudEngineerRoles []struct { - RoleName string `json:"roleName"` - Scope string `json:"scope"` - } `json:"cloudengineerRoles"` + Debug bool `json:"debug"` + ApiUrl string `json:"apiUrl"` + Cloudengineers []string `json:"cloudengineers"` + BatchCapabilityCreators []string `json:"batchCapabilityCreators"` + ServiceCatalogueReaders []string `json:"serviceCatalogueReaders"` + CloudEngineerRoles []RoleBinding `json:"cloudengineerRoles"` AccessToken string // not from config, set from env var 'SELF_SERVICE_API_TOKEN' Roles []Role `json:"roles"` } @@ -590,6 +523,96 @@ func fetchRoleGrantsForGroup(config *Config, groupID string) ([]RoleAssignment, return roleAssignments, nil } +func ensureGroupRoles(config *Config, groupSpec ManagedGroup, availableGroups map[string]Group, availableRoles map[string]string) { + group, exists := availableGroups[groupSpec.Name] + if !exists { + log.Fatalf("group '%s' is not available for role synchronization", groupSpec.Name) + } + + roleAssignments, err := fetchRoleGrantsForGroup(config, group.ID) + if err != nil { + log.Fatalf("failed to fetch role grants for group '%s': %v", groupSpec.Name, err) + } + + existingRoleGrants := make(map[string]RoleAssignment) + for _, assignment := range roleAssignments { + key := fmt.Sprintf("%s|%s|%s", assignment.RoleId, strings.ToLower(assignment.Type), assignment.Resource) + existingRoleGrants[key] = assignment + } + + expectedRoleGrants := make(map[string]RoleBinding) + for _, binding := range groupSpec.Roles { + roleID, roleExists := availableRoles[strings.ToLower(binding.RoleName)] + if !roleExists { + log.Fatalf("role '%s' required for group '%s' does not exist", binding.RoleName, groupSpec.Name) + } + + assignmentType := strings.TrimSpace(binding.Scope) + if assignmentType == "" { + assignmentType = "Global" + } + + key := fmt.Sprintf("%s|%s|*", roleID, strings.ToLower(assignmentType)) + expectedRoleGrants[key] = binding + + if _, granted := existingRoleGrants[key]; !granted { + if err := assignRole(config, roleID, "Group", group.ID, assignmentType, "*"); err != nil { + log.Fatalf("failed to assign role '%s' to group '%s': %v", binding.RoleName, groupSpec.Name, err) + } + if config.Debug { + log.Printf("- Assigned role '%s' (%s) to group '%s'.", binding.RoleName, assignmentType, groupSpec.Name) + } + } + } + + for key, assignment := range existingRoleGrants { + if _, expected := expectedRoleGrants[key]; !expected { + log.Printf("- WARNING: group '%s' has unexpected role assignment (roleId='%s', type='%s', resource='%s'). Please review manually.", groupSpec.Name, assignment.RoleId, assignment.Type, assignment.Resource) + } + } +} + +func ensureGroupMembers(config *Config, groupSpec ManagedGroup, availableGroups map[string]Group) { + group, exists := availableGroups[groupSpec.Name] + if !exists { + log.Fatalf("group '%s' is not available for member synchronization", groupSpec.Name) + } + + existingMembers := make(map[string]string) + for _, member := range group.Members { + normalized := strings.ToLower(strings.TrimSpace(member.UserId)) + if normalized != "" { + existingMembers[normalized] = member.UserId + } + } + + expectedMembers := make(map[string]string) + for _, member := range groupSpec.Members { + normalized := strings.ToLower(strings.TrimSpace(member)) + if normalized != "" { + expectedMembers[normalized] = member + } + } + + for normalized, member := range expectedMembers { + if _, exists := existingMembers[normalized]; !exists { + createMembership(config, group.ID, member) + if config.Debug { + log.Printf("- Added member '%s' to group '%s'.", member, groupSpec.Name) + } + } + } + + for normalized, member := range existingMembers { + if _, expected := expectedMembers[normalized]; !expected { + removeMembership(config, group.ID, member) + if config.Debug { + log.Printf("- Removed member '%s' from group '%s'.", member, groupSpec.Name) + } + } + } +} + func fetchPermissionsForRole(config *Config, roleId string) (map[string][]string, error) { url := fmt.Sprintf("%s/rbac/permission/role/%s", config.ApiUrl, roleId) req, _ := http.NewRequest("GET", url, nil) From e12bbd9d7b73b18dc92c1f4a8492bb6881745d8d Mon Sep 17 00:00:00 2001 From: Andreas Frisch Date: Wed, 22 Jul 2026 15:38:05 +0200 Subject: [PATCH 2/9] format --- .../Application/TestRbacApplicationService.cs | 1 - .../Application/RbacApplicationService.cs | 56 +++---------------- .../Domain/Services/AuthorizationService.cs | 40 ++----------- .../Domain/Services/ECRRepositoryService.cs | 2 +- 4 files changed, 14 insertions(+), 85 deletions(-) diff --git a/src/SelfService.Tests/Application/TestRbacApplicationService.cs b/src/SelfService.Tests/Application/TestRbacApplicationService.cs index 7bc7b4ed..e38afb49 100644 --- a/src/SelfService.Tests/Application/TestRbacApplicationService.cs +++ b/src/SelfService.Tests/Application/TestRbacApplicationService.cs @@ -758,5 +758,4 @@ await rbacSvc.IsUserPermitted( ); */ } - } diff --git a/src/SelfService/Application/RbacApplicationService.cs b/src/SelfService/Application/RbacApplicationService.cs index bad56ace..a57632aa 100644 --- a/src/SelfService/Application/RbacApplicationService.cs +++ b/src/SelfService/Application/RbacApplicationService.cs @@ -934,12 +934,7 @@ public static List BootstrapPermissions() new(RbacNamespace.Rbac, "create", "Manage RBAC", RbacAccessType.Global), new(RbacNamespace.Rbac, "update", "Manage RBAC", RbacAccessType.Global), new(RbacNamespace.Rbac, "delete", "Manage RBAC", RbacAccessType.Global), - new( - RbacNamespace.ServiceCatalogue, - "read", - "Read service catalogue resources", - RbacAccessType.Global - ), + new(RbacNamespace.ServiceCatalogue, "read", "Read service catalogue resources", RbacAccessType.Global), new( RbacNamespace.SystemLegacy, "read", @@ -952,30 +947,10 @@ public static List BootstrapPermissions() "View deleted capabilities", RbacAccessType.Global ), - new( - RbacNamespace.SystemAdmin, - "unset-capability-tags", - "Unset capability tags", - RbacAccessType.Global - ), - new( - RbacNamespace.SystemAdmin, - "create-demo-recording", - "Create demo recordings", - RbacAccessType.Global - ), - new( - RbacNamespace.SystemAdmin, - "update-demo-recording", - "Update demo recordings", - RbacAccessType.Global - ), - new( - RbacNamespace.SystemAdmin, - "delete-demo-recording", - "Delete demo recordings", - RbacAccessType.Global - ), + new(RbacNamespace.SystemAdmin, "unset-capability-tags", "Unset capability tags", RbacAccessType.Global), + new(RbacNamespace.SystemAdmin, "create-demo-recording", "Create demo recordings", RbacAccessType.Global), + new(RbacNamespace.SystemAdmin, "update-demo-recording", "Update demo recordings", RbacAccessType.Global), + new(RbacNamespace.SystemAdmin, "delete-demo-recording", "Delete demo recordings", RbacAccessType.Global), new( RbacNamespace.SystemAdmin, "manage-permission-matrix", @@ -1000,18 +975,8 @@ public static List BootstrapPermissions() "Manage self-assessment options", RbacAccessType.Global ), - new( - RbacNamespace.SystemAdmin, - "create-release-notes", - "Create release notes", - RbacAccessType.Global - ), - new( - RbacNamespace.SystemAdmin, - "update-release-note", - "Update release note", - RbacAccessType.Global - ), + new(RbacNamespace.SystemAdmin, "create-release-notes", "Create release notes", RbacAccessType.Global), + new(RbacNamespace.SystemAdmin, "update-release-note", "Update release note", RbacAccessType.Global), new( RbacNamespace.SystemAdmin, "toggle-release-note-is-active", @@ -1024,12 +989,7 @@ public static List BootstrapPermissions() "List draft release notes", RbacAccessType.Global ), - new( - RbacNamespace.SystemAdmin, - "remove-release-note", - "Remove release note", - RbacAccessType.Global - ), + new(RbacNamespace.SystemAdmin, "remove-release-note", "Remove release note", RbacAccessType.Global), new(RbacNamespace.SystemAdmin, "create-event", "Create events", RbacAccessType.Global), new(RbacNamespace.SystemAdmin, "update-event", "Update events", RbacAccessType.Global), new(RbacNamespace.SystemAdmin, "delete-event", "Delete events", RbacAccessType.Global), diff --git a/src/SelfService/Domain/Services/AuthorizationService.cs b/src/SelfService/Domain/Services/AuthorizationService.cs index 3e5900e6..881fe5e4 100644 --- a/src/SelfService/Domain/Services/AuthorizationService.cs +++ b/src/SelfService/Domain/Services/AuthorizationService.cs @@ -310,13 +310,7 @@ public async Task CanViewAwsAccount(UserId userId, AwsAccountId accountId) { return false; } - return await HasPermission( - userId, - RbacAccessType.Capability, - RbacNamespace.Aws, - "read", - account.CapabilityId - ); + return await HasPermission(userId, RbacAccessType.Capability, RbacNamespace.Aws, "read", account.CapabilityId); } public async Task CanViewAwsAccountInformation(UserId userId, CapabilityId capabilityId) @@ -330,13 +324,7 @@ public async Task CanViewAwsAccountInformation(UserId userId, CapabilityId return false; } - return await HasPermission( - userId, - RbacAccessType.Capability, - RbacNamespace.Aws, - "read", - account.CapabilityId - ); + return await HasPermission(userId, RbacAccessType.Capability, RbacNamespace.Aws, "read", account.CapabilityId); } public async Task CanRequestAwsAccount(UserId userId, CapabilityId capabilityId) @@ -359,24 +347,12 @@ public async Task CanViewAzureResources(UserId userId, CapabilityId capabi public async Task CanRequestAzureResource(UserId userId, CapabilityId capabilityId, string environment) { - return await HasPermission( - userId, - RbacAccessType.Capability, - RbacNamespace.Azure, - "create", - capabilityId - ); + return await HasPermission(userId, RbacAccessType.Capability, RbacNamespace.Azure, "create", capabilityId); } public async Task CanRequestAzureResources(UserId userId, CapabilityId capabilityId) { - return await HasPermission( - userId, - RbacAccessType.Capability, - RbacNamespace.Azure, - "create", - capabilityId - ); + return await HasPermission(userId, RbacAccessType.Capability, RbacNamespace.Azure, "create", capabilityId); } public async Task CanLeave(UserId userId, CapabilityId capabilityId) @@ -551,13 +527,7 @@ public async Task CanViewMembershipApplications(UserId userId, CapabilityI public async Task CanSeeAwsAccountId(PortalUser portalUser, CapabilityId capabilityId) { - return await HasPermission( - portalUser.Id, - RbacAccessType.Capability, - RbacNamespace.Aws, - "read", - capabilityId - ); + return await HasPermission(portalUser.Id, RbacAccessType.Capability, RbacNamespace.Aws, "read", capabilityId); } public async Task CanRetryCreatingMessageContract(PortalUser portalUser, MessageContractId messageContractId) diff --git a/src/SelfService/Domain/Services/ECRRepositoryService.cs b/src/SelfService/Domain/Services/ECRRepositoryService.cs index 4a2c39f4..34830a15 100644 --- a/src/SelfService/Domain/Services/ECRRepositoryService.cs +++ b/src/SelfService/Domain/Services/ECRRepositoryService.cs @@ -40,7 +40,7 @@ public Task HasRepository(string repositoryName) { return _ecrRepositoryRepository.HasRepository(repositoryName); } -cloud + public Task> GetAllECRRepositories() { return _ecrRepositoryRepository.GetAll(); From c48cb0993b6a3fa61b7df3695a96d9adedf8b666 Mon Sep 17 00:00:00 2001 From: Andreas Frisch Date: Thu, 23 Jul 2026 12:09:17 +0200 Subject: [PATCH 3/9] updated seed --- db/seed/RbacGroup.csv | 4 +- db/seed/RbacGroupMember.csv | 3 +- db/seed/RbacPermissionGrants.csv | 221 ++++++++++++++++++++----------- db/seed/RbacRole.csv | 11 +- db/seed/RbacRoleGrants.csv | 11 +- 5 files changed, 156 insertions(+), 94 deletions(-) diff --git a/db/seed/RbacGroup.csv b/db/seed/RbacGroup.csv index 48a51712..1756b1c0 100644 --- a/db/seed/RbacGroup.csv +++ b/db/seed/RbacGroup.csv @@ -1,2 +1,4 @@ Id;CreatedAt;UpdatedAt;Name;Description -91E26750-E58E-43F6-AB8E-EBA2C8EC7019;2000-01-01;2000-01-01;ce - users;Members of Cloud Engineering +BA0AA734-206D-454E-AC95-14855B6901E4;2026-07-23T09:15:35.803168;2026-07-23T09:15:35.803174;CloudEngineers;Group: CloudEngineers +899F8F9E-3F7E-4EF3-A2CD-2E1DDA78E40A;2026-07-23T09:15:35.803179;2026-07-23T09:15:35.803180;BatchCapabilityCreators;Group: BatchCapabilityCreators +7D3D7498-9075-4D1B-B6C1-AF6E95788C3B;2026-07-23T09:15:35.803184;2026-07-23T09:15:35.803185;ServiceCatalogueReaders;Group: ServiceCatalogueReaders diff --git a/db/seed/RbacGroupMember.csv b/db/seed/RbacGroupMember.csv index 99f8899f..6c09ecab 100644 --- a/db/seed/RbacGroupMember.csv +++ b/db/seed/RbacGroupMember.csv @@ -1,2 +1,3 @@ Id;GroupId;UserId;CreatedAt -72C7137D-6EA9-48E7-B505-66D068D272D9;91E26750-E58E-43F6-AB8E-EBA2C8EC7019;emcla@dfds.com;2000-01-01 +7BADF960-B926-4BDC-A4BF-8B0CB444E292;BA0AA734-206D-454E-AC95-14855B6901E4;andfris@dfds.com;2026-07-23T09:15:35.803265 +DEF609DD-40B7-4DEC-A430-0208C49555DB;BA0AA734-206D-454E-AC95-14855B6901E4;emcla@dfds.com;2026-07-23T09:15:35.803272 diff --git a/db/seed/RbacPermissionGrants.csv b/db/seed/RbacPermissionGrants.csv index 548a1d98..f91f6f76 100644 --- a/db/seed/RbacPermissionGrants.csv +++ b/db/seed/RbacPermissionGrants.csv @@ -1,81 +1,142 @@ Id;CreatedAt;AssignedEntityType;AssignedEntityId;Namespace;Permission;Type;Resource -54bdf46c-0c11-438b-9752-7872e344dcd3;2025-11-04T09:26:38.431978;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;topics;create;Global; -4a7547cb-827b-4eea-ab22-cd1f90bb4c83;2025-11-04T09:26:38.431990;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;topics;read-public;Global; -d0de4d8d-b742-46cc-90bb-39459eebd5c7;2025-11-04T09:26:38.431995;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;topics;read-private;Global; -d96c5407-570d-49a4-a706-14a1f9915ed0;2025-11-04T09:26:38.432000;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;topics;update;Global; -874ccad3-3dfa-4652-8b37-339181af27b6;2025-11-04T09:26:38.432004;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;topics;delete;Global; -43c2184b-e87e-4c88-a220-62a9ea9969ca;2025-11-04T09:26:38.432009;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;capability-management;receive-alerts;Global; -9901b3da-8dbf-4535-ae7e-1737eb0e29f9;2025-11-04T09:26:38.432013;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;capability-management;receive-cost;Global; -7c9ff591-f031-4af4-a69e-2148e4ecd84a;2025-11-04T09:26:38.432017;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;capability-management;request-deletion;Global; -67f2650b-0696-4ed1-b58f-85c20c0f39bd;2025-11-04T09:26:38.432022;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;capability-management;manage-permissions;Global; -c2db7e4e-fa8e-433f-a8a7-aa32183da1b6;2025-11-04T09:26:38.432026;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;capability-management;read-self-assess;Global; -6a0d9033-e3e4-42db-a430-d5e05ac732e0;2025-11-04T09:26:38.432030;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;capability-management;create-self-assess;Global; -d716b456-97ff-43be-8c14-aa329c664b14;2025-11-04T09:26:38.432034;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;capability-membership-management;create;Global; -fc6cbdb2-faa6-4f0a-8f40-11e091136f7d;2025-11-04T09:26:38.432038;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;capability-membership-management;delete;Global; -703b7dfb-ea45-4b89-9961-57e9082932ee;2025-11-04T09:26:38.432043;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;capability-membership-management;read;Global; -efb9c48d-03ea-4c94-9a06-880dafd84a77;2025-11-04T09:26:38.432047;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;capability-membership-management;read-requests;Global; -a5631714-ddc2-40d0-9e4d-c2cf12b67319;2025-11-04T09:26:38.432051;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;capability-membership-management;manage-requests;Global; -e3e1a86a-0405-4a7b-b108-98110dd716d8;2025-11-04T09:26:38.432056;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;tags-and-metadata;create;Global; -9718c612-7645-45cc-bd03-3c9307de306f;2025-11-04T09:26:38.432060;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;tags-and-metadata;read;Global; -eb56b78b-14a2-4d66-aed8-f35f0693080b;2025-11-04T09:26:38.432064;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;tags-and-metadata;update;Global; -dd50e2a9-dda4-40ac-99aa-36205ccc3255;2025-11-04T09:26:38.432068;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;tags-and-metadata;delete;Global; -0ede7b36-1d22-41f4-8565-1d377bdcac10;2025-11-04T09:26:38.432072;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;aws;create;Global; -92dd2662-6d2c-4c0a-9c57-c03feca65b61;2025-11-04T09:26:38.432076;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;aws;read;Global; -89da9177-6ca1-414a-ba68-5af648a4f636;2025-11-04T09:26:38.432080;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;aws;manage-provider;Global; -18da8c80-8ce6-4297-8b62-84b21db18105;2025-11-04T09:26:38.432084;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;aws;read-provider;Global; -7f62783a-4c98-41b9-86ba-da6e3d4e0df7;2025-11-04T09:26:38.432089;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;finout;read-dashboards;Global; -fdb2109c-eb7c-49dc-9d54-b4ae415c2cac;2025-11-04T09:26:38.432093;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;finout;manage-dashboards;Global; -283fc905-8405-41b4-8289-88a4bba1402e;2025-11-04T09:26:38.432097;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;finout;manage-alerts;Global; -1e96a88d-a823-479f-bcba-4cfe7b68fafb;2025-11-04T09:26:38.432101;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;finout;read-alerts;Global; -202264a0-671f-4eea-a4a0-b5699cc2b36a;2025-11-04T09:26:38.432105;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;azure;create;Global; -f49486dc-c59f-482c-bbe4-a3eeb001604e;2025-11-04T09:26:38.432109;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;azure;read;Global; -442b139b-5541-403c-9c2f-a98c4f827eb2;2025-11-04T09:26:38.432113;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;azure;read-provider;Global; -fcaeda3f-a13b-45b8-89bd-be1894eb14cc;2025-11-04T09:26:38.432117;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;azure;manage-provider;Global; -71610125-b05e-45f7-a66d-39d8a3baecc2;2025-11-04T09:26:38.432122;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;rbac;create;Global; -00b28544-69de-43c6-a0d4-ff73166905c4;2025-11-04T09:26:38.432158;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;rbac;read;Global; -bbeb6351-dfb1-4f3b-b11b-a208bdabf024;2025-11-04T09:26:38.432282;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;rbac;delete;Global; -b8870dee-fd8e-4796-9883-ca6ede6f1eba;2025-11-04T09:26:38.432291;Role;2C561A6D-90F4-4649-80B3-76A854A64EA2;topics;create;Global; -3ba52cb1-49d7-431d-945a-4f3a02314d51;2025-11-04T09:26:38.432296;Role;2C561A6D-90F4-4649-80B3-76A854A64EA2;topics;read-public;Global; -610ee875-9fc5-4da7-b5f2-7cf4931ec98b;2025-11-04T09:26:38.432300;Role;2C561A6D-90F4-4649-80B3-76A854A64EA2;topics;read-private;Global; -ea06a2ac-07ad-4f6c-9ed2-7bda3645add5;2025-11-04T09:26:38.432304;Role;2C561A6D-90F4-4649-80B3-76A854A64EA2;topics;update;Global; -1da50c4f-eb8e-4761-b7da-bbc8467a93f1;2025-11-04T09:26:38.432308;Role;2C561A6D-90F4-4649-80B3-76A854A64EA2;topics;delete;Global; -64ac3649-ad8f-4a9c-9cd8-c472b867b495;2025-11-04T09:26:38.432313;Role;2C561A6D-90F4-4649-80B3-76A854A64EA2;capability-management;receive-alerts;Global; -124ea781-b6c7-4c30-9ff4-c92519fad83b;2025-11-04T09:26:38.432317;Role;2C561A6D-90F4-4649-80B3-76A854A64EA2;capability-membership-management;create;Global; -58df94cd-e00e-4c07-914f-40ad49f26e8e;2025-11-04T09:26:38.432321;Role;2C561A6D-90F4-4649-80B3-76A854A64EA2;capability-membership-management;read;Global; -85295490-b551-4477-b3cd-d12067b89c05;2025-11-04T09:26:38.432325;Role;2C561A6D-90F4-4649-80B3-76A854A64EA2;capability-membership-management;read-requests;Global; -a8c55428-8d23-43cf-ac06-e0221e067c9c;2025-11-04T09:26:38.432329;Role;2C561A6D-90F4-4649-80B3-76A854A64EA2;capability-membership-management;manage-requests;Global; -87e46c06-7619-4ba8-9627-3421213540d6;2025-11-04T09:26:38.432334;Role;2C561A6D-90F4-4649-80B3-76A854A64EA2;tags-and-metadata;create;Global; -b3a2622f-f915-4fb7-8c15-6bc449b00d81;2025-11-04T09:26:38.432338;Role;2C561A6D-90F4-4649-80B3-76A854A64EA2;tags-and-metadata;read;Global; -dbcef48c-8329-47ea-81e6-0becd4070f5f;2025-11-04T09:26:38.432342;Role;2C561A6D-90F4-4649-80B3-76A854A64EA2;tags-and-metadata;update;Global; -5e8cb2c7-7a6e-46a2-a0c6-58b11a5514e8;2025-11-04T09:26:38.432346;Role;2C561A6D-90F4-4649-80B3-76A854A64EA2;tags-and-metadata;delete;Global; -7868f729-7a8d-4a2b-81b9-1adf3e9e4295;2025-11-04T09:26:38.432350;Role;2C561A6D-90F4-4649-80B3-76A854A64EA2;aws;create;Global; -df530b2b-06e2-4120-a0c0-02ba00b1918d;2025-11-04T09:26:38.432354;Role;2C561A6D-90F4-4649-80B3-76A854A64EA2;aws;read;Global; -b83273c3-a14a-4714-a3c5-16e6fb4010d8;2025-11-04T09:26:38.432358;Role;2C561A6D-90F4-4649-80B3-76A854A64EA2;aws;manage-provider;Global; -84caee59-0ff5-4019-826f-26799bf4e416;2025-11-04T09:26:38.432362;Role;2C561A6D-90F4-4649-80B3-76A854A64EA2;aws;read-provider;Global; -c7a8f9d4-1ba9-4c2b-ac05-13272c1ec7c0;2025-11-04T09:26:38.432366;Role;2C561A6D-90F4-4649-80B3-76A854A64EA2;finout;read-dashboards;Global; -e7676f64-4d2c-43bf-996a-8fc4810a4849;2025-11-04T09:26:38.432370;Role;2C561A6D-90F4-4649-80B3-76A854A64EA2;finout;manage-dashboards;Global; -db61436d-2dc4-4165-b866-3dec884726f2;2025-11-04T09:26:38.432374;Role;2C561A6D-90F4-4649-80B3-76A854A64EA2;finout;manage-alerts;Global; -56ca23ba-4be6-4d45-942e-cdf9b973386e;2025-11-04T09:26:38.432378;Role;2C561A6D-90F4-4649-80B3-76A854A64EA2;finout;read-alerts;Global; -85976588-3512-4300-ad76-8bcd460795da;2025-11-04T09:26:38.432382;Role;2C561A6D-90F4-4649-80B3-76A854A64EA2;azure;create;Global; -a1acdd84-ddd2-4e4f-8f81-510ac9cfb798;2025-11-04T09:26:38.432386;Role;2C561A6D-90F4-4649-80B3-76A854A64EA2;azure;read;Global; -8cc40742-1c0c-459d-983d-655f3623d4a3;2025-11-04T09:26:38.432449;Role;2C561A6D-90F4-4649-80B3-76A854A64EA2;azure;read-provider;Global; -eb657810-2ef9-47cd-836d-47c8dcb8ce4b;2025-11-04T09:26:38.432456;Role;2C561A6D-90F4-4649-80B3-76A854A64EA2;azure;manage-provider;Global; -2736c51e-8ec4-44c3-8fb0-0ef60a314462;2025-11-04T09:26:38.432463;Role;2C561A6D-90F4-4649-80B3-76A854A64EA2;rbac;read;Global; -37feb098-5773-4470-b975-5d37026f3748;2025-11-04T09:26:38.432468;Role;22DAB91B-C2D8-4840-A173-1416EF1B882D;topics;read-public;Global; -dddf901b-9d87-415c-9ef5-6470b00253b6;2025-11-04T09:26:38.432472;Role;22DAB91B-C2D8-4840-A173-1416EF1B882D;topics;read-private;Global; -906098fd-75e3-4dc3-9970-f94aa3ff8c0c;2025-11-04T09:26:38.432476;Role;22DAB91B-C2D8-4840-A173-1416EF1B882D;capability-membership-management;read;Global; -142d9e34-3e78-4789-9f65-4bef08b09b9c;2025-11-04T09:26:38.432480;Role;22DAB91B-C2D8-4840-A173-1416EF1B882D;capability-membership-management;read-requests;Global; -57b299b5-31b9-4eed-a68c-e4a2b703225c;2025-11-04T09:26:38.432484;Role;22DAB91B-C2D8-4840-A173-1416EF1B882D;tags-and-metadata;read;Global; -dac1ab98-bf45-4733-ad44-8f73af892b75;2025-11-04T09:26:38.432489;Role;22DAB91B-C2D8-4840-A173-1416EF1B882D;aws;read;Global; -0a78d897-a899-407b-a666-e6a6203984e1;2025-11-04T09:26:38.432493;Role;22DAB91B-C2D8-4840-A173-1416EF1B882D;aws;read-provider;Global; -0ab86345-bba5-4eae-8e21-d6bdc46da254;2025-11-04T09:26:38.432497;Role;22DAB91B-C2D8-4840-A173-1416EF1B882D;finout;read-dashboards;Global; -7753f721-1815-42ff-92fa-07b561752992;2025-11-04T09:26:38.432501;Role;22DAB91B-C2D8-4840-A173-1416EF1B882D;finout;read-alerts;Global; -dfddfaa5-84c1-4b4b-bfa3-092c8da9e55e;2025-11-04T09:26:38.432505;Role;22DAB91B-C2D8-4840-A173-1416EF1B882D;azure;read;Global; -1a580552-4099-42bd-bd08-5671c13e5c48;2025-11-04T09:26:38.432509;Role;22DAB91B-C2D8-4840-A173-1416EF1B882D;azure;read-provider;Global; -73789730-8caa-457a-b7af-2af171a26065;2025-11-04T09:26:38.432515;Role;22DAB91B-C2D8-4840-A173-1416EF1B882D;rbac;read;Global; -14009bc8-e65b-44be-88ad-10d9577e8dd4;2025-11-04T09:26:38.432520;Role;F67CACC9-8DD4-4481-AC15-00B5DD83B046;topics;read-public;Global; -05d2d497-eb6f-44c3-955b-92fe45a3b6ca;2025-11-04T09:26:38.432524;Role;F67CACC9-8DD4-4481-AC15-00B5DD83B046;capability-membership-management;read;Global; -780966f7-66dd-4664-9e2f-53ba27221f82;2025-11-04T09:26:38.432528;Role;F67CACC9-8DD4-4481-AC15-00B5DD83B046;tags-and-metadata;read;Global; -de617757-441f-44ae-8fbe-b100896c8c7c;2025-11-04T09:26:38.432540;Role;F67CACC9-8DD4-4481-AC15-00B5DD83B046;finout;read-dashboards;Global; -0e2e4192-19d5-428e-b013-0ca7383249e3;2025-11-04T09:26:38.432549;Role;F67CACC9-8DD4-4481-AC15-00B5DD83B046;rbac;read;Global; -d22913cb-38fa-49c1-87f8-b6a975b01507;2026-06-04T00:00:00.000000;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;system-legacy;read;Global; +6636B444-0962-409F-BB43-A8C56B20D5C3;2026-07-23T09:15:35.802046;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;topics;create;Global; +C0C94271-CCE6-427D-A2F7-94B4035AEF10;2026-07-23T09:15:35.802057;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;topics;read-public;Global; +E1E30581-2CC9-4E24-82E9-1800CAD6DC55;2026-07-23T09:15:35.802063;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;topics;read-private;Global; +FBA28174-5714-41FB-AF1E-563C438582CB;2026-07-23T09:15:35.802068;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;topics;update;Global; +03ADA3FC-F4B8-46F8-B28E-884410A0BA14;2026-07-23T09:15:35.802073;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;topics;delete;Global; +CA991F8A-894C-4725-B3D5-BE0B7A3005B7;2026-07-23T09:15:35.802077;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;capability-management;receive-alerts;Global; +6BDF3560-E5FD-476C-92BE-E702021124FC;2026-07-23T09:15:35.802082;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;capability-management;receive-cost;Global; +C5E477C0-8FD2-4DA0-908B-113AEFD0CC07;2026-07-23T09:15:35.802086;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;capability-management;request-deletion;Global; +D4E3F9F6-88E0-4711-A298-018151678B22;2026-07-23T09:15:35.802090;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;capability-management;manage-permissions;Global; +D0941F2B-2D39-472F-9897-D56E5FB0ABCD;2026-07-23T09:15:35.802094;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;capability-management;read-self-assess;Global; +2B855B00-9C78-4F9B-8586-33E621D4A06B;2026-07-23T09:15:35.802099;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;capability-management;create-self-assess;Global; +4F9FBCA3-D38D-42E8-B21A-70561958BB9D;2026-07-23T09:15:35.802103;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;capability-membership-management;create;Global; +A8DA872A-6A59-4A4A-91C3-CCAC99E8D11D;2026-07-23T09:15:35.802107;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;capability-membership-management;delete;Global; +E3A0135F-8668-4EA7-BA5D-4514DB422BAF;2026-07-23T09:15:35.802111;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;capability-membership-management;read;Global; +0EFD309D-0979-4B19-AE71-298C23FE0AE1;2026-07-23T09:15:35.802115;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;capability-membership-management;read-requests;Global; +977EECE6-8221-40FA-AC2B-2098C1F026C8;2026-07-23T09:15:35.802119;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;capability-membership-management;manage-requests;Global; +AB7EE446-3F53-4072-8FD7-D7C0E453FE37;2026-07-23T09:15:35.802126;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;tags-and-metadata;create;Global; +27B0A834-8C28-481A-9A7F-FCFD05F062BB;2026-07-23T09:15:35.802129;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;tags-and-metadata;read;Global; +D67C2375-01B3-4538-B687-AF9C9B713CD1;2026-07-23T09:15:35.802133;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;tags-and-metadata;update;Global; +75F6DD9D-03CB-42D7-B29D-480927A61B34;2026-07-23T09:15:35.802137;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;tags-and-metadata;delete;Global; +D4FFA361-0B62-419C-98AF-843A05ADE320;2026-07-23T09:15:35.802142;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;aws;create;Global; +07DD388C-4FA2-4CD9-BB78-5A33E77A77C4;2026-07-23T09:15:35.802146;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;aws;read;Global; +921A5405-E4DD-4012-B86D-33155A8A26FD;2026-07-23T09:15:35.802150;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;aws;manage-provider;Global; +87C539E6-6BD1-422C-A606-D7B40A8680BA;2026-07-23T09:15:35.802154;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;aws;read-provider;Global; +5E80E7A7-E4E2-4A22-8359-DFDBEC1838CD;2026-07-23T09:15:35.802158;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;finout;read-dashboards;Global; +6D85C79E-BAF1-4FA4-9BF7-97C3E7637227;2026-07-23T09:15:35.802162;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;finout;manage-dashboards;Global; +DEC2C696-4409-4ABB-9D7D-892FF45DD15E;2026-07-23T09:15:35.802166;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;finout;manage-alerts;Global; +5DDDB2B3-DB61-4634-B326-C9EDA660163F;2026-07-23T09:15:35.802170;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;finout;read-alerts;Global; +7215EF9D-6F79-40F3-9B5C-0F588B93527D;2026-07-23T09:15:35.802174;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;azure;create;Global; +FAD7F3CE-1E5A-4590-B8ED-25302A090669;2026-07-23T09:15:35.802178;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;azure;read;Global; +B624A296-195C-496F-A978-F73636496C36;2026-07-23T09:15:35.802181;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;azure;read-provider;Global; +AFA290E9-810B-47E0-A4E4-15E41DEBF3EB;2026-07-23T09:15:35.802185;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;azure;manage-provider;Global; +868B7621-7599-4708-952D-16C48E5014CD;2026-07-23T09:15:35.802189;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;rbac;create;Global; +A4A7D300-90AA-4814-B45C-52319C3D0F09;2026-07-23T09:15:35.802193;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;rbac;read;Global; +EBCF6A80-E00F-48A9-8AD9-7CE0DA48B595;2026-07-23T09:15:35.802197;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;rbac;update;Global; +0CF131F2-DE58-443A-9EDF-B9F39C192080;2026-07-23T09:15:35.802201;Role;36202DFB-D106-440D-8B99-F11BC8D77C9C;rbac;delete;Global; +EAF901BE-4122-4C37-94C8-8434F3F8C7E9;2026-07-23T09:15:35.802206;Role;2C561A6D-90F4-4649-80B3-76A854A64EA2;topics;create;Global; +52E51E0B-1F55-444D-9734-1E8A510A52B1;2026-07-23T09:15:35.802210;Role;2C561A6D-90F4-4649-80B3-76A854A64EA2;topics;read-public;Global; +40AA2A38-57D4-4189-B7E5-F4F7BA70BC80;2026-07-23T09:15:35.802215;Role;2C561A6D-90F4-4649-80B3-76A854A64EA2;topics;read-private;Global; +1997B0AC-68A0-426B-8711-771B72370EE0;2026-07-23T09:15:35.802219;Role;2C561A6D-90F4-4649-80B3-76A854A64EA2;topics;update;Global; +A75E4E4E-D1AC-4286-A7A1-8487F5C0D4E2;2026-07-23T09:15:35.802223;Role;2C561A6D-90F4-4649-80B3-76A854A64EA2;topics;delete;Global; +3D4EF3DA-02CD-45F6-9740-F3B2A145A6C6;2026-07-23T09:15:35.802227;Role;2C561A6D-90F4-4649-80B3-76A854A64EA2;capability-management;receive-alerts;Global; +5AF25ADF-E8FD-4151-B17C-D5F83D139F68;2026-07-23T09:15:35.802232;Role;2C561A6D-90F4-4649-80B3-76A854A64EA2;capability-membership-management;create;Global; +D188347A-CFCF-458B-9AC4-A66F487808B6;2026-07-23T09:15:35.802236;Role;2C561A6D-90F4-4649-80B3-76A854A64EA2;capability-membership-management;read;Global; +A8E6793B-54C2-4B3F-B9DC-81FCD65F1159;2026-07-23T09:15:35.802240;Role;2C561A6D-90F4-4649-80B3-76A854A64EA2;capability-membership-management;read-requests;Global; +162509FD-7C76-4968-8747-9ADB02202CE7;2026-07-23T09:15:35.802244;Role;2C561A6D-90F4-4649-80B3-76A854A64EA2;capability-membership-management;manage-requests;Global; +64E68687-0C94-4F99-A36F-312A2E04543E;2026-07-23T09:15:35.802250;Role;2C561A6D-90F4-4649-80B3-76A854A64EA2;tags-and-metadata;create;Global; +A6B67719-B660-412A-8D5E-0F9787E64A33;2026-07-23T09:15:35.802254;Role;2C561A6D-90F4-4649-80B3-76A854A64EA2;tags-and-metadata;read;Global; +13CBE1F8-8768-4F54-A93C-A0431ACDC2D1;2026-07-23T09:15:35.802258;Role;2C561A6D-90F4-4649-80B3-76A854A64EA2;tags-and-metadata;update;Global; +BEE2F603-834F-49A2-A9EF-9BD59485DBE0;2026-07-23T09:15:35.802262;Role;2C561A6D-90F4-4649-80B3-76A854A64EA2;tags-and-metadata;delete;Global; +2AAF2B8F-71BD-4FFD-9CD0-47197B934353;2026-07-23T09:15:35.802294;Role;2C561A6D-90F4-4649-80B3-76A854A64EA2;aws;create;Global; +12B68CB9-248E-42EB-A231-337DE83D2378;2026-07-23T09:15:35.802301;Role;2C561A6D-90F4-4649-80B3-76A854A64EA2;aws;read;Global; +573A8398-4755-4031-923C-63AAABA5041D;2026-07-23T09:15:35.802307;Role;2C561A6D-90F4-4649-80B3-76A854A64EA2;aws;manage-provider;Global; +20B53E15-8C5A-4289-8A53-9D3BFE0230A0;2026-07-23T09:15:35.802312;Role;2C561A6D-90F4-4649-80B3-76A854A64EA2;aws;read-provider;Global; +12D8B2CD-CD56-4C6A-A21B-5782179505A7;2026-07-23T09:15:35.802316;Role;2C561A6D-90F4-4649-80B3-76A854A64EA2;finout;read-dashboards;Global; +C41C1488-9C86-4B41-8CE9-35DE2120B8B4;2026-07-23T09:15:35.802320;Role;2C561A6D-90F4-4649-80B3-76A854A64EA2;finout;manage-dashboards;Global; +5D4BC161-BDED-4078-872F-494F3A515381;2026-07-23T09:15:35.802324;Role;2C561A6D-90F4-4649-80B3-76A854A64EA2;finout;manage-alerts;Global; +756D3132-687F-4A5C-AEC3-37B3001589AE;2026-07-23T09:15:35.802328;Role;2C561A6D-90F4-4649-80B3-76A854A64EA2;finout;read-alerts;Global; +6779F916-3E6D-490D-BE5C-B94F5A907672;2026-07-23T09:15:35.802332;Role;2C561A6D-90F4-4649-80B3-76A854A64EA2;azure;create;Global; +4F7BA42E-40E8-45BE-830E-45C3888470A5;2026-07-23T09:15:35.802364;Role;2C561A6D-90F4-4649-80B3-76A854A64EA2;azure;read;Global; +8BE5B8C9-2ADB-4A48-8A4D-B0ACC5D609E8;2026-07-23T09:15:35.802369;Role;2C561A6D-90F4-4649-80B3-76A854A64EA2;azure;read-provider;Global; +79693C7B-B9DF-4228-B467-68892D24860F;2026-07-23T09:15:35.802373;Role;2C561A6D-90F4-4649-80B3-76A854A64EA2;azure;manage-provider;Global; +1B015EC2-FCC4-4EE8-9FB6-1364C10B4D48;2026-07-23T09:15:35.802378;Role;22DAB91B-C2D8-4840-A173-1416EF1B882D;topics;read-public;Global; +8DAB5132-2D1C-4889-96C0-87F0932FD174;2026-07-23T09:15:35.802382;Role;22DAB91B-C2D8-4840-A173-1416EF1B882D;topics;read-private;Global; +4A6F9403-5E5B-4D4E-B9BF-80ABAC201E01;2026-07-23T09:15:35.802387;Role;22DAB91B-C2D8-4840-A173-1416EF1B882D;capability-membership-management;read;Global; +D0A528F6-E359-4BC5-A6B4-88C58EDC62A4;2026-07-23T09:15:35.802391;Role;22DAB91B-C2D8-4840-A173-1416EF1B882D;capability-membership-management;read-requests;Global; +D94C9D17-E297-4F65-ACF0-FD11A4FA2E7D;2026-07-23T09:15:35.802395;Role;22DAB91B-C2D8-4840-A173-1416EF1B882D;tags-and-metadata;read;Global; +17426823-83AE-4A75-AA77-68968445A66F;2026-07-23T09:15:35.802399;Role;22DAB91B-C2D8-4840-A173-1416EF1B882D;aws;read;Global; +94D0EF39-EA11-4C34-A972-6A56EBA39E05;2026-07-23T09:15:35.802403;Role;22DAB91B-C2D8-4840-A173-1416EF1B882D;aws;read-provider;Global; +35268B4C-B82F-43C0-9C3E-FE1693BBB4E2;2026-07-23T09:15:35.802407;Role;22DAB91B-C2D8-4840-A173-1416EF1B882D;finout;read-dashboards;Global; +B316CE3E-CA4B-4201-8DD2-0C66EA6E8893;2026-07-23T09:15:35.802411;Role;22DAB91B-C2D8-4840-A173-1416EF1B882D;finout;read-alerts;Global; +4BF97729-9C04-458A-9AE1-1C2B8C4E2448;2026-07-23T09:15:35.802415;Role;22DAB91B-C2D8-4840-A173-1416EF1B882D;azure;read;Global; +24A70679-816D-4542-A291-17C9E3750E9E;2026-07-23T09:15:35.802418;Role;22DAB91B-C2D8-4840-A173-1416EF1B882D;azure;read-provider;Global; +E0582B2F-CDE8-423D-8C46-B5E6C893B389;2026-07-23T09:15:35.802423;Role;F67CACC9-8DD4-4481-AC15-00B5DD83B046;topics;read-public;Global; +62828754-CA11-40EA-8E12-1CD66C423A04;2026-07-23T09:15:35.802427;Role;F67CACC9-8DD4-4481-AC15-00B5DD83B046;capability-membership-management;read;Global; +FE0F02C6-BB2C-4EA5-A2E7-57ED49E007DF;2026-07-23T09:15:35.802431;Role;F67CACC9-8DD4-4481-AC15-00B5DD83B046;tags-and-metadata;read;Global; +5B945857-7EA8-4540-92A6-9D53C649A8DD;2026-07-23T09:15:35.802435;Role;F67CACC9-8DD4-4481-AC15-00B5DD83B046;finout;read-dashboards;Global; +260E3A07-50BD-48A7-A7AF-FBD3CE681E3C;2026-07-23T09:15:35.802439;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;topics;create;Global; +5668DE32-F17B-4466-85BF-EF918BC10791;2026-07-23T09:15:35.802443;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;topics;read-public;Global; +471B5C65-5E85-4FCF-8A4F-58D023055DBB;2026-07-23T09:15:35.802447;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;topics;read-private;Global; +59EB2F5A-85DB-45DA-A099-5B996F042F72;2026-07-23T09:15:35.802451;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;topics;update;Global; +7C04DE78-FC43-4281-BCD7-0A5909ED45E6;2026-07-23T09:15:35.802455;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;topics;delete;Global; +6F001F31-306A-4D44-8760-E77742E97BF5;2026-07-23T09:15:35.802459;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;capability-management;receive-alerts;Global; +5B1ABCEF-5E34-461E-B933-5FC8925E175D;2026-07-23T09:15:35.802463;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;capability-management;receive-cost;Global; +CDE9EC1D-C3BC-42CF-8FF4-4676DE2DF5E0;2026-07-23T09:15:35.802467;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;capability-management;request-deletion;Global; +8F6C9030-5561-43B3-ABAB-AAC4D5FDAAFD;2026-07-23T09:15:35.802471;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;capability-management;manage-permissions;Global; +3BAF39FA-E1F7-405E-BDDE-E54C7A6414EC;2026-07-23T09:15:35.802475;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;capability-management;read-self-assess;Global; +BEA0704D-6B88-40B6-93C9-72C567016502;2026-07-23T09:15:35.802479;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;capability-management;create-self-assess;Global; +AA3ED47C-5372-49CC-BDA9-94EB7405286B;2026-07-23T09:15:35.802483;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;capability-management;batch-create-capabilities;Global; +A7E0615B-6BB6-4C46-B52C-1F532B882A20;2026-07-23T09:15:35.802487;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;capability-membership-management;create;Global; +553B57ED-0B11-4796-8DFF-C87374E13698;2026-07-23T09:15:35.802492;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;capability-membership-management;delete;Global; +2E6F2BA9-F2AC-42B1-A881-2AAD75DD1C67;2026-07-23T09:15:35.802496;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;capability-membership-management;read;Global; +3D95E997-6B97-4971-8225-19291F44D5E5;2026-07-23T09:15:35.802500;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;capability-membership-management;read-requests;Global; +7D322740-BABD-4743-A1F9-85A06F14BEF8;2026-07-23T09:15:35.802504;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;capability-membership-management;manage-requests;Global; +45A0C9C9-B115-476D-9418-A7852F1FAFB0;2026-07-23T09:15:35.802508;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;tags-and-metadata;create;Global; +429EF882-8EED-4C8F-B3F2-633CD0D89FD5;2026-07-23T09:15:35.802512;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;tags-and-metadata;read;Global; +809B28B6-13A2-4996-A28B-5F0C6832F572;2026-07-23T09:15:35.802516;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;tags-and-metadata;update;Global; +E4A77F04-C2EB-4E33-AF19-C1B9F7E249C5;2026-07-23T09:15:35.802520;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;tags-and-metadata;delete;Global; +59F5B9B1-0B16-4907-9892-CAC8D46806DD;2026-07-23T09:15:35.802524;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;aws;create;Global; +1D1C7EB7-2EB4-4A26-A7E3-537BA1F08D80;2026-07-23T09:15:35.802528;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;aws;read;Global; +486D49C3-A72F-4665-9112-10264EF479DC;2026-07-23T09:15:35.802532;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;aws;manage-provider;Global; +986F8E0E-320B-44F2-A95E-56D67774D2B8;2026-07-23T09:15:35.802535;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;aws;read-provider;Global; +533DE13A-9310-45E4-A845-12743F792074;2026-07-23T09:15:35.802539;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;finout;read-dashboards;Global; +160521A7-CE80-41E3-8F44-AAAB5772BA39;2026-07-23T09:15:35.802543;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;finout;manage-dashboards;Global; +AF53572A-ACD9-4A6B-B17B-FAC7293B3527;2026-07-23T09:15:35.802547;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;finout;manage-alerts;Global; +222F6368-DF53-4675-93BE-B5F3F89079F7;2026-07-23T09:15:35.802551;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;finout;read-alerts;Global; +D341E34E-BE0C-48F0-B651-9CFB84EC32FF;2026-07-23T09:15:35.802555;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;azure;create;Global; +0824258B-63A2-4CFE-9218-ED3A594DE109;2026-07-23T09:15:35.802559;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;azure;read;Global; +CD94038F-B325-4625-A50D-EA9E758E55C6;2026-07-23T09:15:35.802563;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;azure;read-provider;Global; +DBA916DD-F2EB-4A59-B780-00B88D4FADB3;2026-07-23T09:15:35.802567;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;azure;manage-provider;Global; +F033D884-EA98-4230-AB31-BE9D50579B16;2026-07-23T09:15:35.802570;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;rbac;create;Global; +F573E433-D2A6-4D99-A3D2-85952DE94EB9;2026-07-23T09:15:35.802574;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;rbac;read;Global; +9B56595A-22AD-4E96-A318-AC8E5C442B4D;2026-07-23T09:15:35.802578;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;rbac;update;Global; +9F28ACD9-082C-48B7-A0F6-737D17388CD7;2026-07-23T09:15:35.802582;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;rbac;delete;Global; +3766C7F2-675F-4DE6-8367-50E0968459CF;2026-07-23T09:15:35.802585;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;service-catalogue;read;Global; +51442D3E-CE6C-4407-9D69-ACE32C4FC7CD;2026-07-23T09:15:35.802590;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;system-legacy;read;Global; +66EEDB4E-48F2-4450-AFD6-8150DA8C91DE;2026-07-23T09:15:35.802594;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;system-admin;view-deleted-capabilities;Global; +3B7336CF-F6F1-41FD-AE4F-3E3469223740;2026-07-23T09:15:35.802598;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;system-admin;unset-capability-tags;Global; +9DE53FD2-FCF2-49AF-82E3-059B9CE6EE01;2026-07-23T09:15:35.802607;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;system-admin;create-demo-recording;Global; +094C2E55-2C74-4136-91F6-C61EFD524913;2026-07-23T09:15:35.802611;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;system-admin;update-demo-recording;Global; +70A6217D-947B-4E79-829A-8332955CD1BE;2026-07-23T09:15:35.802615;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;system-admin;delete-demo-recording;Global; +37648039-F90E-47DE-83DA-71431870ACCA;2026-07-23T09:15:35.802620;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;system-admin;manage-permission-matrix;Global; +F93F8A4A-FDED-43D4-98EA-AB74F67A1893;2026-07-23T09:15:35.802624;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;system-admin;synchronize-aws-ecr-and-database-ecr;Global; +59B5CC02-9D5C-4762-9EB6-D884BB959F90;2026-07-23T09:15:35.802628;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;system-admin;bypass-membership-approvals;Global; +5F685936-A455-4A03-BE71-B4BD02ACE8A7;2026-07-23T09:15:35.802632;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;system-admin;manage-self-assessment-options;Global; +2117F27C-CDEB-46D5-8E4B-EB618E9DA6ED;2026-07-23T09:15:35.802636;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;system-admin;create-release-notes;Global; +DB339914-DF95-4AC3-9236-33262A0F5381;2026-07-23T09:15:35.802640;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;system-admin;update-release-note;Global; +99C2892C-B283-4CC5-9BF9-89FC51A6F52A;2026-07-23T09:15:35.802644;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;system-admin;toggle-release-note-is-active;Global; +F272A151-CE15-49EB-8126-24FF128A8DE7;2026-07-23T09:15:35.802648;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;system-admin;list-draft-release-notes;Global; +D435D795-24A7-4B4D-9DB0-BB0EC6179115;2026-07-23T09:15:35.802652;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;system-admin;remove-release-note;Global; +D6E3BDE2-607F-4B64-998F-2C550728B694;2026-07-23T09:15:35.802656;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;system-admin;create-event;Global; +F7BA0993-5E2E-41D4-BA41-757CD0B3B485;2026-07-23T09:15:35.802660;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;system-admin;update-event;Global; +D82DC6F8-4E45-4AE9-9FE2-29EC1B115C9E;2026-07-23T09:15:35.802664;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;system-admin;delete-event;Global; +12B2C367-EBA8-4369-9780-A3F50A0412DF;2026-07-23T09:15:35.802668;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;system-admin;create-news-item;Global; +8F5947C1-977C-4F95-8C09-DE2F2B8741DC;2026-07-23T09:15:35.802672;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;system-admin;update-news-item;Global; +BE05A459-AA3A-4A74-B300-62BA19B50618;2026-07-23T09:15:35.802676;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;system-admin;delete-news-item;Global; +B908B85B-8489-4E5F-8130-BFA4FAA7A631;2026-07-23T09:15:35.802680;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;system-admin;get-user-emails;Global; +FAABC901-C540-4FDD-9408-16812B3683E3;2026-07-23T09:15:35.802684;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;system-admin;delete-membership-application-as-admin;Global; +708B2C75-2CDD-4545-A717-D5BBDE8C9884;2026-07-23T09:15:35.802688;Role;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;system-admin;retry-creating-message-contract;Global; +7E6A90BB-9E8C-4F11-AFE8-F53D97B4B803;2026-07-23T09:15:35.802693;Role;6A2EE52C-6A9B-4A2A-B9C8-5851DD2D9A6F;capability-management;batch-create-capabilities;Global; +213F793E-3048-427E-863C-D359BBA7D9CA;2026-07-23T09:15:35.802697;Role;A983CF2E-772E-437D-B9D8-5DDF769339D3;service-catalogue;read;Global; diff --git a/db/seed/RbacRole.csv b/db/seed/RbacRole.csv index c926eb23..3fa8de18 100644 --- a/db/seed/RbacRole.csv +++ b/db/seed/RbacRole.csv @@ -1,5 +1,8 @@ Id;OwnerId;CreatedAt;UpdatedAt;Name;Description;Type -36202DFB-D106-440D-8B99-F11BC8D77C9C;0000DFD5-0000-0000-0000-00000000000A;2025-11-04T08:45:52.056886;2025-11-04T08:45:52.056909;Owner;Full access to all resources;Global -2C561A6D-90F4-4649-80B3-76A854A64EA2;0000DFD5-0000-0000-0000-00000000000A;2025-11-04T08:45:52.056913;2025-11-04T08:45:52.056914;Contributor;Can modify existing resources;Global -22DAB91B-C2D8-4840-A173-1416EF1B882D;0000DFD5-0000-0000-0000-00000000000A;2025-11-04T08:45:52.056917;2025-11-04T08:45:52.056917;Reader;Read-only access;Global -F67CACC9-8DD4-4481-AC15-00B5DD83B046;0000DFD5-0000-0000-0000-00000000000A;2025-11-04T08:45:52.056920;2025-11-04T08:45:52.056921;Guest;Very limited access;Global +36202DFB-D106-440D-8B99-F11BC8D77C9C;0000DFD5-0000-0000-0000-00000000000A;2026-07-23T09:15:35.801873;2026-07-23T09:15:35.801901;Owner;Role: Owner;Global +2C561A6D-90F4-4649-80B3-76A854A64EA2;0000DFD5-0000-0000-0000-00000000000A;2026-07-23T09:15:35.801908;2026-07-23T09:15:35.801909;Contributor;Role: Contributor;Global +22DAB91B-C2D8-4840-A173-1416EF1B882D;0000DFD5-0000-0000-0000-00000000000A;2026-07-23T09:15:35.801913;2026-07-23T09:15:35.801914;Reader;Role: Reader;Global +F67CACC9-8DD4-4481-AC15-00B5DD83B046;0000DFD5-0000-0000-0000-00000000000A;2026-07-23T09:15:35.801917;2026-07-23T09:15:35.801918;Guest;Role: Guest;Global +5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;0000DFD5-0000-0000-0000-00000000000A;2026-07-23T09:15:35.801921;2026-07-23T09:15:35.801922;CloudEngineer;Role: CloudEngineer;Global +6A2EE52C-6A9B-4A2A-B9C8-5851DD2D9A6F;0000DFD5-0000-0000-0000-00000000000A;2026-07-23T09:15:35.801925;2026-07-23T09:15:35.801926;BatchCapabilityCreator;Role: BatchCapabilityCreator;Global +A983CF2E-772E-437D-B9D8-5DDF769339D3;0000DFD5-0000-0000-0000-00000000000A;2026-07-23T09:15:35.801929;2026-07-23T09:15:35.801930;ServiceCatalogueReader;Role: ServiceCatalogueReader;Global diff --git a/db/seed/RbacRoleGrants.csv b/db/seed/RbacRoleGrants.csv index cec28d14..67282d0b 100644 --- a/db/seed/RbacRoleGrants.csv +++ b/db/seed/RbacRoleGrants.csv @@ -1,9 +1,4 @@ Id;RoleId;CreatedAt;AssignedEntityType;AssignedEntityId;Type;Resource -FCC8821B-D9E1-4E0D-9D86-1F80965FBA87;36202DFB-D106-440D-8B99-F11BC8D77C9C;2000-01-01;User;andfris@dfds.com;Capability;cloudengineering-xxx -FCC8821B-D9E1-4E0D-9D86-1F80965FBA79;22DAB91B-C2D8-4840-A173-1416EF1B882D;2000-01-01;User;andfris@dfds.com;Capability;cool-beans-xxx -FCC8821B-D9E1-4E0D-9D86-1F80965FBA86;22DAB91B-C2D8-4840-A173-1416EF1B882D;2000-01-01;User;emcla@dfds.com;Capability;cloudengineering-xxx -FCC8821B-D9E1-4E0D-9D86-1F80965FBA88;22DAB91B-C2D8-4840-A173-1416EF1B882D;2000-01-01;User;emcla@dfds.com;Global; -FCC8821B-D9E1-4E0D-9D86-1F80965FBA89;36202DFB-D106-440D-8B99-F11BC8D77C9C;2000-01-01;User;owner@bar.com;Capability;bar -FCC8821B-D9E1-4E0D-9D86-1F80965FBA90;2C561A6D-90F4-4649-80B3-76A854A64EA2;2000-01-01;User;contributor@bar.com;Capability;bar -FCC8821B-D9E1-4E0D-9D86-1F80965FBA91;22DAB91B-C2D8-4840-A173-1416EF1B882D;2000-01-01;User;reader@bar.com;Capability;bar -FCC8821B-D9E1-4E0D-9D86-1F80965FBA92;36202DFB-D106-440D-8B99-F11BC8D77C9C;2000-01-01;User;other@foo.com;Capability;foo +E02D91E6-DE3C-4C55-BC0A-EDABD1492197;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;2026-07-23T09:15:35.803819;Group;BA0AA734-206D-454E-AC95-14855B6901E4;Global; +4CE54588-72CB-4B27-93D3-7F842B9C91AF;6A2EE52C-6A9B-4A2A-B9C8-5851DD2D9A6F;2026-07-23T09:15:35.803831;Group;899F8F9E-3F7E-4EF3-A2CD-2E1DDA78E40A;Global; +059F294D-4FD4-4836-9B11-9A7E8FA125C7;A983CF2E-772E-437D-B9D8-5DDF769339D3;2026-07-23T09:15:35.803837;Group;7D3D7498-9075-4D1B-B6C1-AF6E95788C3B;Global; From 56b5bf54f05771192ffc9ef69e0c4eaa39896f63 Mon Sep 17 00:00:00 2001 From: Andreas Frisch Date: Thu, 23 Jul 2026 12:09:34 +0200 Subject: [PATCH 4/9] updated configs --- tools/config.json | 50 ++++++++++++++++++++++++++++++++------ tools/config_skeleton.json | 7 ++++++ 2 files changed, 49 insertions(+), 8 deletions(-) diff --git a/tools/config.json b/tools/config.json index ab986789..3cf4fd97 100644 --- a/tools/config.json +++ b/tools/config.json @@ -1,20 +1,48 @@ { "debug": true, "apiUrl": "http://localhost:8080", - "cloudengineers": [ - "andfris@dfds.com" - ], - "batchCapabilityCreators": [], - "serviceCatalogueReaders": [], - "cloudengineerRoles": [ + "groups": [ + { + "name": "CloudEngineers", + "existingId": "BA0AA734-206D-454E-AC95-14855B6901E4", + "roles": [ + { + "roleName": "CloudEngineer", + "scope": "Global" + } + ], + "members": [ + "andfris@dfds.com", + "emcla@dfds.com" + ] + }, + { + "name": "BatchCapabilityCreators", + "existingId": "899F8F9E-3F7E-4EF3-A2CD-2E1DDA78E40A", + "roles": [ + { + "roleName": "BatchCapabilityCreator", + "scope": "Global" + } + ], + "members": [] + }, { - "roleName": "CloudEngineer", - "scope": "Global" + "name": "ServiceCatalogueReaders", + "existingId": "7D3D7498-9075-4D1B-B6C1-AF6E95788C3B", + "roles": [ + { + "roleName": "ServiceCatalogueReader", + "scope": "Global" + } + ], + "members": [] } ], "roles": [ { "name": "Owner", + "existingId": "36202DFB-D106-440D-8B99-F11BC8D77C9C", "permissions": { "topics": [ "create", @@ -72,6 +100,7 @@ }, { "name": "Contributor", + "existingId": "2C561A6D-90F4-4649-80B3-76A854A64EA2", "permissions": { "topics": [ "create", @@ -117,6 +146,7 @@ }, { "name": "Reader", + "existingId": "22DAB91B-C2D8-4840-A173-1416EF1B882D", "permissions": { "topics": [ "read-public", @@ -145,6 +175,7 @@ }, { "name": "Guest", + "existingId": "F67CACC9-8DD4-4481-AC15-00B5DD83B046", "permissions": { "topics": [ "read-public" @@ -162,6 +193,7 @@ }, { "name": "CloudEngineer", + "existingId": "5E32EE6A-1A73-4ACF-9C61-90E4D0D59261", "permissions": { "topics": [ "create", @@ -251,6 +283,7 @@ }, { "name": "BatchCapabilityCreator", + "existingId": "6A2EE52C-6A9B-4A2A-B9C8-5851DD2D9A6F", "permissions": { "capability-management": [ "batch-create-capabilities" @@ -259,6 +292,7 @@ }, { "name": "ServiceCatalogueReader", + "existingId": "A983CF2E-772E-437D-B9D8-5DDF769339D3", "permissions": { "service-catalogue": [ "read" diff --git a/tools/config_skeleton.json b/tools/config_skeleton.json index df195ec3..13fefe23 100644 --- a/tools/config_skeleton.json +++ b/tools/config_skeleton.json @@ -24,6 +24,7 @@ "roles": [ { "name": "Owner", + "existingId": "36202DFB-D106-440D-8B99-F11BC8D77C9C", "permissions": { "topics": [ "create", @@ -81,6 +82,7 @@ }, { "name": "Contributor", + "existingId": "2C561A6D-90F4-4649-80B3-76A854A64EA2", "permissions": { "topics": [ "create", @@ -126,6 +128,7 @@ }, { "name": "Reader", + "existingId": "22DAB91B-C2D8-4840-A173-1416EF1B882D", "permissions": { "topics": [ "read-public", @@ -154,6 +157,7 @@ }, { "name": "Guest", + "existingId": "F67CACC9-8DD4-4481-AC15-00B5DD83B046", "permissions": { "topics": [ "read-public" @@ -171,6 +175,7 @@ }, { "name": "CloudEngineer", + "existingId": "5E32EE6A-1A73-4ACF-9C61-90E4D0D59261", "permissions": { "topics": [ "create", @@ -260,6 +265,7 @@ }, { "name": "BatchCapabilityCreator", + "existingId": "6A2EE52C-6A9B-4A2A-B9C8-5851DD2D9A6F", "permissions": { "capability-management": [ "batch-create-capabilities" @@ -268,6 +274,7 @@ }, { "name": "ServiceCatalogueReader", + "existingId": "A983CF2E-772E-437D-B9D8-5DDF769339D3", "permissions": { "service-catalogue": [ "read" From 148b6cfac1e1d83059b8ef2e745d5df04dd03c94 Mon Sep 17 00:00:00 2001 From: Andreas Frisch Date: Thu, 23 Jul 2026 12:09:49 +0200 Subject: [PATCH 5/9] normalize uuids for comparison --- .../Application/RbacApplicationService.cs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/SelfService/Application/RbacApplicationService.cs b/src/SelfService/Application/RbacApplicationService.cs index a57632aa..d17ac416 100644 --- a/src/SelfService/Application/RbacApplicationService.cs +++ b/src/SelfService/Application/RbacApplicationService.cs @@ -172,7 +172,8 @@ public async Task> GetPermissionGrantsForUser(string u user, () => _permissionGrantRepository.GetAllWithPredicate(p => - p.AssignedEntityType == AssignedEntityType.User && p.AssignedEntityId == user + p.AssignedEntityType == AssignedEntityType.User + && string.Equals(p.AssignedEntityId, user, StringComparison.OrdinalIgnoreCase) ) ); } @@ -184,7 +185,8 @@ public async Task> GetRoleGrantsForUser(string user) user, () => _roleGrantRepository.GetAllWithPredicate(p => - p.AssignedEntityType == AssignedEntityType.User && p.AssignedEntityId == user + p.AssignedEntityType == AssignedEntityType.User + && string.Equals(p.AssignedEntityId, user, StringComparison.OrdinalIgnoreCase) ) ); } @@ -205,7 +207,8 @@ public async Task> GetPermissionGrantsForGroup(string groupId, () => _permissionGrantRepository.GetAllWithPredicate(p => - p.AssignedEntityType == AssignedEntityType.Group && p.AssignedEntityId == groupId + p.AssignedEntityType == AssignedEntityType.Group + && string.Equals(p.AssignedEntityId, groupId, StringComparison.OrdinalIgnoreCase) ) ); } @@ -217,7 +220,8 @@ public async Task> GetPermissionGrantsForRole(string r roleId, () => _permissionGrantRepository.GetAllWithPredicate(p => - p.AssignedEntityType == AssignedEntityType.Role && p.AssignedEntityId == roleId + p.AssignedEntityType == AssignedEntityType.Role + && string.Equals(p.AssignedEntityId, roleId, StringComparison.OrdinalIgnoreCase) ) ); } @@ -266,7 +270,8 @@ public async Task> GetRoleGrantsForGroup(string groupId) groupId, () => _roleGrantRepository.GetAllWithPredicate(p => - p.AssignedEntityType == AssignedEntityType.Group && p.AssignedEntityId == groupId + p.AssignedEntityType == AssignedEntityType.Group + && string.Equals(p.AssignedEntityId, groupId, StringComparison.OrdinalIgnoreCase) ) ); } From bdc207dd59fb71e4e4b6a3faa6199d784c0f41e0 Mon Sep 17 00:00:00 2001 From: Andreas Frisch Date: Thu, 23 Jul 2026 12:10:06 +0200 Subject: [PATCH 6/9] permission generators --- tools/generate-rbac-seed.py | 358 +++++++++++++++++++--------- tools/setup-baseline-permissions.go | 161 ++++++++++--- 2 files changed, 367 insertions(+), 152 deletions(-) diff --git a/tools/generate-rbac-seed.py b/tools/generate-rbac-seed.py index f142b8d8..7e5ae7ab 100644 --- a/tools/generate-rbac-seed.py +++ b/tools/generate-rbac-seed.py @@ -1,127 +1,255 @@ import csv +import json import uuid from datetime import datetime +from pathlib import Path + + +CONFIG_PATH = Path(__file__).with_name("config.json") +OWNER_ID = "0000DFD5-0000-0000-0000-00000000000A" +SEED_NAMESPACE = uuid.UUID("bda25e7c-1124-4fca-9f7e-70e28d1901da") -# --- Configuration --- - -# Define your roles -ROLES = [ - { - "name": "Owner", - "existing-id": "36202DFB-D106-440D-8B99-F11BC8D77C9C", - "description": "Full access to all resources" - }, - { - "name": "Contributor", - "existing-id": "2C561A6D-90F4-4649-80B3-76A854A64EA2", - "description": "Can modify existing resources" - }, - { - "name": "Reader", - "existing-id": "22DAB91B-C2D8-4840-A173-1416EF1B882D", - "description": "Read-only access" - }, - { - "name": "Guest", - "existing-id": "F67CACC9-8DD4-4481-AC15-00B5DD83B046", - "description": "Very limited access" - }, -] - -# Define permissions per role (namespace → list of permissions) -ROLE_PERMISSIONS = { - "Owner": { - "topics": ["create", "read-public", "read-private", "update", "delete"], - "capability-management": ["receive-alerts", "receive-cost", "request-deletion", "manage-permissions", "read-self-assess", "create-self-assess"], - "capability-membership-management": ["create", "delete", "read", "read-requests", "manage-requests"], - "tags-and-metadata": ["create", "read", "update", "delete"], - "aws": ["create", "read", "manage-provider", "read-provider"], - "finout": ["read-dashboards", "manage-dashboards", "manage-alerts", "read-alerts"], - "azure": ["create", "read", "read-provider", "manage-provider"], - "rbac": ["create", "read", "delete"], - }, - "Contributor": { - "topics": ["create", "read-public", "read-private", "update", "delete"], - "capability-management": ["receive-alerts"], - "capability-membership-management": ["create", "read", "read-requests", "manage-requests"], - "tags-and-metadata": ["create", "read", "update", "delete"], - "aws": ["create", "read", "manage-provider", "read-provider"], - "finout": ["read-dashboards", "manage-dashboards", "manage-alerts", "read-alerts"], - "azure": ["create", "read", "read-provider", "manage-provider"], - "rbac": ["read"], - }, - "Reader": { - "topics": ["read-public", "read-private"], - "capability-membership-management": ["read", "read-requests"], - "tags-and-metadata": ["read"], - "aws": ["read", "read-provider"], - "finout": ["read-dashboards", "read-alerts"], - "azure": ["read", "read-provider"], - "rbac": ["read"], - }, - "Guest": { - "topics": ["read-public"], - "capability-membership-management": ["read"], - "tags-and-metadata": ["read"], - "finout": ["read-dashboards"], - "rbac": ["read"], - }, -} - -# --- Utility functions --- def new_uuid(): - return str(uuid.uuid4()) + return str(uuid.uuid4()).upper() + def now_iso(): return datetime.utcnow().isoformat() -# --- Generate RbacRole.csv --- - -role_id_map = {} # Map role name → UUID - -with open("RbacRole.csv", "w", newline="") as csvfile: - writer = csv.writer(csvfile, delimiter=";") - writer.writerow(["Id", "OwnerId", "CreatedAt", "UpdatedAt", "Name", "Description", "Type"]) - - owner_id = "0000DFD5-0000-0000-0000-00000000000A" - - for role in ROLES: - role_id = "" - if (role["existing-id"]): - role_id = role["existing-id"] - else: - role_id = new_uuid() - role_id_map[role["name"]] = role_id - writer.writerow([ - role_id, - owner_id, # OwnerId currently unused - now_iso(), - now_iso(), - role["name"], - role["description"], - "Global", - ]) - -# --- Generate RbacPermissionGrants.csv --- - -with open("RbacPermissionGrants.csv", "w", newline="") as csvfile: - writer = csv.writer(csvfile, delimiter=";") - writer.writerow(["Id", "CreatedAt", "AssignedEntityType", "AssignedEntityId", "Namespace", "Permission", "Type", "Resource"]) - - for role_name, namespaces in ROLE_PERMISSIONS.items(): - role_id = role_id_map[role_name] - for namespace, permissions in namespaces.items(): - for perm in permissions: - writer.writerow([ + +def stable_uuid(kind, name): + value = uuid.uuid5(SEED_NAMESPACE, f"{kind}:{name.strip().lower()}") + return str(value).upper() + + +def load_config(): + with open(CONFIG_PATH, "r", encoding="utf-8") as f: + return json.load(f) + + +def resolve_role_id(role): + # Support both current and legacy property names. + existing = role.get("existingId") or role.get("existing-id") + if existing: + return existing + return stable_uuid("role", role.get("name", "")) + + +def resolve_group_id(group): + # Support both current and legacy property names. + existing = group.get("existingId") or group.get("existing-id") + if existing: + return existing + return stable_uuid("group", group.get("name", "")) + + +def create_role_id_map(roles): + role_id_map = {} + for role in roles: + name = role.get("name") + if not name: + raise ValueError("Role entry is missing 'name'") + if name in role_id_map: + raise ValueError(f"Duplicate role name in config: {name}") + role_id_map[name] = resolve_role_id(role) + return role_id_map + + +def create_group_id_map(groups): + group_id_map = {} + for group in groups: + name = group.get("name") + if not name: + raise ValueError("Group entry is missing 'name'") + if name in group_id_map: + raise ValueError(f"Duplicate group name in config: {name}") + group_id_map[name] = resolve_group_id(group) + return group_id_map + + +def write_roles_csv(roles, role_id_map): + with open("RbacRole.csv", "w", newline="", encoding="utf-8") as csvfile: + writer = csv.writer(csvfile, delimiter=";") + writer.writerow(["Id", "OwnerId", "CreatedAt", "UpdatedAt", "Name", "Description", "Type"]) + + for role in roles: + name = role["name"] + writer.writerow( + [ + role_id_map[name], + OWNER_ID, + now_iso(), + now_iso(), + name, + role.get("description") or f"Role: {name}", + role.get("type") or "Global", + ] + ) + + +def write_permission_grants_csv(roles, role_id_map): + with open("RbacPermissionGrants.csv", "w", newline="", encoding="utf-8") as csvfile: + writer = csv.writer(csvfile, delimiter=";") + writer.writerow( + [ + "Id", + "CreatedAt", + "AssignedEntityType", + "AssignedEntityId", + "Namespace", + "Permission", + "Type", + "Resource", + ] + ) + + for role in roles: + role_name = role["name"] + role_id = role_id_map[role_name] + permissions_by_namespace = role.get("permissions", {}) + for namespace, permissions in permissions_by_namespace.items(): + for permission in permissions: + writer.writerow( + [ + new_uuid(), + now_iso(), + "Role", + role_id, + namespace, + permission, + "Global", + "", + ] + ) + + +def write_groups_csv(groups, group_id_map): + with open("RbacGroup.csv", "w", newline="", encoding="utf-8") as csvfile: + writer = csv.writer(csvfile, delimiter=";") + writer.writerow(["Id", "CreatedAt", "UpdatedAt", "Name", "Description"]) + + for group in groups: + name = group["name"] + writer.writerow( + [ + group_id_map[name], + now_iso(), + now_iso(), + name, + group.get("description") or f"Group: {name}", + ] + ) + + +def write_group_members_csv(groups, group_id_map): + with open("RbacGroupMember.csv", "w", newline="", encoding="utf-8") as csvfile: + writer = csv.writer(csvfile, delimiter=";") + writer.writerow(["Id", "GroupId", "UserId", "CreatedAt"]) + + seen = set() + for group in groups: + group_id = group_id_map[group["name"]] + for member in group.get("members", []): + key = (group_id, member.lower()) + if key in seen: + continue + seen.add(key) + writer.writerow([new_uuid(), group_id, member, now_iso()]) + + +def append_group_role_grant(grants, role_id_map, group_id, role_name, grant_type="Global", resource=""): + role_id = role_id_map.get(role_name) + if not role_id: + raise ValueError(f"Role '{role_name}' referenced by grants was not found in config.roles") + + grants.append( + { + "RoleId": role_id, + "AssignedEntityType": "Group", + "AssignedEntityId": group_id, + "Type": grant_type, + "Resource": resource, + } + ) + + +def build_role_grants(groups, role_id_map, group_id_map): + grants = [] + + for group in groups: + group_id = group_id_map[group["name"]] + for binding in group.get("roles", []): + role_name = binding.get("roleName") + if not role_name: + continue + + grant_type = binding.get("scope") or "Global" + resource = binding.get("resource") or "" + append_group_role_grant(grants, role_id_map, group_id, role_name, grant_type, resource) + + # Remove accidental duplicates while preserving first occurrence order. + deduped = [] + seen = set() + for grant in grants: + key = ( + grant["RoleId"], + grant["AssignedEntityType"], + grant["AssignedEntityId"], + grant["Type"], + grant["Resource"], + ) + if key in seen: + continue + seen.add(key) + deduped.append(grant) + + return deduped + + +def write_role_grants_csv(grants): + with open("RbacRoleGrants.csv", "w", newline="", encoding="utf-8") as csvfile: + writer = csv.writer(csvfile, delimiter=";") + writer.writerow(["Id", "RoleId", "CreatedAt", "AssignedEntityType", "AssignedEntityId", "Type", "Resource"]) + + for grant in grants: + writer.writerow( + [ new_uuid(), + grant["RoleId"], now_iso(), - "Role", - role_id, - namespace, - perm, - "Global", - "" - ]) - -print("✅ CSV files generated: RbacRole.csv, RbacPermissionGrants.csv") + grant["AssignedEntityType"], + grant["AssignedEntityId"], + grant["Type"], + grant["Resource"], + ] + ) + + +def main(): + config = load_config() + roles = config.get("roles", []) + if not roles: + raise ValueError("No roles found in config.json") + + groups = config.get("groups", []) + + role_id_map = create_role_id_map(roles) + group_id_map = create_group_id_map(groups) + + write_roles_csv(roles, role_id_map) + write_permission_grants_csv(roles, role_id_map) + write_groups_csv(groups, group_id_map) + write_group_members_csv(groups, group_id_map) + + role_grants = build_role_grants(groups, role_id_map, group_id_map) + write_role_grants_csv(role_grants) + + print( + "✅ CSV files generated: RbacRole.csv, RbacGroup.csv, RbacGroupMember.csv, " + "RbacPermissionGrants.csv, RbacRoleGrants.csv" + ) + + +if __name__ == "__main__": + main() diff --git a/tools/setup-baseline-permissions.go b/tools/setup-baseline-permissions.go index 338e9dd8..20ea8d53 100644 --- a/tools/setup-baseline-permissions.go +++ b/tools/setup-baseline-permissions.go @@ -45,6 +45,12 @@ func main() { } for _, role := range config.Roles { + if shouldSkipRole(role.Name) { + if config.Debug { + log.Printf("Skipping role '%s' in baseline sync (managed implicitly by API semantics).", role.Name) + } + continue + } if _, exists := availableRoles[strings.ToLower(role.Name)]; !exists { log.Printf("required role '%s' does not exist in the system; creating it...", role) createRole(config, role) @@ -55,6 +61,9 @@ func main() { for name := range availableRoles { found := false for _, role := range config.Roles { + if shouldSkipRole(role.Name) { + continue + } if strings.EqualFold(name, role.Name) { found = true break @@ -89,6 +98,13 @@ func main() { */ for _, role := range config.Roles { + if shouldSkipRole(role.Name) { + if config.Debug { + log.Printf(">> Skipping permissions verification for role: %s", role.Name) + } + continue + } + if config.Debug { log.Printf(">> Verifying permissions for role: %s", role.Name) } @@ -103,18 +119,17 @@ func main() { log.Fatalf("failed to fetch permissions for role '%s': %v", role.Name, err) } - for namespace, perms := range role.Permissions { - expectedPerms, ok := role.Permissions[namespace] - if !ok { - log.Printf("- WARNING: role '%s' has unexpected namespace '%s' with permissions %v. Please review manually.", role.Name, namespace, perms) - continue - } - // if namespace exists in current permissions else empty slice - existingPerms, okExisting := permissions[namespace] - if !okExisting { - existingPerms = []string{} + normalizedExpected := normalizePermissionMap(role.Permissions) + normalizedExisting := normalizePermissionMap(permissions) + + for namespace, existingPerms := range normalizedExisting { + if _, ok := normalizedExpected[namespace]; !ok { + log.Printf("- WARNING: role '%s' has unexpected namespace '%s' with permissions %v. Please review manually.", role.Name, namespace, existingPerms) } + } + for namespace, expectedPerms := range normalizedExpected { + existingPerms := normalizedExisting[namespace] extraPermissions, missingPermissions := differences(existingPerms, expectedPerms) for _, p := range extraPermissions { log.Printf("- WARNING: role '%s' has unexpected permission '%s' in namespace '%s'. Please review manually.", role.Name, p, namespace) @@ -133,27 +148,7 @@ func main() { log.Println(">> Consolidating groups...") } - managedGroups := []ManagedGroup{ - { - Name: "CloudEngineers", - Roles: config.CloudEngineerRoles, - Members: config.Cloudengineers, - }, - { - Name: "BatchCapabilityCreators", - Roles: []RoleBinding{ - {RoleName: "BatchCapabilityCreator", Scope: "Global"}, - }, - Members: config.BatchCapabilityCreators, - }, - { - Name: "ServiceCatalogueReaders", - Roles: []RoleBinding{ - {RoleName: "ServiceCatalogueReader", Scope: "Global"}, - }, - Members: config.ServiceCatalogueReaders, - }, - } + managedGroups := resolveManagedGroups(config) availableGroups, err := fetchGroups(config) if err != nil { @@ -174,8 +169,8 @@ func main() { if config.Debug { log.Println("Now Available groups:") - for name, id := range availableGroups { - log.Printf(" - %s (ID: %s)", name, id) + for name, group := range availableGroups { + log.Printf(" - %s (ID: %s)", name, group.ID) } } @@ -221,9 +216,16 @@ type ManagedGroup struct { Members []string } +type ManagedGroupConfig struct { + Name string `json:"name"` + Roles []RoleBinding `json:"roles"` + Members []string `json:"members"` +} + type Config struct { Debug bool `json:"debug"` ApiUrl string `json:"apiUrl"` + Groups []ManagedGroupConfig `json:"groups"` Cloudengineers []string `json:"cloudengineers"` BatchCapabilityCreators []string `json:"batchCapabilityCreators"` ServiceCatalogueReaders []string `json:"serviceCatalogueReaders"` @@ -488,7 +490,7 @@ func assignRole(config *Config, roleId, assignedEntityType, assignedEntityId, as } defer resp.Body.Close() - if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent { + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusNoContent { b, _ := ioutil.ReadAll(resp.Body) return fmt.Errorf("failed to assign role: %s, [error code %d]", string(b), resp.StatusCode) } @@ -536,7 +538,12 @@ func ensureGroupRoles(config *Config, groupSpec ManagedGroup, availableGroups ma existingRoleGrants := make(map[string]RoleAssignment) for _, assignment := range roleAssignments { - key := fmt.Sprintf("%s|%s|%s", assignment.RoleId, strings.ToLower(assignment.Type), assignment.Resource) + key := fmt.Sprintf( + "%s|%s|%s", + assignment.RoleId, + strings.ToLower(strings.TrimSpace(assignment.Type)), + normalizeGrantResource(assignment.Type, assignment.Resource), + ) existingRoleGrants[key] = assignment } @@ -552,11 +559,17 @@ func ensureGroupRoles(config *Config, groupSpec ManagedGroup, availableGroups ma assignmentType = "Global" } - key := fmt.Sprintf("%s|%s|*", roleID, strings.ToLower(assignmentType)) + key := fmt.Sprintf( + "%s|%s|%s", + roleID, + strings.ToLower(assignmentType), + normalizeGrantResource(assignmentType, "*"), + ) expectedRoleGrants[key] = binding if _, granted := existingRoleGrants[key]; !granted { - if err := assignRole(config, roleID, "Group", group.ID, assignmentType, "*"); err != nil { + resource := normalizeGrantResource(assignmentType, "*") + if err := assignRole(config, roleID, "Group", group.ID, assignmentType, resource); err != nil { log.Fatalf("failed to assign role '%s' to group '%s': %v", binding.RoleName, groupSpec.Name, err) } if config.Debug { @@ -572,6 +585,45 @@ func ensureGroupRoles(config *Config, groupSpec ManagedGroup, availableGroups ma } } +func resolveManagedGroups(config *Config) []ManagedGroup { + if len(config.Groups) > 0 { + groups := make([]ManagedGroup, 0, len(config.Groups)) + for _, g := range config.Groups { + groups = append(groups, ManagedGroup{Name: g.Name, Roles: g.Roles, Members: g.Members}) + } + return groups + } + + return []ManagedGroup{ + { + Name: "CloudEngineers", + Roles: config.CloudEngineerRoles, + Members: config.Cloudengineers, + }, + { + Name: "BatchCapabilityCreators", + Roles: []RoleBinding{ + {RoleName: "BatchCapabilityCreator", Scope: "Global"}, + }, + Members: config.BatchCapabilityCreators, + }, + { + Name: "ServiceCatalogueReaders", + Roles: []RoleBinding{ + {RoleName: "ServiceCatalogueReader", Scope: "Global"}, + }, + Members: config.ServiceCatalogueReaders, + }, + } +} + +func normalizeGrantResource(scope, resource string) string { + if strings.EqualFold(strings.TrimSpace(scope), "Global") { + return "" + } + return strings.TrimSpace(resource) +} + func ensureGroupMembers(config *Config, groupSpec ManagedGroup, availableGroups map[string]Group) { group, exists := availableGroups[groupSpec.Name] if !exists { @@ -639,12 +691,47 @@ func fetchPermissionsForRole(config *Config, roleId string) (map[string][]string permissionMap := make(map[string][]string) for _, p := range permissions { + // Baseline script manages global role permissions only. + if !strings.EqualFold(p.Type, "Global") { + continue + } permissionMap[p.Namespace] = append(permissionMap[p.Namespace], p.Permission) } return permissionMap, nil } +func shouldSkipRole(roleName string) bool { + return strings.EqualFold(strings.TrimSpace(roleName), "Guest") +} + +func normalizePermissionMap(input map[string][]string) map[string][]string { + output := make(map[string][]string, len(input)) + for namespace, permissions := range input { + ns := strings.TrimSpace(strings.ToLower(namespace)) + if ns == "" { + continue + } + + seen := map[string]struct{}{} + normalized := make([]string, 0, len(permissions)) + for _, permission := range permissions { + p := strings.TrimSpace(strings.ToLower(permission)) + if p == "" { + continue + } + if _, exists := seen[p]; exists { + continue + } + seen[p] = struct{}{} + normalized = append(normalized, p) + } + + output[ns] = normalized + } + return output +} + func grantPermission(config *Config, entityType, entityId, namespace, permission string) { url := fmt.Sprintf("%s/rbac/permission/grant", config.ApiUrl) From c19c3a5ae3a08d252159237b8460d851b3684838 Mon Sep 17 00:00:00 2001 From: Andreas Frisch Date: Thu, 23 Jul 2026 12:39:43 +0200 Subject: [PATCH 7/9] use correct name for RBAC guard --- .../Api/Catalog/CatalogController.cs | 7 ++-- tools/config.json | 2 +- tools/setup-baseline-permissions.go | 34 ++++++++++++++++--- 3 files changed, 34 insertions(+), 9 deletions(-) diff --git a/src/SelfService/Infrastructure/Api/Catalog/CatalogController.cs b/src/SelfService/Infrastructure/Api/Catalog/CatalogController.cs index d5524a65..4e1a56a4 100644 --- a/src/SelfService/Infrastructure/Api/Catalog/CatalogController.cs +++ b/src/SelfService/Infrastructure/Api/Catalog/CatalogController.cs @@ -4,6 +4,7 @@ namespace SelfService.Infrastructure.Api.Catalog; +// RbacConfig "id" is not used in this controller, but is required to satisfy the RbacConfig attribute. The controller is protected by RequiresPermission attributes on each action. [Route("catalog")] [RbacConfig(nameof(RbacObjectType.Global), "id")] [Produces("application/json")] @@ -23,7 +24,7 @@ CatalogApiResourceFactory apiResourceFactory } [HttpGet("applications")] - [RequiresPermission("service-catalogue", "service-catalogue-read")] + [RequiresPermission("service-catalogue", "read")] [ProducesResponseType(typeof(CatalogApplicationsApiResource), StatusCodes.Status200OK)] public async Task GetApplications( [FromQuery] string? capabilityId, @@ -47,7 +48,7 @@ CancellationToken cancellationToken } [HttpGet("namespaces")] - [RequiresPermission("service-catalogue", "service-catalogue-read")] + [RequiresPermission("service-catalogue", "read")] [ProducesResponseType(typeof(CatalogNamespacesApiResource), StatusCodes.Status200OK)] public async Task GetNamespaces(CancellationToken cancellationToken) { @@ -56,7 +57,7 @@ public async Task GetNamespaces(CancellationToken cancellationTok } [HttpGet("dependencies")] - [RequiresPermission("service-catalogue", "service-catalogue-read")] + [RequiresPermission("service-catalogue", "read")] [ProducesResponseType(typeof(CatalogDependenciesApiResource), StatusCodes.Status200OK)] public async Task GetDependencies( [FromQuery] string? @namespace, diff --git a/tools/config.json b/tools/config.json index 3cf4fd97..6a220149 100644 --- a/tools/config.json +++ b/tools/config.json @@ -1,6 +1,6 @@ { "debug": true, - "apiUrl": "http://localhost:8080", + "apiUrl": "https://ssu-preview.hellman.oxygen.dfds.cloud/api", "groups": [ { "name": "CloudEngineers", diff --git a/tools/setup-baseline-permissions.go b/tools/setup-baseline-permissions.go index 20ea8d53..fb12cbbc 100644 --- a/tools/setup-baseline-permissions.go +++ b/tools/setup-baseline-permissions.go @@ -135,7 +135,16 @@ func main() { log.Printf("- WARNING: role '%s' has unexpected permission '%s' in namespace '%s'. Please review manually.", role.Name, p, namespace) } for _, p := range missingPermissions { - grantPermission(config, "Role", roleId, namespace, p) + if err := grantPermission(config, "Role", roleId, namespace, p); err != nil { + log.Fatalf( + "failed to grant missing permission for role '%s' (roleId='%s', namespace='%s', permission='%s'): %v", + role.Name, + roleId, + namespace, + p, + err, + ) + } } } } @@ -176,7 +185,8 @@ func main() { for _, groupSpec := range managedGroups { ensureGroupRoles(config, groupSpec, availableGroups, availableRoles) - ensureGroupMembers(config, groupSpec, availableGroups) + //ensureGroupMembers(config, groupSpec, availableGroups) + log.Printf("Skipping member synchronization for group '%s' (members are managed manually).", groupSpec.Name) } if config.Debug { @@ -732,7 +742,7 @@ func normalizePermissionMap(input map[string][]string) map[string][]string { return output } -func grantPermission(config *Config, entityType, entityId, namespace, permission string) { +func grantPermission(config *Config, entityType, entityId, namespace, permission string) error { url := fmt.Sprintf("%s/rbac/permission/grant", config.ApiUrl) payload := PermissionGrantCreation{ @@ -744,6 +754,17 @@ func grantPermission(config *Config, entityType, entityId, namespace, permission Resource: "*", } body, _ := json.Marshal(payload) + if config.Debug { + log.Printf( + ">> Granting missing permission: entityType='%s', entityId='%s', namespace='%s', permission='%s', type='%s', resource='%s'", + entityType, + entityId, + namespace, + permission, + payload.Type, + payload.Resource, + ) + } req, _ := http.NewRequest("POST", url, bytes.NewBuffer(body)) req.Header.Set("Authorization", "Bearer "+config.AccessToken) @@ -752,17 +773,20 @@ func grantPermission(config *Config, entityType, entityId, namespace, permission client := &http.Client{} resp, err := client.Do(req) if err != nil { - log.Fatalf("failed to grant permission: %v", err) + return err } defer resp.Body.Close() + responseBody, _ := ioutil.ReadAll(resp.Body) if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusNoContent { - log.Fatalf("failed to grant permission, [error code %d]", resp.StatusCode) + return fmt.Errorf("status=%d response=%q", resp.StatusCode, string(responseBody)) } if config.Debug { log.Printf("- Granted missing permission '%s' in namespace '%s' to %s (%s).", permission, namespace, entityType, entityId) } + + return nil } func differences(a, b []string) (onlyInA, onlyInB []string) { From e54198bcecf01167328e04b04e1e65431c215abb Mon Sep 17 00:00:00 2001 From: Andreas Frisch Date: Thu, 23 Jul 2026 12:47:10 +0200 Subject: [PATCH 8/9] stubs --- src/SelfService.Tests/TestDoubles/StubAuthenticationService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SelfService.Tests/TestDoubles/StubAuthenticationService.cs b/src/SelfService.Tests/TestDoubles/StubAuthenticationService.cs index 31419ddf..e402d2e0 100644 --- a/src/SelfService.Tests/TestDoubles/StubAuthenticationService.cs +++ b/src/SelfService.Tests/TestDoubles/StubAuthenticationService.cs @@ -294,7 +294,7 @@ public bool CanUnsetCapabilityTags(PortalUser portalUser) return _authorized; } - public bool CanBatchCreateCapabilities(PortalUser portalUser, UserId userId) + public bool CanBatchCreateCapabilities(PortalUser portalUser) { return _authorized; } From a5affbd07032d1f4dab668062b71a0f1a97550fe Mon Sep 17 00:00:00 2001 From: Andreas Frisch Date: Thu, 23 Jul 2026 13:06:41 +0200 Subject: [PATCH 9/9] updated tests --- db/seed/RbacRoleGrants.csv | 3 +++ .../when_getting_membership_application_for_applicant.cs | 2 -- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/db/seed/RbacRoleGrants.csv b/db/seed/RbacRoleGrants.csv index 67282d0b..2cf67820 100644 --- a/db/seed/RbacRoleGrants.csv +++ b/db/seed/RbacRoleGrants.csv @@ -2,3 +2,6 @@ Id;RoleId;CreatedAt;AssignedEntityType;AssignedEntityId;Type;Resource E02D91E6-DE3C-4C55-BC0A-EDABD1492197;5E32EE6A-1A73-4ACF-9C61-90E4D0D59261;2026-07-23T09:15:35.803819;Group;BA0AA734-206D-454E-AC95-14855B6901E4;Global; 4CE54588-72CB-4B27-93D3-7F842B9C91AF;6A2EE52C-6A9B-4A2A-B9C8-5851DD2D9A6F;2026-07-23T09:15:35.803831;Group;899F8F9E-3F7E-4EF3-A2CD-2E1DDA78E40A;Global; 059F294D-4FD4-4836-9B11-9A7E8FA125C7;A983CF2E-772E-437D-B9D8-5DDF769339D3;2026-07-23T09:15:35.803837;Group;7D3D7498-9075-4D1B-B6C1-AF6E95788C3B;Global; +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 diff --git a/src/SelfService.Tests/Infrastructure/Api/MembershipApplicationRoutes/when_getting_membership_application_for_applicant.cs b/src/SelfService.Tests/Infrastructure/Api/MembershipApplicationRoutes/when_getting_membership_application_for_applicant.cs index 064d296b..6c1e1070 100644 --- a/src/SelfService.Tests/Infrastructure/Api/MembershipApplicationRoutes/when_getting_membership_application_for_applicant.cs +++ b/src/SelfService.Tests/Infrastructure/Api/MembershipApplicationRoutes/when_getting_membership_application_for_applicant.cs @@ -24,7 +24,6 @@ public async Task InitializeAsync() application.ReplaceService( new StubMembershipApplicationQuery(_aMembershipApplication) ); - /* application.ReplaceService( new StubRbacPermissionGrantRepository( permissions: new[] @@ -40,7 +39,6 @@ public async Task InitializeAsync() } ) ); - */ application.ReplaceService(new StubRbacRoleGrantRepository()); application.ReplaceService(new StubPermissionQuery());