Skip to content

Commit

Permalink
Adapted RestLayerPrivilegesEvaluator
Browse files Browse the repository at this point in the history
Signed-off-by: Nils Bandener <nils.bandener@eliatra.com>
  • Loading branch information
nibix committed Jul 12, 2024
1 parent 0500d12 commit 30fcc0f
Show file tree
Hide file tree
Showing 10 changed files with 186 additions and 180 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1142,7 +1142,7 @@ public Collection<Object> createComponents(
principalExtractor = ReflectionHelper.instantiatePrincipalExtractor(principalExtractorClass);
}

restLayerEvaluator = new RestLayerPrivilegesEvaluator(clusterService, threadPool);
restLayerEvaluator = new RestLayerPrivilegesEvaluator(evaluator);

securityRestHandler = new SecurityRestFilter(
backendRegistry,
Expand All @@ -1161,7 +1161,6 @@ public Collection<Object> createComponents(
dcf.registerDCFListener(xffResolver);
dcf.registerDCFListener(evaluator);
dcf.registerDCFListener(restLayerEvaluator);
dcf.registerDCFListener(securityRestHandler);
dcf.registerDCFListener(tokenManager);
if (!(auditLog instanceof NullAuditLog)) {
// Don't register if advanced modules is disabled in which case auditlog is instance of NullAuditLog
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,14 @@
package org.opensearch.security.filter;

import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Pattern;
import javax.net.ssl.SSLPeerUnverifiedException;

import com.google.common.collect.ImmutableSet;
import org.apache.http.HttpStatus;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
Expand Down Expand Up @@ -225,17 +227,12 @@ void authorizeRequest(RestHandler original, SecurityRequestChannel request, User
if (routeSupportsRestAuthorization) {
PrivilegesEvaluatorResponse pres = new PrivilegesEvaluatorResponse();
NamedRoute route = ((NamedRoute) handler.get());
// if actionNames are present evaluate those first
Set<String> actionNames = route.actionNames();
if (actionNames != null && !actionNames.isEmpty()) {
pres = evaluator.evaluate(user, actionNames);
}

// now if pres.allowed is still false check for the NamedRoute name as a permission
if (!pres.isAllowed()) {
String action = route.name();
pres = evaluator.evaluate(user, Set.of(action));
}
// Check both route.actionNames() and route.name(). The presence of either is sufficient.
Set<String> actionNames = ImmutableSet.<String>builder()
.addAll(route.actionNames() != null ? route.actionNames() : Collections.emptySet())
.add(route.name())
.build();
pres = evaluator.evaluate(user, route.name(), actionNames);

if (log.isDebugEnabled()) {
log.debug(pres.toString());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ public PrivilegesEvaluatorResponse hasClusterPrivilege(PrivilegesEvaluationConte
return cluster.providesPrivilege(context, action, context.getMappedRoles());
}

public PrivilegesEvaluatorResponse hasAnyClusterPrivilege(PrivilegesEvaluationContext context, Set<String> actions) {
return cluster.providesAnyPrivilege(context, actions, context.getMappedRoles());
}

public PrivilegesEvaluatorResponse hasIndexPrivilege(
PrivilegesEvaluationContext context,
Set<String> actions,
Expand Down Expand Up @@ -311,6 +315,46 @@ PrivilegesEvaluatorResponse providesPrivilege(PrivilegesEvaluationContext contex

return PrivilegesEvaluatorResponse.insufficient(action, context);
}

/**
* Checks whether this instance provides privileges for the combination of any of the provided actions and the
* provided roles. Returns a PrivilegesEvaluatorResponse with allowed=true if privileges are available.
* Otherwise, allowed will be false and missingPrivileges will contain the name of the given action.
*/
PrivilegesEvaluatorResponse providesAnyPrivilege(PrivilegesEvaluationContext context, Set<String> actions, Set<String> roles) {
// 1: Check roles with wildcards
if (CollectionUtils.containsAny(roles, this.rolesWithWildcardPermissions)) {
return PrivilegesEvaluatorResponse.ok();
}

// 2: Check well-known actions - this should cover most cases
for (String action : actions) {
ImmutableSet<String> rolesWithPrivileges = this.actionToRoles.get(action);

if (rolesWithPrivileges != null && CollectionUtils.containsAny(roles, rolesWithPrivileges)) {
return PrivilegesEvaluatorResponse.ok();
}
}

// 3: Only if everything else fails: Check the matchers in case we have a non-well-known action
for (String action : actions) {
if (!this.wellKnownClusterActions.contains(action)) {
for (String role : roles) {
WildcardMatcher matcher = this.rolesToActionMatcher.get(role);

if (matcher != null && matcher.test(action)) {
return PrivilegesEvaluatorResponse.ok();
}
}
}
}

if (actions.size() == 1) {
return PrivilegesEvaluatorResponse.insufficient(actions.iterator().next(), context);
} else {
return PrivilegesEvaluatorResponse.insufficient("any of " + actions, context);
}
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public class PrivilegesEvaluationContext {
*/
private final Map<String, WildcardMatcher> renderedPatternTemplateCache = new HashMap<>();

public PrivilegesEvaluationContext(
PrivilegesEvaluationContext(
User user,
ImmutableSet<String> mappedRoles,
String action,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,18 +188,22 @@ public PrivilegesEvaluator(
pitPrivilegesEvaluator = new PitPrivilegesEvaluator();
this.namedXContentRegistry = namedXContentRegistry;

configurationRepository.subscribeOnChange(configMap -> {
try {
// TODO: It is not nice that we cannot use a concrete generic parameter for SecurityDynamicConfiguration
// TODO: At the moment, the implementations only support the V7 config objects
SecurityDynamicConfiguration<?> actionGroupsConfiguration = configurationRepository.getConfiguration(CType.ACTIONGROUPS);
SecurityDynamicConfiguration<?> rolesConfiguration = configurationRepository.getConfiguration(CType.ROLES);

this.updateConfiguration(actionGroupsConfiguration, rolesConfiguration);
} catch (Exception e) {
log.error("Error while updating ActionPrivileges object with {}", configMap, e);
}
});
if (configurationRepository != null) {
configurationRepository.subscribeOnChange(configMap -> {
try {
// TODO: It is not nice that we cannot use a concrete generic parameter for SecurityDynamicConfiguration
// TODO: At the moment, the implementations only support the V7 config objects
SecurityDynamicConfiguration<?> actionGroupsConfiguration = configurationRepository.getConfiguration(
CType.ACTIONGROUPS
);
SecurityDynamicConfiguration<?> rolesConfiguration = configurationRepository.getConfiguration(CType.ROLES);

this.updateConfiguration(actionGroupsConfiguration, rolesConfiguration);
} catch (Exception e) {
log.error("Error while updating ActionPrivileges object with {}", configMap, e);
}
});
}

if (clusterService != null) {
clusterService.addListener(new ClusterStateListener() {
Expand All @@ -226,11 +230,11 @@ void updateConfiguration(
if (rolesConfiguration != null) {
@SuppressWarnings("unchecked")
SecurityDynamicConfiguration<ActionGroupsV7> actionGroupsWithStatics = actionGroupsConfiguration != null
? (SecurityDynamicConfiguration<ActionGroupsV7>) DynamicConfigFactory.addStatics(actionGroupsConfiguration.deepClone())
? (SecurityDynamicConfiguration<ActionGroupsV7>) DynamicConfigFactory.addStatics(actionGroupsConfiguration.clone())
: (SecurityDynamicConfiguration<ActionGroupsV7>) DynamicConfigFactory.addStatics(SecurityDynamicConfiguration.empty());
FlattenedActionGroups flattenedActionGroups = new FlattenedActionGroups(actionGroupsWithStatics);
ActionPrivileges actionPrivileges = new ActionPrivileges(
DynamicConfigFactory.addStatics(rolesConfiguration.deepClone()),
DynamicConfigFactory.addStatics(rolesConfiguration.clone()),
flattenedActionGroups,
() -> clusterStateSupplier.get().metadata().getIndicesLookup()
);
Expand All @@ -249,6 +253,10 @@ public void onDynamicConfigModelChanged(DynamicConfigModel dcm) {
this.dcm = dcm;
}

ActionPrivileges getActionPrivileges() {
return this.actionPrivileges;
}

public SecurityRoles getSecurityRoles(Set<String> roles) {
return configModel.getSecurityRoles().filter(roles);
}
Expand All @@ -264,7 +272,7 @@ private boolean hasRestAdminPermissions(final Set<String> roles, String permissi
}

public boolean isInitialized() {
return configModel != null && configModel.getSecurityRoles() != null && dcm != null;
return configModel != null && dcm != null && actionPrivileges != null;
}

private void setUserInfoInThreadContext(User user) {
Expand All @@ -281,6 +289,10 @@ private void setUserInfoInThreadContext(User user) {
}
}

public PrivilegesEvaluationContext createContext(User user, String action) {
return createContext(user, action, null, null, null);
}

public PrivilegesEvaluationContext createContext(
User user,
String action0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,85 +16,38 @@
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import org.opensearch.OpenSearchSecurityException;
import org.opensearch.cluster.service.ClusterService;
import org.opensearch.common.util.concurrent.ThreadContext;
import org.opensearch.core.common.transport.TransportAddress;
import org.opensearch.security.securityconf.ConfigModel;
import org.opensearch.security.securityconf.SecurityRoles;
import org.opensearch.security.support.ConfigConstants;
import org.opensearch.security.user.User;
import org.opensearch.threadpool.ThreadPool;

import org.greenrobot.eventbus.Subscribe;

public class RestLayerPrivilegesEvaluator {
protected final Logger log = LogManager.getLogger(this.getClass());
private final ClusterService clusterService;
private ThreadContext threadContext;
private ConfigModel configModel;

public RestLayerPrivilegesEvaluator(final ClusterService clusterService, final ThreadPool threadPool) {
this.clusterService = clusterService;
this.threadContext = threadPool.getThreadContext();
}

@Subscribe
public void onConfigModelChanged(final ConfigModel configModel) {
this.configModel = configModel;
}

SecurityRoles getSecurityRoles(final Set<String> roles) {
return configModel.getSecurityRoles().filter(roles);
}
private final PrivilegesEvaluator privilegesEvaluator;

boolean isInitialized() {
return configModel != null && configModel.getSecurityRoles() != null;
public RestLayerPrivilegesEvaluator(PrivilegesEvaluator privilegesEvaluator) {
this.privilegesEvaluator = privilegesEvaluator;
}

public PrivilegesEvaluatorResponse evaluate(final User user, final Set<String> actions) {
if (!isInitialized()) {
throw new OpenSearchSecurityException("OpenSearch Security is not initialized.");
}

final TransportAddress caller = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS);

final Set<String> mappedRoles = mapRoles(user, caller);

final SecurityRoles securityRoles = getSecurityRoles(mappedRoles);
public PrivilegesEvaluatorResponse evaluate(final User user, final String routeName, final Set<String> actions) {
PrivilegesEvaluationContext context = privilegesEvaluator.createContext(user, routeName);

final boolean isDebugEnabled = log.isDebugEnabled();
if (isDebugEnabled) {
log.debug("Evaluate permissions for {} on {}", user, clusterService.localNode().getName());
log.debug("Evaluate permissions for {}", user);
log.debug("Action: {}", actions);
log.debug("Mapped roles: {}", mappedRoles.toString());
log.debug("Mapped roles: {}", context.getMappedRoles().toString());
}

for (final String action : actions) {
if (!securityRoles.impliesClusterPermissionPermission(action)) {
// TODO This will exhibit a weird behaviour when a REST action specifies two permissions, and
// if the user has no permissions for the first one, but has permissions for the second one:
// First, the information "No permission match" will be logged, but then the action will be
// allowed nevertheless.
log.info(
"No permission match for {} [Action [{}]] [RolesChecked {}]. No permissions for {}",
user,
action,
securityRoles.getRoleNames(),
action
);
} else {
if (isDebugEnabled) {
log.debug("Allowed because we have permissions for {}", actions);
}
return PrivilegesEvaluatorResponse.ok();
}
}
PrivilegesEvaluatorResponse result = privilegesEvaluator.getActionPrivileges().hasAnyClusterPrivilege(context, actions);

return PrivilegesEvaluatorResponse.insufficient(actions).resolvedSecurityRoles(mappedRoles);
}
if (!result.allowed) {
log.info(
"No permission match for {} [Action [{}]] [RolesChecked {}]. No permissions for {}",
user,
routeName,
context.getMappedRoles(),
result.getMissingPrivileges()
);
}

Set<String> mapRoles(final User user, final TransportAddress caller) {
return this.configModel.mapSecurityRoles(user, caller);
return result;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ public static <T> SecurityDynamicConfiguration<T> fromYaml(String yaml, CType ct
DefaultObjectMapper.getTypeFactory().constructParametricType(SecurityDynamicConfiguration.class, implementationClass)
);
result.ctype = ctype;
result.version = 2;
return result;
}

Expand Down Expand Up @@ -327,6 +328,18 @@ public Class<?> getImplementingClass() {
return getCType() == null ? null : getCType().getImplementationClass().get(getVersion());
}

@JsonIgnore
public SecurityDynamicConfiguration<T> clone() {
SecurityDynamicConfiguration<T> result = new SecurityDynamicConfiguration<T>();
result.version = this.version;
result.ctype = this.ctype;
result.primaryTerm = this.primaryTerm;
result.seqNo = this.seqNo;
result._meta = this._meta;
result.centries.putAll(this.centries);
return result;
}

@JsonIgnore
public SecurityDynamicConfiguration<T> deepClone() {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -571,8 +571,8 @@ static PrivilegesEvaluationContext ctx(String... roles) {
null,
null,
null,
new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)),
null
new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)),
null
);
}
}
Loading

0 comments on commit 30fcc0f

Please sign in to comment.