Search code examples
c#azure-active-directoryroles.net-core-3.1microsoft-graph-sdks

MS Graph SDK - Grant multiple appRoleAssignments to a user in one request


Revieweing the MS Docs for 'appRoleAssignment' here: Grant an appRoleAssignment to a user

The code example shows adding a user to a single App Role i.e. one role assignment per graph request:

GraphServiceClient graphClient = new GraphServiceClient( authProvider );

var appRoleAssignment = new AppRoleAssignment
{
    PrincipalId = Guid.Parse("principalId-value"),
    ResourceId = Guid.Parse("resourceId-value"),
    AppRoleId = Guid.Parse("appRoleId-value")
};

await graphClient.Users["{id}"].AppRoleAssignments
    .Request()
    .AddAsync(appRoleAssignment);

Q. So how do we assign multiple App Roles in a single request?

Imagine we have a 'Roles' WebbApp page with a list of check boxes, a checkbox for each AppRole, the user selects all relevant Roles they want to assign to a user, then hits the Save button. I need to figure out how we assign multiple roles in one go. In the real world, no one in their right mind is going to hit the save button twenty times just to configure the roles for a single user. Sending multiple requests to the Graph just doesn't seem like the intended solution.

Q. The other conundrum is how can we add and remove AppRole assignments from a user during the same request? i.e. the list of checkboxes represent roles that we may want to remove from the user's membership, as well as adding them to new role assignments at the same time. I had this cause & affect working nicely using the Microsoft Identity package in Net Core in my previous project, but trying to achieve the same cause & affect using Azure AD is not as straight forward...

Image below shows an example in the AppRole selection for a given user:

enter image description here

Thx


Solution

  • I have tried and tested a solution which is working now, but due to the Stack Overflow 30,000 characters limits, i wasn't able to show the complete code example in what I ended up doing, but have included some snippets below.

    Summary of what I was wanting to achieve:

    Provide a UI in in the WebApp whereby and administrator could add/modify the AppRoleAssingments to an Azure AD user without having to login to Azure Portal itself.

    Using .NET Core 3.1 Razor Pages, I created a page with a list of checkboxes, one checkbox for each AppRole. When the page is loaded, the AppRoles are checked for each role the user is currently a member of. The administrator can then check or uncheck any of the roles they want to modify for that user, when hitting the save button, we send the updated list of AppRoles back to the Graph.

    I used Javascript with Ajax to send/receive the data between the razor page and page model.

    Step 1 - AJAX call to the razor page model which in turn calls the MS Graph and fetches a list of all the AppRoles found for the Tenant within Azure AD. Within the same method, we then process a secondary call to the Graph in order to fetch the list of AppRoles the selected user is currently a member of. Using a foreach loop, we look at which AppRoles the user currently belongs to and put a flag against the 'Assigned' = true property for the list of AppRoles we return to the razor page.

    Step 2 - The razor page is populated with checkboxes for each AppRole and checked for ones the user currently belongs to. The administrator checks or unchecks the AppRoles for the user before hitting the save button and posts the updates back to the razor page model.

    Step 3 - Before we post the updates back to MS Graph, we have to determine which AppRoles the user already belongs to, otherwise we hit a Service Exception in the MS Graph SDK.

    This was the hard part to figure out, but after drawing the comparison between what the user already has versus what changes we need to make then we send the updates back to Graph in a foreach loop i.e. we could only add or remove the user to an AppRoleAssignment one at a time.

    The code example below shows the razor page model bits, unfortunately there was too much code and help notes to squeeze in the razor page as well. Hopefully it helps others in finding a similar solution.

    Razor page model:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading.Tasks;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.AspNetCore.Mvc.RazorPages;
    using Microsoft.Extensions.Logging;
    using Microsoft.Extensions.Options;
    using Microsoft.Graph;
    using Microsoft.Identity.Web;
    using WebApp_RAZOR.Models.Users;
    using WebApp_RAZOR.Repository.Users;
    using WebApp_RAZOR.Services;
    using static WebApp_RAZOR.Repository.Users.CurrentUser;
    using Constants = WebApp_RAZOR.Infrastructure.Constants;
    
    namespace WebApp_RAZOR.Pages.AppRoles
    {
        public class IndexModel : PageModel
        {
            private readonly ILogger<IndexModel> _logger;
            private readonly ITokenAcquisition tokenAcquisition;
            private readonly WebOptions webOptions;
            public readonly ICurrentUser _currentUser;
    
            public IndexModel(ILogger<IndexModel> logger,
                ITokenAcquisition tokenAcquisition,
                IOptions<WebOptions> webOptionValue,
                ICurrentUser currentUser)
            {
                _logger = logger;
                this.tokenAcquisition = tokenAcquisition;
                this.webOptions = webOptionValue.Value;
                _currentUser = currentUser;
            }
    
            [BindProperty]
            public TenantUser TenantUser { get; set; }
    
            [BindProperty]
            public List<AppRoles> Roles { get; set; } // Stores the AppRoles
    
            public class AppRoles
            {
                // Used only for the ajax POST request when sending
                // the updated AppRoles list back to the razor page model.
                public string UserId { get; set; }
    
                // AppRole Id, see Azure App Manifest File.
                // This is the unique ID of the specific application role 
                // that the assignment grants access to. A string of 0's 
                // means this is a default access assignment, and anything 
                // else means there has been a specific role defined by the 
                // application. This ID can be useful when performing management 
                // operations against this application using PowerShell or other 
                // programmatic interfaces.
                public Guid? AppRoleId { get; set; }
    
                // Friendly description
                public string Description { get; set; }
    
                // Friendly display name
                public string DisplayName { get; set; }
    
                // Same as displayName, no spaces
                public string Value { get; set; }
    
                // 'true' if the User is assinged the AppRole
                // Use a string value as passing the data to Ajax
                // will parse a boolean as a string anyway.
                public string Assigned { get; set; } 
            }
    
            // Stores the AppRoles that user already belongs to when querying the MS Graph
            // Used in the 'OnPostAddAppRoleAsync' method.
            [BindProperty]
            public List<AppRoleAssingments> AppRolesAlreadyAssignedToUser { get; set; } 
    
            public class AppRoleAssingments
            {
                // AppRole Id, see Azure App Manifest File.
                // This is the unique ID of the specific application role 
                // that the assignment grants access to. A string of 0's 
                // means this is a default access assignment, and anything 
                // else means there has been a specific role defined by the 
                // application. This ID can be useful when performing management 
                // operations against this application using PowerShell or other 
                // programmatic interfaces.
                public Guid? AppRoleId { get; set; }
    
                // This is the unique ID of this specific role assignment, 
                // which is a link between the user or group and the service 
                // principal object. This ID can be useful when performing 
                // management operations against this application using 
                // PowerShell or other programmatic interfaces.
                public string AssingmentId { get; set; }
            }
    
            /// <summary>
            /// Method is run when the razor page is first loaded.
            /// The javascript window.onload function then initiates a call to the server using
            /// 'OnGetFetchAppRolesAsync' method below to fetch the list of AppRoles
            /// as well as the list of AppRoleAssignments for the currently selected user.
            /// </summary>
            /// <param name="id"></param>
            public void OnGet(string id)
            {
                // Get the User Id from the URL in the page request.
                ViewData["MyRouteId"] = id; 
            }
    
            /// <summary>
            /// Methiod is called from Ajax POST and fetches the list of AppRoles
            /// as well as the list of AppRoleAssignments for the currently selected user.
            /// </summary>
            /// <param name="UserId"></param>
            /// <returns></returns>
            public async Task<IActionResult> OnGetFetchAppRolesAsync(string UserId)
            {
                Roles = new List<AppRoles>(); // Newup the Roles list.
    
                // Get and instance of the graphClient.
                GraphServiceClient graphClient = GetGraphServiceClient(new[] { Constants.ScopeUserReadBasicAll });
    
                try
                {
                    //throw new Exception(); // Testing Only!
    
                    var serviceprincipals = await graphClient.ServicePrincipals["36463454-a184-3jf44j-b360-39573950385960"]
                        .Request()
                        .GetAsync();
    
                    var appRoles = serviceprincipals.AppRoles;
    
                    // Iterate through the list of AppRoles returned from MS Graph.
                    foreach (var role in appRoles)
                    {
                        // For each AppRole, add it to the Roles List.
                        Roles.Add(new AppRoles
                        {
                            AppRoleId = role.Id,
                            DisplayName = role.DisplayName,
                            Description = role.Description,
                            Value = role.Value,
                            // Mark 'Assinged' property as false for now, we'll 
                            // check it against the user in next step.
                            Assigned = "false" 
                        }); 
                    }
                }
                catch (ServiceException ex)
                {
                    // Get the current user properties from the httpcontext currentUser repository for logging.
                    CurrentUserProperties currentUser = (CurrentUserProperties)_currentUser.GetCurrentUser();
    
                    // Graph service exception friendly message
                    var errorMessage = ex.Error.Message;
    
                    string logEventCategory = "Microsoft Graph";
                    string logEventType = "Service Exception";
                    string logEventSource = null;
                    string logUserId = currentUser.Id;
                    string logUserName = currentUser.Username;
                    string logForename = currentUser.Forename;
                    string logSurname = currentUser.Surname;
                    string logData = errorMessage;
    
                    _logger.LogError(ex,
                        "{@logEventCategory}" +
                        "{@logEventType}" +
                        "{@logEventSource}" +
                        "{@logUserId}" +
                        "{@logUsername}" +
                        "{@logForename}" +
                        "{@logSurname}" +
                        "{@logData}",
                        logEventCategory,
                        logEventType,
                        logEventSource,
                        logUserId,
                        logUserName,
                        logForename,
                        logSurname,
                        logData);
                }
    
                try
                {
                    //throw new Exception(); // Testing Only!
    
                    // Get the list of AppRoles the currently selected user is a member of.
                    var appRoleAssignments = await graphClient.Users[UserId].AppRoleAssignments
                        .Request()
                        .GetAsync();
    
                    // For each AppRole the user is a member of, update the  
                    // assigned property in the 'List<AppRoles> Roles' to true.
                    // When the razor page is returned, each AppRole the user
                    // is a member of will have the checkbox checked...
                    foreach (var role in appRoleAssignments)
                    {
                        var obj = Roles.FirstOrDefault(x => x.AppRoleId == role.AppRoleId);
                        if (obj != null) obj.Assigned = "true";
                    }
                }
                catch (ServiceException ex)
                {
                    // Get the current user properties from the httpcontext currentUser repository for logging.
                    CurrentUserProperties currentUser = (CurrentUserProperties)_currentUser.GetCurrentUser();
    
                    // Graph service exception friendly message.
                    var errorMessage = ex.Error.Message;
    
                    string logEventCategory = "Microsoft Graph";
                    string logEventType = "Service Exception";
                    string logEventSource = null;
                    string logUserId = currentUser.Id;
                    string logUserName = currentUser.Username;
                    string logForename = currentUser.Forename;
                    string logSurname = currentUser.Surname;
                    string logData = errorMessage;
    
                    _logger.LogError(ex,
                        "{@logEventCategory}" +
                        "{@logEventType}" +
                        "{@logEventSource}" +
                        "{@logUserId}" +
                        "{@logUsername}" +
                        "{@logForename}" +
                        "{@logSurname}" +
                        "{@logData}",
                        logEventCategory,
                        logEventType,
                        logEventSource,
                        logUserId,
                        logUserName,
                        logForename,
                        logSurname,
                        logData);
                }
                return new JsonResult(Roles);
            }
    
            /// <summary>
            /// The method is called by Ajax, the JS code passes the list of the AppRoles from Razor to the page model.
            /// We conduct a comparison against MS Graph to determine which AppRoles the user is currently a member of
            /// and which ones they require to be removed.
            /// </summary>
            /// <param name="updatedRolesArrayFromRazorPage"></param>
            /// <returns></returns>
            public async Task<IActionResult> OnPostAddAppRoleAsync([FromBody]List<AppRoles> updatedRolesArrayFromRazorPage)
            {
                // Get the first object set from the array received 
                // from JS AJAX Call and get the details of the user's id.
                var firstElement = updatedRolesArrayFromRazorPage.First();
                var userId = firstElement.UserId;
    
                // Get and instance of the graphClient.
                GraphServiceClient graphClient = GetGraphServiceClient(new[] { Constants.ScopeUserReadBasicAll });
    
                try
                {
                    //throw new Exception(); // Testing Only!
    
                    // Get the list of AppRoles that the current user is a member of.
                    var appRoleAssignments = await graphClient.Users[userId].AppRoleAssignments
                        .Request()
                        .GetAsync();
    
                    // For each AppRole the user is a member of, add them to the AppRolesAlreadyAssignedToUser list.
                    foreach (var role in appRoleAssignments)
                    {
                        AppRolesAlreadyAssignedToUser.Add(new AppRoleAssingments
                        { 
                            AppRoleId = role.AppRoleId,
                            // 'Assignment ID' blade found in AzureAd/UsersApplications/{thisWebAppName}/{AppRoleName}.
                            // Go to Azure Active Directory > Users > Select specific User > Applications > Select the 
                            // application to navigate to "Assignment Details" blade.
                            AssingmentId = role.Id 
                        });
                    }
                }
                catch (ServiceException ex)
                {
                    // Get the current user properties from the httpcontext currentUser repository for logging.
                    CurrentUserProperties currentUser = (CurrentUserProperties)_currentUser.GetCurrentUser();
    
                    // Graph service exception friendly message.
                    var errorMessage = ex.Error.Message;
    
                    string logEventCategory = "Microsoft Graph";
                    string logEventType = "Service Exception";
                    string logEventSource = null;
                    string logUserId = currentUser.Id;
                    string logUserName = currentUser.Username;
                    string logForename = currentUser.Forename;
                    string logSurname = currentUser.Surname;
                    string logData = errorMessage;
    
                    _logger.LogError(ex,
                        "{@logEventCategory}" +
                        "{@logEventType}" +
                        "{@logEventSource}" +
                        "{@logUserId}" +
                        "{@logUsername}" +
                        "{@logForename}" +
                        "{@logSurname}" +
                        "{@logData}",
                        logEventCategory,
                        logEventType,
                        logEventSource,
                        logUserId,
                        logUserName,
                        logForename,
                        logSurname,
                        logData);
                }
    
                // Now we have a list of both the AppRoles the current user is a memeber of,
                // as well as the updated list of AppRoles that were posted from the Razor Page,
                // we perform a comparison in the next code section below so we can determine
                // which roles are already assigned i.e we dont need to add them via the MS Graph again
                // otherwise we will encounter a ServiceException error advising us the role is already assigned.
                // We then check which roles are already assigned to the user that now need to be removed.
                // We can only add or remove roles from the user one at a time due to the limitations of MS Graph.
    
                // Iterate through list of the AppRoles received from the Razor Page.
                // Note each AppRole will have either a true or false value against the 'Assigned' property.
                foreach (var role in updatedRolesArrayFromRazorPage)
                {
                    // ------------------------------------------------------------------------
                    // Perform the comparison to see which AppRoles we need to add to the user.
                    // ------------------------------------------------------------------------
                    if (role.Assigned == "true") // Assigned status from AppRole checkbox selection in Razor Page.
                    {
                        // We do a comparison between the tow lists, if the role is not alread present in
                        // the list from the MS Graph then we know we need to assign the user to this AppRole.
                        bool exists = AppRolesAlreadyAssignedToUser.Any(r => r.AppRoleId == role.AppRoleId);
    
                        // If returns false the we will assign the user to this role via MS Graph.
                        if (exists == false)
                        {
                            // Declare the new appRoleAssingment.
                            var appRoleAssignment = new AppRoleAssignment
                            {
                                // principalId: The id of the user to whom you are assigning the app role.
                                PrincipalId = Guid.Parse(userId),
    
                                // resourceId: The id of the resource servicePrincipal that has defined the app role.
                                ResourceId = Guid.Parse("6g4656g54g46-a184-4f8a-b360-656h7567567h75"),
    
                                // appRoleId: The id of the appRole (defined on the resource service principal) to assign to the user.
                                AppRoleId = Guid.Parse(role.AppRoleId.ToString())
                            };
    
                            try
                            {
                                // Add the above AppRoleAssingment to the user.
                                await graphClient.Users[userId].AppRoleAssignments
                                    .Request()
                                    .AddAsync(appRoleAssignment);
                            }
                            catch (ServiceException ex)
                            {
                                // Get the current user properties from the httpcontext currentUser repository for logging.
                                CurrentUserProperties currentUser = (CurrentUserProperties)_currentUser.GetCurrentUser();
    
                                // Graph service exception friendly message.
                                var errorMessage = ex.Error.Message;
    
                                string logEventCategory = "Microsoft Graph";
                                string logEventType = "Service Exception";
                                string logEventSource = null;
                                string logUserId = currentUser.Id;
                                string logUserName = currentUser.Username;
                                string logForename = currentUser.Forename;
                                string logSurname = currentUser.Surname;
                                string logData = errorMessage;
    
                                _logger.LogError(ex,
                                    "{@logEventCategory}" +
                                    "{@logEventType}" +
                                    "{@logEventSource}" +
                                    "{@logUserId}" +
                                    "{@logUsername}" +
                                    "{@logForename}" +
                                    "{@logSurname}" +
                                    "{@logData}",
                                    logEventCategory,
                                    logEventType,
                                    logEventSource,
                                    logUserId,
                                    logUserName,
                                    logForename,
                                    logSurname,
                                    logData);
                            }
                        }
                    }
                    // -----------------------------------------------------------------------------
                    // Perform the comparison to see which AppRoles we need to remove from the user.
                    // -----------------------------------------------------------------------------
                    else if (role.Assigned == "false")
                    {
                        var exists = AppRolesAlreadyAssignedToUser.FirstOrDefault(r => r.AppRoleId == role.AppRoleId);
    
                        if (exists != null) // Assigned status from AppRole checkbox selection in Razor Page.
                        {
                            var appRoleId = exists.AppRoleId;
                            var assignmentId = exists.AssingmentId;
    
                            try
                            {
                                await graphClient.Users[userId].AppRoleAssignments[assignmentId]
                                .Request()
                                .DeleteAsync();
                            }
                            catch (ServiceException ex)
                            {
                                // Get the current user properties from the httpcontext currentUser repository for logging.
                                CurrentUserProperties currentUser = (CurrentUserProperties)_currentUser.GetCurrentUser();
    
                                // Graph service exception friendly message.
                                var errorMessage = ex.Error.Message;
    
                                string logEventCategory = "Microsoft Graph";
                                string logEventType = "Service Exception";
                                string logEventSource = null;
                                string logUserId = currentUser.Id;
                                string logUserName = currentUser.Username;
                                string logForename = currentUser.Forename;
                                string logSurname = currentUser.Surname;
                                string logData = errorMessage;
    
                                _logger.LogError(ex,
                                    "{@logEventCategory}" +
                                    "{@logEventType}" +
                                    "{@logEventSource}" +
                                    "{@logUserId}" +
                                    "{@logUsername}" +
                                    "{@logForename}" +
                                    "{@logSurname}" +
                                    "{@logData}",
                                    logEventCategory,
                                    logEventType,
                                    logEventSource,
                                    logUserId,
                                    logUserName,
                                    logForename,
                                    logSurname,
                                    logData);
                            }
                        }
                    }
                }
    
                return new JsonResult(new { Url = "users/index" });
            }
    
            private GraphServiceClient GetGraphServiceClient(string[] scopes)
            {
                return GraphServiceClientFactory.GetAuthenticatedGraphClient(async () =>
                {
                    string result = await tokenAcquisition.GetAccessTokenForUserAsync(scopes);
                    return result;
                }, webOptions.GraphApiUrl);
            }
        }
    }
    

    The Razor page where we update AppRoles for the currently selected user:

    enter image description here