Update documentation on user storage provider in Quarkus

Closes #17394
This commit is contained in:
Martin Kanis 2023-04-11 16:48:05 +02:00 committed by Michal Hajas
parent 9640fb2803
commit 31557f649f
7 changed files with 81 additions and 83 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

View file

@ -15,15 +15,46 @@ in {project_name}'s database. This is called federated user storage and is enca
----
package org.keycloak.storage.federated;
public interface UserFederatedStorageProvider extends Provider {
public interface UserFederatedStorageProvider extends Provider,
UserAttributeFederatedStorage,
UserBrokerLinkFederatedStorage,
UserConsentFederatedStorage,
UserNotBeforeFederatedStorage,
UserGroupMembershipFederatedStorage,
UserRequiredActionsFederatedStorage,
UserRoleMappingsFederatedStorage,
UserFederatedUserCredentialStore {
...
Set<GroupModel> getGroups(RealmModel realm, String userId);
Stream<GroupModel> getGroupsStream(RealmModel realm, String userId)
void joinGroup(RealmModel realm, String userId, GroupModel group);
void leaveGroup(RealmModel realm, String userId, GroupModel group);
List<String> getMembership(RealmModel realm, GroupModel group, int firstResult, int max);
Stream<String> getMembershipStream(RealmModel realm, GroupModel group, Integer firstResult, Integer max);
...
...
interface Streams extends UserFederatedStorageProvider,
UserAttributeFederatedStorage.Streams,
UserBrokerLinkFederatedStorage.Streams,
UserConsentFederatedStorage.Streams,
UserFederatedUserCredentialStore.Streams,
UserGroupMembershipFederatedStorage.Streams,
UserRequiredActionsFederatedStorage.Streams,
UserRoleMappingsFederatedStorage.Streams {
...
@Override
default List<String> getStoredUsers(RealmModel realm, int first, int max) {
return this.getStoredUsersStream(realm, first, max).collect(Collectors.toList());
}
@Override
Stream<String> getStoredUsersStream(RealmModel realm, Integer first, Integer max);
...
}
}
----
The `UserFederatedStorageProvider` instance is available on the `KeycloakSession.userFederatedStorage()` method.
@ -31,6 +62,11 @@ It has all different kinds of methods for storing attributes, group and role map
and required actions. If your external store's datamodel cannot support the full {project_name} feature
set, then this service can fill in the gaps.
Also, the `UserFederatedStorageProvider.Streams` interface exists. It makes all collection-based methods in `UserFederatedStorageProvider`
default by providing implementations that delegate to the stream-based variants instead of the other way around.
It allows for implementations to focus on the stream-based approach for processing sets of data and benefit
from the potential memory and performance optimizations of that approach. See <<_stream_based_interfaces,Stream-based interfaces>> for more information.
{project_name} comes with a helper class `org.keycloak.storage.adapter.AbstractUserAdapterFederatedStorage`
that will delegate every single `UserModel` method except get/set of username to user federated storage. Override
the methods you need to override to delegate to your external storage representations. It is strongly

View file

@ -76,7 +76,9 @@ The next thing we want to do is to verify that this file exists on disk. We do n
}
----
In the `validateConfiguration()` method we get the configuration variable from the `ComponentModel` and we check to see if that file exists on disk. Notice that we use the `org.keycloak.common.util.EnvUtil.replace()` method. With this method any string that has `${}` within it will replace that with a system property value. The `${jboss.server.config.dir}` string corresponds to the `configuration/` directory of our server and is really useful for this example.
The `validateConfiguration()` method provides the configuration variable from the `ComponentModel` to verify if that file exists on disk.
Notice that the use of the `org.keycloak.common.util.EnvUtil.replace()` method. With this method any string that includes `${}` will replace that value with a system property value.
The `${jboss.server.config.dir}` string corresponds to the `conf/` directory of our server and is really useful for this example.
Next thing we have to do is remove the old `init()` method. We do this because user property files are going to be unique per provider instance. We move this logic to the `create()` method.
@ -107,5 +109,5 @@ Now that the configuration is enabled, you can set the `path` variable when you
ifeval::[{project_community}==true]
.Configured Provider
image:images/storage-provider-with-config.png[]
image:images/readonly-user-storage-provider-with-config.png[]
endif::[]

View file

@ -82,7 +82,7 @@ Since we can now save our property file, it also makes sense to allow password u
@Override
public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) {
if (!(input instanceof UserCredentialModel)) return false;
if (!input.getType().equals(CredentialModel.PASSWORD)) return false;
if (!input.getType().equals(PasswordCredentialModel.TYPE)) return false;
UserCredentialModel cred = (UserCredentialModel)input;
synchronized (properties) {
properties.setProperty(user.getUsername(), cred.getValue());
@ -99,7 +99,7 @@ We can now also implement disabling a password.
----
@Override
public void disableCredentialType(RealmModel realm, UserModel user, String credentialType) {
if (!credentialType.equals(CredentialModel.PASSWORD)) return;
if (!credentialType.equals(PasswordCredentialModel.TYPE)) return;
synchronized (properties) {
properties.setProperty(user.getUsername(), UNSET_PASSWORD);
save();
@ -110,13 +110,13 @@ We can now also implement disabling a password.
private static final Set<String> disableableTypes = new HashSet<>();
static {
disableableTypes.add(CredentialModel.PASSWORD);
disableableTypes.add(PasswordCredentialModel.TYPE);
}
@Override
public Set<String> getDisableableCredentialTypes(RealmModel realm, UserModel user) {
public Stream<String> getDisableableCredentialTypes(RealmModel realm, UserModel user) {
return disableableTypes;
return disableableTypes.stream();
}
----
@ -136,95 +136,55 @@ by our example provider. Let's look at implementing this interface.
}
@Override
public List<UserModel> getUsers(RealmModel realm) {
return getUsers(realm, 0, Integer.MAX_VALUE);
}
@Override
public List<UserModel> getUsers(RealmModel realm, int firstResult, int maxResults) {
List<UserModel> users = new LinkedList<>();
int i = 0;
for (Object obj : properties.keySet()) {
if (i++ < firstResult) continue;
String username = (String)obj;
UserModel user = getUserByUsername(username, realm);
users.add(user);
if (users.size() >= maxResults) break;
}
return users;
public Stream<UserModel> searchForUserStream(RealmModel realm, String search, Integer firstResult, Integer maxResults) {
Predicate<String> predicate = "*".equals(search) ? username -> true : username -> username.contains(search);
return properties.keySet().stream()
.map(String.class::cast)
.filter(predicate)
.skip(firstResult)
.map(username -> getUserByUsername(realm, username))
.limit(maxResults);
}
----
The `getUsers()` method iterates over the key set of the property file, delegating to `getUserByUsername()` to load a user.
The first declaration of `searchForUserStream()` takes a `String` parameter. In this example, the parameter represents a username that you want to search by. This string can be a substring, which explains the choice of the `String.contains()`
method when doing the search. Notice the use of `*` to indicate to request a list of all users.
The method iterates over the key set of the property file, delegating to `getUserByUsername()` to load a user.
Notice that we are indexing this call based on the `firstResult` and `maxResults` parameter. If your external store does not support pagination, you will have to do similar logic.
.PropertyFileUserStorageProvider
[source,java]
----
@Override
public List<UserModel> searchForUser(String search, RealmModel realm) {
return searchForUser(search, realm, 0, Integer.MAX_VALUE);
}
@Override
public List<UserModel> searchForUser(String search, RealmModel realm, int firstResult, int maxResults) {
List<UserModel> users = new LinkedList<>();
int i = 0;
for (Object obj : properties.keySet()) {
String username = (String)obj;
if (!username.contains(search)) continue;
if (i++ < firstResult) continue;
UserModel user = getUserByUsername(username, realm);
users.add(user);
if (users.size() >= maxResults) break;
}
return users;
}
----
The first declaration of `searchForUser()` takes a `String` parameter. This is supposed to be a string that you use to
search username and email attributes to find the user. This string can be a substring, which is why we use the `String.contains()`
method when doing our search.
.PropertyFileUserStorageProvider
[source,java]
----
@Override
public List<UserModel> searchForUser(Map<String, String> params, RealmModel realm) {
return searchForUser(params, realm, 0, Integer.MAX_VALUE);
}
@Override
public List<UserModel> searchForUser(Map<String, String> params, RealmModel realm, int firstResult, int maxResults) {
public Stream<UserModel> searchForUserStream(RealmModel realm, Map<String, String> params, Integer firstResult, Integer maxResults) {
// only support searching by username
String usernameSearchString = params.get("username");
if (usernameSearchString == null) return Collections.EMPTY_LIST;
return searchForUser(usernameSearchString, realm, firstResult, maxResults);
if (usernameSearchString != null)
return searchForUserStream(realm, usernameSearchString, firstResult, maxResults);
// if we are not searching by username, return all users
return searchForUserStream(realm, "*", firstResult, maxResults);
}
----
The `searchForUser()` method that takes a `Map` parameter can search for a user based on first, last name, username, and email.
We only store usernames, so we only search based on usernames. We delegate to `searchForUser()` for this.
The `searchForUserStream()` method that takes a `Map` parameter can search for a user based on first, last name, username, and email.
Only usernames are stored, so the search is based only on usernames except when the `Map` parameter does not contain the `username` attribute.
In this case, all users are returned. In that situation, the `searchForUserStream(realm, search, firstResult, maxResults)` is used.
.PropertyFileUserStorageProvider
[source,java]
----
@Override
public List<UserModel> getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults) {
return Collections.EMPTY_LIST;
public Stream<UserModel> getGroupMembersStream(RealmModel realm, GroupModel group, Integer firstResult, Integer maxResults) {
return Stream.empty();
}
@Override
public List<UserModel> getGroupMembers(RealmModel realm, GroupModel group) {
return Collections.EMPTY_LIST;
}
@Override
public List<UserModel> searchForUserByUserAttribute(String attrName, String attrValue, RealmModel realm) {
return Collections.EMPTY_LIST;
public Stream<UserModel> searchForUserByUserAttributeStream(RealmModel realm, String attrName, String attrValue) {
return Stream.empty();
}
----
We do not store groups or attributes, so the other methods return an empty list.
Groups or attributes are not stored, so the other methods return an empty stream.

View file

@ -42,7 +42,7 @@ The constructor for this provider class is going to store the reference to the `
[source,java]
----
@Override
public UserModel getUserByUsername(String username, RealmModel realm) {
public UserModel getUserByUsername(RealmModel realm, String username) {
UserModel adapter = loadedUsers.get(username);
if (adapter == null) {
String password = properties.getProperty(username);
@ -64,14 +64,14 @@ The constructor for this provider class is going to store the reference to the `
}
@Override
public UserModel getUserById(String id, RealmModel realm) {
public UserModel getUserById(RealmModel realm, String id) {
StorageId storageId = new StorageId(id);
String username = storageId.getExternalId();
return getUserByUsername(username, realm);
return getUserByUsername(realm, username);
}
@Override
public UserModel getUserByEmail(String email, RealmModel realm) {
public UserModel getUserByEmail(RealmModel realm, String email) {
return null;
}
@ -144,8 +144,8 @@ As noted before, the only reason we implement the `CredentialInputUpdater` inter
}
@Override
public Set<String> getDisableableCredentialTypes(RealmModel realm, UserModel user) {
return Collections.EMPTY_SET;
public Stream<String> getDisableableCredentialTypesStream(RealmModel realm, UserModel user) {
return Stream.empty();
}
----

View file

@ -1,4 +1,4 @@
[[_stream_based_interfaces]]
=== Stream-based interfaces
Many of the user storage interfaces in {project_name} contain query methods that can return potentially large sets of objects,