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 |
|
@ -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
|
||||
|
|
|
@ -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::[]
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
----
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue