Skip to content

Commit

Permalink
feat: Convert RolesClaimTransformationSource to flags (#97)
Browse files Browse the repository at this point in the history
* feat: Convert RolesClaimTransformationSource to flags to allow for combining sources, add "All" flag for convenience
  • Loading branch information
Alexr03 authored May 9, 2024
1 parent 957dc16 commit 056e649
Show file tree
Hide file tree
Showing 4 changed files with 81 additions and 19 deletions.
27 changes: 21 additions & 6 deletions docs/configuration/configuration-authorization.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
/// <summary>
/// No Transformation. Default
/// Specifies that no transformation should be applied from the source.
/// </summary>
None,
None = 0,

/// <summary>
/// Use realm roles as source
/// Specifies that transformation should be applied to the realm.
/// </summary>
Realm,
Realm = 1 << 0,

/// <summary>
/// Use client roles as source
/// Specifies that transformation should be applied to the resource access.
/// </summary>
ResourceAccess
ResourceAccess = 1 << 1,

/// <summary>
/// Specifies that transformation should be applied to all sources.
/// </summary>
All = Realm | ResourceAccess
}
```

Expand Down Expand Up @@ -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".
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public Task<ClaimsPrincipal> 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))
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -144,8 +142,6 @@ out var rolesElement
identity.AddClaim(new Claim(this.roleClaimType, value));
}
}

return Task.FromResult(result);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,20 +32,26 @@ public class KeycloakAuthorizationOptions : KeycloakInstallationOptions
/// <summary>
/// RolesClaimTransformationSource
/// </summary>
[Flags]
public enum RolesClaimTransformationSource
{
/// <summary>
/// Specifies that no transformation should be applied from the source.
/// </summary>
None,
None = 0,

/// <summary>
/// Specifies that transformation should be applied to the realm.
/// </summary>
Realm,
Realm = 1 << 0,

/// <summary>
/// Specifies that transformation should be applied to the resource access.
/// </summary>
ResourceAccess
ResourceAccess = 1 << 1,

/// <summary>
/// Specifies that transformation should be applied to all sources.
/// </summary>
All = Realm | ResourceAccess
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down Expand Up @@ -72,15 +115,17 @@ public async Task ClaimsTransformationShouldHandleMissingResourceClaim()
"""
{
"roles": [
"my_client_app_role_user",
"my_client_app_role_super_user"
"realm_role_user",
"realm_role_super_user"
]
}
""";

// 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";
Expand Down

0 comments on commit 056e649

Please sign in to comment.