From f8a9d8ae8e268ca1fee8eb1370bbd0b9fd296b46 Mon Sep 17 00:00:00 2001 From: kanpov <71177577+kanpov@users.noreply.github.com> Date: Sun, 16 Jun 2024 09:51:47 +0300 Subject: [PATCH] feat: Add support for the GET /admin/realms/{realm}/users/count endpoint in the Admin API SDK (#120) --- .../Admin/ApiUrls.cs | 2 + .../Admin/IKeycloakUserClient.cs | 39 ++++++++++ .../Admin/KeycloakClient.cs | 51 +++++++++---- .../Users/GetUserCountRequestParameters.cs | 71 +++++++++++++++++++ .../KeycloakUserClientTests.cs | 65 +++++++++++++++++ 5 files changed, 216 insertions(+), 12 deletions(-) create mode 100644 src/Keycloak.AuthServices.Sdk/Admin/Requests/Users/GetUserCountRequestParameters.cs diff --git a/src/Keycloak.AuthServices.Sdk/Admin/ApiUrls.cs b/src/Keycloak.AuthServices.Sdk/Admin/ApiUrls.cs index 9bbf1d5c..8d379d09 100644 --- a/src/Keycloak.AuthServices.Sdk/Admin/ApiUrls.cs +++ b/src/Keycloak.AuthServices.Sdk/Admin/ApiUrls.cs @@ -19,6 +19,8 @@ internal static class ApiUrls internal const string GetUser = $"{GetRealm}/users/{{id}}"; + internal const string GetUserCount = $"{GetRealm}/users/count"; + internal const string CreateUser = $"{GetRealm}/users"; internal const string UpdateUser = $"{GetRealm}/users/{{id}}"; diff --git a/src/Keycloak.AuthServices.Sdk/Admin/IKeycloakUserClient.cs b/src/Keycloak.AuthServices.Sdk/Admin/IKeycloakUserClient.cs index 3193842e..c6000432 100644 --- a/src/Keycloak.AuthServices.Sdk/Admin/IKeycloakUserClient.cs +++ b/src/Keycloak.AuthServices.Sdk/Admin/IKeycloakUserClient.cs @@ -40,6 +40,45 @@ async Task> GetUsersAsync( ?? Enumerable.Empty(); } + /// + /// Get the integer amount of users on the realm that match the provided . + /// + /// Note that the response is not JSON, but simply the integer value as a string. + /// + /// Realm name (not ID). + /// Optional query parameters. + /// + /// An integer amount of users + Task GetUserCountWithResponseAsync( + string realm, + GetUserCountRequestParameters? parameters = default, + CancellationToken cancellationToken = default + ); + + /// + /// Get the integer amount of users on the realm that match the provided . + /// + /// Realm name (not ID). + /// Optional query parameters. + /// + /// An integer amount of users + async Task GetUserCountAsync( + string realm, + GetUserCountRequestParameters? parameters = default, + CancellationToken cancellationToken = default + ) + { + var response = await this.GetUserCountWithResponseAsync( + realm, + parameters, + cancellationToken + ); + var stringContent = await response.Content.ReadAsStringAsync(cancellationToken); +#pragma warning disable CA1305 // Specify IFormatProvider + return Convert.ToInt32(stringContent); +#pragma warning restore CA1305 // Specify IFormatProvider + } + /// /// Get representation of a user. /// diff --git a/src/Keycloak.AuthServices.Sdk/Admin/KeycloakClient.cs b/src/Keycloak.AuthServices.Sdk/Admin/KeycloakClient.cs index 245682c8..848d4761 100644 --- a/src/Keycloak.AuthServices.Sdk/Admin/KeycloakClient.cs +++ b/src/Keycloak.AuthServices.Sdk/Admin/KeycloakClient.cs @@ -11,7 +11,7 @@ /// /// Represents a client for interacting with the Keycloak Admin API. /// -public partial class KeycloakClient : IKeycloakClient +public class KeycloakClient : IKeycloakClient { private readonly HttpClient httpClient; @@ -94,7 +94,42 @@ public async Task GetUsersWithResponseAsync( var responseMessage = await this.httpClient.GetAsync(path + query, cancellationToken); - return responseMessage!; + return responseMessage; + } + + /// + public async Task GetUserCountWithResponseAsync( + string realm, + GetUserCountRequestParameters? parameters = default, + CancellationToken cancellationToken = default + ) + { + var path = ApiUrls.GetUserCount.WithRealm(realm); + + var query = string.Empty; + + if (parameters is not null) + { + var queryParameters = new List>() + { + new("email", parameters.Email), + new("emailVerified", parameters.EmailVerified?.ToString()), + new("enabled", parameters.Enabled?.ToString()), + new("firstName", parameters.FirstName), + new("lastName", parameters.LastName), + new("q", parameters.Query), + new("search", parameters.Search), + new("username", parameters.Username) + }; + + query = new QueryBuilder(queryParameters.Where(q => q.Value is not null)!) + .ToQueryString() + .ToString(); + } + + var responseMessage = await this.httpClient.GetAsync(path + query, cancellationToken); + + return responseMessage; } /// @@ -174,11 +209,7 @@ public async Task SendVerifyEmailWithResponseAsync( var url = path + queryBuilder.ToQueryString(); using var content = new StringContent(string.Empty); - var responseMessage = await this.httpClient.PutAsync( - url, - content, - cancellationToken - ); + var responseMessage = await this.httpClient.PutAsync(url, content, cancellationToken); return responseMessage!; } @@ -272,11 +303,7 @@ public async Task JoinGroupWithResponseAsync( .Replace("{groupId}", groupId); using var content = new StringContent(string.Empty); - var responseMessage = await this.httpClient.PutAsync( - path, - content, - cancellationToken - ); + var responseMessage = await this.httpClient.PutAsync(path, content, cancellationToken); return responseMessage!; } diff --git a/src/Keycloak.AuthServices.Sdk/Admin/Requests/Users/GetUserCountRequestParameters.cs b/src/Keycloak.AuthServices.Sdk/Admin/Requests/Users/GetUserCountRequestParameters.cs new file mode 100644 index 00000000..8832f39f --- /dev/null +++ b/src/Keycloak.AuthServices.Sdk/Admin/Requests/Users/GetUserCountRequestParameters.cs @@ -0,0 +1,71 @@ +namespace Keycloak.AuthServices.Sdk.Admin.Requests.Users; + +/// +/// Optional request parameters for the endpoint. +/// It can be called in three different ways. +/// +/// +/// +/// Don’t specify any criteria. The number of all users within that realm will be returned, not limited by +/// pagination. +/// +/// +/// +/// +/// If is specified, other criteria such as will be ignored +/// even though you set them. The string will be matched against the first and last +/// name, the username and the email of a user. +/// +/// +/// +/// +/// If is unspecified but any of , , +/// or , those criteria are matched against their respective +/// fields on a user entity. Combined with a logical and. +/// +/// +/// +/// +public class GetUserCountRequestParameters +{ + /// + /// An optional filter for a user's email. + /// + public string? Email { get; init; } + + /// + /// Whether a user's email has to be verified to be included. + /// + public bool? EmailVerified { get; init; } + + /// + /// Whether a user has to be enabled to be included. + /// + public bool? Enabled { get; init; } + + /// + /// An optional filter for a user's first name. + /// + public string? FirstName { get; init; } + + /// + /// An optional filter for a user's last name. + /// + public string? LastName { get; init; } + + /// + /// An optional query to search for custom attributes, in the format "key1:value2 key2:value2". + /// + public string? Query { get; init; } + + /// + /// An optional search string for all the fields of a user. + /// Default search behavior is prefix-based (e.g., foo or foo*). Use foo for infix search and "foo" for exact search. + /// + public string? Search { get; init; } + + /// + /// An optional filter for a user's username. + /// + public string? Username { get; init; } +} diff --git a/tests/Keycloak.AuthServices.Sdk.Tests/KeycloakUserClientTests.cs b/tests/Keycloak.AuthServices.Sdk.Tests/KeycloakUserClientTests.cs index 362a95e1..48bcc92a 100644 --- a/tests/Keycloak.AuthServices.Sdk.Tests/KeycloakUserClientTests.cs +++ b/tests/Keycloak.AuthServices.Sdk.Tests/KeycloakUserClientTests.cs @@ -132,6 +132,71 @@ public async Task GetUsersShouldCallCorrectEndpointWithOptionalQueryParameters() this.handler.VerifyNoOutstandingExpectation(); } + [Fact] + public async Task GetUserCountShouldCallCorrectEndpoint() + { + const int userAmount = 5; + + for (var i = 0; i < userAmount; ++i) + { + var id = Guid.NewGuid(); + GetUserRepresentation(id); + } + +#pragma warning disable CA1305 // use locale provider + var response = userAmount.ToString(); +#pragma warning restore CA1305 // use locale provider + + this.handler.Expect(HttpMethod.Get, $"{BaseAddress}/admin/realms/master/users/count") + .Respond(HttpStatusCode.OK, MediaType, response); + + var result = await this.keycloakUserClient.GetUserCountAsync("master"); + + result.Should().Be(userAmount); + this.handler.VerifyNoOutstandingExpectation(); + } + + [Fact] + public async Task GetUserCountShouldCallCorrectEndpointWithOptionalQueryParameters() + { + var getUserCountRequestParameters = new GetUserCountRequestParameters + { + Email = "email", + EmailVerified = false, + Enabled = false, + FirstName = "firstName", + LastName = "lastName", + Query = "query", + Search = "search", + Username = "username" + }; + + const string url = "/admin/realms/master/users/count"; + var queryBuilder = new QueryBuilder + { + { "email", "email" }, + { "emailVerified", "False" }, + { "enabled", "False" }, + { "firstName", "firstName" }, + { "lastName", "lastName" }, + { "q", "query" }, + { "search", "search" }, + { "username", "username" } + }; + + const string response = "0"; + + this.handler.Expect(HttpMethod.Get, url + queryBuilder.ToQueryString()) + .Respond(HttpStatusCode.BadRequest, MediaType, response); + + _ = await this.keycloakUserClient.GetUserCountAsync( + "master", + getUserCountRequestParameters + ); + + this.handler.VerifyNoOutstandingExpectation(); + } + [Fact] public async Task CreateUserShouldCallCorrectEndpoint() {