Hi, I’m learning how to implement a custom storage provider for Keycloak (straight from a database) and so far Keycloak can activate and use my provider, but when listing users, it only shows users in the first page and going to next page shows nothing. This is Keycloak 26.3.3, I’m doing pagination in the DB and I’ve observed that the getUSersCount() method never gets called.
What am I doing wrong?
My provider is implemented as follows:
//package and imports removed for clarity
public class ConsaludSdUserProvider implements UserStorageProvider, UserLookupProvider, UserQueryProvider, CredentialInputValidator, CredentialInputUpdater {
private static final Logger LOG = Logger.getLogger(ConsaludSdUserProvider.class);
private final KeycloakSession session;
private final ComponentModel model;
private final ISdUsuariosClient client;
private final Argon2idEncryptor encryptor;
protected Map<String, UserModel> loadedUsers = new HashMap<>();
public ConsaludSdUserProvider(KeycloakSession session, ComponentModel model) {
this.session = session;
this.model = model;
this.client = new PostgresSdUsuariosClient(model.get(Constants.CONN_STRING_FIELD_NAME), model.get(Constants.USERID_FIELD_NAME), model.get(Constants.PASSWORD_FIELD_NAME));
this.encryptor = new Argon2idEncryptor(model.get(Constants.HASH_SECRET_FIELD_NAME));
}
//region UserStorageProvider
//- removed for clarity
//endregion
//region UserLookupProvider
@Override
public UserModel getUserById(RealmModel realmModel, String s) {
LOG.info("getUserById invocado. ID: '" + s + "'");
try {
var adapter = loadedUsers.get(s);
if (adapter != null) {
return adapter;
}
var external_id = getRutFromKeycloakId(s);
var entity = client.getUsuarioByRut(external_id);
if (entity == null) {
LOG.info("No se encontró usuario con id: " + s);
return null;
}
adapter = new DatosUsuarioUserAdapter(session, realmModel, model, entity);
loadedUsers.put(s, adapter);
return adapter;
} catch (SdUsuariosClientException e) {
LOG.error("Error al traer usuario por RUT", e);
return null;
}
}
@Override
public UserModel getUserByUsername(RealmModel realmModel, String s) {
LOG.info("getUserByUsername invocado. Username: '" + s + "'");
try {
var adapter = loadedUsers.get(s);
if (adapter != null) {
return adapter;
}
var external_id = Integer.parseInt(s);
var entity = client.getUsuarioByRut(external_id);
if (entity == null) {
LOG.info("No se encontró usuario con username: " + s);
return null;
}
adapter = new DatosUsuarioUserAdapter(session, realmModel, model, entity);
loadedUsers.put(s, adapter);
return adapter;
} catch (SdUsuariosClientException e) {
LOG.error("Error al traer usuario por RUT", e);
return null;
} catch (NumberFormatException e) {
LOG.error("Error al convertir ID proporcionado a un RUT", e);
return null;
}
}
@Override
public CredentialValidationOutput getUserByCredential(RealmModel realm, CredentialInput input) {
LOG.info("getUserByCredential invocado. Credential input: '" + input.getType() + "'");
return UserLookupProvider.super.getUserByCredential(realm, input);
}
@Override
public UserModel getUserByEmail(RealmModel realmModel, String s) {
LOG.info("getUserByEmail invocado. Email: '" + s + "'");
try {
var entity = client.getUsuarioPorEmail(s);
if (entity == null) {
LOG.info("No se encontró usuario con email: " + s);
return null;
}
return new DatosUsuarioUserAdapter(session, realmModel, model, entity);
} catch (SdUsuariosClientException e) {
LOG.error("Error al traer usuario por RUT", e);
return null;
}
}
//endregion
//region UserQueryProvider
@Override
public Stream<UserModel> searchForUserStream(RealmModel realmModel, Map<String, String> params, Integer firstResult, Integer maxResults) {
var search_param = params.get(UserModel.SEARCH);
LOG.info("searchForUserStream invocado. search_param: '" + search_param + "', fila inicial: " + firstResult + ", max results: " + maxResults);
try {
var datos = client.getUsuarios(search_param, firstResult, maxResults);
if (datos == null) {
LOG.info("No se encontraron usuarios por el parámetro de búsqueda indicado: " + search_param);
return Stream.empty();
}
return datos.stream().map(r -> new DatosUsuarioUserAdapter(session, realmModel, model, r));
} catch (SdUsuariosClientException e) {
LOG.info("Error al tratar de encontrar usuarios por parámetro de búsqueda: " + search_param, e);
return Stream.empty();
}
}
@Override
public Stream<UserModel> getGroupMembersStream(RealmModel realmModel, GroupModel groupModel, Integer firstResult, Integer maxResults) {
LOG.info("getGroupMembersStream invocado. Fila inicial: " + firstResult + ", max results: " + maxResults);
return Stream.empty();
}
@Override
public Stream<UserModel> searchForUserByUserAttributeStream(RealmModel realmModel, String attrName, String attrValue) {
LOG.info("searchForUserByUserAttributeStream invocado. Nombre atributo: " + attrName + ", valor atributo: " + attrValue);
throw new NotImplementedException("");
}
@Override
public int getUsersCount(RealmModel realm) {
LOG.info("getUsersCount invocado.");
try {
return client.getUsuariosCount();
} catch (SdUsuariosClientException e) {
LOG.error("Error al traer conteo de usuarios", e);
return 0;
}
}
@Override
public int getUsersCount(RealmModel realm, Set<String> groupIds) {
LOG.info("getUsersCount con groupIds invocado.");
return UserQueryProvider.super.getUsersCount(realm, groupIds);
}
//endregion
//region CredentialInputValidator
//- removed for clarity
//endregion
//region CredentialInputUpdater
//- removed for clarity
//endregion
private Integer getRutFromKeycloakId(String keycloakId) {
var external_id = StorageId.externalId(keycloakId);
return Integer.parseInt(external_id);
}
}