diff --git a/config/roles.yml b/config/roles.yml index 00ebdf6edb..62339786ef 100644 --- a/config/roles.yml +++ b/config/roles.yml @@ -329,3 +329,17 @@ security_analytics_ack_alerts: reserved: true cluster_permissions: - 'cluster:admin/opensearch/securityanalytics/alerts/*' + +# Roles for Hello World extension, roles that extensions would like to add should be ultimately be defined elsewhere +extension_hw_greet: + reserved: true + cluster_permissions: + - 'hw:greet' + +extension_hw_full: + reserved: true + cluster_permissions: + - 'hw:greet' + - 'hw:greet_with_adjective' + - 'hw:great_with_name' + - 'hw:goodbye' diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 41db464d55..d440bbadf6 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -149,6 +149,7 @@ import org.opensearch.security.http.XFFResolver; import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.privileges.PrivilegesInterceptor; +import org.opensearch.security.privileges.RestLayerPrivilegesEvaluator; import org.opensearch.security.resolver.IndexResolverReplacer; import org.opensearch.security.rest.DashboardsInfoAction; import org.opensearch.security.rest.SecurityConfigUpdateAction; @@ -206,6 +207,8 @@ public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin private volatile SecurityRestFilter securityRestHandler; private volatile SecurityInterceptor si; private volatile PrivilegesEvaluator evaluator; + + private volatile RestLayerPrivilegesEvaluator restLayerEvaluator; private volatile UserService userService; private volatile ThreadPool threadPool; private volatile ConfigurationRepository cr; @@ -842,7 +845,10 @@ public Collection createComponents(Client localClient, ClusterService cl principalExtractor = ReflectionHelper.instantiatePrincipalExtractor(principalExtractorClass); } - securityRestHandler = new SecurityRestFilter(backendRegistry, auditLog, threadPool, + restLayerEvaluator = new RestLayerPrivilegesEvaluator(clusterService, threadPool, cr, auditLog, + settings, cih, namedXContentRegistry.get()); + + securityRestHandler = new SecurityRestFilter(backendRegistry, restLayerEvaluator, auditLog, threadPool, principalExtractor, settings, configPath, compatConfig); final DynamicConfigFactory dcf = new DynamicConfigFactory(cr, settings, configPath, localClient, threadPool, cih); @@ -851,6 +857,7 @@ public Collection createComponents(Client localClient, ClusterService cl dcf.registerDCFListener(irr); dcf.registerDCFListener(xffResolver); dcf.registerDCFListener(evaluator); + dcf.registerDCFListener(restLayerEvaluator); dcf.registerDCFListener(securityRestHandler); if (!(auditLog instanceof NullAuditLog)) { // Don't register if advanced modules is disabled in which case auditlog is instance of NullAuditLog @@ -876,6 +883,7 @@ public Collection createComponents(Client localClient, ClusterService cl components.add(xffResolver); components.add(backendRegistry); components.add(evaluator); + components.add(restLayerEvaluator); components.add(si); components.add(dcf); components.add(userService); diff --git a/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java b/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java index 2fec235f3e..2aee2f4dc2 100644 --- a/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java +++ b/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java @@ -26,32 +26,30 @@ package org.opensearch.security.filter; -import java.nio.file.Path; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import javax.net.ssl.SSLPeerUnverifiedException; - import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.greenrobot.eventbus.Subscribe; - import org.opensearch.OpenSearchException; +import org.opensearch.OpenSearchSecurityException; import org.opensearch.client.node.NodeClient; import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.rest.BytesRestResponse; +import org.opensearch.rest.ProtectedRoute; import org.opensearch.rest.RestChannel; import org.opensearch.rest.RestHandler; import org.opensearch.rest.RestRequest; import org.opensearch.rest.RestRequest.Method; import org.opensearch.rest.RestStatus; +import org.opensearch.rest.extensions.RestSendToExtensionAction; import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.auditlog.AuditLog.Origin; import org.opensearch.security.auth.BackendRegistry; import org.opensearch.security.configuration.AdminDNs; import org.opensearch.security.configuration.CompatConfig; import org.opensearch.security.dlic.rest.api.AllowlistApiAction; +import org.opensearch.security.privileges.PrivilegesEvaluatorResponse; +import org.opensearch.security.privileges.RestLayerPrivilegesEvaluator; import org.opensearch.security.securityconf.impl.AllowlistingSettings; import org.opensearch.security.securityconf.impl.WhitelistingSettings; import org.opensearch.security.ssl.transport.PrincipalExtractor; @@ -63,6 +61,13 @@ import org.opensearch.security.user.User; import org.opensearch.threadpool.ThreadPool; +import javax.net.ssl.SSLPeerUnverifiedException; +import java.nio.file.Path; +import java.util.List; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + import static org.opensearch.security.OpenSearchSecurityPlugin.LEGACY_OPENDISTRO_PREFIX; import static org.opensearch.security.OpenSearchSecurityPlugin.PLUGINS_PREFIX; @@ -70,6 +75,8 @@ public class SecurityRestFilter { protected final Logger log = LogManager.getLogger(this.getClass()); private final BackendRegistry registry; + + private final RestLayerPrivilegesEvaluator evaluator; private final AuditLog auditLog; private final ThreadContext threadContext; private final PrincipalExtractor principalExtractor; @@ -87,11 +94,12 @@ public class SecurityRestFilter { private static final Pattern PATTERN_PATH_PREFIX = Pattern.compile(REGEX_PATH_PREFIX); - public SecurityRestFilter(final BackendRegistry registry, final AuditLog auditLog, - final ThreadPool threadPool, final PrincipalExtractor principalExtractor, + public SecurityRestFilter(final BackendRegistry registry, final RestLayerPrivilegesEvaluator evaluator, + final AuditLog auditLog, final ThreadPool threadPool, final PrincipalExtractor principalExtractor, final Settings settings, final Path configPath, final CompatConfig compatConfig) { super(); this.registry = registry; + this.evaluator = evaluator; this.auditLog = auditLog; this.threadContext = threadPool.getThreadContext(); this.principalExtractor = principalExtractor; @@ -117,14 +125,16 @@ public SecurityRestFilter(final BackendRegistry registry, final AuditLog auditLo */ public RestHandler wrap(RestHandler original, AdminDNs adminDNs) { return new RestHandler() { - + @Override public void handleRequest(RestRequest request, RestChannel channel, NodeClient client) throws Exception { org.apache.logging.log4j.ThreadContext.clearAll(); if (!checkAndAuthenticateRequest(request, channel, client)) { User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); if (userIsSuperAdmin(user, adminDNs) || (whitelistingSettings.checkRequestIsAllowed(request, channel, client) && allowlistingSettings.checkRequestIsAllowed(request, channel, client))) { - original.handleRequest(request, channel, client); + if (!authorizeRequest(original, request, channel, user)) { + original.handleRequest(request, channel, client); + } } } } @@ -138,11 +148,48 @@ private boolean userIsSuperAdmin(User user, AdminDNs adminDNs) { return user != null && adminDNs.isAdmin(user); } + private boolean authorizeRequest(RestHandler original, RestRequest request, RestChannel channel, User user) throws Exception { + if (original instanceof RestSendToExtensionAction) { + List extensionRoutes = original.routes(); + Optional handler = extensionRoutes.stream() + .filter(rh -> rh.getMethod().equals(request.method())) + .filter(rh -> restPathMatches(request.path(), rh.getPath())) + .findFirst(); + if (handler.isPresent() && handler.get() instanceof ProtectedRoute) { + String action = ((ProtectedRoute)handler.get()).name(); + PrivilegesEvaluatorResponse pres = evaluator.evaluate(user, action); + if (log.isDebugEnabled()) { + log.debug(pres.toString()); + } + + if (pres.isAllowed()) { + // TODO make sure this is audit logged + log.debug("Request has been granted"); + // auditLog.logGrantedPrivileges(action, request, task); + } else { + // auditLog.logMissingPrivileges(action, request, task); + String err; + if(!pres.getMissingSecurityRoles().isEmpty()) { + err = String.format("No mapping for %s on roles %s", user, pres.getMissingSecurityRoles()); + } else { + err = String.format("no permissions for %s and %s", pres.getMissingPrivileges(), user); + } + log.debug(err); + // TODO Figure out why extension hangs intermittently after single unauthorized request + channel.sendResponse(new BytesRestResponse(RestStatus.UNAUTHORIZED, err)); + return true; + } + } + } + + return false; + } + private boolean checkAndAuthenticateRequest(RestRequest request, RestChannel channel, NodeClient client) throws Exception { threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN, Origin.REST.toString()); - + if(HTTPHelper.containsBadHeader(request)) { final OpenSearchException exception = ExceptionUtils.createBadHeaderException(); log.error(exception.toString()); @@ -150,7 +197,7 @@ private boolean checkAndAuthenticateRequest(RestRequest request, RestChannel cha channel.sendResponse(new BytesRestResponse(channel, RestStatus.FORBIDDEN, exception)); return true; } - + if(SSLRequestHelper.containsBadHeader(threadContext, ConfigConstants.OPENDISTRO_SECURITY_CONFIG_PREFIX)) { final OpenSearchException exception = ExceptionUtils.createBadHeaderException(); log.error(exception.toString()); @@ -165,7 +212,7 @@ private boolean checkAndAuthenticateRequest(RestRequest request, RestChannel cha if(sslInfo.getPrincipal() != null) { threadContext.putTransient("_opendistro_security_ssl_principal", sslInfo.getPrincipal()); } - + if(sslInfo.getX509Certs() != null) { threadContext.putTransient("_opendistro_security_ssl_peer_certificates", sslInfo.getX509Certs()); } @@ -178,7 +225,7 @@ private boolean checkAndAuthenticateRequest(RestRequest request, RestChannel cha channel.sendResponse(new BytesRestResponse(channel, RestStatus.FORBIDDEN, e)); return true; } - + if(!compatConfig.restAuthEnabled()) { return false; } @@ -197,7 +244,7 @@ private boolean checkAndAuthenticateRequest(RestRequest request, RestChannel cha org.apache.logging.log4j.ThreadContext.put("user", ((User)threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER)).getName()); } } - + return false; } @@ -210,4 +257,30 @@ public void onWhitelistingSettingChanged(WhitelistingSettings whitelistingSettin public void onAllowlistingSettingChanged(AllowlistingSettings allowlistingSettings) { this.allowlistingSettings = allowlistingSettings; } + + /** + * Determines if the request's path is a match for the configured handler path. + * + * @param requestPath The path from the {@link ProtectedRoute} + * @param handlerPath The path from the {@link RestHandler.Route} + * @return true if the request path matches the route + */ + private boolean restPathMatches(String requestPath, String handlerPath) { + // Check exact match + if (handlerPath.equals(requestPath)) { + return true; + } + // Split path to evaluate named params + String[] handlerSplit = handlerPath.split("/"); + String[] requestSplit = requestPath.split("/"); + if (handlerSplit.length != requestSplit.length) { + return false; + } + for (int i = 0; i < handlerSplit.length; i++) { + if (!(handlerSplit[i].equals(requestSplit[i]) || (handlerSplit[i].startsWith("{") && handlerSplit[i].endsWith("}")))) { + return false; + } + } + return true; + } } diff --git a/src/main/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluator.java b/src/main/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluator.java new file mode 100644 index 0000000000..5ea2b83de9 --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluator.java @@ -0,0 +1,119 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.privileges; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.greenrobot.eventbus.Subscribe; +import org.opensearch.OpenSearchSecurityException; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.settings.Settings; +import org.opensearch.common.transport.TransportAddress; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.core.xcontent.NamedXContentRegistry; +import org.opensearch.security.auditlog.AuditLog; +import org.opensearch.security.configuration.ClusterInfoHolder; +import org.opensearch.security.configuration.ConfigurationRepository; +import org.opensearch.security.securityconf.ConfigModel; +import org.opensearch.security.securityconf.DynamicConfigModel; +import org.opensearch.security.securityconf.SecurityRoles; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.user.User; +import org.opensearch.threadpool.ThreadPool; + +import java.util.Set; + +public class RestLayerPrivilegesEvaluator { + protected final Logger log = LogManager.getLogger(this.getClass()); + private final ClusterService clusterService; + + private final AuditLog auditLog; + private ThreadContext threadContext; + + private final ClusterInfoHolder clusterInfoHolder; + private ConfigModel configModel; + private DynamicConfigModel dcm; + private final NamedXContentRegistry namedXContentRegistry; + + public RestLayerPrivilegesEvaluator(final ClusterService clusterService, final ThreadPool threadPool, + final ConfigurationRepository configurationRepository, + AuditLog auditLog, final Settings settings, final ClusterInfoHolder clusterInfoHolder, + NamedXContentRegistry namedXContentRegistry) { + + super(); + this.clusterService = clusterService; + this.auditLog = auditLog; + + this.threadContext = threadPool.getThreadContext(); + + this.clusterInfoHolder = clusterInfoHolder; + this.namedXContentRegistry = namedXContentRegistry; + } + + @Subscribe + public void onConfigModelChanged(ConfigModel configModel) { + this.configModel = configModel; + } + + @Subscribe + public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { + this.dcm = dcm; + } + + private SecurityRoles getSecurityRoles(Set roles) { + return configModel.getSecurityRoles().filter(roles); + } + + public boolean isInitialized() { + return configModel !=null && configModel.getSecurityRoles() != null && dcm != null; + } + + public PrivilegesEvaluatorResponse evaluate(final User user, String action0) { + + if (!isInitialized()) { + throw new OpenSearchSecurityException("OpenSearch Security is not initialized."); + } + + final PrivilegesEvaluatorResponse presponse = new PrivilegesEvaluatorResponse(); + + final TransportAddress caller = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS); + + Set mappedRoles = mapRoles(user, caller); + + presponse.resolvedSecurityRoles.addAll(mappedRoles); + final SecurityRoles securityRoles = getSecurityRoles(mappedRoles); + + final boolean isDebugEnabled = log.isDebugEnabled(); + if (isDebugEnabled) { + log.debug("Evaluate permissions for {} on {}", user, clusterService.localNode().getName()); + log.debug("Action: {}", action0); + log.debug("Mapped roles: {}", mappedRoles.toString()); + } + if(!securityRoles.impliesClusterPermissionPermission(action0)) { + presponse.missingPrivileges.add(action0); + presponse.allowed = false; + log.info("No extension-level perm match for {} [Action [{}]] [RolesChecked {}]. No permissions for {}", user, action0, + securityRoles.getRoleNames(), presponse.missingPrivileges); + return presponse; + } else { + if (isDebugEnabled) { + log.debug("Allowed because we have extension permissions for {}", action0); + } + presponse.allowed = true; + return presponse; + } + } + + public Set mapRoles(final User user, final TransportAddress caller) { + return this.configModel.mapSecurityRoles(user, caller); + } +} diff --git a/src/main/java/org/opensearch/security/securityconf/impl/v7/RoleV7.java b/src/main/java/org/opensearch/security/securityconf/impl/v7/RoleV7.java index 262f8ed21d..dce4652d63 100644 --- a/src/main/java/org/opensearch/security/securityconf/impl/v7/RoleV7.java +++ b/src/main/java/org/opensearch/security/securityconf/impl/v7/RoleV7.java @@ -51,11 +51,11 @@ public class RoleV7 implements Hideable, StaticDefinable { private List cluster_permissions = Collections.emptyList(); private List index_permissions = Collections.emptyList(); private List tenant_permissions = Collections.emptyList(); - + public RoleV7() { - + } - + public RoleV7(RoleV6 roleV6) { this.reserved = roleV6.isReserved(); this.hidden = roleV6.isHidden(); @@ -63,24 +63,24 @@ public RoleV7(RoleV6 roleV6) { this.cluster_permissions = roleV6.getCluster(); index_permissions = new ArrayList<>(); tenant_permissions = new ArrayList<>(); - + for(Entry v6i: roleV6.getIndices().entrySet()) { index_permissions.add(new Index(v6i.getKey(), v6i.getValue())); } - + //rw tenants List rwTenants = roleV6.getTenants().entrySet().stream().filter(e-> "rw".equalsIgnoreCase(e.getValue())).map(e->e.getKey()).collect(Collectors.toList()); - + if(rwTenants != null && !rwTenants.isEmpty()) { Tenant t = new Tenant(); t.setAllowed_actions(Collections.singletonList("kibana_all_write")); t.setTenant_patterns(rwTenants); tenant_permissions.add(t); } - - + + List roTenants = roleV6.getTenants().entrySet().stream().filter(e-> "ro".equalsIgnoreCase(e.getValue())).map(e->e.getKey()).collect(Collectors.toList()); - + if(roTenants != null && !roTenants.isEmpty()) { Tenant t = new Tenant(); t.setAllowed_actions(Collections.singletonList("kibana_all_read")); @@ -97,25 +97,25 @@ public static class Index { private List fls = Collections.emptyList(); private List masked_fields = Collections.emptyList(); private List allowed_actions = Collections.emptyList(); - + public Index(String pattern, RoleV6.Index v6Index) { super(); index_patterns = Collections.singletonList(pattern); dls = v6Index.get_dls_(); fls = v6Index.get_fls_(); masked_fields = v6Index.get_masked_fields_(); - Set tmpActions = new HashSet<>(); + Set tmpActions = new HashSet<>(); for(Entry> type: v6Index.getTypes().entrySet()) { tmpActions.addAll(type.getValue()); } allowed_actions = new ArrayList<>(tmpActions); } - - + + public Index() { super(); } - + public List getIndex_patterns() { return index_patterns; } @@ -152,27 +152,27 @@ public String toString() { + ", allowed_actions=" + allowed_actions + "]"; } } - - + + public static class Tenant { private List tenant_patterns = Collections.emptyList(); private List allowed_actions = Collections.emptyList(); - + /*public Index(String pattern, RoleV6.Index v6Index) { super(); index_patterns = Collections.singletonList(pattern); dls = v6Index.get_dls_(); fls = v6Index.get_fls_(); masked_fields = v6Index.get_masked_fields_(); - Set tmpActions = new HashSet<>(); + Set tmpActions = new HashSet<>(); for(Entry> type: v6Index.getTypes().entrySet()) { tmpActions.addAll(type.getValue()); } allowed_actions = new ArrayList<>(tmpActions); }*/ - - + + public Tenant() { super(); } @@ -197,10 +197,10 @@ public void setAllowed_actions(List allowed_actions) { public String toString() { return "Tenant [tenant_patterns=" + tenant_patterns + ", allowed_actions=" + allowed_actions + "]"; } - - + + } - + public boolean isHidden() { return hidden; @@ -226,7 +226,7 @@ public void setCluster_permissions(List cluster_permissions) { this.cluster_permissions = cluster_permissions; } - + public List getIndex_permissions() { return index_permissions; @@ -267,9 +267,9 @@ public String toString() { + ", cluster_permissions=" + cluster_permissions + ", index_permissions=" + index_permissions + ", tenant_permissions=" + tenant_permissions + "]"; } - - - + + + } diff --git a/src/main/java/org/opensearch/security/transport/SecurityRequestHandler.java b/src/main/java/org/opensearch/security/transport/SecurityRequestHandler.java index e9c3520665..d0b6decc33 100644 --- a/src/main/java/org/opensearch/security/transport/SecurityRequestHandler.java +++ b/src/main/java/org/opensearch/security/transport/SecurityRequestHandler.java @@ -40,6 +40,7 @@ import org.opensearch.action.support.replication.TransportReplicationAction.ConcreteShardRequest; import org.opensearch.cluster.service.ClusterService; import org.opensearch.common.transport.TransportAddress; +import org.opensearch.common.util.FeatureFlags; import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.extensions.ExtensionsManager; import org.opensearch.search.internal.ShardSearchRequest; @@ -331,7 +332,7 @@ protected void addAdditionalContextValues(final String action, final TransportRe String extensionUniqueId = getThreadContext().getHeader("extension_unique_id"); if (extensionUniqueId != null) { ExtensionsManager extManager = OpenSearchSecurityPlugin.GuiceHolder.getExtensionsManager(); - if (extManager.lookupInitializedExtensionById(extensionUniqueId).isPresent()) { + if (extManager.lookupExtensionSettingsById(extensionUniqueId).isPresent()) { getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_SSL_TRANSPORT_EXTENSION_REQUEST, Boolean.TRUE); } }