Skip to content

Commit

Permalink
feat: Add support for the GET /admin/realms/{realm}/users/count endpo…
Browse files Browse the repository at this point in the history
…int in the Admin API SDK (#120)
  • Loading branch information
kanpov authored Jun 16, 2024
1 parent 5ff66a5 commit f8a9d8a
Show file tree
Hide file tree
Showing 5 changed files with 216 additions and 12 deletions.
2 changes: 2 additions & 0 deletions src/Keycloak.AuthServices.Sdk/Admin/ApiUrls.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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}}";
Expand Down
39 changes: 39 additions & 0 deletions src/Keycloak.AuthServices.Sdk/Admin/IKeycloakUserClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,45 @@ async Task<IEnumerable<UserRepresentation>> GetUsersAsync(
?? Enumerable.Empty<UserRepresentation>();
}

/// <summary>
/// Get the integer amount of users on the realm that match the provided <see cref="GetUserCountRequestParameters"/>.
///
/// Note that the response is not JSON, but simply the integer value as a string.
/// </summary>
/// <param name="realm">Realm name (not ID).</param>
/// <param name="parameters">Optional query parameters.</param>
/// <param name="cancellationToken"></param>
/// <returns>An integer amount of users</returns>
Task<HttpResponseMessage> GetUserCountWithResponseAsync(
string realm,
GetUserCountRequestParameters? parameters = default,
CancellationToken cancellationToken = default
);

/// <summary>
/// Get the integer amount of users on the realm that match the provided <see cref="GetUserCountRequestParameters"/>.
/// </summary>
/// <param name="realm">Realm name (not ID).</param>
/// <param name="parameters">Optional query parameters.</param>
/// <param name="cancellationToken"></param>
/// <returns>An integer amount of users</returns>
async Task<int> 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
}

/// <summary>
/// Get representation of a user.
/// </summary>
Expand Down
51 changes: 39 additions & 12 deletions src/Keycloak.AuthServices.Sdk/Admin/KeycloakClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
/// <summary>
/// Represents a client for interacting with the Keycloak Admin API.
/// </summary>
public partial class KeycloakClient : IKeycloakClient
public class KeycloakClient : IKeycloakClient
{
private readonly HttpClient httpClient;

Expand Down Expand Up @@ -94,7 +94,42 @@ public async Task<HttpResponseMessage> GetUsersWithResponseAsync(

var responseMessage = await this.httpClient.GetAsync(path + query, cancellationToken);

return responseMessage!;
return responseMessage;
}

/// <inheritdoc/>
public async Task<HttpResponseMessage> 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<KeyValuePair<string, string?>>()
{
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;
}

/// <inheritdoc/>
Expand Down Expand Up @@ -174,11 +209,7 @@ public async Task<HttpResponseMessage> 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!;
}
Expand Down Expand Up @@ -272,11 +303,7 @@ public async Task<HttpResponseMessage> 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!;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
namespace Keycloak.AuthServices.Sdk.Admin.Requests.Users;

/// <summary>
/// Optional request parameters for the <see cref="IKeycloakClient.GetUserCountAsync"/> endpoint.

Check warning on line 4 in src/Keycloak.AuthServices.Sdk/Admin/Requests/Users/GetUserCountRequestParameters.cs

View workflow job for this annotation

GitHub Actions / Build-ubuntu-latest

XML comment has cref attribute 'GetUserCountAsync' that could not be resolved

Check warning on line 4 in src/Keycloak.AuthServices.Sdk/Admin/Requests/Users/GetUserCountRequestParameters.cs

View workflow job for this annotation

GitHub Actions / Build-ubuntu-latest

XML comment has cref attribute 'GetUserCountAsync' that could not be resolved

Check warning on line 4 in src/Keycloak.AuthServices.Sdk/Admin/Requests/Users/GetUserCountRequestParameters.cs

View workflow job for this annotation

GitHub Actions / Build-ubuntu-latest

XML comment has cref attribute 'GetUserCountAsync' that could not be resolved

Check warning on line 4 in src/Keycloak.AuthServices.Sdk/Admin/Requests/Users/GetUserCountRequestParameters.cs

View workflow job for this annotation

GitHub Actions / Build-ubuntu-latest

XML comment has cref attribute 'GetUserCountAsync' that could not be resolved

Check warning on line 4 in src/Keycloak.AuthServices.Sdk/Admin/Requests/Users/GetUserCountRequestParameters.cs

View workflow job for this annotation

GitHub Actions / Build-ubuntu-latest

XML comment has cref attribute 'GetUserCountAsync' that could not be resolved

Check warning on line 4 in src/Keycloak.AuthServices.Sdk/Admin/Requests/Users/GetUserCountRequestParameters.cs

View workflow job for this annotation

GitHub Actions / Build-windows-latest

XML comment has cref attribute 'GetUserCountAsync' that could not be resolved

Check warning on line 4 in src/Keycloak.AuthServices.Sdk/Admin/Requests/Users/GetUserCountRequestParameters.cs

View workflow job for this annotation

GitHub Actions / Build-windows-latest

XML comment has cref attribute 'GetUserCountAsync' that could not be resolved

Check warning on line 4 in src/Keycloak.AuthServices.Sdk/Admin/Requests/Users/GetUserCountRequestParameters.cs

View workflow job for this annotation

GitHub Actions / Build-windows-latest

XML comment has cref attribute 'GetUserCountAsync' that could not be resolved

Check warning on line 4 in src/Keycloak.AuthServices.Sdk/Admin/Requests/Users/GetUserCountRequestParameters.cs

View workflow job for this annotation

GitHub Actions / Build-windows-latest

XML comment has cref attribute 'GetUserCountAsync' that could not be resolved

Check warning on line 4 in src/Keycloak.AuthServices.Sdk/Admin/Requests/Users/GetUserCountRequestParameters.cs

View workflow job for this annotation

GitHub Actions / Build-windows-latest

XML comment has cref attribute 'GetUserCountAsync' that could not be resolved
/// It can be called in three different ways.
/// <list type="number">
/// <item>
/// <description>
/// Don’t specify any criteria. The number of all users within that realm will be returned, not limited by
/// pagination.
/// </description>
/// </item>
/// <item>
/// <description>
/// If <see cref="Search"/> is specified, other criteria such as <see cref="LastName"/> will be ignored
/// even though you set them. The <see cref="Search"/> string will be matched against the first and last
/// name, the username and the email of a user.
/// </description>
/// </item>
/// <item>
/// <description>
/// If <see cref="Search"/> is unspecified but any of <see cref="LastName"/>, <see cref="FirstName"/>,
/// <see cref="Email"/> or <see cref="Username"/>, those criteria are matched against their respective
/// fields on a user entity. Combined with a logical and.
/// </description>
/// </item>
/// </list>
/// </summary>
public class GetUserCountRequestParameters
{
/// <summary>
/// An optional filter for a user's email.
/// </summary>
public string? Email { get; init; }

/// <summary>
/// Whether a user's email has to be verified to be included.
/// </summary>
public bool? EmailVerified { get; init; }

/// <summary>
/// Whether a user has to be enabled to be included.
/// </summary>
public bool? Enabled { get; init; }

/// <summary>
/// An optional filter for a user's first name.
/// </summary>
public string? FirstName { get; init; }

/// <summary>
/// An optional filter for a user's last name.
/// </summary>
public string? LastName { get; init; }

/// <summary>
/// An optional query to search for custom attributes, in the format "<c>key1:value2 key2:value2</c>".
/// </summary>
public string? Query { get; init; }

/// <summary>
/// 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.
/// </summary>
public string? Search { get; init; }

/// <summary>
/// An optional filter for a user's username.
/// </summary>
public string? Username { get; init; }
}
65 changes: 65 additions & 0 deletions tests/Keycloak.AuthServices.Sdk.Tests/KeycloakUserClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down

0 comments on commit f8a9d8a

Please sign in to comment.