com.floragunn.searchguard.configuration.PrivilegesEvaluator Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of search-guard-2 Show documentation
Show all versions of search-guard-2 Show documentation
Provide access control related features for Elasticsearch 2
/*
* Copyright 2015 floragunn UG (haftungsbeschränkt)
*
* 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 com.floragunn.searchguard.configuration;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import org.elasticsearch.ElasticsearchSecurityException;
import org.elasticsearch.action.ActionRequest;
import org.elasticsearch.action.CompositeIndicesRequest;
import org.elasticsearch.action.IndicesRequest;
import org.elasticsearch.action.RealtimeRequest;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.support.IndicesOptions;
import org.elasticsearch.cluster.ClusterService;
import org.elasticsearch.cluster.ClusterState;
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
import org.elasticsearch.cluster.metadata.MetaData;
import org.elasticsearch.common.collect.Tuple;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.logging.ESLogger;
import org.elasticsearch.common.logging.Loggers;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.transport.TransportAddress;
import org.elasticsearch.transport.TransportRequest;
import com.floragunn.searchguard.action.configupdate.TransportConfigUpdateAction;
import com.floragunn.searchguard.auditlog.AuditLog;
import com.floragunn.searchguard.support.Base64Helper;
import com.floragunn.searchguard.support.ConfigConstants;
import com.floragunn.searchguard.support.WildcardMatcher;
import com.floragunn.searchguard.user.User;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Multimaps;
import com.google.common.collect.Sets;
import com.google.common.collect.Sets.SetView;
public class PrivilegesEvaluator implements ConfigChangeListener {
private final Set DLSFLS = ImmutableSet.of("_dls_", "_fls_");
protected final ESLogger log = Loggers.getLogger(this.getClass());
private final ClusterService clusterService;
private volatile Settings rolesMapping;
private volatile Settings roles;
private final ActionGroupHolder ah;
private final IndexNameExpressionResolver resolver;
private final Map typeCache = Collections.synchronizedMap(new HashMap(100));
private final Map typesCache = Collections.synchronizedMap(new HashMap(100));
private final List allowedAdminActions = new ArrayList();
private final AuditLog auditLog;
@Inject
public PrivilegesEvaluator(final ClusterService clusterService, final TransportConfigUpdateAction tcua, final ActionGroupHolder ah,
final IndexNameExpressionResolver resolver, AuditLog auditLog) {
super();
tcua.addConfigChangeListener("rolesmapping", this);
tcua.addConfigChangeListener("roles", this);
this.clusterService = clusterService;
this.ah = ah;
this.resolver = resolver;
this.auditLog = auditLog;
allowedAdminActions.add("indices:admin/aliases/exists");
allowedAdminActions.add("indices:admin/aliases/get");
allowedAdminActions.add("indices:admin/analyze");
allowedAdminActions.add("indices:admin/get");
allowedAdminActions.add("indices:admin/exists");
allowedAdminActions.add("indices:admin/mappings/fields/get");
allowedAdminActions.add("indices:admin/mappings/get");
allowedAdminActions.add("indices:admin/types/exists");
allowedAdminActions.add("indices:admin/validate/query");
allowedAdminActions.add("indices:admin/template/put");
allowedAdminActions.add("indices:admin/template/get");
}
@Override
public void onChange(final String event, final Settings settings) {
switch (event) {
case "roles":
roles = settings;
break;
case "rolesmapping":
rolesMapping = settings;
break;
}
}
@Override
public boolean isInitialized() {
return rolesMapping != null && roles != null;
}
@Override
public void validate(final String event, final Settings settings) throws ElasticsearchSecurityException {
// TODO Auto-generated method stub
}
public boolean evaluate(final User user, final String action, final ActionRequest request) {
if(action.startsWith("cluster:admin/snapshot/restore")) {
auditLog.logMissingPrivileges(action, request);
log.warn(action + " is not allowed for a regular user");
return false;
}
final TransportAddress caller = Objects.requireNonNull((TransportAddress) request.getFromContext(ConfigConstants.SG_REMOTE_ADDRESS));
if (log.isDebugEnabled()) {
log.debug("evaluate permissions for {}", user);
log.debug("requested {} from {}", action, caller);
}
final ClusterState clusterState = clusterService.state();
final MetaData metaData = clusterState.metaData();
final Tuple, Set> requestedResolvedAliasesIndicesTypes = resolve(user, action, request, metaData);
final Set requestedResolvedAliasesIndices = requestedResolvedAliasesIndicesTypes.v1();
final Set requestedResolvedTypes = requestedResolvedAliasesIndicesTypes.v2();
if (log.isDebugEnabled()) {
log.debug("requested resolved aliases and indices: {}", requestedResolvedAliasesIndices);
log.debug("requested resolved types: {}", requestedResolvedTypes);
}
if (requestedResolvedAliasesIndices.contains("searchguard")
&& (action.startsWith("indices:data/write") || (action.startsWith("indices:admin") && !allowedAdminActions.contains(action)))) {
auditLog.logSgIndexAttempt(request, action);
log.warn(action + " for 'searchguard' index is not allowed for a regular user");
return false;
}
if (requestedResolvedAliasesIndices.contains("_all")
&& (action.startsWith("indices:data/write") || (action.startsWith("indices:admin") && !allowedAdminActions.contains(action)))) {
auditLog.logSgIndexAttempt(request, action);
log.warn(action + " for '_all' indices is not allowed for a regular user");
return false;
}
if(requestedResolvedAliasesIndices.contains("searchguard") || requestedResolvedAliasesIndices.contains("_all")) {
if(request instanceof SearchRequest) {
((SearchRequest)request).requestCache(Boolean.FALSE);
if(log.isDebugEnabled()) {
log.debug("Disable search request cache for this request");
}
}
if(request instanceof RealtimeRequest) {
((RealtimeRequest) request).realtime(Boolean.FALSE);
if(log.isDebugEnabled()) {
log.debug("Disable realtime for this request");
}
}
}
final Set sgRoles = mapSgRoles(user, caller);
if (log.isDebugEnabled()) {
log.debug("mapped roles: {}", sgRoles);
}
boolean allowAction = false;
final Set dlsQueries = new HashSet();
final Set flsFields = new HashSet();
for (final Iterator iterator = sgRoles.iterator(); iterator.hasNext();) {
final String sgRole = (String) iterator.next();
final Settings sgRoleSettings = roles.getByPrefix(sgRole);
if (sgRoleSettings.names().isEmpty()) {
if (log.isDebugEnabled()) {
log.debug("sg_role {} is empty", sgRole);
}
continue;
}
if (log.isDebugEnabled()) {
log.debug("---------- evaluate sg_role: {}", sgRole);
}
if (action.startsWith("cluster:") || action.startsWith("indices:admin/template/delete")
|| action.startsWith("indices:admin/template/get") || action.startsWith("indices:admin/template/put")
|| action.startsWith("indices:data/read/scroll")) {
final Set resolvedActions = resolveActions(sgRoleSettings.getAsArray(".cluster", new String[0]));
if (log.isDebugEnabled()) {
log.debug(" resolved cluster actions:{}", resolvedActions);
}
if (WildcardMatcher.matchAny(resolvedActions.toArray(new String[0]), action)) {
if (log.isDebugEnabled()) {
log.debug(" found a match for '{}' and {}, skip other roles", sgRole, action);
}
return true;
} else {
//check other roles #108
if (log.isDebugEnabled()) {
log.debug(" not match found a match for '{}' and {}, check next role", sgRole, action);
}
continue;
}
}
final Map permittedAliasesIndices = sgRoleSettings.getGroups(".indices");
/*
sg_role_starfleet:
indices:
sf: #<--- is an alias or cindex, can contain wildcards, will be resolved to concrete indices
# if this contain wildcards we do a wildcard based check
# if contains no wildcards we resolve this to concrete indices an do a exact check
#
ships: <-- is a type, can contain wildcards
- READ
public:
- 'indices:*'
students:
- READ
alumni:
- READ
'admin*':
- READ
'pub*':
'*':
- READ
*/
final ListMultimap resolvedRoleIndices = Multimaps.synchronizedListMultimap(ArrayListMultimap
. create());
//iterate over all beneath indices:
for (final String permittedAliasesIndex : permittedAliasesIndices.keySet()) {
final Set _requestedResolvedAliasesIndices = new HashSet(requestedResolvedAliasesIndices);
final Set _requestedResolvedTypes = new HashSet(requestedResolvedTypes);
if (WildcardMatcher.containsWildcard(permittedAliasesIndex)) {
if (log.isDebugEnabled()) {
log.debug(" Try wildcard match for {}", permittedAliasesIndex);
}
handleIndicesWithWildcard(action, permittedAliasesIndex, permittedAliasesIndices, requestedResolvedAliasesIndices,
requestedResolvedTypes, _requestedResolvedAliasesIndices, _requestedResolvedTypes);
} else {
if (log.isDebugEnabled()) {
log.debug(" Resolve and match {}", permittedAliasesIndex);
}
handleIndicesWithoutWildcard(action, permittedAliasesIndex, permittedAliasesIndices, requestedResolvedAliasesIndices,
requestedResolvedTypes, _requestedResolvedAliasesIndices, _requestedResolvedTypes);
}
if (log.isDebugEnabled()) {
log.debug("For index {} remaining requested aliases and indices: {}", permittedAliasesIndex, _requestedResolvedAliasesIndices);
log.debug("For index {} remaining requested resolved types: {}", permittedAliasesIndex, _requestedResolvedTypes);
}
if (_requestedResolvedAliasesIndices.isEmpty() && _requestedResolvedTypes.isEmpty()) {
if (log.isDebugEnabled()) {
log.debug("found a match for '{}.{}', evaluate other roles", sgRole, permittedAliasesIndex);
}
resolvedRoleIndices.put(sgRole, permittedAliasesIndex);
}
}// end loop permittedAliasesIndices
if (!resolvedRoleIndices.isEmpty()) {
for(String resolvedRole: resolvedRoleIndices.keySet()) {
for(String resolvedIndex: resolvedRoleIndices.get(resolvedRole)) {
String dls = roles.get(resolvedRole+".indices."+resolvedIndex+"._dls_");
final String[] fls = roles.getAsArray(resolvedRole+".indices."+resolvedIndex+"._fls_");
if(dls != null && dls.length() > 0) {
//TODO use UserPropertyReplacer, make it registerable for ldap user
dls = dls.replace("${user.name}", user.getName());
dlsQueries.add(dls);
if (log.isDebugEnabled()) {
log.debug("dls query {}", dls);
}
}
if(fls != null && fls.length > 0) {
flsFields.addAll(Sets.newHashSet(fls));
if (log.isDebugEnabled()) {
log.debug("fls fields {}", Sets.newHashSet(fls));
}
}
}
}
allowAction = true;
}
} // end sg role loop
if (!allowAction && log.isInfoEnabled()) {
log.info("No perm match for {} and {}", action, sgRoles);
}
if(!dlsQueries.isEmpty()) {
request.putHeader(ConfigConstants.SG_DLS_QUERY, Base64Helper.serializeObject((Serializable)dlsQueries));
}
if(!flsFields.isEmpty()) {
request.putHeader(ConfigConstants.SG_FLS_FIELDS, Base64Helper.serializeObject((Serializable)flsFields));
}
return allowAction;
}
//---- end evaluate()
public Set mapSgRoles(User user, TransportAddress caller) {
if(user == null) {
return Collections.EMPTY_SET;
}
final Set sgRoles = new TreeSet();
for (final String roleMap : rolesMapping.names()) {
final Settings roleMapSettings = rolesMapping.getByPrefix(roleMap);
if (WildcardMatcher.matchAny(roleMapSettings.getAsArray(".backendroles"), user.getRoles().toArray(new String[0]))) {
sgRoles.add(roleMap);
continue;
}
if (WildcardMatcher.matchAny(roleMapSettings.getAsArray(".users"), user.getName())) {
sgRoles.add(roleMap);
continue;
}
if (caller != null && WildcardMatcher.matchAny(roleMapSettings.getAsArray(".hosts"), caller.getAddress())) {
sgRoles.add(roleMap);
continue;
}
if (caller != null && WildcardMatcher.matchAny(roleMapSettings.getAsArray(".hosts"), caller.getHost())) {
sgRoles.add(roleMap);
continue;
}
}
return Collections.unmodifiableSet(sgRoles);
}
private void handleIndicesWithWildcard(final String action, final String permittedAliasesIndex,
final Map permittedAliasesIndices, final Set requestedResolvedAliasesIndices,
final Set requestedResolvedTypes, final Set _requestedResolvedAliasesIndices,
final Set _requestedResolvedTypes) {
List wi = null;
// TODO is this secure?
if (!(wi = WildcardMatcher.getMatchAny(permittedAliasesIndex, requestedResolvedAliasesIndices.toArray(new String[0]))).isEmpty()) {
if (log.isDebugEnabled()) {
log.debug(" Wildcard match for {}: {}", permittedAliasesIndex, wi);
}
final Set permittedTypes = new HashSet(permittedAliasesIndices.get(permittedAliasesIndex).names());
permittedTypes.removeAll(DLSFLS);
if (log.isDebugEnabled()) {
log.debug(" matches for {}, will check now types {}", permittedAliasesIndex, permittedTypes);
}
for (final String type : permittedTypes) {
List typeMatches = null;
if (!(typeMatches = WildcardMatcher.getMatchAny(type, requestedResolvedTypes.toArray(new String[0]))).isEmpty()) {
final Set resolvedActions = resolveActions(permittedAliasesIndices.get(permittedAliasesIndex).getAsArray(type));
if (log.isDebugEnabled()) {
log.debug(" resolvedActions for {}/{}: {}", permittedAliasesIndex, type, resolvedActions);
}
if (WildcardMatcher.matchAny(resolvedActions.toArray(new String[0]), action)) {
if (log.isDebugEnabled()) {
log.debug(" match requested action {} against {}/{}: {}", action, permittedAliasesIndex, type,
resolvedActions);
}
_requestedResolvedAliasesIndices.removeAll(wi);
_requestedResolvedTypes.removeAll(typeMatches);
}
} else {
if (log.isDebugEnabled()) {
log.debug(" no match for {} against {}", type, requestedResolvedTypes);
}
}
}
} else {
if (log.isDebugEnabled()) {
log.debug(" No wildcard match found for {}", permittedAliasesIndex);
}
return;
}
}
private void handleIndicesWithoutWildcard(final String action, final String permittedAliasesIndex,
final Map permittedAliasesIndices, final Set requestedResolvedAliasesIndices,
final Set requestedResolvedTypes, final Set _requestedResolvedAliasesIndices,
final Set _requestedResolvedTypes) {
if(!resolver.hasIndexOrAlias(permittedAliasesIndex, clusterService.state())) {
if(log.isDebugEnabled()) {
log.debug("permittedAliasesIndex {} {}", permittedAliasesIndex, action);
log.debug("permittedAliasesIndices {}", permittedAliasesIndices);
log.debug("requestedResolvedAliasesIndices {}", requestedResolvedAliasesIndices);
log.debug("_requestedResolvedAliasesIndices {}", _requestedResolvedAliasesIndices);
}
return;//TODO check create index
}
final Set resolvedPermittedAliasesIndex = new HashSet(Arrays.asList(resolver.concreteIndices(
clusterService.state(), IndicesOptions.fromOptions(false, true, true, false), permittedAliasesIndex)));
if (log.isDebugEnabled()) {
log.debug(" resolved permitted aliases indices for {}: {}", permittedAliasesIndex, resolvedPermittedAliasesIndex);
}
final SetView inters = Sets.intersection(requestedResolvedAliasesIndices, resolvedPermittedAliasesIndex);
final Set permittedTypes = new HashSet(permittedAliasesIndices.get(permittedAliasesIndex).names());
permittedTypes.removeAll(DLSFLS);
if (log.isDebugEnabled()) {
log.debug(" matches for {}, will check now types {}", permittedAliasesIndex, permittedTypes);
}
for (final String type : permittedTypes) {
List typeMatches = null;
if (!(typeMatches = WildcardMatcher.getMatchAny(type, requestedResolvedTypes.toArray(new String[0]))).isEmpty()) {
final Set resolvedActions = resolveActions(permittedAliasesIndices.get(permittedAliasesIndex).getAsArray(type));
if (log.isDebugEnabled()) {
log.debug(" resolvedActions for {}/{}: {}", permittedAliasesIndex, type, resolvedActions);
}
if (WildcardMatcher.matchAny(resolvedActions.toArray(new String[0]), action)) {
if (log.isDebugEnabled()) {
log.debug(" match requested action {} against {}/{}: {}", action, permittedAliasesIndex, type, resolvedActions);
}
_requestedResolvedAliasesIndices.removeAll(inters);
_requestedResolvedTypes.removeAll(typeMatches);
}
} else {
if (log.isDebugEnabled()) {
log.debug(" no match for {} against {}", type, requestedResolvedTypes);
}
}
}
}
private Tuple, Set> resolve(final User user, final String action, final TransportRequest request,
final MetaData metaData) {
if (!(request instanceof CompositeIndicesRequest) && !(request instanceof IndicesRequest)) {
if (log.isDebugEnabled()) {
log.debug("{} is not an IndicesRequest", request.getClass());
}
return new Tuple, Set>(Sets.newHashSet("_all"), Sets.newHashSet("_all"));
}
final Set indices = new HashSet();
final Set types = new HashSet();
if (request instanceof CompositeIndicesRequest) {
for (final IndicesRequest indicesRequest : ((CompositeIndicesRequest) request).subRequests()) {
final Tuple, Set> t = resolve(user, action, indicesRequest, metaData);
indices.addAll(t.v1());
types.addAll(t.v2());
}
} else {
final Tuple, Set> t = resolve(user, action, (IndicesRequest) request, metaData);
indices.addAll(t.v1());
types.addAll(t.v2());
}
//for PutIndexTemplateRequest the index does not exists yet typically
if (IndexNameExpressionResolver.isAllIndices(new ArrayList(indices))) {
if(log.isDebugEnabled()) {
log.debug("The following list are '_all' indices: {}", indices);
}
indices.clear();
indices.add("_all");
}
if (types.isEmpty()) {
types.add("_all");
}
return new Tuple, Set>(Collections.unmodifiableSet(indices), Collections.unmodifiableSet(types));
}
private Tuple, Set> resolve(final User user, final String action, final IndicesRequest request,
final MetaData metaData) {
if (log.isDebugEnabled()) {
log.debug("Resolve {} from {}", request.indices(), request.getClass());
}
final Class extends IndicesRequest> requestClass = request.getClass();
final Set requestTypes = new HashSet();
Method typeMethod = null;
if(typeCache.containsKey(requestClass)) {
typeMethod = typeCache.get(requestClass);
} else {
try {
typeMethod = requestClass.getMethod("type");
typeCache.put(requestClass, typeMethod);
} catch (NoSuchMethodException e) {
typeCache.put(requestClass, null);
} catch (SecurityException e) {
log.error("Cannot evaluate type() for {} due to {}", requestClass, e);
}
}
Method typesMethod = null;
if(typesCache.containsKey(requestClass)) {
typesMethod = typesCache.get(requestClass);
} else {
try {
typesMethod = requestClass.getMethod("types");
typesCache.put(requestClass, typesMethod);
} catch (NoSuchMethodException e) {
typesCache.put(requestClass, null);
} catch (SecurityException e) {
log.error("Cannot evaluate types() for {} due to {}", requestClass, e);
}
}
if(typeMethod != null) {
try {
String type = (String) typeMethod.invoke(request);
if(type != null) {
requestTypes.add(type);
}
} catch (Exception e) {
log.error("Unable to invoke type() for {} due to {}", e, requestClass, e);
}
}
if(typesMethod != null) {
try {
final String[] types = (String[]) typesMethod.invoke(request);
if(types != null) {
requestTypes.addAll(Arrays.asList(types));
}
} catch (Exception e) {
log.error("Unable to invoke types() for {} due to {}", e, requestClass, e);
}
}
if (log.isDebugEnabled()) {
log.debug("indicesOptions {}", request.indicesOptions());
log.debug("raw indices {}", Arrays.toString(request.indices()));
}
final Set indices = new HashSet();
try {
indices.addAll(Arrays.asList(resolver.concreteIndices(clusterService.state(), request)));
if(log.isDebugEnabled()) {
log.debug("Resolved {} to {}", indices);
}
} catch (final Exception e) {
log.warn("Cannot resolve {} so we use the raw values", Arrays.toString(request.indices()));
indices.addAll(Arrays.asList(request.indices()));
}
return new Tuple, Set>(indices, requestTypes);
}
private Set resolveActions(final String[] actions) {
final Set resolvedActions = new HashSet();
for (int i = 0; i < actions.length; i++) {
final String string = actions[i];
final Set groups = ah.getGroupMembers(string);
if (groups.isEmpty()) {
resolvedActions.add(string);
} else {
resolvedActions.addAll(groups);
}
}
return resolvedActions;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy