From 1cf95e2dc3cda92da298904e756f21a3849cb62e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Can=20Karag=C3=B6z?= Date: Mon, 26 Oct 2026 23:29:48 +0300 Subject: [PATCH 1/3] feat(authorization): add self-service permission inspection API --- .../Policy/SandboxUAuthAccessPolicy.cs | 3 +- .../Defaults/UAuthActions.cs | 5 ++ .../IAuthorizationEndpointHandler.cs | 1 + .../Endpoints/UAuthEndpointRegistrar.cs | 4 ++ .../Responses/UserPermissionsResult.cs | 10 ++++ .../Endpoints/AuthorizationEndpointHandler.cs | 24 +++++++- .../Extensions/ServiceCollectionExtensions.cs | 1 + .../Infrastructure/RolePermissionResolver.cs | 3 + .../Services/UserPermissionsService.cs | 58 +++++++++++++++++++ .../Abstractions/IUserPermissionsService.cs | 9 +++ .../Abstractions/IAuthorizationClient.cs | 15 +++++ .../Services/UAuthAuthorizationClient.cs | 6 ++ 12 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 src/authorization/CodeBeam.UltimateAuth.Authorization.Contracts/Responses/UserPermissionsResult.cs create mode 100644 src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Services/UserPermissionsService.cs create mode 100644 src/authorization/CodeBeam.UltimateAuth.Authorization/Abstractions/IUserPermissionsService.cs diff --git a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Policy/SandboxUAuthAccessPolicy.cs b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Policy/SandboxUAuthAccessPolicy.cs index cbe7e692..6eb114e4 100644 --- a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Policy/SandboxUAuthAccessPolicy.cs +++ b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Policy/SandboxUAuthAccessPolicy.cs @@ -190,7 +190,8 @@ UAuthActions.Credentials.ListAdmin or UAuthActions.Sessions.GetChainSelf or UAuthActions.Sessions.GetChainAdmin or UAuthActions.Sessions.ListChainsSelf or - UAuthActions.Sessions.ListChainsAdmin; + UAuthActions.Sessions.ListChainsAdmin or + UAuthActions.Authorization.Permissions.GetSelf; private static bool IsAdminAction(string action) { diff --git a/src/CodeBeam.UltimateAuth.Core/Defaults/UAuthActions.cs b/src/CodeBeam.UltimateAuth.Core/Defaults/UAuthActions.cs index a65087b4..093ec820 100644 --- a/src/CodeBeam.UltimateAuth.Core/Defaults/UAuthActions.cs +++ b/src/CodeBeam.UltimateAuth.Core/Defaults/UAuthActions.cs @@ -134,5 +134,10 @@ public static class Roles public const string SetPermissionsAdmin = "authorization.roles.permissions.admin"; public const string QueryAdmin = "authorization.roles.query.admin"; } + + public static class Permissions + { + public const string GetSelf = "authorization.permissions.get.self"; + } } } diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IAuthorizationEndpointHandler.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IAuthorizationEndpointHandler.cs index 43c65375..28e987d3 100644 --- a/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IAuthorizationEndpointHandler.cs +++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IAuthorizationEndpointHandler.cs @@ -8,6 +8,7 @@ public interface IAuthorizationEndpointHandler { Task CheckAsync(HttpContext ctx); Task GetMyRolesAsync(HttpContext ctx); + Task GetMyPermissionsAsync(HttpContext ctx); Task GetUserRolesAsync(UserKey userKey, HttpContext ctx); Task AssignRoleAsync(UserKey userKey, HttpContext ctx); Task RemoveRoleAsync(UserKey userKey, HttpContext ctx); diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/UAuthEndpointRegistrar.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/UAuthEndpointRegistrar.cs index c40cc611..a2dfbb16 100644 --- a/src/CodeBeam.UltimateAuth.Server/Endpoints/UAuthEndpointRegistrar.cs +++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/UAuthEndpointRegistrar.cs @@ -385,6 +385,10 @@ public void MapEndpoints(RouteGroupBuilder rootGroup, UAuthServerOptions options selfAuthz.MapPost("/roles/get", async ([FromServices] IAuthorizationEndpointHandler h, HttpContext ctx) => await h.GetMyRolesAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.AuthorizationManagement)); + if (Enabled(UAuthActions.Authorization.Permissions.GetSelf)) + selfAuthz.MapPost("/permissions/get", async ([FromServices] IAuthorizationEndpointHandler h, HttpContext ctx) + => await h.GetMyPermissionsAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.AuthorizationManagement)); + if (Enabled(UAuthActions.Authorization.Roles.GetAdmin)) adminAuthz.MapPost("/users/{userKey}/roles/get", async ([FromServices] IAuthorizationEndpointHandler h, UserKey userKey, HttpContext ctx) diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.Contracts/Responses/UserPermissionsResult.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.Contracts/Responses/UserPermissionsResult.cs new file mode 100644 index 00000000..384e2747 --- /dev/null +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.Contracts/Responses/UserPermissionsResult.cs @@ -0,0 +1,10 @@ +namespace CodeBeam.UltimateAuth.Authorization.Contracts; + +public sealed record UserPermissionsResult +{ + /// + /// Gets the normalized, role-derived permissions assigned to the user. + /// These permissions do not represent final authorization decisions. + /// + public required IReadOnlyList Permissions { get; init; } +} diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Endpoints/AuthorizationEndpointHandler.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Endpoints/AuthorizationEndpointHandler.cs index f590f18b..f20b0202 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Endpoints/AuthorizationEndpointHandler.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Endpoints/AuthorizationEndpointHandler.cs @@ -16,19 +16,22 @@ public sealed class AuthorizationEndpointHandler : IAuthorizationEndpointHandler private readonly IUserRoleService _userRoles; private readonly IRoleService _roles; private readonly IAccessContextFactory _accessContextFactory; + private readonly IUserPermissionService _userPermissions; public AuthorizationEndpointHandler( IAuthFlowContextAccessor authFlow, IAuthorizationService authorization, IUserRoleService userRoles, IRoleService roles, - IAccessContextFactory accessContextFactory) + IAccessContextFactory accessContextFactory, + IUserPermissionService userPermissions) { _authFlow = authFlow; _authorization = authorization; _userRoles = userRoles; _roles = roles; _accessContextFactory = accessContextFactory; + _userPermissions = userPermissions; } @@ -284,4 +287,23 @@ public async Task QueryRolesAsync(HttpContext ctx) return Results.Ok(response); } + + public async Task GetMyPermissionsAsync(HttpContext ctx) + { + var flow = _authFlow.Current; + + if (!flow.IsAuthenticated || flow.UserKey is null) + return Results.Unauthorized(); + + var accessContext = await _accessContextFactory.CreateAsync( + flow, + action: UAuthActions.Authorization.Permissions.GetSelf, + resource: "authorization.permissions", + resourceId: flow.UserKey.Value, + ct: ctx.RequestAborted); + + var result = await _userPermissions.GetMyPermissionsAsync(accessContext, ctx.RequestAborted); + + return Results.Ok(result); + } } diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Extensions/ServiceCollectionExtensions.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Extensions/ServiceCollectionExtensions.cs index 49c5f58a..d5239610 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Extensions/ServiceCollectionExtensions.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Extensions/ServiceCollectionExtensions.cs @@ -11,6 +11,7 @@ public static IServiceCollection AddUltimateAuthAuthorizationReference(this ISer services.TryAddScoped(); services.TryAddScoped(); services.TryAddScoped(); + services.TryAddScoped(); services.TryAddScoped(); services.TryAddScoped(); services.TryAddScoped(); diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Infrastructure/RolePermissionResolver.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Infrastructure/RolePermissionResolver.cs index 9073946d..187e213a 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Infrastructure/RolePermissionResolver.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Infrastructure/RolePermissionResolver.cs @@ -24,6 +24,9 @@ public async Task> ResolveAsync(TenantKey tenant foreach (var role in roles) { + if (role.IsDeleted || role.Tenant != tenant) + continue; + foreach (var perm in role.Permissions) permissions.Add(perm); } diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Services/UserPermissionsService.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Services/UserPermissionsService.cs new file mode 100644 index 00000000..548c60c5 --- /dev/null +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Services/UserPermissionsService.cs @@ -0,0 +1,58 @@ +using CodeBeam.UltimateAuth.Authorization; +using CodeBeam.UltimateAuth.Authorization.Contracts; +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Defaults; +using CodeBeam.UltimateAuth.Core.Errors; +using CodeBeam.UltimateAuth.Server.Infrastructure; + +namespace CodeBeam.UltimateAuth.Authorization.Reference; + +internal sealed class UserPermissionService : IUserPermissionService +{ + private readonly IAccessOrchestrator _accessOrchestrator; + private readonly IUserPermissionStore _permissions; + + public UserPermissionService(IAccessOrchestrator accessOrchestrator, IUserPermissionStore permissions) + { + _accessOrchestrator = accessOrchestrator; + _permissions = permissions; + } + + public async Task GetMyPermissionsAsync( + AccessContext context, + CancellationToken ct = default) + { + ct.ThrowIfCancellationRequested(); + + if (!context.IsAuthenticated || + context.ActorUserKey is null || + context.TargetUserKey != context.ActorUserKey || + context.Action != UAuthActions.Authorization.Permissions.GetSelf) + { + throw new UAuthAuthorizationException( + "invalid_permission_self_context"); + } + + var command = new AccessCommand( + async innerCt => + { + var permissions = await _permissions.GetPermissionsAsync( + context.ResourceTenant, + context.ActorUserKey.Value, + innerCt); + + return new UserPermissionsResult + { + Permissions = permissions + .Distinct() + .OrderBy(x => x.Value, StringComparer.Ordinal) + .ToArray() + }; + }); + + return await _accessOrchestrator.ExecuteAsync( + context, + command, + ct); + } +} diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization/Abstractions/IUserPermissionsService.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization/Abstractions/IUserPermissionsService.cs new file mode 100644 index 00000000..4577d2f5 --- /dev/null +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization/Abstractions/IUserPermissionsService.cs @@ -0,0 +1,9 @@ +using CodeBeam.UltimateAuth.Authorization.Contracts; +using CodeBeam.UltimateAuth.Core.Contracts; + +namespace CodeBeam.UltimateAuth.Authorization; + +public interface IUserPermissionService +{ + Task GetMyPermissionsAsync(AccessContext context, CancellationToken ct = default); +} diff --git a/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IAuthorizationClient.cs b/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IAuthorizationClient.cs index 909dc268..fc839430 100644 --- a/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IAuthorizationClient.cs +++ b/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IAuthorizationClient.cs @@ -60,6 +60,21 @@ public interface IAuthorizationClient /// Task> GetMyRolesAsync(PageRequest? request = null); + /// + /// Retrieves the current user's role-derived permissions. + /// + /// + /// + /// Returns normalized permissions assigned through the user's roles. + /// Wildcard permissions are preserved without expansion. + /// + /// + /// This method does not evaluate authorization policies. + /// Use to evaluate access to a specific action. + /// + /// + Task> GetMyPermissionsAsync(); + /// /// Retrieves roles assigned to a specific user. /// diff --git a/src/client/CodeBeam.UltimateAuth.Client/Services/UAuthAuthorizationClient.cs b/src/client/CodeBeam.UltimateAuth.Client/Services/UAuthAuthorizationClient.cs index 7f5fac85..2281ecdf 100644 --- a/src/client/CodeBeam.UltimateAuth.Client/Services/UAuthAuthorizationClient.cs +++ b/src/client/CodeBeam.UltimateAuth.Client/Services/UAuthAuthorizationClient.cs @@ -36,6 +36,12 @@ public async Task> GetMyRolesAsync(PageRequest? return UAuthResultMapper.FromJson(raw); } + public async Task> GetMyPermissionsAsync() + { + var raw = await _request.SendJsonAsync(Url("/me/authorization/permissions/get"), new { }); + return UAuthResultMapper.FromJson(raw); + } + public async Task> GetUserRolesAsync(UserKey userKey, PageRequest? request = null) { request ??= new PageRequest(); From 43fb2c87f5956001464367cf8c20282ffe2cafb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Can=20Karag=C3=B6z?= Date: Tue, 27 Oct 2026 00:02:35 +0300 Subject: [PATCH 2/3] Add Test and Docs --- .../Pages/Dashboard/MyAccessPage.razor | 134 ++++++++---- docs/content/client-sdk/authorization.md | 112 ++++++++++ .../Infrastructure/PermissionNormalizer.cs | 41 ++-- .../AuthorizationManagementTests.cs | 197 +++++++++++++++++ .../AuthorizationEndpointHandlerTests.cs | 70 +++++- .../UserPermissionServiceTests.cs | 207 ++++++++++++++++++ .../Client/UAuthClientAuthorizationTests.cs | 57 +++++ 7 files changed, 761 insertions(+), 57 deletions(-) create mode 100644 tests/CodeBeam.UltimateAuth.Tests.Unit/Authorization/UserPermissionServiceTests.cs diff --git a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/Dashboard/MyAccessPage.razor b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/Dashboard/MyAccessPage.razor index a89afc1a..8ec16a09 100644 --- a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/Dashboard/MyAccessPage.razor +++ b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/Dashboard/MyAccessPage.razor @@ -18,10 +18,7 @@
PERSONAL AUTHORIZATION

My Access

-

- Explore your roles, effective permissions, - and how UltimateAuth evaluates access. -

+

Explore your assigned roles and permissions, then evaluate how UltimateAuth makes authorization decisions.

Roles assigned to your identity - @*
+
- Effective Permissions - 4 - Illustrative permission set -
*@ + + Assigned Permissions + @(_permissionsLoading ? "..." : _permissions.Count.ToString()) + Permissions inherited from your roles +
Available Actions @@ -133,49 +131,71 @@ - @*
+
-

Effective Permissions

-

Example permissions available to your identity.

+

Assigned Permissions

+

Permissions inherited from your assigned roles.

- + +
-
- @foreach (var permission in _permissions) - { -
- - - + @if (_permissionsLoading) + { + + } + else if (_permissionsError is not null) + { + + @_permissionsError + + } + else if (_permissions.Count == 0) + { +
+ + + -
- @permission.Name - @permission.Description + No permissions assigned +

Your assigned roles do not currently grant any permissions.

+
+ } + else + { +
+ @foreach (var permission in _permissions) + { +
+ + + + +
+ @permission.Value +
+ + + Assigned +
+ } +
+ } - - Allowed - -
- } -
-
*@ + + Assigned permissions do not guarantee access. + Policies and request context may still deny an operation. + Use the Authorization Playground below to evaluate access. + +
INTERACTIVE AUTHORIZATION

Authorization Playground

-

- Choose a resource and operation to explore - how access decisions work. -

+

Choose a resource and operation to explore how access decisions work.

@@ -349,6 +369,10 @@ @code { private readonly List _roles = []; + private readonly List _permissions = []; + + private bool _permissionsLoading; + private string? _permissionsError; private bool _loading; private string? _error; @@ -368,7 +392,7 @@ protected override async Task OnInitializedAsync() { - await LoadRolesAsync(); + await Task.WhenAll(LoadRolesAsync(), LoadPermissionsAsync()); } private async Task LoadRolesAsync() @@ -406,6 +430,36 @@ } } + private async Task LoadPermissionsAsync() + { + _permissionsLoading = true; + _permissionsError = null; + + try + { + var result = await UAuthClient.Authorization.GetMyPermissionsAsync(); + + if (!result.IsSuccess || result.Value is null) + { + _permissionsError = + result.ErrorText ?? "Unable to load your permissions."; + + return; + } + + _permissions.Clear(); + _permissions.AddRange(result.Value.Permissions); + } + catch (Exception) + { + _permissionsError = "Unable to load your permissions."; + } + finally + { + _permissionsLoading = false; + } + } + private async Task OnRolePageChangedAsync(int page) { _rolePage = page; diff --git a/docs/content/client-sdk/authorization.md b/docs/content/client-sdk/authorization.md index 53fe8852..1304bd4f 100644 --- a/docs/content/client-sdk/authorization.md +++ b/docs/content/client-sdk/authorization.md @@ -473,6 +473,118 @@ The returned role collection is paginated. Do not assume that the first page contains all assigned roles. +## 8. Get My Permissions + +Use `GetMyPermissionsAsync()` to retrieve the permissions assigned to the currently authenticated user through their roles. + +Unlike `GetMyRolesAsync()`, which returns role assignments, this method returns the resulting permission collection. + +```csharp +var result = await UAuthClient.Authorization.GetMyPermissionsAsync(); + +if (result.IsSuccess && result.Value is { } data) +{ + foreach (var permission in data.Permissions) + { + Console.WriteLine(permission.Value); + } +} +``` + +The method is self-service and does not require administrative permission. + +The user identity and tenant context are derived from the authenticated session. No user identifier or tenant identifier is accepted as a request parameter. + +### UserPermissionsResult + +The response contains the user's role-derived permissions: + +```csharp +public sealed record UserPermissionsResult +{ + public required IReadOnlyList Permissions { get; init; } +} +``` + +The returned collection: + +- Combines permissions from the user's assigned roles. +- Removes duplicate permissions. +- Uses deterministic ordering. +- Preserves wildcard permissions such as `users.*` and `*`. +- Returns an empty collection when no permissions are assigned. +- Reflects the current permissions resolved from the configured authorization stores. + +For example, a user assigned multiple roles may receive: + +```text +reports.* +users.read.admin +users.update.admin +``` + +The result does not identify which role contributed each permission. + +### Assigned Permissions vs. Authorization Decisions + +**Assigned permissions are not final authorization decisions.** + +A user may possess a permission but still be denied access because of an applicable policy, tenant boundary, target restriction, or other security requirement. + +For example: + +```csharp +var permissions = await UAuthClient.Authorization.GetMyPermissionsAsync(); +``` + +This retrieves the user's assigned permissions. + +To determine whether a specific operation is currently authorized, use `CheckAsync()`: + +```csharp +var result = await UAuthClient.Authorization.CheckAsync( + new AuthorizationCheckRequest + { + Action = "users.delete.admin", + Resource = "users", + ResourceId = targetUserKey.Value + }); + +if (result.IsSuccess && result.Value is { } decision) +{ + if (decision.IsAllowed) + { + // The authorization check allowed this operation. + } +} +``` + +Even a successful client-side authorization check does not replace authorization enforcement at the server-side operation boundary. + +### Wildcard Permissions + +Wildcard permissions are returned without expansion. + +For example: + +```text +users.* +``` + +remains `users.*` rather than being expanded into individual user-management actions. + +This preserves the stored permission semantics and avoids depending on a potentially incomplete action catalog. + +### Security Considerations + +`GetMyPermissionsAsync()` is intended for permission inspection, user interfaces, and administrative transparency. + +It must not be used as the sole mechanism for protecting sensitive operations. + +The server remains responsible for evaluating authorization policies and enforcing access at the actual execution boundary. + +See [Check Authorization](/docs/client-sdk/authorization#4-check-authorization) for policy-aware authorization checks. + ## 8. Get Another User's Roles Administrative interfaces can retrieve the roles assigned to a specific user. diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.Contracts/Infrastructure/PermissionNormalizer.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.Contracts/Infrastructure/PermissionNormalizer.cs index a9f64276..5dea607b 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.Contracts/Infrastructure/PermissionNormalizer.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.Contracts/Infrastructure/PermissionNormalizer.cs @@ -4,40 +4,49 @@ public static class PermissionNormalizer { public static IReadOnlyCollection Normalize(IEnumerable permissions, IEnumerable catalog) { - var selected = new HashSet(permissions.Select(p => p.Value), StringComparer.OrdinalIgnoreCase); + var selected = new HashSet( + permissions.Select(p => p.Value), + StringComparer.OrdinalIgnoreCase); if (selected.Contains("*")) - return new[] { Permission.Wildcard }; + return [Permission.Wildcard]; - if (selected.Count == catalog.Count() && catalog.All(selected.Contains)) - { - return new[] { Permission.Wildcard }; - } + var catalogItems = catalog.ToHashSet( + StringComparer.OrdinalIgnoreCase); + + // Only collapse to "*" when there are no additional + // permissions outside the known catalog. + if (selected.SetEquals(catalogItems)) + return [Permission.Wildcard]; - var result = new HashSet(); + // Preserve all permissions, including custom ones. + var result = new HashSet( + selected, + StringComparer.OrdinalIgnoreCase); - var catalogGroups = catalog.GroupBy(p => p.Split('.')[0]); + var catalogGroups = catalogItems.GroupBy( + p => p.Split('.')[0]); foreach (var group in catalogGroups) { var prefix = group.Key; - var allPermissions = group.ToHashSet(StringComparer.OrdinalIgnoreCase); + var groupPermissions = group.ToHashSet( + StringComparer.OrdinalIgnoreCase); var selectedInGroup = selected - .Where(p => p.StartsWith(prefix + ".", StringComparison.OrdinalIgnoreCase)) + .Where(p => groupPermissions.Contains(p)) .ToHashSet(StringComparer.OrdinalIgnoreCase); - if (selectedInGroup.SetEquals(allPermissions)) + if (selectedInGroup.SetEquals(groupPermissions)) { + result.ExceptWith(groupPermissions); result.Add(prefix + ".*"); } - else - { - result.UnionWith(selectedInGroup); - } } - return result.Select(Permission.From).ToArray(); + return result + .Select(Permission.From) + .ToArray(); } } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/AuthorizationManagementTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/AuthorizationManagementTests.cs index 55b2be56..9e1a9ff9 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration/AuthorizationManagementTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/AuthorizationManagementTests.cs @@ -753,6 +753,203 @@ public async Task RoleStore_ShouldIsolateRolesBetweenTenants() fromB.Should().BeNull(); } + [Fact] + public async Task GetMyPermissions_WhenRolePermissionsChange_ReturnsUpdatedPermissions() + { + using var factory = NewFactory(); + + var user = await factory.CreateLoginUserAsync(); + + var oldPermission = Permission.From("reports.read"); + var newPermission = Permission.From("reports.export"); + + var role = await SeedRoleAsync( + factory, + [oldPermission]); + + await SeedAssignmentAsync( + factory, + user.UserKey, + role.Id); + + using var client = await AuthenticatedClientAsync(factory, user); + + // First request: the user has the original permission. + using (var response = await client.PostAsJsonAsync( + $"{SelfBase}/permissions/get", + new { })) + { + response.StatusCode.Should().Be(HttpStatusCode.OK); + + var result = await response.Content + .ReadFromJsonAsync(); + + result.Should().NotBeNull(); + + result!.Permissions.Should().Contain(oldPermission); + result.Permissions.Should().NotContain(newPermission); + } + + // Update the persisted role permissions. + using (var scope = factory.Services.CreateScope()) + { + var roleStoreFactory = scope.ServiceProvider + .GetRequiredService(); + + var store = roleStoreFactory.Create(TenantKeys.Single); + + var stored = await store.GetAsync(new RoleKey(TenantKeys.Single, role.Id)); + + stored.Should().NotBeNull(); + + var expectedVersion = stored!.Version; + + stored.SetPermissions( + [newPermission], + factory.Clock.UtcNow); + + // Check the entity immediately after mutation. + stored.Permissions.Should().Contain(newPermission); + stored.Permissions.Should().NotContain(oldPermission); + + await store.SaveAsync(stored, expectedVersion); + + // Check persistence using a fresh read. + var saved = await store.GetAsync( + new RoleKey(TenantKeys.Single, role.Id)); + + saved.Should().NotBeNull(); + saved!.Permissions.Should().Contain(newPermission); + saved.Permissions.Should().NotContain(oldPermission); + } + + using (var scope = factory.Services.CreateScope()) + { + var roleStoreFactory = scope.ServiceProvider + .GetRequiredService(); + + var userRoleStoreFactory = scope.ServiceProvider + .GetRequiredService(); + + var permissionStore = scope.ServiceProvider + .GetRequiredService(); + + var roleStore = roleStoreFactory.Create(TenantKeys.Single); + + var userRoleStore = userRoleStoreFactory.Create(TenantKeys.Single); + + // 1. Was the role updated? + var updatedRole = await roleStore.GetAsync( + new RoleKey(TenantKeys.Single, role.Id)); + + updatedRole.Should().NotBeNull(); + + updatedRole!.Permissions.Should().Contain(newPermission); + updatedRole.Permissions.Should().NotContain(oldPermission); + + // 2. Does the user still have the role? + var assignments = await userRoleStore.GetAssignmentsAsync( + user.UserKey); + + assignments.Should().Contain(x => x.RoleId == role.Id); + + // 3. Can the permission store resolve the updated permission? + var permissions = await permissionStore.GetPermissionsAsync( + TenantKeys.Single, + user.UserKey); + + permissions.Should().Contain(newPermission); + permissions.Should().NotContain(oldPermission); + } + + // Second request: the new permission must be visible. + using (var response = await client.PostAsJsonAsync( + $"{SelfBase}/permissions/get", + new { })) + { + response.StatusCode.Should().Be(HttpStatusCode.OK); + + var result = await response.Content + .ReadFromJsonAsync(); + + result.Should().NotBeNull(); + + result!.Permissions.Should().Contain(newPermission); + result.Permissions.Should().NotContain(oldPermission); + } + } + + [Fact] + public async Task GetMyPermissions_Anonymous_ShouldReturnUnauthorized() + { + using var factory = NewFactory(); + + using var client = CreateClient(factory); + + using var response = await client.PostAsJsonAsync( + $"{SelfBase}/permissions/get", + new { }); + + response.StatusCode.Should().Be(HttpStatusCode.Unauthorized); + } + + [Fact] + public async Task GetMyPermissions_WhenNoRoles_ReturnsEmpty() + { + using var factory = NewFactory(); + + var user = await factory.CreateLoginUserAsync(); + + using var client = await AuthenticatedClientAsync(factory, user); + + using var response = await client.PostAsJsonAsync( + $"{SelfBase}/permissions/get", + new { }); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + + var result = await response.Content + .ReadFromJsonAsync(); + + result.Should().NotBeNull(); + result!.Permissions.Should().BeEmpty(); + } + + [Fact] + public async Task GetMyPermissions_DoesNotReturnOtherUsersPermissions() + { + using var factory = NewFactory(); + + var user = await factory.CreateLoginUserAsync(); + var other = await factory.CreateLoginUserAsync(); + + var permission = Permission.From("reports.export"); + + var role = await SeedRoleAsync( + factory, + [permission]); + + await SeedAssignmentAsync( + factory, + other.UserKey, + role.Id); + + using var client = await AuthenticatedClientAsync(factory, user); + + using var response = await client.PostAsJsonAsync( + $"{SelfBase}/permissions/get", + new { }); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + + var result = await response.Content + .ReadFromJsonAsync(); + + result.Should().NotBeNull(); + + result!.Permissions.Should().NotContain(permission); + } + // -------------------------------------------------- // Test infrastructure // -------------------------------------------------- diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Authorization/AuthorizationEndpointHandlerTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Authorization/AuthorizationEndpointHandlerTests.cs index 222e26e5..91bfeb02 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Authorization/AuthorizationEndpointHandlerTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Authorization/AuthorizationEndpointHandlerTests.cs @@ -730,6 +730,70 @@ public void Permission_JsonRoundTrip_ShouldPreserveValue() restored.IsValid.Should().BeTrue(); } + [Fact] + public async Task GetMyPermissionsAsync_WhenUnauthenticated_ReturnsUnauthorized() + { + var f = new Fixture(isAuthenticated: false); + + var result = await f.Sut.GetMyPermissionsAsync(f.Http()); + + AssertStatusCode( + result, + StatusCodes.Status401Unauthorized); + + f.AccessContextFactory.VerifyNoOtherCalls(); + f.UserPermissions.VerifyNoOtherCalls(); + } + + [Fact] + public async Task GetMyPermissionsAsync_WhenAuthenticated_UsesCurrentUser() + { + var f = new Fixture(); + var ctx = f.Http(); + + var accessContext = f.AccessContext( + UAuthActions.Authorization.Permissions.GetSelf); + + f.SetupAccessContext( + UAuthActions.Authorization.Permissions.GetSelf, + "authorization.permissions", + f.UserKey.Value, + accessContext); + + var expected = new UserPermissionsResult + { + Permissions = + [ + Permission.From("users.read"), + Permission.From("reports.*") + ] + }; + + f.UserPermissions + .Setup(x => x.GetMyPermissionsAsync( + accessContext, + ctx.RequestAborted)) + .ReturnsAsync(expected); + + var result = await f.Sut.GetMyPermissionsAsync(ctx); + + var ok = result.Should() + .BeOfType>() + .Subject; + + ok.Value.Should().NotBeNull(); + + ok.Value!.Permissions.Select(x => x.Value) + .Should().BeEquivalentTo( + "users.read", + "reports.*"); + + f.UserPermissions.Verify(x => x.GetMyPermissionsAsync( + accessContext, + ctx.RequestAborted), + Times.Once); + } + // ========================================================= // Fixture // ========================================================= @@ -753,6 +817,9 @@ private sealed class Fixture public Mock AccessContextFactory { get; } = new(MockBehavior.Strict); + public Mock UserPermissions { get; } + = new(MockBehavior.Strict); + public AuthorizationEndpointHandler Sut { get; } public Fixture(bool isAuthenticated = true) @@ -770,7 +837,8 @@ public Fixture(bool isAuthenticated = true) Authorization.Object, UserRoles.Object, Roles.Object, - AccessContextFactory.Object); + AccessContextFactory.Object, + UserPermissions.Object); } public DefaultHttpContext Http() diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Authorization/UserPermissionServiceTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Authorization/UserPermissionServiceTests.cs new file mode 100644 index 00000000..39fd8b08 --- /dev/null +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Authorization/UserPermissionServiceTests.cs @@ -0,0 +1,207 @@ +using AwesomeAssertions; +using CodeBeam.UltimateAuth.Authorization; +using CodeBeam.UltimateAuth.Authorization.Contracts; +using CodeBeam.UltimateAuth.Authorization.Reference; +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Defaults; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.Errors; +using CodeBeam.UltimateAuth.Core.MultiTenancy; +using CodeBeam.UltimateAuth.Server.Infrastructure; +using CodeBeam.UltimateAuth.Tests.Unit.Helpers; +using Moq; + +namespace CodeBeam.UltimateAuth.Tests.Unit; + +public sealed class UserPermissionServiceTests +{ + private const string Action = + UAuthActions.Authorization.Permissions.GetSelf; + + [Fact] + public async Task GetMyPermissionsAsync_ReturnsDistinctSortedPermissions() + { + var f = new Fixture(); + + f.Permissions + .Setup(x => x.GetPermissionsAsync( + TenantKey.Single, + f.UserKey, + It.IsAny())) + .ReturnsAsync( + [ + Permission.From("users.read"), + Permission.From("reports.*"), + Permission.From("users.read"), + Permission.From("*"), + Permission.From("orders.update") + ]); + + var result = await f.Sut.GetMyPermissionsAsync(f.Context); + + result.Permissions.Select(x => x.Value) + .Should().Equal( + "*", + "orders.update", + "reports.*", + "users.read"); + + f.Orchestrator.Verify(x => x.ExecuteAsync( + f.Context, + It.IsAny>(), + It.IsAny()), + Times.Once); + } + + [Fact] + public async Task GetMyPermissionsAsync_WhenNoPermissions_ReturnsEmpty() + { + var f = new Fixture(); + + f.Permissions + .Setup(x => x.GetPermissionsAsync( + TenantKey.Single, + f.UserKey, + It.IsAny())) + .ReturnsAsync(Array.Empty()); + + var result = await f.Sut.GetMyPermissionsAsync(f.Context); + + result.Permissions.Should().BeEmpty(); + } + + [Fact] + public async Task GetMyPermissionsAsync_UsesResourceTenantAndActor() + { + var f = new Fixture(); + + f.Permissions + .Setup(x => x.GetPermissionsAsync( + f.Context.ResourceTenant, + f.UserKey, + It.IsAny())) + .ReturnsAsync(Array.Empty()); + + await f.Sut.GetMyPermissionsAsync(f.Context); + + f.Permissions.Verify(x => x.GetPermissionsAsync( + f.Context.ResourceTenant, + f.UserKey, + It.IsAny()), + Times.Once); + } + + [Fact] + public async Task GetMyPermissionsAsync_WhenTargetDiffers_Throws() + { + var f = new Fixture(); + + var context = TestAccessContext.ForTargetUser( + f.UserKey, + UserKey.New(), + Action, + resource: "authorization.permissions"); + + var act = () => f.Sut.GetMyPermissionsAsync(context); + + await act.Should() + .ThrowAsync(); + + f.Permissions.VerifyNoOtherCalls(); + f.Orchestrator.VerifyNoOtherCalls(); + } + + [Fact] + public async Task GetMyPermissionsAsync_WhenActionDiffers_Throws() + { + var f = new Fixture(); + + var context = TestAccessContext.ForUser( + f.UserKey, + UAuthActions.Authorization.Roles.GetAdmin, + resource: "authorization.permissions"); + + var act = () => f.Sut.GetMyPermissionsAsync(context); + + await act.Should() + .ThrowAsync(); + + f.Permissions.VerifyNoOtherCalls(); + f.Orchestrator.VerifyNoOtherCalls(); + } + + [Fact] + public async Task GetMyPermissionsAsync_WhenUnauthenticated_Throws() + { + var f = new Fixture(); + + var context = TestAccessContext.WithAction(Action); + + var act = () => f.Sut.GetMyPermissionsAsync(context); + + await act.Should() + .ThrowAsync(); + + f.Permissions.VerifyNoOtherCalls(); + f.Orchestrator.VerifyNoOtherCalls(); + } + + [Fact] + public async Task GetMyPermissionsAsync_WhenAuthorizationDenied_DoesNotReadPermissions() + { + var f = new Fixture(allowExecution: false); + + var act = () => f.Sut.GetMyPermissionsAsync(f.Context); + + await act.Should() + .ThrowAsync(); + + f.Permissions.VerifyNoOtherCalls(); + } + + private sealed class Fixture + { + public UserKey UserKey { get; } = UserKey.New(); + + public Mock Permissions { get; } + = new(MockBehavior.Strict); + + public Mock Orchestrator { get; } + = new(MockBehavior.Strict); + + public AccessContext Context { get; } + + public UserPermissionService Sut { get; } + + public Fixture(bool allowExecution = true) + { + Context = TestAccessContext.ForUser( + UserKey, + Action, + resource: "authorization.permissions"); + + Orchestrator + .Setup(x => x.ExecuteAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .Returns, + CancellationToken>( + async (_, command, ct) => + { + if (!allowExecution) + { + throw new UAuthAuthorizationException( + "access_denied"); + } + + return await command.ExecuteAsync(ct); + }); + + Sut = new UserPermissionService( + Orchestrator.Object, + Permissions.Object); + } + } +} diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthClientAuthorizationTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthClientAuthorizationTests.cs index 6511d7f0..5efa066d 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthClientAuthorizationTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthClientAuthorizationTests.cs @@ -318,4 +318,61 @@ public async Task Check_Should_Return_Structured_Denial(bool requiresReauthentic r.ResourceId == request.ResourceId)), Times.Once); } + + [Fact] + public async Task GetMyPermissions_Should_Call_Self_Endpoint_And_Return_Data() + { + var response = new UserPermissionsResult + { + Permissions = + [ + Permission.From("users.read"), + Permission.From("reports.*") + ] + }; + + Request.Setup(x => x.SendJsonAsync( + "/auth/me/authorization/permissions/get", + It.IsAny())) + .ReturnsAsync(SuccessJson(response)); + + var client = CreateClient(); + + var result = await client.Authorization.GetMyPermissionsAsync(); + + result.IsSuccess.Should().BeTrue(); + result.Value.Should().NotBeNull(); + + result.Value!.Permissions.Select(x => x.Value) + .Should().BeEquivalentTo( + "users.read", + "reports.*"); + + Request.Verify(x => x.SendJsonAsync( + "/auth/me/authorization/permissions/get", + It.IsAny()), + Times.Once); + + Events.Verify(x => x.PublishAsync( + It.IsAny()), + Times.Never); + } + + [Fact] + public async Task GetMyPermissions_WhenRequestFails_ReturnsFailure() + { + Request.Setup(x => x.SendJsonAsync("/auth/me/authorization/permissions/get", It.IsAny())) + .ReturnsAsync(new UAuthTransportResult + { + Ok = false, + Status = 401 + }); + + var client = CreateClient(); + + var result = await client.Authorization.GetMyPermissionsAsync(); + + result.IsSuccess.Should().BeFalse(); + result.Value.Should().BeNull(); + } } From 30e8ae32c9467d478c8ad397f05c7f95aff0fbeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Can=20Karag=C3=B6z?= Date: Tue, 27 Oct 2026 00:15:41 +0300 Subject: [PATCH 3/3] Docs Fix --- docs/content/client-sdk/authorization.md | 34 +++++----- .../docs/client-sdk/authorization.json | 65 +++++++++++++------ 2 files changed, 62 insertions(+), 37 deletions(-) diff --git a/docs/content/client-sdk/authorization.md b/docs/content/client-sdk/authorization.md index 1304bd4f..05be2f25 100644 --- a/docs/content/client-sdk/authorization.md +++ b/docs/content/client-sdk/authorization.md @@ -585,7 +585,7 @@ The server remains responsible for evaluating authorization policies and enforci See [Check Authorization](/docs/client-sdk/authorization#4-check-authorization) for policy-aware authorization checks. -## 8. Get Another User's Roles +## 9. Get Another User's Roles Administrative interfaces can retrieve the roles assigned to a specific user. @@ -612,7 +612,7 @@ The `UserKey` identifies the target user, not the current actor. The server determines whether the current actor may inspect that user's role assignments. -## 9. Query Roles +## 10. Query Roles Use `QueryRolesAsync` to retrieve roles with pagination and optional filtering. @@ -678,7 +678,7 @@ Use `IncludeDeleted` only where the application needs administrative access to d The server remains responsible for enforcing the query's permissions and lifecycle rules. -## 10. Understanding RoleInfo +## 11. Understanding RoleInfo Each role returned by the SDK is represented by `RoleInfo`. @@ -726,7 +726,7 @@ Use `RoleId` for role management requests. Do not confuse it with `UserKey` or other identifier types. -## 11. Create a Role +## 12. Create a Role Use `CreateRoleAsync` to create a role. @@ -791,7 +791,7 @@ var result = await UAuthClient.Authorization.CreateRoleAsync( Use role names that describe the responsibility or capability represented by the role. -## 12. Rename a Role +## 13. Rename a Role Use `RenameRoleAsync` to change a role's name. @@ -820,7 +820,7 @@ Role renaming does not mean that permissions are automatically changed. After a successful rename, the SDK publishes an `AuthorizationChanged` state event. -## 13. Set Role Permissions +## 14. Set Role Permissions Use `SetRolePermissionsAsync` to configure a role's permissions. @@ -944,7 +944,7 @@ An empty collection requests that the role have no permissions. The server determines whether the operation is permitted. -## 14. Assign a Role to a User +## 15. Assign a Role to a User Use `AssignRoleToUserAsync` to assign an existing role to a user. @@ -997,7 +997,7 @@ This example does not provide transactional, all-or-nothing behavior across the For complex provisioning workflows, consider whether the application needs a server-side orchestration operation. -## 15. Remove a Role from a User +## 16. Remove a Role from a User Use `RemoveRoleFromUserAsync`: @@ -1041,7 +1041,7 @@ Removing `Manager` does not necessarily remove the user's ability to read produc To determine whether the user may perform an action, rely on the server's authorization decision rather than inspecting one role assignment in isolation. -## 16. Delete a Role +## 17. Delete a Role Use `DeleteRoleAsync` to delete a role. @@ -1108,7 +1108,7 @@ This can affect multiple users at once. For that reason, role deletion should be presented as a sensitive administrative operation with appropriate confirmation. -## 17. Build a Role Management Page +## 18. Build a Role Management Page The following Blazor example demonstrates querying roles, creating a role, and deleting a selected role. @@ -1256,7 +1256,7 @@ This example demonstrates the SDK integration rather than a complete production A production implementation should include pagination, role assignment controls, permission editing, deletion confirmation, and appropriate loading and error handling for every operation. -## 18. Build a User Role Assignment Interface +## 19. Build a User Role Assignment Interface An administrative user details page can display assigned roles and allow administrators to add or remove them. @@ -1312,7 +1312,7 @@ This pattern works well alongside `UAuthClient.Users` for building a unified use See [User Management](/docs/client/user-management). -## 19. Authorization State Events +## 20. Authorization State Events The current `UAuthAuthorizationClient` publishes: @@ -1345,7 +1345,7 @@ For example, an administrator assigning a role to another user does not automati Applications requiring cross-client updates should provide an appropriate synchronization mechanism. -## 20. Authorization Changes and Active Sessions +## 21. Authorization Changes and Active Sessions Role and permission changes may affect active users. @@ -1375,7 +1375,7 @@ Applications may combine these operations when security policy requires immediat See [Session Management](/docs/client/session-management). -## 21. Multi-Tenant Authorization +## 22. Multi-Tenant Authorization UltimateAuth authorization operations are scoped to the current tenant context. @@ -1407,7 +1407,7 @@ The server must enforce tenant isolation and administrative authorization. When implementing multi-tenant administration, do not assume role names or permission configurations are globally shared. -## 22. Handling Authorization Errors +## 23. Handling Authorization Errors Authorization methods return `UAuthResult` or `UAuthResult`. @@ -1461,7 +1461,7 @@ Use the server's structured error information for diagnostics where appropriate. Do not expose sensitive policy internals unnecessarily. -## 23. Security Considerations +## 24. Security Considerations ### Server Authorization Is Authoritative @@ -1509,7 +1509,7 @@ Permission changes may not propagate instantly to every active client or cached Design sensitive operations around server-side enforcement and appropriate cache invalidation. -## 24. Putting Everything Together +## 25. Putting Everything Together A typical administrative authorization workflow may look like this: diff --git a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/client-sdk/authorization.json b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/client-sdk/authorization.json index 8df073ca..427e1a0d 100644 --- a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/client-sdk/authorization.json +++ b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/client-sdk/authorization.json @@ -1,7 +1,7 @@ { "Slug": "client-sdk/authorization", "Title": "Authorization", - "Html": "\n\u003Cp\u003EUltimateAuth provides a unified client API for managing roles, assigning permissions, controlling user-role relationships, and requesting authorization decisions from the server.\u003C/p\u003E\n\u003Cp\u003EThrough \u003Ccode\u003EUAuthClient.Authorization\u003C/code\u003E, applications can build administrative authorization interfaces and check whether the current user is allowed to perform a particular action.\u003C/p\u003E\n\u003Cp\u003EAuthorization is enforced on the server. Client-side permission checks are useful for improving the user experience, but they do not replace authorization at the endpoint or application-service level.\u003C/p\u003E\n\u003Cp\u003EThis guide covers the current \u003Ccode\u003EIAuthorizationClient\u003C/code\u003E API, practical role management workflows, permission patterns, and secure client integration.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022understanding-authorization\u0022\u003E1. Understanding Authorization\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth authorization combines several concepts:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EConcept\u003C/th\u003E\n\u003Cth\u003EPurpose\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ERoles\u003C/td\u003E\n\u003Ctd\u003ENamed collections of permissions\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EPermissions\u003C/td\u003E\n\u003Ctd\u003EFine-grained capabilities represented by normalized values\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EUser-role assignments\u003C/td\u003E\n\u003Ctd\u003EAssociations between users and roles\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EPolicies\u003C/td\u003E\n\u003Ctd\u003EServer-side rules that participate in access decisions\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EAuthorization context\u003C/td\u003E\n\u003Ctd\u003EInformation about the actor, resource, action, and current security state\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EA typical authorization relationship looks like this:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003EUser\n |\n \u002B-- Manager\n | |\n | \u002B-- products.read\n | \u002B-- products.create\n | \u002B-- products.update\n |\n \u002B-- Auditor\n |\n \u002B-- reports.read\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EA user can have multiple roles, and each role can contain multiple permissions.\u003C/p\u003E\n\u003Cp\u003EHowever, \u003Cstrong\u003Ehaving a permission does not automatically guarantee access\u003C/strong\u003E. The server\u0027s authorization pipeline may apply additional requirements, such as resource ownership, session validity, tenant isolation, or application policies.\u003C/p\u003E\n\u003Cp\u003EFor the internal decision model, see \u003Ca href=\u0022/docs/security/authorization-pipeline\u0022\u003EAuthorization Pipeline Internals\u003C/a\u003E.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022accessing-the-authorization-client\u0022\u003E2. Accessing the Authorization Client\u003C/h2\u003E\n\u003Cp\u003EInject \u003Ccode\u003EIUAuthClient\u003C/code\u003E into a Blazor component:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@using CodeBeam.UltimateAuth.Client\n@inject IUAuthClient UAuthClient\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EAuthorization operations are available through:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EUAuthClient.Authorization\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis property exposes \u003Ccode\u003EIAuthorizationClient\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EThe interface provides three main groups of operations:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Cstrong\u003EAuthorization checks:\u003C/strong\u003E Evaluate whether the current user may perform an action.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003ERole management:\u003C/strong\u003E Create, query, rename, configure, and delete roles.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003EUser-role management:\u003C/strong\u003E Retrieve, assign, and remove user roles.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EThe SDK sends requests to the corresponding UltimateAuth endpoints and returns structured \u003Ccode\u003EUAuthResult\u003C/code\u003E values.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022understanding-permissions\u0022\u003E3. Understanding Permissions\u003C/h2\u003E\n\u003Cp\u003EA permission is represented by the \u003Ccode\u003EPermission\u003C/code\u003E value type.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar permission = Permission.From(\u0026quot;products.create\u0026quot;);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe current implementation normalizes permission values by trimming surrounding whitespace and converting the value to lowercase.\u003C/p\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar permission = Permission.From(\u0026quot; PRODUCTS.CREATE \u0026quot;);\n\nConsole.WriteLine(permission.Value);\n// products.create\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe constructor rejects null, empty, and whitespace-only values.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022permission-patterns\u0022\u003EPermission Patterns\u003C/h3\u003E\n\u003Cp\u003EUltimateAuth recognizes three important forms:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EPattern\u003C/th\u003E\n\u003Cth\u003EExample\u003C/th\u003E\n\u003Cth\u003EMeaning\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EExact\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Eproducts.create\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EA specific permission\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EPrefix wildcard\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Eproducts.*\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPermissions within the corresponding prefix\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EGlobal wildcard\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E*\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EUnrestricted permission matching within the applicable authorization rules\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003ECreate them using:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar exact = Permission.From(\u0026quot;products.create\u0026quot;);\n\nvar prefix = Permission.From(\u0026quot;products.*\u0026quot;);\n\nvar global = Permission.Wildcard;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe type also exposes:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Epermission.IsWildcard;\npermission.IsPrefix;\npermission.IsValid;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022permission-normalization\u0022\u003EPermission Normalization\u003C/h3\u003E\n\u003Cp\u003EThe following values normalize to the same representation:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EPermission.From(\u0026quot;Products.Create\u0026quot;);\nPermission.From(\u0026quot;products.create\u0026quot;);\nPermission.From(\u0026quot; PRODUCTS.CREATE \u0026quot;);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EAll produce:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003Eproducts.create\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EUse consistent permission names across application services, policies, and role configuration.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022wildcards-are-powerful\u0022\u003EWildcards Are Powerful\u003C/h3\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EPermission.From(\u0026quot;users.*\u0026quot;)\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003Emay grant broad access to operations within the \u003Ccode\u003Eusers\u003C/code\u003E permission prefix.\u003C/p\u003E\n\u003Cp\u003EThe global wildcard:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EPermission.Wildcard\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003Eis especially sensitive.\u003C/p\u003E\n\u003Cp\u003EAvoid assigning broad wildcard permissions merely to simplify development. Use least-privilege permissions for ordinary application roles.\u003C/p\u003E\n\u003Cp\u003EA wildcard is not a substitute for satisfying independent security policies, authentication requirements, or tenant restrictions.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022check-authorization\u0022\u003E4. Check Authorization\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003ECheckAsync\u003C/code\u003E evaluates whether the current authenticated user is authorized to perform a specific action against a resource.\u003C/p\u003E\n\u003Cp\u003EIt provides an \u003Cstrong\u003Eon-demand, server-side authorization decision\u003C/strong\u003E based on the current authentication context, permissions, roles, and applicable authorization policies.\u003C/p\u003E\n\u003Cp\u003EUnlike operations that perform protected actions, \u003Ccode\u003ECheckAsync\u003C/code\u003E only evaluates access. It does not execute the requested operation, modify permissions, grant access, or reserve authorization for a future request.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022when-to-use-checkasync\u0022\u003EWhen to Use CheckAsync\u003C/h3\u003E\n\u003Cp\u003EUse \u003Ccode\u003ECheckAsync\u003C/code\u003E when your application needs to:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EDetermine whether an authenticated user can perform an action before presenting it in the UI.\u003C/li\u003E\n\u003Cli\u003EEnable, disable, or hide controls based on the current authorization decision.\u003C/li\u003E\n\u003Cli\u003EExplain why an operation is unavailable, using the returned denial reason where appropriate.\u003C/li\u003E\n\u003Cli\u003EDetermine whether an operation requires reauthentication.\u003C/li\u003E\n\u003Cli\u003EEvaluate resource-specific access when the applicable policies support it.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EFor example, an application may check whether the current user can delete a particular order before displaying the Delete button.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022what-checkasync-does-not-guarantee\u0022\u003EWhat CheckAsync Does Not Guarantee\u003C/h3\u003E\n\u003Cp\u003E\u003Cstrong\u003EA successful authorization check is not a guarantee that a subsequent operation will succeed.\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EAuthorization is evaluated against the security context available at the time of the check. That context may change before the actual operation is performed.\u003C/p\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EAn administrator may revoke the user\u0027s permissions.\u003C/li\u003E\n\u003Cli\u003EThe user\u0027s session may expire or become invalid.\u003C/li\u003E\n\u003Cli\u003EThe target resource may be deleted or modified.\u003C/li\u003E\n\u003Cli\u003EThe resource\u0027s ownership or tenant context may change.\u003C/li\u003E\n\u003Cli\u003EAdditional business rules may prevent the operation.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EThe protected operation must independently enforce authorization using its current context.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003ENever use a previous \u003Ccode\u003ECheckAsync\u003C/code\u003E result as proof of authorization for a later request.\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003ECheckAsync\u003C/code\u003E is primarily a developer-experience and user-experience API. It helps applications understand and present authorization decisions without replacing the security enforcement performed by protected endpoints and application services.\u003C/p\u003E\n\u003Cp\u003EUse \u003Ccode\u003ECheckAsync\u003C/code\u003E to ask the server whether the current user is authorized to perform an action.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.CheckAsync(\n new AuthorizationCheckRequest\n {\n Action = \u0026quot;read\u0026quot;,\n Resource = \u0026quot;products\u0026quot;\n });\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { IsAllowed: true })\n{\n // The server authorized this action.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe method returns:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EUAuthResult\u0026lt;AuthorizationResult\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022authorizationcheckrequest\u0022\u003EAuthorizationCheckRequest\u003C/h3\u003E\n\u003Cp\u003EThe request contains:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EType\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAction\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Estring\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERequested operation\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EResource\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Estring\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EResource being accessed\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EResourceId\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Estring?\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOptional identifier for a specific resource instance\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EBoth \u003Ccode\u003EAction\u003C/code\u003E and \u003Ccode\u003EResource\u003C/code\u003E are required.\u003C/p\u003E\n\u003Cp\u003EThe action and resource values must follow the conventions understood by your server-side authorization configuration.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022check-a-specific-resource\u0022\u003ECheck a Specific Resource\u003C/h3\u003E\n\u003Cp\u003EFor example, check access to a particular product:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.CheckAsync(\n new AuthorizationCheckRequest\n {\n Action = \u0026quot;update\u0026quot;,\n Resource = \u0026quot;products\u0026quot;,\n ResourceId = productId.ToString()\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EProviding \u003Ccode\u003EResourceId\u003C/code\u003E allows the server to evaluate resource-specific rules where supported by the configured authorization pipeline.\u003C/p\u003E\n\u003Cp\u003EFor example, the application may distinguish between updating any product and updating a product owned by the current user.\u003C/p\u003E\n\u003Cp\u003EDo not assume that sending \u003Ccode\u003EResourceId\u003C/code\u003E automatically enables ownership checking. The relevant server-side policy must implement the required rule.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022understanding-authorization-results\u0022\u003E5. Understanding Authorization Results\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003ECheckAsync\u003C/code\u003E returns an \u003Ccode\u003EAuthorizationResult\u003C/code\u003E with the following properties:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIsAllowed\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EWhether the request was authorized\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERequiresReauthentication\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EWhether the decision indicates reauthentication is required\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EDenyReason\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOptional denial reason code\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022authorization-decisions-vs.http-results\u0022\u003EAuthorization Decisions vs. HTTP Results\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003ECheckAsync\u003C/code\u003E separates the outcome of the API request from the authorization decision itself.\u003C/p\u003E\n\u003Cp\u003EThe method returns \u003Ccode\u003EUAuthResult\u0026lt;AuthorizationResult\u0026gt;\u003C/code\u003E:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ccode\u003EUAuthResult.IsSuccess\u003C/code\u003E indicates whether the authorization check request completed successfully.\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003EAuthorizationResult.IsAllowed\u003C/code\u003E indicates whether the requested action is currently authorized.\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003EAuthorizationResult.RequiresReauthentication\u003C/code\u003E indicates whether additional authentication is required.\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003EAuthorizationResult.DenyReason\u003C/code\u003E provides an optional reason for denial.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EA denied authorization decision is a valid result of a successful authorization check.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EScenario\u003C/th\u003E\n\u003Cth\u003EHTTP Status\u003C/th\u003E\n\u003Cth\u003EAuthorization Result\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EAction allowed\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E200 OK\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EIsAllowed = true\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EAction denied\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E200 OK\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EIsAllowed = false\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EReauthentication required\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E200 OK\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003ERequiresReauthentication = true\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ECaller not authenticated\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E401 Unauthorized\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ENo authorization decision\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EInvalid request\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E400 Bad Request\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ENo authorization decision\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EServer-side evaluation failure\u003C/td\u003E\n\u003Ctd\u003EAppropriate error status\u003C/td\u003E\n\u003Ctd\u003ENo authorization decision\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Cstrong\u003EHTTP 200 means the authorization evaluation completed successfully. It does not mean access was granted.\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EOnly authenticated callers can use the current \u003Ccode\u003ECheckAsync\u003C/code\u003E endpoint. However, callers do not need permission to perform the action they are checking.\u003C/p\u003E\n\u003Cp\u003EThis distinction is intentional: the purpose of \u003Ccode\u003ECheckAsync\u003C/code\u003E is to determine whether the action is permitted.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022handling-denied-decisions\u0022\u003EHandling Denied Decisions\u003C/h3\u003E\n\u003Cp\u003EApplications should distinguish between authorization denial, reauthentication requirements, and request failures.\u003C/p\u003E\n\u003Cp\u003EA denied decision may be used to disable an action or display an appropriate message.\u003C/p\u003E\n\u003Cp\u003EA reauthentication requirement may be used to initiate the application\u0027s supported reauthentication flow.\u003C/p\u003E\n\u003Cp\u003EA failed request must not be interpreted as permission to proceed.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EFail closed:\u003C/strong\u003E If the authorization decision is unavailable, treat the operation as unavailable in the UI and rely on server-side authorization when the actual operation is requested.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022handle-all-decision-outcomes\u0022\u003EHandle All Decision Outcomes\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.CheckAsync(\n new AuthorizationCheckRequest\n {\n Action = \u0026quot;delete\u0026quot;,\n Resource = \u0026quot;products\u0026quot;,\n ResourceId = productId.ToString()\n });\n\nif (!result.IsSuccess)\n{\n // The authorization request itself failed.\n}\nelse if (result.Value is { RequiresReauthentication: true })\n{\n // Ask the user to authenticate again.\n}\nelse if (result.Value is { IsAllowed: true })\n{\n // The operation is allowed.\n}\nelse\n{\n // The operation was denied.\n var reason = result.Value?.DenyReason;\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EAn unsuccessful API request and an explicit authorization denial are different outcomes.\u003C/p\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ccode\u003Eresult.IsSuccess == false\u003C/code\u003E means the authorization request did not complete successfully at the API level.\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003Eresult.Value.IsAllowed == false\u003C/code\u003E means the server returned a denial decision.\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003Eresult.Value.RequiresReauthentication == true\u003C/code\u003E indicates that the caller needs to reestablish authentication.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003ETreat failed or unavailable authorization checks as \u003Cstrong\u003Enot authorized\u003C/strong\u003E in the UI.\u003C/p\u003E\n\u003Cp\u003ENever grant access because an authorization request failed.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022use-authorization-checks-in-blazor\u0022\u003E6. Use Authorization Checks in Blazor\u003C/h2\u003E\n\u003Cp\u003EA common use case is controlling whether an action button should be displayed.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@using CodeBeam.UltimateAuth.Client\n@using CodeBeam.UltimateAuth.Authorization.Contracts\n@inject IUAuthClient UAuthClient\n\n@if (_canCreate)\n{\n \u0026lt;button @onclick=\u0026quot;CreateProductAsync\u0026quot;\u0026gt;Create Product\u0026lt;/button\u0026gt;\n}\n\n@code {\n private bool _canCreate;\n\n protected override async Task OnInitializedAsync()\n {\n var result = await UAuthClient.Authorization.CheckAsync(\n new AuthorizationCheckRequest\n {\n Action = \u0026quot;create\u0026quot;,\n Resource = \u0026quot;products\u0026quot;\n });\n\n _canCreate = result.IsSuccess \u0026amp;\u0026amp; result.Value is { IsAllowed: true };\n }\n\n private Task CreateProductAsync()\n {\n // Invoke the protected application operation.\n return Task.CompletedTask;\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis improves the interface by hiding unavailable actions.\u003C/p\u003E\n\u003Cp\u003EHowever, the endpoint handling product creation must still enforce authorization independently.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022why-client-checks-are-not-security-boundaries\u0022\u003EWhy Client Checks Are Not Security Boundaries\u003C/h3\u003E\n\u003Cp\u003EA user can bypass a hidden button and send a request directly to an endpoint.\u003C/p\u003E\n\u003Cp\u003ETherefore:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003EClient authorization check\n |\n v\nImprove the user experience\n |\n v\nUser submits an operation\n |\n v\nServer independently authorizes the operation\n |\n v\nExecute or deny\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EClient authorization checks should never be the only protection for sensitive operations.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022get-my-roles\u0022\u003E7. Get My Roles\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003EGetMyRolesAsync\u003C/code\u003E to retrieve roles assigned to the current authenticated user.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.GetMyRolesAsync();\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } data)\n{\n foreach (var role in data.Roles.Items)\n {\n Console.WriteLine(role.Name);\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe method returns:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EUAuthResult\u0026lt;UserRolesGetResult\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022userrolesgetresult\u0022\u003EUserRolesGetResult\u003C/h3\u003E\n\u003Cp\u003EThe response contains:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EUserKey\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EUser whose roles were requested\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERoles\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPaginated collection of role assignments\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EEach entry is a \u003Ccode\u003EUserRoleInfo\u003C/code\u003E.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022userroleinfo\u0022\u003EUserRoleInfo\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ETenant\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ETenant associated with the assignment\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EUserKey\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAssigned user\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERoleId\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAssigned role\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EName\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERole name\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAssignedAt\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAssignment timestamp\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022pagination\u0022\u003EPagination\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.GetMyRolesAsync(\n new PageRequest\n {\n PageNumber = 1,\n PageSize = 20\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe returned role collection is paginated.\u003C/p\u003E\n\u003Cp\u003EDo not assume that the first page contains all assigned roles.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022get-another-users-roles\u0022\u003E8. Get Another User\u0027s Roles\u003C/h2\u003E\n\u003Cp\u003EAdministrative interfaces can retrieve the roles assigned to a specific user.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.GetUserRolesAsync(userKey,\n new PageRequest\n {\n PageNumber = 1,\n PageSize = 20\n });\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } data)\n{\n foreach (var role in data.Roles.Items)\n {\n Console.WriteLine($\u0026quot;{role.Name} \u2014 {role.AssignedAt}\u0026quot;);\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis operation requires appropriate administrative authorization.\u003C/p\u003E\n\u003Cp\u003EThe \u003Ccode\u003EUserKey\u003C/code\u003E identifies the target user, not the current actor.\u003C/p\u003E\n\u003Cp\u003EThe server determines whether the current actor may inspect that user\u0027s role assignments.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022query-roles\u0022\u003E9. Query Roles\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003EQueryRolesAsync\u003C/code\u003E to retrieve roles with pagination and optional filtering.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.QueryRolesAsync(\n new RoleQuery\n {\n PageNumber = 1,\n PageSize = 20\n });\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } page)\n{\n foreach (var role in page.Items)\n {\n Console.WriteLine(role.Name);\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe method returns:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EUAuthResult\u0026lt;PagedResult\u0026lt;RoleInfo\u0026gt;\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022rolequery\u0022\u003ERoleQuery\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003ERoleQuery\u003C/code\u003E inherits from \u003Ccode\u003EPageRequest\u003C/code\u003E and adds:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ESearch\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOptional role-name search\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIncludeDeleted\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EInclude deleted roles when supported and authorized\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPageNumber\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERequested page\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPageSize\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERequested page size\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022search-roles\u0022\u003ESearch Roles\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.QueryRolesAsync(\n new RoleQuery\n {\n Search = \u0026quot;manager\u0026quot;,\n PageNumber = 1,\n PageSize = 10\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022include-deleted-roles\u0022\u003EInclude Deleted Roles\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.QueryRolesAsync(\n new RoleQuery\n {\n IncludeDeleted = true,\n PageNumber = 1,\n PageSize = 20\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EUse \u003Ccode\u003EIncludeDeleted\u003C/code\u003E only where the application needs administrative access to deleted role records.\u003C/p\u003E\n\u003Cp\u003EThe server remains responsible for enforcing the query\u0027s permissions and lifecycle rules.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022understanding-roleinfo\u0022\u003E10. Understanding RoleInfo\u003C/h2\u003E\n\u003Cp\u003EEach role returned by the SDK is represented by \u003Ccode\u003ERoleInfo\u003C/code\u003E.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EType\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EId\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003ERoleId\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERole identifier\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EName\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Estring\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERole name\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPermissions\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EIReadOnlyCollection\u0026lt;Permission\u0026gt;\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAssigned permissions\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECreatedAt\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EDateTimeOffset\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECreation timestamp\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EUpdatedAt\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EDateTimeOffset?\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ELast update timestamp\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.QueryRolesAsync(\n new RoleQuery\n {\n PageNumber = 1,\n PageSize = 20\n });\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } page)\n{\n foreach (var role in page.Items)\n {\n Console.WriteLine($\u0026quot;Role: {role.Name}\u0026quot;);\n\n foreach (var permission in role.Permissions)\n {\n Console.WriteLine($\u0026quot; {permission.Value}\u0026quot;);\n }\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003ERoleId\u003C/code\u003E is a strongly typed identifier rather than a plain \u003Ccode\u003EGuid\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003ERoleId roleId = RoleId.From(existingGuid);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EUse \u003Ccode\u003ERoleId\u003C/code\u003E for role management requests.\u003C/p\u003E\n\u003Cp\u003EDo not confuse it with \u003Ccode\u003EUserKey\u003C/code\u003E or other identifier types.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022create-a-role\u0022\u003E11. Create a Role\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003ECreateRoleAsync\u003C/code\u003E to create a role.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.CreateRoleAsync(\n new CreateRoleRequest\n {\n Name = \u0026quot;Manager\u0026quot;\n });\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } role)\n{\n Console.WriteLine($\u0026quot;Created: {role.Name}\u0026quot;);\n Console.WriteLine($\u0026quot;Role ID: {role.Id}\u0026quot;);\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe method returns:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EUAuthResult\u0026lt;RoleInfo\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003ERole names must be unique within the relevant tenant according to the server\u0027s role-name rules.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022create-a-role-with-permissions\u0022\u003ECreate a Role with Permissions\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003ECreateRoleRequest\u003C/code\u003E supports an optional initial permission collection.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.CreateRoleAsync(\n new CreateRoleRequest\n {\n Name = \u0026quot;ProductManager\u0026quot;,\n Permissions =\n [\n Permission.From(\u0026quot;products.read\u0026quot;),\n Permission.From(\u0026quot;products.create\u0026quot;),\n Permission.From(\u0026quot;products.update\u0026quot;)\n ]\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis allows applications to create and configure a role in one operation.\u003C/p\u003E\n\u003Cp\u003EThe server validates the requested permissions and whether the caller is authorized to create the role.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022create-a-read-only-role\u0022\u003ECreate a Read-Only Role\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.CreateRoleAsync(\n new CreateRoleRequest\n {\n Name = \u0026quot;Auditor\u0026quot;,\n Permissions =\n [\n Permission.From(\u0026quot;reports.read\u0026quot;),\n Permission.From(\u0026quot;products.read\u0026quot;)\n ]\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EUse role names that describe the responsibility or capability represented by the role.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022rename-a-role\u0022\u003E12. Rename a Role\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003ERenameRoleAsync\u003C/code\u003E to change a role\u0027s name.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.RenameRoleAsync(\n new RenameRoleRequest\n {\n Id = roleId,\n Name = \u0026quot;SeniorManager\u0026quot;\n });\n\nif (result.IsSuccess)\n{\n // Role renamed.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003ERenameRoleRequest\u003C/code\u003E requires:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ccode\u003EId\u003C/code\u003E: The role\u0027s \u003Ccode\u003ERoleId\u003C/code\u003E.\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003EName\u003C/code\u003E: The new role name.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EThe operation may fail if another role already uses the requested name.\u003C/p\u003E\n\u003Cp\u003ERole renaming does not mean that permissions are automatically changed.\u003C/p\u003E\n\u003Cp\u003EAfter a successful rename, the SDK publishes an \u003Ccode\u003EAuthorizationChanged\u003C/code\u003E state event.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022set-role-permissions\u0022\u003E13. Set Role Permissions\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003ESetRolePermissionsAsync\u003C/code\u003E to configure a role\u0027s permissions.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.SetRolePermissionsAsync(\n new SetRolePermissionsRequest\n {\n RoleId = roleId,\n Permissions =\n [\n Permission.From(\u0026quot;products.read\u0026quot;),\n Permission.From(\u0026quot;products.create\u0026quot;),\n Permission.From(\u0026quot;products.update\u0026quot;)\n ]\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe request contains:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERoleId\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERole to modify\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPermissions\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ENew permission collection\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022important-this-is-a-replacement-operation\u0022\u003EImportant: This Is a Replacement Operation\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003ESetRolePermissionsAsync\u003C/code\u003E replaces the role\u0027s existing permission collection.\u003C/p\u003E\n\u003Cp\u003EIt does not append permissions to the current collection.\u003C/p\u003E\n\u003Cp\u003EFor example, suppose a role currently contains:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003Eproducts.read\nproducts.create\nproducts.update\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003ESubmitting:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EPermissions =\n[\n Permission.From(\u0026quot;products.read\u0026quot;)\n]\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003Emeans the intended new permission collection contains only:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003Eproducts.read\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe other permissions are removed from the role.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022safely-add-a-permission-to-an-existing-role\u0022\u003ESafely Add a Permission to an Existing Role\u003C/h3\u003E\n\u003Cp\u003ETo preserve existing permissions, first retrieve the role and construct the complete desired collection.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar query = await UAuthClient.Authorization.QueryRolesAsync(\n new RoleQuery\n {\n Search = \u0026quot;ProductManager\u0026quot;,\n PageNumber = 1,\n PageSize = 50\n });\n\nif (query.IsSuccess \u0026amp;\u0026amp; query.Value is { } page)\n{\n var role = page.Items.FirstOrDefault(x =\u0026gt; x.Name == \u0026quot;ProductManager\u0026quot;);\n\n if (role is not null)\n {\n var permissions = role.Permissions.Append(Permission.From(\u0026quot;products.delete\u0026quot;)).Distinct().ToArray();\n\n var result =\n await UAuthClient.Authorization.SetRolePermissionsAsync(\n new SetRolePermissionsRequest\n {\n RoleId = role.Id,\n Permissions = permissions\n });\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis is a read-modify-write pattern, not an atomic append operation.\u003C/p\u003E\n\u003Cp\u003EIf multiple administrators edit the same role concurrently, the last successful replacement may overwrite another administrator\u0027s changes.\u003C/p\u003E\n\u003Cp\u003EApplications requiring concurrency-safe editing should implement suitable coordination or version checking at the application level.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022remove-a-permission\u0022\u003ERemove a Permission\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar updatedPermissions = role.Permissions.Where(x =\u0026gt; x.Value != \u0026quot;products.delete\u0026quot;).ToArray();\n\nvar result = await UAuthClient.Authorization.SetRolePermissionsAsync(\n new SetRolePermissionsRequest\n {\n RoleId = role.Id,\n Permissions = updatedPermissions\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EAgain, submit the complete desired permission set.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022clear-all-permissions\u0022\u003EClear All Permissions\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.SetRolePermissionsAsync(\n new SetRolePermissionsRequest\n {\n RoleId = roleId,\n Permissions = []\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EAn empty collection requests that the role have no permissions.\u003C/p\u003E\n\u003Cp\u003EThe server determines whether the operation is permitted.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022assign-a-role-to-a-user\u0022\u003E14. Assign a Role to a User\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003EAssignRoleToUserAsync\u003C/code\u003E to assign an existing role to a user.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.AssignRoleToUserAsync(\n new AssignRoleRequest\n {\n UserKey = userKey,\n RoleName = \u0026quot;Manager\u0026quot;\n });\n\nif (result.IsSuccess)\n{\n // Role assigned.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe request contains:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EUserKey\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ETarget user\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERoleName\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EExisting role name\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThe role must exist, and the current actor must be authorized to assign it.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022assign-multiple-roles\u0022\u003EAssign Multiple Roles\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eforeach (var roleName in new[] { \u0026quot;Manager\u0026quot;, \u0026quot;Auditor\u0026quot; })\n{\n var result = await UAuthClient.Authorization.AssignRoleToUserAsync(\n new AssignRoleRequest\n {\n UserKey = userKey,\n RoleName = roleName\n });\n\n if (!result.IsSuccess)\n {\n // Handle the failed assignment.\n break;\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EEach call is a separate operation.\u003C/p\u003E\n\u003Cp\u003EThis example does not provide transactional, all-or-nothing behavior across the entire collection.\u003C/p\u003E\n\u003Cp\u003EFor complex provisioning workflows, consider whether the application needs a server-side orchestration operation.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022remove-a-role-from-a-user\u0022\u003E15. Remove a Role from a User\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003ERemoveRoleFromUserAsync\u003C/code\u003E:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.RemoveRoleFromUserAsync(\n new RemoveRoleRequest\n {\n UserKey = userKey,\n RoleName = \u0026quot;Manager\u0026quot;\n });\n\nif (result.IsSuccess)\n{\n // Role removed.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003ERemoving a role changes the user\u0027s role assignments.\u003C/p\u003E\n\u003Cp\u003EIt does not delete the role itself.\u003C/p\u003E\n\u003Cp\u003EOther users assigned to that role are unaffected by this particular operation.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022important-effective-permissions-may-remain\u0022\u003EImportant: Effective Permissions May Remain\u003C/h3\u003E\n\u003Cp\u003EA user may receive the same permission through multiple roles.\u003C/p\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003EUser\n |\n \u002B-- Manager\n | \u002B-- products.read\n |\n \u002B-- Auditor\n \u002B-- products.read\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003ERemoving \u003Ccode\u003EManager\u003C/code\u003E does not necessarily remove the user\u0027s ability to read products because \u003Ccode\u003EAuditor\u003C/code\u003E still provides \u003Ccode\u003Eproducts.read\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003ETo determine whether the user may perform an action, rely on the server\u0027s authorization decision rather than inspecting one role assignment in isolation.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022delete-a-role\u0022\u003E16. Delete a Role\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003EDeleteRoleAsync\u003C/code\u003E to delete a role.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.DeleteRoleAsync(\n new DeleteRoleRequest\n {\n Id = roleId,\n Mode = DeleteMode.Soft\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe request supports a deletion mode.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022hard-delete\u0022\u003EHard Delete\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.DeleteRoleAsync(\n new DeleteRoleRequest\n {\n Id = roleId,\n Mode = DeleteMode.Hard\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe availability and effect of each deletion mode are determined by server-side lifecycle rules.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022roledeleteresult\u0022\u003ERoleDeleteResult\u003C/h3\u003E\n\u003Cp\u003EThe method returns:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EUAuthResult\u0026lt;RoleDeleteResult\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe response contains:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERoleId\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EDeleted role\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERemovedAssignments\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ENumber of user-role assignments removed\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMode\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EApplied deletion mode\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EDeletedAt\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EDeletion timestamp\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022inspect-the-result\u0022\u003EInspect the Result\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.DeleteRoleAsync(\n new DeleteRoleRequest\n {\n Id = roleId,\n Mode = DeleteMode.Soft\n });\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } deleted)\n{\n Console.WriteLine($\u0026quot;Removed assignments: {deleted.RemovedAssignments}\u0026quot;);\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EDeleting a role removes its assignments from users.\u003C/p\u003E\n\u003Cp\u003EThis can affect multiple users at once.\u003C/p\u003E\n\u003Cp\u003EFor that reason, role deletion should be presented as a sensitive administrative operation with appropriate confirmation.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022build-a-role-management-page\u0022\u003E17. Build a Role Management Page\u003C/h2\u003E\n\u003Cp\u003EThe following Blazor example demonstrates querying roles, creating a role, and deleting a selected role.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@using CodeBeam.UltimateAuth.Client\n@using CodeBeam.UltimateAuth.Authorization.Contracts\n@using CodeBeam.UltimateAuth.Core.Contracts\n@inject IUAuthClient UAuthClient\n\n\u0026lt;h3\u0026gt;Role Management\u0026lt;/h3\u0026gt;\n\n\u0026lt;input @bind=\u0026quot;_search\u0026quot; placeholder=\u0026quot;Search roles\u0026quot; /\u0026gt;\n\n\u0026lt;button @onclick=\u0026quot;LoadRolesAsync\u0026quot;\u0026gt;Search\u0026lt;/button\u0026gt;\n\n@if (_loading)\n{\n \u0026lt;p\u0026gt;Loading roles...\u0026lt;/p\u0026gt;\n}\nelse\n{\n @foreach (var role in _roles)\n {\n \u0026lt;div @key=\u0026quot;role.Id\u0026quot;\u0026gt;\n \u0026lt;strong\u0026gt;@role.Name\u0026lt;/strong\u0026gt;\n \u0026lt;span\u0026gt;@role.Permissions.Count permissions\u0026lt;/span\u0026gt;\n \u0026lt;button @onclick=\u0026quot;() =\u0026gt; DeleteRoleAsync(role.Id)\u0026quot;\u0026gt;Delete\u0026lt;/button\u0026gt;\n \u0026lt;/div\u0026gt;\n }\n}\n\n\u0026lt;h4\u0026gt;Create Role\u0026lt;/h4\u0026gt;\n\u0026lt;input @bind=\u0026quot;_newRoleName\u0026quot; placeholder=\u0026quot;Role name\u0026quot; /\u0026gt;\n\u0026lt;button @onclick=\u0026quot;CreateRoleAsync\u0026quot; disabled=\u0026quot;@_busy\u0026quot;\u0026gt;Create\u0026lt;/button\u0026gt;\n\n@if (_message is not null)\n{\n \u0026lt;p role=\u0026quot;status\u0026quot;\u0026gt;@_message\u0026lt;/p\u0026gt;\n}\n\n@code {\n private IReadOnlyList\u0026lt;RoleInfo\u0026gt; _roles = [];\n\n private string _search = \u0026quot;\u0026quot;;\n private string _newRoleName = \u0026quot;\u0026quot;;\n private string? _message;\n\n private bool _loading = true;\n private bool _busy;\n\n protected override async Task OnInitializedAsync()\n {\n await LoadRolesAsync();\n }\n\n private async Task LoadRolesAsync()\n {\n _loading = true;\n\n try\n {\n var result = await UAuthClient.Authorization.QueryRolesAsync(\n new RoleQuery\n {\n Search = _search,\n PageNumber = 1,\n PageSize = 50\n });\n\n if (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } page)\n {\n _roles = page.Items;\n }\n else\n {\n _message = \u0026quot;Unable to load roles.\u0026quot;;\n }\n }\n catch (Exception)\n {\n _message = \u0026quot;The authorization service is unavailable.\u0026quot;;\n }\n finally\n {\n _loading = false;\n }\n }\n\n private async Task CreateRoleAsync()\n {\n if (_busy || string.IsNullOrWhiteSpace(_newRoleName))\n return;\n\n _busy = true;\n _message = null;\n\n try\n {\n var result = await UAuthClient.Authorization.CreateRoleAsync(\n new CreateRoleRequest\n {\n Name = _newRoleName\n });\n\n if (result.IsSuccess)\n {\n _newRoleName = \u0026quot;\u0026quot;;\n await LoadRolesAsync();\n }\n else\n {\n _message = \u0026quot;Unable to create role.\u0026quot;;\n }\n }\n finally\n {\n _busy = false;\n }\n }\n\n private async Task DeleteRoleAsync(RoleId roleId)\n {\n // Add a confirmation dialog before deletion in production.\n\n var result = await UAuthClient.Authorization.DeleteRoleAsync(\n new DeleteRoleRequest\n {\n Id = roleId,\n Mode = DeleteMode.Soft\n });\n\n if (result.IsSuccess)\n {\n await LoadRolesAsync();\n }\n else\n {\n _message = \u0026quot;Unable to delete role.\u0026quot;;\n }\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis example demonstrates the SDK integration rather than a complete production administration interface.\u003C/p\u003E\n\u003Cp\u003EA production implementation should include pagination, role assignment controls, permission editing, deletion confirmation, and appropriate loading and error handling for every operation.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022build-a-user-role-assignment-interface\u0022\u003E18. Build a User Role Assignment Interface\u003C/h2\u003E\n\u003Cp\u003EAn administrative user details page can display assigned roles and allow administrators to add or remove them.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022load-user-roles\u0022\u003ELoad User Roles\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.GetUserRolesAsync(userKey,\n new PageRequest\n {\n PageNumber = 1,\n PageSize = 50\n });\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } data)\n{\n var assignedRoles = data.Roles.Items.ToList();\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022assign-a-selected-role\u0022\u003EAssign a Selected Role\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.AssignRoleToUserAsync(\n new AssignRoleRequest\n {\n UserKey = userKey,\n RoleName = selectedRoleName\n });\n\nif (result.IsSuccess)\n{\n // Reload the user\u0027s assigned roles.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022remove-a-selected-role\u0022\u003ERemove a Selected Role\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.RemoveRoleFromUserAsync(\n new RemoveRoleRequest\n {\n UserKey = userKey,\n RoleName = selectedRoleName\n });\n\nif (result.IsSuccess)\n{\n // Reload the user\u0027s assigned roles.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis pattern works well alongside \u003Ccode\u003EUAuthClient.Users\u003C/code\u003E for building a unified user administration interface.\u003C/p\u003E\n\u003Cp\u003ESee \u003Ca href=\u0022/docs/client/user-management\u0022\u003EUser Management\u003C/a\u003E.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022authorization-state-events\u0022\u003E19. Authorization State Events\u003C/h2\u003E\n\u003Cp\u003EThe current \u003Ccode\u003EUAuthAuthorizationClient\u003C/code\u003E publishes:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EUAuthStateEvent.AuthorizationChanged\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003Eafter successful authorization mutations.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EOperation\u003C/th\u003E\n\u003Cth\u003EPublishes \u003Ccode\u003EAuthorizationChanged\u003C/code\u003E\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAssignRoleToUserAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EYes\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERemoveRoleFromUserAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EYes\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERenameRoleAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EYes\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ESetRolePermissionsAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EYes\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EDeleteRoleAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EYes\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECreateRoleAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ENo\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EQueryRolesAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ENo\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EGetMyRolesAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ENo\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EGetUserRolesAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ENo\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECheckAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ENo\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThe event follows the configured client state event handling mode.\u003C/p\u003E\n\u003Cp\u003EIt can be used to refresh navigation menus, role assignment views, or permission-sensitive UI.\u003C/p\u003E\n\u003Cp\u003EHowever, it is a \u003Cstrong\u003Elocal client event\u003C/strong\u003E, not a guarantee that all active clients or distributed caches have immediately observed the change.\u003C/p\u003E\n\u003Cp\u003EFor example, an administrator assigning a role to another user does not automatically mean that user\u0027s open browser receives the administrator\u0027s local event.\u003C/p\u003E\n\u003Cp\u003EApplications requiring cross-client updates should provide an appropriate synchronization mechanism.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022authorization-changes-and-active-sessions\u0022\u003E20. Authorization Changes and Active Sessions\u003C/h2\u003E\n\u003Cp\u003ERole and permission changes may affect active users.\u003C/p\u003E\n\u003Cp\u003EHowever, the timing of those changes depends on how authorization data is resolved and cached by the server.\u003C/p\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Col\u003E\n\u003Cli\u003EAn administrator removes a role.\u003C/li\u003E\n\u003Cli\u003EThe server persists the new role assignment state.\u003C/li\u003E\n\u003Cli\u003EA subsequent authorization evaluation uses the effective authorization state available to the server.\u003C/li\u003E\n\u003C/ol\u003E\n\u003Cp\u003EIf the application caches authorization information, changes may not become visible everywhere immediately.\u003C/p\u003E\n\u003Cp\u003EDo not assume that a client event invalidates every active session or refreshes every distributed authorization cache.\u003C/p\u003E\n\u003Cp\u003EFor sensitive operations, authorization should be evaluated against sufficiently current server-side state.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022roles-are-not-sessions\u0022\u003ERoles Are Not Sessions\u003C/h3\u003E\n\u003Cp\u003ERemoving a role and revoking a session are different operations.\u003C/p\u003E\n\u003Cp\u003ERemoving a role changes authorization data.\u003C/p\u003E\n\u003Cp\u003ERevoking a session changes whether an authentication session remains valid.\u003C/p\u003E\n\u003Cp\u003EApplications may combine these operations when security policy requires immediate termination of access, but the concepts should remain separate.\u003C/p\u003E\n\u003Cp\u003ESee \u003Ca href=\u0022/docs/client/session-management\u0022\u003ESession Management\u003C/a\u003E.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022multi-tenant-authorization\u0022\u003E21. Multi-Tenant Authorization\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth authorization operations are scoped to the current tenant context.\u003C/p\u003E\n\u003Cp\u003ERole names are unique within the relevant tenant, and role assignments belong to the applicable tenant.\u003C/p\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003ETenant A\n |\n \u002B-- Manager\n | \u002B-- products.read\n |\n \u002B-- User A\n\nTenant B\n |\n \u002B-- Manager\n | \u002B-- reports.read\n |\n \u002B-- User B\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe same role name may represent different permission sets in different tenants.\u003C/p\u003E\n\u003Cp\u003EThe client does not gain cross-tenant access merely by providing another user\u0027s \u003Ccode\u003EUserKey\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EThe server must enforce tenant isolation and administrative authorization.\u003C/p\u003E\n\u003Cp\u003EWhen implementing multi-tenant administration, do not assume role names or permission configurations are globally shared.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022handling-authorization-errors\u0022\u003E22. Handling Authorization Errors\u003C/h2\u003E\n\u003Cp\u003EAuthorization methods return \u003Ccode\u003EUAuthResult\u003C/code\u003E or \u003Ccode\u003EUAuthResult\u0026lt;T\u0026gt;\u003C/code\u003E.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022handle-role-creation-errors\u0022\u003EHandle Role Creation Errors\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.CreateRoleAsync(\n new CreateRoleRequest\n {\n Name = \u0026quot;Manager\u0026quot;\n });\n\nif (result.IsSuccess)\n{\n // Role created.\n}\nelse if (result.Http.IsConflict)\n{\n // A conflicting role may already exist.\n}\nelse if (result.Http.IsForbidden)\n{\n // Caller lacks permission.\n}\nelse if (result.Http.IsBadRequest)\n{\n // Invalid request.\n}\nelse\n{\n // Handle another failure.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022handle-authorization-check-failures\u0022\u003EHandle Authorization Check Failures\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.CheckAsync(\n new AuthorizationCheckRequest\n {\n Action = \u0026quot;update\u0026quot;,\n Resource = \u0026quot;products\u0026quot;\n });\n\nvar allowed = result.IsSuccess \u0026amp;\u0026amp; result.Value is { IsAllowed: true };\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis fail-closed pattern is appropriate when determining whether the UI should expose a protected operation.\u003C/p\u003E\n\u003Cp\u003EUse the server\u0027s structured error information for diagnostics where appropriate.\u003C/p\u003E\n\u003Cp\u003EDo not expose sensitive policy internals unnecessarily.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022security-considerations\u0022\u003E23. Security Considerations\u003C/h2\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022server-authorization-is-authoritative\u0022\u003EServer Authorization Is Authoritative\u003C/h3\u003E\n\u003Cp\u003EClient-side checks improve usability but cannot protect an endpoint by themselves.\u003C/p\u003E\n\u003Cp\u003EEvery protected operation must enforce authorization on the server.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022follow-least-privilege\u0022\u003EFollow Least Privilege\u003C/h3\u003E\n\u003Cp\u003EAssign only the permissions a role actually needs.\u003C/p\u003E\n\u003Cp\u003EAvoid using \u003Ccode\u003E*\u003C/code\u003E or broad prefix wildcards for ordinary users.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022protect-administrative-operations\u0022\u003EProtect Administrative Operations\u003C/h3\u003E\n\u003Cp\u003ERole creation, permission replacement, role assignment, and role deletion are privileged operations.\u003C/p\u003E\n\u003Cp\u003EThe server must authorize each mutation independently.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022treat-permission-replacement-carefully\u0022\u003ETreat Permission Replacement Carefully\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003ESetRolePermissionsAsync\u003C/code\u003E replaces the entire permission collection.\u003C/p\u003E\n\u003Cp\u003EAlways inspect the existing role configuration before submitting changes.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022handle-reauthentication-requirements\u0022\u003EHandle Reauthentication Requirements\u003C/h3\u003E\n\u003Cp\u003EAn authorization decision can indicate \u003Ccode\u003ERequiresReauthentication\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EApplications should handle this state explicitly rather than treating it as an ordinary permission denial.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022respect-tenant-boundaries\u0022\u003ERespect Tenant Boundaries\u003C/h3\u003E\n\u003Cp\u003ERoles and assignments must remain within their authorized tenant context.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022do-not-infer-effective-access-from-one-role\u0022\u003EDo Not Infer Effective Access from One Role\u003C/h3\u003E\n\u003Cp\u003EA user can have multiple roles, overlapping permissions, and additional policy restrictions.\u003C/p\u003E\n\u003Cp\u003EUse the server\u0027s authorization decision when determining whether an action is permitted.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022account-for-cached-authorization-state\u0022\u003EAccount for Cached Authorization State\u003C/h3\u003E\n\u003Cp\u003EPermission changes may not propagate instantly to every active client or cached authorization context.\u003C/p\u003E\n\u003Cp\u003EDesign sensitive operations around server-side enforcement and appropriate cache invalidation.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022putting-everything-together\u0022\u003E24. Putting Everything Together\u003C/h2\u003E\n\u003Cp\u003EA typical administrative authorization workflow may look like this:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003ECreate role\n |\n v\nConfigure permissions\n |\n v\nAssign role to user\n |\n v\nUser requests protected action\n |\n v\nServer evaluates authorization\n |\n \u002B-- Allow --\u0026gt; Execute operation\n |\n \u002B-- Deny --\u0026gt; Reject operation\n |\n \u002B-- Reauthentication required\n |\n v\n Authenticate again\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EUltimateAuth separates role management from authorization enforcement.\u003C/p\u003E\n\u003Cp\u003EThis allows applications to provide flexible administrative interfaces while keeping access decisions under server control.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022next-steps\u0022\u003ENext Steps\u003C/h2\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/plugin-domains/policies-access-control\u0022\u003EPolicies \u0026amp; Access Control\u003C/a\u003E \u2014 Understand application policies and access rules.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/security/authorization-pipeline-internals\u0022\u003EAuthorization Pipeline Internals\u003C/a\u003E \u2014 Explore how authorization decisions are evaluated.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/client/user-management\u0022\u003EUser Management\u003C/a\u003E \u2014 Manage user accounts and lifecycle.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/client/session-management\u0022\u003ESession Management\u003C/a\u003E \u2014 Manage authenticated sessions and devices.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/security/session-security\u0022\u003ESession Security Model\u003C/a\u003E \u2014 Understand session validity and security boundaries.\u003C/li\u003E\n\u003C/ul\u003E\n", + "Html": "\n\u003Cp\u003EUltimateAuth provides a unified client API for managing roles, assigning permissions, controlling user-role relationships, and requesting authorization decisions from the server.\u003C/p\u003E\n\u003Cp\u003EThrough \u003Ccode\u003EUAuthClient.Authorization\u003C/code\u003E, applications can build administrative authorization interfaces and check whether the current user is allowed to perform a particular action.\u003C/p\u003E\n\u003Cp\u003EAuthorization is enforced on the server. Client-side permission checks are useful for improving the user experience, but they do not replace authorization at the endpoint or application-service level.\u003C/p\u003E\n\u003Cp\u003EThis guide covers the current \u003Ccode\u003EIAuthorizationClient\u003C/code\u003E API, practical role management workflows, permission patterns, and secure client integration.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022understanding-authorization\u0022\u003E1. Understanding Authorization\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth authorization combines several concepts:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EConcept\u003C/th\u003E\n\u003Cth\u003EPurpose\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ERoles\u003C/td\u003E\n\u003Ctd\u003ENamed collections of permissions\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EPermissions\u003C/td\u003E\n\u003Ctd\u003EFine-grained capabilities represented by normalized values\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EUser-role assignments\u003C/td\u003E\n\u003Ctd\u003EAssociations between users and roles\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EPolicies\u003C/td\u003E\n\u003Ctd\u003EServer-side rules that participate in access decisions\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EAuthorization context\u003C/td\u003E\n\u003Ctd\u003EInformation about the actor, resource, action, and current security state\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EA typical authorization relationship looks like this:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003EUser\n |\n \u002B-- Manager\n | |\n | \u002B-- products.read\n | \u002B-- products.create\n | \u002B-- products.update\n |\n \u002B-- Auditor\n |\n \u002B-- reports.read\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EA user can have multiple roles, and each role can contain multiple permissions.\u003C/p\u003E\n\u003Cp\u003EHowever, \u003Cstrong\u003Ehaving a permission does not automatically guarantee access\u003C/strong\u003E. The server\u0027s authorization pipeline may apply additional requirements, such as resource ownership, session validity, tenant isolation, or application policies.\u003C/p\u003E\n\u003Cp\u003EFor the internal decision model, see \u003Ca href=\u0022/docs/security/authorization-pipeline\u0022\u003EAuthorization Pipeline Internals\u003C/a\u003E.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022accessing-the-authorization-client\u0022\u003E2. Accessing the Authorization Client\u003C/h2\u003E\n\u003Cp\u003EInject \u003Ccode\u003EIUAuthClient\u003C/code\u003E into a Blazor component:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@using CodeBeam.UltimateAuth.Client\n@inject IUAuthClient UAuthClient\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EAuthorization operations are available through:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EUAuthClient.Authorization\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis property exposes \u003Ccode\u003EIAuthorizationClient\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EThe interface provides three main groups of operations:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Cstrong\u003EAuthorization checks:\u003C/strong\u003E Evaluate whether the current user may perform an action.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003ERole management:\u003C/strong\u003E Create, query, rename, configure, and delete roles.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003EUser-role management:\u003C/strong\u003E Retrieve, assign, and remove user roles.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EThe SDK sends requests to the corresponding UltimateAuth endpoints and returns structured \u003Ccode\u003EUAuthResult\u003C/code\u003E values.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022understanding-permissions\u0022\u003E3. Understanding Permissions\u003C/h2\u003E\n\u003Cp\u003EA permission is represented by the \u003Ccode\u003EPermission\u003C/code\u003E value type.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar permission = Permission.From(\u0026quot;products.create\u0026quot;);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe current implementation normalizes permission values by trimming surrounding whitespace and converting the value to lowercase.\u003C/p\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar permission = Permission.From(\u0026quot; PRODUCTS.CREATE \u0026quot;);\n\nConsole.WriteLine(permission.Value);\n// products.create\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe constructor rejects null, empty, and whitespace-only values.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022permission-patterns\u0022\u003EPermission Patterns\u003C/h3\u003E\n\u003Cp\u003EUltimateAuth recognizes three important forms:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EPattern\u003C/th\u003E\n\u003Cth\u003EExample\u003C/th\u003E\n\u003Cth\u003EMeaning\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EExact\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Eproducts.create\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EA specific permission\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EPrefix wildcard\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Eproducts.*\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPermissions within the corresponding prefix\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EGlobal wildcard\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E*\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EUnrestricted permission matching within the applicable authorization rules\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003ECreate them using:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar exact = Permission.From(\u0026quot;products.create\u0026quot;);\n\nvar prefix = Permission.From(\u0026quot;products.*\u0026quot;);\n\nvar global = Permission.Wildcard;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe type also exposes:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Epermission.IsWildcard;\npermission.IsPrefix;\npermission.IsValid;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022permission-normalization\u0022\u003EPermission Normalization\u003C/h3\u003E\n\u003Cp\u003EThe following values normalize to the same representation:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EPermission.From(\u0026quot;Products.Create\u0026quot;);\nPermission.From(\u0026quot;products.create\u0026quot;);\nPermission.From(\u0026quot; PRODUCTS.CREATE \u0026quot;);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EAll produce:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003Eproducts.create\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EUse consistent permission names across application services, policies, and role configuration.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022wildcards-are-powerful\u0022\u003EWildcards Are Powerful\u003C/h3\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EPermission.From(\u0026quot;users.*\u0026quot;)\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003Emay grant broad access to operations within the \u003Ccode\u003Eusers\u003C/code\u003E permission prefix.\u003C/p\u003E\n\u003Cp\u003EThe global wildcard:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EPermission.Wildcard\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003Eis especially sensitive.\u003C/p\u003E\n\u003Cp\u003EAvoid assigning broad wildcard permissions merely to simplify development. Use least-privilege permissions for ordinary application roles.\u003C/p\u003E\n\u003Cp\u003EA wildcard is not a substitute for satisfying independent security policies, authentication requirements, or tenant restrictions.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022check-authorization\u0022\u003E4. Check Authorization\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003ECheckAsync\u003C/code\u003E evaluates whether the current authenticated user is authorized to perform a specific action against a resource.\u003C/p\u003E\n\u003Cp\u003EIt provides an \u003Cstrong\u003Eon-demand, server-side authorization decision\u003C/strong\u003E based on the current authentication context, permissions, roles, and applicable authorization policies.\u003C/p\u003E\n\u003Cp\u003EUnlike operations that perform protected actions, \u003Ccode\u003ECheckAsync\u003C/code\u003E only evaluates access. It does not execute the requested operation, modify permissions, grant access, or reserve authorization for a future request.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022when-to-use-checkasync\u0022\u003EWhen to Use CheckAsync\u003C/h3\u003E\n\u003Cp\u003EUse \u003Ccode\u003ECheckAsync\u003C/code\u003E when your application needs to:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EDetermine whether an authenticated user can perform an action before presenting it in the UI.\u003C/li\u003E\n\u003Cli\u003EEnable, disable, or hide controls based on the current authorization decision.\u003C/li\u003E\n\u003Cli\u003EExplain why an operation is unavailable, using the returned denial reason where appropriate.\u003C/li\u003E\n\u003Cli\u003EDetermine whether an operation requires reauthentication.\u003C/li\u003E\n\u003Cli\u003EEvaluate resource-specific access when the applicable policies support it.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EFor example, an application may check whether the current user can delete a particular order before displaying the Delete button.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022what-checkasync-does-not-guarantee\u0022\u003EWhat CheckAsync Does Not Guarantee\u003C/h3\u003E\n\u003Cp\u003E\u003Cstrong\u003EA successful authorization check is not a guarantee that a subsequent operation will succeed.\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EAuthorization is evaluated against the security context available at the time of the check. That context may change before the actual operation is performed.\u003C/p\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EAn administrator may revoke the user\u0027s permissions.\u003C/li\u003E\n\u003Cli\u003EThe user\u0027s session may expire or become invalid.\u003C/li\u003E\n\u003Cli\u003EThe target resource may be deleted or modified.\u003C/li\u003E\n\u003Cli\u003EThe resource\u0027s ownership or tenant context may change.\u003C/li\u003E\n\u003Cli\u003EAdditional business rules may prevent the operation.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EThe protected operation must independently enforce authorization using its current context.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003ENever use a previous \u003Ccode\u003ECheckAsync\u003C/code\u003E result as proof of authorization for a later request.\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003ECheckAsync\u003C/code\u003E is primarily a developer-experience and user-experience API. It helps applications understand and present authorization decisions without replacing the security enforcement performed by protected endpoints and application services.\u003C/p\u003E\n\u003Cp\u003EUse \u003Ccode\u003ECheckAsync\u003C/code\u003E to ask the server whether the current user is authorized to perform an action.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.CheckAsync(\n new AuthorizationCheckRequest\n {\n Action = \u0026quot;read\u0026quot;,\n Resource = \u0026quot;products\u0026quot;\n });\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { IsAllowed: true })\n{\n // The server authorized this action.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe method returns:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EUAuthResult\u0026lt;AuthorizationResult\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022authorizationcheckrequest\u0022\u003EAuthorizationCheckRequest\u003C/h3\u003E\n\u003Cp\u003EThe request contains:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EType\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAction\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Estring\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERequested operation\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EResource\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Estring\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EResource being accessed\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EResourceId\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Estring?\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOptional identifier for a specific resource instance\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EBoth \u003Ccode\u003EAction\u003C/code\u003E and \u003Ccode\u003EResource\u003C/code\u003E are required.\u003C/p\u003E\n\u003Cp\u003EThe action and resource values must follow the conventions understood by your server-side authorization configuration.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022check-a-specific-resource\u0022\u003ECheck a Specific Resource\u003C/h3\u003E\n\u003Cp\u003EFor example, check access to a particular product:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.CheckAsync(\n new AuthorizationCheckRequest\n {\n Action = \u0026quot;update\u0026quot;,\n Resource = \u0026quot;products\u0026quot;,\n ResourceId = productId.ToString()\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EProviding \u003Ccode\u003EResourceId\u003C/code\u003E allows the server to evaluate resource-specific rules where supported by the configured authorization pipeline.\u003C/p\u003E\n\u003Cp\u003EFor example, the application may distinguish between updating any product and updating a product owned by the current user.\u003C/p\u003E\n\u003Cp\u003EDo not assume that sending \u003Ccode\u003EResourceId\u003C/code\u003E automatically enables ownership checking. The relevant server-side policy must implement the required rule.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022understanding-authorization-results\u0022\u003E5. Understanding Authorization Results\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003ECheckAsync\u003C/code\u003E returns an \u003Ccode\u003EAuthorizationResult\u003C/code\u003E with the following properties:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIsAllowed\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EWhether the request was authorized\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERequiresReauthentication\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EWhether the decision indicates reauthentication is required\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EDenyReason\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOptional denial reason code\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022authorization-decisions-vs.http-results\u0022\u003EAuthorization Decisions vs. HTTP Results\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003ECheckAsync\u003C/code\u003E separates the outcome of the API request from the authorization decision itself.\u003C/p\u003E\n\u003Cp\u003EThe method returns \u003Ccode\u003EUAuthResult\u0026lt;AuthorizationResult\u0026gt;\u003C/code\u003E:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ccode\u003EUAuthResult.IsSuccess\u003C/code\u003E indicates whether the authorization check request completed successfully.\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003EAuthorizationResult.IsAllowed\u003C/code\u003E indicates whether the requested action is currently authorized.\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003EAuthorizationResult.RequiresReauthentication\u003C/code\u003E indicates whether additional authentication is required.\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003EAuthorizationResult.DenyReason\u003C/code\u003E provides an optional reason for denial.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EA denied authorization decision is a valid result of a successful authorization check.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EScenario\u003C/th\u003E\n\u003Cth\u003EHTTP Status\u003C/th\u003E\n\u003Cth\u003EAuthorization Result\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EAction allowed\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E200 OK\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EIsAllowed = true\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EAction denied\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E200 OK\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EIsAllowed = false\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EReauthentication required\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E200 OK\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003ERequiresReauthentication = true\u003C/code\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ECaller not authenticated\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E401 Unauthorized\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ENo authorization decision\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EInvalid request\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003E400 Bad Request\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ENo authorization decision\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EServer-side evaluation failure\u003C/td\u003E\n\u003Ctd\u003EAppropriate error status\u003C/td\u003E\n\u003Ctd\u003ENo authorization decision\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Cstrong\u003EHTTP 200 means the authorization evaluation completed successfully. It does not mean access was granted.\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EOnly authenticated callers can use the current \u003Ccode\u003ECheckAsync\u003C/code\u003E endpoint. However, callers do not need permission to perform the action they are checking.\u003C/p\u003E\n\u003Cp\u003EThis distinction is intentional: the purpose of \u003Ccode\u003ECheckAsync\u003C/code\u003E is to determine whether the action is permitted.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022handling-denied-decisions\u0022\u003EHandling Denied Decisions\u003C/h3\u003E\n\u003Cp\u003EApplications should distinguish between authorization denial, reauthentication requirements, and request failures.\u003C/p\u003E\n\u003Cp\u003EA denied decision may be used to disable an action or display an appropriate message.\u003C/p\u003E\n\u003Cp\u003EA reauthentication requirement may be used to initiate the application\u0027s supported reauthentication flow.\u003C/p\u003E\n\u003Cp\u003EA failed request must not be interpreted as permission to proceed.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EFail closed:\u003C/strong\u003E If the authorization decision is unavailable, treat the operation as unavailable in the UI and rely on server-side authorization when the actual operation is requested.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022handle-all-decision-outcomes\u0022\u003EHandle All Decision Outcomes\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.CheckAsync(\n new AuthorizationCheckRequest\n {\n Action = \u0026quot;delete\u0026quot;,\n Resource = \u0026quot;products\u0026quot;,\n ResourceId = productId.ToString()\n });\n\nif (!result.IsSuccess)\n{\n // The authorization request itself failed.\n}\nelse if (result.Value is { RequiresReauthentication: true })\n{\n // Ask the user to authenticate again.\n}\nelse if (result.Value is { IsAllowed: true })\n{\n // The operation is allowed.\n}\nelse\n{\n // The operation was denied.\n var reason = result.Value?.DenyReason;\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EAn unsuccessful API request and an explicit authorization denial are different outcomes.\u003C/p\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ccode\u003Eresult.IsSuccess == false\u003C/code\u003E means the authorization request did not complete successfully at the API level.\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003Eresult.Value.IsAllowed == false\u003C/code\u003E means the server returned a denial decision.\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003Eresult.Value.RequiresReauthentication == true\u003C/code\u003E indicates that the caller needs to reestablish authentication.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003ETreat failed or unavailable authorization checks as \u003Cstrong\u003Enot authorized\u003C/strong\u003E in the UI.\u003C/p\u003E\n\u003Cp\u003ENever grant access because an authorization request failed.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022use-authorization-checks-in-blazor\u0022\u003E6. Use Authorization Checks in Blazor\u003C/h2\u003E\n\u003Cp\u003EA common use case is controlling whether an action button should be displayed.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@using CodeBeam.UltimateAuth.Client\n@using CodeBeam.UltimateAuth.Authorization.Contracts\n@inject IUAuthClient UAuthClient\n\n@if (_canCreate)\n{\n \u0026lt;button @onclick=\u0026quot;CreateProductAsync\u0026quot;\u0026gt;Create Product\u0026lt;/button\u0026gt;\n}\n\n@code {\n private bool _canCreate;\n\n protected override async Task OnInitializedAsync()\n {\n var result = await UAuthClient.Authorization.CheckAsync(\n new AuthorizationCheckRequest\n {\n Action = \u0026quot;create\u0026quot;,\n Resource = \u0026quot;products\u0026quot;\n });\n\n _canCreate = result.IsSuccess \u0026amp;\u0026amp; result.Value is { IsAllowed: true };\n }\n\n private Task CreateProductAsync()\n {\n // Invoke the protected application operation.\n return Task.CompletedTask;\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis improves the interface by hiding unavailable actions.\u003C/p\u003E\n\u003Cp\u003EHowever, the endpoint handling product creation must still enforce authorization independently.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022why-client-checks-are-not-security-boundaries\u0022\u003EWhy Client Checks Are Not Security Boundaries\u003C/h3\u003E\n\u003Cp\u003EA user can bypass a hidden button and send a request directly to an endpoint.\u003C/p\u003E\n\u003Cp\u003ETherefore:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003EClient authorization check\n |\n v\nImprove the user experience\n |\n v\nUser submits an operation\n |\n v\nServer independently authorizes the operation\n |\n v\nExecute or deny\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EClient authorization checks should never be the only protection for sensitive operations.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022get-my-roles\u0022\u003E7. Get My Roles\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003EGetMyRolesAsync\u003C/code\u003E to retrieve roles assigned to the current authenticated user.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.GetMyRolesAsync();\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } data)\n{\n foreach (var role in data.Roles.Items)\n {\n Console.WriteLine(role.Name);\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe method returns:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EUAuthResult\u0026lt;UserRolesGetResult\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022userrolesgetresult\u0022\u003EUserRolesGetResult\u003C/h3\u003E\n\u003Cp\u003EThe response contains:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EUserKey\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EUser whose roles were requested\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERoles\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EPaginated collection of role assignments\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EEach entry is a \u003Ccode\u003EUserRoleInfo\u003C/code\u003E.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022userroleinfo\u0022\u003EUserRoleInfo\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ETenant\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ETenant associated with the assignment\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EUserKey\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAssigned user\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERoleId\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAssigned role\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EName\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERole name\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAssignedAt\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAssignment timestamp\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022pagination\u0022\u003EPagination\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.GetMyRolesAsync(\n new PageRequest\n {\n PageNumber = 1,\n PageSize = 20\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe returned role collection is paginated.\u003C/p\u003E\n\u003Cp\u003EDo not assume that the first page contains all assigned roles.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022get-my-permissions\u0022\u003E8. Get My Permissions\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003EGetMyPermissionsAsync()\u003C/code\u003E to retrieve the permissions assigned to the currently authenticated user through their roles.\u003C/p\u003E\n\u003Cp\u003EUnlike \u003Ccode\u003EGetMyRolesAsync()\u003C/code\u003E, which returns role assignments, this method returns the resulting permission collection.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.GetMyPermissionsAsync();\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } data)\n{\n foreach (var permission in data.Permissions)\n {\n Console.WriteLine(permission.Value);\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe method is self-service and does not require administrative permission.\u003C/p\u003E\n\u003Cp\u003EThe user identity and tenant context are derived from the authenticated session. No user identifier or tenant identifier is accepted as a request parameter.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022userpermissionsresult\u0022\u003EUserPermissionsResult\u003C/h3\u003E\n\u003Cp\u003EThe response contains the user\u0027s role-derived permissions:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Epublic sealed record UserPermissionsResult\n{\n public required IReadOnlyList\u0026lt;Permission\u0026gt; Permissions { get; init; }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe returned collection:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003ECombines permissions from the user\u0027s assigned roles.\u003C/li\u003E\n\u003Cli\u003ERemoves duplicate permissions.\u003C/li\u003E\n\u003Cli\u003EUses deterministic ordering.\u003C/li\u003E\n\u003Cli\u003EPreserves wildcard permissions such as \u003Ccode\u003Eusers.*\u003C/code\u003E and \u003Ccode\u003E*\u003C/code\u003E.\u003C/li\u003E\n\u003Cli\u003EReturns an empty collection when no permissions are assigned.\u003C/li\u003E\n\u003Cli\u003EReflects the current permissions resolved from the configured authorization stores.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EFor example, a user assigned multiple roles may receive:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003Ereports.*\nusers.read.admin\nusers.update.admin\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe result does not identify which role contributed each permission.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022assigned-permissions-vs.authorization-decisions\u0022\u003EAssigned Permissions vs. Authorization Decisions\u003C/h3\u003E\n\u003Cp\u003E\u003Cstrong\u003EAssigned permissions are not final authorization decisions.\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EA user may possess a permission but still be denied access because of an applicable policy, tenant boundary, target restriction, or other security requirement.\u003C/p\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar permissions = await UAuthClient.Authorization.GetMyPermissionsAsync();\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis retrieves the user\u0027s assigned permissions.\u003C/p\u003E\n\u003Cp\u003ETo determine whether a specific operation is currently authorized, use \u003Ccode\u003ECheckAsync()\u003C/code\u003E:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.CheckAsync(\n new AuthorizationCheckRequest\n {\n Action = \u0026quot;users.delete.admin\u0026quot;,\n Resource = \u0026quot;users\u0026quot;,\n ResourceId = targetUserKey.Value\n });\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } decision)\n{\n if (decision.IsAllowed)\n {\n // The authorization check allowed this operation.\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EEven a successful client-side authorization check does not replace authorization enforcement at the server-side operation boundary.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022wildcard-permissions\u0022\u003EWildcard Permissions\u003C/h3\u003E\n\u003Cp\u003EWildcard permissions are returned without expansion.\u003C/p\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003Eusers.*\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003Eremains \u003Ccode\u003Eusers.*\u003C/code\u003E rather than being expanded into individual user-management actions.\u003C/p\u003E\n\u003Cp\u003EThis preserves the stored permission semantics and avoids depending on a potentially incomplete action catalog.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022security-considerations\u0022\u003ESecurity Considerations\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003EGetMyPermissionsAsync()\u003C/code\u003E is intended for permission inspection, user interfaces, and administrative transparency.\u003C/p\u003E\n\u003Cp\u003EIt must not be used as the sole mechanism for protecting sensitive operations.\u003C/p\u003E\n\u003Cp\u003EThe server remains responsible for evaluating authorization policies and enforcing access at the actual execution boundary.\u003C/p\u003E\n\u003Cp\u003ESee \u003Ca href=\u0022/docs/client-sdk/authorization#4-check-authorization\u0022\u003ECheck Authorization\u003C/a\u003E for policy-aware authorization checks.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022get-another-users-roles\u0022\u003E9. Get Another User\u0027s Roles\u003C/h2\u003E\n\u003Cp\u003EAdministrative interfaces can retrieve the roles assigned to a specific user.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.GetUserRolesAsync(userKey,\n new PageRequest\n {\n PageNumber = 1,\n PageSize = 20\n });\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } data)\n{\n foreach (var role in data.Roles.Items)\n {\n Console.WriteLine($\u0026quot;{role.Name} \u2014 {role.AssignedAt}\u0026quot;);\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis operation requires appropriate administrative authorization.\u003C/p\u003E\n\u003Cp\u003EThe \u003Ccode\u003EUserKey\u003C/code\u003E identifies the target user, not the current actor.\u003C/p\u003E\n\u003Cp\u003EThe server determines whether the current actor may inspect that user\u0027s role assignments.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022query-roles\u0022\u003E10. Query Roles\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003EQueryRolesAsync\u003C/code\u003E to retrieve roles with pagination and optional filtering.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.QueryRolesAsync(\n new RoleQuery\n {\n PageNumber = 1,\n PageSize = 20\n });\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } page)\n{\n foreach (var role in page.Items)\n {\n Console.WriteLine(role.Name);\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe method returns:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EUAuthResult\u0026lt;PagedResult\u0026lt;RoleInfo\u0026gt;\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022rolequery\u0022\u003ERoleQuery\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003ERoleQuery\u003C/code\u003E inherits from \u003Ccode\u003EPageRequest\u003C/code\u003E and adds:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ESearch\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EOptional role-name search\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EIncludeDeleted\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EInclude deleted roles when supported and authorized\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPageNumber\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERequested page\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPageSize\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERequested page size\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022search-roles\u0022\u003ESearch Roles\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.QueryRolesAsync(\n new RoleQuery\n {\n Search = \u0026quot;manager\u0026quot;,\n PageNumber = 1,\n PageSize = 10\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022include-deleted-roles\u0022\u003EInclude Deleted Roles\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.QueryRolesAsync(\n new RoleQuery\n {\n IncludeDeleted = true,\n PageNumber = 1,\n PageSize = 20\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EUse \u003Ccode\u003EIncludeDeleted\u003C/code\u003E only where the application needs administrative access to deleted role records.\u003C/p\u003E\n\u003Cp\u003EThe server remains responsible for enforcing the query\u0027s permissions and lifecycle rules.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022understanding-roleinfo\u0022\u003E11. Understanding RoleInfo\u003C/h2\u003E\n\u003Cp\u003EEach role returned by the SDK is represented by \u003Ccode\u003ERoleInfo\u003C/code\u003E.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EType\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EId\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003ERoleId\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERole identifier\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EName\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003Estring\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERole name\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPermissions\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EIReadOnlyCollection\u0026lt;Permission\u0026gt;\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EAssigned permissions\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECreatedAt\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EDateTimeOffset\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ECreation timestamp\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EUpdatedAt\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Ccode\u003EDateTimeOffset?\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ELast update timestamp\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.QueryRolesAsync(\n new RoleQuery\n {\n PageNumber = 1,\n PageSize = 20\n });\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } page)\n{\n foreach (var role in page.Items)\n {\n Console.WriteLine($\u0026quot;Role: {role.Name}\u0026quot;);\n\n foreach (var permission in role.Permissions)\n {\n Console.WriteLine($\u0026quot; {permission.Value}\u0026quot;);\n }\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003ERoleId\u003C/code\u003E is a strongly typed identifier rather than a plain \u003Ccode\u003EGuid\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003ERoleId roleId = RoleId.From(existingGuid);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EUse \u003Ccode\u003ERoleId\u003C/code\u003E for role management requests.\u003C/p\u003E\n\u003Cp\u003EDo not confuse it with \u003Ccode\u003EUserKey\u003C/code\u003E or other identifier types.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022create-a-role\u0022\u003E12. Create a Role\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003ECreateRoleAsync\u003C/code\u003E to create a role.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.CreateRoleAsync(\n new CreateRoleRequest\n {\n Name = \u0026quot;Manager\u0026quot;\n });\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } role)\n{\n Console.WriteLine($\u0026quot;Created: {role.Name}\u0026quot;);\n Console.WriteLine($\u0026quot;Role ID: {role.Id}\u0026quot;);\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe method returns:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EUAuthResult\u0026lt;RoleInfo\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003ERole names must be unique within the relevant tenant according to the server\u0027s role-name rules.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022create-a-role-with-permissions\u0022\u003ECreate a Role with Permissions\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003ECreateRoleRequest\u003C/code\u003E supports an optional initial permission collection.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.CreateRoleAsync(\n new CreateRoleRequest\n {\n Name = \u0026quot;ProductManager\u0026quot;,\n Permissions =\n [\n Permission.From(\u0026quot;products.read\u0026quot;),\n Permission.From(\u0026quot;products.create\u0026quot;),\n Permission.From(\u0026quot;products.update\u0026quot;)\n ]\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis allows applications to create and configure a role in one operation.\u003C/p\u003E\n\u003Cp\u003EThe server validates the requested permissions and whether the caller is authorized to create the role.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022create-a-read-only-role\u0022\u003ECreate a Read-Only Role\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.CreateRoleAsync(\n new CreateRoleRequest\n {\n Name = \u0026quot;Auditor\u0026quot;,\n Permissions =\n [\n Permission.From(\u0026quot;reports.read\u0026quot;),\n Permission.From(\u0026quot;products.read\u0026quot;)\n ]\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EUse role names that describe the responsibility or capability represented by the role.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022rename-a-role\u0022\u003E13. Rename a Role\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003ERenameRoleAsync\u003C/code\u003E to change a role\u0027s name.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.RenameRoleAsync(\n new RenameRoleRequest\n {\n Id = roleId,\n Name = \u0026quot;SeniorManager\u0026quot;\n });\n\nif (result.IsSuccess)\n{\n // Role renamed.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Ccode\u003ERenameRoleRequest\u003C/code\u003E requires:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ccode\u003EId\u003C/code\u003E: The role\u0027s \u003Ccode\u003ERoleId\u003C/code\u003E.\u003C/li\u003E\n\u003Cli\u003E\u003Ccode\u003EName\u003C/code\u003E: The new role name.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EThe operation may fail if another role already uses the requested name.\u003C/p\u003E\n\u003Cp\u003ERole renaming does not mean that permissions are automatically changed.\u003C/p\u003E\n\u003Cp\u003EAfter a successful rename, the SDK publishes an \u003Ccode\u003EAuthorizationChanged\u003C/code\u003E state event.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022set-role-permissions\u0022\u003E14. Set Role Permissions\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003ESetRolePermissionsAsync\u003C/code\u003E to configure a role\u0027s permissions.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.SetRolePermissionsAsync(\n new SetRolePermissionsRequest\n {\n RoleId = roleId,\n Permissions =\n [\n Permission.From(\u0026quot;products.read\u0026quot;),\n Permission.From(\u0026quot;products.create\u0026quot;),\n Permission.From(\u0026quot;products.update\u0026quot;)\n ]\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe request contains:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERoleId\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ERole to modify\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EPermissions\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ENew permission collection\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022important-this-is-a-replacement-operation\u0022\u003EImportant: This Is a Replacement Operation\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003ESetRolePermissionsAsync\u003C/code\u003E replaces the role\u0027s existing permission collection.\u003C/p\u003E\n\u003Cp\u003EIt does not append permissions to the current collection.\u003C/p\u003E\n\u003Cp\u003EFor example, suppose a role currently contains:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003Eproducts.read\nproducts.create\nproducts.update\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003ESubmitting:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EPermissions =\n[\n Permission.From(\u0026quot;products.read\u0026quot;)\n]\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003Emeans the intended new permission collection contains only:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003Eproducts.read\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe other permissions are removed from the role.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022safely-add-a-permission-to-an-existing-role\u0022\u003ESafely Add a Permission to an Existing Role\u003C/h3\u003E\n\u003Cp\u003ETo preserve existing permissions, first retrieve the role and construct the complete desired collection.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar query = await UAuthClient.Authorization.QueryRolesAsync(\n new RoleQuery\n {\n Search = \u0026quot;ProductManager\u0026quot;,\n PageNumber = 1,\n PageSize = 50\n });\n\nif (query.IsSuccess \u0026amp;\u0026amp; query.Value is { } page)\n{\n var role = page.Items.FirstOrDefault(x =\u0026gt; x.Name == \u0026quot;ProductManager\u0026quot;);\n\n if (role is not null)\n {\n var permissions = role.Permissions.Append(Permission.From(\u0026quot;products.delete\u0026quot;)).Distinct().ToArray();\n\n var result =\n await UAuthClient.Authorization.SetRolePermissionsAsync(\n new SetRolePermissionsRequest\n {\n RoleId = role.Id,\n Permissions = permissions\n });\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis is a read-modify-write pattern, not an atomic append operation.\u003C/p\u003E\n\u003Cp\u003EIf multiple administrators edit the same role concurrently, the last successful replacement may overwrite another administrator\u0027s changes.\u003C/p\u003E\n\u003Cp\u003EApplications requiring concurrency-safe editing should implement suitable coordination or version checking at the application level.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022remove-a-permission\u0022\u003ERemove a Permission\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar updatedPermissions = role.Permissions.Where(x =\u0026gt; x.Value != \u0026quot;products.delete\u0026quot;).ToArray();\n\nvar result = await UAuthClient.Authorization.SetRolePermissionsAsync(\n new SetRolePermissionsRequest\n {\n RoleId = role.Id,\n Permissions = updatedPermissions\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EAgain, submit the complete desired permission set.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022clear-all-permissions\u0022\u003EClear All Permissions\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.SetRolePermissionsAsync(\n new SetRolePermissionsRequest\n {\n RoleId = roleId,\n Permissions = []\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EAn empty collection requests that the role have no permissions.\u003C/p\u003E\n\u003Cp\u003EThe server determines whether the operation is permitted.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022assign-a-role-to-a-user\u0022\u003E15. Assign a Role to a User\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003EAssignRoleToUserAsync\u003C/code\u003E to assign an existing role to a user.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.AssignRoleToUserAsync(\n new AssignRoleRequest\n {\n UserKey = userKey,\n RoleName = \u0026quot;Manager\u0026quot;\n });\n\nif (result.IsSuccess)\n{\n // Role assigned.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe request contains:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EUserKey\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ETarget user\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERoleName\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EExisting role name\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThe role must exist, and the current actor must be authorized to assign it.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022assign-multiple-roles\u0022\u003EAssign Multiple Roles\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eforeach (var roleName in new[] { \u0026quot;Manager\u0026quot;, \u0026quot;Auditor\u0026quot; })\n{\n var result = await UAuthClient.Authorization.AssignRoleToUserAsync(\n new AssignRoleRequest\n {\n UserKey = userKey,\n RoleName = roleName\n });\n\n if (!result.IsSuccess)\n {\n // Handle the failed assignment.\n break;\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EEach call is a separate operation.\u003C/p\u003E\n\u003Cp\u003EThis example does not provide transactional, all-or-nothing behavior across the entire collection.\u003C/p\u003E\n\u003Cp\u003EFor complex provisioning workflows, consider whether the application needs a server-side orchestration operation.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022remove-a-role-from-a-user\u0022\u003E16. Remove a Role from a User\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003ERemoveRoleFromUserAsync\u003C/code\u003E:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.RemoveRoleFromUserAsync(\n new RemoveRoleRequest\n {\n UserKey = userKey,\n RoleName = \u0026quot;Manager\u0026quot;\n });\n\nif (result.IsSuccess)\n{\n // Role removed.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003ERemoving a role changes the user\u0027s role assignments.\u003C/p\u003E\n\u003Cp\u003EIt does not delete the role itself.\u003C/p\u003E\n\u003Cp\u003EOther users assigned to that role are unaffected by this particular operation.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022important-effective-permissions-may-remain\u0022\u003EImportant: Effective Permissions May Remain\u003C/h3\u003E\n\u003Cp\u003EA user may receive the same permission through multiple roles.\u003C/p\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003EUser\n |\n \u002B-- Manager\n | \u002B-- products.read\n |\n \u002B-- Auditor\n \u002B-- products.read\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003ERemoving \u003Ccode\u003EManager\u003C/code\u003E does not necessarily remove the user\u0027s ability to read products because \u003Ccode\u003EAuditor\u003C/code\u003E still provides \u003Ccode\u003Eproducts.read\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003ETo determine whether the user may perform an action, rely on the server\u0027s authorization decision rather than inspecting one role assignment in isolation.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022delete-a-role\u0022\u003E17. Delete a Role\u003C/h2\u003E\n\u003Cp\u003EUse \u003Ccode\u003EDeleteRoleAsync\u003C/code\u003E to delete a role.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.DeleteRoleAsync(\n new DeleteRoleRequest\n {\n Id = roleId,\n Mode = DeleteMode.Soft\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe request supports a deletion mode.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022hard-delete\u0022\u003EHard Delete\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.DeleteRoleAsync(\n new DeleteRoleRequest\n {\n Id = roleId,\n Mode = DeleteMode.Hard\n });\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe availability and effect of each deletion mode are determined by server-side lifecycle rules.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022roledeleteresult\u0022\u003ERoleDeleteResult\u003C/h3\u003E\n\u003Cp\u003EThe method returns:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EUAuthResult\u0026lt;RoleDeleteResult\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe response contains:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EProperty\u003C/th\u003E\n\u003Cth\u003EDescription\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERoleId\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EDeleted role\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERemovedAssignments\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ENumber of user-role assignments removed\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EMode\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EApplied deletion mode\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EDeletedAt\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EDeletion timestamp\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022inspect-the-result\u0022\u003EInspect the Result\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.DeleteRoleAsync(\n new DeleteRoleRequest\n {\n Id = roleId,\n Mode = DeleteMode.Soft\n });\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } deleted)\n{\n Console.WriteLine($\u0026quot;Removed assignments: {deleted.RemovedAssignments}\u0026quot;);\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EDeleting a role removes its assignments from users.\u003C/p\u003E\n\u003Cp\u003EThis can affect multiple users at once.\u003C/p\u003E\n\u003Cp\u003EFor that reason, role deletion should be presented as a sensitive administrative operation with appropriate confirmation.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022build-a-role-management-page\u0022\u003E18. Build a Role Management Page\u003C/h2\u003E\n\u003Cp\u003EThe following Blazor example demonstrates querying roles, creating a role, and deleting a selected role.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003ERazor\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-razor\u0022\u003E@using CodeBeam.UltimateAuth.Client\n@using CodeBeam.UltimateAuth.Authorization.Contracts\n@using CodeBeam.UltimateAuth.Core.Contracts\n@inject IUAuthClient UAuthClient\n\n\u0026lt;h3\u0026gt;Role Management\u0026lt;/h3\u0026gt;\n\n\u0026lt;input @bind=\u0026quot;_search\u0026quot; placeholder=\u0026quot;Search roles\u0026quot; /\u0026gt;\n\n\u0026lt;button @onclick=\u0026quot;LoadRolesAsync\u0026quot;\u0026gt;Search\u0026lt;/button\u0026gt;\n\n@if (_loading)\n{\n \u0026lt;p\u0026gt;Loading roles...\u0026lt;/p\u0026gt;\n}\nelse\n{\n @foreach (var role in _roles)\n {\n \u0026lt;div @key=\u0026quot;role.Id\u0026quot;\u0026gt;\n \u0026lt;strong\u0026gt;@role.Name\u0026lt;/strong\u0026gt;\n \u0026lt;span\u0026gt;@role.Permissions.Count permissions\u0026lt;/span\u0026gt;\n \u0026lt;button @onclick=\u0026quot;() =\u0026gt; DeleteRoleAsync(role.Id)\u0026quot;\u0026gt;Delete\u0026lt;/button\u0026gt;\n \u0026lt;/div\u0026gt;\n }\n}\n\n\u0026lt;h4\u0026gt;Create Role\u0026lt;/h4\u0026gt;\n\u0026lt;input @bind=\u0026quot;_newRoleName\u0026quot; placeholder=\u0026quot;Role name\u0026quot; /\u0026gt;\n\u0026lt;button @onclick=\u0026quot;CreateRoleAsync\u0026quot; disabled=\u0026quot;@_busy\u0026quot;\u0026gt;Create\u0026lt;/button\u0026gt;\n\n@if (_message is not null)\n{\n \u0026lt;p role=\u0026quot;status\u0026quot;\u0026gt;@_message\u0026lt;/p\u0026gt;\n}\n\n@code {\n private IReadOnlyList\u0026lt;RoleInfo\u0026gt; _roles = [];\n\n private string _search = \u0026quot;\u0026quot;;\n private string _newRoleName = \u0026quot;\u0026quot;;\n private string? _message;\n\n private bool _loading = true;\n private bool _busy;\n\n protected override async Task OnInitializedAsync()\n {\n await LoadRolesAsync();\n }\n\n private async Task LoadRolesAsync()\n {\n _loading = true;\n\n try\n {\n var result = await UAuthClient.Authorization.QueryRolesAsync(\n new RoleQuery\n {\n Search = _search,\n PageNumber = 1,\n PageSize = 50\n });\n\n if (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } page)\n {\n _roles = page.Items;\n }\n else\n {\n _message = \u0026quot;Unable to load roles.\u0026quot;;\n }\n }\n catch (Exception)\n {\n _message = \u0026quot;The authorization service is unavailable.\u0026quot;;\n }\n finally\n {\n _loading = false;\n }\n }\n\n private async Task CreateRoleAsync()\n {\n if (_busy || string.IsNullOrWhiteSpace(_newRoleName))\n return;\n\n _busy = true;\n _message = null;\n\n try\n {\n var result = await UAuthClient.Authorization.CreateRoleAsync(\n new CreateRoleRequest\n {\n Name = _newRoleName\n });\n\n if (result.IsSuccess)\n {\n _newRoleName = \u0026quot;\u0026quot;;\n await LoadRolesAsync();\n }\n else\n {\n _message = \u0026quot;Unable to create role.\u0026quot;;\n }\n }\n finally\n {\n _busy = false;\n }\n }\n\n private async Task DeleteRoleAsync(RoleId roleId)\n {\n // Add a confirmation dialog before deletion in production.\n\n var result = await UAuthClient.Authorization.DeleteRoleAsync(\n new DeleteRoleRequest\n {\n Id = roleId,\n Mode = DeleteMode.Soft\n });\n\n if (result.IsSuccess)\n {\n await LoadRolesAsync();\n }\n else\n {\n _message = \u0026quot;Unable to delete role.\u0026quot;;\n }\n }\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis example demonstrates the SDK integration rather than a complete production administration interface.\u003C/p\u003E\n\u003Cp\u003EA production implementation should include pagination, role assignment controls, permission editing, deletion confirmation, and appropriate loading and error handling for every operation.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022build-a-user-role-assignment-interface\u0022\u003E19. Build a User Role Assignment Interface\u003C/h2\u003E\n\u003Cp\u003EAn administrative user details page can display assigned roles and allow administrators to add or remove them.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022load-user-roles\u0022\u003ELoad User Roles\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.GetUserRolesAsync(userKey,\n new PageRequest\n {\n PageNumber = 1,\n PageSize = 50\n });\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { } data)\n{\n var assignedRoles = data.Roles.Items.ToList();\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022assign-a-selected-role\u0022\u003EAssign a Selected Role\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.AssignRoleToUserAsync(\n new AssignRoleRequest\n {\n UserKey = userKey,\n RoleName = selectedRoleName\n });\n\nif (result.IsSuccess)\n{\n // Reload the user\u0027s assigned roles.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022remove-a-selected-role\u0022\u003ERemove a Selected Role\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.RemoveRoleFromUserAsync(\n new RemoveRoleRequest\n {\n UserKey = userKey,\n RoleName = selectedRoleName\n });\n\nif (result.IsSuccess)\n{\n // Reload the user\u0027s assigned roles.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis pattern works well alongside \u003Ccode\u003EUAuthClient.Users\u003C/code\u003E for building a unified user administration interface.\u003C/p\u003E\n\u003Cp\u003ESee \u003Ca href=\u0022/docs/client/user-management\u0022\u003EUser Management\u003C/a\u003E.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022authorization-state-events\u0022\u003E20. Authorization State Events\u003C/h2\u003E\n\u003Cp\u003EThe current \u003Ccode\u003EUAuthAuthorizationClient\u003C/code\u003E publishes:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EUAuthStateEvent.AuthorizationChanged\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003Eafter successful authorization mutations.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EOperation\u003C/th\u003E\n\u003Cth\u003EPublishes \u003Ccode\u003EAuthorizationChanged\u003C/code\u003E\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EAssignRoleToUserAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EYes\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERemoveRoleFromUserAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EYes\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ERenameRoleAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EYes\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ESetRolePermissionsAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EYes\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EDeleteRoleAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EYes\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECreateRoleAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ENo\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EQueryRolesAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ENo\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EGetMyRolesAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ENo\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003EGetUserRolesAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ENo\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003ECheckAsync\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003ENo\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThe event follows the configured client state event handling mode.\u003C/p\u003E\n\u003Cp\u003EIt can be used to refresh navigation menus, role assignment views, or permission-sensitive UI.\u003C/p\u003E\n\u003Cp\u003EHowever, it is a \u003Cstrong\u003Elocal client event\u003C/strong\u003E, not a guarantee that all active clients or distributed caches have immediately observed the change.\u003C/p\u003E\n\u003Cp\u003EFor example, an administrator assigning a role to another user does not automatically mean that user\u0027s open browser receives the administrator\u0027s local event.\u003C/p\u003E\n\u003Cp\u003EApplications requiring cross-client updates should provide an appropriate synchronization mechanism.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022authorization-changes-and-active-sessions\u0022\u003E21. Authorization Changes and Active Sessions\u003C/h2\u003E\n\u003Cp\u003ERole and permission changes may affect active users.\u003C/p\u003E\n\u003Cp\u003EHowever, the timing of those changes depends on how authorization data is resolved and cached by the server.\u003C/p\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Col\u003E\n\u003Cli\u003EAn administrator removes a role.\u003C/li\u003E\n\u003Cli\u003EThe server persists the new role assignment state.\u003C/li\u003E\n\u003Cli\u003EA subsequent authorization evaluation uses the effective authorization state available to the server.\u003C/li\u003E\n\u003C/ol\u003E\n\u003Cp\u003EIf the application caches authorization information, changes may not become visible everywhere immediately.\u003C/p\u003E\n\u003Cp\u003EDo not assume that a client event invalidates every active session or refreshes every distributed authorization cache.\u003C/p\u003E\n\u003Cp\u003EFor sensitive operations, authorization should be evaluated against sufficiently current server-side state.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022roles-are-not-sessions\u0022\u003ERoles Are Not Sessions\u003C/h3\u003E\n\u003Cp\u003ERemoving a role and revoking a session are different operations.\u003C/p\u003E\n\u003Cp\u003ERemoving a role changes authorization data.\u003C/p\u003E\n\u003Cp\u003ERevoking a session changes whether an authentication session remains valid.\u003C/p\u003E\n\u003Cp\u003EApplications may combine these operations when security policy requires immediate termination of access, but the concepts should remain separate.\u003C/p\u003E\n\u003Cp\u003ESee \u003Ca href=\u0022/docs/client/session-management\u0022\u003ESession Management\u003C/a\u003E.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022multi-tenant-authorization\u0022\u003E22. Multi-Tenant Authorization\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth authorization operations are scoped to the current tenant context.\u003C/p\u003E\n\u003Cp\u003ERole names are unique within the relevant tenant, and role assignments belong to the applicable tenant.\u003C/p\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003ETenant A\n |\n \u002B-- Manager\n | \u002B-- products.read\n |\n \u002B-- User A\n\nTenant B\n |\n \u002B-- Manager\n | \u002B-- reports.read\n |\n \u002B-- User B\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe same role name may represent different permission sets in different tenants.\u003C/p\u003E\n\u003Cp\u003EThe client does not gain cross-tenant access merely by providing another user\u0027s \u003Ccode\u003EUserKey\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EThe server must enforce tenant isolation and administrative authorization.\u003C/p\u003E\n\u003Cp\u003EWhen implementing multi-tenant administration, do not assume role names or permission configurations are globally shared.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022handling-authorization-errors\u0022\u003E23. Handling Authorization Errors\u003C/h2\u003E\n\u003Cp\u003EAuthorization methods return \u003Ccode\u003EUAuthResult\u003C/code\u003E or \u003Ccode\u003EUAuthResult\u0026lt;T\u0026gt;\u003C/code\u003E.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022handle-role-creation-errors\u0022\u003EHandle Role Creation Errors\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.CreateRoleAsync(\n new CreateRoleRequest\n {\n Name = \u0026quot;Manager\u0026quot;\n });\n\nif (result.IsSuccess)\n{\n // Role created.\n}\nelse if (result.Http.IsConflict)\n{\n // A conflicting role may already exist.\n}\nelse if (result.Http.IsForbidden)\n{\n // Caller lacks permission.\n}\nelse if (result.Http.IsBadRequest)\n{\n // Invalid request.\n}\nelse\n{\n // Handle another failure.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022handle-authorization-check-failures\u0022\u003EHandle Authorization Check Failures\u003C/h3\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.CheckAsync(\n new AuthorizationCheckRequest\n {\n Action = \u0026quot;update\u0026quot;,\n Resource = \u0026quot;products\u0026quot;\n });\n\nvar allowed = result.IsSuccess \u0026amp;\u0026amp; result.Value is { IsAllowed: true };\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis fail-closed pattern is appropriate when determining whether the UI should expose a protected operation.\u003C/p\u003E\n\u003Cp\u003EUse the server\u0027s structured error information for diagnostics where appropriate.\u003C/p\u003E\n\u003Cp\u003EDo not expose sensitive policy internals unnecessarily.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022security-considerations-1\u0022\u003E24. Security Considerations\u003C/h2\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022server-authorization-is-authoritative\u0022\u003EServer Authorization Is Authoritative\u003C/h3\u003E\n\u003Cp\u003EClient-side checks improve usability but cannot protect an endpoint by themselves.\u003C/p\u003E\n\u003Cp\u003EEvery protected operation must enforce authorization on the server.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022follow-least-privilege\u0022\u003EFollow Least Privilege\u003C/h3\u003E\n\u003Cp\u003EAssign only the permissions a role actually needs.\u003C/p\u003E\n\u003Cp\u003EAvoid using \u003Ccode\u003E*\u003C/code\u003E or broad prefix wildcards for ordinary users.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022protect-administrative-operations\u0022\u003EProtect Administrative Operations\u003C/h3\u003E\n\u003Cp\u003ERole creation, permission replacement, role assignment, and role deletion are privileged operations.\u003C/p\u003E\n\u003Cp\u003EThe server must authorize each mutation independently.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022treat-permission-replacement-carefully\u0022\u003ETreat Permission Replacement Carefully\u003C/h3\u003E\n\u003Cp\u003E\u003Ccode\u003ESetRolePermissionsAsync\u003C/code\u003E replaces the entire permission collection.\u003C/p\u003E\n\u003Cp\u003EAlways inspect the existing role configuration before submitting changes.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022handle-reauthentication-requirements\u0022\u003EHandle Reauthentication Requirements\u003C/h3\u003E\n\u003Cp\u003EAn authorization decision can indicate \u003Ccode\u003ERequiresReauthentication\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EApplications should handle this state explicitly rather than treating it as an ordinary permission denial.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022respect-tenant-boundaries\u0022\u003ERespect Tenant Boundaries\u003C/h3\u003E\n\u003Cp\u003ERoles and assignments must remain within their authorized tenant context.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022do-not-infer-effective-access-from-one-role\u0022\u003EDo Not Infer Effective Access from One Role\u003C/h3\u003E\n\u003Cp\u003EA user can have multiple roles, overlapping permissions, and additional policy restrictions.\u003C/p\u003E\n\u003Cp\u003EUse the server\u0027s authorization decision when determining whether an action is permitted.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022account-for-cached-authorization-state\u0022\u003EAccount for Cached Authorization State\u003C/h3\u003E\n\u003Cp\u003EPermission changes may not propagate instantly to every active client or cached authorization context.\u003C/p\u003E\n\u003Cp\u003EDesign sensitive operations around server-side enforcement and appropriate cache invalidation.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022putting-everything-together\u0022\u003E25. Putting Everything Together\u003C/h2\u003E\n\u003Cp\u003EA typical administrative authorization workflow may look like this:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EText\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-text\u0022\u003ECreate role\n |\n v\nConfigure permissions\n |\n v\nAssign role to user\n |\n v\nUser requests protected action\n |\n v\nServer evaluates authorization\n |\n \u002B-- Allow --\u0026gt; Execute operation\n |\n \u002B-- Deny --\u0026gt; Reject operation\n |\n \u002B-- Reauthentication required\n |\n v\n Authenticate again\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EUltimateAuth separates role management from authorization enforcement.\u003C/p\u003E\n\u003Cp\u003EThis allows applications to provide flexible administrative interfaces while keeping access decisions under server control.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022next-steps\u0022\u003ENext Steps\u003C/h2\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/plugin-domains/policies-access-control\u0022\u003EPolicies \u0026amp; Access Control\u003C/a\u003E \u2014 Understand application policies and access rules.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/security/authorization-pipeline-internals\u0022\u003EAuthorization Pipeline Internals\u003C/a\u003E \u2014 Explore how authorization decisions are evaluated.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/client/user-management\u0022\u003EUser Management\u003C/a\u003E \u2014 Manage user accounts and lifecycle.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/client/session-management\u0022\u003ESession Management\u003C/a\u003E \u2014 Manage authenticated sessions and devices.\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/security/session-security\u0022\u003ESession Security Model\u003C/a\u003E \u2014 Understand session validity and security boundaries.\u003C/li\u003E\n\u003C/ul\u003E\n", "Headings": [ { "Id": "understanding-authorization", @@ -108,14 +108,39 @@ "Text": "Pagination", "Level": 1 }, + { + "Id": "get-my-permissions", + "Text": "8. Get My Permissions", + "Level": 0 + }, + { + "Id": "userpermissionsresult", + "Text": "UserPermissionsResult", + "Level": 1 + }, + { + "Id": "assigned-permissions-vs.authorization-decisions", + "Text": "Assigned Permissions vs. Authorization Decisions", + "Level": 1 + }, + { + "Id": "wildcard-permissions", + "Text": "Wildcard Permissions", + "Level": 1 + }, + { + "Id": "security-considerations", + "Text": "Security Considerations", + "Level": 1 + }, { "Id": "get-another-users-roles", - "Text": "8. Get Another User\u0027s Roles", + "Text": "9. Get Another User\u0027s Roles", "Level": 0 }, { "Id": "query-roles", - "Text": "9. Query Roles", + "Text": "10. Query Roles", "Level": 0 }, { @@ -135,12 +160,12 @@ }, { "Id": "understanding-roleinfo", - "Text": "10. Understanding RoleInfo", + "Text": "11. Understanding RoleInfo", "Level": 0 }, { "Id": "create-a-role", - "Text": "11. Create a Role", + "Text": "12. Create a Role", "Level": 0 }, { @@ -155,12 +180,12 @@ }, { "Id": "rename-a-role", - "Text": "12. Rename a Role", + "Text": "13. Rename a Role", "Level": 0 }, { "Id": "set-role-permissions", - "Text": "13. Set Role Permissions", + "Text": "14. Set Role Permissions", "Level": 0 }, { @@ -185,7 +210,7 @@ }, { "Id": "assign-a-role-to-a-user", - "Text": "14. Assign a Role to a User", + "Text": "15. Assign a Role to a User", "Level": 0 }, { @@ -195,7 +220,7 @@ }, { "Id": "remove-a-role-from-a-user", - "Text": "15. Remove a Role from a User", + "Text": "16. Remove a Role from a User", "Level": 0 }, { @@ -205,7 +230,7 @@ }, { "Id": "delete-a-role", - "Text": "16. Delete a Role", + "Text": "17. Delete a Role", "Level": 0 }, { @@ -225,12 +250,12 @@ }, { "Id": "build-a-role-management-page", - "Text": "17. Build a Role Management Page", + "Text": "18. Build a Role Management Page", "Level": 0 }, { "Id": "build-a-user-role-assignment-interface", - "Text": "18. Build a User Role Assignment Interface", + "Text": "19. Build a User Role Assignment Interface", "Level": 0 }, { @@ -250,12 +275,12 @@ }, { "Id": "authorization-state-events", - "Text": "19. Authorization State Events", + "Text": "20. Authorization State Events", "Level": 0 }, { "Id": "authorization-changes-and-active-sessions", - "Text": "20. Authorization Changes and Active Sessions", + "Text": "21. Authorization Changes and Active Sessions", "Level": 0 }, { @@ -265,12 +290,12 @@ }, { "Id": "multi-tenant-authorization", - "Text": "21. Multi-Tenant Authorization", + "Text": "22. Multi-Tenant Authorization", "Level": 0 }, { "Id": "handling-authorization-errors", - "Text": "22. Handling Authorization Errors", + "Text": "23. Handling Authorization Errors", "Level": 0 }, { @@ -284,8 +309,8 @@ "Level": 1 }, { - "Id": "security-considerations", - "Text": "23. Security Considerations", + "Id": "security-considerations-1", + "Text": "24. Security Considerations", "Level": 0 }, { @@ -330,7 +355,7 @@ }, { "Id": "putting-everything-together", - "Text": "24. Putting Everything Together", + "Text": "25. Putting Everything Together", "Level": 0 }, { @@ -339,6 +364,6 @@ "Level": 0 } ], - "SourceHash": "D8570DD540FA723868466A82B7E1871265626B449FC5C1E0DD6BD02246282C53", + "SourceHash": "3304F9F304C1A8D3DFDE25BB8369FD2C1265457FAAB5A975F2662FC94FA1FB6D", "BuilderVersion": "1" } \ No newline at end of file