Skip to content

Commit

Permalink
Deployer: Check for role definition before adding it (#805)
Browse files Browse the repository at this point in the history
  • Loading branch information
BMurri authored Aug 20, 2024
1 parent 5b0e566 commit ac770fa
Show file tree
Hide file tree
Showing 2 changed files with 145 additions and 78 deletions.
149 changes: 71 additions & 78 deletions src/deploy-cromwell-on-azure/Deployer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,6 @@ public class Deployer(Configuration configuration)
.Handle<Exception>()
.WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(1));

/// <summary>
/// Grants full access to manage all resources, but does not allow you to assign roles in Azure RBAC, manage assignments in Azure Blueprints, or share image galleries.
/// </summary>
private static readonly ResourceIdentifier All_Role_Contributor = AuthorizationRoleDefinitionResource.CreateResourceIdentifier(string.Empty, new("b24988ac-6180-42a0-ab88-20f7382dd24c"));

public const string WorkflowsContainerName = "workflows";
public const string ConfigurationContainerName = "configuration";
public const string TesInternalContainerName = "tes-internal";
Expand Down Expand Up @@ -546,14 +541,14 @@ await Task.WhenAll(
await WritePersonalizedFilesToStorageAccountAsync(storageAccountData);
await AssignVmAsContributorToStorageAccountAsync(managedIdentity, storageAccount);
await AssignMIAsDataOwnerToStorageAccountAsync(managedIdentity, storageAccount, true);
await AssignMIAsDataOwnerToStorageAccountAsync(managedIdentity, storageAccount);
await AssignManagedIdOperatorToResourceAsync(managedIdentity, resourceGroup);
await AssignMIAsNetworkContributorToResourceAsync(managedIdentity, resourceGroup, true);
await AssignMIAsNetworkContributorToResourceAsync(managedIdentity, resourceGroup);
if (aksNodepoolIdentity is not null)
{
await AssignVmAsContributorToStorageAccountAsync(aksNodepoolIdentity, storageAccount);
await AssignMIAsDataOwnerToStorageAccountAsync(aksNodepoolIdentity, storageAccount, true);
await AssignMIAsDataOwnerToStorageAccountAsync(aksNodepoolIdentity, storageAccount);
await AssignManagedIdOperatorToResourceAsync(aksNodepoolIdentity, resourceGroup);
}
}),
Expand Down Expand Up @@ -1282,62 +1277,20 @@ await Execute(
}

private Task AssignManagedIdOperatorToResourceAsync(UserAssignedIdentityResource managedIdentity, ArmResource resource)
{
// https://docs.microsoft.com/en-us/azure/role-based-access-control/built-in-roles#managed-identity-operator
=> AssignRoleToResourceAsync(managedIdentity, resource, GetSubscriptionRoleDefinition(RoleDefinitions.Identity.ManagedIdentityOperator),
$"Assigning '{RoleDefinitions.GetDisplayName(RoleDefinitions.Identity.ManagedIdentityOperator)}' role for the managed id to resource group scope...");

var roleDefinitionId = AuthorizationRoleDefinitionResource.CreateResourceIdentifier(SubscriptionResource.CreateResourceIdentifier(configuration.SubscriptionId), new("f1a07417-d97a-45cb-824c-7a7467783830"));
return Execute(
$"Assigning 'Managed ID Operator' role for the managed id to resource group scope...",
() => roleAssignmentHashConflictRetryPolicy.ExecuteAsync(
ct => (Task)resource.GetRoleAssignments().CreateOrUpdateAsync(WaitUntil.Completed, Guid.NewGuid().ToString(),
new(roleDefinitionId, managedIdentity.Data.PrincipalId.Value)
{
PrincipalType = Azure.ResourceManager.Authorization.Models.RoleManagementPrincipalType.ServicePrincipal
}, ct), cts.Token));
}
private Task AssignMIAsNetworkContributorToResourceAsync(UserAssignedIdentityResource managedIdentity, ArmResource resource)
=> AssignRoleToResourceAsync(managedIdentity, resource, GetSubscriptionRoleDefinition(RoleDefinitions.Networking.NetworkContributor),
$"Assigning '{RoleDefinitions.GetDisplayName(RoleDefinitions.Networking.NetworkContributor)}' role for the managed id to resource group scope...");

private Task AssignMIAsNetworkContributorToResourceAsync(UserAssignedIdentityResource managedIdentity, ArmResource resource, bool cancelOnException = true)
{
// https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles#network-contributor
var roleDefinitionId = AuthorizationRoleDefinitionResource.CreateResourceIdentifier(SubscriptionResource.CreateResourceIdentifier(configuration.SubscriptionId), new("4d97b98b-1d4f-4787-a291-c67834d212e7"));
return Execute(
$"Assigning 'Network Contributor' role for the managed id to resource group scope...",
() => roleAssignmentHashConflictRetryPolicy.ExecuteAsync(
ct => (Task)resource.GetRoleAssignments().CreateOrUpdateAsync(WaitUntil.Completed, Guid.NewGuid().ToString(),
new(roleDefinitionId, managedIdentity.Data.PrincipalId.Value)
{
PrincipalType = Azure.ResourceManager.Authorization.Models.RoleManagementPrincipalType.ServicePrincipal
}, ct),
cts.Token),
cancelOnException: cancelOnException);
}

private Task AssignMIAsDataOwnerToStorageAccountAsync(UserAssignedIdentityResource managedIdentity, StorageAccountResource storageAccount, bool cancelOnException = true)
{
//https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles#storage-blob-data-owner
var roleDefinitionId = AuthorizationRoleDefinitionResource.CreateResourceIdentifier(SubscriptionResource.CreateResourceIdentifier(configuration.SubscriptionId), new("b7e6dc6d-f1e8-4753-8033-0f276bb0955b"));

return Execute(
$"Assigning 'Storage Blob Data Owner' role for user-managed identity to Storage Account resource scope...",
() => roleAssignmentHashConflictRetryPolicy.ExecuteAsync(
ct => (Task)storageAccount.GetRoleAssignments().CreateOrUpdateAsync(WaitUntil.Completed, Guid.NewGuid().ToString(),
new(roleDefinitionId, managedIdentity.Data.PrincipalId.Value)
{
PrincipalType = Azure.ResourceManager.Authorization.Models.RoleManagementPrincipalType.ServicePrincipal
}, ct),
cts.Token),
cancelOnException: cancelOnException);
}
private Task AssignMIAsDataOwnerToStorageAccountAsync(UserAssignedIdentityResource managedIdentity, StorageAccountResource storageAccount)
=> AssignRoleToResourceAsync(managedIdentity, storageAccount, GetSubscriptionRoleDefinition(RoleDefinitions.Storage.StorageBlobDataOwner),
$"Assigning '{RoleDefinitions.GetDisplayName(RoleDefinitions.Storage.StorageBlobDataOwner)}' role for user-managed identity to Storage Account resource scope...");

private Task AssignVmAsContributorToStorageAccountAsync(UserAssignedIdentityResource managedIdentity, StorageAccountResource storageAccount)
=> Execute(
$"Assigning 'Contributor' role for user-managed identity to Storage Account resource scope...",
() => roleAssignmentHashConflictRetryPolicy.ExecuteAsync(
ct => (Task)storageAccount.GetRoleAssignments().CreateOrUpdateAsync(WaitUntil.Completed, Guid.NewGuid().ToString(),
new(All_Role_Contributor, managedIdentity.Data.PrincipalId.Value)
{
PrincipalType = Azure.ResourceManager.Authorization.Models.RoleManagementPrincipalType.ServicePrincipal
}, ct), cts.Token));
=> AssignRoleToResourceAsync(managedIdentity, storageAccount, GetSubscriptionRoleDefinition(RoleDefinitions.General.Contributor),
$"Assigning '{RoleDefinitions.GetDisplayName(RoleDefinitions.General.Contributor)}' role for user-managed identity to Storage Account resource scope...");

private Task<StorageAccountResource> CreateStorageAccountAsync()
=> Execute(
Expand Down Expand Up @@ -1431,14 +1384,8 @@ await UploadTextToStorageAccountAsync(GetBlobClient(storageAccount, Configuratio
});

private Task AssignVmAsContributorToBatchAccountAsync(UserAssignedIdentityResource managedIdentity, BatchAccountResource batchAccount)
=> Execute(
$"Assigning 'Contributor' role for user-managed identity to Batch Account resource scope...",
() => roleAssignmentHashConflictRetryPolicy.ExecuteAsync(
ct => (Task)batchAccount.GetRoleAssignments().CreateOrUpdateAsync(WaitUntil.Completed, Guid.NewGuid().ToString(),
new(All_Role_Contributor, managedIdentity.Data.PrincipalId.Value)
{
PrincipalType = Azure.ResourceManager.Authorization.Models.RoleManagementPrincipalType.ServicePrincipal
}, ct), cts.Token));
=> AssignRoleToResourceAsync(managedIdentity, batchAccount, GetSubscriptionRoleDefinition(RoleDefinitions.General.Contributor),
$"Assigning '{RoleDefinitions.GetDisplayName(RoleDefinitions.General.Contributor)}' role for user-managed identity to Batch Account resource scope...");

private async Task<PostgreSqlFlexibleServerResource> CreatePostgreSqlServerAndDatabaseAsync(SubnetResource subnet, PrivateDnsZoneResource postgreSqlDnsZone)
{
Expand Down Expand Up @@ -1482,14 +1429,60 @@ await Execute(
}

private Task AssignVmAsContributorToAppInsightsAsync(UserAssignedIdentityResource managedIdentity, ArmResource appInsights)
=> Execute(
$"Assigning 'Contributor' role for user-managed identity to App Insights resource scope...",
() => roleAssignmentHashConflictRetryPolicy.ExecuteAsync(
ct => (Task)appInsights.GetRoleAssignments().CreateOrUpdateAsync(WaitUntil.Completed, Guid.NewGuid().ToString(),
new(All_Role_Contributor, managedIdentity.Data.PrincipalId.Value)
{
PrincipalType = Azure.ResourceManager.Authorization.Models.RoleManagementPrincipalType.ServicePrincipal
}, ct), cts.Token));
=> AssignRoleToResourceAsync(managedIdentity, appInsights, GetSubscriptionRoleDefinition(RoleDefinitions.General.Contributor),
$"Assigning '{RoleDefinitions.GetDisplayName(RoleDefinitions.General.Contributor)}' role for user-managed identity to App Insights resource scope...");

private ResourceIdentifier GetSubscriptionRoleDefinition(Guid roleDefinition)
=> AuthorizationRoleDefinitionResource.CreateResourceIdentifier(SubscriptionResource.CreateResourceIdentifier(configuration.SubscriptionId), new(roleDefinition.ToString("D")));

private async Task AssignRoleToResourceAsync(UserAssignedIdentityResource managedIdentity, 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 => managedIdentity.Data.PrincipalId.Value.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, managedIdentity.Data.PrincipalId.Value)
{
PrincipalType = Azure.ResourceManager.Authorization.Models.RoleManagementPrincipalType.ServicePrincipal
},
token),
cts.Token));

static Func<CancellationToken, Task<Response<RoleAssignmentResource>>> CallGetAsync(RoleAssignmentResource resource)
{
return new Func<CancellationToken, Task<Response<RoleAssignmentResource>>>(async cancellationToken =>
{
try
{
return await resource.GetAsync(cancellationToken: cancellationToken);
}
catch (RequestFailedException ex) when ("AuthorizationFailed".Equals(ex.ErrorCode))
{
return new NullResponse<RoleAssignmentResource>();
}
});
}
}

private class NullResponse<T> : Response<T>
{
public override bool HasValue => false;

public override T Value => default;

public override Response GetRawResponse()
{
throw new NotImplementedException();
}
}

private Task<(VirtualNetworkResource virtualNetwork, SubnetResource vmSubnet, SubnetResource postgreSqlSubnet, SubnetResource batchSubnet)> CreateVnetAndSubnetsAsync()
=> Execute(
Expand Down Expand Up @@ -1876,8 +1869,8 @@ private async Task ValidateRegionNameAsync(string regionName)

private async Task ValidateSubscriptionAndResourceGroupAsync(Configuration configuration)
{
const string ownerRoleId = "8e3af657-a8ff-443c-a75c-2fe8c4bcb635";
const string contributorRoleId = "b24988ac-6180-42a0-ab88-20f7382dd24c";
var ownerRoleId = RoleDefinitions.General.Owner.ToString("D");
var contributorRoleId = RoleDefinitions.General.Contributor.ToString("D");

bool rgExists;

Expand Down
74 changes: 74 additions & 0 deletions src/deploy-cromwell-on-azure/RoleDefinitions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Collections.Immutable;

namespace CromwellOnAzureDeployer
{
internal static class RoleDefinitions
{
internal static string GetDisplayName(Guid name) => _displayNames[name];

private record struct GuidAndDisplayName(Guid Name, string DisplayName);
private static readonly ImmutableDictionary<string, GuidAndDisplayName> _roleDefinitions
= ImmutableDictionary<string, GuidAndDisplayName>.Empty.AddRange(
// Add in order of https://learn.microsoft.com/azure/role-based-access-control/built-in-roles, followed by any custom definitions (in a separate subclass).
[
// https://learn.microsoft.com/azure/role-based-access-control/built-in-roles/general#contributor
new($"{nameof(General)}.{nameof(General.Contributor)}", new(new("b24988ac-6180-42a0-ab88-20f7382dd24c"), "Contributor")),

// https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles/general#owner
new($"{nameof(General)}.{nameof(General.Owner)}", new(new("8e3af657-a8ff-443c-a75c-2fe8c4bcb635"), "Grants full access to manage all resources, including the ability to assign roles in Azure RBAC.")),

// https://learn.microsoft.com/azure/role-based-access-control/built-in-roles/networking#network-contributor
new($"{nameof(Networking)}.{nameof(Networking.NetworkContributor)}", new(new("4d97b98b-1d4f-4787-a291-c67834d212e7"), "Network Contributor")),

// https://learn.microsoft.com/azure/role-based-access-control/built-in-roles/storage#storage-blob-data-owner
new($"{nameof(Storage)}.{nameof(Storage.StorageBlobDataOwner)}", new(new("b7e6dc6d-f1e8-4753-8033-0f276bb0955b"), "Storage Blob Data Owner")),

// https://learn.microsoft.com/azure/role-based-access-control/built-in-roles/identity#managed-identity-operator
new($"{nameof(Identity)}.{nameof(Identity.ManagedIdentityOperator)}", new(new("f1a07417-d97a-45cb-824c-7a7467783830"), "Managed Identity Operator")),
]);

private static readonly ImmutableDictionary<Guid, string> _displayNames
= _roleDefinitions.Values.ToImmutableDictionary(t => t.Name, t => t.DisplayName);

internal static class General
{
/// <summary>
/// Grants full access to manage all resources, but does not allow you to assign roles in Azure RBAC, manage assignments in Azure Blueprints, or share image galleries.
/// </summary>
internal static Guid Contributor { get; } = _roleDefinitions[$"{nameof(General)}.{nameof(Contributor)}"].Name;

/// <summary>
/// Grants full access to manage all resources, including the ability to assign roles in Azure RBAC.
/// </summary>
internal static Guid Owner { get; } = _roleDefinitions[$"{nameof(General)}.{nameof(Owner)}"].Name;
}

internal static class Networking
{
/// <summary>
/// Lets you manage networks, but not access to them.
/// </summary>
internal static Guid NetworkContributor { get; } = _roleDefinitions[$"{nameof(Networking)}.{nameof(NetworkContributor)}"].Name;
}

internal static class Storage
{
/// <summary>
/// Provides full access to Azure Storage blob containers and data, including assigning POSIX access control. To learn which actions are required for a given data operation, see [Permissions for calling data operations](https://learn.microsoft.com/rest/api/storageservices/authorize-with-azure-active-directory#permissions-for-calling-data-operations)/>.
/// </summary>
internal static Guid StorageBlobDataOwner { get; } = _roleDefinitions[$"{nameof(Storage)}.{nameof(StorageBlobDataOwner)}"].Name;
}

internal static class Identity
{
/// <summary>
/// Read and Assign User Assigned Identity.
/// </summary>
internal static Guid ManagedIdentityOperator { get; } = _roleDefinitions[$"{nameof(Identity)}.{nameof(ManagedIdentityOperator)}"].Name;
}
}
}

0 comments on commit ac770fa

Please sign in to comment.