Split UserQueryProvider
into UserQueryMethods
and UserCountMethods
and make LdapStorageProvider
implement only UserQueryMethods
Co-authored-by: mhajas <mhajas@redhat.com> Closed #20156
This commit is contained in:
parent
d76c295c09
commit
a175efcb72
11 changed files with 571 additions and 358 deletions
|
@ -9,7 +9,9 @@ If you have examined the `UserStorageProvider` interface closely you might notic
|
||||||
|SPI|Description
|
|SPI|Description
|
||||||
|
|
||||||
|`org.keycloak.storage.user.UserLookupProvider`|This interface is required if you want to be able to log in with users from this external store. Most (all?) providers implement this interface.
|
|`org.keycloak.storage.user.UserLookupProvider`|This interface is required if you want to be able to log in with users from this external store. Most (all?) providers implement this interface.
|
||||||
|`org.keycloak.storage.user.UserQueryProvider`|Defines complex queries that are used to locate one or more users. You must implement this interface if you want to view and manage users from the administration console.
|
|`org.keycloak.storage.user.UserQueryMethodsProvider`|Defines complex queries that are used to locate one or more users. You must implement this interface if you want to view and manage users from the administration console.
|
||||||
|
|`org.keycloak.storage.user.UserCountMethodsProvider`|Implement this interface if your provider supports count queries.
|
||||||
|
|`org.keycloak.storage.user.UserQueryProvider`|This interface is combined capability of `UserQueryMethodsProvider` and `UserCountMethodsProvider`.
|
||||||
|`org.keycloak.storage.user.UserRegistrationProvider`|Implement this interface if your provider supports adding and removing users.
|
|`org.keycloak.storage.user.UserRegistrationProvider`|Implement this interface if your provider supports adding and removing users.
|
||||||
|`org.keycloak.storage.user.UserBulkUpdateProvider`|Implement this interface if your provider supports bulk update of a set of users.
|
|`org.keycloak.storage.user.UserBulkUpdateProvider`|Implement this interface if your provider supports bulk update of a set of users.
|
||||||
|`org.keycloak.credential.CredentialInputValidator`|Implement this interface if your provider can validate one or more different credential types (for example, if your provider can validate a password).
|
|`org.keycloak.credential.CredentialInputValidator`|Implement this interface if your provider can validate one or more different credential types (for example, if your provider can validate a password).
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
One thing we have not done with our example is allow it to add and remove users or change passwords. Users defined in our example are
|
One thing we have not done with our example is allow it to add and remove users or change passwords. Users defined in our example are
|
||||||
also not queryable or viewable in the Admin Console. To add these enhancements, our example provider must implement
|
also not queryable or viewable in the Admin Console. To add these enhancements, our example provider must implement
|
||||||
the `UserQueryProvider` and `UserRegistrationProvider` interfaces.
|
the `UserQueryMethodsProvider` (or `UserQueryProvider`) and `UserRegistrationProvider` interfaces.
|
||||||
|
|
||||||
==== Implementing UserRegistrationProvider
|
==== Implementing UserRegistrationProvider
|
||||||
|
|
||||||
|
@ -124,7 +124,7 @@ With these methods implemented, you'll now be able to change and disable the pas
|
||||||
|
|
||||||
==== Implementing UserQueryProvider
|
==== Implementing UserQueryProvider
|
||||||
|
|
||||||
Without implementing `UserQueryProvider` the Admin Console would not be able to view and manage users that were loaded
|
`UserQueryProvider` is combination of `UserQueryMethodsProvider` and `UserCountMethodsProvider`. Without implementing `UserQueryMethodsProvider` the Admin Console would not be able to view and manage users that were loaded
|
||||||
by our example provider. Let's look at implementing this interface.
|
by our example provider. Let's look at implementing this interface.
|
||||||
|
|
||||||
.PropertyFileUserStorageProvider
|
.PropertyFileUserStorageProvider
|
||||||
|
|
|
@ -191,11 +191,19 @@ The implementation has now been unified for all themes. In general, the message
|
||||||
|
|
||||||
Probably this can be better explained with an example: When the variant `de-CH-1996` is requested and there is a realm localization message for the variant, this message will be used. If such a realm localization message does not exist, the Theme i18n files are searched for a corresponding message for that variant. If such a message does not exist, a realm localization message for the region (`de-CH`) will be searched. If such a realm localization message does not exist, the Theme i18n files are searched for a message for that region. If still no message is found, a realm localization message for the language (`de`) will be searched. If there is no matching realm localization message, the Theme i18n files are be searched for a message for that language. As last fallback, the English (`en`) translation is used: First, an English realm localization will be searched - if not found, the Theme 18n files are searched for an English message.
|
Probably this can be better explained with an example: When the variant `de-CH-1996` is requested and there is a realm localization message for the variant, this message will be used. If such a realm localization message does not exist, the Theme i18n files are searched for a corresponding message for that variant. If such a message does not exist, a realm localization message for the region (`de-CH`) will be searched. If such a realm localization message does not exist, the Theme i18n files are searched for a message for that region. If still no message is found, a realm localization message for the language (`de`) will be searched. If there is no matching realm localization message, the Theme i18n files are be searched for a message for that language. As last fallback, the English (`en`) translation is used: First, an English realm localization will be searched - if not found, the Theme 18n files are searched for an English message.
|
||||||
|
|
||||||
= LDAPStorageProvider search changes
|
= `UserQueryProvider` changes
|
||||||
|
|
||||||
|
`UserQueryProvider` interface was split into two. One is `UserQueryMethodsProvider` providing capabilities for querying users. Second one is `UserCountMethodsProvider` which provides capability for counting number of users in particular storage.
|
||||||
|
|
||||||
|
Keycloak now has the ability to differentiate between user storage providers that can efficiently execute count queries and those that cannot. The `UserQueryProvider` interface still exists and extends both new interfaces. Therefore, there is no need for any modifications in the existing implementations of `UserQueryProvider` since it retains the same methods.
|
||||||
|
|
||||||
|
= `LDAPStorageProvider` search changes
|
||||||
|
|
||||||
Starting with this release Keycloak uses a pagination mechanism when querying federated LDAP database.
|
Starting with this release Keycloak uses a pagination mechanism when querying federated LDAP database.
|
||||||
Searching for users should be consistent with search in local database.
|
Searching for users should be consistent with search in local database.
|
||||||
|
|
||||||
|
Since this release `LDAPStorageProvider` implements only `UserQueryMethodsProvider`, not `UserQueryProvider`.
|
||||||
|
|
||||||
= Deprecation of Keycloak OpenID Connect Adapters
|
= Deprecation of Keycloak OpenID Connect Adapters
|
||||||
|
|
||||||
Starting with this release, we no longer will invest our time on the following Keycloak OpenID Connect Adapters:
|
Starting with this release, we no longer will invest our time on the following Keycloak OpenID Connect Adapters:
|
||||||
|
|
|
@ -18,7 +18,6 @@
|
||||||
package org.keycloak.storage.ldap;
|
package org.keycloak.storage.ldap;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
|
@ -84,7 +83,7 @@ import org.keycloak.storage.ldap.mappers.LDAPStorageMapperManager;
|
||||||
import org.keycloak.storage.ldap.mappers.PasswordUpdateCallback;
|
import org.keycloak.storage.ldap.mappers.PasswordUpdateCallback;
|
||||||
import org.keycloak.storage.user.ImportedUserValidation;
|
import org.keycloak.storage.user.ImportedUserValidation;
|
||||||
import org.keycloak.storage.user.UserLookupProvider;
|
import org.keycloak.storage.user.UserLookupProvider;
|
||||||
import org.keycloak.storage.user.UserQueryProvider;
|
import org.keycloak.storage.user.UserQueryMethodsProvider;
|
||||||
import org.keycloak.storage.user.UserRegistrationProvider;
|
import org.keycloak.storage.user.UserRegistrationProvider;
|
||||||
|
|
||||||
import static org.keycloak.utils.StreamsUtil.paginatedStream;
|
import static org.keycloak.utils.StreamsUtil.paginatedStream;
|
||||||
|
@ -100,7 +99,7 @@ public class LDAPStorageProvider implements UserStorageProvider,
|
||||||
CredentialAuthentication,
|
CredentialAuthentication,
|
||||||
UserLookupProvider,
|
UserLookupProvider,
|
||||||
UserRegistrationProvider,
|
UserRegistrationProvider,
|
||||||
UserQueryProvider,
|
UserQueryMethodsProvider,
|
||||||
ImportedUserValidation {
|
ImportedUserValidation {
|
||||||
private static final Logger logger = Logger.getLogger(LDAPStorageProvider.class);
|
private static final Logger logger = Logger.getLogger(LDAPStorageProvider.class);
|
||||||
private static final int DEFAULT_MAX_RESULTS = Integer.MAX_VALUE >> 1;
|
private static final int DEFAULT_MAX_RESULTS = Integer.MAX_VALUE >> 1;
|
||||||
|
@ -344,36 +343,6 @@ public class LDAPStorageProvider implements UserStorageProvider,
|
||||||
return getUserByUsername(realm, storageId.getExternalId());
|
return getUserByUsername(realm, storageId.getExternalId());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getUsersCount(RealmModel realm) {
|
|
||||||
return getUsersCount(realm, Collections.emptyMap());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getUsersCount(RealmModel realm, String search) {
|
|
||||||
return (int) searchLDAP(realm, search, null, null).filter(filterLocalUsers(realm)).count();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getUsersCount(RealmModel realm, Map<String, String> params) {
|
|
||||||
return (int) searchLDAPByAttributes(realm, params, null, null).filter(filterLocalUsers(realm)).count();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getUsersCount(RealmModel realm, Set<String> groupIds) {
|
|
||||||
throw new UnsupportedOperationException("Not implemented");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getUsersCount(RealmModel realm, String search, Set<String> groupIds) {
|
|
||||||
throw new UnsupportedOperationException("Not implemented");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getUsersCount(RealmModel realm, Map<String, String> params, Set<String> groupIds) {
|
|
||||||
throw new UnsupportedOperationException("Not implemented");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Stream<UserModel> searchForUserStream(RealmModel realm, String search, Integer firstResult, Integer maxResults) {
|
public Stream<UserModel> searchForUserStream(RealmModel realm, String search, Integer firstResult, Integer maxResults) {
|
||||||
return searchForUserStream(realm, Map.of(UserModel.SEARCH, search), firstResult, maxResults);
|
return searchForUserStream(realm, Map.of(UserModel.SEARCH, search), firstResult, maxResults);
|
||||||
|
|
|
@ -27,6 +27,7 @@ import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
|
@ -64,7 +65,9 @@ import org.keycloak.storage.federated.UserFederatedStorageProvider;
|
||||||
import org.keycloak.storage.managers.UserStorageSyncManager;
|
import org.keycloak.storage.managers.UserStorageSyncManager;
|
||||||
import org.keycloak.storage.user.ImportedUserValidation;
|
import org.keycloak.storage.user.ImportedUserValidation;
|
||||||
import org.keycloak.storage.user.UserBulkUpdateProvider;
|
import org.keycloak.storage.user.UserBulkUpdateProvider;
|
||||||
|
import org.keycloak.storage.user.UserCountMethodsProvider;
|
||||||
import org.keycloak.storage.user.UserLookupProvider;
|
import org.keycloak.storage.user.UserLookupProvider;
|
||||||
|
import org.keycloak.storage.user.UserQueryMethodsProvider;
|
||||||
import org.keycloak.storage.user.UserQueryProvider;
|
import org.keycloak.storage.user.UserQueryProvider;
|
||||||
import org.keycloak.storage.user.UserRegistrationProvider;
|
import org.keycloak.storage.user.UserRegistrationProvider;
|
||||||
|
|
||||||
|
@ -202,7 +205,7 @@ public class UserStorageManager extends AbstractStorageManager<UserStorageProvid
|
||||||
protected Stream<UserModel> query(PaginatedQuery pagedQuery, CountQuery countQuery, RealmModel realm, Integer firstResult, Integer maxResults) {
|
protected Stream<UserModel> query(PaginatedQuery pagedQuery, CountQuery countQuery, RealmModel realm, Integer firstResult, Integer maxResults) {
|
||||||
if (maxResults != null && maxResults == 0) return Stream.empty();
|
if (maxResults != null && maxResults == 0) return Stream.empty();
|
||||||
|
|
||||||
Stream<Object> providersStream = Stream.concat(Stream.of((Object) localStorage()), getEnabledStorageProviders(realm, UserQueryProvider.class));
|
Stream<Object> providersStream = Stream.concat(Stream.of((Object) localStorage()), getEnabledStorageProviders(realm, UserQueryMethodsProvider.class));
|
||||||
|
|
||||||
UserFederatedStorageProvider federatedStorageProvider = getFederatedStorage();
|
UserFederatedStorageProvider federatedStorageProvider = getFederatedStorage();
|
||||||
if (federatedStorageProvider != null) {
|
if (federatedStorageProvider != null) {
|
||||||
|
@ -210,10 +213,12 @@ public class UserStorageManager extends AbstractStorageManager<UserStorageProvid
|
||||||
}
|
}
|
||||||
|
|
||||||
final AtomicInteger currentFirst;
|
final AtomicInteger currentFirst;
|
||||||
|
final AtomicBoolean needsAdditionalFirstResultFiltering = new AtomicBoolean(false);
|
||||||
|
|
||||||
if (firstResult == null || firstResult <= 0) { // We don't want to skip any users so we don't need to do firstResult filtering
|
if (firstResult == null || firstResult <= 0) { // We don't want to skip any users so we don't need to do firstResult filtering
|
||||||
currentFirst = new AtomicInteger(0);
|
currentFirst = new AtomicInteger(0);
|
||||||
} else {
|
} else {
|
||||||
|
// This is an optimization using count query to skip querying users if we can use count method to determine how many users can be provided by each provider
|
||||||
AtomicBoolean droppingProviders = new AtomicBoolean(true);
|
AtomicBoolean droppingProviders = new AtomicBoolean(true);
|
||||||
currentFirst = new AtomicInteger(firstResult);
|
currentFirst = new AtomicInteger(firstResult);
|
||||||
|
|
||||||
|
@ -221,7 +226,16 @@ public class UserStorageManager extends AbstractStorageManager<UserStorageProvid
|
||||||
.filter(provider -> { // This is basically dropWhile
|
.filter(provider -> { // This is basically dropWhile
|
||||||
if (!droppingProviders.get()) return true; // We have already gathered enough users to pass firstResult number in previous providers, we can take all following providers
|
if (!droppingProviders.get()) return true; // We have already gathered enough users to pass firstResult number in previous providers, we can take all following providers
|
||||||
|
|
||||||
|
if (!(provider instanceof UserCountMethodsProvider)) {
|
||||||
|
logger.tracef("We encountered a provider (%s) that does not implement count queries therefore we can't say how many users it can provide.", provider.getClass().getSimpleName());
|
||||||
|
// for this reason we need to start querying this provider and all following providers
|
||||||
|
droppingProviders.set(false);
|
||||||
|
needsAdditionalFirstResultFiltering.set(true);
|
||||||
|
return true; // don't filter out this provider because we are unable to say how many users it can provide
|
||||||
|
}
|
||||||
|
|
||||||
long expectedNumberOfUsersForProvider = countQuery.query(provider, 0, currentFirst.get() + 1); // check how many users we can obtain from this provider
|
long expectedNumberOfUsersForProvider = countQuery.query(provider, 0, currentFirst.get() + 1); // check how many users we can obtain from this provider
|
||||||
|
logger.tracef("This provider (%s) is able to return %d users.", provider.getClass().getSimpleName(), expectedNumberOfUsersForProvider);
|
||||||
|
|
||||||
if (expectedNumberOfUsersForProvider == currentFirst.get()) { // This provider provides exactly the amount of users we need for passing firstResult, we can set currentFirst to 0 and drop this provider
|
if (expectedNumberOfUsersForProvider == currentFirst.get()) { // This provider provides exactly the amount of users we need for passing firstResult, we can set currentFirst to 0 and drop this provider
|
||||||
currentFirst.set(0);
|
currentFirst.set(0);
|
||||||
|
@ -234,10 +248,31 @@ public class UserStorageManager extends AbstractStorageManager<UserStorageProvid
|
||||||
return true; // don't filter out this provider because we are going to return some users from it
|
return true; // don't filter out this provider because we are going to return some users from it
|
||||||
}
|
}
|
||||||
|
|
||||||
// This provider cannot provide enough users to pass firstResult so we are going to filter it out and change firstResult for next provider
|
logger.tracef("This provider (%s) cannot provide enough users to pass firstResult so we are going to filter it out and change "
|
||||||
|
+ "firstResult for next provider: %d - %d = %d", provider.getClass().getSimpleName(),
|
||||||
|
currentFirst.get(), expectedNumberOfUsersForProvider, currentFirst.get() - expectedNumberOfUsersForProvider);
|
||||||
currentFirst.set((int) (currentFirst.get() - expectedNumberOfUsersForProvider));
|
currentFirst.set((int) (currentFirst.get() - expectedNumberOfUsersForProvider));
|
||||||
return false;
|
return false;
|
||||||
});
|
})
|
||||||
|
// collecting stream of providers to ensure the filtering (above) is evaluated before we move forward to actual querying
|
||||||
|
.collect(Collectors.toList()).stream();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needsAdditionalFirstResultFiltering.get() && currentFirst.get() > 0) {
|
||||||
|
logger.tracef("In the providerStream there is a provider that does not support count queries and we need to skip some users.");
|
||||||
|
// we need to make sure, we skip firstResult users from this or the following providers
|
||||||
|
if (maxResults == null || maxResults < 0) {
|
||||||
|
return paginatedStream(providersStream
|
||||||
|
.flatMap(provider -> pagedQuery.query(provider, null, null)), currentFirst.get(), null);
|
||||||
|
} else {
|
||||||
|
final AtomicInteger currentMax = new AtomicInteger(currentFirst.get() + maxResults);
|
||||||
|
|
||||||
|
return paginatedStream(providersStream
|
||||||
|
.flatMap(provider -> pagedQuery.query(provider, null, currentMax.get()))
|
||||||
|
.peek(userModel -> {
|
||||||
|
currentMax.updateAndGet(i -> i > 0 ? i - 1 : i);
|
||||||
|
}), currentFirst.get(), maxResults);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Actual user querying
|
// Actual user querying
|
||||||
|
@ -355,8 +390,8 @@ public class UserStorageManager extends AbstractStorageManager<UserStorageProvid
|
||||||
@Override
|
@Override
|
||||||
public Stream<UserModel> getGroupMembersStream(final RealmModel realm, final GroupModel group, Integer firstResult, Integer maxResults) {
|
public Stream<UserModel> getGroupMembersStream(final RealmModel realm, final GroupModel group, Integer firstResult, Integer maxResults) {
|
||||||
Stream<UserModel> results = query((provider, firstResultInQuery, maxResultsInQuery) -> {
|
Stream<UserModel> results = query((provider, firstResultInQuery, maxResultsInQuery) -> {
|
||||||
if (provider instanceof UserQueryProvider) {
|
if (provider instanceof UserQueryMethodsProvider) {
|
||||||
return ((UserQueryProvider)provider).getGroupMembersStream(realm, group, firstResultInQuery, maxResultsInQuery);
|
return ((UserQueryMethodsProvider)provider).getGroupMembersStream(realm, group, firstResultInQuery, maxResultsInQuery);
|
||||||
|
|
||||||
} else if (provider instanceof UserFederatedStorageProvider) {
|
} else if (provider instanceof UserFederatedStorageProvider) {
|
||||||
return ((UserFederatedStorageProvider)provider).getMembershipStream(realm, group, firstResultInQuery, maxResultsInQuery).
|
return ((UserFederatedStorageProvider)provider).getMembershipStream(realm, group, firstResultInQuery, maxResultsInQuery).
|
||||||
|
@ -371,8 +406,8 @@ public class UserStorageManager extends AbstractStorageManager<UserStorageProvid
|
||||||
@Override
|
@Override
|
||||||
public Stream<UserModel> getRoleMembersStream(final RealmModel realm, final RoleModel role, Integer firstResult, Integer maxResults) {
|
public Stream<UserModel> getRoleMembersStream(final RealmModel realm, final RoleModel role, Integer firstResult, Integer maxResults) {
|
||||||
Stream<UserModel> results = query((provider, firstResultInQuery, maxResultsInQuery) -> {
|
Stream<UserModel> results = query((provider, firstResultInQuery, maxResultsInQuery) -> {
|
||||||
if (provider instanceof UserQueryProvider) {
|
if (provider instanceof UserQueryMethodsProvider) {
|
||||||
return ((UserQueryProvider)provider).getRoleMembersStream(realm, role, firstResultInQuery, maxResultsInQuery);
|
return ((UserQueryMethodsProvider)provider).getRoleMembersStream(realm, role, firstResultInQuery, maxResultsInQuery);
|
||||||
}
|
}
|
||||||
return Stream.empty();
|
return Stream.empty();
|
||||||
}, realm, firstResult, maxResults);
|
}, realm, firstResult, maxResults);
|
||||||
|
@ -382,7 +417,7 @@ public class UserStorageManager extends AbstractStorageManager<UserStorageProvid
|
||||||
@Override
|
@Override
|
||||||
public int getUsersCount(RealmModel realm, boolean includeServiceAccount) {
|
public int getUsersCount(RealmModel realm, boolean includeServiceAccount) {
|
||||||
int localStorageUsersCount = localStorage().getUsersCount(realm, includeServiceAccount);
|
int localStorageUsersCount = localStorage().getUsersCount(realm, includeServiceAccount);
|
||||||
int storageProvidersUsersCount = mapEnabledStorageProvidersWithTimeout(realm, UserQueryProvider.class,
|
int storageProvidersUsersCount = mapEnabledStorageProvidersWithTimeout(realm, UserCountMethodsProvider.class,
|
||||||
userQueryProvider -> userQueryProvider.getUsersCount(realm))
|
userQueryProvider -> userQueryProvider.getUsersCount(realm))
|
||||||
.reduce(0, Integer::sum);
|
.reduce(0, Integer::sum);
|
||||||
|
|
||||||
|
@ -422,13 +457,13 @@ public class UserStorageManager extends AbstractStorageManager<UserStorageProvid
|
||||||
@Override
|
@Override
|
||||||
public Stream<UserModel> searchForUserStream(RealmModel realm, String search, Integer firstResult, Integer maxResults) {
|
public Stream<UserModel> searchForUserStream(RealmModel realm, String search, Integer firstResult, Integer maxResults) {
|
||||||
Stream<UserModel> results = query((provider, firstResultInQuery, maxResultsInQuery) -> {
|
Stream<UserModel> results = query((provider, firstResultInQuery, maxResultsInQuery) -> {
|
||||||
if (provider instanceof UserQueryProvider) {
|
if (provider instanceof UserQueryMethodsProvider) {
|
||||||
return ((UserQueryProvider)provider).searchForUserStream(realm, search, firstResultInQuery, maxResultsInQuery);
|
return ((UserQueryMethodsProvider)provider).searchForUserStream(realm, search, firstResultInQuery, maxResultsInQuery);
|
||||||
}
|
}
|
||||||
return Stream.empty();
|
return Stream.empty();
|
||||||
}, (provider, firstResultInQuery, maxResultsInQuery) -> {
|
}, (provider, firstResultInQuery, maxResultsInQuery) -> {
|
||||||
if (provider instanceof UserQueryProvider) {
|
if (provider instanceof UserCountMethodsProvider) {
|
||||||
return ((UserQueryProvider)provider).getUsersCount(realm, search);
|
return ((UserCountMethodsProvider)provider).getUsersCount(realm, search);
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}, realm, firstResult, maxResults);
|
}, realm, firstResult, maxResults);
|
||||||
|
@ -438,21 +473,21 @@ public class UserStorageManager extends AbstractStorageManager<UserStorageProvid
|
||||||
@Override
|
@Override
|
||||||
public Stream<UserModel> searchForUserStream(RealmModel realm, Map<String, String> attributes, Integer firstResult, Integer maxResults) {
|
public Stream<UserModel> searchForUserStream(RealmModel realm, Map<String, String> attributes, Integer firstResult, Integer maxResults) {
|
||||||
Stream<UserModel> results = query((provider, firstResultInQuery, maxResultsInQuery) -> {
|
Stream<UserModel> results = query((provider, firstResultInQuery, maxResultsInQuery) -> {
|
||||||
if (provider instanceof UserQueryProvider) {
|
if (provider instanceof UserQueryMethodsProvider) {
|
||||||
if (attributes.containsKey(UserModel.SEARCH)) {
|
if (attributes.containsKey(UserModel.SEARCH)) {
|
||||||
return ((UserQueryProvider)provider).searchForUserStream(realm, attributes.get(UserModel.SEARCH), firstResultInQuery, maxResultsInQuery);
|
return ((UserQueryMethodsProvider)provider).searchForUserStream(realm, attributes.get(UserModel.SEARCH), firstResultInQuery, maxResultsInQuery);
|
||||||
} else {
|
} else {
|
||||||
return ((UserQueryProvider)provider).searchForUserStream(realm, attributes, firstResultInQuery, maxResultsInQuery);
|
return ((UserQueryMethodsProvider)provider).searchForUserStream(realm, attributes, firstResultInQuery, maxResultsInQuery);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Stream.empty();
|
return Stream.empty();
|
||||||
},
|
},
|
||||||
(provider, firstResultInQuery, maxResultsInQuery) -> {
|
(provider, firstResultInQuery, maxResultsInQuery) -> {
|
||||||
if (provider instanceof UserQueryProvider) {
|
if (provider instanceof UserCountMethodsProvider) {
|
||||||
if (attributes.containsKey(UserModel.SEARCH)) {
|
if (attributes.containsKey(UserModel.SEARCH)) {
|
||||||
return ((UserQueryProvider)provider).getUsersCount(realm, attributes.get(UserModel.SEARCH));
|
return ((UserCountMethodsProvider)provider).getUsersCount(realm, attributes.get(UserModel.SEARCH));
|
||||||
} else {
|
} else {
|
||||||
return ((UserQueryProvider)provider).getUsersCount(realm, attributes);
|
return ((UserCountMethodsProvider)provider).getUsersCount(realm, attributes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
|
@ -464,8 +499,8 @@ public class UserStorageManager extends AbstractStorageManager<UserStorageProvid
|
||||||
@Override
|
@Override
|
||||||
public Stream<UserModel> searchForUserByUserAttributeStream(RealmModel realm, String attrName, String attrValue) {
|
public Stream<UserModel> searchForUserByUserAttributeStream(RealmModel realm, String attrName, String attrValue) {
|
||||||
Stream<UserModel> results = query((provider, firstResultInQuery, maxResultsInQuery) -> {
|
Stream<UserModel> results = query((provider, firstResultInQuery, maxResultsInQuery) -> {
|
||||||
if (provider instanceof UserQueryProvider) {
|
if (provider instanceof UserQueryMethodsProvider) {
|
||||||
return paginatedStream(((UserQueryProvider)provider).searchForUserByUserAttributeStream(realm, attrName, attrValue), firstResultInQuery, maxResultsInQuery);
|
return paginatedStream(((UserQueryMethodsProvider)provider).searchForUserByUserAttributeStream(realm, attrName, attrValue), firstResultInQuery, maxResultsInQuery);
|
||||||
} else if (provider instanceof UserFederatedStorageProvider) {
|
} else if (provider instanceof UserFederatedStorageProvider) {
|
||||||
return paginatedStream(((UserFederatedStorageProvider)provider).getUsersByUserAttributeStream(realm, attrName, attrValue)
|
return paginatedStream(((UserFederatedStorageProvider)provider).getUsersByUserAttributeStream(realm, attrName, attrValue)
|
||||||
.map(id -> getUserById(realm, id))
|
.map(id -> getUserById(realm, id))
|
||||||
|
|
|
@ -28,7 +28,9 @@ import org.keycloak.provider.Provider;
|
||||||
* are extended by implementing one or more of the following capability interfaces:
|
* are extended by implementing one or more of the following capability interfaces:
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>{@link org.keycloak.storage.user.UserLookupProvider UserLookupProvider} - Provide basic lookup methods. After implementing it is possible to login using users from the storage.</li>
|
* <li>{@link org.keycloak.storage.user.UserLookupProvider UserLookupProvider} - Provide basic lookup methods. After implementing it is possible to login using users from the storage.</li>
|
||||||
* <li>{@link org.keycloak.storage.user.UserQueryProvider UserQueryProvider} - Provide complex lookup methods. After implementing it is possible to manage users from admin console.</li>
|
* <li>{@link org.keycloak.storage.user.UserQueryMethodsProvider UserQueryMethodsProvider} - Provide complex lookup methods. After implementing it is possible to manage users from admin console.</li>
|
||||||
|
* <li>{@link org.keycloak.storage.user.UserCountMethodsProvider UserCountMethodsProvider} - Provide complex count methods. After implementing it is possible to leverage optimizations during querying for users.</li>
|
||||||
|
* <li>{@link org.keycloak.storage.user.UserQueryProvider UserQueryProvider} - This interface is combined capability of {@code UserQueryMethodsProvider} and {@code UserCountMethodsProvider}.</li>
|
||||||
* <li>{@link org.keycloak.storage.user.UserRegistrationProvider UserRegistrationProvider} - Provide methods for adding users. After implementing it is possible to store registered users in the storage.</li>
|
* <li>{@link org.keycloak.storage.user.UserRegistrationProvider UserRegistrationProvider} - Provide methods for adding users. After implementing it is possible to store registered users in the storage.</li>
|
||||||
* <li>{@link org.keycloak.storage.user.UserBulkUpdateProvider UserBulkUpdateProvider} - After implementing it is possible to perform bulk operations on all users from storage (for example, addition of a role to all users).</li>
|
* <li>{@link org.keycloak.storage.user.UserBulkUpdateProvider UserBulkUpdateProvider} - After implementing it is possible to perform bulk operations on all users from storage (for example, addition of a role to all users).</li>
|
||||||
* <li>{@link org.keycloak.storage.user.ImportedUserValidation ImportedUserValidation} - Provider method for validating users within Keycloak local storage that are imported from the storage.</li>
|
* <li>{@link org.keycloak.storage.user.ImportedUserValidation ImportedUserValidation} - Provider method for validating users within Keycloak local storage that are imported from the storage.</li>
|
||||||
|
|
|
@ -0,0 +1,149 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* 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.keycloak.storage.user;
|
||||||
|
|
||||||
|
import org.keycloak.models.GroupModel;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is an optional capability interface that is intended to be implemented by
|
||||||
|
* <code>UserStorageProvider</code> that supports count queries.
|
||||||
|
*
|
||||||
|
* By implementing this interface, count queries could be leveraged when querying users with some offset ({@code firstResult}).
|
||||||
|
*
|
||||||
|
* <p/>
|
||||||
|
* Note that all methods in this interface should limit search only to data available within the storage that is
|
||||||
|
* represented by this provider. They should not lookup other storage providers for additional information.
|
||||||
|
*/
|
||||||
|
public interface UserCountMethodsProvider {
|
||||||
|
/**
|
||||||
|
* Returns the number of users, without consider any service account.
|
||||||
|
*
|
||||||
|
* @param realm the realm
|
||||||
|
* @return the number of users
|
||||||
|
*/
|
||||||
|
default int getUsersCount(RealmModel realm) {
|
||||||
|
return getUsersCount(realm, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the number of users that are in at least one of the groups
|
||||||
|
* given.
|
||||||
|
*
|
||||||
|
* @param realm the realm
|
||||||
|
* @param groupIds set of groups IDs, the returned user needs to belong to at least one of them
|
||||||
|
* @return the number of users that are in at least one of the groups
|
||||||
|
*/
|
||||||
|
default int getUsersCount(RealmModel realm, Set<String> groupIds) {
|
||||||
|
if (groupIds == null || groupIds.isEmpty() || !(this instanceof UserQueryMethodsProvider)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return countUsersInGroups(((UserQueryMethodsProvider)this).searchForUserStream(realm, Collections.emptyMap()), groupIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the number of users that would be returned by a call to {@link #searchForUserStream(RealmModel, String) searchForUserStream}
|
||||||
|
*
|
||||||
|
* @param realm the realm
|
||||||
|
* @param search case insensitive list of strings separated by whitespaces.
|
||||||
|
* @return number of users that match the search
|
||||||
|
*/
|
||||||
|
default int getUsersCount(RealmModel realm, String search) {
|
||||||
|
if (!(this instanceof UserQueryMethodsProvider)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int) ((UserQueryMethodsProvider)this).searchForUserStream(realm, search).count();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the number of users that would be returned by a call to {@link #searchForUserStream(RealmModel, String) searchForUserStream}
|
||||||
|
* and are members of at least one of the groups given by the {@code groupIds} set.
|
||||||
|
*
|
||||||
|
* @param realm the realm
|
||||||
|
* @param search case insensitive list of strings separated by whitespaces.
|
||||||
|
* @param groupIds set of groups IDs, the returned user needs to belong to at least one of them
|
||||||
|
* @return number of users that match the search and given groups
|
||||||
|
*/
|
||||||
|
default int getUsersCount(RealmModel realm, String search, Set<String> groupIds) {
|
||||||
|
if (groupIds == null || groupIds.isEmpty() || !(this instanceof UserQueryMethodsProvider)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return countUsersInGroups(((UserQueryMethodsProvider)this).searchForUserStream(realm, search), groupIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the number of users that match the given filter parameters.
|
||||||
|
*
|
||||||
|
* @param realm the realm
|
||||||
|
* @param params filter parameters
|
||||||
|
* @return number of users that match the given filters
|
||||||
|
*/
|
||||||
|
default int getUsersCount(RealmModel realm, Map<String, String> params) {
|
||||||
|
if (!(this instanceof UserQueryMethodsProvider)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return (int) ((UserQueryMethodsProvider)this).searchForUserStream(realm, params).count();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the number of users that match the given filter parameters and is in
|
||||||
|
* at least one of the given groups.
|
||||||
|
*
|
||||||
|
* @param params filter parameters
|
||||||
|
* @param realm the realm
|
||||||
|
* @param groupIds set if groups to check for
|
||||||
|
* @return number of users that match the given filters and groups
|
||||||
|
*/
|
||||||
|
default int getUsersCount(RealmModel realm, Map<String, String> params, Set<String> groupIds) {
|
||||||
|
if (groupIds == null || groupIds.isEmpty() || !(this instanceof UserQueryMethodsProvider)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return countUsersInGroups(((UserQueryMethodsProvider)this).searchForUserStream(realm, params), groupIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the number of users from the given list of users that are in at
|
||||||
|
* least one of the groups given in the groups set.
|
||||||
|
*
|
||||||
|
* @param users list of users to check
|
||||||
|
* @param groupIds id of groups that should be checked for
|
||||||
|
* @return number of users that are in at least one of the groups
|
||||||
|
*/
|
||||||
|
static int countUsersInGroups(Stream<UserModel> users, Set<String> groupIds) {
|
||||||
|
return (int) users.filter(u -> u.getGroupsStream().map(GroupModel::getId).anyMatch(groupIds::contains)).count();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the number of users.
|
||||||
|
*
|
||||||
|
* @param realm the realm
|
||||||
|
* @param includeServiceAccount if true, the number of users will also include service accounts. Otherwise, only the number of users.
|
||||||
|
* @return the number of users
|
||||||
|
*/
|
||||||
|
default int getUsersCount(RealmModel realm, boolean includeServiceAccount) {
|
||||||
|
throw new RuntimeException("Not implemented");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,210 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* 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.keycloak.storage.user;
|
||||||
|
|
||||||
|
import org.keycloak.models.GroupModel;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.RoleModel;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is an optional capability interface that is intended to be implemented by any
|
||||||
|
* <code>UserStorageProvider</code> that supports complex user querying. You must
|
||||||
|
* implement this interface if you want to view and manage users from the administration console.
|
||||||
|
* <p/>
|
||||||
|
* Note that all methods in this interface should limit search only to data available within the storage that is
|
||||||
|
* represented by this provider. They should not lookup other storage providers for additional information.
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
* @version $Revision: 1 $
|
||||||
|
*/
|
||||||
|
public interface UserQueryMethodsProvider {
|
||||||
|
/**
|
||||||
|
* Searches all users in the realm.
|
||||||
|
*
|
||||||
|
* @param realm a reference to the realm.
|
||||||
|
* @return a non-null {@link Stream} of users.
|
||||||
|
* @deprecated Use {@link #searchForUserStream(RealmModel, Map)} with an empty params map instead.
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
|
default Stream<UserModel> getUsersStream(RealmModel realm) {
|
||||||
|
return searchForUserStream(realm, Collections.emptyMap());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Searches all users in the realm, starting from the {@code firstResult} and containing at most {@code maxResults}.
|
||||||
|
*
|
||||||
|
* @param realm a reference to the realm.
|
||||||
|
* @param firstResult first result to return. Ignored if negative or {@code null}.
|
||||||
|
* @param maxResults maximum number of results to return. Ignored if negative or {@code null}.
|
||||||
|
* @return a non-null {@link Stream} of users.
|
||||||
|
* @deprecated Use {@link #searchForUserStream(RealmModel, Map, Integer, Integer)} with an empty params map instead.
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
|
default Stream<UserModel> getUsersStream(RealmModel realm, Integer firstResult, Integer maxResults) {
|
||||||
|
return searchForUserStream(realm, Collections.emptyMap(), firstResult, maxResults);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Searches for users whose username, email, first name or last name contain any of the strings in {@code search} separated by whitespace.
|
||||||
|
* <p/>
|
||||||
|
* If possible, implementations should treat the parameter values as partial match patterns (i.e. in RDMBS terms use LIKE).
|
||||||
|
* <p/>
|
||||||
|
* This method is used by the admin console search box
|
||||||
|
*
|
||||||
|
* @param realm a reference to the realm.
|
||||||
|
* @param search case insensitive list of string separated by whitespaces.
|
||||||
|
* @return a non-null {@link Stream} of users that match the search string.
|
||||||
|
*/
|
||||||
|
default Stream<UserModel> searchForUserStream(RealmModel realm, String search) {
|
||||||
|
return searchForUserStream(realm, search, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Searches for users whose username, email, first name or last name contain any of the strings in {@code search} separated by whitespace.
|
||||||
|
* <p/>
|
||||||
|
* If possible, implementations should treat the parameter values as partial match patterns (i.e. in RDMBS terms use LIKE).
|
||||||
|
* <p/>
|
||||||
|
* This method is used by the admin console search box
|
||||||
|
*
|
||||||
|
* @param realm a reference to the realm.
|
||||||
|
* @param search case insensitive list of string separated by whitespaces.
|
||||||
|
* @param firstResult first result to return. Ignored if negative, zero, or {@code null}.
|
||||||
|
* @param maxResults maximum number of results to return. Ignored if negative or {@code null}.
|
||||||
|
* @return a non-null {@link Stream} of users that match the search criteria.
|
||||||
|
*/
|
||||||
|
Stream<UserModel> searchForUserStream(RealmModel realm, String search, Integer firstResult, Integer maxResults);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Searches for user by parameter.
|
||||||
|
* If possible, implementations should treat the parameter values as partial match patterns (i.e. in RDMBS terms use LIKE).
|
||||||
|
* <p/>
|
||||||
|
* Valid parameters are:
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link UserModel#FIRST_NAME} - first name (case insensitive string)</li>
|
||||||
|
* <li>{@link UserModel#LAST_NAME} - last name (case insensitive string)</li>
|
||||||
|
* <li>{@link UserModel#EMAIL} - email (case insensitive string)</li>
|
||||||
|
* <li>{@link UserModel#USERNAME} - username (case insensitive string)</li>
|
||||||
|
* <li>{@link UserModel#EMAIL_VERIFIED} - search only for users with verified/non-verified email (true/false)</li>
|
||||||
|
* <li>{@link UserModel#ENABLED} - search only for enabled/disabled users (true/false)</li>
|
||||||
|
* <li>{@link UserModel#IDP_ALIAS} - search only for users that have a federated identity
|
||||||
|
* from idp with the given alias configured (case sensitive string)</li>
|
||||||
|
* <li>{@link UserModel#IDP_USER_ID} - search for users with federated identity with
|
||||||
|
* the given userId (case sensitive string)</li>
|
||||||
|
* </ul>
|
||||||
|
* <p>
|
||||||
|
* This method is used by the REST API when querying users.
|
||||||
|
*
|
||||||
|
* @param realm a reference to the realm.
|
||||||
|
* @param params a map containing the search parameters.
|
||||||
|
* @return a non-null {@link Stream} of users that match the search parameters.
|
||||||
|
*/
|
||||||
|
default Stream<UserModel> searchForUserStream(RealmModel realm, Map<String, String> params) {
|
||||||
|
return searchForUserStream(realm, params, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Searches for user by parameter. If possible, implementations should treat the parameter values as partial match patterns
|
||||||
|
* (i.e. in RDMBS terms use LIKE).
|
||||||
|
* <p/>
|
||||||
|
* Valid parameters are:
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link UserModel#FIRST_NAME} - first name (case insensitive string)</li>
|
||||||
|
* <li>{@link UserModel#LAST_NAME} - last name (case insensitive string)</li>
|
||||||
|
* <li>{@link UserModel#EMAIL} - email (case insensitive string)</li>
|
||||||
|
* <li>{@link UserModel#USERNAME} - username (case insensitive string)</li>
|
||||||
|
* <li>{@link UserModel#EMAIL_VERIFIED} - search only for users with verified/non-verified email (true/false)</li>
|
||||||
|
* <li>{@link UserModel#ENABLED} - search only for enabled/disabled users (true/false)</li>
|
||||||
|
* <li>{@link UserModel#IDP_ALIAS} - search only for users that have a federated identity
|
||||||
|
* from idp with the given alias configured (case sensitive string)</li>
|
||||||
|
* <li>{@link UserModel#IDP_USER_ID} - search for users with federated identity with
|
||||||
|
* the given userId (case sensitive string)</li>
|
||||||
|
* </ul>
|
||||||
|
* <p>
|
||||||
|
* Any other parameters will be treated as custom user attributes.
|
||||||
|
* <p>
|
||||||
|
* This method is used by the REST API when querying users.
|
||||||
|
*
|
||||||
|
* @param realm a reference to the realm.
|
||||||
|
* @param params a map containing the search parameters.
|
||||||
|
* @param firstResult first result to return. Ignored if negative, zero, or {@code null}.
|
||||||
|
* @param maxResults maximum number of results to return. Ignored if negative or {@code null}.
|
||||||
|
* @return a non-null {@link Stream} of users that match the search criteria.
|
||||||
|
*/
|
||||||
|
Stream<UserModel> searchForUserStream(RealmModel realm, Map<String, String> params, Integer firstResult, Integer maxResults);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtains users that belong to a specific group.
|
||||||
|
*
|
||||||
|
* @param realm a reference to the realm.
|
||||||
|
* @param group a reference to the group.
|
||||||
|
* @return a non-null {@link Stream} of users that belong to the group.
|
||||||
|
*/
|
||||||
|
default Stream<UserModel> getGroupMembersStream(RealmModel realm, GroupModel group) {
|
||||||
|
return getGroupMembersStream(realm, group, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtains users that belong to a specific group.
|
||||||
|
*
|
||||||
|
* @param realm a reference to the realm.
|
||||||
|
* @param group a reference to the group.
|
||||||
|
* @param firstResult first result to return. Ignored if negative, zero, or {@code null}.
|
||||||
|
* @param maxResults maximum number of results to return. Ignored if negative or {@code null}.
|
||||||
|
* @return a non-null {@link Stream} of users that belong to the group.
|
||||||
|
*/
|
||||||
|
Stream<UserModel> getGroupMembersStream(RealmModel realm, GroupModel group, Integer firstResult, Integer maxResults);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtains users that have the specified role.
|
||||||
|
*
|
||||||
|
* @param realm a reference to the realm.
|
||||||
|
* @param role a reference to the role.
|
||||||
|
* @return a non-null {@link Stream} of users that have the specified role.
|
||||||
|
*/
|
||||||
|
default Stream<UserModel> getRoleMembersStream(RealmModel realm, RoleModel role) {
|
||||||
|
return getRoleMembersStream(realm, role, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Searches for users that have the specified role.
|
||||||
|
*
|
||||||
|
* @param realm a reference to the realm.
|
||||||
|
* @param role a reference to the role.
|
||||||
|
* @param firstResult first result to return. Ignored if negative or {@code null}.
|
||||||
|
* @param maxResults maximum number of results to return. Ignored if negative or {@code null}.
|
||||||
|
* @return a non-null {@link Stream} of users that have the specified role.
|
||||||
|
*/
|
||||||
|
default Stream<UserModel> getRoleMembersStream(RealmModel realm, RoleModel role, Integer firstResult, Integer maxResults) {
|
||||||
|
return Stream.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Searches for users that have a specific attribute with a specific value.
|
||||||
|
*
|
||||||
|
* @param realm a reference to the realm.
|
||||||
|
* @param attrName the attribute name.
|
||||||
|
* @param attrValue the attribute value.
|
||||||
|
* @return a non-null {@link Stream} of users that match the search criteria.
|
||||||
|
*/
|
||||||
|
Stream<UserModel> searchForUserByUserAttributeStream(RealmModel realm, String attrName, String attrValue);
|
||||||
|
}
|
|
@ -16,304 +16,13 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.storage.user;
|
package org.keycloak.storage.user;
|
||||||
|
|
||||||
import org.keycloak.models.GroupModel;
|
|
||||||
import org.keycloak.models.RealmModel;
|
|
||||||
import org.keycloak.models.RoleModel;
|
|
||||||
import org.keycloak.models.UserModel;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
|
||||||
* This is an optional capability interface that is intended to be implemented by any
|
* This is an optional capability interface that is intended to be implemented by any
|
||||||
* <code>UserStorageProvider</code> that supports complex user querying. You must
|
* <code>UserStorageProvider</code> that supports complex user querying.
|
||||||
* implement this interface if you want to view and manage users from the administration console.
|
|
||||||
* <p/>
|
|
||||||
* Note that all methods in this interface should limit search only to data available within the storage that is
|
|
||||||
* represented by this provider. They should not lookup other storage providers for additional information.
|
|
||||||
*
|
*
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
* It's a combination of {@link UserQueryMethodsProvider} and {@link UserCountMethodsProvider}
|
||||||
* @version $Revision: 1 $
|
|
||||||
*/
|
*/
|
||||||
public interface UserQueryProvider {
|
public interface UserQueryProvider extends UserQueryMethodsProvider, UserCountMethodsProvider {
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the number of users, without consider any service account.
|
|
||||||
*
|
|
||||||
* @param realm the realm
|
|
||||||
* @return the number of users
|
|
||||||
*/
|
|
||||||
default int getUsersCount(RealmModel realm) {
|
|
||||||
return getUsersCount(realm, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the number of users that are in at least one of the groups
|
|
||||||
* given.
|
|
||||||
*
|
|
||||||
* @param realm the realm
|
|
||||||
* @param groupIds set of groups IDs, the returned user needs to belong to at least one of them
|
|
||||||
* @return the number of users that are in at least one of the groups
|
|
||||||
*/
|
|
||||||
default int getUsersCount(RealmModel realm, Set<String> groupIds) {
|
|
||||||
if (groupIds == null || groupIds.isEmpty()) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return countUsersInGroups(searchForUserStream(realm, Collections.emptyMap()), groupIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the number of users that would be returned by a call to {@link #searchForUserStream(RealmModel, String) searchForUserStream}
|
|
||||||
*
|
|
||||||
* @param realm the realm
|
|
||||||
* @param search case insensitive list of strings separated by whitespaces.
|
|
||||||
* @return number of users that match the search
|
|
||||||
*/
|
|
||||||
default int getUsersCount(RealmModel realm, String search) {
|
|
||||||
return (int) searchForUserStream(realm, search).count();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the number of users that would be returned by a call to {@link #searchForUserStream(RealmModel, String) searchForUserStream}
|
|
||||||
* and are members of at least one of the groups given by the {@code groupIds} set.
|
|
||||||
*
|
|
||||||
* @param realm the realm
|
|
||||||
* @param search case insensitive list of strings separated by whitespaces.
|
|
||||||
* @param groupIds set of groups IDs, the returned user needs to belong to at least one of them
|
|
||||||
* @return number of users that match the search and given groups
|
|
||||||
*/
|
|
||||||
default int getUsersCount(RealmModel realm, String search, Set<String> groupIds) {
|
|
||||||
if (groupIds == null || groupIds.isEmpty()) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return countUsersInGroups(searchForUserStream(realm, search), groupIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the number of users that match the given filter parameters.
|
|
||||||
*
|
|
||||||
* @param realm the realm
|
|
||||||
* @param params filter parameters
|
|
||||||
* @return number of users that match the given filters
|
|
||||||
*/
|
|
||||||
default int getUsersCount(RealmModel realm, Map<String, String> params) {
|
|
||||||
return (int) searchForUserStream(realm, params).count();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the number of users that match the given filter parameters and is in
|
|
||||||
* at least one of the given groups.
|
|
||||||
*
|
|
||||||
* @param params filter parameters
|
|
||||||
* @param realm the realm
|
|
||||||
* @param groupIds set if groups to check for
|
|
||||||
* @return number of users that match the given filters and groups
|
|
||||||
*/
|
|
||||||
default int getUsersCount(RealmModel realm, Map<String, String> params, Set<String> groupIds) {
|
|
||||||
if (groupIds == null || groupIds.isEmpty()) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return countUsersInGroups(searchForUserStream(realm, params), groupIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the number of users from the given list of users that are in at
|
|
||||||
* least one of the groups given in the groups set.
|
|
||||||
*
|
|
||||||
* @param users list of users to check
|
|
||||||
* @param groupIds id of groups that should be checked for
|
|
||||||
* @return number of users that are in at least one of the groups
|
|
||||||
*/
|
|
||||||
static int countUsersInGroups(Stream<UserModel> users, Set<String> groupIds) {
|
|
||||||
return (int) users.filter(u -> u.getGroupsStream().map(GroupModel::getId).anyMatch(groupIds::contains)).count();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the number of users.
|
|
||||||
*
|
|
||||||
* @param realm the realm
|
|
||||||
* @param includeServiceAccount if true, the number of users will also include service accounts. Otherwise, only the number of users.
|
|
||||||
* @return the number of users
|
|
||||||
*/
|
|
||||||
default int getUsersCount(RealmModel realm, boolean includeServiceAccount) {
|
|
||||||
throw new RuntimeException("Not implemented");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Searches all users in the realm.
|
|
||||||
*
|
|
||||||
* @param realm a reference to the realm.
|
|
||||||
* @return a non-null {@link Stream} of users.
|
|
||||||
* @deprecated Use {@link #searchForUserStream(RealmModel, Map)} with an empty params map instead.
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
default Stream<UserModel> getUsersStream(RealmModel realm) {
|
|
||||||
return searchForUserStream(realm, Collections.emptyMap());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Searches all users in the realm, starting from the {@code firstResult} and containing at most {@code maxResults}.
|
|
||||||
*
|
|
||||||
* @param realm a reference to the realm.
|
|
||||||
* @param firstResult first result to return. Ignored if negative or {@code null}.
|
|
||||||
* @param maxResults maximum number of results to return. Ignored if negative or {@code null}.
|
|
||||||
* @return a non-null {@link Stream} of users.
|
|
||||||
* @deprecated Use {@link #searchForUserStream(RealmModel, Map, Integer, Integer)} with an empty params map instead.
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
default Stream<UserModel> getUsersStream(RealmModel realm, Integer firstResult, Integer maxResults) {
|
|
||||||
return searchForUserStream(realm, Collections.emptyMap(), firstResult, maxResults);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Searches for users whose username, email, first name or last name contain any of the strings in {@code search} separated by whitespace.
|
|
||||||
* <p/>
|
|
||||||
* If possible, implementations should treat the parameter values as partial match patterns (i.e. in RDMBS terms use LIKE).
|
|
||||||
* <p/>
|
|
||||||
* This method is used by the admin console search box
|
|
||||||
*
|
|
||||||
* @param realm a reference to the realm.
|
|
||||||
* @param search case insensitive list of string separated by whitespaces.
|
|
||||||
* @return a non-null {@link Stream} of users that match the search string.
|
|
||||||
*/
|
|
||||||
default Stream<UserModel> searchForUserStream(RealmModel realm, String search) {
|
|
||||||
return searchForUserStream(realm, search, null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Searches for users whose username, email, first name or last name contain any of the strings in {@code search} separated by whitespace.
|
|
||||||
* <p/>
|
|
||||||
* If possible, implementations should treat the parameter values as partial match patterns (i.e. in RDMBS terms use LIKE).
|
|
||||||
* <p/>
|
|
||||||
* This method is used by the admin console search box
|
|
||||||
*
|
|
||||||
* @param realm a reference to the realm.
|
|
||||||
* @param search case insensitive list of string separated by whitespaces.
|
|
||||||
* @param firstResult first result to return. Ignored if negative, zero, or {@code null}.
|
|
||||||
* @param maxResults maximum number of results to return. Ignored if negative or {@code null}.
|
|
||||||
* @return a non-null {@link Stream} of users that match the search criteria.
|
|
||||||
*/
|
|
||||||
Stream<UserModel> searchForUserStream(RealmModel realm, String search, Integer firstResult, Integer maxResults);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Searches for user by parameter.
|
|
||||||
* If possible, implementations should treat the parameter values as partial match patterns (i.e. in RDMBS terms use LIKE).
|
|
||||||
* <p/>
|
|
||||||
* Valid parameters are:
|
|
||||||
* <ul>
|
|
||||||
* <li>{@link UserModel#FIRST_NAME} - first name (case insensitive string)</li>
|
|
||||||
* <li>{@link UserModel#LAST_NAME} - last name (case insensitive string)</li>
|
|
||||||
* <li>{@link UserModel#EMAIL} - email (case insensitive string)</li>
|
|
||||||
* <li>{@link UserModel#USERNAME} - username (case insensitive string)</li>
|
|
||||||
* <li>{@link UserModel#EMAIL_VERIFIED} - search only for users with verified/non-verified email (true/false)</li>
|
|
||||||
* <li>{@link UserModel#ENABLED} - search only for enabled/disabled users (true/false)</li>
|
|
||||||
* <li>{@link UserModel#IDP_ALIAS} - search only for users that have a federated identity
|
|
||||||
* from idp with the given alias configured (case sensitive string)</li>
|
|
||||||
* <li>{@link UserModel#IDP_USER_ID} - search for users with federated identity with
|
|
||||||
* the given userId (case sensitive string)</li>
|
|
||||||
* </ul>
|
|
||||||
*
|
|
||||||
* This method is used by the REST API when querying users.
|
|
||||||
*
|
|
||||||
* @param realm a reference to the realm.
|
|
||||||
* @param params a map containing the search parameters.
|
|
||||||
* @return a non-null {@link Stream} of users that match the search parameters.
|
|
||||||
*/
|
|
||||||
default Stream<UserModel> searchForUserStream(RealmModel realm, Map<String, String> params) {
|
|
||||||
return searchForUserStream(realm, params, null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Searches for user by parameter. If possible, implementations should treat the parameter values as partial match patterns
|
|
||||||
* (i.e. in RDMBS terms use LIKE).
|
|
||||||
* <p/>
|
|
||||||
* Valid parameters are:
|
|
||||||
* <ul>
|
|
||||||
* <li>{@link UserModel#FIRST_NAME} - first name (case insensitive string)</li>
|
|
||||||
* <li>{@link UserModel#LAST_NAME} - last name (case insensitive string)</li>
|
|
||||||
* <li>{@link UserModel#EMAIL} - email (case insensitive string)</li>
|
|
||||||
* <li>{@link UserModel#USERNAME} - username (case insensitive string)</li>
|
|
||||||
* <li>{@link UserModel#EMAIL_VERIFIED} - search only for users with verified/non-verified email (true/false)</li>
|
|
||||||
* <li>{@link UserModel#ENABLED} - search only for enabled/disabled users (true/false)</li>
|
|
||||||
* <li>{@link UserModel#IDP_ALIAS} - search only for users that have a federated identity
|
|
||||||
* from idp with the given alias configured (case sensitive string)</li>
|
|
||||||
* <li>{@link UserModel#IDP_USER_ID} - search for users with federated identity with
|
|
||||||
* the given userId (case sensitive string)</li>
|
|
||||||
* </ul>
|
|
||||||
*
|
|
||||||
* Any other parameters will be treated as custom user attributes.
|
|
||||||
*
|
|
||||||
* This method is used by the REST API when querying users.
|
|
||||||
*
|
|
||||||
* @param realm a reference to the realm.
|
|
||||||
* @param params a map containing the search parameters.
|
|
||||||
* @param firstResult first result to return. Ignored if negative, zero, or {@code null}.
|
|
||||||
* @param maxResults maximum number of results to return. Ignored if negative or {@code null}.
|
|
||||||
* @return a non-null {@link Stream} of users that match the search criteria.
|
|
||||||
*/
|
|
||||||
Stream<UserModel> searchForUserStream(RealmModel realm, Map<String, String> params, Integer firstResult, Integer maxResults);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Obtains users that belong to a specific group.
|
|
||||||
*
|
|
||||||
* @param realm a reference to the realm.
|
|
||||||
* @param group a reference to the group.
|
|
||||||
* @return a non-null {@link Stream} of users that belong to the group.
|
|
||||||
*/
|
|
||||||
default Stream<UserModel> getGroupMembersStream(RealmModel realm, GroupModel group) {
|
|
||||||
return getGroupMembersStream(realm, group, null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Obtains users that belong to a specific group.
|
|
||||||
*
|
|
||||||
* @param realm a reference to the realm.
|
|
||||||
* @param group a reference to the group.
|
|
||||||
* @param firstResult first result to return. Ignored if negative, zero, or {@code null}.
|
|
||||||
* @param maxResults maximum number of results to return. Ignored if negative or {@code null}.
|
|
||||||
* @return a non-null {@link Stream} of users that belong to the group.
|
|
||||||
*/
|
|
||||||
Stream<UserModel> getGroupMembersStream(RealmModel realm, GroupModel group, Integer firstResult, Integer maxResults);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Obtains users that have the specified role.
|
|
||||||
*
|
|
||||||
* @param realm a reference to the realm.
|
|
||||||
* @param role a reference to the role.
|
|
||||||
* @return a non-null {@link Stream} of users that have the specified role.
|
|
||||||
*/
|
|
||||||
default Stream<UserModel> getRoleMembersStream(RealmModel realm, RoleModel role) {
|
|
||||||
return getRoleMembersStream(realm, role, null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Searches for users that have the specified role.
|
|
||||||
*
|
|
||||||
* @param realm a reference to the realm.
|
|
||||||
* @param role a reference to the role.
|
|
||||||
* @param firstResult first result to return. Ignored if negative or {@code null}.
|
|
||||||
* @param maxResults maximum number of results to return. Ignored if negative or {@code null}.
|
|
||||||
* @return a non-null {@link Stream} of users that have the specified role.
|
|
||||||
*/
|
|
||||||
default Stream<UserModel> getRoleMembersStream(RealmModel realm, RoleModel role, Integer firstResult, Integer maxResults) {
|
|
||||||
return Stream.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Searches for users that have a specific attribute with a specific value.
|
|
||||||
*
|
|
||||||
* @param realm a reference to the realm.
|
|
||||||
* @param attrName the attribute name.
|
|
||||||
* @param attrValue the attribute value.
|
|
||||||
* @return a non-null {@link Stream} of users that match the search criteria.
|
|
||||||
*/
|
|
||||||
Stream<UserModel> searchForUserByUserAttributeStream(RealmModel realm, String attrName, String attrValue);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated This interface is no longer necessary, collection-based methods were removed from the parent interface
|
* @deprecated This interface is no longer necessary, collection-based methods were removed from the parent interface
|
||||||
|
|
|
@ -225,10 +225,14 @@ public class UserPropertyFileStorage implements UserLookupProvider, UserStorageP
|
||||||
public Stream<UserModel> searchForUserStream(RealmModel realm, Map<String, String> attributes, Integer firstResult, Integer maxResults) {
|
public Stream<UserModel> searchForUserStream(RealmModel realm, Map<String, String> attributes, Integer firstResult, Integer maxResults) {
|
||||||
String search = Optional.ofNullable(attributes.get(UserModel.USERNAME))
|
String search = Optional.ofNullable(attributes.get(UserModel.USERNAME))
|
||||||
.orElseGet(()-> attributes.get(UserModel.SEARCH));
|
.orElseGet(()-> attributes.get(UserModel.SEARCH));
|
||||||
if (search == null) return Stream.empty();
|
Predicate<String> p;
|
||||||
Predicate<String> p = Boolean.valueOf(attributes.getOrDefault(UserModel.EXACT, Boolean.FALSE.toString()))
|
if (search == null) {
|
||||||
|
p = x -> true;
|
||||||
|
} else {
|
||||||
|
p = Boolean.parseBoolean(attributes.getOrDefault(UserModel.EXACT, Boolean.FALSE.toString()))
|
||||||
? username -> username.equals(search)
|
? username -> username.equals(search)
|
||||||
: username -> username.contains(search);
|
: username -> username.contains(search);
|
||||||
|
}
|
||||||
return searchForUser(realm, search, firstResult, maxResults, p);
|
return searchForUser(realm, search, firstResult, maxResults, p);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,125 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* 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.keycloak.testsuite.federation.ldap;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import org.apache.commons.io.FileUtils;
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.hamcrest.Matchers.hasSize;
|
||||||
|
import org.junit.ClassRule;
|
||||||
|
import org.junit.FixMethodOrder;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runners.MethodSorters;
|
||||||
|
import org.keycloak.common.util.MultivaluedHashMap;
|
||||||
|
import org.keycloak.component.ComponentModel;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.storage.UserStorageProvider;
|
||||||
|
import org.keycloak.testsuite.federation.UserPropertyFileStorageFactory;
|
||||||
|
import org.keycloak.testsuite.util.LDAPRule;
|
||||||
|
import org.keycloak.testsuite.util.LDAPTestUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* It tests correct behavior when using {@code firstResult} during querying users
|
||||||
|
* with a provider not-implementing {@code UserCountMethodsProvider} - LDAPStorageProvider.
|
||||||
|
*/
|
||||||
|
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
|
||||||
|
public class LDAPCountQueryTest extends AbstractLDAPTest {
|
||||||
|
|
||||||
|
private static final File CONFIG_DIR = new File(System.getProperty("auth.server.config.dir", ""));
|
||||||
|
|
||||||
|
@ClassRule
|
||||||
|
public static LDAPRule ldapRule = new LDAPRule();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected LDAPRule getLDAPRule() {
|
||||||
|
return ldapRule;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void afterImportTestRealm() {
|
||||||
|
copyPropertiesFiles();
|
||||||
|
final String ldapComponentId = ldapModelId;
|
||||||
|
final String propertyFile = CONFIG_DIR.getAbsolutePath() + File.separator + "user-password.properties";
|
||||||
|
testingClient.server().run(session -> {
|
||||||
|
|
||||||
|
LDAPTestContext ctx = LDAPTestContext.init(session);
|
||||||
|
RealmModel appRealm = ctx.getRealm();
|
||||||
|
|
||||||
|
// set high priority to ensure the provider which doesn't implement UserCountMethodsProvider is queried first
|
||||||
|
ComponentModel ldapComponent = appRealm.getComponent(ldapComponentId);
|
||||||
|
ldapComponent.getConfig().putSingle("priority", Integer.toString(0));
|
||||||
|
appRealm.updateComponent(ldapComponent);
|
||||||
|
|
||||||
|
// Delete all local users and add some new for testing
|
||||||
|
session.users().searchForUserStream(appRealm, Map.of()).collect(Collectors.toList()).forEach(u -> session.users().removeUser(appRealm, u));
|
||||||
|
|
||||||
|
LDAPTestUtils.addLocalUser(session, appRealm, "user1", "user1@email", "password");
|
||||||
|
LDAPTestUtils.addLocalUser(session, appRealm, "user2", "user2@email", "password");
|
||||||
|
|
||||||
|
// Delete all LDAP users and add some new for testing
|
||||||
|
LDAPTestUtils.removeAllLDAPUsers(ctx.getLdapProvider(), appRealm);
|
||||||
|
|
||||||
|
LDAPTestUtils.addLDAPUser(ctx.getLdapProvider(), appRealm, "john00", "john", "Doe", "john0@email.org", null, "1234");
|
||||||
|
LDAPTestUtils.addLDAPUser(ctx.getLdapProvider(), appRealm, "john01", "john", "Doe", "john1@email.org", null, "1234");
|
||||||
|
LDAPTestUtils.addLDAPUser(ctx.getLdapProvider(), appRealm, "john02", "john", "Doe", "john2@email.org", null, "1234");
|
||||||
|
|
||||||
|
// add user-prop provider with lower priority
|
||||||
|
ComponentModel userPropProvider = new ComponentModel();
|
||||||
|
userPropProvider.setName("user-props");
|
||||||
|
userPropProvider.setProviderId(UserPropertyFileStorageFactory.PROVIDER_ID);
|
||||||
|
userPropProvider.setProviderType(UserStorageProvider.class.getName());
|
||||||
|
userPropProvider.setConfig(new MultivaluedHashMap<>());
|
||||||
|
userPropProvider.getConfig().putSingle("priority", Integer.toString(1));
|
||||||
|
userPropProvider.getConfig().putSingle("propertyFile", propertyFile);
|
||||||
|
userPropProvider.getConfig().putSingle("federatedStorage", "false");
|
||||||
|
appRealm.addComponentModel(userPropProvider);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void copyPropertiesFiles() throws RuntimeException {
|
||||||
|
try {
|
||||||
|
// copy files used by the following user-props user provider
|
||||||
|
File stResDir = new File(getClass().getResource("/storage-test").toURI());
|
||||||
|
if (stResDir.exists() && stResDir.isDirectory() && CONFIG_DIR.exists() && CONFIG_DIR.isDirectory()) {
|
||||||
|
for (File f : stResDir.listFiles()) {
|
||||||
|
log.infof("Copying %s to %s", f.getName(), CONFIG_DIR.getAbsolutePath());
|
||||||
|
FileUtils.copyFileToDirectory(f, CONFIG_DIR);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new RuntimeException("Property `auth.server.config.dir` must be set to run the test.");
|
||||||
|
}
|
||||||
|
} catch (IOException | RuntimeException | URISyntaxException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFirstResultWithMultipleProviders() {
|
||||||
|
assertThat(adminClient.realm(TEST_REALM_NAME).users().list(6, null), hasSize(4));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFirstResultWithMultipleProvidersMaxResultSet() {
|
||||||
|
assertThat(adminClient.realm(TEST_REALM_NAME).users().list(6, 20), hasSize(4));
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue