KEYCLOAK-14781 Extend Admin REST API with search by federated identity
- Add parameters idpAlias and idpUserId to the resource /{realm}/users and allow it to be combined with the other search parameters like username, email and so on - Add attribute "federatedIdentities" to UserEntity to allow joining on this field - extend integration test "UserTest"
This commit is contained in:
parent
f917302ace
commit
de8d2eafa3
6 changed files with 254 additions and 43 deletions
|
@ -53,6 +53,38 @@ public interface UsersResource {
|
|||
@QueryParam("enabled") Boolean enabled,
|
||||
@QueryParam("briefRepresentation") Boolean briefRepresentation);
|
||||
|
||||
/**
|
||||
* Search for users based on the given filters.
|
||||
*
|
||||
* @param username a value contained in username
|
||||
* @param firstName a value contained in first name
|
||||
* @param lastName a value contained in last name
|
||||
* @param email a value contained in email
|
||||
* @param emailVerified whether the email has been verified
|
||||
* @param idpAlias the alias of the Identity Provider
|
||||
* @param idpUserId the userId at the Identity Provider
|
||||
* @param firstResult the position of the first result to retrieve
|
||||
* @param maxResults the maximum number of results to retrieve
|
||||
* @param enabled only return enabled or disabled users
|
||||
* @param briefRepresentation Only return basic information (only guaranteed to return id, username, created, first
|
||||
* and last name, email, enabled state, email verification state, federation link, and access.
|
||||
* Note that it means that namely user attributes, required actions, and not before are not returned.)
|
||||
* @return a list of {@link UserRepresentation}
|
||||
*/
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
List<UserRepresentation> search(@QueryParam("username") String username,
|
||||
@QueryParam("firstName") String firstName,
|
||||
@QueryParam("lastName") String lastName,
|
||||
@QueryParam("email") String email,
|
||||
@QueryParam("emailVerified") Boolean emailVerified,
|
||||
@QueryParam("idpAlias") String idpAlias,
|
||||
@QueryParam("idpUserId") String idpUserId,
|
||||
@QueryParam("first") Integer firstResult,
|
||||
@QueryParam("max") Integer maxResults,
|
||||
@QueryParam("enabled") Boolean enabled,
|
||||
@QueryParam("briefRepresentation") Boolean briefRepresentation);
|
||||
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
List<UserRepresentation> search(@QueryParam("username") String username,
|
||||
|
|
|
@ -54,6 +54,7 @@ import javax.persistence.TypedQuery;
|
|||
import javax.persistence.criteria.CriteriaBuilder;
|
||||
import javax.persistence.criteria.CriteriaQuery;
|
||||
import javax.persistence.criteria.Expression;
|
||||
import javax.persistence.criteria.Join;
|
||||
import javax.persistence.criteria.Predicate;
|
||||
import javax.persistence.criteria.Root;
|
||||
import javax.persistence.criteria.Subquery;
|
||||
|
@ -840,6 +841,8 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore {
|
|||
predicates.add(root.get("serviceAccountClientLink").isNull());
|
||||
}
|
||||
|
||||
Join<Object, Object> federatedIdentitiesJoin = null;
|
||||
|
||||
for (Map.Entry<String, String> entry : attributes.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
String value = entry.getValue();
|
||||
|
@ -852,7 +855,8 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore {
|
|||
case UserModel.SEARCH:
|
||||
List<Predicate> orPredicates = new ArrayList();
|
||||
|
||||
orPredicates.add(builder.like(builder.lower(root.get(USERNAME)), "%" + value.toLowerCase() + "%"));
|
||||
orPredicates
|
||||
.add(builder.like(builder.lower(root.get(USERNAME)), "%" + value.toLowerCase() + "%"));
|
||||
orPredicates.add(builder.like(builder.lower(root.get(EMAIL)), "%" + value.toLowerCase() + "%"));
|
||||
orPredicates.add(builder.like(
|
||||
builder.lower(builder.concat(builder.concat(
|
||||
|
@ -878,7 +882,20 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore {
|
|||
predicates.add(builder.equal(root.get(key), Boolean.parseBoolean(value.toLowerCase())));
|
||||
break;
|
||||
case UserModel.ENABLED:
|
||||
predicates.add(builder.equal(builder.lower(root.get(key)), Boolean.parseBoolean(value.toLowerCase())));
|
||||
predicates.add(builder.equal(root.get(key), Boolean.parseBoolean(value)));
|
||||
break;
|
||||
case UserModel.IDP_ALIAS:
|
||||
if (federatedIdentitiesJoin == null) {
|
||||
federatedIdentitiesJoin = root.join("federatedIdentities");
|
||||
}
|
||||
predicates.add(builder.equal(federatedIdentitiesJoin.get("identityProvider"), value));
|
||||
break;
|
||||
case UserModel.IDP_USER_ID:
|
||||
if (federatedIdentitiesJoin == null) {
|
||||
federatedIdentitiesJoin = root.join("federatedIdentities");
|
||||
}
|
||||
predicates.add(builder.equal(federatedIdentitiesJoin.get("userId"), value));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -110,6 +110,11 @@ public class UserEntity {
|
|||
@BatchSize(size = 20)
|
||||
protected Collection<CredentialEntity> credentials;
|
||||
|
||||
@OneToMany(mappedBy="user")
|
||||
@Fetch(FetchMode.SELECT)
|
||||
@BatchSize(size = 20)
|
||||
protected Collection<FederatedIdentityEntity> federatedIdentities;
|
||||
|
||||
@Column(name="FEDERATION_LINK")
|
||||
protected String federationLink;
|
||||
|
||||
|
@ -233,6 +238,17 @@ public class UserEntity {
|
|||
this.credentials = credentials;
|
||||
}
|
||||
|
||||
public Collection<FederatedIdentityEntity> getFederatedIdentities() {
|
||||
if (federatedIdentities == null) {
|
||||
federatedIdentities = new LinkedList<>();
|
||||
}
|
||||
return federatedIdentities;
|
||||
}
|
||||
|
||||
public void setFederatedIdentities(Collection<FederatedIdentityEntity> federatedIdentities) {
|
||||
this.federatedIdentities = federatedIdentities;
|
||||
}
|
||||
|
||||
public String getFederationLink() {
|
||||
return federationLink;
|
||||
}
|
||||
|
|
|
@ -38,6 +38,8 @@ public interface UserModel extends RoleMapperModel {
|
|||
String EMAIL_VERIFIED = "emailVerified";
|
||||
String LOCALE = "locale";
|
||||
String ENABLED = "enabled";
|
||||
String IDP_ALIAS = "keycloak.session.realm.users.query.idp_alias";
|
||||
String IDP_USER_ID = "keycloak.session.realm.users.query.idp_user_id";
|
||||
String INCLUDE_SERVICE_ACCOUNT = "keycloak.session.realm.users.query.include_service_account";
|
||||
String GROUPS = "keycloak.session.realm.users.query.groups";
|
||||
String SEARCH = "keycloak.session.realm.users.query.search";
|
||||
|
|
|
@ -208,14 +208,19 @@ public class UsersResource {
|
|||
* Returns a list of users, filtered according to query parameters
|
||||
*
|
||||
* @param search A String contained in username, first or last name, or email
|
||||
* @param last
|
||||
* @param first
|
||||
* @param email
|
||||
* @param username
|
||||
* @param enabled Boolean representing if user is enabled or not
|
||||
* @param first Pagination offset
|
||||
* @param last A String contained in lastName, or the complete lastName, if param "exact" is true
|
||||
* @param first A String contained in firstName, or the complete firstName, if param "exact" is true
|
||||
* @param email A String contained in email, or the complete email, if param "exact" is true
|
||||
* @param username A String contained in username, or the complete username, if param "exact" is true
|
||||
* @param emailVerified whether the email has been verified
|
||||
* @param idpAlias The alias of an Identity Provider linked to the user
|
||||
* @param idpUserId The userId at an Identity Provider linked to the user
|
||||
* @param firstResult Pagination offset
|
||||
* @param maxResults Maximum results size (defaults to 100)
|
||||
* @return
|
||||
* @param enabled Boolean representing if user is enabled or not
|
||||
* @param briefRepresentation Boolean which defines whether brief representations are returned (default: false)
|
||||
* @param exact Boolean which defines whether the params "last", "first", "email" and "username" must match exactly
|
||||
* @return the list of users
|
||||
*/
|
||||
@GET
|
||||
@NoCache
|
||||
|
@ -226,6 +231,8 @@ public class UsersResource {
|
|||
@QueryParam("email") String email,
|
||||
@QueryParam("username") String username,
|
||||
@QueryParam("emailVerified") Boolean emailVerified,
|
||||
@QueryParam("idpAlias") String idpAlias,
|
||||
@QueryParam("idpUserId") String idpUserId,
|
||||
@QueryParam("first") Integer firstResult,
|
||||
@QueryParam("max") Integer maxResults,
|
||||
@QueryParam("enabled") Boolean enabled,
|
||||
|
@ -241,7 +248,8 @@ public class UsersResource {
|
|||
List<UserModel> userModels = Collections.emptyList();
|
||||
if (search != null) {
|
||||
if (search.startsWith(SEARCH_ID_PARAMETER)) {
|
||||
UserModel userModel = session.users().getUserById(search.substring(SEARCH_ID_PARAMETER.length()).trim(), realm);
|
||||
UserModel userModel =
|
||||
session.users().getUserById(search.substring(SEARCH_ID_PARAMETER.length()).trim(), realm);
|
||||
if (userModel != null) {
|
||||
userModels = Collections.singletonList(userModel);
|
||||
}
|
||||
|
@ -251,35 +259,45 @@ public class UsersResource {
|
|||
if (enabled != null) {
|
||||
attributes.put(UserModel.ENABLED, enabled.toString());
|
||||
}
|
||||
return searchForUser(attributes, realm, userPermissionEvaluator, briefRepresentation, firstResult, maxResults, false);
|
||||
return searchForUser(attributes, realm, userPermissionEvaluator, briefRepresentation, firstResult,
|
||||
maxResults, false);
|
||||
}
|
||||
} else if (last != null || first != null || email != null || username != null || emailVerified != null || enabled != null || exact != null) {
|
||||
Map<String, String> attributes = new HashMap<>();
|
||||
if (last != null) {
|
||||
attributes.put(UserModel.LAST_NAME, last);
|
||||
}
|
||||
if (first != null) {
|
||||
attributes.put(UserModel.FIRST_NAME, first);
|
||||
}
|
||||
if (email != null) {
|
||||
attributes.put(UserModel.EMAIL, email);
|
||||
}
|
||||
if (username != null) {
|
||||
attributes.put(UserModel.USERNAME, username);
|
||||
}
|
||||
if (enabled != null) {
|
||||
attributes.put(UserModel.ENABLED, enabled.toString());
|
||||
}
|
||||
if (exact != null) {
|
||||
attributes.put(UserModel.EXACT, exact.toString());
|
||||
}
|
||||
if (emailVerified != null) {
|
||||
attributes.put(UserModel.EMAIL_VERIFIED, emailVerified.toString());
|
||||
}
|
||||
return searchForUser(attributes, realm, userPermissionEvaluator, briefRepresentation, firstResult, maxResults, true);
|
||||
} else {
|
||||
return searchForUser(new HashMap<>(), realm, userPermissionEvaluator, briefRepresentation, firstResult, maxResults, false);
|
||||
}
|
||||
} else if (last != null || first != null || email != null || username != null || emailVerified != null
|
||||
|| idpAlias != null || idpUserId != null || enabled != null || exact != null) {
|
||||
Map<String, String> attributes = new HashMap<>();
|
||||
if (last != null) {
|
||||
attributes.put(UserModel.LAST_NAME, last);
|
||||
}
|
||||
if (first != null) {
|
||||
attributes.put(UserModel.FIRST_NAME, first);
|
||||
}
|
||||
if (email != null) {
|
||||
attributes.put(UserModel.EMAIL, email);
|
||||
}
|
||||
if (username != null) {
|
||||
attributes.put(UserModel.USERNAME, username);
|
||||
}
|
||||
if (emailVerified != null) {
|
||||
attributes.put(UserModel.EMAIL_VERIFIED, emailVerified.toString());
|
||||
}
|
||||
if (idpAlias != null) {
|
||||
attributes.put(UserModel.IDP_ALIAS, idpAlias);
|
||||
}
|
||||
if (idpUserId != null) {
|
||||
attributes.put(UserModel.IDP_USER_ID, idpUserId);
|
||||
}
|
||||
if (enabled != null) {
|
||||
attributes.put(UserModel.ENABLED, enabled.toString());
|
||||
}
|
||||
if (exact != null) {
|
||||
attributes.put(UserModel.EXACT, exact.toString());
|
||||
}
|
||||
return searchForUser(attributes, realm, userPermissionEvaluator, briefRepresentation, firstResult,
|
||||
maxResults, true);
|
||||
} else {
|
||||
return searchForUser(new HashMap<>(), realm, userPermissionEvaluator, briefRepresentation,
|
||||
firstResult, maxResults, false);
|
||||
}
|
||||
|
||||
return toRepresentation(realm, userPermissionEvaluator, briefRepresentation, userModels);
|
||||
}
|
||||
|
|
|
@ -718,6 +718,130 @@ public class UserTest extends AbstractAdminTest {
|
|||
assertEquals(0, searchInvalidSizeAndDisabled.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void searchByIdp() {
|
||||
// Add user without IDP
|
||||
createUser();
|
||||
|
||||
// add sample Identity Providers
|
||||
final String identityProviderAlias1 = "identity-provider-alias1";
|
||||
addSampleIdentityProvider(identityProviderAlias1, 0);
|
||||
final String identityProviderAlias2 = "identity-provider-alias2";
|
||||
addSampleIdentityProvider(identityProviderAlias2, 1);
|
||||
|
||||
final String commonIdpUserId = "commonIdpUserId";
|
||||
|
||||
// create first IDP1 User with link
|
||||
final String idp1User1Username = "idp1user1";
|
||||
final String idp1User1KeycloakId = createUser(idp1User1Username, "idp1user1@localhost");
|
||||
final String idp1User1UserId = "idp1user1Id";
|
||||
FederatedIdentityRepresentation link1_1 = new FederatedIdentityRepresentation();
|
||||
link1_1.setUserId(idp1User1UserId);
|
||||
link1_1.setUserName(idp1User1Username);
|
||||
addFederatedIdentity(idp1User1KeycloakId, identityProviderAlias1, link1_1);
|
||||
|
||||
// create second IDP1 User with link
|
||||
final String idp1User2Username = "idp1user2";
|
||||
final String idp1User2KeycloakId = createUser(idp1User2Username, "idp1user2@localhost");
|
||||
FederatedIdentityRepresentation link1_2 = new FederatedIdentityRepresentation();
|
||||
link1_2.setUserId(commonIdpUserId);
|
||||
link1_2.setUserName(idp1User2Username);
|
||||
addFederatedIdentity(idp1User2KeycloakId, identityProviderAlias1, link1_2);
|
||||
|
||||
// create IDP2 user with link
|
||||
final String idp2UserUsername = "idp2user";
|
||||
final String idp2UserKeycloakId = createUser(idp2UserUsername, "idp2user@localhost");
|
||||
FederatedIdentityRepresentation link2 = new FederatedIdentityRepresentation();
|
||||
link2.setUserId(commonIdpUserId);
|
||||
link2.setUserName(idp2UserUsername);
|
||||
addFederatedIdentity(idp2UserKeycloakId, identityProviderAlias2, link2);
|
||||
|
||||
// run search tests
|
||||
List<UserRepresentation> searchForAllUsers =
|
||||
realm.users().search(null, null, null, null, null, null, null, null, null, null, null);
|
||||
assertEquals(4, searchForAllUsers.size());
|
||||
|
||||
List<UserRepresentation> searchByIdpAlias =
|
||||
realm.users().search(null, null, null, null, null, identityProviderAlias1, null, null, null, null,
|
||||
null);
|
||||
assertEquals(2, searchByIdpAlias.size());
|
||||
assertEquals(idp1User1Username, searchByIdpAlias.get(0).getUsername());
|
||||
assertEquals(idp1User2Username, searchByIdpAlias.get(1).getUsername());
|
||||
|
||||
List<UserRepresentation> searchByIdpUserId =
|
||||
realm.users().search(null, null, null, null, null, null, commonIdpUserId, null, null, null, null);
|
||||
assertEquals(2, searchByIdpUserId.size());
|
||||
assertEquals(idp1User2Username, searchByIdpUserId.get(0).getUsername());
|
||||
assertEquals(idp2UserUsername, searchByIdpUserId.get(1).getUsername());
|
||||
|
||||
List<UserRepresentation> searchByIdpAliasAndUserId =
|
||||
realm.users().search(null, null, null, null, null, identityProviderAlias1, idp1User1UserId, null, null,
|
||||
null,
|
||||
null);
|
||||
assertEquals(1, searchByIdpAliasAndUserId.size());
|
||||
assertEquals(idp1User1Username, searchByIdpAliasAndUserId.get(0).getUsername());
|
||||
}
|
||||
|
||||
private void addFederatedIdentity(String keycloakUserId, String identityProviderAlias1,
|
||||
FederatedIdentityRepresentation link) {
|
||||
Response response1 = realm.users().get(keycloakUserId).addFederatedIdentity(identityProviderAlias1, link);
|
||||
assertAdminEvents.assertEvent(realmId, OperationType.CREATE,
|
||||
AdminEventPaths.userFederatedIdentityLink(keycloakUserId, identityProviderAlias1), link,
|
||||
ResourceType.USER);
|
||||
assertEquals(204, response1.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void searchByIdpAndEnabled() {
|
||||
// add sample Identity Provider
|
||||
final String identityProviderAlias = "identity-provider-alias";
|
||||
addSampleIdentityProvider(identityProviderAlias, 0);
|
||||
|
||||
// add disabled user with IDP link
|
||||
UserRepresentation disabledUser = new UserRepresentation();
|
||||
final String disabledUsername = "disabled_username";
|
||||
disabledUser.setUsername(disabledUsername);
|
||||
disabledUser.setEmail("disabled@localhost");
|
||||
disabledUser.setEnabled(false);
|
||||
final String disabledUserKeycloakId = createUser(disabledUser);
|
||||
FederatedIdentityRepresentation disabledUserLink = new FederatedIdentityRepresentation();
|
||||
final String disabledUserId = "disabledUserId";
|
||||
disabledUserLink.setUserId(disabledUserId);
|
||||
disabledUserLink.setUserName(disabledUsername);
|
||||
addFederatedIdentity(disabledUserKeycloakId, identityProviderAlias, disabledUserLink);
|
||||
|
||||
// add enabled user with IDP link
|
||||
UserRepresentation enabledUser = new UserRepresentation();
|
||||
final String enabledUsername = "enabled_username";
|
||||
enabledUser.setUsername(enabledUsername);
|
||||
enabledUser.setEmail("enabled@localhost");
|
||||
enabledUser.setEnabled(true);
|
||||
final String enabledUserKeycloakId = createUser(enabledUser);
|
||||
FederatedIdentityRepresentation enabledUserLink = new FederatedIdentityRepresentation();
|
||||
final String enabledUserId = "enabledUserId";
|
||||
enabledUserLink.setUserId(enabledUserId);
|
||||
enabledUserLink.setUserName(enabledUsername);
|
||||
addFederatedIdentity(enabledUserKeycloakId, identityProviderAlias, enabledUserLink);
|
||||
|
||||
// run search tests
|
||||
List<UserRepresentation> searchByIdpAliasAndEnabled =
|
||||
realm.users().search(null, null, null, null, null, identityProviderAlias, null, null, null, true, null);
|
||||
assertEquals(1, searchByIdpAliasAndEnabled.size());
|
||||
assertEquals(enabledUsername, searchByIdpAliasAndEnabled.get(0).getUsername());
|
||||
|
||||
List<UserRepresentation> searchByIdpAliasAndDisabled =
|
||||
realm.users().search(null, null, null, null, null, identityProviderAlias, null, null, null, false,
|
||||
null);
|
||||
assertEquals(1, searchByIdpAliasAndDisabled.size());
|
||||
assertEquals(disabledUsername, searchByIdpAliasAndDisabled.get(0).getUsername());
|
||||
|
||||
List<UserRepresentation> searchByIdpAliasWithoutEnabledFlag =
|
||||
realm.users().search(null, null, null, null, null, identityProviderAlias, null, null, null, null, null);
|
||||
assertEquals(2, searchByIdpAliasWithoutEnabledFlag.size());
|
||||
assertEquals(disabledUsername, searchByIdpAliasWithoutEnabledFlag.get(0).getUsername());
|
||||
assertEquals(enabledUsername, searchByIdpAliasWithoutEnabledFlag.get(1).getUsername());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void searchById() {
|
||||
String expectedUserId = createUsers().get(0);
|
||||
|
@ -829,9 +953,7 @@ public class UserTest extends AbstractAdminTest {
|
|||
FederatedIdentityRepresentation link = new FederatedIdentityRepresentation();
|
||||
link.setUserId("social-user-id");
|
||||
link.setUserName("social-username");
|
||||
Response response = user.addFederatedIdentity("social-provider-id", link);
|
||||
assertEquals(204, response.getStatus());
|
||||
assertAdminEvents.assertEvent(realmId, OperationType.CREATE, AdminEventPaths.userFederatedIdentityLink(id, "social-provider-id"), link, ResourceType.USER);
|
||||
addFederatedIdentity(id, "social-provider-id", link);
|
||||
|
||||
// Verify social link is here
|
||||
user = realm.users().get(id);
|
||||
|
@ -851,11 +973,15 @@ public class UserTest extends AbstractAdminTest {
|
|||
}
|
||||
|
||||
private void addSampleIdentityProvider() {
|
||||
addSampleIdentityProvider("social-provider-id", 0);
|
||||
}
|
||||
|
||||
private void addSampleIdentityProvider(final String alias, final int expectedInitialIdpCount) {
|
||||
List<IdentityProviderRepresentation> providers = realm.identityProviders().findAll();
|
||||
Assert.assertEquals(0, providers.size());
|
||||
Assert.assertEquals(expectedInitialIdpCount, providers.size());
|
||||
|
||||
IdentityProviderRepresentation rep = new IdentityProviderRepresentation();
|
||||
rep.setAlias("social-provider-id");
|
||||
rep.setAlias(alias);
|
||||
rep.setProviderId("oidc");
|
||||
|
||||
realm.identityProviders().create(rep);
|
||||
|
|
Loading…
Reference in a new issue