Skip to content

Commit

Permalink
Role Assignment Helpers
Browse files Browse the repository at this point in the history
  • Loading branch information
rudiv committed Apr 16, 2024
1 parent f52d9d0 commit c8a06bf
Show file tree
Hide file tree
Showing 4 changed files with 224 additions and 23 deletions.
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,18 @@ var cosmos = builder.AddAzureCosmosDbNoSqlAccount("cosmos", acc =>
ac.Resource.WithDevelopmentDefaults();
ac.WithDevelopmentGlobalAccess(); // Adds your local principal to have access to everything in the account
}
acc.AddDatabase("db", db => { }).AddContainer("cn", cn =>
var db = acc.AddDatabase("db");
var cn = db.AddContainer("cn", cn =>
{
cn.PartitionKey = new CosmosDbSqlContainerPartitionKey("/id");
});

if (builder.ExecutionContext.IsPublishMode) {
// Add a Managed Identity to the Cosmos DB
ac.AddRoleAssignment(acc.Resource, id, CosmosDbSqlBuiltInRoles.Contributor);
//ac.AddRoleAssignment(db.Resource, id, CosmosDbSqlBuiltInRoles.Contributor); // Or the Database
//ac.AddRoleAssignment(cn, id, CosmosDbSqlBuiltInRoles.Contributor); // Or the Container
}
});
```

Expand Down Expand Up @@ -157,6 +165,7 @@ them I'm more than happy to accept PRs or look at implementing things myself, ju

- 0.1.0 - Minimal to support the addition of Managed Identities as well as the Custom ID support.
- 0.2.0 - Added Bicep generator for the creation of more complex resources. Added Cosmos DB and Role Assignment support.
- 0.2.1 - Adds convenience methods for Role Assignments
- 0.2.X - More resources (see above). Tidy up/unify the APIs a little.
- 0.3.0 - Add a tool to complement `azd` so that the below is not required.
- 0.4.0 - Use above tool to also allow full customisation of the generated Bicep templates, down to the Container App Environment.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,16 @@ public CosmosDbSqlRoleAssignmentResource WithScope(CosmosDbSqlContainerResource
return this;
}

public CosmosDbSqlRoleAssignmentResource WithBuiltInRole(CosmosDbSqlBuiltInRole role)
{
return role switch
{
CosmosDbSqlBuiltInRole.Reader => WithReaderRole(),
CosmosDbSqlBuiltInRole.Contributor => WithContributorRole(),
_ => throw new ArgumentOutOfRangeException(nameof(role), "Invalid Built in Role")
};
}

public CosmosDbSqlRoleAssignmentResource WithReaderRole()
{
RoleDefinitionId = GetBaseBuiltInRolePrefix().WithArgument(new BicepStringValue("00000000-0000-0000-0000-000000000001"));
Expand Down Expand Up @@ -78,23 +88,12 @@ private BicepVariableValue GetDatabaseAccountReference()

private BicepFunctionCallValue GetBaseBuiltInRolePrefix()
{
// /subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.DocumentDB/databaseAccounts/${cosmosDbAccount.name}/sqlRoleDefinitions/00000000-0000-0000-0000-000000000002
return new BicepFunctionCallValue("resourceId", new BicepStringValue("Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions"),
new BicepPropertyAccessValue(GetDatabaseAccountReference(), "name"));
}

public override void Construct()
{
/*resource rvIdAccess 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments@2023-11-15' = {
parent: cosmosDbAccount
name: guid(cosmosDbAccount.id, 'rvIdAccess')
properties: {
roleDefinitionId: contributorRole
scope: cosmosDbAccount.id
principalId: '73f51bfc-7674-4256-8657-38cf695abe56'
}
}*/

Body.Add(new BicepResourceProperty("parent", GetDatabaseAccountReference()));
Body.Add(new BicepResourceProperty("name", new BicepFunctionCallValue("guid", new BicepPropertyAccessValue(GetDatabaseAccountReference(), "id"), new BicepStringValue(Name))));
var propertyBag = new BicepResourcePropertyBag("properties");
Expand Down Expand Up @@ -132,4 +131,10 @@ public override void Construct()
propertyBag.AddProperty("principalId", PrincipalId);
Body.Add(propertyBag);
}
}

public enum CosmosDbSqlBuiltInRole
{
Reader,
Contributor
}
58 changes: 54 additions & 4 deletions src/Achieve.Aspire.AzureProvisioning/CosmosDb.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ public static IResourceBuilder<AzureCosmosDbResource> AddAzureCosmosDbNoSqlAccou
{
fileOutput.AddParameter(new BicepParameter(AzureBicepResource.KnownParameters.PrincipalId, BicepSupportedType.String));
}

if (options.Principals.Count > 0)
{
foreach(var (paramName, bicepOutputReference) in options.Principals)
{
fileOutput.AddParameter(new BicepParameter(paramName, BicepSupportedType.String));
}
}

fileOutput.AddResource(accountResource);
foreach (var database in options.Databases)
Expand All @@ -38,6 +46,14 @@ public static IResourceBuilder<AzureCosmosDbResource> AddAzureCosmosDbNoSqlAccou
}
}

if (options.RoleAssignments.Count > 0)
{
foreach (var roleAssignment in options.RoleAssignments)
{
fileOutput.AddResource(roleAssignment);
}
}

fileOutput.AddOutput(new BicepOutput(AzureCosmosDbResource.AccountEndpointOutput, BicepSupportedType.String, accountResource.Name + ".properties.documentEndpoint"));

var resource = new AzureCosmosDbResource(name, fileOutput);
Expand All @@ -46,6 +62,13 @@ public static IResourceBuilder<AzureCosmosDbResource> AddAzureCosmosDbNoSqlAccou
{
resourceBuilder.WithParameter(AzureBicepResource.KnownParameters.PrincipalId);
}
if (options.Principals.Count > 0)
{
foreach(var (paramName, bicepOutputReference) in options.Principals)
{
resourceBuilder.WithParameter(paramName, bicepOutputReference);
}
}
return resourceBuilder.WithManifestPublishingCallback(resource.WriteToManifest);
}

Expand All @@ -72,10 +95,12 @@ public class CosmosDbAccountOptions(CosmosDbAccountResource resource)

public bool EnablePassPrincipalId { get; set; }

public CosmosDbDatabaseOptions AddDatabase(string name, Action<CosmosDbSqlDatabaseResource> configure)
public List<(string, BicepOutputReference)> Principals { get; set; } = [];

public CosmosDbDatabaseOptions AddDatabase(string name, Action<CosmosDbSqlDatabaseResource>? configure = null)
{
var database = new CosmosDbSqlDatabaseResource(Resource, name);
configure(database);
configure?.Invoke(database);
Databases.Add(name, new CosmosDbDatabaseOptions(this, database));
return Databases[name];
}
Expand All @@ -89,17 +114,42 @@ public CosmosDbAccountOptions WithDevelopmentGlobalAccess()
.WithContributorRole());
return this;
}

/// <summary>
/// Add a Role Assignment to the Cosmos DB Account.
/// </summary>
/// <param name="scope">Must be a <see cref="CosmosDbAccountResource" />, <see cref="CosmosDbSqlDatabaseResource"/> or <see cref="CosmosDbSqlContainerResource"/>.</param>
/// <param name="output">Bicep Output Reference. Must be a Principal ID.</param>
/// <param name="role">Built in role (currently) to assign.</param>
/// <returns></returns>
public CosmosDbAccountOptions WithRoleAssignment(BicepResource scope, BicepOutputReference output, CosmosDbSqlBuiltInRole role)
{
var paramName = output.Resource.Name + "Principal";
if (Principals.All(p => p.Item1 != paramName))
{
Principals.Add((paramName, output));
}
var roleAssignment = new CosmosDbSqlRoleAssignmentResource(output.Resource.Name + "Ra_" + Helpers.StableIdentifier(output.Resource.Name + scope.Name + role));
// Can't use WithScope as typed
roleAssignment.Scope = scope;
roleAssignment.WithBuiltInRole(role).PrincipalId = new BicepVariableValue(paramName);
RoleAssignments.Add(roleAssignment);
return this;
}

public CosmosDbAccountOptions WithRoleAssignment(BicepResource scope, IResourceBuilder<AzureManagedIdentityResource> identity, CosmosDbSqlBuiltInRole role) =>
WithRoleAssignment(scope, identity.GetOutput("PrincipalId"), role);
}

public class CosmosDbDatabaseOptions(CosmosDbAccountOptions parent, CosmosDbSqlDatabaseResource sqlDatabase)

Check warning on line 144 in src/Achieve.Aspire.AzureProvisioning/CosmosDb.cs

View workflow job for this annotation

GitHub Actions / publish

Parameter 'parent' is unread.
{
public CosmosDbSqlDatabaseResource Resource { get; set; } = sqlDatabase;
public Dictionary<string, CosmosDbSqlContainerResource> Containers { get; set; } = [];

public CosmosDbSqlContainerResource AddContainer(string name, Action<CosmosDbSqlContainerResource> configure)
public CosmosDbSqlContainerResource AddContainer(string name, Action<CosmosDbSqlContainerResource>? configure = null)
{
var container = new CosmosDbSqlContainerResource(Resource, name);
configure(container);
configure?.Invoke(container);
Containers.Add(name, container);
return container;
}
Expand Down
151 changes: 144 additions & 7 deletions tests/Achieve.Aspire.AzureProvisioning.Tests/CosmosDbTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,11 @@ public async Task BasicCosmosDbGeneratesCorrectly()
var id = builder.AddManagedIdentity("testid");
var cosmos = builder.AddAzureCosmosDbNoSqlAccount("cosmos", acc =>
{
acc.AddDatabase("db", db =>
{

}).AddContainer("cn", cn =>
{
cn.PartitionKey = new CosmosDbSqlContainerPartitionKey("/id");
});
acc.AddDatabase("db")
.AddContainer("cn", cn =>
{
cn.PartitionKey = new CosmosDbSqlContainerPartitionKey("/id");
});
});

var cosmosManifestBicep = await ManifestUtils.GetManifestWithBicep(cosmos.Resource);
Expand Down Expand Up @@ -106,4 +104,143 @@ public async Task BasicCosmosDbGeneratesCorrectly()
""";
Assert.Equal(expectedBicep, cosmosManifestBicep.BicepText);
}

[Fact]
public async Task CosmosAccountWithRbacGeneratesCorrectly()
{
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
var id = builder.AddManagedIdentity("testid");
var cosmos = builder.AddAzureCosmosDbNoSqlAccount("cosmos", acc =>
{
var db = acc.AddDatabase("db");
var conn = db.AddContainer("cn", cn =>
{
cn.PartitionKey = new CosmosDbSqlContainerPartitionKey("/id");
});
acc.WithDevelopmentGlobalAccess();
acc.WithRoleAssignment(db.Resource, id, CosmosDbSqlBuiltInRole.Reader);
acc.WithRoleAssignment(conn, id, CosmosDbSqlBuiltInRole.Contributor);
});


var cosmosManifestBicep = await ManifestUtils.GetManifestWithBicep(cosmos.Resource);

var expectedManifest = """
{
"type": "azure.bicep.v0",
"connectionString": "{cosmos.outputs.accountEndpoint}",
"path": "cosmos.achieve.bicep",
"params": {
"principalId": "",
"testidPrincipal": "{testid.outputs.PrincipalId}"
}
}
""";
Assert.Equal(expectedManifest, cosmosManifestBicep.ManifestNode.ToString());

var expected = """
targetScope = 'resourceGroup'
@description('The location of the resource group.')
param location string = resourceGroup().location
param principalId string
param testidPrincipal string
resource cosmosDbAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' = {
name: 'cosmos${uniqueString(resourceGroup().id)}'
location: location
properties: {
databaseAccountOfferType: 'Standard'
backupPolicy: {
type: 'Continuous'
continuousModeProperties: {
tier: 'Continuous7Days'
}
}
capabilities: [
{
name: 'EnableServerless'
}
]
consistencyPolicy: {
defaultConsistencyLevel: 'Session'
}
locations: [
{
failoverPriority: 0
locationName: location
isZoneRedundant: false
}
]
minimalTlsVersion: 'Tls12'
publicNetworkAccess: 'SecuredByPerimeter'
}
}
resource db 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2023-11-15' = {
parent: cosmosDbAccount
name: 'db'
location: location
properties: {
resource: {
id: 'db'
}
}
}
resource cn 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2023-11-15' = {
parent: db
name: 'cn'
location: location
properties: {
resource: {
id: 'cn'
partitionKey: {
kind: 'Hash'
paths: [
'/id'
]
}
}
}
}
resource developmentAccess 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments@2023-11-15' = {
parent: cosmosDbAccount
name: guid(cosmosDbAccount.id,'developmentAccess')
properties: {
roleDefinitionId: resourceId('Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions',cosmosDbAccount.name,'00000000-0000-0000-0000-000000000002')
scope: cosmosDbAccount.id
principalId: principalId
}
}
resource testidRa_B9899A8C 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments@2023-11-15' = {
parent: cosmosDbAccount
name: guid(cosmosDbAccount.id,'testidRa_B9899A8C')
properties: {
roleDefinitionId: resourceId('Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions',cosmosDbAccount.name,'00000000-0000-0000-0000-000000000001')
scope: '${cosmosDbAccount.id}/dbs/${db.name}'
principalId: testidPrincipal
}
}
resource testidRa_BD6A548A 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments@2023-11-15' = {
parent: cosmosDbAccount
name: guid(cosmosDbAccount.id,'testidRa_BD6A548A')
properties: {
roleDefinitionId: resourceId('Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions',cosmosDbAccount.name,'00000000-0000-0000-0000-000000000002')
scope: '${cosmosDbAccount.id}/dbs/${db.name}/colls/${cn.name}'
principalId: testidPrincipal
}
}
output accountEndpoint string = cosmosDbAccount.properties.documentEndpoint
""";

Assert.Equal(expected, cosmosManifestBicep.BicepText);
}
}

0 comments on commit c8a06bf

Please sign in to comment.