KeycloakAdminClient.java
/*
* Copyright 2026 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.bremersee.keycloak.api.webflux;
import static java.util.Objects.requireNonNullElseGet;
import static org.springframework.util.ObjectUtils.isEmpty;
import org.bremersee.keycloak.api.GetGroupsParameters;
import org.bremersee.keycloak.api.GetUsersParameters;
import org.bremersee.keycloak.api.model.GroupRepresentation;
import org.bremersee.keycloak.api.model.UserRepresentation;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import org.springframework.util.Assert;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* The keycloak admin client.
*
* @author Christian Bremer
*/
public record KeycloakAdminClient(@NonNull AdminApi adminApi) {
/**
* Returns a stream of users. Note that the 'credentials' field in the returned UserRepresentation
* objects is typically not populated for performance reasons. If specific credential metadata is
* required, use the dedicated 'GET /admin/realms/{realm}/users/{user-id}/credentials' endpoint.
*
* @param realm the realm name
* @param parameters the parameters
* @return the stream of users
*/
public Flux<UserRepresentation> getUsers(
@NonNull String realm,
@Nullable GetUsersParameters parameters) {
GetUsersParameters params = requireNonNullElseGet(parameters, GetUsersParameters::defaults);
return adminApi.adminRealmsRealmUsersGet(
realm,
params.getBriefRepresentation(),
params.getCreatedAfterAsString(),
params.getCreatedBeforeAsString(),
params.getEmail(),
params.getEmailVerified(),
params.getEnabled(),
params.getExact(),
params.getFirst(),
params.getFirstName(),
params.getIdpAlias(),
params.getIdpUserId(),
params.getLastName(),
params.getMax(),
params.getQuery(),
params.getSearch(),
params.getUsername());
}
/**
* Get representation of the user.
*
* @param realm the realm name
* @param userId the user ID
* @param userProfileMetadata Indicates if the user profile metadata should be added to the
* response
* @return the user
*/
public Mono<UserRepresentation> getUserById(
@NonNull String realm,
@NonNull String userId,
@Nullable Boolean userProfileMetadata) {
return adminApi.adminRealmsRealmUsersUserIdGet(realm, userId, userProfileMetadata);
}
/**
* Adds the given user to the given group.
*
* @param realm the realm name
* @param userId the user ID
* @param groupId the group ID
* @return void
*/
public Mono<Void> addUserToGroup(
@NonNull String realm,
@NonNull String userId,
@NonNull String groupId) {
return adminApi.adminRealmsRealmUsersUserIdGroupsGroupIdPut(realm, userId, groupId);
}
/**
* Removes the given user from the given group.
*
* @param realm the realm name
* @param userId the user ID
* @param groupId the group ID
* @return void
*/
public Mono<Void> removeUserFromGroup(
@NonNull String realm,
@NonNull String userId,
@NonNull String groupId) {
return adminApi.adminRealmsRealmUsersUserIdGroupsGroupIdDelete(realm, userId, groupId);
}
/**
* Get group hierarchy. Only `name` and `id` are returned. `subGroups` are only returned when
* using the `search` or `q` parameter. If none of these parameters is provided, the top-level
* groups are returned without `subGroups` being filled.
*
* @param realm the realm name
* @param parameters the parameters
* @return the stream of groups
*/
public Flux<GroupRepresentation> getGroups(
@NonNull String realm,
@Nullable GetGroupsParameters parameters) {
GetGroupsParameters params = requireNonNullElseGet(parameters, GetGroupsParameters::defaults);
return adminApi.adminRealmsRealmGroupsGet(
realm,
params.getBriefRepresentation(),
params.getExact(),
params.getFirst(),
params.getMax(),
params.getPopulateHierarchy(),
params.getQuery(),
params.getSearch(),
params.getSubGroupsCount());
}
/**
* Gets group by ID.
*
* @param realm the realm name
* @param groupId the group ID
* @return the group
*/
public Mono<GroupRepresentation> getGroupById(
@NonNull String realm,
@NonNull String groupId) {
return adminApi.adminRealmsRealmGroupsGroupIdGet(realm, groupId);
}
/**
* Gets group by path.
*
* @param realm the realm name
* @param path the group path (e.g. '/groupname-1/groupsname-2')
* @return the group
*/
public Mono<GroupRepresentation> getGroupByPath(@NonNull String realm, @NonNull String path) {
return adminApi.adminRealmsRealmGroupByPathPathGet(realm, path);
}
/**
* Gets group members.
*
* @param realm the realm name
* @param groupId the group ID
* @param briefRepresentation Only return basic information (only guaranteed to return id,
* username, created, first and last name, email, enabled state, email verification state,
* federation link, and access. Note that it means that namely user attributes, required
* actions, and not before are not returned.)
* @param first Pagination offset
* @param max Maximum results size (defaults to 100)
* @return the group members
*/
public Flux<UserRepresentation> getGroupMembers(
@NonNull String realm,
@NonNull String groupId,
@Nullable Boolean briefRepresentation,
@Nullable Integer first,
@Nullable Integer max) {
return adminApi.adminRealmsRealmGroupsGroupIdMembersGet(
realm,
groupId,
briefRepresentation,
first,
max);
}
/**
* Creates or updates a group.
*
* @param realm the realm name
* @param group the group
* @return the group
*/
public Mono<GroupRepresentation> saveGroup(
@NonNull String realm,
@NonNull GroupRepresentation group) {
Assert.hasText(group.getName(), "Group name must not be null or empty.");
if (isEmpty(group.getId())) {
return adminApi.adminRealmsRealmGroupsPost(realm, Mono.just(group))
.then(adminApi.adminRealmsRealmGroupByPathPathGet(realm, "/" + group.getName()));
}
return adminApi.adminRealmsRealmGroupsGroupIdPut(realm, group.getId(), Mono.just(group))
.then(adminApi.adminRealmsRealmGroupsGroupIdGet(realm, group.getId()));
}
/**
* Creates a subgroup.
*
* @param realm the realm name
* @param groupId the parent group ID
* @param group the subgroup
* @return the subgroup
*/
public Mono<GroupRepresentation> createSubGroup(
@NonNull String realm,
@NonNull String groupId,
@NonNull GroupRepresentation group) {
Assert.hasText(group.getName(), "Group name must not be null or empty.");
return getGroupById(realm, groupId)
.flatMap(parentGroup -> adminApi
.adminRealmsRealmGroupsGroupIdChildrenPost(realm, groupId, Mono.just(group))
.then(adminApi.adminRealmsRealmGroupByPathPathGet(
realm, parentGroup.getPath() + "/" + group.getName())));
}
/**
* Gets subgroups.
*
* @param realm the realm
* @param groupId the group id
* @param parameters the parameters
* @return the subgroups
*/
public Flux<GroupRepresentation> getSubGroups(
@NonNull String realm,
@NonNull String groupId,
@Nullable GetGroupsParameters parameters) {
Assert.hasText(realm, "Realm must not be null or empty.");
Assert.hasText(groupId, "Group ID must not be null or empty.");
GetGroupsParameters params = requireNonNullElseGet(parameters, GetGroupsParameters::defaults);
return adminApi.adminRealmsRealmGroupsGroupIdChildrenGet(
realm,
groupId,
params.getBriefRepresentation(),
params.getExact(),
params.getFirst(),
params.getMax(),
params.getSearch(),
params.getSubGroupsCount());
}
/**
* Deletes group.
*
* @param realm the realm name
* @param groupId the group ID
* @return void
*/
public Mono<Void> deleteGroup(@NonNull String realm, @NonNull String groupId) {
return adminApi.adminRealmsRealmGroupsGroupIdDelete(realm, groupId);
}
}