From 056e649284277187f37a77d40944ddd92b1a55c4 Mon Sep 17 00:00:00 2001 From: Alex Redding Date: Thu, 9 May 2024 17:51:18 -0500 Subject: [PATCH] feat: Convert RolesClaimTransformationSource to flags (#97) * feat: Convert RolesClaimTransformationSource to flags to allow for combining sources, add "All" flag for convenience --- .../configuration-authorization.md | 27 +++++++--- .../KeycloakRolesClaimsTransformation.cs | 8 +-- .../KeycloakAuthorizationOptions.cs | 12 +++-- .../KeycloakRolesClaimsTransformationTests.cs | 53 +++++++++++++++++-- 4 files changed, 81 insertions(+), 19 deletions(-) diff --git a/docs/configuration/configuration-authorization.md b/docs/configuration/configuration-authorization.md index a260a0d4..8bf5067f 100644 --- a/docs/configuration/configuration-authorization.md +++ b/docs/configuration/configuration-authorization.md @@ -61,22 +61,28 @@ Here an example of how to configure client role: There are three options to determine a source for the roles: ```csharp +[Flags] public enum RolesClaimTransformationSource { /// - /// No Transformation. Default + /// Specifies that no transformation should be applied from the source. /// - None, + None = 0, /// - /// Use realm roles as source + /// Specifies that transformation should be applied to the realm. /// - Realm, + Realm = 1 << 0, /// - /// Use client roles as source + /// Specifies that transformation should be applied to the resource access. /// - ResourceAccess + ResourceAccess = 1 << 1, + + /// + /// Specifies that transformation should be applied to all sources. + /// + All = Realm | ResourceAccess } ``` @@ -135,4 +141,13 @@ If we specify `KeycloakAuthorizationOptions.EnableRolesMapping = RolesClaimTrans Result = ["manage-account","manage-account-links","view-profile"] +See below the table for possible combinations: + +| EnableRolesMapping | RolesResource | Result | +|--------------------|---------------|----------------------------------------------------------------------------------------------------------------------| +| Realm | N/A | `["default-roles-test","offline_access","uma_authorization"]` | +| ResourceAccess | `test-client` | `["manage-account","manage-account-links","view-profile"]` | +| All | `test-client` | `["default-roles-test","offline_access","uma_authorization","manage-account","manage-account-links","view-profile"]` | + + The target claim can be configured `KeycloakAuthorizationOptions.RoleClaimType`, the default value is "role". diff --git a/src/Keycloak.AuthServices.Authorization/Claims/KeycloakRolesClaimsTransformation.cs b/src/Keycloak.AuthServices.Authorization/Claims/KeycloakRolesClaimsTransformation.cs index 96a7c9f1..d168eb63 100644 --- a/src/Keycloak.AuthServices.Authorization/Claims/KeycloakRolesClaimsTransformation.cs +++ b/src/Keycloak.AuthServices.Authorization/Claims/KeycloakRolesClaimsTransformation.cs @@ -70,7 +70,7 @@ public Task TransformAsync(ClaimsPrincipal principal) var result = principal.Clone(); - if (this.roleSource == RolesClaimTransformationSource.ResourceAccess) + if (this.roleSource.HasFlag(RolesClaimTransformationSource.ResourceAccess)) { var resourceAccessValue = principal.FindFirst("resource_access")?.Value; if (string.IsNullOrWhiteSpace(resourceAccessValue)) @@ -107,11 +107,9 @@ out var rolesElement identity.AddClaim(new Claim(this.roleClaimType, value)); } } - - return Task.FromResult(result); } - if (this.roleSource == RolesClaimTransformationSource.Realm) + if (this.roleSource.HasFlag(RolesClaimTransformationSource.Realm)) { var realmAccessValue = principal.FindFirst("realm_access")?.Value; if (string.IsNullOrWhiteSpace(realmAccessValue)) @@ -144,8 +142,6 @@ out var rolesElement identity.AddClaim(new Claim(this.roleClaimType, value)); } } - - return Task.FromResult(result); } } diff --git a/src/Keycloak.AuthServices.Authorization/KeycloakAuthorizationOptions.cs b/src/Keycloak.AuthServices.Authorization/KeycloakAuthorizationOptions.cs index 2a464f20..32a3bb0e 100644 --- a/src/Keycloak.AuthServices.Authorization/KeycloakAuthorizationOptions.cs +++ b/src/Keycloak.AuthServices.Authorization/KeycloakAuthorizationOptions.cs @@ -32,20 +32,26 @@ public class KeycloakAuthorizationOptions : KeycloakInstallationOptions /// /// RolesClaimTransformationSource /// +[Flags] public enum RolesClaimTransformationSource { /// /// Specifies that no transformation should be applied from the source. /// - None, + None = 0, /// /// Specifies that transformation should be applied to the realm. /// - Realm, + Realm = 1 << 0, /// /// Specifies that transformation should be applied to the resource access. /// - ResourceAccess + ResourceAccess = 1 << 1, + + /// + /// Specifies that transformation should be applied to all sources. + /// + All = Realm | ResourceAccess } diff --git a/tests/Keycloak.AuthServices.Authorization.Tests/Claims/KeycloakRolesClaimsTransformationTests.cs b/tests/Keycloak.AuthServices.Authorization.Tests/Claims/KeycloakRolesClaimsTransformationTests.cs index 376eebfe..416bbffa 100644 --- a/tests/Keycloak.AuthServices.Authorization.Tests/Claims/KeycloakRolesClaimsTransformationTests.cs +++ b/tests/Keycloak.AuthServices.Authorization.Tests/Claims/KeycloakRolesClaimsTransformationTests.cs @@ -19,12 +19,55 @@ public async Task ClaimsTransformationShouldMap(RolesClaimTransformationSource r for (var testCount = 0; testCount < 3; testCount++) { claimsPrincipal = await target.TransformAsync(claimsPrincipal); - claimsPrincipal.HasClaim(ClaimTypes.Role, AppRoleUserClaim).Should().BeTrue(); - claimsPrincipal.HasClaim(ClaimTypes.Role, AppRoleSuperUserClaim).Should().BeTrue(); + switch (roleSource) + { + case RolesClaimTransformationSource.Realm: + claimsPrincipal.HasClaim(ClaimTypes.Role, RealmRoleUserClaim).Should().BeTrue(); + claimsPrincipal.HasClaim(ClaimTypes.Role, RealmRoleSuperUserClaim).Should().BeTrue(); + break; + case RolesClaimTransformationSource.ResourceAccess: + claimsPrincipal.HasClaim(ClaimTypes.Role, AppRoleUserClaim).Should().BeTrue(); + claimsPrincipal.HasClaim(ClaimTypes.Role, AppRoleSuperUserClaim).Should().BeTrue(); + break; + default: + throw new ArgumentOutOfRangeException(nameof(roleSource), roleSource, "Unexpected role source"); + } claimsPrincipal.Claims.Count(item => ClaimTypes.Role == item.Type).Should().Be(2); } } + [Fact] + public async Task ClaimsTransformationShouldHandleNoneSource() + { + var target = new KeycloakRolesClaimsTransformation( + ClaimTypes.Role, + RolesClaimTransformationSource.None, + ClientId + ); + var claimsPrincipal = GetClaimsPrincipal(MyRealmClaimValue, MyResourceClaimValue); + + claimsPrincipal = await target.TransformAsync(claimsPrincipal); + claimsPrincipal.Claims.Count(item => ClaimTypes.Role == item.Type).Should().Be(0); + } + + [Fact] + public async Task ClaimsTransformationShouldHandleAllSource() + { + var target = new KeycloakRolesClaimsTransformation( + ClaimTypes.Role, + RolesClaimTransformationSource.All, + ClientId + ); + var claimsPrincipal = GetClaimsPrincipal(MyRealmClaimValue, MyResourceClaimValue); + + claimsPrincipal = await target.TransformAsync(claimsPrincipal); + claimsPrincipal.HasClaim(ClaimTypes.Role, AppRoleUserClaim).Should().BeTrue(); + claimsPrincipal.HasClaim(ClaimTypes.Role, AppRoleSuperUserClaim).Should().BeTrue(); + claimsPrincipal.HasClaim(ClaimTypes.Role, RealmRoleUserClaim).Should().BeTrue(); + claimsPrincipal.HasClaim(ClaimTypes.Role, RealmRoleSuperUserClaim).Should().BeTrue(); + claimsPrincipal.Claims.Count(item => ClaimTypes.Role == item.Type).Should().Be(4); + } + [Fact] public async Task ClaimsTransformationShouldHandleMissingResourceClaim() { @@ -72,8 +115,8 @@ public async Task ClaimsTransformationShouldHandleMissingResourceClaim() """ { "roles": [ - "my_client_app_role_user", - "my_client_app_role_super_user" + "realm_role_user", + "realm_role_super_user" ] } """; @@ -81,6 +124,8 @@ public async Task ClaimsTransformationShouldHandleMissingResourceClaim() // Fake claim values private const string AppRoleUserClaim = "my_client_app_role_user"; private const string AppRoleSuperUserClaim = "my_client_app_role_super_user"; + private const string RealmRoleUserClaim = "realm_role_user"; + private const string RealmRoleSuperUserClaim = "realm_role_super_user"; // The issuer/original issuer private const string MyUrl = "https://keycloak.mydomain.com/realms/my_realm";