MS Graph SDK - 在一个请求中向用户授予多个 appRoleAssignments

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

正在此处查看 'appRoleAssignment' 的 MS 文档:Grant an appRoleAssignment to a user

代码示例显示将用户添加到单个应用程序角色,即每个图形请求分配一个角色:

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);

问。那么我们如何在单个请求中分配多个应用程序角色呢?

假设我们有一个 'Roles' WebbApp 页面,其中包含一个复选框列表,每个 AppRole 都有一个复选框,用户选择他们想要分配给用户的所有相关角色,然后点击保存按钮。我需要弄清楚我们如何一次性分配多个角色。在现实世界中,头脑正常的人不会为了为单个用户配置角色而点击保存按钮二十次。向 Graph 发送多个请求似乎不是预期的解决方案。

问。另一个难题是我们如何在同一请求期间添加和删除用户的 AppRole 分配?即复选框列表代表我们可能希望从用户成员资格中删除的角色,同时将它们添加到新的角色分配中。在我之前的项目中,我使用 Net Core 中的 Microsoft Identity 包很好地工作了这个原因和影响,但是尝试使用 Azure AD 实现相同的原因和影响并不是那么简单...

下图显示了给定用户的 AppRole 选择示例:

感谢

这或多或少适用于你的两个问题,我不完全确定这是否有效,但你可以 Json 图形的批处理端点将多个命令构建为 1 个批处理。 https://docs.microsoft.com/en-us/graph/json-batching

如果这不起作用,恐怕您将不得不多次循环调用图表。

对于第 2 季度,这假定您已经拥有当前角色列表,以便您可以针对特定角色分配专门调用删除端点。如果您没有当前角色列表,https://docs.microsoft.com/en-us/graph/api/serviceprincipal-get?view=graph-rest-1.0&tabs=http this is to get the entire app profile which includes the app roles, then use the user app role assignments endpoint https://docs.microsoft.com/en-us/graph/api/user-list-approleassignments?view=graph-rest-1.0&tabs=http 以获取分配给用户的角色列表。将他们匹配起来,看看他们目前属于哪些角色。

我已经尝试并测试了一个现在有效的解决方案,但由于 Stack Overflow 30,000 个字符的限制,我无法展示我最终所做的完整代码示例,但包含了一些片段下面。

我想要实现的目标的总结:

在 WebApp 中提供 UI,管理员可以 add/modify 将 AppRoleAssingments 分配给 Azure AD 用户,而无需登录到 Azure 门户本身。

使用 .NET Core 3.1 Razor Pages,我创建了一个包含复选框列表的页面,每个 AppRole 都有一个复选框。加载页面时,将检查用户当前所属的每个角色的 AppRoles。然后管理员可以选中或取消选中他们想要为该用户修改的任何角色,当点击保存按钮时,我们将更新的 AppRoles 列表发送回 Graph。

我使用 Javascript 和 Ajax 来 send/receive razor 页面和页面模型之间的数据。

第 1 步 - AJAX 调用 razor 页面模型,该模型又调用 MS Graph 并获取为 Azure AD 中的租户找到的所有 AppRoles 的列表。在同一方法中,我们然后处理对 Graph 的二次调用,以获取所选用户当前所属的 AppRoles 列表。使用 foreach 循环,我们查看用户当前属于哪个 AppRoles,并针对 'Assinged' = true 属性 标记我们 return 到剃刀页面的 AppRoles 列表。

第 2 步 - razor 页面填充了每个 AppRole 的复选框,并选中了用户当前所属的复选框。管理员在点击保存按钮之前为用户选中或取消选中 AppRoles,然后post将更新返回到 razor 页面模型。

第 3 步 - 在我们 post 更新回 MS Graph 之前,我们必须确定用户已经属于哪些 AppRoles,否则我们会在 MS Graph SDK 中遇到服务异常。

这是很难弄清楚的部分,但是在比较了用户已经拥有的内容与我们需要进行的更改之后,我们在 foreach 循环中将更新发送回 Graph,即我们只能添加或一次将用户移至 AppRoleAssignment。

下面的代码示例显示了 razor 页面模型位,不幸的是,razor 页面中的代码和帮助注释太多,无法挤进去。希望它能帮助其他人找到类似的解决方案。


Razor 页面模型

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 currentley 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 currentley 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);
        }
    }
}

我们为当前选定的用户更新 AppRoles 的 Razor 页面: