Skip to content
This repository was archived by the owner on Jul 28, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 44 additions & 13 deletions src/deploy-cromwell-on-azure/Deployer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,14 @@ private static async Task<T> FetchResourceDataAsync<T>(Func<CancellationToken, T
return result;
}

private async ValueTask EnsureResourceGroup()
{
if (resourceGroup is null && !string.IsNullOrWhiteSpace(configuration.ResourceGroupName))
{
resourceGroup = (await armSubscription.GetResourceGroupAsync(configuration.ResourceGroupName, cts.Token)).Value;
}
}

private BlobClient GetBlobClient(StorageAccountData storageAccount, string containerName, string blobName)
{
return new(new BlobUriBuilder(storageAccount.PrimaryEndpoints.BlobUri) { BlobContainerName = containerName, BlobName = blobName }.ToUri(),
Expand Down Expand Up @@ -256,7 +264,7 @@ await Execute("Connecting to Azure Services...", async () =>

if (configuration.Update)
{
resourceGroup = (await armSubscription.GetResourceGroupAsync(configuration.ResourceGroupName, cts.Token)).Value;
await EnsureResourceGroup();
configuration.RegionName = resourceGroup.Id.Location ?? (resourceGroup.HasData
? resourceGroup.Data.Location.Name
: (await FetchResourceDataAsync(resourceGroup.GetAsync, cts.Token, resource => resourceGroup = resource)).Data.Location.Name);
Expand Down Expand Up @@ -628,15 +636,15 @@ await Execute("Validating existing Azure resources...", async () =>

var vnetAndSubnet = await ValidateAndGetExistingVirtualNetworkAsync();

if (string.IsNullOrWhiteSpace(configuration.ResourceGroupName))
if (resourceGroup is null)
{
configuration.ResourceGroupName = Utility.RandomResourceName($"{configuration.MainIdentifierPrefix}-", 15);
resourceGroup = await CreateResourceGroupAsync();
isResourceGroupCreated = true;
}
else
{
resourceGroup = (await armSubscription.GetResourceGroupAsync(configuration.ResourceGroupName, cts.Token)).Value;
await EnsureResourceGroup();
}

if (!string.IsNullOrWhiteSpace(configuration.IdentityResourceId))
Expand Down Expand Up @@ -1718,8 +1726,8 @@ private Task AssignMIAsDataOwnerToStorageAccountAsync(UserAssignedIdentityResour
$"Assigning '{RoleDefinitions.GetDisplayName(RoleDefinitions.Storage.StorageBlobDataOwner)}' role for user-managed identity to Storage Account resource scope...");

private Task AssignVmAsContributorToStorageAccountAsync(UserAssignedIdentityResource managedIdentity, StorageAccountResource storageAccount)
=> AssignRoleToResourceAsync(managedIdentity, storageAccount, GetSubscriptionRoleDefinition(RoleDefinitions.General.Contributor),
$"Assigning '{RoleDefinitions.GetDisplayName(RoleDefinitions.General.Contributor)}' role for user-managed identity to Storage Account resource scope...");
=> AssignRoleToResourceAsync(managedIdentity, storageAccount, GetSubscriptionRoleDefinition(RoleDefinitions.Privileged.Contributor),
$"Assigning '{RoleDefinitions.GetDisplayName(RoleDefinitions.Privileged.Contributor)}' role for user-managed identity to Storage Account resource scope...");

private async Task AssignMeAsRbacClusterAdminToManagedClusterAsync(ContainerServiceManagedClusterResource managedCluster)
{
Expand Down Expand Up @@ -1873,8 +1881,8 @@ await UploadTextToStorageAccountAsync(GetBlobClient(storageAccount, Configuratio
});

private Task AssignVmAsContributorToBatchAccountAsync(UserAssignedIdentityResource managedIdentity, BatchAccountResource batchAccount)
=> AssignRoleToResourceAsync(managedIdentity, batchAccount, GetSubscriptionRoleDefinition(RoleDefinitions.General.Contributor),
$"Assigning '{RoleDefinitions.GetDisplayName(RoleDefinitions.General.Contributor)}' role for user-managed identity to Batch Account resource scope...");
=> AssignRoleToResourceAsync(managedIdentity, batchAccount, GetSubscriptionRoleDefinition(RoleDefinitions.Privileged.Contributor),
$"Assigning '{RoleDefinitions.GetDisplayName(RoleDefinitions.Privileged.Contributor)}' role for user-managed identity to Batch Account resource scope...");

private async Task<PostgreSqlFlexibleServerResource> CreatePostgreSqlServerAndDatabaseAsync(SubnetResource subnet, PrivateDnsZoneResource postgreSqlDnsZone)
{
Expand Down Expand Up @@ -1918,8 +1926,8 @@ await Execute(
}

private Task AssignVmAsContributorToAppInsightsAsync(UserAssignedIdentityResource managedIdentity, ArmResource appInsights)
=> AssignRoleToResourceAsync(managedIdentity, appInsights, GetSubscriptionRoleDefinition(RoleDefinitions.General.Contributor),
$"Assigning '{RoleDefinitions.GetDisplayName(RoleDefinitions.General.Contributor)}' role for user-managed identity to App Insights resource scope...");
=> AssignRoleToResourceAsync(managedIdentity, appInsights, GetSubscriptionRoleDefinition(RoleDefinitions.Privileged.Contributor),
$"Assigning '{RoleDefinitions.GetDisplayName(RoleDefinitions.Privileged.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")));
Expand Down Expand Up @@ -2405,8 +2413,9 @@ private async Task ValidateRegionNameAsync(string regionName)

private async Task ValidateSubscriptionAndResourceGroupAsync(Configuration configuration)
{
var ownerRoleId = RoleDefinitions.General.Owner.ToString("D");
var contributorRoleId = RoleDefinitions.General.Contributor.ToString("D");
var ownerRoleId = RoleDefinitions.Privileged.Owner.ToString("D");
var contributorRoleId = RoleDefinitions.Privileged.Contributor.ToString("D");
var userAccessAdministrator = RoleDefinitions.Privileged.UserAccessAdministrator.ToString("D");

bool rgExists;

Expand All @@ -2428,15 +2437,37 @@ private async Task ValidateSubscriptionAndResourceGroupAsync(Configuration confi
var currentPrincipalObjectId = new JwtSecurityTokenHandler().ReadJwtToken(token.Token).Claims.FirstOrDefault(c => c.Type == "oid").Value;

var currentPrincipalSubscriptionRoleIds = armSubscription.GetRoleAssignments().GetAllAsync($"atScope() and assignedTo('{currentPrincipalObjectId}')", cancellationToken: cts.Token)
.SelectAwaitWithCancellation(async (b, c) => await FetchResourceDataAsync(t => b.GetAsync(cancellationToken: t), c)).Select(b => b.Data.RoleDefinitionId.Name);
.SelectAwaitWithCancellation(async (b, c) => b.HasData ? b : await FetchResourceDataAsync(t => b.GetAsync(cancellationToken: t), c)).Select(b => b.Data.RoleDefinitionId.Name);

var isSubOwner = false;
var isSubContributor = false;
var isSubUAA = false;
bool HasSubscriptionAccess(string role)
{
if (ownerRoleId.Equals(role, StringComparison.OrdinalIgnoreCase))
{
isSubOwner = true;
}
else if (contributorRoleId.Equals(role, StringComparison.OrdinalIgnoreCase))
{
isSubContributor = true;
}
else if (userAccessAdministrator.Equals(role, StringComparison.OrdinalIgnoreCase))
{
isSubUAA = true;
}

return isSubOwner || (isSubContributor && isSubUAA);
}

if (!await currentPrincipalSubscriptionRoleIds.AnyAsync(role => ownerRoleId.Equals(role, StringComparison.OrdinalIgnoreCase) || contributorRoleId.Equals(role, StringComparison.OrdinalIgnoreCase), cts.Token))
if (!await currentPrincipalSubscriptionRoleIds.AnyAsync(HasSubscriptionAccess, cts.Token))
{
if (!rgExists)
{
throw new ValidationException($"Insufficient access to deploy. You must be: 1) Owner of the subscription, or 2) Contributor and User Access Administrator of the subscription, or 3) Owner of the resource group", displayExample: false);
}

await EnsureResourceGroup();
var currentPrincipalRgRoleIds = resourceGroup.GetRoleAssignments().GetAllAsync($"atScope() and assignedTo('{currentPrincipalObjectId}')", cancellationToken: cts.Token)
.SelectAwaitWithCancellation(async (b, c) => await FetchResourceDataAsync(t => b.GetAsync(cancellationToken: t), c)).Select(b => b.Data.RoleDefinitionId.Name);

Expand Down
22 changes: 15 additions & 7 deletions src/deploy-cromwell-on-azure/RoleDefinitions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,14 @@ private static readonly ImmutableDictionary<string, GuidAndDisplayName> _roleDef
= 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/azure/role-based-access-control/built-in-roles/privileged#contributor
new($"{nameof(Privileged)}.{nameof(Privileged.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/en-us/azure/role-based-access-control/built-in-roles/privileged#owner
new($"{nameof(Privileged)}.{nameof(Privileged.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/en-us/azure/role-based-access-control/built-in-roles/privileged#user-access-administrator
new($"{nameof(Privileged)}.{nameof(Privileged.UserAccessAdministrator)}", new(new("18d7d88d-d35e-4fb5-a5c3-7773c20a72d9"), "Lets you manage user access to Azure resources.")),

// 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")),
Expand All @@ -43,17 +46,22 @@ private static readonly ImmutableDictionary<string, GuidAndDisplayName> _roleDef
private static readonly ImmutableDictionary<Guid, string> _displayNames
= _roleDefinitions.Values.ToImmutableDictionary(t => t.Name, t => t.DisplayName);

internal static class General
internal static class Privileged
{
/// <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;
internal static Guid Contributor { get; } = _roleDefinitions[$"{nameof(Privileged)}.{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 Guid Owner { get; } = _roleDefinitions[$"{nameof(Privileged)}.{nameof(Owner)}"].Name;

/// <summary>
/// Lets you manage user access to Azure resources.
/// </summary>
internal static Guid UserAccessAdministrator { get; } = _roleDefinitions[$"{nameof(Privileged)}.{nameof(UserAccessAdministrator)}"].Name;
}

internal static class Networking
Expand Down