Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for the GET /admin/realms/{realm}/users/count endpoint in the Admin API SDK #120

Merged
merged 5 commits into from
Jun 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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-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

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
/// 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
Loading