blob: a3886461aef796deedb441ecd859400a63b97863 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.syncope.core.persistence.jpa.dao;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.commons.lang3.tuple.Triple;
import org.apache.syncope.common.lib.SyncopeClientException;
import org.apache.syncope.common.lib.SyncopeConstants;
import org.apache.syncope.common.lib.types.AnyTypeKind;
import org.apache.syncope.common.lib.types.AttrSchemaType;
import org.apache.syncope.common.lib.types.ClientExceptionType;
import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO;
import org.apache.syncope.core.persistence.api.dao.DynRealmDAO;
import org.apache.syncope.core.persistence.api.dao.GroupDAO;
import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO;
import org.apache.syncope.core.persistence.api.dao.RealmDAO;
import org.apache.syncope.core.persistence.api.dao.UserDAO;
import org.apache.syncope.core.persistence.api.dao.search.AnyCond;
import org.apache.syncope.core.persistence.api.dao.search.AnyTypeCond;
import org.apache.syncope.core.persistence.api.dao.search.AssignableCond;
import org.apache.syncope.core.persistence.api.dao.search.AttrCond;
import org.apache.syncope.core.persistence.api.dao.search.DynRealmCond;
import org.apache.syncope.core.persistence.api.dao.search.MemberCond;
import org.apache.syncope.core.persistence.api.dao.search.MembershipCond;
import org.apache.syncope.core.persistence.api.dao.search.OrderByClause;
import org.apache.syncope.core.persistence.api.dao.search.PrivilegeCond;
import org.apache.syncope.core.persistence.api.dao.search.RelationshipCond;
import org.apache.syncope.core.persistence.api.dao.search.RelationshipTypeCond;
import org.apache.syncope.core.persistence.api.dao.search.ResourceCond;
import org.apache.syncope.core.persistence.api.dao.search.RoleCond;
import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
import org.apache.syncope.core.persistence.api.entity.Any;
import org.apache.syncope.core.persistence.api.entity.AnyUtils;
import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory;
import org.apache.syncope.core.persistence.api.entity.DynRealm;
import org.apache.syncope.core.persistence.api.entity.EntityFactory;
import org.apache.syncope.core.persistence.api.entity.PlainAttrValue;
import org.apache.syncope.core.persistence.api.entity.PlainSchema;
import org.apache.syncope.core.persistence.api.entity.Realm;
import org.apache.syncope.core.provisioning.api.utils.RealmUtils;
import org.apache.syncope.core.spring.security.AuthContextUtils;
import org.apache.syncope.ext.elasticsearch.client.ElasticsearchUtils;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchType;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.index.query.DisMaxQueryBuilder;
import org.elasticsearch.index.query.MatchAllQueryBuilder;
import org.elasticsearch.index.query.MatchNoneQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.sort.FieldSortBuilder;
import org.elasticsearch.search.sort.SortBuilder;
import org.elasticsearch.search.sort.SortOrder;
/**
* Search engine implementation for users, groups and any objects, based on Elasticsearch.
*/
public class ElasticsearchAnySearchDAO extends AbstractAnySearchDAO {
protected static final QueryBuilder MATCH_NONE_QUERY_BUILDER = new MatchNoneQueryBuilder();
protected static final QueryBuilder MATCH_ALL_QUERY_BUILDER = new MatchAllQueryBuilder();
protected static final char[] ELASTICSEARCH_REGEX_CHARS = new char[] {
'.', '?', '+', '*', '|', '{', '}', '[', ']', '(', ')', '"', '\\', '&' };
protected static String escapeForLikeRegex(final char c) {
StringBuilder output = new StringBuilder();
if (ArrayUtils.contains(ELASTICSEARCH_REGEX_CHARS, c)) {
output.append('\\');
}
output.append(c);
return output.toString();
}
protected final RestHighLevelClient client;
protected final ElasticsearchUtils elasticsearchUtils;
public ElasticsearchAnySearchDAO(
final RealmDAO realmDAO,
final DynRealmDAO dynRealmDAO,
final UserDAO userDAO,
final GroupDAO groupDAO,
final AnyObjectDAO anyObjectDAO,
final PlainSchemaDAO schemaDAO,
final EntityFactory entityFactory,
final AnyUtilsFactory anyUtilsFactory,
final RestHighLevelClient client,
final ElasticsearchUtils elasticsearchUtils) {
super(realmDAO, dynRealmDAO, userDAO, groupDAO, anyObjectDAO, schemaDAO, entityFactory, anyUtilsFactory);
this.client = client;
this.elasticsearchUtils = elasticsearchUtils;
}
protected Triple<Optional<QueryBuilder>, Set<String>, Set<String>> getAdminRealmsFilter(
final AnyTypeKind kind, final Set<String> adminRealms) {
DisMaxQueryBuilder builder = QueryBuilders.disMaxQuery();
Set<String> dynRealmKeys = new HashSet<>();
Set<String> groupOwners = new HashSet<>();
adminRealms.forEach(realmPath -> {
Optional<Pair<String, String>> goRealm = RealmUtils.parseGroupOwnerRealm(realmPath);
if (goRealm.isPresent()) {
groupOwners.add(goRealm.get().getRight());
} else if (realmPath.startsWith("/")) {
Realm realm = realmDAO.findByFullPath(realmPath);
if (realm == null) {
SyncopeClientException noRealm = SyncopeClientException.build(ClientExceptionType.InvalidRealm);
noRealm.getElements().add("Invalid realm specified: " + realmPath);
throw noRealm;
} else {
realmDAO.findDescendants(realm).forEach(
descendant -> builder.add(QueryBuilders.termQuery("realm", descendant.getFullPath())));
}
} else {
DynRealm dynRealm = dynRealmDAO.find(realmPath);
if (dynRealm == null) {
LOG.warn("Ignoring invalid dynamic realm {}", realmPath);
} else {
dynRealmKeys.add(dynRealm.getKey());
builder.add(QueryBuilders.termQuery("dynRealm", dynRealm.getKey()));
}
}
});
return Triple.of(
dynRealmKeys.isEmpty() && groupOwners.isEmpty() ? Optional.of(builder) : Optional.empty(),
dynRealmKeys,
groupOwners);
}
protected SearchRequest searchRequest(
final Set<String> adminRealms,
final SearchCond cond,
final AnyTypeKind kind,
final int from,
final int size,
final List<SortBuilder<?>> sortBuilders) {
Triple<Optional<QueryBuilder>, Set<String>, Set<String>> filter = getAdminRealmsFilter(kind, adminRealms);
QueryBuilder queryBuilder;
if (SyncopeConstants.FULL_ADMIN_REALMS.equals(adminRealms)) {
queryBuilder = getQueryBuilder(cond, kind);
} else {
queryBuilder = getQueryBuilder(buildEffectiveCond(cond, filter.getMiddle(), filter.getRight(), kind), kind);
if (filter.getLeft().isPresent()) {
queryBuilder = QueryBuilders.boolQuery().
must(filter.getLeft().get()).
must(queryBuilder);
}
}
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder().
query(queryBuilder).
from(from).
size(size);
sortBuilders.forEach(sourceBuilder::sort);
return new SearchRequest(ElasticsearchUtils.getContextDomainName(AuthContextUtils.getDomain(), kind)).
searchType(SearchType.QUERY_THEN_FETCH).
source(sourceBuilder);
}
@Override
protected int doCount(final Set<String> adminRealms, final SearchCond cond, final AnyTypeKind kind) {
SearchRequest request = searchRequest(adminRealms, cond, kind, 0, 0, List.of());
try {
return (int) client.search(request, RequestOptions.DEFAULT).getHits().getTotalHits().value;
} catch (IOException e) {
LOG.error("Search error", e);
return 0;
}
}
protected List<SortBuilder<?>> sortBuilders(
final AnyTypeKind kind,
final List<OrderByClause> orderBy) {
AnyUtils anyUtils = anyUtilsFactory.getInstance(kind);
List<SortBuilder<?>> builders = new ArrayList<>();
orderBy.forEach(clause -> {
String sortName = null;
// Manage difference among external key attribute and internal JPA @Id
String fieldName = "key".equals(clause.getField()) ? "id" : clause.getField();
Field anyField = anyUtils.getField(fieldName);
if (anyField == null) {
PlainSchema schema = plainSchemaDAO.find(fieldName);
if (schema != null) {
sortName = fieldName;
}
} else {
sortName = fieldName;
}
if (sortName == null) {
LOG.warn("Cannot build any valid clause from {}", clause);
} else {
builders.add(new FieldSortBuilder(sortName).order(SortOrder.valueOf(clause.getDirection().name())));
}
});
return builders;
}
@Override
protected <T extends Any<?>> List<T> doSearch(
final Set<String> adminRealms,
final SearchCond cond,
final int page,
final int itemsPerPage,
final List<OrderByClause> orderBy,
final AnyTypeKind kind) {
SearchRequest request = searchRequest(
adminRealms,
cond,
kind,
(itemsPerPage * (page <= 0 ? 0 : page - 1)),
(itemsPerPage < 0 ? elasticsearchUtils.getIndexMaxResultWindow() : itemsPerPage),
sortBuilders(kind, orderBy));
SearchHit[] esResult = null;
try {
esResult = client.search(request, RequestOptions.DEFAULT).getHits().getHits();
} catch (Exception e) {
LOG.error("While searching in Elasticsearch", e);
}
return ArrayUtils.isEmpty(esResult)
? List.of()
: buildResult(Stream.of(esResult).map(SearchHit::getId).collect(Collectors.toList()), kind);
}
protected QueryBuilder getQueryBuilder(final SearchCond cond, final AnyTypeKind kind) {
QueryBuilder builder = null;
switch (cond.getType()) {
case LEAF:
case NOT_LEAF:
builder = cond.getLeaf(AnyTypeCond.class).
filter(leaf -> AnyTypeKind.ANY_OBJECT == kind).
map(this::getQueryBuilder).
orElse(null);
if (builder == null) {
builder = cond.getLeaf(RelationshipTypeCond.class).
filter(leaf -> AnyTypeKind.GROUP != kind).
map(this::getQueryBuilder).
orElse(null);
}
if (builder == null) {
builder = cond.getLeaf(RelationshipCond.class).
filter(leaf -> AnyTypeKind.GROUP != kind).
map(this::getQueryBuilder).
orElse(null);
}
if (builder == null) {
builder = cond.getLeaf(MembershipCond.class).
filter(leaf -> AnyTypeKind.GROUP != kind).
map(this::getQueryBuilder).
orElse(null);
}
if (builder == null) {
builder = cond.getLeaf(MemberCond.class).
filter(leaf -> AnyTypeKind.GROUP == kind).
map(this::getQueryBuilder).
orElse(null);
}
if (builder == null) {
builder = cond.getLeaf(AssignableCond.class).
map(this::getQueryBuilder).
orElse(null);
}
if (builder == null) {
builder = cond.getLeaf(RoleCond.class).
filter(leaf -> AnyTypeKind.USER == kind).
map(this::getQueryBuilder).
orElse(null);
}
if (builder == null) {
builder = cond.getLeaf(PrivilegeCond.class).
filter(leaf -> AnyTypeKind.USER == kind).
map(this::getQueryBuilder).
orElse(null);
}
if (builder == null) {
builder = cond.getLeaf(DynRealmCond.class).
map(this::getQueryBuilder).
orElse(null);
}
if (builder == null) {
builder = cond.getLeaf(ResourceCond.class).
map(this::getQueryBuilder).
orElse(null);
}
if (builder == null) {
Optional<AnyCond> anyCond = cond.getLeaf(AnyCond.class);
if (anyCond.isPresent()) {
builder = getQueryBuilder(anyCond.get(), kind);
} else {
builder = cond.getLeaf(AttrCond.class).
map(leaf -> getQueryBuilder(leaf, kind)).
orElse(null);
}
}
// allow for additional search conditions
if (builder == null) {
builder = getQueryBuilderForCustomConds(cond, kind);
}
if (builder == null) {
builder = MATCH_NONE_QUERY_BUILDER;
}
if (cond.getType() == SearchCond.Type.NOT_LEAF) {
builder = QueryBuilders.boolQuery().mustNot(builder);
}
break;
case AND:
builder = QueryBuilders.boolQuery().
must(getQueryBuilder(cond.getLeft(), kind)).
must(getQueryBuilder(cond.getRight(), kind));
break;
case OR:
builder = QueryBuilders.disMaxQuery().
add(getQueryBuilder(cond.getLeft(), kind)).
add(getQueryBuilder(cond.getRight(), kind));
break;
default:
}
return builder;
}
protected QueryBuilder getQueryBuilder(final AnyTypeCond cond) {
return QueryBuilders.termQuery("anyType", cond.getAnyTypeKey());
}
protected QueryBuilder getQueryBuilder(final RelationshipTypeCond cond) {
return QueryBuilders.termQuery("relationshipTypes", cond.getRelationshipTypeKey());
}
protected QueryBuilder getQueryBuilder(final RelationshipCond cond) {
String rightAnyObjectKey;
try {
rightAnyObjectKey = check(cond);
} catch (IllegalArgumentException e) {
return MATCH_NONE_QUERY_BUILDER;
}
return QueryBuilders.termQuery("relationships", rightAnyObjectKey);
}
protected QueryBuilder getQueryBuilder(final MembershipCond cond) {
List<String> groupKeys;
try {
groupKeys = check(cond);
} catch (IllegalArgumentException e) {
return MATCH_NONE_QUERY_BUILDER;
}
if (groupKeys.size() == 1) {
return QueryBuilders.termQuery("memberships", groupKeys.get(0));
}
DisMaxQueryBuilder builder = QueryBuilders.disMaxQuery();
groupKeys.forEach(key -> builder.add(QueryBuilders.termQuery("memberships", key)));
return builder;
}
protected QueryBuilder getQueryBuilder(final AssignableCond cond) {
Realm realm;
try {
realm = check(cond);
} catch (IllegalArgumentException e) {
return MATCH_NONE_QUERY_BUILDER;
}
DisMaxQueryBuilder builder = QueryBuilders.disMaxQuery();
if (cond.isFromGroup()) {
realmDAO.findDescendants(realm).forEach(
current -> builder.add(QueryBuilders.termQuery("realm", current.getFullPath())));
} else {
for (Realm current = realm; current.getParent() != null; current = current.getParent()) {
builder.add(QueryBuilders.termQuery("realm", current.getFullPath()));
}
builder.add(QueryBuilders.termQuery("realm", realmDAO.getRoot().getFullPath()));
}
return builder;
}
protected QueryBuilder getQueryBuilder(final RoleCond cond) {
return QueryBuilders.termQuery("roles", cond.getRole());
}
protected QueryBuilder getQueryBuilder(final PrivilegeCond cond) {
return QueryBuilders.termQuery("privileges", cond.getPrivilege());
}
protected QueryBuilder getQueryBuilder(final DynRealmCond cond) {
return QueryBuilders.termQuery("dynRealms", cond.getDynRealm());
}
protected QueryBuilder getQueryBuilder(final MemberCond cond) {
String memberKey;
try {
memberKey = check(cond);
} catch (IllegalArgumentException e) {
return MATCH_NONE_QUERY_BUILDER;
}
return QueryBuilders.termQuery("members", memberKey);
}
protected QueryBuilder getQueryBuilder(final ResourceCond cond) {
return QueryBuilders.termQuery("resources", cond.getResourceKey());
}
protected QueryBuilder fillAttrQuery(
final PlainSchema schema,
final PlainAttrValue attrValue,
final AttrCond cond) {
Object value = schema.getType() == AttrSchemaType.Date && attrValue.getDateValue() != null
? attrValue.getDateValue().getTime()
: attrValue.getValue();
QueryBuilder builder = MATCH_NONE_QUERY_BUILDER;
switch (cond.getType()) {
case ISNOTNULL:
builder = QueryBuilders.existsQuery(schema.getKey());
break;
case ISNULL:
builder = QueryBuilders.boolQuery().mustNot(QueryBuilders.existsQuery(schema.getKey()));
break;
case ILIKE:
StringBuilder output = new StringBuilder();
for (char c : cond.getExpression().toLowerCase().toCharArray()) {
if (c == '%') {
output.append(".*");
} else if (Character.isLetter(c)) {
output.append('[').
append(c).
append(Character.toUpperCase(c)).
append(']');
} else {
output.append(escapeForLikeRegex(c));
}
}
builder = QueryBuilders.regexpQuery(schema.getKey(), output.toString());
break;
case LIKE:
builder = QueryBuilders.wildcardQuery(schema.getKey(), cond.getExpression().replace('%', '*'));
break;
case IEQ:
builder = QueryBuilders.matchQuery(schema.getKey(), cond.getExpression().toLowerCase());
break;
case EQ:
builder = QueryBuilders.termQuery(schema.getKey(), value);
break;
case GE:
builder = QueryBuilders.rangeQuery(schema.getKey()).gte(value);
break;
case GT:
builder = QueryBuilders.rangeQuery(schema.getKey()).gt(value);
break;
case LE:
builder = QueryBuilders.rangeQuery(schema.getKey()).lte(value);
break;
case LT:
builder = QueryBuilders.rangeQuery(schema.getKey()).lt(value);
break;
default:
}
return builder;
}
protected QueryBuilder getQueryBuilder(final AttrCond cond, final AnyTypeKind kind) {
Pair<PlainSchema, PlainAttrValue> checked;
try {
checked = check(cond, kind);
} catch (IllegalArgumentException e) {
return MATCH_NONE_QUERY_BUILDER;
}
return fillAttrQuery(checked.getLeft(), checked.getRight(), cond);
}
protected QueryBuilder getQueryBuilder(final AnyCond cond, final AnyTypeKind kind) {
Triple<PlainSchema, PlainAttrValue, AnyCond> checked;
try {
checked = check(cond, kind);
} catch (IllegalArgumentException e) {
return MATCH_NONE_QUERY_BUILDER;
}
return fillAttrQuery(checked.getLeft(), checked.getMiddle(), checked.getRight());
}
protected QueryBuilder getQueryBuilderForCustomConds(final SearchCond cond, final AnyTypeKind kind) {
return MATCH_ALL_QUERY_BUILDER;
}
}