Assigning Roles to Principals with Azure.ResourceManager.Authorization

I felt compelled to write this post for a few reasons, most centrally that, while I do applaud the team for putting out a nice modern library I must also confess that it has more than a few idiosyncrasies and the documentation is very lacking. To the effect of the former, I feel the need to talk through an experience I had recently involving a client project.

In Azure there are many different kinds of users, each relating back to a principal: User, Group, Service, Application, and perhaps others. Of these all but Application can be assigned RBAC (Role Based Access Control) roles in Azure, the foundational way security is handled.

The Azure.ResourceManager (link) and its related subprojects are the newest release aimed at helping developers code against the various Azure APIs to enable code based execution of common operations – this all replaces the previous package Microsoft.Azure.Management (link) which has been deprecated.

A full tutorial on this would be helpful and while the team has put together some documentation, more is needed. For this post I would like to focus on one particular aspect.

Assigning a Role

I recently authored the following code aimed at assigning a Service Principal to an Azure RBAC role. Attempting this code frequently led to an error stating I was trying to change Tenant Id, Application Id, Principal Id, or Scope. Yet as you can see, none of those should be changing.

public Task AssignRoleToServicePrincipal(Guid objectId, string roleName, string scopePath)
{
var tcs = new TaskCompletionSource();
Task.Run(() =>
{
try
{
var roleAssignmentResourceId = RoleAssignmentResource.CreateResourceIdentifier(scopePath, roleName);
var roleAssignmentResource = _armClient.GetRoleAssignmentResource(roleAssignmentResourceId);
var operationContent = new RoleAssignmentCreateOrUpdateContent(roleAssignmentResource.Id, objectId)
{
PrincipalType = RoleManagementPrincipalType.ServicePrincipal
};
var operationOutcome = roleAssignmentResource.Update(Azure.WaitUntil.Completed, operationContent);
tcs.TrySetResult();
}
catch (Exception ex)
{
tcs.TrySetException(ex);
}
});
return tcs.Task;
}
view raw version1.cs hosted with ❤ by GitHub

I have some poor variable naming in here but here is a description of the parameters to this method:

  • objectId – the unique identifier for the object within Azure AD (Entra). It is using this Id that we assign roles and take actions involving this Service Prinicipal
  • roleName – in Azure parlance, this is the name of the role which is a Guid, it can also be thought of as the role Id. There is another property called RoleName which returns the human readable name, ie Reader or Contributor.
  • scopePath – this is the path of assignment, that is where in the Azure resource hierarchy we want to make the assignment. This could reference a Subscription, a Resource Group, or a Resource itself

As you can see, there is no mutating of the values listed. While RoleAssignmentCreateOrUpdateContent does have a Scope property, it is read-only. The error was sporadic and annoying. Eventually I realized the issue, it is simple but does require a deeper understanding of how role assignments work in Azure.

The Key is the Id

Frankly, knowing what I know now I am not sure how the above code ever worked. See, when you create a role assignment that action, in and of itself, has to have a unique identifier. A sort of entry that represents this role definition with this scoping. In the above I am missing that, I am trying to use the Role Definition Id instead. After much analysis I finally realized this and modified the code as such:

public Task AssignRoleToServicePrincipal(Guid objectId, string roleDefId, string scopePath)
{
var tcs = new TaskCompletionSource();
Task.Run(() =>
{
try
{
var scopePathResource = new ResourceIdentifier(scopePath);
var roleDefId = $"/subscriptions/{scopePathResource.SubscriptionId}/providers/Microsoft.Authorization/roleDefinitions/{roleName}";
var operationContent = new RoleAssignmentCreateOrUpdateContent(new ResourceIdentifier(roleDefId), objectId)
{
PrincipalType = RoleManagementPrincipalType.ServicePrincipal
};
var roleAssignmentResourceId = RoleAssignmentResource.CreateResourceIdentifier(scopePath, Guid.NewGuid().ToString());
var roleAssignmentResource = _armClient.GetRoleAssignmentResource(roleAssignmentResourceId);
var operationOutcome = roleAssignmentResource.Update(Azure.WaitUntil.Completed, operationContent);
tcs.TrySetResult();
}
catch (Exception ex)
{
tcs.TrySetException(ex);
}
});
return tcs.Task;
}
view raw version2.cs hosted with ❤ by GitHub

As you can see, this code expands things. Most notably is the first section where I build the full Role Definition Resource Id this is the unique Id for a Role Definition, which can later be assigned.

Using this library, the content object indicates what I want to do – assign objectId the role definition provided. However, what I was missing was the second part: I had to tell it WHERE to make this assignment. It seems obvious now but, it was not at the time.

The obvious solution here, since it has to be unique, is to just use Guid.NewGuid().ToString(). When I call Update it will get the point of assignment from roleAssignmentResource.

And that was it, just like that, the error went away (a horribly misleading error mind you). Now the system works and I learned something about how this library works.

Hope it helps.

One thought on “Assigning Roles to Principals with Azure.ResourceManager.Authorization

Leave a comment