diff --git a/securityconfig/allowlist.yml b/securityconfig/allowlist.yml new file mode 100644 index 0000000000..e669557d7e --- /dev/null +++ b/securityconfig/allowlist.yml @@ -0,0 +1,69 @@ +--- +_meta: + type: "allowlist" + config_version: 2 + +# Description: +# enabled - feature flag. +# if enabled is false, the allowlisting feature is removed. +# This is like removing the check that checks if an API is allowlisted. +# This is equivalent to continuing with the usual access control checks, and removing all the code that implements allowlisting. +# if enabled is true, then all users except SuperAdmin can access only the APIs in requests +# SuperAdmin can access all APIs. +# SuperAdmin is defined by the SuperAdmin certificate, which is configured in the opensearch.yml setting: plugins.security.authcz.admin_dn: +# Refer to the example setting in opensearch.yml.example, and the opendistro documentation to know more about configuring SuperAdmin. +# +# requests - map of allowlisted endpoints, and the allowlisted HTTP requests for those endpoints + +# Examples showing how to configure this yml file (make sure the _meta data from above is also there): +# Example 1: +# To enable allowlisting and allowlist GET /_cluster/settings +# +#config: +# enabled: true +# requests: +# /_cluster/settings: +# - GET +# +# Example 2: +# If you want to allowlist multiple request methods for /_cluster/settings (GET,PUT): +# +#config: +# enabled: true +# requests: +# /_cluster/settings: +# - GET +# - PUT +# +# Example 3: +# If you want to allowlist other APIs as well, for example GET /_cat/nodes, and GET /_cat/shards: +# +#config: +# enabled: true +# requests: +# /_cluster/settings: +# - GET +# - PUT +# /_cat/nodes: +# - GET +# /_cat/shards: +# - GET +# +# Example 4: +# If you want to disable the allowlisting feature, set enabled to false. +# enabled: false +# requests: +# /_cluster/settings: +# - GET +# +#At this point, all APIs become allowlisted because the feature to allowlist is off, so requests is irrelevant. + + +#this name must be config +config: + enabled: false + requests: + /_cluster/settings: + - GET + /_cat/nodes: + - GET diff --git a/src/main/java/org/opensearch/security/configuration/ConfigurationLoaderSecurity7.java b/src/main/java/org/opensearch/security/configuration/ConfigurationLoaderSecurity7.java index 31f5102483..3b1c1e14ba 100644 --- a/src/main/java/org/opensearch/security/configuration/ConfigurationLoaderSecurity7.java +++ b/src/main/java/org/opensearch/security/configuration/ConfigurationLoaderSecurity7.java @@ -148,8 +148,8 @@ public void noData(String id) { // Since NODESDN is newly introduced data-type applying for existing clusters as well, we make it backward compatible by returning valid empty // SecurityDynamicConfiguration. - // Same idea for new setting WHITELIST - if (cType == CType.NODESDN || cType == CType.WHITELIST) { + // Same idea for new setting WHITELIST/ALLOWLIST + if (cType == CType.NODESDN || cType == CType.WHITELIST || cType == CType.ALLOWLIST) { try { SecurityDynamicConfiguration empty = ConfigHelper.createEmptySdc(cType, ConfigurationRepository.getDefaultConfigVersion()); rs.put(cType, empty); diff --git a/src/main/java/org/opensearch/security/configuration/ConfigurationRepository.java b/src/main/java/org/opensearch/security/configuration/ConfigurationRepository.java index 6e0afbaa9f..cb3a4a4349 100644 --- a/src/main/java/org/opensearch/security/configuration/ConfigurationRepository.java +++ b/src/main/java/org/opensearch/security/configuration/ConfigurationRepository.java @@ -145,6 +145,7 @@ public void run() { final boolean populateEmptyIfFileMissing = true; ConfigHelper.uploadFile(client, cd+"nodes_dn.yml", securityIndex, CType.NODESDN, DEFAULT_CONFIG_VERSION, populateEmptyIfFileMissing); ConfigHelper.uploadFile(client, cd + "whitelist.yml", securityIndex, CType.WHITELIST, DEFAULT_CONFIG_VERSION, populateEmptyIfFileMissing); + ConfigHelper.uploadFile(client, cd + "allowlist.yml", securityIndex, CType.ALLOWLIST, DEFAULT_CONFIG_VERSION, populateEmptyIfFileMissing); // audit.yml is not packaged by default final String auditConfigPath = cd + "audit.yml"; diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/AllowlistApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/AllowlistApiAction.java new file mode 100644 index 0000000000..39821bddba --- /dev/null +++ b/src/main/java/org/opensearch/security/dlic/rest/api/AllowlistApiAction.java @@ -0,0 +1,180 @@ +/* + * Copyright OpenSearch Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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.opensearch.security.dlic.rest.api; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; + +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.collect.ImmutableList; + +import org.opensearch.action.index.IndexResponse; +import org.opensearch.client.Client; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.bytes.BytesReference; +import org.opensearch.common.inject.Inject; +import org.opensearch.common.settings.Settings; +import org.opensearch.rest.RestChannel; +import org.opensearch.rest.RestController; +import org.opensearch.rest.RestRequest; +import org.opensearch.security.DefaultObjectMapper; +import org.opensearch.security.auditlog.AuditLog; +import org.opensearch.security.configuration.AdminDNs; +import org.opensearch.security.configuration.ConfigurationRepository; +import org.opensearch.security.dlic.rest.validation.AbstractConfigurationValidator; +import org.opensearch.security.dlic.rest.validation.AllowlistValidator; +import org.opensearch.security.privileges.PrivilegesEvaluator; +import org.opensearch.security.securityconf.impl.CType; +import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; +import org.opensearch.security.ssl.transport.PrincipalExtractor; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.tools.SecurityAdmin; +import org.opensearch.threadpool.ThreadPool; + +/** + * This class implements GET and PUT operations to manage dynamic AllowlistingSettings. + *

+ * These APIs are only accessible to SuperAdmin since the configuration controls what APIs are accessible by normal users. + * Eg: If allowlisting is enabled, and a specific API like "/_cat/nodes" is not allowlisted, then only the SuperAdmin can use "/_cat/nodes" + * These APIs allow the SuperAdmin to enable/disable allowlisting, and also change the list of allowlisted APIs. + *

+ * A SuperAdmin is identified by a certificate which represents a distinguished name(DN). + * SuperAdmin DN's can be set in {@link ConfigConstants#SECURITY_AUTHCZ_ADMIN_DN} + * SuperAdmin certificate for the default superuser is stored as a kirk.pem file in config folder of OpenSearch + *

+ * Example calling the PUT API as SuperAdmin using curl (if http basic auth is on): + * curl -v --cacert path_to_config/root-ca.pem --cert path_to_config/kirk.pem --key path_to_config/kirk-key.pem -XPUT https://localhost:9200/_plugins/_security/api/allowlist -H "Content-Type: application/json" -d’ + * { + * "enabled" : false, + * "requests" : {"/_cat/nodes": ["GET"], "/_plugins/_security/api/allowlist": ["GET"]} + * } + * + * Example using the PATCH API to change the requests as SuperAdmin: + * curl -v --cacert path_to_config/root-ca.pem --cert path_to_config/kirk.pem --key path_to_config/kirk-key.pem -XPATCH https://localhost:9200/_plugins/_security/api/allowlist -H "Content-Type: application/json" -d’ + * { + * "op":"replace", + * "path":"/config/requests", + * "value": {"/_cat/nodes": ["GET"], "/_plugins/_security/api/allowlist": ["GET"]} + * } + * + * To update enabled, use the "add" operation instead of the "replace" operation, since boolean variables are not recognized as valid paths when they are false. + * eg: + * curl -v --cacert path_to_config/root-ca.pem --cert path_to_config/kirk.pem --key path_to_config/kirk-key.pem -XPATCH https://localhost:9200/_plugins/_security/api/allowlist -H "Content-Type: application/json" -d’ + * { + * "op":"add", + * "path":"/config/enabled", + * "value": true + * } + * + * The backing data is stored in {@link ConfigConstants#SECURITY_CONFIG_INDEX_NAME} which is populated during bootstrap. + * For existing clusters, {@link SecurityAdmin} tool can + * be used to populate the index. + *

+ */ +public class AllowlistApiAction extends PatchableResourceApiAction { + private static final List routes = ImmutableList.of( + new Route(RestRequest.Method.GET, "/_plugins/_security/api/allowlist"), + new Route(RestRequest.Method.PUT, "/_plugins/_security/api/allowlist"), + new Route(RestRequest.Method.PATCH, "/_plugins/_security/api/allowlist") + ); + + private static final String name = "config"; + + @Inject + public AllowlistApiAction(final Settings settings, final Path configPath, final RestController controller, final Client client, + final AdminDNs adminDNs, final ConfigurationRepository cl, final ClusterService cs, + final PrincipalExtractor principalExtractor, final PrivilegesEvaluator evaluator, ThreadPool threadPool, AuditLog auditLog) { + super(settings, configPath, controller, client, adminDNs, cl, cs, principalExtractor, evaluator, threadPool, auditLog); + } + + @Override + protected void handleApiRequest(final RestChannel channel, final RestRequest request, final Client client) throws IOException { + if (!isSuperAdmin()) { + forbidden(channel, "API allowed only for super admin."); + return; + } + super.handleApiRequest(channel, request, client); + } + + @Override + protected void handleGet(final RestChannel channel, RestRequest request, Client client, final JsonNode content) + throws IOException { + + + final SecurityDynamicConfiguration configuration = load(getConfigName(), true); + filter(configuration); + successResponse(channel, configuration); + } + + @Override + protected void handleDelete(final RestChannel channel, final RestRequest request, final Client client, final JsonNode content) throws IOException { + notImplemented(channel, RestRequest.Method.DELETE); + } + + @Override + protected void handlePut(final RestChannel channel, final RestRequest request, final Client client, final JsonNode content) throws IOException { + final SecurityDynamicConfiguration existingConfiguration = load(getConfigName(), false); + + if (existingConfiguration.getSeqNo() < 0) { + forbidden(channel, "Security index need to be updated to support '" + getConfigName().toLCString() + "'. Use SecurityAdmin to populate."); + return; + } + + boolean existed = existingConfiguration.exists(name); + existingConfiguration.putCObject(name, DefaultObjectMapper.readTree(content, existingConfiguration.getImplementingClass())); + + saveAnUpdateConfigs(client, request, getConfigName(), existingConfiguration, new OnSucessActionListener(channel) { + + @Override + public void onResponse(IndexResponse response) { + if (existed) { + successResponse(channel, "'" + name + "' updated."); + } else { + createdResponse(channel, "'" + name + "' created."); + } + } + }); + } + + + @Override + public List routes() { + return routes; + } + + @Override + protected Endpoint getEndpoint() { + return Endpoint.ALLOWLIST; + } + + @Override + protected AbstractConfigurationValidator getValidator(RestRequest request, BytesReference ref, Object... param) { + return new AllowlistValidator(request, ref, this.settings, param); + } + + @Override + protected String getResourceName() { + return name; + } + + @Override + protected CType getConfigName() { + return CType.ALLOWLIST; + } + +} diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/Endpoint.java b/src/main/java/org/opensearch/security/dlic/rest/api/Endpoint.java index e44806c8ce..1b69c2b517 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/Endpoint.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/Endpoint.java @@ -31,5 +31,6 @@ public enum Endpoint { MIGRATE, VALIDATE, WHITELIST, + ALLOWLIST, NODESDN; } diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java index d61abcb048..ed72af9fbc 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java @@ -53,6 +53,7 @@ public static Collection getHandler(Settings settings, Path configP handlers.add(new AccountApiAction(settings, configPath, controller, client, adminDns, cr, cs, principalExtractor, evaluator, threadPool, auditLog)); handlers.add(new NodesDnApiAction(settings, configPath, controller, client, adminDns, cr, cs, principalExtractor, evaluator, threadPool, auditLog)); handlers.add(new WhitelistApiAction(settings, configPath, controller, client, adminDns, cr, cs, principalExtractor, evaluator, threadPool, auditLog)); + handlers.add(new AllowlistApiAction(settings, configPath, controller, client, adminDns, cr, cs, principalExtractor, evaluator, threadPool, auditLog)); handlers.add(new AuditApiAction(settings, configPath, controller, client, adminDns, cr, cs, principalExtractor, evaluator, threadPool, auditLog)); return Collections.unmodifiableCollection(handlers); } diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/WhitelistApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/WhitelistApiAction.java index b71d0f21ae..657d3b878a 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/WhitelistApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/WhitelistApiAction.java @@ -16,35 +16,27 @@ package org.opensearch.security.dlic.rest.api; -import java.io.IOException; import java.nio.file.Path; +import java.util.Collections; import java.util.List; -import com.fasterxml.jackson.databind.JsonNode; import com.google.common.collect.ImmutableList; -import org.opensearch.action.index.IndexResponse; import org.opensearch.client.Client; import org.opensearch.cluster.service.ClusterService; -import org.opensearch.common.bytes.BytesReference; import org.opensearch.common.inject.Inject; import org.opensearch.common.settings.Settings; -import org.opensearch.rest.RestChannel; import org.opensearch.rest.RestController; import org.opensearch.rest.RestRequest; -import org.opensearch.security.DefaultObjectMapper; import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.configuration.AdminDNs; import org.opensearch.security.configuration.ConfigurationRepository; -import org.opensearch.security.dlic.rest.validation.AbstractConfigurationValidator; -import org.opensearch.security.dlic.rest.validation.WhitelistValidator; import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.securityconf.impl.CType; -import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; import org.opensearch.security.ssl.transport.PrincipalExtractor; import org.opensearch.threadpool.ThreadPool; -import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; +import static org.opensearch.security.dlic.rest.support.Utils.addDeprecatedRoutesPrefix; /** * This class implements GET and PUT operations to manage dynamic WhitelistingSettings. @@ -86,15 +78,13 @@ * be used to populate the index. *

*/ -public class WhitelistApiAction extends PatchableResourceApiAction { - private static final List routes = addRoutesPrefix(ImmutableList.of( - new Route(RestRequest.Method.GET, "/whitelist"), - new Route(RestRequest.Method.PUT, "/whitelist"), - new Route(RestRequest.Method.PATCH, "/whitelist") +public class WhitelistApiAction extends AllowlistApiAction { + private static final List routes = addDeprecatedRoutesPrefix(ImmutableList.of( + new DeprecatedRoute(RestRequest.Method.GET, "/whitelist", "[/whitelist] is a deprecated endpoint. Please use [/allowlist] instead."), + new DeprecatedRoute(RestRequest.Method.PUT, "/whitelist", "[/whitelist] is a deprecated endpoint. Please use [/allowlist] instead."), + new DeprecatedRoute(RestRequest.Method.PATCH, "/whitelist", "[/whitelist] is a deprecated endpoint. Please use [/allowlist] instead.") )); - private static final String name = "config"; - @Inject public WhitelistApiAction(final Settings settings, final Path configPath, final RestController controller, final Client client, final AdminDNs adminDNs, final ConfigurationRepository cl, final ClusterService cs, @@ -102,58 +92,12 @@ public WhitelistApiAction(final Settings settings, final Path configPath, final super(settings, configPath, controller, client, adminDNs, cl, cs, principalExtractor, evaluator, threadPool, auditLog); } - @Override - protected void handleApiRequest(final RestChannel channel, final RestRequest request, final Client client) throws IOException { - if (!isSuperAdmin()) { - forbidden(channel, "API allowed only for super admin."); - return; - } - super.handleApiRequest(channel, request, client); - } - - @Override - protected void handleGet(final RestChannel channel, RestRequest request, Client client, final JsonNode content) - throws IOException { - - - final SecurityDynamicConfiguration configuration = load(getConfigName(), true); - filter(configuration); - successResponse(channel, configuration); - } - - @Override - protected void handleDelete(final RestChannel channel, final RestRequest request, final Client client, final JsonNode content) throws IOException { - notImplemented(channel, RestRequest.Method.DELETE); - } - - @Override - protected void handlePut(final RestChannel channel, final RestRequest request, final Client client, final JsonNode content) throws IOException { - final SecurityDynamicConfiguration existingConfiguration = load(getConfigName(), false); - - if (existingConfiguration.getSeqNo() < 0) { - forbidden(channel, "Security index need to be updated to support '" + getConfigName().toLCString() + "'. Use SecurityAdmin to populate."); - return; - } - - boolean existed = existingConfiguration.exists(name); - existingConfiguration.putCObject(name, DefaultObjectMapper.readTree(content, existingConfiguration.getImplementingClass())); - - saveAnUpdateConfigs(client, request, getConfigName(), existingConfiguration, new OnSucessActionListener(channel) { - - @Override - public void onResponse(IndexResponse response) { - if (existed) { - successResponse(channel, "'" + name + "' updated."); - } else { - createdResponse(channel, "'" + name + "' created."); - } - } - }); + public List routes() { + return Collections.emptyList(); } - @Override - public List routes() { + public List deprecatedRoutes() { return routes; } @@ -162,16 +106,6 @@ protected Endpoint getEndpoint() { return Endpoint.WHITELIST; } - @Override - protected AbstractConfigurationValidator getValidator(RestRequest request, BytesReference ref, Object... param) { - return new WhitelistValidator(request, ref, this.settings, param); - } - - @Override - protected String getResourceName() { - return name; - } - @Override protected CType getConfigName() { return CType.WHITELIST; diff --git a/src/main/java/org/opensearch/security/dlic/rest/support/Utils.java b/src/main/java/org/opensearch/security/dlic/rest/support/Utils.java index 28f5c204e6..3865322681 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/support/Utils.java +++ b/src/main/java/org/opensearch/security/dlic/rest/support/Utils.java @@ -45,6 +45,7 @@ import org.opensearch.common.xcontent.XContentParser; import org.opensearch.common.xcontent.XContentType; import org.opensearch.common.xcontent.json.JsonXContent; +import org.opensearch.rest.RestHandler.DeprecatedRoute; import org.opensearch.rest.RestHandler.Route; import org.opensearch.security.DefaultObjectMapper; @@ -241,4 +242,29 @@ public static List addRoutesPrefix(List routes, final String... pr .map(p -> new Route(r.getMethod(), p + r.getPath()))) .collect(ImmutableList.toImmutableList()); } + + /** + * Add prefixes(_plugins...) to rest API routes + * @param deprecatedRoutes Routes being deprecated + * @return new list of API routes prefixed with _opendistro... and _plugins... + *Total number of routes is expanded as twice as the number of routes passed in + */ + public static List addDeprecatedRoutesPrefix(List deprecatedRoutes){ + return addDeprecatedRoutesPrefix(deprecatedRoutes, "/_opendistro/_security/api", "/_plugins/_security/api"); + } + + /** + * Add customized prefix(_opendistro... and _plugins...)to API rest routes + * @param deprecatedRoutes Routes being deprecated + * @param prefixes all api prefix + * @return new list of API routes prefixed with the strings listed in prefixes + * Total number of routes will be expanded len(prefixes) as much comparing to the list passed in + */ + public static List addDeprecatedRoutesPrefix(List deprecatedRoutes, final String... prefixes){ + return deprecatedRoutes.stream() + .flatMap( + r -> Arrays.stream(prefixes) + .map(p -> new DeprecatedRoute(r.getMethod(), p + r.getPath(), r.getDeprecationMessage()))) + .collect(ImmutableList.toImmutableList()); + } } diff --git a/src/main/java/org/opensearch/security/dlic/rest/validation/AllowlistValidator.java b/src/main/java/org/opensearch/security/dlic/rest/validation/AllowlistValidator.java new file mode 100644 index 0000000000..31d0ad1130 --- /dev/null +++ b/src/main/java/org/opensearch/security/dlic/rest/validation/AllowlistValidator.java @@ -0,0 +1,30 @@ +/* + * Copyright OpenSearch Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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.opensearch.security.dlic.rest.validation; + +import org.opensearch.common.bytes.BytesReference; +import org.opensearch.common.settings.Settings; +import org.opensearch.rest.RestRequest; + +public class AllowlistValidator extends AbstractConfigurationValidator { + + public AllowlistValidator(final RestRequest request, final BytesReference ref, final Settings opensearchSettings, Object... param) { + super(request, ref, opensearchSettings, param); + this.payloadMandatory = true; + allowedKeys.put("enabled", DataType.BOOLEAN); + allowedKeys.put("requests", DataType.OBJECT); + } +} diff --git a/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java b/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java index a029a9263f..49a31fcea2 100644 --- a/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java +++ b/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java @@ -55,6 +55,8 @@ 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.securityconf.impl.AllowlistingSettings; import org.opensearch.security.securityconf.impl.WhitelistingSettings; import org.opensearch.security.ssl.transport.PrincipalExtractor; import org.opensearch.security.ssl.util.ExceptionUtils; @@ -80,6 +82,7 @@ public class SecurityRestFilter { private final CompatConfig compatConfig; private WhitelistingSettings whitelistingSettings; + private AllowlistingSettings allowlistingSettings; private static final String HEALTH_SUFFIX = "health"; private static final String WHO_AM_I_SUFFIX = "whoami"; @@ -100,6 +103,7 @@ public SecurityRestFilter(final BackendRegistry registry, final AuditLog auditLo this.configPath = configPath; this.compatConfig = compatConfig; this.whitelistingSettings = new WhitelistingSettings(); + this.allowlistingSettings = new AllowlistingSettings(); } /** @@ -112,7 +116,7 @@ public SecurityRestFilter(final BackendRegistry registry, final AuditLog auditLo * For example: if whitelisting is enabled and requests = ["/_cat/nodes"], then SuperAdmin can access all APIs, but non SuperAdmin * can only access "/_cat/nodes" * Further note: Some APIs are only accessible by SuperAdmin, regardless of whitelisting. For example: /_opendistro/_security/api/whitelist is only accessible by SuperAdmin. - * See {@link WhitelistApiAction} for the implementation of this API. + * See {@link AllowlistApiAction} for the implementation of this API. * SuperAdmin is identified by credentials, which can be passed in the curl request. */ public RestHandler wrap(RestHandler original, AdminDNs adminDNs) { @@ -123,7 +127,7 @@ public void handleRequest(RestRequest request, RestChannel channel, NodeClient c 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)) { + if (userIsSuperAdmin(user, adminDNs) || (whitelistingSettings.checkRequestIsAllowed(request, channel, client) && allowlistingSettings.checkRequestIsAllowed(request, channel, client))) { original.handleRequest(request, channel, client); } } @@ -205,4 +209,9 @@ private boolean checkAndAuthenticateRequest(RestRequest request, RestChannel cha public void onWhitelistingSettingChanged(WhitelistingSettings whitelistingSettings) { this.whitelistingSettings = whitelistingSettings; } + + @Subscribe + public void onAllowlistingSettingChanged(AllowlistingSettings allowlistingSettings) { + this.allowlistingSettings = allowlistingSettings; + } } diff --git a/src/main/java/org/opensearch/security/securityconf/DynamicConfigFactory.java b/src/main/java/org/opensearch/security/securityconf/DynamicConfigFactory.java index 93e4176eea..019cf608f5 100644 --- a/src/main/java/org/opensearch/security/securityconf/DynamicConfigFactory.java +++ b/src/main/java/org/opensearch/security/securityconf/DynamicConfigFactory.java @@ -56,6 +56,7 @@ import org.opensearch.security.configuration.ConfigurationChangeListener; import org.opensearch.security.configuration.ConfigurationRepository; import org.opensearch.security.configuration.StaticResourceException; +import org.opensearch.security.securityconf.impl.AllowlistingSettings; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.securityconf.impl.NodesDn; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; @@ -82,6 +83,7 @@ public class DynamicConfigFactory implements Initializable, ConfigurationChangeL private static SecurityDynamicConfiguration staticActionGroups = SecurityDynamicConfiguration.empty(); private static SecurityDynamicConfiguration staticTenants = SecurityDynamicConfiguration.empty(); private static final WhitelistingSettings defaultWhitelistingSettings = new WhitelistingSettings(); + private static final AllowlistingSettings defaultAllowlistingSettings = new AllowlistingSettings(); static void resetStatics() { staticRoles = SecurityDynamicConfiguration.empty(); @@ -171,6 +173,7 @@ public void onChange(Map> typeToConfig) { SecurityDynamicConfiguration tenants = cr.getConfiguration(CType.TENANTS); SecurityDynamicConfiguration nodesDn = cr.getConfiguration(CType.NODESDN); SecurityDynamicConfiguration whitelistingSetting = cr.getConfiguration(CType.WHITELIST); + SecurityDynamicConfiguration allowlistingSetting = cr.getConfiguration(CType.ALLOWLIST); if (log.isDebugEnabled()) { @@ -182,7 +185,8 @@ public void onChange(Map> typeToConfig) { " rolesmapping: " + rolesmapping.getImplementingClass() + " with " + rolesmapping.getCEntries().size() + " entries\n" + " tenants: " + tenants.getImplementingClass() + " with " + tenants.getCEntries().size() + " entries\n" + " nodesdn: " + nodesDn.getImplementingClass() + " with " + nodesDn.getCEntries().size() + " entries\n" + - " whitelist " + whitelistingSetting.getImplementingClass() + " with " + whitelistingSetting.getCEntries().size() + " entries\n"; + " whitelist " + whitelistingSetting.getImplementingClass() + " with " + whitelistingSetting.getCEntries().size() + " entries\n" + + " allowlist " + allowlistingSetting.getImplementingClass() + " with " + allowlistingSetting.getCEntries().size() + " entries\n"; log.debug(logmsg); } @@ -192,6 +196,7 @@ public void onChange(Map> typeToConfig) { final ConfigModel cm; final NodesDnModel nm = new NodesDnModelImpl(nodesDn); final WhitelistingSettings whitelist = (WhitelistingSettings) cr.getConfiguration(CType.WHITELIST).getCEntry("config"); + final AllowlistingSettings allowlist = (AllowlistingSettings) cr.getConfiguration(CType.ALLOWLIST).getCEntry("config"); final AuditConfig audit = (AuditConfig)cr.getConfiguration(CType.AUDIT).getCEntry("config"); if(config.getImplementingClass() == ConfigV7.class) { @@ -255,6 +260,7 @@ public void onChange(Map> typeToConfig) { eventBus.post(ium); eventBus.post(nm); eventBus.post(whitelist==null? defaultWhitelistingSettings: whitelist); + eventBus.post(allowlist==null? defaultAllowlistingSettings: allowlist); if (cr.isAuditHotReloadingEnabled()) { eventBus.post(audit); } diff --git a/src/main/java/org/opensearch/security/securityconf/Migration.java b/src/main/java/org/opensearch/security/securityconf/Migration.java index 153cca3839..e2b1140c55 100644 --- a/src/main/java/org/opensearch/security/securityconf/Migration.java +++ b/src/main/java/org/opensearch/security/securityconf/Migration.java @@ -39,6 +39,7 @@ import org.opensearch.common.Strings; import org.opensearch.common.collect.Tuple; import org.opensearch.security.auditlog.config.AuditConfig; +import org.opensearch.security.securityconf.impl.AllowlistingSettings; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.securityconf.impl.Meta; import org.opensearch.security.securityconf.impl.NodesDn; @@ -165,6 +166,19 @@ public static SecurityDynamicConfiguration migrateWhitelis return migrated; } + public static SecurityDynamicConfiguration migrateAllowlistingSetting(SecurityDynamicConfiguration allowlistingSetting) { + final SecurityDynamicConfiguration migrated = SecurityDynamicConfiguration.empty(); + migrated.setCType(allowlistingSetting.getCType()); + migrated.set_meta(new Meta()); + migrated.get_meta().setConfig_version(2); + migrated.get_meta().setType("whitelist"); + + for(final Entry entry: allowlistingSetting.getCEntries().entrySet()) { + migrated.putCEntry(entry.getKey(), new AllowlistingSettings(entry.getValue())); + } + return migrated; + } + public static SecurityDynamicConfiguration migrateInternalUsers(SecurityDynamicConfiguration r6is) throws MigrationException { final SecurityDynamicConfiguration i7 = SecurityDynamicConfiguration.empty(); i7.setCType(r6is.getCType()); diff --git a/src/main/java/org/opensearch/security/securityconf/impl/AllowlistingSettings.java b/src/main/java/org/opensearch/security/securityconf/impl/AllowlistingSettings.java new file mode 100644 index 0000000000..6650a79725 --- /dev/null +++ b/src/main/java/org/opensearch/security/securityconf/impl/AllowlistingSettings.java @@ -0,0 +1,128 @@ +/* + * Copyright OpenSearch Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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.opensearch.security.securityconf.impl; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.opensearch.client.node.NodeClient; +import org.opensearch.rest.BytesRestResponse; +import org.opensearch.rest.RestChannel; +import org.opensearch.rest.RestRequest; +import org.opensearch.rest.RestStatus; + +public class AllowlistingSettings { + private boolean enabled; + private Map> requests; + + /** + * Used to parse the yml files, do not remove. + */ + public AllowlistingSettings() { + enabled = false; + requests = Collections.emptyMap(); + } + + public AllowlistingSettings(AllowlistingSettings allowlistingSettings) { + this.enabled = allowlistingSettings.getEnabled(); + this.requests = allowlistingSettings.getRequests(); + } + + public boolean getEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public Map> getRequests() { + return this.requests == null ? Collections.emptyMap(): this.requests; + } + + public void setRequests(Map> requests) { + this.requests = requests; + } + + @Override + public String toString() { + return "AllowlistingSetting [enabled=" + enabled + ", requests=" + requests + ']'; + } + + + /** + * Helper function to check if a rest request is allowlisted, by checking if the path is allowlisted, + * and then if the Http method is allowlisted. + * This method also contains logic to trim the path request, and check both with and without extra '/' + * This allows users to allowlist either /_cluster/settings/ or /_cluster/settings, to avoid potential issues. + * This also ensures that requests to the cluster can have a trailing '/' + * Scenarios: + * 1. Allowlisted API does not have an extra '/'. eg: If GET /_cluster/settings is allowlisted, these requests have the following response: + * GET /_cluster/settings - OK + * GET /_cluster/settings/ - OK + * + * 2. Allowlisted API has an extra '/'. eg: If GET /_cluster/settings/ is allowlisted, these requests have the following response: + * GET /_cluster/settings - OK + * GET /_cluster/settings/ - OK + */ + private boolean requestIsAllowlisted(RestRequest request){ + + //ALSO ALLOWS REQUEST TO HAVE TRAILING '/' + //pathWithoutTrailingSlash stores the endpoint path without extra '/'. eg: /_cat/nodes + //pathWithTrailingSlash stores the endpoint path with extra '/'. eg: /_cat/nodes/ + String path = request.path(); + String pathWithoutTrailingSlash; + String pathWithTrailingSlash; + + //first obtain pathWithoutTrailingSlash, then add a '/' to it to get pathWithTrailingSlash + pathWithoutTrailingSlash = path.endsWith("/") ? path.substring(0, path.length() - 1) : path; + pathWithTrailingSlash = pathWithoutTrailingSlash + '/'; + + //check if pathWithoutTrailingSlash is allowlisted + if(requests.containsKey(pathWithoutTrailingSlash) && requests.get(pathWithoutTrailingSlash).contains(HttpRequestMethods.valueOf(request.method().toString()))) + return true; + + //check if pathWithTrailingSlash is allowlisted + if(requests.containsKey(pathWithTrailingSlash) && requests.get(pathWithTrailingSlash).contains(HttpRequestMethods.valueOf(request.method().toString()))) + return true; + return false; + } + + /** + * Checks that a given request is allowlisted, for non SuperAdmin. + * For SuperAdmin this function is bypassed. + * In a future version, should add a regex check to improve the functionality. + * Currently, each individual PUT/PATCH request needs to be allowlisted separately for the specific resource to be changed/added. + * This should be improved so that, for example if PUT /_opendistro/_security/api/rolesmapping is allowlisted, + * then all PUT /_opendistro/_security/api/rolesmapping/{resource_name} work. + * Currently, each resource_name has to be allowlisted separately + */ + public boolean checkRequestIsAllowed(RestRequest request, RestChannel channel, + NodeClient client) throws IOException { + // if allowlisting is enabled but the request is not allowlisted, then return false, otherwise true. + if (this.enabled && !requestIsAllowlisted(request)){ + channel.sendResponse(new BytesRestResponse(RestStatus.FORBIDDEN, channel.newErrorBuilder().startObject() + .field("error", request.method() + " " + request.path() + " API not allowlisted") + .field("status", RestStatus.FORBIDDEN) + .endObject() + )); + return false; + } + return true; + } +} diff --git a/src/main/java/org/opensearch/security/securityconf/impl/CType.java b/src/main/java/org/opensearch/security/securityconf/impl/CType.java index d2efd5a101..1b1c9980a8 100644 --- a/src/main/java/org/opensearch/security/securityconf/impl/CType.java +++ b/src/main/java/org/opensearch/security/securityconf/impl/CType.java @@ -64,6 +64,7 @@ public enum CType { TENANTS(toMap(2, TenantV7.class)), NODESDN(toMap(1, NodesDn.class, 2, NodesDn.class)), WHITELIST(toMap(1, WhitelistingSettings.class, 2, WhitelistingSettings.class)), + ALLOWLIST(toMap(1, AllowlistingSettings.class, 2, AllowlistingSettings.class)), AUDIT(toMap(1, AuditConfig.class, 2, AuditConfig.class)); private Map> implementations; diff --git a/src/main/java/org/opensearch/security/securityconf/impl/WhitelistingSettings.java b/src/main/java/org/opensearch/security/securityconf/impl/WhitelistingSettings.java index 10fac0e6cc..eb1b599c4e 100644 --- a/src/main/java/org/opensearch/security/securityconf/impl/WhitelistingSettings.java +++ b/src/main/java/org/opensearch/security/securityconf/impl/WhitelistingSettings.java @@ -26,7 +26,7 @@ import org.opensearch.rest.RestRequest; import org.opensearch.rest.RestStatus; -public class WhitelistingSettings { +public class WhitelistingSettings extends AllowlistingSettings { private boolean enabled; private Map> requests; @@ -112,6 +112,7 @@ private boolean requestIsWhitelisted(RestRequest request){ * then all PUT /_opendistro/_security/api/rolesmapping/{resource_name} work. * Currently, each resource_name has to be whitelisted separately */ + @Override public boolean checkRequestIsAllowed(RestRequest request, RestChannel channel, NodeClient client) throws IOException { // if whitelisting is enabled but the request is not whitelisted, then return false, otherwise true. diff --git a/src/main/java/org/opensearch/security/tools/SecurityAdmin.java b/src/main/java/org/opensearch/security/tools/SecurityAdmin.java index 2b5f1c0043..98385c9310 100644 --- a/src/main/java/org/opensearch/security/tools/SecurityAdmin.java +++ b/src/main/java/org/opensearch/security/tools/SecurityAdmin.java @@ -123,6 +123,7 @@ import org.opensearch.security.NonValidatingObjectMapper; import org.opensearch.security.auditlog.config.AuditConfig; import org.opensearch.security.securityconf.Migration; +import org.opensearch.security.securityconf.impl.AllowlistingSettings; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.securityconf.impl.NodesDn; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; @@ -1267,6 +1268,10 @@ private static int migrate(RestHighLevelClient tc, String index, File backupDir, Migration.migrateWhitelistingSetting(SecurityDynamicConfiguration.fromNode( DefaultObjectMapper.YAML_MAPPER.readTree(ConfigHelper.createFileOrStringReader(CType.WHITELIST, 1, new File(backupDir,"whitelist.yml").getAbsolutePath(), true)), CType.WHITELIST, 1, 0, 0)); + SecurityDynamicConfiguration allowlistingSettings = + Migration.migrateAllowlistingSetting(SecurityDynamicConfiguration.fromNode( + DefaultObjectMapper.YAML_MAPPER.readTree(ConfigHelper.createFileOrStringReader(CType.ALLOWLIST, 1, new File(backupDir,"allowlist.yml").getAbsolutePath(), true)), + CType.ALLOWLIST, 1, 0, 0)); SecurityDynamicConfiguration audit = Migration.migrateAudit(SecurityDynamicConfiguration.fromNode(DefaultObjectMapper.YAML_MAPPER.readTree(new File(backupDir,"audit.yml")), CType.AUDIT, 1, 0, 0)); DefaultObjectMapper.YAML_MAPPER.writeValue(new File(v7Dir, "/action_groups.yml"), actionGroupsV7); @@ -1277,6 +1282,7 @@ private static int migrate(RestHighLevelClient tc, String index, File backupDir, DefaultObjectMapper.YAML_MAPPER.writeValue(new File(v7Dir, "/roles_mapping.yml"), rolesmappingV7); DefaultObjectMapper.YAML_MAPPER.writeValue(new File(v7Dir, "/nodes_dn.yml"), nodesDn); DefaultObjectMapper.YAML_MAPPER.writeValue(new File(v7Dir, "/whitelist.yml"), whitelistingSettings); + DefaultObjectMapper.YAML_MAPPER.writeValue(new File(v7Dir, "/allowlist.yml"), allowlistingSettings); DefaultObjectMapper.YAML_MAPPER.writeValue(new File(v7Dir, "/audit.yml"), audit); } catch (Exception e) { diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/AllowlistApiTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/AllowlistApiTest.java new file mode 100644 index 0000000000..d84908323b --- /dev/null +++ b/src/test/java/org/opensearch/security/dlic/rest/api/AllowlistApiTest.java @@ -0,0 +1,243 @@ +/* + * Copyright OpenSearch Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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.opensearch.security.dlic.rest.api; + + +import java.util.Map; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.collect.ImmutableMap; +import org.apache.http.Header; +import org.apache.http.HttpStatus; +import org.junit.Assert; +import org.junit.Test; + +import org.opensearch.common.settings.Settings; +import org.opensearch.security.DefaultObjectMapper; +import org.opensearch.security.auditlog.impl.AuditCategory; +import org.opensearch.security.auditlog.impl.AuditMessage; +import org.opensearch.security.auditlog.integration.TestAuditlogImpl; +import org.opensearch.security.dlic.rest.validation.AbstractConfigurationValidator; +import org.opensearch.security.filter.SecurityRestFilter; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.test.helper.file.FileHelper; +import org.opensearch.security.test.helper.rest.RestHelper; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * Testing class to verify that {@link AllowlistApiAction} works correctly. + * Check {@link SecurityRestFilter} for extra tests for allowlisting functionality. + */ +public class AllowlistApiTest extends AbstractRestApiUnitTest { + private RestHelper.HttpResponse response; + + /** + * admin_all_access is a user who has all permissions - essentially an admin user, not the same as superadmin. + * superadmin is identified by a certificate that should be passed as a part of the request header. + */ + private final Header adminCredsHeader = encodeBasicHeader("admin_all_access", "admin_all_access"); + private final Header nonAdminCredsHeader = encodeBasicHeader("sarek", "sarek"); + + private final String ENDPOINT = "/_plugins/_security/api/allowlist"; + + /** + * Helper function to test the GET and PUT endpoints. + * + * @throws Exception + */ + private void checkGetAndPutAllowlistPermissions(final int expectedStatus, final boolean sendAdminCertificate, final Header... headers) throws Exception { + + final boolean prevSendAdminCertificate = rh.sendAdminCertificate; + rh.sendAdminCertificate = sendAdminCertificate; + + //CHECK GET REQUEST + response = rh.executeGetRequest(ENDPOINT, headers); + assertThat(response.getBody(), response.getStatusCode(), equalTo(expectedStatus)); + if (expectedStatus == HttpStatus.SC_OK) { + //Note: the response has no whitespaces, so the .json file does not have whitespaces + Assert.assertEquals(FileHelper.loadFile("restapi/whitelist_response_success.json"), FileHelper.loadFile("restapi/whitelist_response_success.json")); + } + //FORBIDDEN FOR NON SUPER ADMIN + if (expectedStatus == HttpStatus.SC_FORBIDDEN) { + assertTrue(response.getBody().contains("API allowed only for super admin.")); + } + //CHECK PUT REQUEST + response = rh.executePutRequest(ENDPOINT, "{\"enabled\": true, \"requests\": {\"/_cat/nodes\": [\"GET\"],\"/_cat/indices\": [\"GET\"] }}", headers); + assertThat(response.getBody(), response.getStatusCode(), equalTo(expectedStatus)); + + rh.sendAdminCertificate = prevSendAdminCertificate; + } + + @Test + public void testResponseDoesNotContainMetaHeader() throws Exception { + + setup(); + + rh.sendAdminCertificate = true; + RestHelper.HttpResponse response = rh.executeGetRequest(ENDPOINT); + Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); + Assert.assertFalse(response.getHeaders().contains("_meta")); + } + + @Test + public void testPutUnknownKey() throws Exception { + + setup(); + + rh.sendAdminCertificate = true; + RestHelper.HttpResponse response = rh.executePutRequest(ENDPOINT, "{ \"unknownkey\": true, \"requests\": {\"/_cat/nodes\": [\"GET\"],\"/_cat/indices\": [\"GET\"] }}"); + Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); + assertTrue(response.getBody().contains("invalid_keys")); + assertHealthy(); + } + + @Test + public void testPutInvalidJson() throws Exception { + setup(); + + rh.sendAdminCertificate = true; + RestHelper.HttpResponse response = rh.executePutRequest(ENDPOINT, "{ \"invalid\"::{{ [\"*\"], \"requests\": {\"/_cat/nodes\": [\"GET\"],\"/_cat/indices\": [\"GET\"] }}"); + Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); + assertHealthy(); + } + + /** + * Tests that the PUT API requires a payload. i.e non empty payloads give an error. + * + * @throws Exception + */ + @Test + public void testPayloadMandatory() throws Exception { + setup(); + + rh.sendAdminCertificate = true; + response = rh.executePutRequest(ENDPOINT, "", new Header[0]); + Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); + JsonNode settings = DefaultObjectMapper.readTree(response.getBody()); + Assert.assertEquals(AbstractConfigurationValidator.ErrorType.PAYLOAD_MANDATORY.getMessage(), settings.get("reason").asText()); + } + + /** + * Tests 4 scenarios for accessing and using the API. + * No creds, no admin certificate - UNAUTHORIZED + * non admin creds, no admin certificate - FORBIDDEN + * admin creds, no admin certificate - FORBIDDEN + * any creds, admin certificate - OK + * + * @throws Exception + */ + @Test + public void testAllowlistApi() throws Exception { + setupWithRestRoles(null); + // No creds, no admin certificate - UNAUTHORIZED + checkGetAndPutAllowlistPermissions(HttpStatus.SC_UNAUTHORIZED, false); + + //non admin creds, no admin certificate - FORBIDDEN + checkGetAndPutAllowlistPermissions(HttpStatus.SC_FORBIDDEN, false, nonAdminCredsHeader); + + // admin creds, no admin certificate - FORBIDDEN + checkGetAndPutAllowlistPermissions(HttpStatus.SC_FORBIDDEN, false, adminCredsHeader); + + // any creds, admin certificate - OK + checkGetAndPutAllowlistPermissions(HttpStatus.SC_OK, true, nonAdminCredsHeader); + } + + @Test + public void testAllowlistAuditComplianceLogging() throws Exception { + Settings settings = Settings.builder() + .put("plugins.security.audit.type", TestAuditlogImpl.class.getName()) + .put(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_ENABLE_TRANSPORT, false) + .put(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_ENABLE_REST, false) + .put(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_RESOLVE_BULK_REQUESTS, false) + .put(ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_WRITE_LOG_DIFFS, true) + .put(ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_EXTERNAL_CONFIG_ENABLED, false) + .put(ConfigConstants.SECURITY_COMPLIANCE_HISTORY_INTERNAL_CONFIG_ENABLED, true) + .put(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_CONFIG_DISABLED_TRANSPORT_CATEGORIES, "authenticated,GRANTED_PRIVILEGES") + .put(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_CONFIG_DISABLED_REST_CATEGORIES, "authenticated,GRANTED_PRIVILEGES") + .build(); + setupWithRestRoles(settings); + TestAuditlogImpl.clear(); + + // any creds, admin certificate - OK + checkGetAndPutAllowlistPermissions(HttpStatus.SC_OK, true, nonAdminCredsHeader); + + //TESTS THAT 1 READ AND 1 WRITE HAPPENS IN testGetAndPut() + final Map expectedCategoryCounts = ImmutableMap.of( + AuditCategory.COMPLIANCE_INTERNAL_CONFIG_READ, 1L, + AuditCategory.COMPLIANCE_INTERNAL_CONFIG_WRITE, 1L); + Map actualCategoryCounts = TestAuditlogImpl.messages.stream().collect(Collectors.groupingBy(AuditMessage::getCategory, Collectors.counting())); + + assertThat(actualCategoryCounts, equalTo(expectedCategoryCounts)); + } + + @Test + public void testAllowlistInvalidHttpRequestMethod() throws Exception{ + setup(); + rh.sendAdminCertificate = true; + + response = rh.executePutRequest(ENDPOINT, "{\"enabled\": true, \"requests\": {\"/_cat/nodes\": [\"GE\"],\"/_cat/indices\": [\"PUT\"] }}", adminCredsHeader); + assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_INTERNAL_SERVER_ERROR)); + assertTrue(response.getBody().contains("\\\"GE\\\": not one of the values accepted for Enum class")); + } + + /** + * Tests that the PATCH Api works correctly. + * Note: boolean variables are not recognized as valid paths in "replace" operation when they are false. + * To get around this issue, to update boolean variables (here: 'enabled'), one must use the "add" operation instead. + * + * @throws Exception + */ + @Test + public void testPatchApi() throws Exception{ + setup(); + rh.sendAdminCertificate = true; + + //PATCH entire config entry + response = rh.executePatchRequest(ENDPOINT, "[{ \"op\": \"replace\", \"path\": \"/config\", \"value\": {\"enabled\": true, \"requests\": {\"/_cat/nodes\": [\"GET\"],\"/_cat/indices\": [\"PUT\"] }}}]", new Header[0]); + Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); + response = rh.executeGetRequest(ENDPOINT, adminCredsHeader); + assertEquals(response.getBody(),"{\"config\":{\"enabled\":true,\"requests\":{\"/_cat/nodes\":[\"GET\"],\"/_cat/indices\":[\"PUT\"]}}}"); + + //PATCH just requests + response = rh.executePatchRequest(ENDPOINT, "[{ \"op\": \"replace\", \"path\": \"/config/requests\", \"value\": {\"/_cat/nodes\": [\"GET\"]}}]", new Header[0]); + Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); + response = rh.executeGetRequest(ENDPOINT, adminCredsHeader); + assertTrue(response.getBody().contains("\"requests\":{\"/_cat/nodes\":[\"GET\"]}")); + + //PATCH just allowlisted_enabled using "replace" operation - works when enabled is already true + response = rh.executePatchRequest(ENDPOINT, "[{ \"op\": \"replace\", \"path\": \"/config/enabled\", \"value\": false}]", new Header[0]); + Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); + response = rh.executeGetRequest(ENDPOINT, adminCredsHeader); + assertTrue(response.getBody().contains("\"enabled\":false")); + + //PATCH just enabled using "add" operation when it is currently false - works correctly + response = rh.executePatchRequest(ENDPOINT, "[{ \"op\": \"add\", \"path\": \"/config/enabled\", \"value\": true}]", new Header[0]); + Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); + response = rh.executeGetRequest(ENDPOINT, adminCredsHeader); + assertTrue(response.getBody().contains("\"enabled\":true")); + + //PATCH just enabled using "add" operation when it is currently true - works correctly + response = rh.executePatchRequest(ENDPOINT, "[{ \"op\": \"add\", \"path\": \"/config/enabled\", \"value\": false}]", new Header[0]); + Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode());response = rh.executeGetRequest(ENDPOINT, adminCredsHeader); + response = rh.executeGetRequest(ENDPOINT, adminCredsHeader); + assertTrue(response.getBody().contains("\"enabled\":false")); + } +} diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/WhitelistApiTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/WhitelistApiTest.java index 8849331e3a..40562b7e35 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/WhitelistApiTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/WhitelistApiTest.java @@ -81,7 +81,7 @@ public static Iterable endpoints() { * * @throws Exception */ - private void testGetAndPut(final int expectedStatus, final boolean sendAdminCertificate, final Header... headers) throws Exception { + private void checkGetAndPutWhitelistPermissions(final int expectedStatus, final boolean sendAdminCertificate, final Header... headers) throws Exception { final boolean prevSendAdminCertificate = rh.sendAdminCertificate; rh.sendAdminCertificate = sendAdminCertificate; @@ -104,34 +104,22 @@ private void testGetAndPut(final int expectedStatus, final boolean sendAdminCert rh.sendAdminCertificate = prevSendAdminCertificate; } - /** - * Tests that the response does not have a _meta header - * - * @throws Exception - */ @Test public void testResponseDoesNotContainMetaHeader() throws Exception { setup(); - rh.keystore = "restapi/kirk-keystore.jks"; rh.sendAdminCertificate = true; RestHelper.HttpResponse response = rh.executeGetRequest(ENDPOINT + "/whitelist"); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); Assert.assertFalse(response.getBody().contains("_meta")); } - /** - * Tests that putting an unknown key fails - * - * @throws Exception - */ @Test public void testPutUnknownKey() throws Exception { setup(); - rh.keystore = "restapi/kirk-keystore.jks"; rh.sendAdminCertificate = true; RestHelper.HttpResponse response = rh.executePutRequest(ENDPOINT + "/whitelist", "{ \"unknownkey\": true, \"requests\": {\"/_cat/nodes\": [\"GET\"],\"/_cat/indices\": [\"GET\"] }}"); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); @@ -139,16 +127,10 @@ public void testPutUnknownKey() throws Exception { assertHealthy(); } - /** - * Tests that invalid json body fails - * - * @throws Exception - */ @Test public void testPutInvalidJson() throws Exception { setup(); - rh.keystore = "restapi/kirk-keystore.jks"; rh.sendAdminCertificate = true; RestHelper.HttpResponse response = rh.executePutRequest(ENDPOINT + "/whitelist", "{ \"invalid\"::{{ [\"*\"], \"requests\": {\"/_cat/nodes\": [\"GET\"],\"/_cat/indices\": [\"GET\"] }}"); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); @@ -164,7 +146,6 @@ public void testPutInvalidJson() throws Exception { public void testPayloadMandatory() throws Exception { setup(); - rh.keystore = "restapi/kirk-keystore.jks"; rh.sendAdminCertificate = true; response = rh.executePutRequest(ENDPOINT + "/whitelist", "", new Header[0]); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); @@ -184,18 +165,17 @@ public void testPayloadMandatory() throws Exception { @Test public void testWhitelistApi() throws Exception { setupWithRestRoles(null); - rh.keystore = "restapi/kirk-keystore.jks"; // No creds, no admin certificate - UNAUTHORIZED - testGetAndPut(HttpStatus.SC_UNAUTHORIZED, false); + checkGetAndPutWhitelistPermissions(HttpStatus.SC_UNAUTHORIZED, false); //non admin creds, no admin certificate - FORBIDDEN - testGetAndPut(HttpStatus.SC_FORBIDDEN, false, nonAdminCredsHeader); + checkGetAndPutWhitelistPermissions(HttpStatus.SC_FORBIDDEN, false, nonAdminCredsHeader); // admin creds, no admin certificate - FORBIDDEN - testGetAndPut(HttpStatus.SC_FORBIDDEN, false, adminCredsHeader); + checkGetAndPutWhitelistPermissions(HttpStatus.SC_FORBIDDEN, false, adminCredsHeader); // any creds, admin certificate - OK - testGetAndPut(HttpStatus.SC_OK, true, nonAdminCredsHeader); + checkGetAndPutWhitelistPermissions(HttpStatus.SC_OK, true, nonAdminCredsHeader); } @Test @@ -215,8 +195,7 @@ public void testWhitelistAuditComplianceLogging() throws Exception { TestAuditlogImpl.clear(); // any creds, admin certificate - OK - rh.keystore = "restapi/kirk-keystore.jks"; - testGetAndPut(HttpStatus.SC_OK, true, nonAdminCredsHeader); + checkGetAndPutWhitelistPermissions(HttpStatus.SC_OK, true, nonAdminCredsHeader); //TESTS THAT 1 READ AND 1 WRITE HAPPENS IN testGetAndPut() final Map expectedCategoryCounts = ImmutableMap.of( @@ -230,7 +209,6 @@ public void testWhitelistAuditComplianceLogging() throws Exception { @Test public void testWhitelistInvalidHttpRequestMethod() throws Exception{ setup(); - rh.keystore = "restapi/kirk-keystore.jks"; rh.sendAdminCertificate = true; response = rh.executePutRequest(ENDPOINT + "/whitelist", "{\"enabled\": true, \"requests\": {\"/_cat/nodes\": [\"GE\"],\"/_cat/indices\": [\"PUT\"] }}", adminCredsHeader); @@ -248,7 +226,6 @@ public void testWhitelistInvalidHttpRequestMethod() throws Exception{ @Test public void testPatchApi() throws Exception{ setup(); - rh.keystore = "restapi/kirk-keystore.jks"; rh.sendAdminCertificate = true; //PATCH entire config entry diff --git a/src/test/java/org/opensearch/security/filter/SecurityRestFilterTest.java b/src/test/java/org/opensearch/security/filter/SecurityRestFilterTest.java index 42e93a42ba..44e71d10c0 100644 --- a/src/test/java/org/opensearch/security/filter/SecurityRestFilterTest.java +++ b/src/test/java/org/opensearch/security/filter/SecurityRestFilterTest.java @@ -20,6 +20,7 @@ import org.junit.Test; import org.opensearch.security.dlic.rest.api.AbstractRestApiUnitTest; +import org.opensearch.security.securityconf.impl.AllowlistingSettings; import org.opensearch.security.securityconf.impl.WhitelistingSettings; import org.opensearch.security.test.helper.rest.RestHelper; @@ -75,6 +76,40 @@ public void checkWhitelistedApisAreAccessible() throws Exception { assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_OK)); } + /** + * Tests that allowlisted APIs can be accessed by all users. + * + * @throws Exception + */ + @Test + public void checkAllowlistedApisAreAccessible() throws Exception { + + setup(); + + //ADD SOME ALLOWLISTED APIs + rh.keystore = "restapi/kirk-keystore.jks"; + rh.sendAdminCertificate = true; + response = rh.executePutRequest("_plugins/_security/api/allowlist", "{\"enabled\": true, \"requests\": {\"/_cat/nodes\": [\"GET\"],\"/_cat/indices\": [\"GET\"] }}", adminCredsHeader); + + log.warn("the response is:" + rh.executeGetRequest("_plugins/_security/api/allowlist", adminCredsHeader)); + + //NON ADMIN TRIES ACCESSING A ALLOWLISTED API - OK + rh.sendAdminCertificate = false; + response = rh.executeGetRequest("_cat/nodes", nonAdminCredsHeader); + assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_OK)); + + //ADMIN TRIES ACCESSING A ALLOWLISTED API - OK + rh.sendAdminCertificate = false; + response = rh.executeGetRequest("_cat/nodes", adminCredsHeader); + log.warn("the second response is:{}", response); + assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_OK)); + + //SUPERADMIN TRIES ACCESSING A ALLOWLISTED API - OK + rh.sendAdminCertificate = true; + response = rh.executeGetRequest("_cat/nodes", adminCredsHeader); + assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_OK)); + } + /** * Tests that non-whitelisted APIs are only accessible by superadmin * @@ -105,6 +140,36 @@ public void checkNonWhitelistedApisAccessibleOnlyBySuperAdmin() throws Exception assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_OK)); } + /** + * Tests that non-allowlisted APIs are only accessible by superadmin + * + * @throws Exception + */ + @Test + public void checkNonAllowlistedApisAccessibleOnlyBySuperAdmin() throws Exception { + setup(); + + //ADD SOME ALLOWLISTED APIs - /_cat/nodes and /_cat/indices + rh.keystore = "restapi/kirk-keystore.jks"; + rh.sendAdminCertificate = true; + response = rh.executePutRequest("_plugins/_security/api/allowlist", "{\"enabled\": true, \"requests\": {\"/_cat/nodes\": [\"GET\"],\"/_cat/indices\": [\"GET\"] }}", nonAdminCredsHeader); + + //NON ADMIN TRIES ACCESSING A NON-ALLOWLISTED API - FORBIDDEN + rh.sendAdminCertificate = false; + response = rh.executeGetRequest("_cat/plugins", nonAdminCredsHeader); + assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN)); + + //ADMIN TRIES ACCESSING A NON-ALLOWLISTED API - FORBIDDEN + rh.sendAdminCertificate = false; + response = rh.executeGetRequest("_cat/plugins", adminCredsHeader); + assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN)); + + //SUPERADMIN TRIES ACCESSING A NON-ALLOWLISTED API - OK + rh.sendAdminCertificate = true; + response = rh.executeGetRequest("_cat/plugins", adminCredsHeader); + assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_OK)); + } + /** * Checks that all APIs are accessible by any user when {@link WhitelistingSettings#getEnabled()} is false */ @@ -140,6 +205,41 @@ public void checkAllApisWhenWhitelistingNotEnabled() throws Exception { assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_OK)); } + /** + * Checks that all APIs are accessible by any user when {@link AllowlistingSettings#getEnabled()} is false + */ + @Test + public void checkAllApisWhenAllowlistingNotEnabled() throws Exception { + setup(); + + //DISABLE ALLOWLISTED BUT ADD SOME ALLOWLISTED APIs - /_cat/nodes and /_cat/plugins + rh.keystore = "restapi/kirk-keystore.jks"; + rh.sendAdminCertificate = true; + response = rh.executePutRequest("_plugins/_security/api/allowlist", "{\"enabled\": false, \"requests\": {\"/_cat/nodes\": [\"GET\"],\"/_cat/indices\": [\"GET\"] }}", nonAdminCredsHeader); + + //NON-ADMIN TRIES ACCESSING 2 APIs: One in the list and one outside - OK for both (Because allowlisting is off) + rh.sendAdminCertificate = false; + response = rh.executeGetRequest("_cat/plugins", nonAdminCredsHeader); + assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_OK)); + response = rh.executeGetRequest("_cat/nodes", nonAdminCredsHeader); + assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_OK)); + + //ADMIN USER TRIES ACCESSING 2 APIs: One in the list and one outside - OK for both (Because allowlisting is off) + rh.sendAdminCertificate = false; + response = rh.executeGetRequest("_cat/plugins", adminCredsHeader); + assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_OK)); + response = rh.executeGetRequest("_cat/nodes", adminCredsHeader); + assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_OK)); + + //SUPERADMIN TRIES ACCESSING 2 APIS - OK (would work even if allowlisting was on) + + rh.sendAdminCertificate = true; + response = rh.executeGetRequest("_cat/plugins", adminCredsHeader); + assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_OK)); + response = rh.executeGetRequest("_cat/nodes", adminCredsHeader); + assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_OK)); + } + /** * Checks that request method specific whitelisting works properly. * Checks that if only GET /_cluster/settings is whitelisted, then: @@ -180,6 +280,46 @@ public void checkSpecificRequestMethodWhitelisting() throws Exception{ assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_OK)); } + /** + * Checks that request method specific allowlisting works properly. + * Checks that if only GET /_cluster/settings is allowlisted, then: + * non admin user can access GET /_cluster/settings, but not PUT /_cluster/settings + * admin user can access GET /_cluster/settings, but not PUT /_cluster/settings + * SuperAdmin can access GET /_cluster/settings and PUT /_cluster/settings + * + */ + @Test + public void checkSpecificRequestMethodAllowlisting() throws Exception{ + setup(); + + //WHITELIST GET /_cluster/settings + rh.keystore = "restapi/kirk-keystore.jks"; + rh.sendAdminCertificate = true; + response = rh.executePutRequest("_plugins/_security/api/allowlist", "{\"enabled\": true, \"requests\": {\"/_cluster/settings\": [\"GET\"]}}", nonAdminCredsHeader); + + //NON-ADMIN TRIES ACCESSING GET - OK, PUT - FORBIDDEN + + rh.sendAdminCertificate = false; + response = rh.executeGetRequest("_cluster/settings", nonAdminCredsHeader); + assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_OK)); + response = rh.executePutRequest("_cluster/settings","{\"persistent\": { }, \"transient\": {\"indices.recovery.max_bytes_per_sec\": \"15mb\" }}", nonAdminCredsHeader); + assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN)); + + //ADMIN USER TRIES ACCESSING GET - OK, PUT - FORBIDDEN + rh.sendAdminCertificate = false; + response = rh.executeGetRequest("_cluster/settings", adminCredsHeader); + assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_OK)); + response = rh.executePutRequest("_cluster/settings","{\"persistent\": { }, \"transient\": {\"indices.recovery.max_bytes_per_sec\": \"15mb\" }}", adminCredsHeader); + assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN)); + + //SUPERADMIN TRIES ACCESSING GET - OK, PUT - OK + rh.sendAdminCertificate = true; + response = rh.executeGetRequest("_cluster/settings", adminCredsHeader); + assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_OK)); + response = rh.executePutRequest("_cluster/settings","{\"persistent\": { }, \"transient\": {\"indices.recovery.max_bytes_per_sec\": \"15mb\" }}", adminCredsHeader); + assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_OK)); + } + /** * Tests that a whitelisted API with an extra '/' does not cause an issue @@ -218,6 +358,43 @@ public void testWhitelistedApiWithExtraSlash() throws Exception{ } + /** + * Tests that a allowlisted API with an extra '/' does not cause an issue + * i.e if only GET /_cluster/settings/ is allowlisted, then: + * GET /_cluster/settings/ - OK + * GET /_cluster/settings - OK + * PUT /_cluster/settings/ - FORBIDDEN + * PUT /_cluster/settings - FORBIDDEN + * @throws Exception + */ + @Test + public void testAllowlistedApiWithExtraSlash() throws Exception{ + setup(); + + //WHITELIST GET /_cluster/settings/ - extra / in the request + rh.keystore = "restapi/kirk-keystore.jks"; + rh.sendAdminCertificate = true; + response = rh.executePutRequest("_plugins/_security/api/allowlist", "{\"enabled\": true, \"requests\": {\"/_cluster/settings/\": [\"GET\"]}}", nonAdminCredsHeader); + + //NON ADMIN ACCESS GET /_cluster/settings/ - OK + rh.sendAdminCertificate = false; + response = rh.executeGetRequest("_cluster/settings/", nonAdminCredsHeader); + assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_OK)); + + //NON ADMIN ACCESS GET /_cluster/settings - OK + response = rh.executeGetRequest("_cluster/settings", nonAdminCredsHeader); + assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_OK)); + + //NON ADMIN ACCESS PUT /_cluster/settings/ - FORBIDDEN + response = rh.executePutRequest("_cluster/settings/","{\"persistent\": { }, \"transient\": {\"indices.recovery.max_bytes_per_sec\": \"15mb\" }}", adminCredsHeader); + assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN)); + + //NON ADMIN ACCESS PUT /_cluster/settings - FORBIDDEN + response = rh.executePutRequest("_cluster/settings","{\"persistent\": { }, \"transient\": {\"indices.recovery.max_bytes_per_sec\": \"15mb\" }}", adminCredsHeader); + assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN)); + + } + /** * Tests that a whitelisted API without an extra '/' does not cause an issue * i.e if only GET /_cluster/settings is whitelisted, then: @@ -253,4 +430,40 @@ public void testWhitelistedApiWithoutExtraSlash() throws Exception{ response = rh.executePutRequest("_cluster/settings","{\"persistent\": { }, \"transient\": {\"indices.recovery.max_bytes_per_sec\": \"15mb\" }}", adminCredsHeader); assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN)); } + + /** + * Tests that a allowlisted API without an extra '/' does not cause an issue + * i.e if only GET /_cluster/settings is allowlisted, then: + * GET /_cluster/settings/ - OK + * GET /_cluster/settings - OK + * PUT /_cluster/settings/ - FORBIDDEN + * PUT /_cluster/settings - FORBIDDEN + * @throws Exception + */ + @Test + public void testAllowlistedApiWithoutExtraSlash() throws Exception{ + setup(); + + //WHITELIST GET /_cluster/settings (no extra / in request) + rh.keystore = "restapi/kirk-keystore.jks"; + rh.sendAdminCertificate = true; + response = rh.executePutRequest("_plugins/_security/api/allowlist", "{\"enabled\": true, \"requests\": {\"/_cluster/settings\": [\"GET\"]}}", nonAdminCredsHeader); + + //NON ADMIN ACCESS GET /_cluster/settings/ - OK + rh.sendAdminCertificate = false; + response = rh.executeGetRequest("_cluster/settings/", nonAdminCredsHeader); + assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_OK)); + + //NON ADMIN ACCESS GET /_cluster/settings - OK + response = rh.executeGetRequest("_cluster/settings", nonAdminCredsHeader); + assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_OK)); + + //NON ADMIN ACCESS PUT /_cluster/settings/ - FORBIDDEN + response = rh.executePutRequest("_cluster/settings/","{\"persistent\": { }, \"transient\": {\"indices.recovery.max_bytes_per_sec\": \"15mb\" }}", adminCredsHeader); + assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN)); + + //NON ADMIN ACCESS PUT /_cluster/settings - FORBIDDEN + response = rh.executePutRequest("_cluster/settings","{\"persistent\": { }, \"transient\": {\"indices.recovery.max_bytes_per_sec\": \"15mb\" }}", adminCredsHeader); + assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN)); + } } diff --git a/src/test/java/org/opensearch/security/test/DynamicSecurityConfig.java b/src/test/java/org/opensearch/security/test/DynamicSecurityConfig.java index 91a3a79c8b..40397a65e0 100644 --- a/src/test/java/org/opensearch/security/test/DynamicSecurityConfig.java +++ b/src/test/java/org/opensearch/security/test/DynamicSecurityConfig.java @@ -50,6 +50,7 @@ public class DynamicSecurityConfig { private String securityActionGroups = "action_groups.yml"; private String securityNodesDn = "nodes_dn.yml"; private String securityWhitelist= "whitelist.yml"; + private String securityAllowlist= "allowlist.yml"; private String securityAudit = "audit.yml"; private String securityConfigAsYamlString = null; private String legacyConfigFolder = ""; @@ -103,6 +104,11 @@ public DynamicSecurityConfig setSecurityWhitelist(String whitelist){ return this; } + public DynamicSecurityConfig setSecurityAllowlist(String allowlist){ + this.securityAllowlist = allowlist; + return this; + } + public DynamicSecurityConfig setSecurityAudit(String audit) { this.securityAudit = audit; return this; @@ -166,6 +172,14 @@ public List getDynamicConfig(String folder) { .source(CType.WHITELIST.toLCString(), FileHelper.readYamlContent(whitelistYmlFile))); } + final String allowlistYmlFile = prefix + securityAllowlist; + if (null != FileHelper.getAbsoluteFilePathFromClassPath(allowlistYmlFile)) { + ret.add(new IndexRequest(securityIndexName) + .id(CType.ALLOWLIST.toLCString()) + .setRefreshPolicy(RefreshPolicy.IMMEDIATE) + .source(CType.ALLOWLIST.toLCString(), FileHelper.readYamlContent(allowlistYmlFile))); + } + final String auditYmlFile = prefix + securityAudit; if (null != FileHelper.getAbsoluteFilePathFromClassPath(auditYmlFile)) { ret.add(new IndexRequest(securityIndexName) diff --git a/src/test/resources/allowlist.yml b/src/test/resources/allowlist.yml new file mode 100644 index 0000000000..d90ab38da6 --- /dev/null +++ b/src/test/resources/allowlist.yml @@ -0,0 +1,17 @@ +--- +_meta: + type: "allowlist" + config_version: 2 + +#this name must be config +config: + enabled: false + requests: + /_cat/nodes: + - GET + /_cat/plugins: + - GET + /_cluster/health: + - GET + /_cluster/settings: + - GET diff --git a/src/test/resources/restapi/allowlist.yml b/src/test/resources/restapi/allowlist.yml new file mode 100644 index 0000000000..d90ab38da6 --- /dev/null +++ b/src/test/resources/restapi/allowlist.yml @@ -0,0 +1,17 @@ +--- +_meta: + type: "allowlist" + config_version: 2 + +#this name must be config +config: + enabled: false + requests: + /_cat/nodes: + - GET + /_cat/plugins: + - GET + /_cluster/health: + - GET + /_cluster/settings: + - GET