8000 Advanced query language to search for users by nicolasiannarilli · Pull Request #15998 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

package org.keycloak.storage.ldap;

import static org.keycloak.utils.StreamsUtil.paginatedStream;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
Expand Down Expand Up @@ -52,11 +54,22 @@
import org.keycloak.models.UserManager;
import org.keycloak.models.UserModel;
import org.keycloak.models.cache.CachedUserModel;
import org.keycloak.models.cache.UserCache;
import org.keycloak.models.credential.PasswordCredentialModel;
import org.keycloak.models.search.SearchQueryJson;
import org.keycloak.models.search.SearchQueryJsonAnd;
import org.keycloak.models.search.SearchQueryJsonEquals;
import org.keycloak.models.search.SearchQueryJsonGt;
import org.keycloak.models.search.SearchQueryJsonGte;
import org.keycloak.models.search.SearchQueryJsonIn;
import org.keycloak.models.search.SearchQueryJsonLike;
import org.keycloak.models.search.SearchQueryJsonLt;
import org.keycloak.models.search.SearchQueryJsonLte;
import org.keycloak.models.search.SearchQueryJsonNot;
import org.keycloak.models.search.SearchQueryJsonOr;
import org.keycloak.models.utils.ReadOnlyUserModelDelegate;
import org.keycloak.policy.PasswordPolicyManagerProvider;
import org.keycloak.policy.PolicyError;
import org.keycloak.models.cache.UserCache;
import org.keycloak.storage.DatastoreProvider;
import org.keycloak.storage.LegacyStoreManagers;
import org.keycloak.storage.ReadOnlyException;
Expand Down Expand Up @@ -84,8 +97,6 @@
import org.keycloak.storage.user.UserQueryProvider;
import org.keycloak.storage.user.UserRegistrationProvider;

import static org.keycloak.utils.StreamsUtil.paginatedStream;

/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
Expand Down Expand Up @@ -840,4 +851,65 @@ public LDAPObject loadLDAPUserByUuid(RealmModel realm, String uuid) {
}
}

@Override
public Stream<UserModel> searchForUserStream(RealmModel realm, SearchQueryJson query, Integer firstResult, Integer maxResults) {
Stream<LDAPObject> stream = searchLDAP(realm, query).stream()
.filter(ldapObject -> {
String ldapUsername = LDAPUtils.getUsername(ldapObject, this.ldapIdentityStore.getConfig());
return (UserStoragePrivateUtil.userLocalStorage(session).getUserByUsername(realm, ldapUsername) == null);
});

return paginatedStream(stream, firstResult, maxResults).map(ldapObject -> importUserFromLDAP(session, realm, ldapObject));
}

protected List<LDAPObject> searchLDAP(RealmModel realm, SearchQueryJson query) {
List<LDAPObject> results = new ArrayList<LDAPObject>();

try (LDAPQuery ldapQuery = LDAPUtils.createQueryForUserSearch(this, realm)) {
ldapQuery.addWhereCondition(buildCondition(query));

results.addAll(ldapQuery.getResultList());
}

return results;
}

private Condition buildCondition(SearchQueryJson query) {
LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder();
switch (query.getOperator()) {
case AND:
SearchQueryJsonAnd queryAnd = (SearchQueryJsonAnd) query;
return conditionsBuilder.andCondition(queryAnd.getValues().stream().map(this::buildCondition).toArray(Condition[]::new));
case OR:
SearchQueryJsonOr queryOr = (SearchQueryJsonOr) query;
return conditionsBuilder.orCondition(queryOr.getValues().stream().map(this::buildCondition).toArray(Condition[]::new));
case IN:
SearchQueryJsonIn queryIn = (SearchQueryJsonIn) query;
return conditionsBuilder.in(queryIn.getProperty(), queryIn.getValues().toArray());
case NOT:
SearchQueryJsonNot queryNot = (SearchQueryJsonNot) query;
return conditionsBuilder.notCondition(buildCondition(queryNot));
case EQUALS:
SearchQueryJsonEquals queryEquals = (SearchQueryJsonEquals) query;
return conditionsBuilder.equal(queryEquals.getProperty(), queryEquals.getValue(), EscapeStrategy.NON_ASCII_CHARS_ONLY);
case LIKE:
SearchQueryJsonLike queryLike = (SearchQueryJsonLike) query;
return conditionsBuilder.equal(queryLike.getProperty(), queryLike.getValue(), EscapeStrategy.NON_ASCII_CHARS_ONLY);
case GT:
SearchQueryJsonGt queryGt = (SearchQueryJsonGt) query;
return conditionsBuilder.greaterThan(queryGt.getProperty(), queryGt.getValue());
case GTE:
SearchQueryJsonGte queryGte = (SearchQueryJsonGte) query;
return conditionsBuilder.greaterThanOrEqualTo(queryGte.getProperty(), queryGte.getValue());
case LT:
SearchQueryJsonLt queryLt = (SearchQueryJsonLt) query;
return conditionsBuilder.lessThan(queryLt.getProperty(), queryLt.getValue());
case LTE:
SearchQueryJsonLte queryLte = (SearchQueryJsonLte) query;
return conditionsBuilder.lessThanOrEqualTo(queryLte.getProperty(), queryLte.getValue());
default:
throw new ModelException("No implementation to build condition available for " + query.getOperator().name());
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* Copyright 2016 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.ldap.idm.query.internal;

import org.keycloak.storage.ldap.idm.query.Condition;

/**
* @author Nicolas Iannarilli
*/
class AndCondition implements Condition {

private final Condition[] innerConditions;

public AndCondition(Condition... innerConditions) {
this.innerConditions = innerConditions;
}

@Override
public String getParameterName() {
return null;
}

@Override
public void setParameterName(String parameterName) {
}

@Override
public void updateParameterName(String modelParamName, String ldapParamName) {
for (Condition innerCondition : innerConditions) {
innerCondition.updateParameterName(modelParamName, ldapParamName);
}
}

@Override
public void applyCondition(StringBuilder filter) {
filter.append("(&");

for (Condition innerCondition : innerConditions) {
innerCondition.applyCondition(filter);
}

filter.append(")");
}

@Override
public void setBinary(boolean binary) {
}

@Override
public boolean isBinary() {
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,27 @@ public Condition between(String paramName, Comparable x, Comparable y) {
return new BetweenCondition(paramName, x, y);
}

public Condition andCondition(Condition... conditions) {
if (conditions == null || conditions.length == 0) {
throw new ModelException("At least one condition should be provided to AND query");
}
return new AndCondition(conditions);
}

public Condition orCondition(Condition... conditions) {
if (conditions == null || conditions.length == 0) {
throw new ModelException("At least one condition should be provided to OR query");
}
return new OrCondition(conditions);
}

public Condition notCondition(Condition condition) {
if (condition == null) {
throw new ModelException("One condition should be provided to NOT query");
}
return new NotCondition(condition);
}

public Condition addCustomLDAPFilter(String filter) {
filter = filter.trim();
return new CustomLDAPFilter(filter);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Copyright 2016 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.ldap.idm.query.internal;

import org.keycloak.storage.ldap.idm.query.Condition;

/**
* @author Nicolas Iannarilli
*/
class NotCondition implements Condition {

private final Condition innerCondition;

public NotCondition(Condition innerCondition) {
this.innerCondition = innerCondition;
}

@Override
public String getParameterName() {
return null;
}

@Override
public void setParameterName(String parameterName) {
}

@Override
public void updateParameterName(String modelParamName, String ldapParamName) {
innerCondition.updateParameterName(modelParamName, ldapParamName);
}

@Override
public void applyCondition(StringBuilder filter) {
filter.append("(!");
innerCondition.applyCondition(filter);
filter.append(")");
}

@Override
public void setBinary(boolean binary) {
}

@Override
public boolean isBinary() {
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,13 @@ List<UserRepresentation> search(@QueryParam("search") String search,
@QueryParam("max") Integer maxResults,
@QueryParam("briefRepresentation") Boolean briefRepresentation);

@GET
@Produces(MediaType.APPLICATION_JSON)
List<UserRepresentation> searchJson(@QueryParam("query") String query,
@QueryParam("first") Integer firstResult,
@QueryParam("max") Integer maxResults,
@QueryParam("briefRepresentation") Boolean briefRepresentation);

@GET
@Produces(MediaType.APPLICATION_JSON)
List<UserRepresentation> list(@QueryParam("first") Integer firstResult,
Expand Down Expand Up @@ -248,6 +255,18 @@ List<UserRepresentation> list(@QueryParam("first") Integer firstResult,
@Produces(MediaType.APPLICATION_JSON)
Integer count(@QueryParam("search") String search);

/**
* Returns the number of users that can be viewed and match the given search criteria.
* If none is specified this is equivalent to {{@link #count()}}.
*
* @param search criteria to search for
* @return number of users matching the search criteria
*/
@Path("count")
@GET
@Produces(MediaType.APPLICATION_JSON)
Integer countJson(@QueryParam("query") String query);

/**
* Returns the number of users that can be viewed and match the given filters.
* If none of the filters is specified this is equivalent to {{@link #count()}}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
import org.keycloak.models.cache.infinispan.events.UserFullInvalidationEvent;
import org.keycloak.models.cache.infinispan.events.UserUpdatedEvent;
import org.keycloak.models.cache.infinispan.stream.InIdentityProviderPredicate;
import org.keycloak.models.search.SearchQueryJson;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.ReadOnlyUserModelDelegate;
import org.keycloak.storage.CacheableStorageProviderModel;
Expand Down Expand Up @@ -605,6 +606,11 @@ public Stream<UserModel> searchForUserByUserAttributeStream(RealmModel realm, St
return getDelegate().searchForUserByUserAttributeStream(realm, attrName, attrValue);
}

@Override
public Stream<UserModel> searchForUserStream(RealmModel realm, SearchQueryJson query, Integer firstResult, Integer maxResults) {
return getDelegate().searchForUserStream(realm, query, firstResult, maxResults);
}

@Override
public Stream<FederatedIdentityModel> getFederatedIdentitiesStream(RealmModel realm, UserModel user) {
logger.tracev("getFederatedIdentities: {0}", user.getUsername());
Expand Down
Loading
0