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:
Daniel Fesenmeyer 2020-07-30 03:30:40 -07:00 committed by Stian Thorgersen
parent f917302ace
commit de8d2eafa3
6 changed files with 254 additions and 43 deletions

View file

@ -53,6 +53,38 @@ public interface UsersResource {
@QueryParam("enabled") Boolean enabled, @QueryParam("enabled") Boolean enabled,
@QueryParam("briefRepresentation") Boolean briefRepresentation); @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 @GET
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
List<UserRepresentation> search(@QueryParam("username") String username, List<UserRepresentation> search(@QueryParam("username") String username,

View file

@ -54,6 +54,7 @@ import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Expression; import javax.persistence.criteria.Expression;
import javax.persistence.criteria.Join;
import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root; import javax.persistence.criteria.Root;
import javax.persistence.criteria.Subquery; import javax.persistence.criteria.Subquery;
@ -840,6 +841,8 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore {
predicates.add(root.get("serviceAccountClientLink").isNull()); predicates.add(root.get("serviceAccountClientLink").isNull());
} }
Join<Object, Object> federatedIdentitiesJoin = null;
for (Map.Entry<String, String> entry : attributes.entrySet()) { for (Map.Entry<String, String> entry : attributes.entrySet()) {
String key = entry.getKey(); String key = entry.getKey();
String value = entry.getValue(); String value = entry.getValue();
@ -852,7 +855,8 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore {
case UserModel.SEARCH: case UserModel.SEARCH:
List<Predicate> orPredicates = new ArrayList(); 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(root.get(EMAIL)), "%" + value.toLowerCase() + "%"));
orPredicates.add(builder.like( orPredicates.add(builder.like(
builder.lower(builder.concat(builder.concat( 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()))); predicates.add(builder.equal(root.get(key), Boolean.parseBoolean(value.toLowerCase())));
break; break;
case UserModel.ENABLED: 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;
} }
} }

View file

@ -110,6 +110,11 @@ public class UserEntity {
@BatchSize(size = 20) @BatchSize(size = 20)
protected Collection<CredentialEntity> credentials; protected Collection<CredentialEntity> credentials;
@OneToMany(mappedBy="user")
@Fetch(FetchMode.SELECT)
@BatchSize(size = 20)
protected Collection<FederatedIdentityEntity> federatedIdentities;
@Column(name="FEDERATION_LINK") @Column(name="FEDERATION_LINK")
protected String federationLink; protected String federationLink;
@ -233,6 +238,17 @@ public class UserEntity {
this.credentials = credentials; 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() { public String getFederationLink() {
return federationLink; return federationLink;
} }

View file

@ -38,6 +38,8 @@ public interface UserModel extends RoleMapperModel {
String EMAIL_VERIFIED = "emailVerified"; String EMAIL_VERIFIED = "emailVerified";
String LOCALE = "locale"; String LOCALE = "locale";
String ENABLED = "enabled"; 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 INCLUDE_SERVICE_ACCOUNT = "keycloak.session.realm.users.query.include_service_account";
String GROUPS = "keycloak.session.realm.users.query.groups"; String GROUPS = "keycloak.session.realm.users.query.groups";
String SEARCH = "keycloak.session.realm.users.query.search"; String SEARCH = "keycloak.session.realm.users.query.search";

View file

@ -208,14 +208,19 @@ public class UsersResource {
* Returns a list of users, filtered according to query parameters * Returns a list of users, filtered according to query parameters
* *
* @param search A String contained in username, first or last name, or email * @param search A String contained in username, first or last name, or email
* @param last * @param last A String contained in lastName, or the complete lastName, if param "exact" is true
* @param first * @param first A String contained in firstName, or the complete firstName, if param "exact" is true
* @param email * @param email A String contained in email, or the complete email, if param "exact" is true
* @param username * @param username A String contained in username, or the complete username, if param "exact" is true
* @param enabled Boolean representing if user is enabled or not * @param emailVerified whether the email has been verified
* @param first Pagination offset * @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) * @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 @GET
@NoCache @NoCache
@ -226,6 +231,8 @@ public class UsersResource {
@QueryParam("email") String email, @QueryParam("email") String email,
@QueryParam("username") String username, @QueryParam("username") String username,
@QueryParam("emailVerified") Boolean emailVerified, @QueryParam("emailVerified") Boolean emailVerified,
@QueryParam("idpAlias") String idpAlias,
@QueryParam("idpUserId") String idpUserId,
@QueryParam("first") Integer firstResult, @QueryParam("first") Integer firstResult,
@QueryParam("max") Integer maxResults, @QueryParam("max") Integer maxResults,
@QueryParam("enabled") Boolean enabled, @QueryParam("enabled") Boolean enabled,
@ -241,7 +248,8 @@ public class UsersResource {
List<UserModel> userModels = Collections.emptyList(); List<UserModel> userModels = Collections.emptyList();
if (search != null) { if (search != null) {
if (search.startsWith(SEARCH_ID_PARAMETER)) { 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) { if (userModel != null) {
userModels = Collections.singletonList(userModel); userModels = Collections.singletonList(userModel);
} }
@ -251,35 +259,45 @@ public class UsersResource {
if (enabled != null) { if (enabled != null) {
attributes.put(UserModel.ENABLED, enabled.toString()); 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) { } else if (last != null || first != null || email != null || username != null || emailVerified != null
Map<String, String> attributes = new HashMap<>(); || idpAlias != null || idpUserId != null || enabled != null || exact != null) {
if (last != null) { Map<String, String> attributes = new HashMap<>();
attributes.put(UserModel.LAST_NAME, last); if (last != null) {
} attributes.put(UserModel.LAST_NAME, last);
if (first != null) { }
attributes.put(UserModel.FIRST_NAME, first); if (first != null) {
} attributes.put(UserModel.FIRST_NAME, first);
if (email != null) { }
attributes.put(UserModel.EMAIL, email); if (email != null) {
} attributes.put(UserModel.EMAIL, email);
if (username != null) { }
attributes.put(UserModel.USERNAME, username); if (username != null) {
} attributes.put(UserModel.USERNAME, username);
if (enabled != null) { }
attributes.put(UserModel.ENABLED, enabled.toString()); if (emailVerified != null) {
} attributes.put(UserModel.EMAIL_VERIFIED, emailVerified.toString());
if (exact != null) { }
attributes.put(UserModel.EXACT, exact.toString()); if (idpAlias != null) {
} attributes.put(UserModel.IDP_ALIAS, idpAlias);
if (emailVerified != null) { }
attributes.put(UserModel.EMAIL_VERIFIED, emailVerified.toString()); if (idpUserId != null) {
} attributes.put(UserModel.IDP_USER_ID, idpUserId);
return searchForUser(attributes, realm, userPermissionEvaluator, briefRepresentation, firstResult, maxResults, true); }
} else { if (enabled != null) {
return searchForUser(new HashMap<>(), realm, userPermissionEvaluator, briefRepresentation, firstResult, maxResults, false); 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); return toRepresentation(realm, userPermissionEvaluator, briefRepresentation, userModels);
} }

View file

@ -718,6 +718,130 @@ public class UserTest extends AbstractAdminTest {
assertEquals(0, searchInvalidSizeAndDisabled.size()); 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 @Test
public void searchById() { public void searchById() {
String expectedUserId = createUsers().get(0); String expectedUserId = createUsers().get(0);
@ -829,9 +953,7 @@ public class UserTest extends AbstractAdminTest {
FederatedIdentityRepresentation link = new FederatedIdentityRepresentation(); FederatedIdentityRepresentation link = new FederatedIdentityRepresentation();
link.setUserId("social-user-id"); link.setUserId("social-user-id");
link.setUserName("social-username"); link.setUserName("social-username");
Response response = user.addFederatedIdentity("social-provider-id", link); addFederatedIdentity(id, "social-provider-id", link);
assertEquals(204, response.getStatus());
assertAdminEvents.assertEvent(realmId, OperationType.CREATE, AdminEventPaths.userFederatedIdentityLink(id, "social-provider-id"), link, ResourceType.USER);
// Verify social link is here // Verify social link is here
user = realm.users().get(id); user = realm.users().get(id);
@ -851,11 +973,15 @@ public class UserTest extends AbstractAdminTest {
} }
private void addSampleIdentityProvider() { private void addSampleIdentityProvider() {
addSampleIdentityProvider("social-provider-id", 0);
}
private void addSampleIdentityProvider(final String alias, final int expectedInitialIdpCount) {
List<IdentityProviderRepresentation> providers = realm.identityProviders().findAll(); List<IdentityProviderRepresentation> providers = realm.identityProviders().findAll();
Assert.assertEquals(0, providers.size()); Assert.assertEquals(expectedInitialIdpCount, providers.size());
IdentityProviderRepresentation rep = new IdentityProviderRepresentation(); IdentityProviderRepresentation rep = new IdentityProviderRepresentation();
rep.setAlias("social-provider-id"); rep.setAlias(alias);
rep.setProviderId("oidc"); rep.setProviderId("oidc");
realm.identityProviders().create(rep); realm.identityProviders().create(rep);