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