diff --git a/src/deploy-cromwell-on-azure/Configuration.cs b/src/deploy-cromwell-on-azure/Configuration.cs index 3615f7d8..609293a3 100644 --- a/src/deploy-cromwell-on-azure/Configuration.cs +++ b/src/deploy-cromwell-on-azure/Configuration.cs @@ -90,7 +90,7 @@ public abstract class UserAccessibleConfiguration public string PostgreSqlServerSslMode { get; set; } = "VerifyFull"; public string KeyVaultName { get; set; } public string ContainersToMountPath { get; set; } - public string AadGroupIds { get; set; } // TODO: remove me + public string AadGroupIds { get; set; } public string DeploymentOrganizationName { get; set; } public string DeploymentOrganizationUrl { get; set; } public string DeploymentContactUri { get; set; } diff --git a/src/deploy-cromwell-on-azure/Deployer.cs b/src/deploy-cromwell-on-azure/Deployer.cs index 36dae31a..250b87ee 100644 --- a/src/deploy-cromwell-on-azure/Deployer.cs +++ b/src/deploy-cromwell-on-azure/Deployer.cs @@ -1362,7 +1362,7 @@ private Task AssignVmAsContributorToStorageAccountAsync(UserAssignedIdentityReso private async Task AssignMeAsRbacClusterAdminToManagedClusterAsync(ContainerServiceManagedClusterResource managedCluster) => await AssignRoleToResourceAsync(await GetUserObjectAsync(), managedCluster, GetSubscriptionRoleDefinition(RoleDefinitions.Containers.RbacClusterAdmin), - $"Assigning '{RoleDefinitions.GetDisplayName(RoleDefinitions.Containers.RbacClusterAdmin)}' role for the deployer to AKS cluster resource scope..."); + $"Assigning '{RoleDefinitions.GetDisplayName(RoleDefinitions.Containers.RbacClusterAdmin)}' role for {{Admins}} to AKS cluster resource scope..."); private Task CreateStorageAccountAsync() => Execute( @@ -1508,31 +1508,36 @@ private ResourceIdentifier GetSubscriptionRoleDefinition(Guid roleDefinition) => AuthorizationRoleDefinitionResource.CreateResourceIdentifier(SubscriptionResource.CreateResourceIdentifier(configuration.SubscriptionId), new(roleDefinition.ToString("D"))); private Task AssignRoleToResourceAsync(UserAssignedIdentityResource managedIdentity, ArmResource resource, ResourceIdentifier roleDefinitionId, string message) - => AssignRoleToResourceAsync(managedIdentity.Data.PrincipalId.Value, Azure.ResourceManager.Authorization.Models.RoleManagementPrincipalType.ServicePrincipal, resource, roleDefinitionId, message); + => AssignRoleToResourceAsync([managedIdentity.Data.PrincipalId.Value], Azure.ResourceManager.Authorization.Models.RoleManagementPrincipalType.ServicePrincipal, resource, roleDefinitionId, message); private Task AssignRoleToResourceAsync(Microsoft.Graph.Models.User user, ArmResource resource, ResourceIdentifier roleDefinitionId, string message) - => user is null ? Task.CompletedTask : AssignRoleToResourceAsync(new Guid(user.Id), Azure.ResourceManager.Authorization.Models.RoleManagementPrincipalType.User, resource, roleDefinitionId, message); + => string.IsNullOrWhiteSpace(configuration.AadGroupIds) + ? user is null ? Task.CompletedTask : AssignRoleToResourceAsync([new Guid(user.Id)], Azure.ResourceManager.Authorization.Models.RoleManagementPrincipalType.User, resource, roleDefinitionId, message.Replace(@"{Admins}", "deployer user")) + : AssignRoleToResourceAsync(configuration.AadGroupIds.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).Select(Guid.Parse), Azure.ResourceManager.Authorization.Models.RoleManagementPrincipalType.User, resource, roleDefinitionId, message.Replace(@"{Admins}", "designated group")); - private async Task AssignRoleToResourceAsync(Guid principalId, Azure.ResourceManager.Authorization.Models.RoleManagementPrincipalType principalType, ArmResource resource, ResourceIdentifier roleDefinitionId, string message) + private async Task AssignRoleToResourceAsync(IEnumerable principalIds, Azure.ResourceManager.Authorization.Models.RoleManagementPrincipalType principalType, ArmResource resource, ResourceIdentifier roleDefinitionId, string message) { - if (await resource.GetRoleAssignments().GetAllAsync(filter: "atScope()", cancellationToken: cts.Token) - .SelectAwaitWithCancellation(async (a, ct) => await EnsureResourceDataAsync(a, r => r.HasData, CallGetAsync, ct)) - .Where(a => a?.HasData ?? false) - .Where(a => principalId.Equals(a.Data.PrincipalId.Value)) - .Where(a => roleDefinitionId.Equals(a.Data.RoleDefinitionId)) - .AnyAsync(cts.Token)) + foreach (var principalId in principalIds) { - return; - } + if (await resource.GetRoleAssignments().GetAllAsync(filter: "atScope()", cancellationToken: cts.Token) + .SelectAwaitWithCancellation(async (a, ct) => await EnsureResourceDataAsync(a, r => r.HasData, CallGetAsync, ct)) + .Where(a => a?.HasData ?? false) + .Where(a => principalId.Equals(a.Data.PrincipalId.Value)) + .Where(a => roleDefinitionId.Equals(a.Data.RoleDefinitionId)) + .AnyAsync(cts.Token)) + { + return; + } - await Execute(message, () => roleAssignmentHashConflictRetryPolicy.ExecuteAsync(token => - (Task)resource.GetRoleAssignments().CreateOrUpdateAsync(WaitUntil.Completed, Guid.NewGuid().ToString(), - new(roleDefinitionId, principalId) - { - PrincipalType = principalType - }, - token), - cts.Token)); + await Execute(message, () => roleAssignmentHashConflictRetryPolicy.ExecuteAsync(token => + (Task)resource.GetRoleAssignments().CreateOrUpdateAsync(WaitUntil.Completed, Guid.NewGuid().ToString(), + new(roleDefinitionId, principalId) + { + PrincipalType = principalType + }, + token), + cts.Token)); + } static Func>> CallGetAsync(RoleAssignmentResource resource) { @@ -2318,6 +2323,7 @@ void ValidateHelmInstall(string helmPath, string featureName) ThrowIfProvidedForUpdate(configuration.VnetResourceGroupName, nameof(configuration.VnetResourceGroupName)); ThrowIfProvidedForUpdate(configuration.SubnetName, nameof(configuration.SubnetName)); ThrowIfProvidedForUpdate(configuration.Tags, nameof(configuration.Tags)); + ThrowIfProvidedForUpdate(configuration.AadGroupIds, nameof(configuration.AadGroupIds)); ThrowIfTagsFormatIsUnacceptable(configuration.Tags, nameof(configuration.Tags)); if (!configuration.ManualHelmDeployment) @@ -2343,6 +2349,21 @@ void ValidateHelmInstall(string helmPath, string featureName) { throw new ValidationException("Invalid configuration options DeploymentOrganizationName and DeploymentOrganizationUrl must both be provided together."); } + + if (!string.IsNullOrWhiteSpace(configuration.AadGroupIds)) + { + try + { + if (!configuration.AadGroupIds.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).Select(Guid.Parse).Any()) + { + throw new FormatException(); + } + } + catch (FormatException) + { + throw new ValidationException("Invalid configuration option AadGroupIds is not formatted correctly."); + } + } } private static void DisplayValidationExceptionAndExit(ValidationException validationException)