Skip to content

Commit

Permalink
feat: Finish UserClient; add AuthorzationOptions overloads
Browse files Browse the repository at this point in the history
  • Loading branch information
NikiforovAll committed May 3, 2024
1 parent 3025662 commit 64f0aa6
Show file tree
Hide file tree
Showing 15 changed files with 1,090 additions and 373 deletions.
21 changes: 19 additions & 2 deletions docs/configuration/configuration-authorization.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,30 @@ With [Keycloak.AuthServices.Authorization](https://www.nuget.org/packages/Keyclo

<<< @/../tests/Keycloak.AuthServices.IntegrationTests/PolicyTests.cs#RequireClientRoles_TestClientRole_Verified

Configure default source:
Configure default source. The client name is taken from the `KeycloakAuthorizationOptions.Resource`:

<<< @/../tests/Keycloak.AuthServices.IntegrationTests/PolicyTests.cs#RequireClientRoles_TestClientRoleWithConfiguration_Verified

```json
{
"Keycloak": {
"resource": "test-client"
}
}
```

> [!IMPORTANT]
> If you don't configure the default source `KeycloakException` exception will be thrown.
Override default source with `KeycloakAuthorizationOptions.RolesResource`:

<<< @/../tests/Keycloak.AuthServices.IntegrationTests/PolicyTests.cs#RequireClientRoles_TestClientRoleWithInlineConfiguration_Verified

Note it has more priority over the `KeycloakAuthorizationOptions.Resource`

## Keycloak Role Claims Transformation

Keycloak roles can be automatically transformed to [AspNetCore Roles](https://learn.microsoft.com/en-us/aspnet/core/security/authorization/roles). This feature is disabled by default and is based on `KeycloakRolesClaimsTransformation`.
Keycloak roles can be automatically transformed to [AspNetCore Roles](https://learn.microsoft.com/en-us/aspnet/core/security/authorization/roles). This feature is **disabled by default** and is based on `KeycloakRolesClaimsTransformation`.

Specify `KeycloakAuthorizationOptions.EnableRolesMapping` to enable it. E.g.:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public static class PoliciesBuilderExtensions
{
/// <summary>
/// Adds resource role requirement to builder. Ensures that at least one resource role is present in resource claims.
/// Note, make sure role source is configure. See documentation for more details.
/// </summary>
/// <param name="builder"></param>
/// <param name="roles"></param>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ ResourceAccessRequirement requirement
if (string.IsNullOrWhiteSpace(clientId))
{
throw new KeycloakException(
$"Unable to resolve Resource for Role Validation - please make sure {nameof(KeycloakAuthorizationOptions)} are configured."
$"Unable to resolve Resource for Role Validation - please make sure {nameof(KeycloakAuthorizationOptions)} are configured. \n\n See documentation for more details - https://nikiforovall.github.io/keycloak-authorization-services-dotnet/configuration/configuration-authorization.html#require-resource-roles"
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,26 +20,33 @@ public static class ServiceCollectionExtensions
/// </summary>
/// <param name="services"></param>
/// <param name="configuration"></param>
/// <param name="keycloakClientSectionName"></param>
/// <param name="configSectionName"></param>
/// <returns></returns>
[Obsolete(
"Method overload will be removed in future versions. Use AddKeycloakAuthorization together with AddAuthorizationServer instead"
)]
public static IServiceCollection AddKeycloakAuthorization(
this IServiceCollection services,
IConfiguration configuration,
string keycloakClientSectionName = KeycloakProtectionClientOptions.Section
string configSectionName = KeycloakAuthorizationOptions.Section
)
{
services.AddAuthorizationServer(configuration, keycloakClientSectionName);

services.AddSingleton<IAuthorizationHandler, RptRequirementHandler>();
services.AddSingleton<IAuthorizationHandler, RealmAccessRequirementHandler>();
services.AddSingleton<IAuthorizationHandler, ResourceAccessRequirementHandler>();
var configurationSection = configuration.GetSection(configSectionName);

return services;
return services.AddKeycloakAuthorization(configurationSection);
}

/// <summary>
/// Adds keycloak authorization services
/// </summary>
/// <param name="services"></param>
/// <param name="configurationSection"></param>
/// <returns></returns>
public static IServiceCollection AddKeycloakAuthorization(
this IServiceCollection services,
IConfigurationSection configurationSection
) =>
services.AddKeycloakAuthorization(options =>
configurationSection.BindKeycloakOptions(options)
);

/// <summary>
/// Adds keycloak authorization services
/// </summary>
Expand All @@ -50,6 +57,9 @@ public static IServiceCollection AddKeycloakAuthorization(
Action<KeycloakAuthorizationOptions>? configureKeycloakAuthorizationOptions = null
)
{
configureKeycloakAuthorizationOptions ??= _ => { };
services.Configure(configureKeycloakAuthorizationOptions);

services.AddSingleton<IAuthorizationHandler, RptRequirementHandler>();
services.AddSingleton<IAuthorizationHandler, RealmAccessRequirementHandler>();
services.AddSingleton<IAuthorizationHandler, ResourceAccessRequirementHandler>();
Expand All @@ -66,9 +76,6 @@ public static IServiceCollection AddKeycloakAuthorization(
keycloakOptions.RolesResource ?? keycloakOptions.Resource
);
});
configureKeycloakAuthorizationOptions ??= _ => { };

services.Configure<KeycloakAuthorizationOptions>(configureKeycloakAuthorizationOptions);

return services;
}
Expand All @@ -84,14 +91,13 @@ public static IServiceCollection AddKeycloakAuthorization(
public static IHttpClientBuilder AddAuthorizationServer(
this IServiceCollection services,
IConfiguration configuration,
string configSectionName = KeycloakProtectionClientOptions.Section,
Action<HttpClient>? configureClient = default
)
{
var configurationSection = configuration.GetSection(configSectionName);

return services.AddAuthorizationServer(configurationSection, configureClient);
}
Action<HttpClient>? configureClient = default,
string configSectionName = KeycloakProtectionClientOptions.Section
) =>
services.AddAuthorizationServer(
configuration.GetSection(configSectionName),
configureClient
);

/// <summary>
/// Adds Keycloak Protection client and auto header propagation
Expand Down Expand Up @@ -123,10 +129,14 @@ public static IHttpClientBuilder AddAuthorizationServer(
Action<HttpClient>? configureClient = default
)
{
configureKeycloakOptions ??= _ => { };
services.Configure(configureKeycloakOptions);

services.AddHttpContextAccessor();

services.AddSingleton<IAuthorizationHandler, DecisionRequirementHandler>();

// (!) resolved locally, will not work with PostConfigure and IOptions pattern
var keycloakOptions = new KeycloakProtectionClientOptions();
configureKeycloakOptions.Invoke(keycloakOptions);

Expand All @@ -135,8 +145,6 @@ public static IHttpClientBuilder AddAuthorizationServer(
services.AddSingleton<IAuthorizationPolicyProvider, ProtectedResourcePolicyProvider>();
}

services.AddOptions<KeycloakProtectionClientOptions>().Configure(configureKeycloakOptions);

return services
.AddHttpClient<IKeycloakProtectionClient, KeycloakProtectionClient>()
.ConfigureHttpClient(
Expand Down
4 changes: 4 additions & 0 deletions src/Keycloak.AuthServices.Sdk/Admin/ApiUrls.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ internal static class ApiUrls

internal const string ExecuteActionsEmail = $"{GetRealm}/users/{{id}}/execute-actions-email";
internal const string GetUserGroups = $"{GetRealm}/users/{{id}}/groups";

internal const string JoinGroup = $"{GetRealm}/users/{{id}}/groups/{{groupId}}";

internal const string LeaveGroup = $"{GetRealm}/users/{{id}}/groups/{{groupId}}";
#endregion

#region Group API
Expand Down
67 changes: 67 additions & 0 deletions src/Keycloak.AuthServices.Sdk/Admin/IKeycloakGroupClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,71 @@ public interface IKeycloakGroupClient
// /// <param name="group">Group representation.</param>
// /// <returns></returns>
// Task<HttpResponseMessage> UpdateGroup(string realm, string groupId, Group group);

// /// <summary>
// /// Get a stream of groups on the realm.
// /// </summary>
// /// <param name="realm">Realm name (not ID).</param>
// /// <param name="parameters">Optional query parameters.</param>
// /// <returns>A stream of groups, filtered according to query parameters.</returns>
// [Get(KeycloakClientApiConstants.GetGroups)]
// [Headers("Accept: application/json")]
// Task<IEnumerable<Group>> GetGroups(string realm, [Query] GetGroupsRequestParameters? parameters = default);

// /// <summary>
// /// Get representation of a Group.
// /// </summary>
// /// <param name="realm">Realm name (not ID).</param>
// /// <param name="groupId">Group ID.</param>
// /// <returns>The group representation.</returns>
// [Get(KeycloakClientApiConstants.GetGroup)]
// [Headers("Accept: application/json")]
// Task<Group> GetGroup(string realm, [AliasAs("id")] string groupId);

// /// <summary>
// /// Create a new Group.
// /// </summary>
// /// <remarks>
// /// Name must be unique.
// /// </remarks>
// /// <param name="realm">Realm name (not ID).</param>
// /// <param name="group">Group representation.</param>
// /// <returns></returns>
// [Post(KeycloakClientApiConstants.CreateGroup)]
// [Headers("Content-Type: application/json")]
// Task<HttpResponseMessage> CreateGroup(string realm, [Body] Group group);

// /// <summary>
// /// Update the Group.
// /// </summary>
// /// <param name="realm">Realm name (not ID).</param>
// /// <param name="groupId">Group ID.</param>
// /// <param name="group">Group representation.</param>
// /// <returns></returns>
// [Put(KeycloakClientApiConstants.UpdateGroup)]
// [Headers("Content-Type: application/json")]
// Task UpdateGroup(string realm, [AliasAs("id")] string groupId, [Body] Group group);

// /// <summary>
// /// Create a new child group.
// /// </summary>
// /// <remarks>
// /// Name must be unique.
// /// </remarks>
// /// <param name="realm">Realm name (not ID).</param>
// /// <param name="groupId">Group ID.</param>
// /// <param name="group">Group representation.</param>
// /// <returns></returns>
// [Post(KeycloakClientApiConstants.CreateChildGroup)]
// [Headers("Content-Type: application/json")]
// Task<HttpResponseMessage> CreateChildGroup(string realm, [AliasAs("id")] string groupId, [Body] Group group);

// /// <summary>
// /// Delete a group. Note: the Keycloak documentation does not specify if this deletes subgroups.
// /// </summary>
// /// <param name="realm">Realm name (not ID).</param>
// /// <param name="groupId">Group ID.</param>
// /// <returns></returns>
// [Delete(KeycloakClientApiConstants.DeleteGroup)]
// Task DeleteGroup(string realm, [AliasAs("id")] string groupId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,81 @@ public interface IKeycloakProtectedResourceClient
// /// <param name="resource"></param>
// /// <returns></returns>
// Task UpdateResource(string realm, string id, Resource resource);

// /// <summary>
// /// Searches for resource
// /// </summary>
// /// <param name="realm">Realm name (not ID).</param>
// /// <param name="getResourcesRequestParameters">Optional query parameters</param>
// /// <returns></returns>
// [Get(KeycloakClientApiConstants.GetResources)]
// [Headers("Accept: application/json")]
// Task<List<string>> GetResources(string realm, [Query] GetResourcesRequestParameters? getResourcesRequestParameters = default);

// /// <summary>
// /// Searches for resources. NOTE: you must set the <see cref="GetResourcesRequestParameters.Deep"/> property to true
// /// </summary>
// /// <param name="realm">Realm name (not ID).</param>
// /// <param name="getResourcesDeepRequestParameters">Query parameters</param>
// /// <returns></returns>
// [Get(KeycloakClientApiConstants.GetResources)]
// [Headers("Accept: application/json")]
// Task<List<ResourceResponse>> GetResourcesDeep(string realm, [Query] GetResourcesDeepRequestParameters getResourcesDeepRequestParameters);

// /// <summary>
// /// Gets resource by Id
// /// </summary>
// /// <param name="realm">Realm name (not ID).</param>
// /// <param name="resourceId">Resource ID.</param>
// /// <returns></returns>
// [Get(KeycloakClientApiConstants.GetResource)]
// [Headers("Accept: application/json")]
// Task<ResourceResponse> GetResource(string realm, [AliasAs("id")] string resourceId);

// /// <summary>
// /// Gets resource by Name
// /// </summary>
// /// <remarks>
// /// https://github.com/keycloak/keycloak/blob/main/docs/documentation/authorization_services/topics/service-protection-resources-api-papi.adoc#querying-resources
// /// </remarks>
// /// <param name="realm">Realm name (not ID).</param>
// /// <param name="name"></param>
// /// <returns></returns>
// [Get(KeycloakClientApiConstants.GetResourceByExactName)]
// [Headers("Accept: application/json")]
// Task<string[]> SearchResourcesByName(string realm, [Query] string name);

// /// <summary>
// /// Creates resource
// /// </summary>
// /// <param name="realm">Realm name (not ID).</param>
// /// <param name="resource"></param>
// /// <returns></returns>
// [Post(KeycloakClientApiConstants.CreateResource)]
// [Headers("Accept: application/json", "Content-Type: application/json")]
// Task<ResourceResponse> CreateResource(string realm, [Body] Resource resource);

// /// <summary>
// /// Updates resource
// /// </summary>
// /// <remarks>
// /// Docs: https://github.com/keycloak/keycloak-documentation/blob/main/authorization_services/topics/service-protection-resources-api-papi.adoc#updating-resources
// /// </remarks>
// /// <param name="realm">Realm name (not ID).</param>
// /// <param name="resourceId">Resource ID.</param>
// /// <param name="resource"></param>
// /// <returns></returns>
// [Put(KeycloakClientApiConstants.PutResource)]
// [Headers("Accept: application/json", "Content-Type: application/json")]
// Task UpdateResource(string realm, [AliasAs("id")] string resourceId, [Body] Resource resource);

// /// <summary>
// /// Delete a resource
// /// </summary>
// /// <param name="realm">Realm name (not ID).</param>
// /// <param name="resourceId">Resource ID.</param>
// /// <returns></returns>
// [Delete(KeycloakClientApiConstants.DeleteResource)]
// [Headers("Accept: application/json")]
// Task DeleteResource(string realm, [AliasAs("id")] string resourceId);
}
Loading

0 comments on commit 64f0aa6

Please sign in to comment.