在此处查看 MS 文档中的“appRoleAssignment”:
代码示例显示将用户添加到单个应用程序角色,即每个图形请求分配一个角色:
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);
问。那么我们如何在单个请求中分配多个应用程序角色呢?
假设我们有一个“角色”WebbApp 页面,其中包含复选框列表,每个 AppRole 都有一个复选框,用户选择要分配给用户的所有相关角色,然后单击“保存”按钮。我需要弄清楚如何一次性分配多个角色。在现实世界中,任何头脑正常的人都不会仅仅为了配置单个用户的角色而点击保存按钮二十次。向图表发送多个请求似乎不是预期的解决方案。
问。另一个难题是我们如何在同一请求期间添加和删除用户的 AppRole 分配?即复选框列表代表我们可能希望从用户成员资格中删除的角色,并同时将它们添加到新的角色分配中。在我之前的项目中,我使用 Net Core 中的 Microsoft Identity 包很好地实现了此原因和影响,但尝试使用 Azure AD 实现相同的原因和影响并不那么简单......
下图显示了给定用户的 AppRole 选择示例:
谢谢
这或多或少适用于您的两个问题,我不完全确定这会起作用,但您可以使用图形的 Json 批处理端点将多个命令构建为一批。 https://learn.microsoft.com/en-us/graph/json-batching
如果这不起作用,恐怕你将不得不多次循环和调用图表。
对于第二季度,假设您已经拥有当前角色的列表,以便您可以针对特定角色分配专门调用删除端点。如果您没有当前角色的列表,https://learn.microsoft.com/en-us/graph/api/serviceprincipal-get?view=graph-rest-1.0&tabs=http这是为了获取包括应用程序角色的整个应用程序配置文件,然后使用用户应用程序角色分配端点 https://learn.microsoft.com/en-us/graph/api/user-list-approleassignments?view=graph-rest-1.0 &tabs=http 获取用户分配到的角色列表。将他们匹配起来,看看他们目前属于哪个角色。
我已经尝试并测试了一个现在有效的解决方案,但由于 Stack Overflow 30,000 个字符的限制,我无法在我最终所做的事情中显示完整的代码示例,但在下面包含了一些片段。
我想要实现的目标总结:
在 WebApp 中提供 UI,管理员可以通过该 UI 向 Azure AD 用户添加/修改 AppRoleAssingments,而无需登录到 Azure 门户本身。
使用 .NET Core 3.1 Razor Pages,我创建了一个包含复选框列表的页面,每个 AppRole 对应一个复选框。加载页面时,将检查用户当前所属的每个角色的 AppRoles。然后,管理员可以选中或取消选中他们想要为该用户修改的任何角色,当点击保存按钮时,我们会将更新后的 AppRoles 列表发送回图表。
我使用 Javascript 和 Ajax 在 razor 页面和页面模型之间发送/接收数据。
第 1 步 - 对 razor 页面模型的 AJAX 调用,该模型进而调用 MS Graph 并获取为 Azure AD 中的租户找到的所有 AppRoles 的列表。在同一方法中,我们随后处理对 Graph 的二次调用,以获取所选用户当前所属的 AppRoles 列表。使用 foreach 循环,我们查看用户当前属于哪个 AppRoles,并针对我们返回到 razor 页面的 AppRoles 列表的 'Assigned' = true 属性放置一个标志。
第 2 步 - razor 页面填充了每个 AppRole 的复选框,并检查用户当前所属的角色。管理员在点击保存按钮之前检查或取消选中用户的 AppRoles,并将更新发布回 razor 页面模型。
第 3 步 - 在将更新发布回 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 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);
}
}
}
我们为当前所选用户更新 AppRoles 的 Razor 页面: