Skip to content

Commit

Permalink
KEYCLOAK-17084 KEYCLOAK-17434 Support querying clients by client attr…
Browse files Browse the repository at this point in the history
…ibutes
  • Loading branch information
vmuzikar authored and hmlnarik committed May 14, 2021
1 parent 62e17f3 commit 62e6883
Show file tree
Hide file tree
Showing 26 changed files with 566 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,8 @@ List<ClientRepresentation> findAll(@QueryParam("clientId") String clientId,
@Produces(MediaType.APPLICATION_JSON)
List<ClientRepresentation> findByClientId(@QueryParam("clientId") String clientId);

@GET
@Produces(MediaType.APPLICATION_JSON)
List<ClientRepresentation> query(@QueryParam("q") String searchQuery);

}
Original file line number Diff line number Diff line change
Expand Up @@ -819,6 +819,11 @@ public Stream<ClientModel> searchClientByClientIdStream(String clientId, Integer
return cacheSession.searchClientsByClientIdStream(this, clientId, firstResult, maxResults);
}

@Override
public Stream<ClientModel> searchClientByAttributes(Map<String, String> attributes, Integer firstResult, Integer maxResults) {
return cacheSession.searchClientsByAttributes(this, attributes, firstResult, maxResults);
}

@Override
public Stream<ClientModel> getClientsStream(Integer firstResult, Integer maxResults) {
return cacheSession.getClientsStream(this, firstResult, maxResults);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1161,6 +1161,11 @@ public Stream<ClientModel> searchClientsByClientIdStream(RealmModel realm, Strin
return getClientDelegate().searchClientsByClientIdStream(realm, clientId, firstResult, maxResults);
}

@Override
public Stream<ClientModel> searchClientsByAttributes(RealmModel realm, Map<String, String> attributes, Integer firstResult, Integer maxResults) {
return getClientDelegate().searchClientsByAttributes(realm, attributes, firstResult, maxResults);
}

@Override
public ClientModel getClientByClientId(RealmModel realm, String clientId) {
String cacheKey = getClientByClientIdCacheKey(clientId, realm.getId());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,30 @@
import org.keycloak.models.KeycloakSessionFactory;

import javax.persistence.EntityManager;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import static org.keycloak.models.jpa.JpaRealmProviderFactory.PROVIDER_ID;
import static org.keycloak.models.jpa.JpaRealmProviderFactory.PROVIDER_PRIORITY;

public class JpaClientProviderFactory implements ClientProviderFactory {

private Set<String> clientSearchableAttributes = null;

@Override
public void init(Config.Scope config) {
String[] searchableAttrsArr = config.getArray("searchableAttributes");
if (searchableAttrsArr == null) {
String s = System.getProperty("keycloak.client.searchableAttributes");
searchableAttrsArr = s == null ? null : s.split("\\s*,\\s*");
}
if (searchableAttrsArr != null) {
clientSearchableAttributes = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(searchableAttrsArr)));
}
else {
clientSearchableAttributes = Collections.emptySet();
}
}

@Override
Expand All @@ -47,7 +64,7 @@ public String getId() {
@Override
public ClientProvider create(KeycloakSession session) {
EntityManager em = session.getProvider(JpaConnectionProvider.class).getEntityManager();
return new JpaRealmProvider(session, em);
return new JpaRealmProvider(session, em, clientSearchableAttributes);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public String getId() {
@Override
public ClientScopeProvider create(KeycloakSession session) {
EntityManager em = session.getProvider(JpaConnectionProvider.class).getEntityManager();
return new JpaRealmProvider(session, em);
return new JpaRealmProvider(session, em, null);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public String getId() {
@Override
public GroupProvider create(KeycloakSession session) {
EntityManager em = session.getProvider(JpaConnectionProvider.class).getEntityManager();
return new JpaRealmProvider(session, em);
return new JpaRealmProvider(session, em, null);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import static org.keycloak.models.jpa.PaginationUtils.paginateQuery;
import static org.keycloak.utils.StreamsUtil.closing;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand All @@ -34,7 +35,11 @@
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaDelete;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Join;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;

import org.jboss.logging.Logger;
import org.keycloak.common.util.Time;
import org.keycloak.connections.jpa.util.JpaUtils;
Expand All @@ -55,6 +60,7 @@
import org.keycloak.models.RoleModel;
import org.keycloak.models.RoleProvider;
import org.keycloak.models.ServerInfoProvider;
import org.keycloak.models.jpa.entities.ClientAttributeEntity;
import org.keycloak.models.jpa.entities.ClientEntity;
import org.keycloak.models.jpa.entities.ClientInitialAccessEntity;
import org.keycloak.models.jpa.entities.ClientScopeClientMappingEntity;
Expand All @@ -74,10 +80,12 @@ public class JpaRealmProvider implements RealmProvider, ClientProvider, ClientSc
protected static final Logger logger = Logger.getLogger(JpaRealmProvider.class);
private final KeycloakSession session;
protected EntityManager em;
private Set<String> clientSearchableAttributes;

public JpaRealmProvider(KeycloakSession session, EntityManager em) {
public JpaRealmProvider(KeycloakSession session, EntityManager em, Set<String> clientSearchableAttributes) {
this.session = session;
this.em = em;
this.clientSearchableAttributes = clientSearchableAttributes;
}

@Override
Expand Down Expand Up @@ -685,6 +693,39 @@ public Stream<ClientModel> searchClientsByClientIdStream(RealmModel realm, Strin
return closing(results.map(c -> session.clients().getClientById(realm, c)));
}

@Override
public Stream<ClientModel> searchClientsByAttributes(RealmModel realm, Map<String, String> attributes, Integer firstResult, Integer maxResults) {
Map<String, String> filteredAttributes = clientSearchableAttributes == null ? attributes :
attributes.entrySet().stream().filter(m -> clientSearchableAttributes.contains(m.getKey()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<ClientEntity> queryBuilder = builder.createQuery(ClientEntity.class);
Root<ClientEntity> root = queryBuilder.from(ClientEntity.class);

List<Predicate> predicates = new ArrayList<>();

predicates.add(builder.equal(root.get("realmId"), realm.getId()));

for (Map.Entry<String, String> entry : filteredAttributes.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();

Join<ClientEntity, ClientAttributeEntity> attributeJoin = root.join("attributes");

Predicate attrNamePredicate = builder.equal(attributeJoin.get("name"), key);
Predicate attrValuePredicate = builder.equal(attributeJoin.get("value"), value);
predicates.add(builder.and(attrNamePredicate, attrValuePredicate));
}

Predicate finalPredicate = builder.and(predicates.toArray(new Predicate[0]));
queryBuilder.where(finalPredicate).orderBy(builder.asc(root.get("clientId")));

TypedQuery<ClientEntity> query = em.createQuery(queryBuilder);
return closing(paginateQuery(query, firstResult, maxResults).getResultStream())
.map(c -> session.clients().getClientById(realm, c.getId()));
}

@Override
public void removeClients(RealmModel realm) {
TypedQuery<String> query = em.createNamedQuery("getClientIdsByRealm", String.class);
Expand Down Expand Up @@ -963,4 +1004,8 @@ public boolean deleteLocalizationText(RealmModel realm, String locale, String ke
return false;
}
}

public Set<String> getClientSearchableAttributes() {
return clientSearchableAttributes;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public String getId() {
@Override
public JpaRealmProvider create(KeycloakSession session) {
EntityManager em = session.getProvider(JpaConnectionProvider.class).getEntityManager();
return new JpaRealmProvider(session, em);
return new JpaRealmProvider(session, em, null);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public String getId() {
@Override
public RoleProvider create(KeycloakSession session) {
EntityManager em = session.getProvider(JpaConnectionProvider.class).getEntityManager();
return new JpaRealmProvider(session, em);
return new JpaRealmProvider(session, em, null);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public String getId() {
@Override
public ServerInfoProvider create(KeycloakSession session) {
EntityManager em = session.getProvider(JpaConnectionProvider.class).getEntityManager();
return new JpaRealmProvider(session, em);
return new JpaRealmProvider(session, em, null);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -783,6 +783,11 @@ public Stream<ClientModel> searchClientByClientIdStream(String clientId, Integer
return session.clients().searchClientsByClientIdStream(this, clientId, firstResult, maxResults);
}

@Override
public Stream<ClientModel> searchClientByAttributes(Map<String, String> attributes, Integer firstResult, Integer maxResults) {
return session.clients().searchClientsByAttributes(this, attributes, firstResult, maxResults);
}

private static final String BROWSER_HEADER_PREFIX = "_browser_header.";

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,21 @@ public Stream<ClientModel> searchClientsByClientIdStream(RealmModel realm, Strin
return paginatedStream(s, firstResult, maxResults).map(entityToAdapterFunc(realm));
}

@Override
public Stream<ClientModel> searchClientsByAttributes(RealmModel realm, Map<String, String> attributes, Integer firstResult, Integer maxResults) {
ModelCriteriaBuilder<ClientModel> mcb = clientStore.createCriteriaBuilder()
.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId());

for (Map.Entry<String, String> entry : attributes.entrySet()) {
mcb = mcb.compare(SearchableFields.ATTRIBUTE, Operator.EQ, entry.getKey(), entry.getValue());
}

Stream<MapClientEntity<K>> s = tx.getUpdatedNotRemoved(mcb)
.sorted(COMPARE_BY_CLIENT_ID);

return paginatedStream(s, firstResult, maxResults).map(entityToAdapterFunc(realm));
}

@Override
public void addClientScopes(RealmModel realm, ClientModel client, Set<ClientScopeModel> clientScopes, boolean defaultScope) {
final String id = client.getId();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,11 @@ public Stream<ClientModel> searchClientByClientIdStream(String clientId, Integer
return session.clients().searchClientsByClientIdStream(this, clientId, firstResult, maxResults);
}

@Override
public Stream<ClientModel> searchClientByAttributes(Map<String, String> attributes, Integer firstResult, Integer maxResults) {
return session.clients().searchClientsByAttributes(this, attributes, firstResult, maxResults);
}

@Override
public Map<String, String> getSmtpConfig() {
return Collections.unmodifiableMap(entity.getSmtpConfig());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,12 @@ public Stream<ClientModel> searchClientsByClientIdStream(RealmModel realm, Strin
return session.clients().searchClientsByClientIdStream(realm, clientId, firstResult, maxResults);
}

@Override
@Deprecated
public Stream<ClientModel> searchClientsByAttributes(RealmModel realm, Map<String, String> attributes, Integer firstResult, Integer maxResults) {
return session.clients().searchClientsByAttributes(realm, attributes, firstResult, maxResults);
}

@Override
@Deprecated
public void addClientScopes(RealmModel realm, ClientModel client, Set<ClientScopeModel> clientScopes, boolean defaultScope) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ public class MapFieldPredicates {
put(CLIENT_PREDICATES, ClientModel.SearchableFields.REALM_ID, MapClientEntity::getRealmId);
put(CLIENT_PREDICATES, ClientModel.SearchableFields.CLIENT_ID, MapClientEntity::getClientId);
put(CLIENT_PREDICATES, ClientModel.SearchableFields.SCOPE_MAPPING_ROLE, MapFieldPredicates::checkScopeMappingRole);
put(CLIENT_PREDICATES, ClientModel.SearchableFields.ATTRIBUTE, MapFieldPredicates::checkClientAttributes);

put(CLIENT_SCOPE_PREDICATES, ClientScopeModel.SearchableFields.REALM_ID, MapClientScopeEntity::getRealmId);
put(CLIENT_SCOPE_PREDICATES, ClientScopeModel.SearchableFields.NAME, MapClientScopeEntity::getName);
Expand Down Expand Up @@ -214,7 +215,7 @@ private static <K, V extends AbstractEntity<K>, M> void put(
SearchableModelField<M> field, UpdatePredicatesFunc<K, V, M> function) {
map.put(field, function);
}

private static <V extends AbstractEntity<?>> Function<V, Object> predicateForKeyField(Function<V, Object> extractor) {
return entity -> {
Object o = extractor.apply(entity);
Expand Down Expand Up @@ -287,6 +288,22 @@ private static MapModelCriteriaBuilder<Object, MapUserEntity<Object>, UserModel>
return mcb.fieldCompare(Boolean.TRUE::equals, getter);
}

private static MapModelCriteriaBuilder<Object, MapClientEntity<Object>, ClientModel> checkClientAttributes(MapModelCriteriaBuilder<Object, MapClientEntity<Object>, ClientModel> mcb, Operator op, Object[] values) {
if (values == null || values.length != 2) {
throw new CriterionNotSupportedException(ClientModel.SearchableFields.ATTRIBUTE, op, "Invalid arguments, expected attribute_name-value pair, got: " + Arrays.toString(values));
}

final Object attrName = values[0];
if (! (attrName instanceof String)) {
throw new CriterionNotSupportedException(ClientModel.SearchableFields.ATTRIBUTE, op, "Invalid arguments, expected (String attribute_name), got: " + Arrays.toString(values));
}
String attrNameS = (String) attrName;
Function<MapClientEntity<Object>, ?> getter = ue -> ue.getAttribute(attrNameS);
Object[] realValue = {values[1]};

return mcb.fieldCompare(op, getter, realValue);
}

private static MapModelCriteriaBuilder<Object, MapUserEntity<Object>, UserModel> checkGrantedUserRole(MapModelCriteriaBuilder<Object, MapUserEntity<Object>, UserModel> mcb, Operator op, Object[] values) {
String roleIdS = ensureEqSingleValue(UserModel.SearchableFields.ASSIGNED_ROLE, "role_id", op, values);
Function<MapUserEntity<Object>, ?> getter;
Expand Down
6 changes: 6 additions & 0 deletions server-spi/src/main/java/org/keycloak/models/ClientModel.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ public static class SearchableFields {
public static final SearchableModelField<ClientModel> REALM_ID = new SearchableModelField<>("realmId", String.class);
public static final SearchableModelField<ClientModel> CLIENT_ID = new SearchableModelField<>("clientId", String.class);
public static final SearchableModelField<ClientModel> SCOPE_MAPPING_ROLE = new SearchableModelField<>("scopeMappingRole", String.class);

/**
* Search for attribute value. The parameters is a pair {@code (attribute_name, values...)} where {@code attribute_name}
* is always checked for equality, and the value is checked per the operator.
*/
public static final SearchableModelField<ClientModel> ATTRIBUTE = new SearchableModelField<>("attribute", String[].class);
}

interface ClientCreationEvent extends ProviderEvent {
Expand Down
4 changes: 3 additions & 1 deletion server-spi/src/main/java/org/keycloak/models/RealmModel.java
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,9 @@ default List<ClientModel> searchClientByClientId(String clientId, Integer firstR
* @return Stream of {@link ClientModel}. Never returns {@code null}.
*/
Stream<ClientModel> searchClientByClientIdStream(String clientId, Integer firstResult, Integer maxResults);


Stream<ClientModel> searchClientByAttributes(Map<String, String> attributes, Integer firstResult, Integer maxResults);

void updateRequiredCredentials(Set<String> creds);

Map<String, String> getBrowserSecurityHeaders();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ default List<ClientModel> searchClientsByClientId(String clientId, Integer first
*/
Stream<ClientModel> searchClientsByClientIdStream(RealmModel realm, String clientId, Integer firstResult, Integer maxResults);

Stream<ClientModel> searchClientsByAttributes(RealmModel realm, Map<String, String> attributes, Integer firstResult, Integer maxResults);

/**
* Return all default scopes (if {@code defaultScope} is {@code true}) or all optional scopes (if {@code defaultScope} is {@code false}) linked with the client
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import org.keycloak.services.managers.ClientManager;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
import org.keycloak.utils.SearchQueryUtils;
import org.keycloak.validation.ValidationUtil;

import javax.ws.rs.Consumes;
Expand All @@ -54,6 +55,7 @@
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Stream;

Expand Down Expand Up @@ -100,16 +102,23 @@ public ClientsResource(RealmModel realm, AdminPermissionEvaluator auth, AdminEve
public Stream<ClientRepresentation> getClients(@QueryParam("clientId") String clientId,
@QueryParam("viewableOnly") @DefaultValue("false") boolean viewableOnly,
@QueryParam("search") @DefaultValue("false") boolean search,
@QueryParam("q") String searchQuery,
@QueryParam("first") Integer firstResult,
@QueryParam("max") Integer maxResults) {
boolean canView = auth.clients().canView();
Stream<ClientModel> clientModels = Stream.empty();

if (clientId == null || clientId.trim().equals("")) {
if (searchQuery != null) {
auth.clients().requireList();
Map<String, String> attributes = SearchQueryUtils.getFields(searchQuery);
clientModels = canView
? realm.searchClientByAttributes(attributes, firstResult, maxResults)
: realm.searchClientByAttributes(attributes, -1, -1);
} else if (clientId == null || clientId.trim().equals("")) {
auth.clients().requireList();
clientModels = canView
? realm.getClientsStream(firstResult, maxResults)
: realm.getClientsStream();
auth.clients().requireList();
} else if (search) {
clientModels = canView
? realm.searchClientByClientIdStream(clientId, firstResult, maxResults)
Expand Down
Loading

0 comments on commit 62e6883

Please sign in to comment.