From c75f47bb21cfb47c73d0ea460fa79334093bd74e Mon Sep 17 00:00:00 2001 From: Andrey Pleskach Date: Thu, 9 Feb 2023 12:21:47 -0500 Subject: [PATCH] Rest admin permissions (#2411) Permissions for REST admin user Added granular permissions for all REST API actions in OpenSearch to be individually assigned. Permissions are: - 'restapi:admin/actiongroups' - allow full access to actiongroups - 'restapi:admin/allowlist' - allow full access to allowlist - 'restapi:admin/internalusers'- allow full access to internalusers - 'restapi:admin/nodesdn'- allow full access to nodesdn - 'restapi:admin/roles' - allow full access to roles - 'restapi:admin/rolesmapping' - allow full access to roles mappings - 'restapi:admin/ssl/certs/info' - allow full access to certs info - 'restapi:admin/ssl/certs/reload' - allow full access to certs reload - 'restapi:admin/tenants' - allow full access to tenants Adds tests for these permissions. Signed-off-by: Andrey Pleskach --- config/roles.yml | 15 +- .../security/OpenSearchSecurityPlugin.java | 29 +- .../dlic/rest/api/AbstractApiAction.java | 22 +- .../dlic/rest/api/AccountApiAction.java | 7 + .../dlic/rest/api/ActionGroupsApiAction.java | 27 +- .../dlic/rest/api/AllowlistApiAction.java | 7 + .../dlic/rest/api/AuditApiAction.java | 7 + .../rest/api/AuthTokenProcessorAction.java | 8 + .../security/dlic/rest/api/Endpoint.java | 3 +- .../dlic/rest/api/FlushCacheApiAction.java | 8 + .../dlic/rest/api/InternalUsersApiAction.java | 7 + .../dlic/rest/api/MigrateApiAction.java | 7 + .../dlic/rest/api/NodesDnApiAction.java | 7 + .../rest/api/PatchableResourceApiAction.java | 21 +- .../api/RestApiAdminPrivilegesEvaluator.java | 164 +++++++ .../rest/api/RestApiPrivilegesEvaluator.java | 16 +- .../dlic/rest/api/RolesApiAction.java | 20 + .../dlic/rest/api/RolesMappingApiAction.java | 27 ++ .../dlic/rest/api/SecurityConfigAction.java | 7 + .../dlic/rest/api/SecurityRestApiActions.java | 20 +- .../dlic/rest/api/SecuritySSLCertsAction.java | 346 ++++++++++++++ .../dlic/rest/api/TenantsApiAction.java | 8 + .../dlic/rest/api/ValidateApiAction.java | 7 + .../security/dlic/rest/support/Utils.java | 12 + .../privileges/PrivilegesEvaluator.java | 12 + .../security/securityconf/ConfigModelV6.java | 10 + .../security/securityconf/ConfigModelV7.java | 8 + .../security/securityconf/SecurityRoles.java | 2 + .../ssl/rest/SecuritySSLCertsInfoAction.java | 190 -------- .../rest/SecuritySSLReloadCertsAction.java | 155 ------ .../rest/api/AbstractRestApiUnitTest.java | 65 ++- .../dlic/rest/api/ActionGroupsApiTest.java | 273 +++++++---- .../dlic/rest/api/AllowlistApiTest.java | 25 +- .../dlic/rest/api/NodesDnApiTest.java | 79 +++ .../security/dlic/rest/api/RolesApiTest.java | 450 ++++++++++++++---- .../dlic/rest/api/RolesMappingApiTest.java | 385 ++++++++++----- .../dlic/rest/api/SslCertsApiTest.java | 174 +++++++ .../security/dlic/rest/api/UserApiTest.java | 199 +++++--- .../api/legacy/LegacySslCertsApiTest.java | 29 ++ .../SecurityRolesPermissionsTest.java | 274 +++++++++++ .../ssl/SecuritySSLCertsInfoActionTests.java | 110 ----- .../SecuritySSLReloadCertsActionTests.java | 44 -- src/test/resources/restapi/internal_users.yml | 66 +++ src/test/resources/restapi/roles.yml | 48 ++ src/test/resources/restapi/roles_mapping.yml | 54 +++ src/test/resources/restapi/roles_tenants.yml | 10 + 46 files changed, 2522 insertions(+), 942 deletions(-) create mode 100644 src/main/java/org/opensearch/security/dlic/rest/api/RestApiAdminPrivilegesEvaluator.java create mode 100644 src/main/java/org/opensearch/security/dlic/rest/api/SecuritySSLCertsAction.java delete mode 100644 src/main/java/org/opensearch/security/ssl/rest/SecuritySSLCertsInfoAction.java delete mode 100644 src/main/java/org/opensearch/security/ssl/rest/SecuritySSLReloadCertsAction.java create mode 100644 src/test/java/org/opensearch/security/dlic/rest/api/SslCertsApiTest.java create mode 100644 src/test/java/org/opensearch/security/dlic/rest/api/legacy/LegacySslCertsApiTest.java create mode 100644 src/test/java/org/opensearch/security/securityconf/SecurityRolesPermissionsTest.java delete mode 100644 src/test/java/org/opensearch/security/ssl/SecuritySSLCertsInfoActionTests.java diff --git a/config/roles.yml b/config/roles.yml index 29f6fcbe5d..fd485423fe 100644 --- a/config/roles.yml +++ b/config/roles.yml @@ -9,7 +9,20 @@ kibana_read_only: # The security REST API access role is used to assign specific users access to change the security settings through the REST API. security_rest_api_access: reserved: true - + +security_rest_api_full_access: + reserved: true + cluster_permissions: + - 'restapi:admin/actiongroups' + - 'restapi:admin/allowlist' + - 'restapi:admin/internalusers' + - 'restapi:admin/nodesdn' + - 'restapi:admin/roles' + - 'restapi:admin/rolesmapping' + - 'restapi:admin/ssl/certs/info' + - 'restapi:admin/ssl/certs/reload' + - 'restapi:admin/tenants' + # Allows users to view monitors, destinations and alerts alerting_read_access: reserved: true diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index add4a285bd..43fff06aca 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -156,8 +156,6 @@ import org.opensearch.security.ssl.OpenSearchSecuritySSLPlugin; import org.opensearch.security.ssl.SslExceptionHandler; import org.opensearch.security.ssl.http.netty.ValidatingDispatcher; -import org.opensearch.security.ssl.rest.SecuritySSLCertsInfoAction; -import org.opensearch.security.ssl.rest.SecuritySSLReloadCertsAction; import org.opensearch.security.ssl.transport.DefaultPrincipalExtractor; import org.opensearch.security.ssl.transport.SecuritySSLNettyTransport; import org.opensearch.security.ssl.util.SSLConfigConstants; @@ -466,18 +464,25 @@ public List getRestHandlers(Settings settings, RestController restC if(!SSLConfig.isSslOnlyMode()) { handlers.add(new SecurityInfoAction(settings, restController, Objects.requireNonNull(evaluator), Objects.requireNonNull(threadPool))); handlers.add(new SecurityHealthAction(settings, restController, Objects.requireNonNull(backendRegistry))); - handlers.add(new SecuritySSLCertsInfoAction(settings, restController, sks, Objects.requireNonNull(threadPool), Objects.requireNonNull(adminDns))); handlers.add(new DashboardsInfoAction(settings, restController, Objects.requireNonNull(evaluator), Objects.requireNonNull(threadPool))); handlers.add(new TenantInfoAction(settings, restController, Objects.requireNonNull(evaluator), Objects.requireNonNull(threadPool), - Objects.requireNonNull(cs), Objects.requireNonNull(adminDns), Objects.requireNonNull(cr))); - handlers.add(new SecurityConfigUpdateAction(settings, restController,Objects.requireNonNull(threadPool), adminDns, configPath, principalExtractor)); - handlers.add(new SecurityWhoAmIAction(settings ,restController,Objects.requireNonNull(threadPool), adminDns, configPath, principalExtractor)); - if (sslCertReloadEnabled) { - handlers.add(new SecuritySSLReloadCertsAction(settings, restController, sks, Objects.requireNonNull(threadPool), Objects.requireNonNull(adminDns))); - } - final Collection apiHandlers = SecurityRestApiActions.getHandler(settings, configPath, restController, localClient, adminDns, cr, cs, principalExtractor, evaluator, threadPool, Objects.requireNonNull(auditLog)); - handlers.addAll(apiHandlers); - log.debug("Added {} management rest handler(s)", apiHandlers.size()); + Objects.requireNonNull(cs), Objects.requireNonNull(adminDns), Objects.requireNonNull(cr))); + handlers.add(new SecurityConfigUpdateAction(settings, restController, Objects.requireNonNull(threadPool), adminDns, configPath, principalExtractor)); + handlers.add(new SecurityWhoAmIAction(settings, restController, Objects.requireNonNull(threadPool), adminDns, configPath, principalExtractor)); + handlers.addAll( + SecurityRestApiActions.getHandler( + settings, + configPath, + restController, + localClient, + adminDns, + cr, cs, principalExtractor, + evaluator, + threadPool, + Objects.requireNonNull(auditLog), sks, + sslCertReloadEnabled) + ); + log.debug("Added {} rest handler(s)", handlers.size()); } } diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/AbstractApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/AbstractApiAction.java index 8ceb7eaaae..873656f927 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/AbstractApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/AbstractApiAction.java @@ -75,9 +75,9 @@ public abstract class AbstractApiAction extends BaseRestHandler { final ThreadPool threadPool; protected String opendistroIndex; private final RestApiPrivilegesEvaluator restApiPrivilegesEvaluator; + protected final RestApiAdminPrivilegesEvaluator restApiAdminPrivilegesEvaluator; protected final AuditLog auditLog; protected final Settings settings; - private AdminDNs adminDNs; protected AbstractApiAction(final Settings settings, final Path configPath, final RestController controller, final Client client, final AdminDNs adminDNs, final ConfigurationRepository cl, @@ -88,12 +88,13 @@ protected AbstractApiAction(final Settings settings, final Path configPath, fina this.opendistroIndex = settings.get(ConfigConstants.SECURITY_CONFIG_INDEX_NAME, ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX); - this.adminDNs = adminDNs; this.cl = cl; this.cs = cs; this.threadPool = threadPool; this.restApiPrivilegesEvaluator = new RestApiPrivilegesEvaluator(settings, adminDNs, evaluator, principalExtractor, configPath, threadPool); + this.restApiAdminPrivilegesEvaluator = + new RestApiAdminPrivilegesEvaluator(threadPool.getThreadContext(), evaluator, adminDNs); this.auditLog = auditLog; } @@ -195,7 +196,12 @@ protected void handlePut(final RestChannel channel, final RestRequest request, f } boolean existed = existingConfiguration.exists(name); - existingConfiguration.putCObject(name, DefaultObjectMapper.readTree(content, existingConfiguration.getImplementingClass())); + final Object newContent = DefaultObjectMapper.readTree(content, existingConfiguration.getImplementingClass()); + if (!hasPermissionsToCreate(existingConfiguration, newContent, getResourceName())) { + forbidden(channel, "No permissions"); + return; + } + existingConfiguration.putCObject(name, newContent); saveAnUpdateConfigs(client, request, getConfigName(), existingConfiguration, new OnSucessActionListener(channel) { @@ -216,6 +222,12 @@ protected void handlePost(final RestChannel channel, final RestRequest request, notImplemented(channel, Method.POST); } + protected boolean hasPermissionsToCreate(final SecurityDynamicConfiguration dynamicConfigFactory, + final Object content, + final String resourceName) throws IOException { + return false; + } + protected void handleGet(final RestChannel channel, RestRequest request, Client client, final JsonNode content) throws IOException{ @@ -448,7 +460,6 @@ protected static XContentBuilder convertToJson(RestChannel channel, ToXContent t } protected void response(RestChannel channel, RestStatus status, String message) { - try { final XContentBuilder builder = channel.newBuilder(); builder.startObject(); @@ -558,8 +569,7 @@ public String getName() { protected abstract Endpoint getEndpoint(); protected boolean isSuperAdmin() { - User user = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); - return adminDNs.isAdmin(user); + return restApiAdminPrivilegesEvaluator.isCurrentUserRestApiAdminFor(getEndpoint()); } /** diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/AccountApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/AccountApiAction.java index 07a0424055..20de3500dd 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/AccountApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/AccountApiAction.java @@ -83,6 +83,13 @@ public AccountApiAction(Settings settings, this.threadContext = threadPool.getThreadContext(); } + @Override + protected boolean hasPermissionsToCreate(final SecurityDynamicConfiguration dynamicConfigFactory, + final Object content, + final String resourceName) { + return true; + } + @Override public List routes() { return routes; diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/ActionGroupsApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/ActionGroupsApiAction.java index 83d1a993ff..23a3a451b9 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/ActionGroupsApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/ActionGroupsApiAction.java @@ -118,12 +118,35 @@ protected void handlePut(RestChannel channel, RestRequest request, Client client // Prevent the case where action group references to itself in the allowed_actions. final SecurityDynamicConfiguration existingActionGroupsConfig = load(getConfigName(), false); - existingActionGroupsConfig.putCObject(name, DefaultObjectMapper.readTree(content, existingActionGroupsConfig.getImplementingClass())); + final Object actionGroup = DefaultObjectMapper.readTree(content, existingActionGroupsConfig.getImplementingClass()); + existingActionGroupsConfig.putCObject(name, actionGroup); if (hasActionGroupSelfReference(existingActionGroupsConfig, name)) { badRequestResponse(channel, name + " cannot be an allowed_action of itself"); return; } - + // prevent creation of groups for REST admin api + if (restApiAdminPrivilegesEvaluator.containsRestApiAdminPermissions(actionGroup)) { + forbidden(channel, "Not allowed"); + return; + } super.handlePut(channel, request, client, content); } + + @Override + protected boolean hasPermissionsToCreate(final SecurityDynamicConfiguration dynamicConfiguration, + final Object content, + final String resourceName) throws IOException { + if (restApiAdminPrivilegesEvaluator.containsRestApiAdminPermissions(content)) { + return false; + } + return true; + } + + @Override + protected boolean isReadOnly(SecurityDynamicConfiguration existingConfiguration, String name) { + if (restApiAdminPrivilegesEvaluator.containsRestApiAdminPermissions(existingConfiguration.getCEntry(name))) { + return true; + } + return super.isReadOnly(existingConfiguration, name); + } } 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 index afe08bc486..b37375a461 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/AllowlistApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/AllowlistApiAction.java @@ -98,6 +98,13 @@ public AllowlistApiAction(final Settings settings, final Path configPath, final super(settings, configPath, controller, client, adminDNs, cl, cs, principalExtractor, evaluator, threadPool, auditLog); } + @Override + protected boolean hasPermissionsToCreate(final SecurityDynamicConfiguration dynamicConfigFactory, + final Object content, + final String resourceName) { + return true; + } + @Override protected void handleApiRequest(final RestChannel channel, final RestRequest request, final Client client) throws IOException { if (!isSuperAdmin()) { diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/AuditApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/AuditApiAction.java index ce11b74509..e19f04d437 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/AuditApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/AuditApiAction.java @@ -165,6 +165,13 @@ public AuditApiAction(final Settings settings, } } + @Override + protected boolean hasPermissionsToCreate(final SecurityDynamicConfiguration dynamicConfigFactory, + final Object content, + final String resourceName) { + return true; + } + @Override public List routes() { return routes; diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/AuthTokenProcessorAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/AuthTokenProcessorAction.java index 2338fe15e9..497efcdf76 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/AuthTokenProcessorAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/AuthTokenProcessorAction.java @@ -34,6 +34,7 @@ import org.opensearch.security.dlic.rest.validation.NoOpValidator; 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; @@ -53,6 +54,13 @@ public AuthTokenProcessorAction(final Settings settings, final Path configPath, auditLog); } + @Override + protected boolean hasPermissionsToCreate(final SecurityDynamicConfiguration dynamicConfigFactory, + final Object content, + final String resourceName) { + return true; + } + @Override public List routes() { return routes; 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 ce57070825..84a447bcac 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 @@ -28,5 +28,6 @@ public enum Endpoint { VALIDATE, WHITELIST, ALLOWLIST, - NODESDN; + NODESDN, + SSL; } diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/FlushCacheApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/FlushCacheApiAction.java index 7e6d7989a9..406b81679c 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/FlushCacheApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/FlushCacheApiAction.java @@ -38,6 +38,7 @@ import org.opensearch.security.dlic.rest.validation.NoOpValidator; 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; @@ -58,6 +59,13 @@ public FlushCacheApiAction(final Settings settings, final Path configPath, final super(settings, configPath, controller, client, adminDNs, cl, cs, principalExtractor, evaluator, threadPool, auditLog); } + @Override + protected boolean hasPermissionsToCreate(final SecurityDynamicConfiguration dynamicConfigFactory, + final Object content, + final String resourceName) { + return true; + } + @Override public List routes() { return routes; diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/InternalUsersApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/InternalUsersApiAction.java index 2394488041..3f0ff75f8f 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/InternalUsersApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/InternalUsersApiAction.java @@ -78,6 +78,13 @@ public InternalUsersApiAction(final Settings settings, final Path configPath, fi auditLog); } + @Override + protected boolean hasPermissionsToCreate(final SecurityDynamicConfiguration dynamicConfigFactory, + final Object content, + final String resourceName) { + return true; + } + @Override public List routes() { return routes; diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/MigrateApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/MigrateApiAction.java index 7ea87cba09..1445086979 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/MigrateApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/MigrateApiAction.java @@ -92,6 +92,13 @@ protected Endpoint getEndpoint() { return Endpoint.MIGRATE; } + @Override + protected boolean hasPermissionsToCreate(final SecurityDynamicConfiguration dynamicConfigFactory, + final Object content, + final String resourceName) { + return true; + } + @SuppressWarnings("unchecked") @Override protected void handlePost(RestChannel channel, RestRequest request, Client client, final JsonNode content) throws IOException { diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/NodesDnApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/NodesDnApiAction.java index 5498c2a50b..22897b8305 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/NodesDnApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/NodesDnApiAction.java @@ -77,6 +77,13 @@ public NodesDnApiAction(final Settings settings, final Path configPath, final Re this.staticNodesDnFromEsYml = settings.getAsList(ConfigConstants.SECURITY_NODES_DN, Collections.emptyList()); } + @Override + protected boolean hasPermissionsToCreate(final SecurityDynamicConfiguration dynamicConfigFactory, + final Object content, + final String resourceName) { + return true; + } + @Override public List routes() { if (settings.getAsBoolean(ConfigConstants.SECURITY_NODES_DN_DYNAMIC_CONFIG_ENABLED, false)) { diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/PatchableResourceApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/PatchableResourceApiAction.java index 74abb1d10a..6d644c1eae 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/PatchableResourceApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/PatchableResourceApiAction.java @@ -55,9 +55,9 @@ public abstract class PatchableResourceApiAction extends AbstractApiAction { protected final Logger log = LogManager.getLogger(this.getClass()); public PatchableResourceApiAction(Settings settings, Path configPath, RestController controller, Client client, - AdminDNs adminDNs, ConfigurationRepository cl, ClusterService cs, - PrincipalExtractor principalExtractor, PrivilegesEvaluator evaluator, ThreadPool threadPool, - AuditLog auditLog) { + AdminDNs adminDNs, ConfigurationRepository cl, ClusterService cs, + PrincipalExtractor principalExtractor, PrivilegesEvaluator evaluator, ThreadPool threadPool, + AuditLog auditLog) { super(settings, configPath, controller, client, adminDNs, cl, cs, principalExtractor, evaluator, threadPool, auditLog); } @@ -146,7 +146,7 @@ private void handleSinglePatch(RestChannel channel, RestRequest request, Client if (!validator.validate()) { request.params().clear(); - badRequestResponse(channel, validator); + badRequestResponse(channel, validator); return; } @@ -156,7 +156,7 @@ private void handleSinglePatch(RestChannel channel, RestRequest request, Client , existingConfiguration.getVersion(), existingConfiguration.getSeqNo(), existingConfiguration.getPrimaryTerm()); if (existingConfiguration.getCType().equals(CType.ACTIONGROUPS)) { - if(hasActionGroupSelfReference(mdc, name)) { + if (hasActionGroupSelfReference(mdc, name)) { badRequestResponse(channel, name + " cannot be an allowed_action of itself"); return; } @@ -188,7 +188,6 @@ private void handleBulkPatch(RestChannel channel, RestRequest request, Client cl for (String resourceName : existingConfiguration.getCEntries().keySet()) { JsonNode oldResource = existingAsObjectNode.get(resourceName); JsonNode patchedResource = patchedAsJsonNode.get(resourceName); - if (oldResource != null && !oldResource.equals(patchedResource) && !isWriteable(channel, existingConfiguration, resourceName)) { return; } @@ -206,7 +205,7 @@ private void handleBulkPatch(RestChannel channel, RestRequest request, Client cl if(originalValidator != null) { if (!originalValidator.validate()) { request.params().clear(); - badRequestResponse(channel, originalValidator); + badRequestResponse(channel, originalValidator); return; } } @@ -222,7 +221,13 @@ private void handleBulkPatch(RestChannel channel, RestRequest request, Client cl if (!validator.validate()) { request.params().clear(); - badRequestResponse(channel, validator); + badRequestResponse(channel, validator); + return; + } + final Object newContent = DefaultObjectMapper.readTree(patchedResource, existingConfiguration.getImplementingClass()); + if (!hasPermissionsToCreate(existingConfiguration, newContent, resourceName)) { + request.params().clear(); + forbidden(channel, "No permissions"); return; } } diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/RestApiAdminPrivilegesEvaluator.java b/src/main/java/org/opensearch/security/dlic/rest/api/RestApiAdminPrivilegesEvaluator.java new file mode 100644 index 0000000000..c3449e99bb --- /dev/null +++ b/src/main/java/org/opensearch/security/dlic/rest/api/RestApiAdminPrivilegesEvaluator.java @@ -0,0 +1,164 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.dlic.rest.api; + +import java.util.Locale; +import java.util.Map; + +import com.google.common.collect.ImmutableMap; +import org.apache.commons.lang3.tuple.Pair; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.common.transport.TransportAddress; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.security.configuration.AdminDNs; +import org.opensearch.security.dlic.rest.support.Utils; +import org.opensearch.security.privileges.PrivilegesEvaluator; +import org.opensearch.security.securityconf.impl.v7.ActionGroupsV7; +import org.opensearch.security.securityconf.impl.v7.RoleV7; +import org.opensearch.security.support.WildcardMatcher; +import org.opensearch.security.user.User; + +public class RestApiAdminPrivilegesEvaluator { + + protected final Logger logger = LogManager.getLogger(RestApiAdminPrivilegesEvaluator.class); + + public final static String CERTS_INFO_ACTION = "certs"; + + public final static String RELOAD_CERTS_ACTION = "reloadcerts"; + + private final static String REST_API_PERMISSION_PREFIX = "restapi:admin"; + + private final static String REST_ENDPOINT_PERMISSION_PATTERN = + REST_API_PERMISSION_PREFIX + "/%s"; + + private final static String REST_ENDPOINT_ACTION_PERMISSION_PATTERN = + REST_API_PERMISSION_PREFIX + "/%s/%s"; + + private final static WildcardMatcher REST_API_PERMISSION_PREFIX_MATCHER = + WildcardMatcher.from(REST_API_PERMISSION_PREFIX + "/*"); + + @FunctionalInterface + public interface PermissionBuilder { + + default String build() { + return build(null); + } + + String build(final String action); + + } + + public final static Map ENDPOINTS_WITH_PERMISSIONS = + ImmutableMap.builder() + .put(Endpoint.ACTIONGROUPS, action -> buildEndpointPermission(Endpoint.ACTIONGROUPS)) + .put(Endpoint.ALLOWLIST, action -> buildEndpointPermission(Endpoint.ALLOWLIST)) + .put(Endpoint.INTERNALUSERS, action -> buildEndpointPermission(Endpoint.INTERNALUSERS)) + .put(Endpoint.NODESDN, action -> buildEndpointPermission(Endpoint.NODESDN)) + .put(Endpoint.ROLES, action -> buildEndpointPermission(Endpoint.ROLES)) + .put(Endpoint.ROLESMAPPING, action -> buildEndpointPermission(Endpoint.ROLESMAPPING)) + .put(Endpoint.TENANTS, action -> buildEndpointPermission(Endpoint.TENANTS)) + .put(Endpoint.SSL, action -> { + switch (action) { + case CERTS_INFO_ACTION: + return buildEndpointActionPermission(Endpoint.SSL, "certs/info"); + case RELOAD_CERTS_ACTION: + return buildEndpointActionPermission(Endpoint.SSL, "certs/reload"); + default: + return null; + } + }).build(); + + private final ThreadContext threadContext; + + private final PrivilegesEvaluator privilegesEvaluator; + + private final AdminDNs adminDNs; + + public RestApiAdminPrivilegesEvaluator( + final ThreadContext threadContext, + final PrivilegesEvaluator privilegesEvaluator, + final AdminDNs adminDNs) { + this.threadContext = threadContext; + this.privilegesEvaluator = privilegesEvaluator; + this.adminDNs = adminDNs; + } + + public boolean isCurrentUserRestApiAdminFor(final Endpoint endpoint, final String action) { + final Pair userAndRemoteAddress = Utils.userAndRemoteAddressFrom(threadContext); + if (userAndRemoteAddress.getLeft() == null) { + return false; + } + if (adminDNs.isAdmin(userAndRemoteAddress.getLeft())) { + if (logger.isDebugEnabled()) { + logger.debug( + "Security admin permissions required for endpoint {} but {} is not an admin", + endpoint, userAndRemoteAddress.getLeft().getName()); + } + return true; + } + if (!ENDPOINTS_WITH_PERMISSIONS.containsKey(endpoint)) { + if (logger.isDebugEnabled()) { + logger.debug("No permission found for {} endpoint", endpoint); + } + return false; + } + final String permission = ENDPOINTS_WITH_PERMISSIONS.get(endpoint).build(action); + if (logger.isDebugEnabled()) { + logger.debug("Checking permission {} for endpoint {}", permission, endpoint); + } + return privilegesEvaluator.hasRestAdminPermissions( + userAndRemoteAddress.getLeft(), + userAndRemoteAddress.getRight(), + permission + ); + } + + public boolean containsRestApiAdminPermissions(final Object configObject) { + if (configObject == null) { + return false; + } + if (configObject instanceof RoleV7) { + return ((RoleV7) configObject) + .getCluster_permissions() + .stream() + .anyMatch(REST_API_PERMISSION_PREFIX_MATCHER); + } else if (configObject instanceof ActionGroupsV7) { + return ((ActionGroupsV7) configObject) + .getAllowed_actions() + .stream() + .anyMatch(REST_API_PERMISSION_PREFIX_MATCHER); + } else { + return false; + } + } + + public boolean isCurrentUserRestApiAdminFor(final Endpoint endpoint) { + return isCurrentUserRestApiAdminFor(endpoint, null); + } + + private static String buildEndpointActionPermission(final Endpoint endpoint, final String action) { + return String.format( + REST_ENDPOINT_ACTION_PERMISSION_PATTERN, + endpoint.name().toLowerCase(Locale.ROOT), + action); + } + + private static String buildEndpointPermission(final Endpoint endpoint) { + return String.format( + REST_ENDPOINT_PERMISSION_PATTERN, + endpoint.name().toLowerCase(Locale.ROOT) + ); + } + +} diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/RestApiPrivilegesEvaluator.java b/src/main/java/org/opensearch/security/dlic/rest/api/RestApiPrivilegesEvaluator.java index 21316876cb..04b4b31c77 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/RestApiPrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/RestApiPrivilegesEvaluator.java @@ -25,6 +25,7 @@ import java.util.Map.Entry; import java.util.Set; +import org.apache.commons.lang3.tuple.Pair; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -71,7 +72,11 @@ public class RestApiPrivilegesEvaluator { private final Boolean roleBasedAccessEnabled; - public RestApiPrivilegesEvaluator(Settings settings, AdminDNs adminDNs, PrivilegesEvaluator privilegesEvaluator, PrincipalExtractor principalExtractor, Path configPath, + public RestApiPrivilegesEvaluator(final Settings settings, + final AdminDNs adminDNs, + final PrivilegesEvaluator privilegesEvaluator, + final PrincipalExtractor principalExtractor, + final Path configPath, ThreadPool threadPool) { this.adminDNs = adminDNs; @@ -80,9 +85,7 @@ public RestApiPrivilegesEvaluator(Settings settings, AdminDNs adminDNs, Privileg this.configPath = configPath; this.threadPool = threadPool; this.settings = settings; - // set up - // all endpoints and methods Map> allEndpoints = new HashMap<>(); for(Endpoint endpoint : Endpoint.values()) { @@ -344,8 +347,10 @@ private String checkRoleBasedAccessPermissions(RestRequest request, Endpoint end if (this.roleBasedAccessEnabled) { // get current user and roles - final User user = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); - final TransportAddress remoteAddress = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS); + final Pair userAndRemoteAddress = + Utils.userAndRemoteAddressFrom(threadPool.getThreadContext()); + final User user = userAndRemoteAddress.getLeft(); + final TransportAddress remoteAddress = userAndRemoteAddress.getRight(); // map the users Security roles Set userRoles = privilegesEvaluator.mapRoles(user, remoteAddress); @@ -355,7 +360,6 @@ private String checkRoleBasedAccessPermissions(RestRequest request, Endpoint end // yes, calculate disabled end points. Since a user can have // multiple roles, the endpoint // needs to be disabled in all roles. - Map> disabledEndpointsForUser = getDisabledEndpointsForCurrentUser(user.getName(), userRoles); if (isDebugEnabled) { diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/RolesApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/RolesApiAction.java index 7fb6dfa915..7b2676c246 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/RolesApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/RolesApiAction.java @@ -11,6 +11,7 @@ package org.opensearch.security.dlic.rest.api; +import java.io.IOException; import java.nio.file.Path; import java.util.List; @@ -31,6 +32,7 @@ import org.opensearch.security.dlic.rest.validation.RolesValidator; 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; @@ -77,4 +79,22 @@ protected CType getConfigName() { return CType.ROLES; } + @Override + protected boolean hasPermissionsToCreate(final SecurityDynamicConfiguration dynamicConfiguration, final Object content, final String resourceName) throws IOException { + if (restApiAdminPrivilegesEvaluator.containsRestApiAdminPermissions(content)) { + return isSuperAdmin(); + } else { + return true; + } + } + + @Override + protected boolean isReadOnly(SecurityDynamicConfiguration existingConfiguration, String name) { + if (restApiAdminPrivilegesEvaluator.containsRestApiAdminPermissions(existingConfiguration.getCEntry(name))) { + return !isSuperAdmin(); + } else { + return super.isReadOnly(existingConfiguration, name); + } + } + } diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/RolesMappingApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/RolesMappingApiAction.java index c7e7f3d7ec..b056a25ad2 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/RolesMappingApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/RolesMappingApiAction.java @@ -68,11 +68,18 @@ protected void handlePut(RestChannel channel, final RestRequest request, final C return; } + final SecurityDynamicConfiguration rolesConfiguration = load(CType.ROLES, false); final SecurityDynamicConfiguration rolesMappingConfiguration = load(getConfigName(), false); final boolean rolesMappingExists = rolesMappingConfiguration.exists(name); if (!isValidRolesMapping(channel, name)) return; + if (restApiAdminPrivilegesEvaluator.containsRestApiAdminPermissions(rolesConfiguration.getCEntry(name))) { + if (!isSuperAdmin()) { + forbidden(channel, "No permissions"); + return; + } + } rolesMappingConfiguration.putCObject(name, DefaultObjectMapper.readTree(content, rolesMappingConfiguration.getImplementingClass())); saveAnUpdateConfigs(client, request, getConfigName(), rolesMappingConfiguration, new OnSucessActionListener(channel) { @@ -89,6 +96,26 @@ public void onResponse(IndexResponse response) { }); } + @Override + protected boolean hasPermissionsToCreate(final SecurityDynamicConfiguration dynamicConfigFactory, final Object content, final String resourceName) throws IOException { + final SecurityDynamicConfiguration rolesConfiguration = load(CType.ROLES, false); + if (restApiAdminPrivilegesEvaluator.containsRestApiAdminPermissions(rolesConfiguration.getCEntry(resourceName))) { + return isSuperAdmin(); + } else { + return true; + } + } + + @Override + protected boolean isReadOnly(SecurityDynamicConfiguration existingConfiguration, String name) { + final SecurityDynamicConfiguration rolesConfiguration = load(CType.ROLES, false); + if (restApiAdminPrivilegesEvaluator.containsRestApiAdminPermissions(rolesConfiguration.getCEntry(name))) { + return !isSuperAdmin(); + } else { + return super.isReadOnly(existingConfiguration, name); + } + } + @Override public List routes() { return routes; diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/SecurityConfigAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityConfigAction.java index 7545f47113..66888bc126 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/SecurityConfigAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityConfigAction.java @@ -74,6 +74,13 @@ public List routes() { return allowPutOrPatch ? allRoutes : getRoutes; } + @Override + protected boolean hasPermissionsToCreate(final SecurityDynamicConfiguration dynamicConfigFactory, + final Object content, + final String resourceName) { + return true; + } + @Override protected void handleGet(RestChannel channel, RestRequest request, Client client, final JsonNode content) throws IOException{ final SecurityDynamicConfiguration configuration = load(getConfigName(), true); 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 54ea46efd1..9655ba67ea 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 @@ -26,15 +26,26 @@ import org.opensearch.security.configuration.AdminDNs; import org.opensearch.security.configuration.ConfigurationRepository; import org.opensearch.security.privileges.PrivilegesEvaluator; +import org.opensearch.security.ssl.SecurityKeyStore; import org.opensearch.security.ssl.transport.PrincipalExtractor; import org.opensearch.threadpool.ThreadPool; public class SecurityRestApiActions { - public static Collection getHandler(Settings settings, Path configPath, RestController controller, Client client, - AdminDNs adminDns, ConfigurationRepository cr, ClusterService cs, PrincipalExtractor principalExtractor, - final PrivilegesEvaluator evaluator, ThreadPool threadPool, AuditLog auditLog) { - final List handlers = new ArrayList(15); + public static Collection getHandler(final Settings settings, + final Path configPath, + final RestController controller, + final Client client, + final AdminDNs adminDns, + final ConfigurationRepository cr, + final ClusterService cs, + final PrincipalExtractor principalExtractor, + final PrivilegesEvaluator evaluator, + final ThreadPool threadPool, + final AuditLog auditLog, + final SecurityKeyStore securityKeyStore, + final boolean certificatesReloadEnabled) { + final List handlers = new ArrayList(16); handlers.add(new InternalUsersApiAction(settings, configPath, controller, client, adminDns, cr, cs, principalExtractor, evaluator, threadPool, auditLog)); handlers.add(new RolesMappingApiAction(settings, configPath, controller, client, adminDns, cr, cs, principalExtractor, evaluator, threadPool, auditLog)); handlers.add(new RolesApiAction(settings, configPath, controller, client, adminDns, cr, cs, principalExtractor, evaluator, threadPool, auditLog)); @@ -51,6 +62,7 @@ public static Collection getHandler(Settings settings, Path configP 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)); + handlers.add(new SecuritySSLCertsAction(settings, configPath, controller, client, adminDns, cr, cs, principalExtractor, evaluator, threadPool, auditLog, securityKeyStore, certificatesReloadEnabled)); return Collections.unmodifiableCollection(handlers); } diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/SecuritySSLCertsAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/SecuritySSLCertsAction.java new file mode 100644 index 0000000000..8e936b167a --- /dev/null +++ b/src/main/java/org/opensearch/security/dlic/rest/api/SecuritySSLCertsAction.java @@ -0,0 +1,346 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.dlic.rest.api; + +import java.io.IOException; +import java.nio.file.Path; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.client.Client; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.bytes.BytesReference; +import org.opensearch.common.settings.Settings; +import org.opensearch.common.xcontent.XContentBuilder; +import org.opensearch.rest.BytesRestResponse; +import org.opensearch.rest.RestChannel; +import org.opensearch.rest.RestController; +import org.opensearch.rest.RestRequest; +import org.opensearch.rest.RestRequest.Method; +import org.opensearch.rest.RestStatus; +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.privileges.PrivilegesEvaluator; +import org.opensearch.security.securityconf.impl.CType; +import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; +import org.opensearch.security.ssl.SecurityKeyStore; +import org.opensearch.security.ssl.transport.PrincipalExtractor; +import org.opensearch.security.ssl.util.SSLConfigConstants; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.threadpool.ThreadPool; + +import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; + + +/** + * Rest API action to get SSL certificate information related to http and transport encryption. + * Only super admin users are allowed to access this API. + * This action serves GET request for _plugins/_security/api/ssl/certs endpoint and + * PUT _plugins/_security/api/ssl/{certType}/reloadcerts + */ +public class SecuritySSLCertsAction extends AbstractApiAction { + private static final List ROUTES = addRoutesPrefix( + ImmutableList.of( + new Route(Method.GET, "/ssl/certs"), + new Route(Method.PUT, "/ssl/{certType}/reloadcerts") + ) + ); + + private final Logger log = LogManager.getLogger(this.getClass()); + + private final SecurityKeyStore securityKeyStore; + + private final boolean certificatesReloadEnabled; + + private final RestApiAdminPrivilegesEvaluator restApiAdminPrivilegesEvaluator; + + private final boolean httpsEnabled; + + public SecuritySSLCertsAction(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 privilegesEvaluator, + final ThreadPool threadPool, + final AuditLog auditLog, + final SecurityKeyStore securityKeyStore, + final boolean certificatesReloadEnabled) { + super(settings, configPath, controller, client, adminDNs, cl, cs, principalExtractor, privilegesEvaluator, threadPool, auditLog); + this.securityKeyStore = securityKeyStore; + this.restApiAdminPrivilegesEvaluator = + new RestApiAdminPrivilegesEvaluator(threadPool.getThreadContext(), privilegesEvaluator, adminDNs); + this.certificatesReloadEnabled = certificatesReloadEnabled; + this.httpsEnabled = settings.getAsBoolean(SSLConfigConstants.SECURITY_SSL_HTTP_ENABLED, true); + } + + @Override + protected boolean hasPermissionsToCreate(final SecurityDynamicConfiguration dynamicConfigFactory, + final Object content, + final String resourceName) { + return true; + } + + @Override + public List routes() { + return ROUTES; + } + + @Override + protected void handleApiRequest(final RestChannel channel, final RestRequest request, final Client client) throws IOException { + switch (request.method()) { + case GET: + if (!restApiAdminPrivilegesEvaluator.isCurrentUserRestApiAdminFor(getEndpoint(), "certs")) { + forbidden(channel, ""); + return; + } + handleGet(channel, request, client, null); + break; + case PUT: + if (!restApiAdminPrivilegesEvaluator.isCurrentUserRestApiAdminFor(getEndpoint(), "reloadcerts")) { + forbidden(channel, ""); + return; + } + if (!certificatesReloadEnabled) { + badRequestResponse( + channel, + String.format( + "no handler found for uri [%s] and method [%s]. In order to use SSL reload functionality set %s to true", + request.path(), + request.method(), + ConfigConstants.SECURITY_SSL_CERT_RELOAD_ENABLED + ) + ); + return; + } + handlePut(channel, request, client, null); + break; + default: + notImplemented(channel, request.method()); + break; + } + } + + /** + * GET request to fetch transport certificate details + * + * Sample request: + * GET _plugins/_security/api/ssl/certs + * + * Sample response: + * { + * "http_certificates_list" : [ + * { + * "issuer_dn" : "CN=Example Com Inc. Signing CA, OU=Example Com Inc. Signing CA, O=Example Com Inc., DC=example, DC=com", + * "subject_dn" : "CN=transport-0.example.com, OU=SSL, O=Test, L=Test, C=DE", + * "san", "[[2, node-0.example.com], [2, localhost], [7, 127.0.0.1], [8, 1.2.3.4.5.5]]", + * "not_before" : "2018-05-05T14:37:09.000Z", + * "not_after" : "2028-05-02T14:37:09.000Z" + * } + * "transport_certificates_list" : [ + * { + * "issuer_dn" : "CN=Example Com Inc. Signing CA, OU=Example Com Inc. Signing CA, O=Example Com Inc., DC=example, DC=com", + * "subject_dn" : "CN=transport-0.example.com, OU=SSL, O=Test, L=Test, C=DE", + * "san", "[[2, node-0.example.com], [2, localhost], [7, 127.0.0.1], [8, 1.2.3.4.5.5]]", + * "not_before" : "2018-05-05T14:37:09.000Z", + * "not_after" : "2028-05-02T14:37:09.000Z" + * } + * ] + * } + * + * @param request request to be served + * @param client client + * @throws IOException + */ + @Override + protected void handleGet(final RestChannel channel, + final RestRequest request, + final Client client, + final JsonNode content) throws IOException { + if (securityKeyStore == null) { + noKeyStoreResponse(channel); + return; + } + try (final XContentBuilder contentBuilder = channel.newBuilder()) { + channel.sendResponse( + new BytesRestResponse( + RestStatus.OK, + contentBuilder + .startObject() + .field( + "http_certificates_list", + httpsEnabled ? generateCertDetailList(securityKeyStore.getHttpCerts()) : null + ).field( + "transport_certificates_list", + generateCertDetailList(securityKeyStore.getTransportCerts()) + ).endObject() + ) + ); + } catch (final Exception e) { + internalErrorResponse(channel, e.getMessage()); + log.error("Error handle request ", e); + } + } + + /** + * PUT request to reload SSL Certificates. + * + * Sample request: + * PUT _opendistro/_security/api/ssl/transport/reloadcerts + * PUT _opendistro/_security/api/ssl/http/reloadcerts + * + * NOTE: No request body is required. We will assume new certificates are loaded in the paths specified in your opensearch.yml file + * (https://docs-beta.opensearch.org/docs/security/configuration/tls/) + * + * Sample response: + * { "message": "updated http certs" } + * + * @param request request to be served + * @param client client + * @throws IOException + */ + @Override + protected void handlePut(final RestChannel channel, + final RestRequest request, + final Client client, + final JsonNode content) throws IOException { + if (securityKeyStore == null) { + noKeyStoreResponse(channel); + return; + } + final String certType = request.param("certType").toLowerCase().trim(); + try (final XContentBuilder contentBuilder = channel.newBuilder()) { + switch (certType) { + case "http": + if (!httpsEnabled) { + badRequestResponse(channel, "SSL for HTTP is disabled"); + return; + } + securityKeyStore.initHttpSSLConfig(); + channel.sendResponse( + new BytesRestResponse( + RestStatus.OK, + contentBuilder + .startObject() + .field("message", "updated http certs") + .endObject() + ) + ); + break; + case "transport": + securityKeyStore.initTransportSSLConfig(); + channel.sendResponse( + new BytesRestResponse( + RestStatus.OK, + contentBuilder + .startObject() + .field("message", "updated transport certs") + .endObject() + ) + ); + break; + default: + forbidden(channel, + "invalid uri path, please use /_plugins/_security/api/ssl/http/reload or " + + "/_plugins/_security/api/ssl/transport/reload" + ); + break; + } + } catch (final Exception e) { + log.error("Reload of certificates for {} failed", certType, e); + try (final XContentBuilder contentBuilder = channel.newBuilder()) { + channel.sendResponse(new BytesRestResponse( + RestStatus.INTERNAL_SERVER_ERROR, + contentBuilder + .startObject() + .field("error", e.toString()) + .endObject() + ) + ); + } + } + } + + private List> generateCertDetailList(final X509Certificate[] certs) { + if (certs == null) { + return null; + } + return Arrays + .stream(certs) + .map(cert -> { + final String issuerDn = cert != null && cert.getIssuerX500Principal() != null ? cert.getIssuerX500Principal().getName() : ""; + final String subjectDn = cert != null && cert.getSubjectX500Principal() != null ? cert.getSubjectX500Principal().getName() : ""; + + final String san = securityKeyStore.getSubjectAlternativeNames(cert); + + final String notBefore = cert != null && cert.getNotBefore() != null ? cert.getNotBefore().toInstant().toString() : ""; + final String notAfter = cert != null && cert.getNotAfter() != null ? cert.getNotAfter().toInstant().toString() : ""; + return ImmutableMap.of( + "issuer_dn", issuerDn, + "subject_dn", subjectDn, + "san", san, + "not_before", notBefore, + "not_after", notAfter + ); + }) + .collect(Collectors.toList()); + } + + private void noKeyStoreResponse(final RestChannel channel) throws IOException { + response(channel, RestStatus.OK, "keystore is not initialized"); + } + + @Override + protected Endpoint getEndpoint() { + return Endpoint.SSL; + } + + @Override + public String getName() { + return "SSL Certificates Action"; + } + + @Override + protected AbstractConfigurationValidator getValidator(RestRequest request, BytesReference ref, Object... params) { + return null; + } + + @Override + protected void consumeParameters(RestRequest request) { + request.param("certType"); + } + + @Override + protected String getResourceName() { + return null; + } + + @Override + protected CType getConfigName() { + return null; + } + +} diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/TenantsApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/TenantsApiAction.java index 26d39767d5..ef8d1d3b9f 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/TenantsApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/TenantsApiAction.java @@ -47,6 +47,7 @@ import org.opensearch.security.dlic.rest.validation.TenantValidator; 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; @@ -69,6 +70,13 @@ public TenantsApiAction(final Settings settings, final Path configPath, final Re super(settings, configPath, controller, client, adminDNs, cl, cs, principalExtractor, evaluator, threadPool, auditLog); } + @Override + protected boolean hasPermissionsToCreate(final SecurityDynamicConfiguration dynamicConfigFactory, + final Object content, + final String resourceName) { + return true; + } + @Override public List routes() { return routes; diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/ValidateApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/ValidateApiAction.java index a3f2b202ee..8e2222cab0 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/ValidateApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/ValidateApiAction.java @@ -67,6 +67,13 @@ public ValidateApiAction(final Settings settings, final Path configPath, final R super(settings, configPath, controller, client, adminDNs, cl, cs, principalExtractor, evaluator, threadPool, auditLog); } + @Override + protected boolean hasPermissionsToCreate(final SecurityDynamicConfiguration dynamicConfigFactory, + final Object content, + final String resourceName) { + return true; + } + @Override public List routes() { return routes; 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 047a649718..099087523b 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 @@ -29,12 +29,15 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; +import org.apache.commons.lang3.tuple.Pair; import org.bouncycastle.crypto.generators.OpenBSDBCrypt; import org.opensearch.ExceptionsHelper; import org.opensearch.OpenSearchParseException; import org.opensearch.SpecialPermission; import org.opensearch.common.bytes.BytesReference; +import org.opensearch.common.transport.TransportAddress; +import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.common.xcontent.NamedXContentRegistry; import org.opensearch.common.xcontent.ToXContent; import org.opensearch.common.xcontent.XContentHelper; @@ -44,6 +47,8 @@ import org.opensearch.rest.RestHandler.DeprecatedRoute; import org.opensearch.rest.RestHandler.Route; import org.opensearch.security.DefaultObjectMapper; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.user.User; import static org.opensearch.common.xcontent.DeprecationHandler.THROW_UNSUPPORTED_OPERATION; @@ -265,4 +270,11 @@ public static List addDeprecatedRoutesPrefix(List new DeprecatedRoute(r.getMethod(), p + r.getPath(), r.getDeprecationMessage()))) .collect(ImmutableList.toImmutableList()); } + + public static Pair userAndRemoteAddressFrom(final ThreadContext threadContext) { + final User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + final TransportAddress remoteAddress = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS); + return Pair.of(user, remoteAddress); + } + } diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java index df9c432827..9967859d9f 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java @@ -179,6 +179,18 @@ private SecurityRoles getSecurityRoles(Set roles) { return configModel.getSecurityRoles().filter(roles); } + public boolean hasRestAdminPermissions(final User user, + final TransportAddress remoteAddress, + final String permissions) { + final Set userRoles = mapRoles(user, remoteAddress); + return hasRestAdminPermissions(userRoles, permissions); + } + + private boolean hasRestAdminPermissions(final Set roles, String permission) { + final SecurityRoles securityRoles = getSecurityRoles(roles); + return securityRoles.hasExplicitClusterPermissionPermission(permission); + } + public boolean isInitialized() { return configModel !=null && configModel.getSecurityRoles() != null && dcm != null; } diff --git a/src/main/java/org/opensearch/security/securityconf/ConfigModelV6.java b/src/main/java/org/opensearch/security/securityconf/ConfigModelV6.java index a9ba2d7142..9de3cb9417 100644 --- a/src/main/java/org/opensearch/security/securityconf/ConfigModelV6.java +++ b/src/main/java/org/opensearch/security/securityconf/ConfigModelV6.java @@ -482,10 +482,20 @@ public boolean get(Resolved resolved, User user, String[] actions, IndexNameExpr return false; } + @Override public boolean impliesClusterPermissionPermission(String action) { return roles.stream().filter(r -> r.impliesClusterPermission(action)).count() > 0; } + @Override + public boolean hasExplicitClusterPermissionPermission(String action) { + return roles.stream() + .map(r -> { + final WildcardMatcher m = WildcardMatcher.from(r.clusterPerms); + return m == WildcardMatcher.ANY ? WildcardMatcher.NONE : m; + }).filter(m -> m.test(action)).count() > 0; + } + //rolespan public boolean impliesTypePermGlobal(Resolved resolved, User user, String[] actions, IndexNameExpressionResolver resolver, ClusterService cs) { diff --git a/src/main/java/org/opensearch/security/securityconf/ConfigModelV7.java b/src/main/java/org/opensearch/security/securityconf/ConfigModelV7.java index d5d682f272..8845f95aba 100644 --- a/src/main/java/org/opensearch/security/securityconf/ConfigModelV7.java +++ b/src/main/java/org/opensearch/security/securityconf/ConfigModelV7.java @@ -478,10 +478,18 @@ public boolean get(Resolved resolved, User user, String[] actions, IndexNameExpr return false; } + @Override public boolean impliesClusterPermissionPermission(String action) { return roles.stream().filter(r -> r.impliesClusterPermission(action)).count() > 0; } + @Override + public boolean hasExplicitClusterPermissionPermission(String action) { + return roles.stream() + .map(r -> r.clusterPerms == WildcardMatcher.ANY ? WildcardMatcher.NONE : r.clusterPerms) + .filter(m -> m.test(action)).count() > 0; + } + //rolespan public boolean impliesTypePermGlobal(Resolved resolved, User user, String[] actions, IndexNameExpressionResolver resolver, ClusterService cs) { diff --git a/src/main/java/org/opensearch/security/securityconf/SecurityRoles.java b/src/main/java/org/opensearch/security/securityconf/SecurityRoles.java index d71b279afb..478e0f03dd 100644 --- a/src/main/java/org/opensearch/security/securityconf/SecurityRoles.java +++ b/src/main/java/org/opensearch/security/securityconf/SecurityRoles.java @@ -39,6 +39,8 @@ public interface SecurityRoles { boolean impliesClusterPermissionPermission(String action0); + boolean hasExplicitClusterPermissionPermission(String action); + Set getRoleNames(); Set reduce(Resolved requestedResolved, User user, String[] strings, IndexNameExpressionResolver resolver, ClusterService clusterService); diff --git a/src/main/java/org/opensearch/security/ssl/rest/SecuritySSLCertsInfoAction.java b/src/main/java/org/opensearch/security/ssl/rest/SecuritySSLCertsInfoAction.java deleted file mode 100644 index 6c1ec5296c..0000000000 --- a/src/main/java/org/opensearch/security/ssl/rest/SecuritySSLCertsInfoAction.java +++ /dev/null @@ -1,190 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.security.ssl.rest; - -import java.io.IOException; -import java.security.cert.X509Certificate; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import org.opensearch.client.node.NodeClient; -import org.opensearch.common.settings.Settings; -import org.opensearch.common.util.concurrent.ThreadContext; -import org.opensearch.common.xcontent.XContentBuilder; -import org.opensearch.rest.BaseRestHandler; -import org.opensearch.rest.BytesRestResponse; -import org.opensearch.rest.RestChannel; -import org.opensearch.rest.RestController; -import org.opensearch.rest.RestRequest; -import org.opensearch.rest.RestRequest.Method; -import org.opensearch.rest.RestStatus; -import org.opensearch.security.configuration.AdminDNs; -import org.opensearch.security.ssl.SecurityKeyStore; -import org.opensearch.security.support.ConfigConstants; -import org.opensearch.security.user.User; -import org.opensearch.threadpool.ThreadPool; - -import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; - - -/** - * Rest API action to get SSL certificate information related to http and transport encryption. - * Only super admin users are allowed to access this API. - * Currently this action serves GET request for _plugins/_security/api/ssl/certs endpoint - */ -public class SecuritySSLCertsInfoAction extends BaseRestHandler { - private static final List routes = addRoutesPrefix(ImmutableList.of( - new Route(Method.GET, "/ssl/certs") - )); - - private final Logger log = LogManager.getLogger(this.getClass()); - private Settings settings; - private SecurityKeyStore odsks; - private AdminDNs adminDns; - private ThreadContext threadContext; - - public SecuritySSLCertsInfoAction(final Settings settings, - final RestController restController, - final SecurityKeyStore odsks, - final ThreadPool threadPool, - final AdminDNs adminDns) { - super(); - this.settings = settings; - this.odsks = odsks; - this.adminDns = adminDns; - this.threadContext = threadPool.getThreadContext(); - } - - @Override - public List routes() { - return routes; - } - - /** - * GET request to fetch transport certificate details - * - * Sample request: - * GET _plugins/_security/api/ssl/certs - * - * Sample response: - * { - * "http_certificates_list" : [ - * { - * "issuer_dn" : "CN=Example Com Inc. Signing CA, OU=Example Com Inc. Signing CA, O=Example Com Inc., DC=example, DC=com", - * "subject_dn" : "CN=transport-0.example.com, OU=SSL, O=Test, L=Test, C=DE", - * "san", "[[2, node-0.example.com], [2, localhost], [7, 127.0.0.1], [8, 1.2.3.4.5.5]]", - * "not_before" : "2018-05-05T14:37:09.000Z", - * "not_after" : "2028-05-02T14:37:09.000Z" - * } - * "transport_certificates_list" : [ - * { - * "issuer_dn" : "CN=Example Com Inc. Signing CA, OU=Example Com Inc. Signing CA, O=Example Com Inc., DC=example, DC=com", - * "subject_dn" : "CN=transport-0.example.com, OU=SSL, O=Test, L=Test, C=DE", - * "san", "[[2, node-0.example.com], [2, localhost], [7, 127.0.0.1], [8, 1.2.3.4.5.5]]", - * "not_before" : "2018-05-05T14:37:09.000Z", - * "not_after" : "2028-05-02T14:37:09.000Z" - * } - * ] - * } - * - * @param request request to be served - * @param client client - * @throws IOException - */ - @Override - protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { - - return new RestChannelConsumer() { - - @Override - public void accept(RestChannel channel) throws Exception { - XContentBuilder builder = channel.newBuilder(); - BytesRestResponse response = null; - - // Check for Super admin user - final User user = (User)threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); - if(user == null || !adminDns.isAdmin(user)) { - response = new BytesRestResponse(RestStatus.FORBIDDEN, builder); - } else { - try { - // Check if keystore initialised - if (odsks != null) { - builder.startObject(); - builder.field("http_certificates_list", generateCertDetailList(odsks.getHttpCerts())); - builder.field("transport_certificates_list", generateCertDetailList(odsks.getTransportCerts())); - builder.endObject(); - response = new BytesRestResponse(RestStatus.OK, builder); - } else { - builder.startObject(); - builder.field("message", "keystore is not initialized"); - builder.endObject(); - response = new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, builder); - } - } catch (final Exception e1) { - log.error("Error handle request ", e1); - builder = channel.newBuilder(); - builder.startObject(); - builder.field("error", e1.toString()); - builder.endObject(); - response = new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, builder); - } finally { - if (builder != null) { - builder.close(); - } - } - } - channel.sendResponse(response); - } - - /** - * Helper that construct list of certificate details. - * @param certs list of certificates. - * @return Array containing certificate details. - */ - private List> generateCertDetailList(final X509Certificate[] certs) { - if (certs == null) { - return null; - } - return Arrays.stream(certs) - .map(cert -> { - final String issuerDn = cert != null && cert.getIssuerX500Principal() != null ? cert.getIssuerX500Principal().getName(): ""; - final String subjectDn = cert != null && cert.getSubjectX500Principal() != null ? cert.getSubjectX500Principal().getName(): ""; - - final String san = odsks.getSubjectAlternativeNames(cert); - - final String notBefore = cert != null && cert.getNotBefore() != null ? cert.getNotBefore().toInstant().toString(): ""; - final String notAfter = cert != null && cert.getNotAfter() != null ? cert.getNotAfter().toInstant().toString(): ""; - return ImmutableMap.builder() - .put("issuer_dn", issuerDn) - .put("subject_dn", subjectDn) - .put("san", san) - .put("not_before", notBefore) - .put("not_after", notAfter) - .build(); - }) - .collect(Collectors.toList()); - } - }; - } - - @Override - public String getName() { - return "SSL Certificate Information Action"; - } -} diff --git a/src/main/java/org/opensearch/security/ssl/rest/SecuritySSLReloadCertsAction.java b/src/main/java/org/opensearch/security/ssl/rest/SecuritySSLReloadCertsAction.java deleted file mode 100644 index ef4c89c625..0000000000 --- a/src/main/java/org/opensearch/security/ssl/rest/SecuritySSLReloadCertsAction.java +++ /dev/null @@ -1,155 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.security.ssl.rest; - -import java.io.IOException; -import java.util.Collections; -import java.util.List; - -import org.opensearch.client.node.NodeClient; -import org.opensearch.common.settings.Settings; -import org.opensearch.common.util.concurrent.ThreadContext; -import org.opensearch.common.xcontent.XContentBuilder; -import org.opensearch.rest.BaseRestHandler; -import org.opensearch.rest.BytesRestResponse; -import org.opensearch.rest.RestChannel; -import org.opensearch.rest.RestController; -import org.opensearch.rest.RestRequest; -import org.opensearch.rest.RestStatus; -import org.opensearch.security.configuration.AdminDNs; -import org.opensearch.security.ssl.SecurityKeyStore; -import org.opensearch.security.support.ConfigConstants; -import org.opensearch.security.user.User; -import org.opensearch.threadpool.ThreadPool; - -import static org.opensearch.rest.RestRequest.Method.PUT; - - -/** - * Rest API action to reload SSL certificates. - * Can be used to reload SSL certificates that are about to expire without restarting OpenSearch node. - * This API assumes that new certificates are in the same location specified by the security configurations in opensearch.yml - * (https://docs-beta.opensearch.org/docs/security-configuration/tls/) - * To keep sensitive certificate reload secure, this API will only allow hot reload - * with certificates issued by the same Issuer and Subject DN and SAN with expiry dates after the current one. - * Currently this action serves PUT request for /_opendistro/_security/ssl/http/reloadcerts or /_opendistro/_security/ssl/transport/reloadcerts endpoint - */ -public class SecuritySSLReloadCertsAction extends BaseRestHandler { - private static final List routes = Collections.singletonList( - new Route(PUT, "_opendistro/_security/api/ssl/{certType}/reloadcerts/") - ); - - private final Settings settings; - private final SecurityKeyStore sks; - private final ThreadContext threadContext; - private final AdminDNs adminDns; - - public SecuritySSLReloadCertsAction(final Settings settings, - final RestController restController, - final SecurityKeyStore sks, - final ThreadPool threadPool, - final AdminDNs adminDns) { - super(); - this.settings = settings; - this.sks = sks; - this.adminDns = adminDns; - this.threadContext = threadPool.getThreadContext(); - } - - @Override - public List routes() { - return routes; - } - - /** - * PUT request to reload SSL Certificates. - * - * Sample request: - * PUT _opendistro/_security/api/ssl/transport/reloadcerts - * PUT _opendistro/_security/api/ssl/http/reloadcerts - * - * NOTE: No request body is required. We will assume new certificates are loaded in the paths specified in your opensearch.yml file - * (https://docs-beta.opensearch.org/docs/security/configuration/tls/) - * - * Sample response: - * { "message": "updated http certs" } - * - * @param request request to be served - * @param client client - * @throws IOException - */ - @Override - protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { - return new RestChannelConsumer() { - - final String certType = request.param("certType").toLowerCase().trim(); - - @Override - public void accept(RestChannel channel) throws Exception { - XContentBuilder builder = channel.newBuilder(); - BytesRestResponse response = null; - - // Check for Super admin user - final User user = (User) threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); - if(user ==null||!adminDns.isAdmin(user)) { - response = new BytesRestResponse(RestStatus.FORBIDDEN, ""); - } else { - try { - builder.startObject(); - if (sks != null) { - switch (certType) { - case "http": - sks.initHttpSSLConfig(); - builder.field("message", "updated http certs"); - builder.endObject(); - response = new BytesRestResponse(RestStatus.OK, builder); - break; - case "transport": - sks.initTransportSSLConfig(); - builder.field("message", "updated transport certs"); - builder.endObject(); - response = new BytesRestResponse(RestStatus.OK, builder); - break; - default: - builder.field("message", "invalid uri path, please use /_opendistro/_security/api/ssl/http/reload or " + - "/_opendistro/_security/api/ssl/transport/reload"); - builder.endObject(); - response = new BytesRestResponse(RestStatus.FORBIDDEN, builder); - break; - } - } else { - builder.field("message", "keystore is not initialized"); - builder.endObject(); - response = new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, builder); - } - } catch (final Exception e1) { - builder = channel.newBuilder(); - builder.startObject(); - builder.field("error", e1.toString()); - builder.endObject(); - response = new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, builder); - } finally { - if (builder != null) { - builder.close(); - } - } - } - channel.sendResponse(response); - } - }; - } - - @Override - public String getName() { - return "SSL Cert Reload Action"; - } -} diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/AbstractRestApiUnitTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/AbstractRestApiUnitTest.java index 59e8feb198..98730a81cb 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/AbstractRestApiUnitTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/AbstractRestApiUnitTest.java @@ -16,10 +16,14 @@ import java.util.Collection; import java.util.HashMap; import java.util.Map; +import java.util.stream.Stream; import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import org.apache.http.Header; import org.apache.http.HttpStatus; import org.junit.Assert; @@ -90,18 +94,7 @@ protected final void setupWithRestRoles(Settings nodeOverride) throws Exception .put("plugins.security.ssl.http.truststore_filepath", FileHelper.getAbsoluteFilePathFromClassPath("restapi/truststore.jks")); - builder.put("plugins.security.restapi.roles_enabled.0", "opendistro_security_role_klingons"); - builder.put("plugins.security.restapi.roles_enabled.1", "opendistro_security_role_vulcans"); - builder.put("plugins.security.restapi.roles_enabled.2", "opendistro_security_test"); - - builder.put("plugins.security.restapi.endpoints_disabled.global.CACHE.0", "*"); - - builder.put("plugins.security.restapi.endpoints_disabled.opendistro_security_role_klingons.conFiGuration.0", "*"); - builder.put("plugins.security.restapi.endpoints_disabled.opendistro_security_role_klingons.wRongType.0", "WRONGType"); - builder.put("plugins.security.restapi.endpoints_disabled.opendistro_security_role_klingons.ROLESMAPPING.0", "PUT"); - builder.put("plugins.security.restapi.endpoints_disabled.opendistro_security_role_klingons.ROLESMAPPING.1", "DELETE"); - - builder.put("plugins.security.restapi.endpoints_disabled.opendistro_security_role_vulcans.CONFIG.0", "*"); + builder.put(rolesSettings()); if (null != nodeOverride) { builder.put(nodeOverride); @@ -114,6 +107,20 @@ protected final void setupWithRestRoles(Settings nodeOverride) throws Exception AuditTestUtils.updateAuditConfig(rh, nodeOverride != null ? nodeOverride : Settings.EMPTY); } + protected Settings rolesSettings() { + return Settings.builder() + .put("plugins.security.restapi.roles_enabled.0", "opendistro_security_role_klingons") + .put("plugins.security.restapi.roles_enabled.1", "opendistro_security_role_vulcans") + .put("plugins.security.restapi.roles_enabled.2", "opendistro_security_test") + .put("plugins.security.restapi.endpoints_disabled.global.CACHE.0", "*") + .put("plugins.security.restapi.endpoints_disabled.opendistro_security_role_klingons.conFiGuration.0", "*") + .put("plugins.security.restapi.endpoints_disabled.opendistro_security_role_klingons.wRongType.0", "WRONGType") + .put("plugins.security.restapi.endpoints_disabled.opendistro_security_role_klingons.ROLESMAPPING.0", "PUT") + .put("plugins.security.restapi.endpoints_disabled.opendistro_security_role_klingons.ROLESMAPPING.1", "DELETE") + .put("plugins.security.restapi.endpoints_disabled.opendistro_security_role_vulcans.CONFIG.0", "*") + .build(); + } + protected void deleteUser(String username) throws Exception { boolean sendAdminCertificate = rh.sendAdminCertificate; rh.sendAdminCertificate = true; @@ -197,30 +204,23 @@ protected void checkGeneralAccess(int status, String username, String password) protected String checkReadAccess(int status, String username, String password, String indexName, String actionType, int id) throws Exception { - boolean sendAdminCertificate = rh.sendAdminCertificate; rh.sendAdminCertificate = false; String action = indexName + "/" + actionType + "/" + id; - HttpResponse response = rh.executeGetRequest(action, - encodeBasicHeader(username, password)); + HttpResponse response = rh.executeGetRequest(action, encodeBasicHeader(username, password)); int returnedStatus = response.getStatusCode(); Assert.assertEquals(status, returnedStatus); - rh.sendAdminCertificate = sendAdminCertificate; return response.getBody(); } protected String checkWriteAccess(int status, String username, String password, String indexName, String actionType, int id) throws Exception { - - boolean sendAdminCertificate = rh.sendAdminCertificate; rh.sendAdminCertificate = false; String action = indexName + "/" + actionType + "/" + id; String payload = "{\"value\" : \"true\"}"; - HttpResponse response = rh.executePutRequest(action, payload, - encodeBasicHeader(username, password)); + HttpResponse response = rh.executePutRequest(action, payload, encodeBasicHeader(username, password)); int returnedStatus = response.getStatusCode(); Assert.assertEquals(status, returnedStatus); - rh.sendAdminCertificate = sendAdminCertificate; return response.getBody(); } @@ -260,4 +260,27 @@ protected Map jsonStringToMap(String json) throws JsonParseExcep protected static Collection> asCollection(Class... plugins) { return Arrays.asList(plugins); } + + String createRestAdminPermissionsPayload(String... additionPerms) throws JsonProcessingException { + final ObjectNode rootNode = (ObjectNode) DefaultObjectMapper.objectMapper.createObjectNode(); + rootNode.set("cluster_permissions", clusterPermissionsForRestAdmin(additionPerms)); + return DefaultObjectMapper.objectMapper.writeValueAsString(rootNode); + } + + ArrayNode clusterPermissionsForRestAdmin(String... additionPerms) { + final ArrayNode permissionsArray = (ArrayNode) DefaultObjectMapper.objectMapper.createArrayNode(); + for (final Map.Entry entry : RestApiAdminPrivilegesEvaluator.ENDPOINTS_WITH_PERMISSIONS.entrySet()) { + if (entry.getKey() == Endpoint.SSL) { + permissionsArray + .add(entry.getValue().build("certs")) + .add(entry.getValue().build("reloadcerts")); + } else { + permissionsArray.add(entry.getValue().build()); + } + } + if (additionPerms.length != 0) { + Stream.of(additionPerms).forEach(permissionsArray::add); + } + return permissionsArray; + } } diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/ActionGroupsApiTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/ActionGroupsApiTest.java index 09efae9fbe..bf5acdf0c3 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/ActionGroupsApiTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/ActionGroupsApiTest.java @@ -13,6 +13,9 @@ import java.util.List; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import org.apache.http.Header; import org.apache.http.HttpStatus; import org.junit.Assert; @@ -20,6 +23,7 @@ import org.opensearch.common.settings.Settings; import org.opensearch.common.xcontent.XContentType; +import org.opensearch.security.DefaultObjectMapper; import org.opensearch.security.dlic.rest.validation.AbstractConfigurationValidator; import org.opensearch.security.test.helper.file.FileHelper; import org.opensearch.security.test.helper.rest.RestHelper.HttpResponse; @@ -44,9 +48,27 @@ public void testActionGroupsApi() throws Exception { rh.keystore = "restapi/kirk-keystore.jks"; rh.sendAdminCertificate = true; + // create index + setupStarfleetIndex(); + + // add user picard, role starfleet, maps to opendistro_security_role_starfleet + addUserWithPassword("picard", "picard", new String[] { "starfleet" }, HttpStatus.SC_CREATED); + checkReadAccess(HttpStatus.SC_OK, "picard", "picard", "sf", "_doc", 0); + checkWriteAccess(HttpStatus.SC_FORBIDDEN, "picard", "picard", "sf", "_doc", 0); + rh.sendAdminCertificate = true; + verifyGetForSuperAdmin(new Header[0]); + rh.sendAdminCertificate = true; + verifyDeleteForSuperAdmin(new Header[0], true); + rh.sendAdminCertificate = true; + verifyPutForSuperAdmin(new Header[0], true); + rh.sendAdminCertificate = true; + verifyPatchForSuperAdmin(new Header[0], true); + } + + void verifyGetForSuperAdmin(final Header[] header) throws Exception { // --- GET_UT // GET_UT, actiongroup exists - HttpResponse response = rh.executeGetRequest(ENDPOINT+"/CRUD_UT", new Header[0]); + HttpResponse response = rh.executeGetRequest(ENDPOINT+"/CRUD_UT", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); Settings settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); List permissions = settings.getAsList("CRUD_UT.allowed_actions"); @@ -56,61 +78,50 @@ public void testActionGroupsApi() throws Exception { Assert.assertTrue(permissions.contains("OPENDISTRO_SECURITY_WRITE")); // GET_UT, actiongroup does not exist - response = rh.executeGetRequest(ENDPOINT+"/nothinghthere", new Header[0]); + response = rh.executeGetRequest(ENDPOINT+"/nothinghthere", header); Assert.assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatusCode()); // GET_UT, old endpoint - response = rh.executeGetRequest(ENDPOINT, new Header[0]); + response = rh.executeGetRequest(ENDPOINT, header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); // GET_UT, old endpoint - response = rh.executeGetRequest(ENDPOINT, new Header[0]); + response = rh.executeGetRequest(ENDPOINT, header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); // GET_UT, new endpoint which replaces configuration endpoint - response = rh.executeGetRequest(ENDPOINT, new Header[0]); + response = rh.executeGetRequest(ENDPOINT, header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); // GET_UT, old endpoint - response = rh.executeGetRequest(ENDPOINT, new Header[0]); + response = rh.executeGetRequest(ENDPOINT, header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); // GET_UT, new endpoint which replaces configuration endpoint - response = rh.executeGetRequest(ENDPOINT, new Header[0]); + response = rh.executeGetRequest(ENDPOINT, header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); // GET_UT, new endpoint which replaces configuration endpoint - response = rh.executeGetRequest(ENDPOINT, new Header[0]); + response = rh.executeGetRequest(ENDPOINT, header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); + } - // create index - setupStarfleetIndex(); - - // add user picard, role starfleet, maps to opendistro_security_role_starfleet - addUserWithPassword("picard", "picard", new String[] { "starfleet" }, HttpStatus.SC_CREATED); - checkReadAccess(HttpStatus.SC_OK, "picard", "picard", "sf", "_doc", 0); - // TODO: only one doctype allowed for ES6 - // checkReadAccess(HttpStatus.SC_OK, "picard", "picard", "sf", "public", 0); - checkWriteAccess(HttpStatus.SC_FORBIDDEN, "picard", "picard", "sf", "_doc", 0); - // TODO: only one doctype allowed for ES6 - //checkWriteAccess(HttpStatus.SC_OK, "picard", "picard", "sf", "public", 0); - + void verifyDeleteForSuperAdmin(final Header[] header, final boolean userAdminCert) throws Exception { // -- DELETE // Non-existing role - rh.sendAdminCertificate = true; + rh.sendAdminCertificate = userAdminCert; - response = rh.executeDeleteRequest(ENDPOINT+"/idonotexist", new Header[0]); + HttpResponse response = rh.executeDeleteRequest(ENDPOINT+"/idonotexist", header); Assert.assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatusCode()); // remove action group READ_UT, read access not possible since // opendistro_security_role_starfleet // uses this action group. - response = rh.executeDeleteRequest(ENDPOINT+"/READ_UT", new Header[0]); + response = rh.executeDeleteRequest(ENDPOINT+"/READ_UT", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); rh.sendAdminCertificate = false; checkReadAccess(HttpStatus.SC_FORBIDDEN, "picard", "picard", "sf", "_doc", 0); - // put picard in captains role. Role opendistro_security_role_captains uses the CRUD_UT // action group // which uses READ_UT and WRITE action groups. We removed READ_UT, so only @@ -125,24 +136,25 @@ public void testActionGroupsApi() throws Exception { rh.sendAdminCertificate = false; checkWriteAccess(HttpStatus.SC_FORBIDDEN, "picard", "picard", "sf", "_doc", 0); checkReadAccess(HttpStatus.SC_FORBIDDEN, "picard", "picard", "sf", "_doc", 0); + } + void verifyPutForSuperAdmin(final Header[] header, final boolean userAdminCert) throws Exception { // -- PUT - // put with empty payload, must fail - rh.sendAdminCertificate = true; - response = rh.executePutRequest(ENDPOINT+"/SOMEGROUP", "", new Header[0]); + rh.sendAdminCertificate = userAdminCert; + HttpResponse response = rh.executePutRequest(ENDPOINT+"/SOMEGROUP", "", header); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); + Settings settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); Assert.assertEquals(AbstractConfigurationValidator.ErrorType.PAYLOAD_MANDATORY.getMessage(), settings.get("reason")); // put new configuration with invalid payload, must fail response = rh.executePutRequest(ENDPOINT+"/SOMEGROUP", FileHelper.loadFile("restapi/actiongroup_not_parseable.json"), - new Header[0]); + header); settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); Assert.assertEquals(AbstractConfigurationValidator.ErrorType.BODY_NOT_PARSEABLE.getMessage(), settings.get("reason")); - response = rh.executePutRequest(ENDPOINT+"/CRUD_UT", FileHelper.loadFile("restapi/actiongroup_crud.json"), new Header[0]); + response = rh.executePutRequest(ENDPOINT+"/CRUD_UT", FileHelper.loadFile("restapi/actiongroup_crud.json"), header); Assert.assertEquals(HttpStatus.SC_CREATED, response.getStatusCode()); rh.sendAdminCertificate = false; @@ -152,8 +164,8 @@ public void testActionGroupsApi() throws Exception { checkWriteAccess(HttpStatus.SC_OK, "picard", "picard", "sf", "_doc", 0); // restore READ_UT action groups - rh.sendAdminCertificate = true; - response = rh.executePutRequest(ENDPOINT+"/READ_UT", FileHelper.loadFile("restapi/actiongroup_read.json"), new Header[0]); + rh.sendAdminCertificate = userAdminCert; + response = rh.executePutRequest(ENDPOINT+"/READ_UT", FileHelper.loadFile("restapi/actiongroup_read.json"), header); Assert.assertEquals(HttpStatus.SC_CREATED, response.getStatusCode()); rh.sendAdminCertificate = false; @@ -162,99 +174,106 @@ public void testActionGroupsApi() throws Exception { checkWriteAccess(HttpStatus.SC_OK, "picard", "picard", "sf", "_doc", 0); // -- PUT, new JSON format including readonly flag, disallowed in REST API - rh.sendAdminCertificate = true; - response = rh.executePutRequest(ENDPOINT+"/CRUD_UT", FileHelper.loadFile("restapi/actiongroup_readonly.json"), new Header[0]); + rh.sendAdminCertificate = userAdminCert; + response = rh.executePutRequest(ENDPOINT+"/CRUD_UT", FileHelper.loadFile("restapi/actiongroup_readonly.json"), header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); // -- DELETE read only resource, must be forbidden // superAdmin can delete read only resource - rh.sendAdminCertificate = true; - response = rh.executeDeleteRequest(ENDPOINT+"/GET_UT", new Header[0]); + rh.sendAdminCertificate = userAdminCert; + response = rh.executeDeleteRequest(ENDPOINT+"/GET_UT", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); // -- PUT read only resource, must be forbidden // superAdmin can add/update read only resource - rh.sendAdminCertificate = true; - response = rh.executePutRequest(ENDPOINT+"/GET_UT", FileHelper.loadFile("restapi/actiongroup_read.json"), new Header[0]); + rh.sendAdminCertificate = userAdminCert; + response = rh.executePutRequest(ENDPOINT+"/GET_UT", FileHelper.loadFile("restapi/actiongroup_read.json"), header); Assert.assertEquals(HttpStatus.SC_CREATED, response.getStatusCode()); Assert.assertFalse(response.getBody().contains("Resource 'GET_UT' is read-only.")); // PUT with role name - rh.sendAdminCertificate = true; - response = rh.executePutRequest(ENDPOINT+"/kibana_user", FileHelper.loadFile("restapi/actiongroup_read.json"), new Header[0]); + rh.sendAdminCertificate = userAdminCert; + response = rh.executePutRequest(ENDPOINT+"/kibana_user", FileHelper.loadFile("restapi/actiongroup_read.json"), header); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); Assert.assertTrue(response.getBody().contains("kibana_user is an existing role. A action group cannot be named with an existing role name.")); // PUT with self-referencing action groups - rh.sendAdminCertificate = true; - response = rh.executePutRequest(ENDPOINT+"/reference_itself", "{\"allowed_actions\": [\"reference_itself\"]}", new Header[0]); + rh.sendAdminCertificate = userAdminCert; + response = rh.executePutRequest(ENDPOINT+"/reference_itself", "{\"allowed_actions\": [\"reference_itself\"]}", header); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); Assert.assertTrue(response.getBody().contains("reference_itself cannot be an allowed_action of itself")); // -- GET_UT hidden resource, must be 404 but super admin can find it - rh.sendAdminCertificate = true; - response = rh.executeGetRequest(ENDPOINT+"/INTERNAL", new Header[0]); + rh.sendAdminCertificate = userAdminCert; + response = rh.executeGetRequest(ENDPOINT+"/INTERNAL", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); Assert.assertTrue(response.getBody().contains("\"hidden\":true")); // -- DELETE hidden resource, must be 404 - rh.sendAdminCertificate = true; - response = rh.executeDeleteRequest(ENDPOINT+"/INTERNAL", new Header[0]); + rh.sendAdminCertificate = userAdminCert; + response = rh.executeDeleteRequest(ENDPOINT+"/INTERNAL", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); Assert.assertTrue(response.getBody().contains("'INTERNAL' deleted.")); // -- PUT hidden resource, must be forbidden - rh.sendAdminCertificate = true; - response = rh.executePutRequest(ENDPOINT+"/INTERNAL", FileHelper.loadFile("restapi/actiongroup_read.json"), new Header[0]); + rh.sendAdminCertificate = userAdminCert; + response = rh.executePutRequest(ENDPOINT+"/INTERNAL", FileHelper.loadFile("restapi/actiongroup_read.json"), header); Assert.assertEquals(HttpStatus.SC_CREATED, response.getStatusCode()); + } + void verifyPatchForSuperAdmin(final Header[] header, final boolean userAdminCert) throws Exception { // -- PATCH // PATCH on non-existing resource - rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT+"/imnothere", "[{ \"op\": \"add\", \"path\": \"/a/b/c\", \"value\": [ \"foo\", \"bar\" ] }]", new Header[0]); + rh.sendAdminCertificate = userAdminCert; + HttpResponse response = rh.executePatchRequest(ENDPOINT+"/imnothere", + "[{ \"op\": \"add\", \"path\": \"/a/b/c\", \"value\": [ \"foo\", \"bar\" ] }]", header); Assert.assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatusCode()); // PATCH read only resource, must be forbidden // SuperAdmin can patch read only resource - rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT+"/GET_UT", "[{ \"op\": \"add\", \"path\": \"/description\", \"value\": \"foo\" }]", new Header[0]); + rh.sendAdminCertificate = userAdminCert; + response = rh.executePatchRequest(ENDPOINT+"/GET_UT", "[{ \"op\": \"add\", \"path\": \"/description\", \"value\": \"foo\" }]", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); // PATCH with self-referencing action groups - rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT+"/GET_UT", "[{ \"op\": \"add\", \"path\": \"/allowed_actions/-\", \"value\": \"GET_UT\" }]", new Header[0]); + rh.sendAdminCertificate = userAdminCert; + response = rh.executePatchRequest(ENDPOINT+"/GET_UT", "[{ \"op\": \"add\", \"path\": \"/allowed_actions/-\", \"value\": \"GET_UT\" }]", header); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); Assert.assertTrue(response.getBody().contains("GET_UT cannot be an allowed_action of itself")); // bulk PATCH with self-referencing action groups - response = rh.executePatchRequest(ENDPOINT, "[{ \"op\": \"add\", \"path\": \"/BULKNEW1\", \"value\": {\"allowed_actions\": [\"BULKNEW1\"] } }," + "{ \"op\": \"add\", \"path\": \"/BULKNEW2\", \"value\": {\"allowed_actions\": [\"READ_UT\"] } }]", new Header[0]); + response = rh.executePatchRequest(ENDPOINT, "[{ \"op\": \"add\", \"path\": \"/BULKNEW1\", \"value\": {\"allowed_actions\": [\"BULKNEW1\"] } }," + + "{ \"op\": \"add\", \"path\": \"/BULKNEW2\", \"value\": {\"allowed_actions\": [\"READ_UT\"] } }]", header); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); Assert.assertTrue(response.getBody().contains("BULKNEW1 cannot be an allowed_action of itself")); // PATCH hidden resource, must be not found, can be found by superadmin, but fails with no path exist error - rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT+"/INTERNAL", "[{ \"op\": \"add\", \"path\": \"/a/b/c\", \"value\": [ \"foo\", \"bar\" ] }]", new Header[0]); + rh.sendAdminCertificate = userAdminCert; + response = rh.executePatchRequest(ENDPOINT+"/INTERNAL", + "[{ \"op\": \"add\", \"path\": \"/a/b/c\", \"value\": [ \"foo\", \"bar\" ] }]", header); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); // PATCH value of hidden flag, must fail with validation error - rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT+"/CRUD_UT", "[{ \"op\": \"add\", \"path\": \"/hidden\", \"value\": true }]", new Header[0]); + rh.sendAdminCertificate = userAdminCert; + response = rh.executePatchRequest(ENDPOINT+"/CRUD_UT", "[{ \"op\": \"add\", \"path\": \"/hidden\", \"value\": true }]", header); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); Assert.assertTrue(response.getBody(), response.getBody().matches(".*\"invalid_keys\"\\s*:\\s*\\{\\s*\"keys\"\\s*:\\s*\"hidden\"\\s*\\}.*")); // PATCH with relative JSON pointer, must fail - rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT+"/CRUD_UT", "[{ \"op\": \"add\", \"path\": \"1/INTERNAL/allowed_actions/-\", \"value\": \"OPENDISTRO_SECURITY_DELETE\" }]", new Header[0]); + rh.sendAdminCertificate = userAdminCert; + response = rh.executePatchRequest(ENDPOINT+"/CRUD_UT", "[{ \"op\": \"add\", \"path\": \"1/INTERNAL/allowed_actions/-\", " + + "\"value\": \"OPENDISTRO_SECURITY_DELETE\" }]", header); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); // PATCH new format - rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT+"/CRUD_UT", "[{ \"op\": \"add\", \"path\": \"/allowed_actions/-\", \"value\": \"OPENDISTRO_SECURITY_DELETE\" }]", new Header[0]); + rh.sendAdminCertificate = userAdminCert; + response = rh.executePatchRequest(ENDPOINT+"/CRUD_UT", "[{ \"op\": \"add\", \"path\": \"/allowed_actions/-\", " + + "\"value\": \"OPENDISTRO_SECURITY_DELETE\" }]", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - response = rh.executeGetRequest(ENDPOINT+"/CRUD_UT", new Header[0]); + response = rh.executeGetRequest(ENDPOINT+"/CRUD_UT", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); - permissions = settings.getAsList("CRUD_UT.allowed_actions"); + Settings settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); + List permissions = settings.getAsList("CRUD_UT.allowed_actions"); Assert.assertNotNull(permissions); Assert.assertEquals(3, permissions.size()); Assert.assertTrue(permissions.contains("READ_UT")); @@ -265,49 +284,50 @@ public void testActionGroupsApi() throws Exception { // -- PATCH on whole config resource // PATCH read only resource, must be forbidden // SuperAdmin can patch read only resource - rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT, "[{ \"op\": \"add\", \"path\": \"/GET_UT/a\", \"value\": [ \"foo\", \"bar\" ] }]", new Header[0]); + rh.sendAdminCertificate = userAdminCert; + response = rh.executePatchRequest(ENDPOINT, "[{ \"op\": \"add\", \"path\": \"/GET_UT/a\", \"value\": [ \"foo\", \"bar\" ] }]", header); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT, "[{ \"op\": \"add\", \"path\": \"/GET_UT/description\", \"value\": \"foo\" }]", new Header[0]); + rh.sendAdminCertificate = userAdminCert; + response = rh.executePatchRequest(ENDPOINT, "[{ \"op\": \"add\", \"path\": \"/GET_UT/description\", \"value\": \"foo\" }]", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); // PATCH hidden resource, must be bad request - rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT, "[{ \"op\": \"add\", \"path\": \"/INTERNAL/a\", \"value\": [ \"foo\", \"bar\" ] }]", new Header[0]); + rh.sendAdminCertificate = userAdminCert; + response = rh.executePatchRequest(ENDPOINT, "[{ \"op\": \"add\", \"path\": \"/INTERNAL/a\", \"value\": [ \"foo\", \"bar\" ] }]", header); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); // PATCH delete read only resource, must be forbidden // SuperAdmin can delete read only resource - rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT, "[{ \"op\": \"remove\", \"path\": \"/GET_UT\" }]", new Header[0]); + rh.sendAdminCertificate = userAdminCert; + response = rh.executePatchRequest(ENDPOINT, "[{ \"op\": \"remove\", \"path\": \"/GET_UT\" }]", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); // PATCH delete hidden resource, must be bad request - rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT, "[{ \"op\": \"remove\", \"path\": \"/INTERNAL\" }]", new Header[0]); + rh.sendAdminCertificate = userAdminCert; + response = rh.executePatchRequest(ENDPOINT, "[{ \"op\": \"remove\", \"path\": \"/INTERNAL\" }]", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); Assert.assertTrue(response.getBody().contains("\"message\":\"Resource updated.")); // PATCH value of hidden flag, must fail with validation error - rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT, "[{ \"op\": \"add\", \"path\": \"/CRUD_UT/hidden\", \"value\": true }]", new Header[0]); + rh.sendAdminCertificate = userAdminCert; + response = rh.executePatchRequest(ENDPOINT, "[{ \"op\": \"add\", \"path\": \"/CRUD_UT/hidden\", \"value\": true }]", header); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); Assert.assertTrue(response.getBody().matches(".*\"invalid_keys\"\\s*:\\s*\\{\\s*\"keys\"\\s*:\\s*\"hidden\"\\s*\\}.*")); // add new resource with hidden flag, must fail with validation error - rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT, "[{ \"op\": \"add\", \"path\": \"/NEWNEWNEW\", \"value\": {\"allowed_actions\": [\"indices:data/write*\"], \"hidden\":true }}]", new Header[0]); + rh.sendAdminCertificate = userAdminCert; + response = rh.executePatchRequest(ENDPOINT, + "[{ \"op\": \"add\", \"path\": \"/NEWNEWNEW\", \"value\": {\"allowed_actions\": [\"indices:data/write*\"], \"hidden\":true }}]", header); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); Assert.assertTrue(response.getBody().matches(".*\"invalid_keys\"\\s*:\\s*\\{\\s*\"keys\"\\s*:\\s*\"hidden\"\\s*\\}.*")); // add new valid resources - rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT, "[{ \"op\": \"add\", \"path\": \"/BULKNEW1\", \"value\": {\"allowed_actions\": [\"indices:data/*\", \"cluster:monitor/*\"] } }," + "{ \"op\": \"add\", \"path\": \"/BULKNEW2\", \"value\": {\"allowed_actions\": [\"READ_UT\"] } }]", new Header[0]); + rh.sendAdminCertificate = userAdminCert; + response = rh.executePatchRequest(ENDPOINT, "[{ \"op\": \"add\", \"path\": \"/BULKNEW1\", \"value\": {\"allowed_actions\": [\"indices:data/*\", \"cluster:monitor/*\"] } }," + "{ \"op\": \"add\", \"path\": \"/BULKNEW2\", \"value\": {\"allowed_actions\": [\"READ_UT\"] } }]", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - response = rh.executeGetRequest(ENDPOINT+"/BULKNEW1", new Header[0]); + response = rh.executeGetRequest(ENDPOINT+"/BULKNEW1", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); permissions = settings.getAsList("BULKNEW1.allowed_actions"); @@ -316,7 +336,7 @@ public void testActionGroupsApi() throws Exception { Assert.assertTrue(permissions.contains("indices:data/*")); Assert.assertTrue(permissions.contains("cluster:monitor/*")); - response = rh.executeGetRequest(ENDPOINT+"/BULKNEW2", new Header[0]); + response = rh.executeGetRequest(ENDPOINT+"/BULKNEW2", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); permissions = settings.getAsList("BULKNEW2.allowed_actions"); @@ -325,13 +345,13 @@ public void testActionGroupsApi() throws Exception { Assert.assertTrue(permissions.contains("READ_UT")); // delete resource - response = rh.executePatchRequest(ENDPOINT, "[{ \"op\": \"remove\", \"path\": \"/BULKNEW1\" }]", new Header[0]); + response = rh.executePatchRequest(ENDPOINT, "[{ \"op\": \"remove\", \"path\": \"/BULKNEW1\" }]", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - response = rh.executeGetRequest(ENDPOINT+"/BULKNEW1", new Header[0]); + response = rh.executeGetRequest(ENDPOINT+"/BULKNEW1", header); Assert.assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatusCode()); // assert other resource is still there - response = rh.executeGetRequest(ENDPOINT+"/BULKNEW2", new Header[0]); + response = rh.executeGetRequest(ENDPOINT+"/BULKNEW2", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); permissions = settings.getAsList("BULKNEW2.allowed_actions"); @@ -340,6 +360,85 @@ public void testActionGroupsApi() throws Exception { Assert.assertTrue(permissions.contains("READ_UT")); } + @Test + public void testActionGroupsApiForRestAdmin() throws Exception { + setupWithRestRoles(); + rh.sendAdminCertificate = false; + // create index + setupStarfleetIndex(); + final Header restApiAdminHeader = encodeBasicHeader("rest_api_admin_user", "rest_api_admin_user"); + + // add user picard, role starfleet, maps to opendistro_security_role_starfleet + addUserWithPassword("picard", "picard", new String[] { "starfleet" }, HttpStatus.SC_CREATED); + checkReadAccess(HttpStatus.SC_OK, "picard", "picard", "sf", "_doc", 0); + checkWriteAccess(HttpStatus.SC_FORBIDDEN, "picard", "picard", "sf", "_doc", 0); + verifyGetForSuperAdmin(new Header[] {restApiAdminHeader}); + verifyDeleteForSuperAdmin(new Header[]{restApiAdminHeader}, false); + verifyPutForSuperAdmin(new Header[]{restApiAdminHeader}, false); + verifyPatchForSuperAdmin(new Header[]{restApiAdminHeader}, false); + } + + @Test + public void testActionGroupsApiForActionGroupsRestApiAdmin() throws Exception { + setupWithRestRoles(); + rh.sendAdminCertificate = false; + // create index + setupStarfleetIndex(); + final Header restApiAdminActionGroupsHeader = encodeBasicHeader("rest_api_admin_actiongroups", "rest_api_admin_actiongroups"); + + // add user picard, role starfleet, maps to opendistro_security_role_starfleet + addUserWithPassword("picard", "picard", new String[] { "starfleet" }, HttpStatus.SC_CREATED); + checkReadAccess(HttpStatus.SC_OK, "picard", "picard", "sf", "_doc", 0); + checkWriteAccess(HttpStatus.SC_FORBIDDEN, "picard", "picard", "sf", "_doc", 0); + verifyGetForSuperAdmin(new Header[] {restApiAdminActionGroupsHeader}); + verifyDeleteForSuperAdmin(new Header[]{restApiAdminActionGroupsHeader}, false); + verifyPutForSuperAdmin(new Header[]{restApiAdminActionGroupsHeader}, false); + verifyPatchForSuperAdmin(new Header[]{restApiAdminActionGroupsHeader}, false); + } + + @Test + public void testCreateActionGroupWithRestAdminPermissionsForbidden() throws Exception { + setupWithRestRoles(); + rh.sendAdminCertificate = false; + final Header restApiAdminHeader = encodeBasicHeader("rest_api_admin_user", "rest_api_admin_user"); + final Header restApiAdminActionGroupsHeader = encodeBasicHeader("rest_api_admin_actiongroups", "rest_api_admin_actiongroups"); + final Header restApiHeader = encodeBasicHeader("test", "test"); + + HttpResponse response = rh.executePutRequest(ENDPOINT + "/rest_api_admin_group", restAdminAllowedActions(), + restApiAdminHeader); + Assert.assertEquals(response.getBody(), HttpStatus.SC_FORBIDDEN, response.getStatusCode()); + response = rh.executePutRequest(ENDPOINT + "/rest_api_admin_group", restAdminAllowedActions(), restApiAdminActionGroupsHeader); + Assert.assertEquals(response.getBody(), HttpStatus.SC_FORBIDDEN, response.getStatusCode()); + response = rh.executePutRequest(ENDPOINT + "/rest_api_admin_group", restAdminAllowedActions(), restApiHeader); + Assert.assertEquals(response.getBody(), HttpStatus.SC_FORBIDDEN, response.getStatusCode()); + + response = rh.executePatchRequest(ENDPOINT, restAdminPatchBody(), restApiAdminHeader); + Assert.assertEquals(response.getBody(), HttpStatus.SC_FORBIDDEN, response.getStatusCode()); + response = rh.executePatchRequest(ENDPOINT, restAdminPatchBody(), restApiAdminActionGroupsHeader); + Assert.assertEquals(response.getBody(), HttpStatus.SC_FORBIDDEN, response.getStatusCode()); + response = rh.executePatchRequest(ENDPOINT, restAdminPatchBody(), restApiHeader); + Assert.assertEquals(response.getBody(), HttpStatus.SC_FORBIDDEN, response.getStatusCode()); + } + + String restAdminAllowedActions() throws JsonProcessingException { + final ObjectNode rootNode = DefaultObjectMapper.objectMapper.createObjectNode(); + rootNode.set("allowed_actions", clusterPermissionsForRestAdmin("cluster/*")); + return DefaultObjectMapper.objectMapper.writeValueAsString(rootNode); + } + + String restAdminPatchBody() throws JsonProcessingException { + final ArrayNode rootNode = DefaultObjectMapper.objectMapper.createArrayNode(); + final ObjectNode opAddRootNode = DefaultObjectMapper.objectMapper.createObjectNode(); + final ObjectNode allowedActionsNode = DefaultObjectMapper.objectMapper.createObjectNode(); + allowedActionsNode.set("allowed_actions", clusterPermissionsForRestAdmin("cluster/*")); + opAddRootNode + .put("op", "add") + .put("path", "/rest_api_admin_group") + .set("value", allowedActionsNode); + rootNode.add(opAddRootNode); + return DefaultObjectMapper.objectMapper.writeValueAsString(rootNode); + } + @Test public void testActionGroupsApiForNonSuperAdmin() throws Exception { 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 index 1c97d138da..a4a121bc06 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/AllowlistApiTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/AllowlistApiTest.java @@ -142,7 +142,7 @@ public void testPayloadMandatory() throws Exception { */ @Test public void testAllowlistApi() throws Exception { - setupWithRestRoles(null); + setupWithRestRoles(); // No creds, no admin certificate - UNAUTHORIZED checkGetAndPutAllowlistPermissions(HttpStatus.SC_UNAUTHORIZED, false); @@ -156,6 +156,29 @@ public void testAllowlistApi() throws Exception { checkGetAndPutAllowlistPermissions(HttpStatus.SC_OK, true, nonAdminCredsHeader); } + @Test + public void testAllowlistApiWithPermissions() throws Exception { + setupWithRestRoles(); + + final Header restApiAdminHeader = encodeBasicHeader("rest_api_admin_user", "rest_api_admin_user"); + final Header restApiAllowlistHeader = encodeBasicHeader("rest_api_admin_allowlist", "rest_api_admin_allowlist"); + final Header restApiUserHeader = encodeBasicHeader("test", "test"); + + checkGetAndPutAllowlistPermissions(HttpStatus.SC_FORBIDDEN, false, restApiUserHeader); + checkGetAndPutAllowlistPermissions(HttpStatus.SC_OK, false, restApiAdminHeader); + } + + @Test + public void testAllowlistApiWithAllowListPermissions() throws Exception { + setupWithRestRoles(); + + final Header restApiAllowlistHeader = encodeBasicHeader("rest_api_admin_allowlist", "rest_api_admin_allowlist"); + final Header restApiUserHeader = encodeBasicHeader("test", "test"); + + checkGetAndPutAllowlistPermissions(HttpStatus.SC_FORBIDDEN, false, restApiUserHeader); + checkGetAndPutAllowlistPermissions(HttpStatus.SC_OK, false, restApiAllowlistHeader); + } + @Test public void testAllowlistAuditComplianceLogging() throws Exception { Settings settings = Settings.builder() diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/NodesDnApiTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/NodesDnApiTest.java index f72375600c..ca73e7b527 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/NodesDnApiTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/NodesDnApiTest.java @@ -181,6 +181,85 @@ public void testNodesDnApi() throws Exception { } } + + @Test + public void testNodesDnApiWithPermissions() throws Exception { + Settings settings = Settings.builder().put(ConfigConstants.SECURITY_NODES_DN_DYNAMIC_CONFIG_ENABLED, true) + .build(); + setupWithRestRoles(settings); + final Header restApiAdminHeader = encodeBasicHeader("rest_api_admin_user", "rest_api_admin_user"); + final Header restApiNodesDnHeader = encodeBasicHeader("rest_api_admin_nodesdn", "rest_api_admin_nodesdn"); + final Header restApiUserHeader = encodeBasicHeader("test", "test"); + //full access admin + { + rh.sendAdminCertificate = false; + response = rh.executeGetRequest( + ENDPOINT + "/nodesdn", restApiAdminHeader); + assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_OK)); + + response = rh.executePutRequest( + ENDPOINT + "/nodesdn/c1", "{\"nodes_dn\": [\"cn=popeye\"]}", + restApiAdminHeader); + assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_CREATED)); + + response = rh.executePatchRequest( + ENDPOINT + "/nodesdn/c1", + "[{ \"op\": \"add\", \"path\": \"/nodes_dn/-\", \"value\": \"bluto\" }]", + restApiAdminHeader + ); + assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_OK)); + + response = rh.executeDeleteRequest(ENDPOINT + "/nodesdn/c1", restApiAdminHeader); + assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_OK)); + } + //NodesDN only + { + rh.sendAdminCertificate = false; + response = rh.executeGetRequest(ENDPOINT + "/nodesdn", restApiNodesDnHeader); + assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_OK)); + + response = rh.executePutRequest( + ENDPOINT + "/nodesdn/c1", "{\"nodes_dn\": [\"cn=popeye\"]}", + restApiNodesDnHeader); + assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_CREATED)); + + response = rh.executePatchRequest( + ENDPOINT + "/nodesdn/c1", + "[{ \"op\": \"add\", \"path\": \"/nodes_dn/-\", \"value\": \"bluto\" }]", + restApiNodesDnHeader + ); + assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_OK)); + + response = rh.executeDeleteRequest(ENDPOINT + "/nodesdn/c1", restApiNodesDnHeader); + assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_OK)); + + response = rh.executeGetRequest(ENDPOINT + "/actiongroups", restApiNodesDnHeader); + assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_OK)); + } + //rest api user + { + rh.sendAdminCertificate = false; + response = rh.executeGetRequest(ENDPOINT + "/nodesdn", restApiUserHeader); + assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN)); + + response = rh.executePutRequest( + ENDPOINT + "/nodesdn/c1", "{\"nodes_dn\": [\"cn=popeye\"]}", + restApiUserHeader); + assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN)); + + response = rh.executePatchRequest( + ENDPOINT + "/nodesdn/c1", + "[{ \"op\": \"add\", \"path\": \"/nodes_dn/-\", \"value\": \"bluto\" }]", + restApiUserHeader + ); + assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN)); + + response = rh.executeDeleteRequest(ENDPOINT + "/nodesdn/c1", restApiUserHeader); + assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN)); + } + + } + @Test public void testNodesDnApiAuditComplianceLogging() throws Exception { Settings settings = Settings.builder().put(ConfigConstants.SECURITY_NODES_DN_DYNAMIC_CONFIG_ENABLED, true) diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/RolesApiTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/RolesApiTest.java index 8dc18f5043..cfb6b38145 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/RolesApiTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/RolesApiTest.java @@ -13,7 +13,10 @@ import java.util.List; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import org.apache.http.Header; import org.apache.http.HttpStatus; import org.junit.Assert; @@ -30,12 +33,13 @@ import static org.opensearch.security.OpenSearchSecurityPlugin.PLUGINS_PREFIX; public class RolesApiTest extends AbstractRestApiUnitTest { - private final String ENDPOINT; + private final String ENDPOINT; + protected String getEndpointPrefix() { return PLUGINS_PREFIX; } - public RolesApiTest(){ + public RolesApiTest() { ENDPOINT = getEndpointPrefix() + "/api"; } @@ -67,7 +71,26 @@ public void testAllRolesForSuperAdmin() throws Exception { rh.keystore = "restapi/kirk-keystore.jks"; rh.sendAdminCertificate = true; - HttpResponse response = rh.executeGetRequest(ENDPOINT + "/roles"); + + checkSuperAdminRoles(new Header[0]); + } + + @Test + public void testAllRolesForRestAdmin() throws Exception { + setupWithRestRoles(); + final Header restApiAdminHeader = encodeBasicHeader("rest_api_admin_user", "rest_api_admin_user"); + checkSuperAdminRoles(new Header[]{restApiAdminHeader}); + } + + @Test + public void testAllRolesForRolesRestAdmin() throws Exception { + setupWithRestRoles(); + final Header restApiAdminRolesHeader = encodeBasicHeader("rest_api_admin_roles", "rest_api_admin_roles"); + checkSuperAdminRoles(new Header[]{restApiAdminRolesHeader}); + } + + void checkSuperAdminRoles(final Header[] header) { + HttpResponse response = rh.executeGetRequest(ENDPOINT + "/roles", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); Assert.assertFalse(response.getBody().contains("_meta")); @@ -121,103 +144,110 @@ public void testRolesApi() throws Exception { rh.keystore = "restapi/kirk-keystore.jks"; rh.sendAdminCertificate = true; + // create index + setupStarfleetIndex(); + + // add user picard, role starfleet, maps to opendistro_security_role_starfleet + addUserWithPassword("picard", "picard", new String[]{"starfleet", "captains"}, HttpStatus.SC_CREATED); + checkReadAccess(HttpStatus.SC_OK, "picard", "picard", "sf", "_doc", 0); + checkWriteAccess(HttpStatus.SC_OK, "picard", "picard", "sf", "_doc", 0); + + rh.sendAdminCertificate = true; + verifyGetForSuperAdmin(new Header[0]); + rh.sendAdminCertificate = true; + verifyDeleteForSuperAdmin(new Header[0], true); + rh.sendAdminCertificate = true; + verifyPutForSuperAdmin(new Header[0], true); + rh.sendAdminCertificate = true; + verifyPatchForSuperAdmin(new Header[0], true); + } + + void verifyGetForSuperAdmin(final Header[] header) throws Exception { // check roles exists - HttpResponse response = rh.executeGetRequest(ENDPOINT + "/roles"); + HttpResponse response = rh.executeGetRequest(ENDPOINT + "/roles", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); // -- GET - // GET opendistro_security_role_starfleet - response = rh.executeGetRequest(ENDPOINT + "/roles/opendistro_security_role_starfleet", new Header[0]); + response = rh.executeGetRequest(ENDPOINT + "/roles/opendistro_security_role_starfleet", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); JsonNode settings = DefaultObjectMapper.readTree(response.getBody()); Assert.assertEquals(1, settings.size()); // GET, role does not exist - response = rh.executeGetRequest(ENDPOINT + "/roles/nothinghthere", new Header[0]); + response = rh.executeGetRequest(ENDPOINT + "/roles/nothinghthere", header); Assert.assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatusCode()); - response = rh.executeGetRequest(ENDPOINT + "/roles/", new Header[0]); + response = rh.executeGetRequest(ENDPOINT + "/roles/", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - response = rh.executeGetRequest(ENDPOINT + "/roles", new Header[0]); + response = rh.executeGetRequest(ENDPOINT + "/roles", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); Assert.assertTrue(response.getBody().contains("\"cluster_permissions\":[\"*\"]")); Assert.assertFalse(response.getBody().contains("\"cluster_permissions\" : [")); - response = rh.executeGetRequest(ENDPOINT + "/roles?pretty", new Header[0]); + response = rh.executeGetRequest(ENDPOINT + "/roles?pretty", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); Assert.assertFalse(response.getBody().contains("\"cluster_permissions\":[\"*\"]")); Assert.assertTrue(response.getBody().contains("\"cluster_permissions\" : [")); // Super admin should be able to describe hidden role - response = rh.executeGetRequest(ENDPOINT + "/roles/opendistro_security_hidden", new Header[0]); + response = rh.executeGetRequest(ENDPOINT + "/roles/opendistro_security_hidden", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); Assert.assertTrue(response.getBody().contains("\"hidden\":true")); + } - // create index - setupStarfleetIndex(); - - // add user picard, role starfleet, maps to opendistro_security_role_starfleet - addUserWithPassword("picard", "picard", new String[] { "starfleet", "captains" }, HttpStatus.SC_CREATED); - checkReadAccess(HttpStatus.SC_OK, "picard", "picard", "sf", "_doc", 0); - checkWriteAccess(HttpStatus.SC_OK, "picard", "picard", "sf", "_doc", 0); - - + void verifyDeleteForSuperAdmin(final Header[] header, final boolean sendAdminCert) throws Exception { // -- DELETE - - rh.sendAdminCertificate = true; - // Non-existing role - response = rh.executeDeleteRequest(ENDPOINT + "/roles/idonotexist", new Header[0]); + HttpResponse response = rh.executeDeleteRequest(ENDPOINT + "/roles/idonotexist", header); Assert.assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatusCode()); // read only role, SuperAdmin can delete the read-only role - response = rh.executeDeleteRequest(ENDPOINT + "/roles/opendistro_security_transport_client", new Header[0]); + response = rh.executeDeleteRequest(ENDPOINT + "/roles/opendistro_security_transport_client", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); // hidden role allowed for superadmin - response = rh.executeDeleteRequest(ENDPOINT + "/roles/opendistro_security_internal", new Header[0]); + response = rh.executeDeleteRequest(ENDPOINT + "/roles/opendistro_security_internal", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); Assert.assertTrue(response.getBody().contains("'opendistro_security_internal' deleted.")); // remove complete role mapping for opendistro_security_role_starfleet_captains - response = rh.executeDeleteRequest(ENDPOINT + "/roles/opendistro_security_role_starfleet_captains", new Header[0]); + response = rh.executeDeleteRequest(ENDPOINT + "/roles/opendistro_security_role_starfleet_captains", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - rh.sendAdminCertificate = false; - // user has only role starfleet left, role has READ access only checkWriteAccess(HttpStatus.SC_FORBIDDEN, "picard", "picard", "sf", "_doc", 1); - // ES7 only supports one doc type, but OpenSearch permission checks run first // So we also get a 403 FORBIDDEN when tring to add new document type checkWriteAccess(HttpStatus.SC_FORBIDDEN, "picard", "picard", "sf", "_doc", 0); - rh.sendAdminCertificate = true; + rh.sendAdminCertificate = sendAdminCert; // remove also starfleet role, nothing is allowed anymore - response = rh.executeDeleteRequest(ENDPOINT + "/roles/opendistro_security_role_starfleet", new Header[0]); + response = rh.executeDeleteRequest(ENDPOINT + "/roles/opendistro_security_role_starfleet", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); checkReadAccess(HttpStatus.SC_FORBIDDEN, "picard", "picard", "sf", "_doc", 0); checkWriteAccess(HttpStatus.SC_FORBIDDEN, "picard", "picard", "sf", "_doc", 0); + } + void verifyPutForSuperAdmin(final Header[] header, final boolean sendAdminCert) throws Exception { // -- PUT // put with empty roles, must fail - response = rh.executePutRequest(ENDPOINT + "/roles/opendistro_security_role_starfleet", "", new Header[0]); + HttpResponse response = rh.executePutRequest(ENDPOINT + "/roles/opendistro_security_role_starfleet", "", header); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - settings = DefaultObjectMapper.readTree(response.getBody()); + JsonNode settings = DefaultObjectMapper.readTree(response.getBody()); Assert.assertEquals(AbstractConfigurationValidator.ErrorType.PAYLOAD_MANDATORY.getMessage(), settings.get("reason").asText()); // put new configuration with invalid payload, must fail response = rh.executePutRequest(ENDPOINT + "/roles/opendistro_security_role_starfleet", - FileHelper.loadFile("restapi/roles_not_parseable.json"), new Header[0]); + FileHelper.loadFile("restapi/roles_not_parseable.json"), header); settings = DefaultObjectMapper.readTree(response.getBody()); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); Assert.assertEquals(AbstractConfigurationValidator.ErrorType.BODY_NOT_PARSEABLE.getMessage(), settings.get("reason").asText()); // put new configuration with invalid keys, must fail response = rh.executePutRequest(ENDPOINT + "/roles/opendistro_security_role_starfleet", - FileHelper.loadFile("restapi/roles_invalid_keys.json"), new Header[0]); + FileHelper.loadFile("restapi/roles_invalid_keys.json"), header); settings = DefaultObjectMapper.readTree(response.getBody()); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); Assert.assertEquals(AbstractConfigurationValidator.ErrorType.INVALID_CONFIGURATION.getMessage(), settings.get("reason").asText()); @@ -227,7 +257,7 @@ public void testRolesApi() throws Exception { // put new configuration with wrong datatypes, must fail response = rh.executePutRequest(ENDPOINT + "/roles/opendistro_security_role_starfleet", - FileHelper.loadFile("restapi/roles_wrong_datatype.json"), new Header[0]); + FileHelper.loadFile("restapi/roles_wrong_datatype.json"), header); settings = DefaultObjectMapper.readTree(response.getBody()); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); Assert.assertEquals(AbstractConfigurationValidator.ErrorType.WRONG_DATATYPE.getMessage(), settings.get("reason").asText()); @@ -236,17 +266,17 @@ public void testRolesApi() throws Exception { // put read only role, must be forbidden // But SuperAdmin can still create it response = rh.executePutRequest(ENDPOINT + "/roles/opendistro_security_transport_client", - FileHelper.loadFile("restapi/roles_captains.json"), new Header[0]); + FileHelper.loadFile("restapi/roles_captains.json"), header); Assert.assertEquals(HttpStatus.SC_CREATED, response.getStatusCode()); // put hidden role, must be forbidden, but allowed for super admin response = rh.executePutRequest(ENDPOINT + "/roles/opendistro_security_internal", - FileHelper.loadFile("restapi/roles_captains.json"), new Header[0]); + FileHelper.loadFile("restapi/roles_captains.json"), header); Assert.assertEquals(HttpStatus.SC_CREATED, response.getStatusCode()); // restore starfleet role response = rh.executePutRequest(ENDPOINT + "/roles/opendistro_security_role_starfleet", - FileHelper.loadFile("restapi/roles_starfleet.json"), new Header[0]); + FileHelper.loadFile("restapi/roles_starfleet.json"), header); Assert.assertEquals(HttpStatus.SC_CREATED, response.getStatusCode()); rh.sendAdminCertificate = false; checkReadAccess(HttpStatus.SC_OK, "picard", "picard", "sf", "_doc", 0); @@ -258,41 +288,41 @@ public void testRolesApi() throws Exception { // - 'indices:*' checkWriteAccess(HttpStatus.SC_OK, "picard", "picard", "sf", "_doc", 0); - rh.sendAdminCertificate = true; + rh.sendAdminCertificate = sendAdminCert; // restore captains role response = rh.executePutRequest(ENDPOINT + "/roles/opendistro_security_role_starfleet_captains", - FileHelper.loadFile("restapi/roles_captains.json"), new Header[0]); + FileHelper.loadFile("restapi/roles_captains.json"), header); Assert.assertEquals(HttpStatus.SC_CREATED, response.getStatusCode()); rh.sendAdminCertificate = false; checkReadAccess(HttpStatus.SC_OK, "picard", "picard", "sf", "_doc", 0); checkWriteAccess(HttpStatus.SC_OK, "picard", "picard", "sf", "_doc", 0); - rh.sendAdminCertificate = true; + rh.sendAdminCertificate = sendAdminCert; response = rh.executePutRequest(ENDPOINT + "/roles/opendistro_security_role_starfleet_captains", - FileHelper.loadFile("restapi/roles_complete_invalid.json"), new Header[0]); + FileHelper.loadFile("restapi/roles_complete_invalid.json"), header); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); -// rh.sendAdminCertificate = true; +// rh.sendAdminCertificate = sendAdminCert; // response = rh.executePutRequest(ENDPOINT + "/roles/opendistro_security_role_starfleet_captains", -// FileHelper.loadFile("restapi/roles_multiple.json"), new Header[0]); +// FileHelper.loadFile("restapi/roles_multiple.json"), header); // Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); response = rh.executePutRequest(ENDPOINT + "/roles/opendistro_security_role_starfleet_captains", - FileHelper.loadFile("restapi/roles_multiple_2.json"), new Header[0]); + FileHelper.loadFile("restapi/roles_multiple_2.json"), header); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); // check tenants - rh.sendAdminCertificate = true; + rh.sendAdminCertificate = sendAdminCert; response = rh.executePutRequest(ENDPOINT + "/roles/opendistro_security_role_starfleet_captains", - FileHelper.loadFile("restapi/roles_captains_tenants.json"), new Header[0]); + FileHelper.loadFile("restapi/roles_captains_tenants.json"), header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); settings = DefaultObjectMapper.readTree(response.getBody()); Assert.assertEquals(2, settings.size()); Assert.assertEquals(settings.get("status").asText(), "OK"); - response = rh.executeGetRequest(ENDPOINT + "/roles/opendistro_security_role_starfleet_captains", new Header[0]); + response = rh.executeGetRequest(ENDPOINT + "/roles/opendistro_security_role_starfleet_captains", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); System.out.println(response.getBody()); settings = DefaultObjectMapper.readTree(response.getBody()); @@ -305,13 +335,13 @@ public void testRolesApi() throws Exception { response = rh.executePutRequest(ENDPOINT + "/roles/opendistro_security_role_starfleet_captains", - FileHelper.loadFile("restapi/roles_captains_tenants2.json"), new Header[0]); + FileHelper.loadFile("restapi/roles_captains_tenants2.json"), header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); settings = DefaultObjectMapper.readTree(response.getBody()); Assert.assertEquals(2, settings.size()); Assert.assertEquals(settings.get("status").asText(), "OK"); - response = rh.executeGetRequest(ENDPOINT + "/roles/opendistro_security_role_starfleet_captains", new Header[0]); + response = rh.executeGetRequest(ENDPOINT + "/roles/opendistro_security_role_starfleet_captains", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); settings = DefaultObjectMapper.readTree(response.getBody()); Assert.assertEquals(1, settings.size()); @@ -327,13 +357,13 @@ public void testRolesApi() throws Exception { // remove tenants from role response = rh.executePutRequest(ENDPOINT + "/roles/opendistro_security_role_starfleet_captains", - FileHelper.loadFile("restapi/roles_captains_no_tenants.json"), new Header[0]); + FileHelper.loadFile("restapi/roles_captains_no_tenants.json"), header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); settings = DefaultObjectMapper.readTree(response.getBody()); Assert.assertEquals(2, settings.size()); Assert.assertEquals(settings.get("status").asText(), "OK"); - response = rh.executeGetRequest(ENDPOINT + "/roles/opendistro_security_role_starfleet_captains", new Header[0]); + response = rh.executeGetRequest(ENDPOINT + "/roles/opendistro_security_role_starfleet_captains", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); settings = DefaultObjectMapper.readTree(response.getBody()); Assert.assertEquals(1, settings.size()); @@ -341,32 +371,46 @@ public void testRolesApi() throws Exception { Assert.assertTrue(new SecurityJsonNode(settings).getDotted("opendistro_security_role_starfleet_captains.tenant_permissions").get(0).isNull()); response = rh.executePutRequest(ENDPOINT + "/roles/opendistro_security_role_starfleet_captains", - FileHelper.loadFile("restapi/roles_captains_tenants_malformed.json"), new Header[0]); + FileHelper.loadFile("restapi/roles_captains_tenants_malformed.json"), header); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); settings = DefaultObjectMapper.readTree(response.getBody()); Assert.assertEquals(settings.get("status").asText(), "error"); Assert.assertEquals(settings.get("reason").asText(), AbstractConfigurationValidator.ErrorType.INVALID_CONFIGURATION.getMessage()); + } + void verifyPatchForSuperAdmin(final Header[] header, final boolean sendAdminCert) throws Exception { // -- PATCH // PATCH on non-existing resource - rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT + "/roles/imnothere", "[{ \"op\": \"add\", \"path\": \"/a/b/c\", \"value\": [ \"foo\", \"bar\" ] }]", new Header[0]); + rh.sendAdminCertificate = sendAdminCert; + HttpResponse response = rh.executePatchRequest( + ENDPOINT + "/roles/imnothere", + "[{ \"op\": \"add\", \"path\": \"/a/b/c\", \"value\": [ \"foo\", \"bar\" ] }]", + header); Assert.assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatusCode()); // PATCH read only resource, must be forbidden // SuperAdmin can patch it - rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT + "/roles/opendistro_security_transport_client", "[{ \"op\": \"add\", \"path\": \"/description\", \"value\": \"foo\" }]", new Header[0]); + rh.sendAdminCertificate = sendAdminCert; + response = rh.executePatchRequest( + ENDPOINT + "/roles/opendistro_security_transport_client", + "[{ \"op\": \"add\", \"path\": \"/description\", \"value\": \"foo\" }]", + header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); // PATCH hidden resource, must be not found, can be found for superadmin, but will fail with no path present exception - rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT + "/roles/opendistro_security_internal", "[{ \"op\": \"add\", \"path\": \"/a/b/c\", \"value\": [ \"foo\", \"bar\" ] }]", new Header[0]); + rh.sendAdminCertificate = sendAdminCert; + response = rh.executePatchRequest( + ENDPOINT + "/roles/opendistro_security_internal", + "[{ \"op\": \"add\", \"path\": \"/a/b/c\", \"value\": [ \"foo\", \"bar\" ] }]", + header); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); // PATCH value of hidden flag, must fail with validation error - rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT + "/roles/opendistro_security_role_starfleet", "[{ \"op\": \"add\", \"path\": \"/hidden\", \"value\": true }]", new Header[0]); + rh.sendAdminCertificate = sendAdminCert; + response = rh.executePatchRequest( + ENDPOINT + "/roles/opendistro_security_role_starfleet", + "[{ \"op\": \"add\", \"path\": \"/hidden\", \"value\": true }]", + header); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); Assert.assertTrue(response.getBody(), response.getBody().matches(".*\"invalid_keys\"\\s*:\\s*\\{\\s*\"keys\"\\s*:\\s*\"hidden\"\\s*\\}.*")); @@ -389,122 +433,328 @@ public void testRolesApi() throws Exception { // -- PATCH on whole config resource // PATCH on non-existing resource - rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT + "/roles", "[{ \"op\": \"add\", \"path\": \"/imnothere/a/b/c\", \"value\": [ \"foo\", \"bar\" ] }]", new Header[0]); + rh.sendAdminCertificate = sendAdminCert; + response = rh.executePatchRequest( + ENDPOINT + "/roles", + "[{ \"op\": \"add\", \"path\": \"/imnothere/a/b/c\", \"value\": [ \"foo\", \"bar\" ] }]", + header); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); // PATCH read only resource, must be forbidden - rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT + "/roles", "[{ \"op\": \"add\", \"path\": \"/opendistro_security_transport_client/a\", \"value\": [ \"foo\", \"bar\" ] }]", new Header[0]); + rh.sendAdminCertificate = sendAdminCert; + response = rh.executePatchRequest( + ENDPOINT + "/roles", + "[{ \"op\": \"add\", \"path\": \"/opendistro_security_transport_client/a\", \"value\": [ \"foo\", \"bar\" ] }]", + header); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); // PATCH hidden resource, must be bad request - rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT + "/roles", "[{ \"op\": \"add\", \"path\": \"/opendistro_security_internal/a\", \"value\": [ \"foo\", \"bar\" ] }]", new Header[0]); + rh.sendAdminCertificate = sendAdminCert; + response = rh.executePatchRequest( + ENDPOINT + "/roles", + "[{ \"op\": \"add\", \"path\": \"/opendistro_security_internal/a\", \"value\": [ \"foo\", \"bar\" ] }]", + header); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); // PATCH delete read only resource, must be forbidden // SuperAdmin can delete read only user - rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT + "/roles", "[{ \"op\": \"remove\", \"path\": \"/opendistro_security_transport_client\" }]", new Header[0]); + rh.sendAdminCertificate = sendAdminCert; + response = rh.executePatchRequest( + ENDPOINT + "/roles", "[{ \"op\": \"remove\", \"path\": \"/opendistro_security_transport_client\" }]", + header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); // PATCH hidden resource, must be bad request, but allowed for superadmin - rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT + "/roles", "[{ \"op\": \"remove\", \"path\": \"/opendistro_security_internal\"}]", new Header[0]); + rh.sendAdminCertificate = sendAdminCert; + response = rh.executePatchRequest( + ENDPOINT + "/roles", + "[{ \"op\": \"remove\", \"path\": \"/opendistro_security_internal\"}]", + header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); Assert.assertTrue(response.getBody().contains("\"message\":\"Resource updated.")); // PATCH value of hidden flag, must fail with validation error - rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT + "/roles", "[{ \"op\": \"add\", \"path\": \"/newnewnew\", \"value\": { \"hidden\": true, \"index_permissions\" : [ {\"index_patterns\" : [ \"sf\" ],\"allowed_actions\" : [ \"OPENDISTRO_SECURITY_READ\" ]}] }}]", new Header[0]); + rh.sendAdminCertificate = sendAdminCert; + response = rh.executePatchRequest( + ENDPOINT + "/roles", + "[{ \"op\": \"add\", \"path\": \"/newnewnew\", \"value\": { \"hidden\": true, \"index_permissions\" : " + + "[ {\"index_patterns\" : [ \"sf\" ],\"allowed_actions\" : [ \"OPENDISTRO_SECURITY_READ\" ]}] }}]", + header); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); Assert.assertTrue(response.getBody().matches(".*\"invalid_keys\"\\s*:\\s*\\{\\s*\"keys\"\\s*:\\s*\"hidden\"\\s*\\}.*")); // PATCH - rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT + "/roles", "[{ \"op\": \"add\", \"path\": \"/bulknew1\", \"value\": { \"index_permissions\" : [ {\"index_patterns\" : [ \"sf\" ],\"allowed_actions\" : [ \"OPENDISTRO_SECURITY_READ\" ]}] }}]", new Header[0]); + rh.sendAdminCertificate = sendAdminCert; + response = rh.executePatchRequest( + ENDPOINT + "/roles", + "[{ \"op\": \"add\", \"path\": \"/bulknew1\", \"value\": { \"index_permissions\" : " + + "[ {\"index_patterns\" : [ \"sf\" ],\"allowed_actions\" : [ \"OPENDISTRO_SECURITY_READ\" ]}] }}]", + header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - response = rh.executeGetRequest(ENDPOINT + "/roles/bulknew1", new Header[0]); + response = rh.executeGetRequest(ENDPOINT + "/roles/bulknew1", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - settings = DefaultObjectMapper.readTree(response.getBody()); - permissions = new SecurityJsonNode(settings).get("bulknew1").get("index_permissions").get(0).get("allowed_actions").asList(); + JsonNode settings = DefaultObjectMapper.readTree(response.getBody()); + permissions = new SecurityJsonNode(settings).get("bulknew1").get("index_permissions").get(0).get("allowed_actions").asList(); Assert.assertNotNull(permissions); Assert.assertEquals(1, permissions.size()); Assert.assertTrue(permissions.contains("OPENDISTRO_SECURITY_READ")); // delete resource - rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT + "/roles", "[{ \"op\": \"remove\", \"path\": \"/bulknew1\"}]", new Header[0]); + rh.sendAdminCertificate = sendAdminCert; + response = rh.executePatchRequest(ENDPOINT + "/roles", "[{ \"op\": \"remove\", \"path\": \"/bulknew1\"}]", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - response = rh.executeGetRequest(ENDPOINT + "/roles/bulknew1", new Header[0]); + response = rh.executeGetRequest(ENDPOINT + "/roles/bulknew1", header); Assert.assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatusCode()); // put valid field masks response = rh.executePutRequest(ENDPOINT + "/roles/opendistro_security_field_mask_valid", - FileHelper.loadFile("restapi/roles_field_masks_valid.json"), new Header[0]); + FileHelper.loadFile("restapi/roles_field_masks_valid.json"), header); Assert.assertEquals(response.getBody(), HttpStatus.SC_CREATED, response.getStatusCode()); // put invalid field masks response = rh.executePutRequest(ENDPOINT + "/roles/opendistro_security_field_mask_invalid", - FileHelper.loadFile("restapi/roles_field_masks_invalid.json"), new Header[0]); + FileHelper.loadFile("restapi/roles_field_masks_invalid.json"), header); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); + } + + @Test + public void testRolesApiWithAllRestApiPermissions() throws Exception { + setupWithRestRoles(); + final Header restApiAdminHeader = encodeBasicHeader("rest_api_admin_user", "rest_api_admin_user"); + + rh.sendAdminCertificate = false; + setupStarfleetIndex(); + + // add user picard, role starfleet, maps to opendistro_security_role_starfleet + addUserWithPassword("picard", "picard", new String[]{"starfleet", "captains"}, HttpStatus.SC_CREATED); + checkReadAccess(HttpStatus.SC_OK, "picard", "picard", "sf", "_doc", 0); + checkWriteAccess(HttpStatus.SC_OK, "picard", "picard", "sf", "_doc", 0); + + verifyGetForSuperAdmin(new Header[]{restApiAdminHeader}); + verifyDeleteForSuperAdmin(new Header[]{restApiAdminHeader}, false); + verifyPutForSuperAdmin(new Header[]{restApiAdminHeader}, false); + verifyPatchForSuperAdmin(new Header[]{restApiAdminHeader}, false); } @Test - public void testRolesApiForNonSuperAdmin() throws Exception { + public void testRolesApiWithRestApiRolePermission() throws Exception { + setupWithRestRoles(); + + final Header restApiRolesHeader = encodeBasicHeader("rest_api_admin_roles", "rest_api_admin_roles"); + + rh.sendAdminCertificate = false; + setupStarfleetIndex(); + + // add user picard, role starfleet, maps to opendistro_security_role_starfleet + addUserWithPassword("picard", "picard", new String[]{"starfleet", "captains"}, HttpStatus.SC_CREATED); + checkReadAccess(HttpStatus.SC_OK, "picard", "picard", "sf", "_doc", 0); + checkWriteAccess(HttpStatus.SC_OK, "picard", "picard", "sf", "_doc", 0); + + + verifyGetForSuperAdmin(new Header[]{restApiRolesHeader}); + verifyDeleteForSuperAdmin(new Header[]{restApiRolesHeader}, false); + verifyPutForSuperAdmin(new Header[]{restApiRolesHeader}, false); + verifyPatchForSuperAdmin(new Header[]{restApiRolesHeader}, false); + } + + @Test + public void testCreateOrUpdateRestApiAdminRoleForbiddenForNonSuperAdmin() throws Exception { + setupWithRestRoles(); + rh.sendAdminCertificate = false; + + final Header restApiAdminHeader = encodeBasicHeader("rest_api_admin_user", "rest_api_admin_user"); + final Header adminHeader = encodeBasicHeader("admin", "admin"); + final Header restApiHeader = encodeBasicHeader("test", "test"); + + final String restAdminPermissionsPayload = createRestAdminPermissionsPayload("cluster/*"); + HttpResponse response = rh.executePutRequest( + ENDPOINT + "/roles/new_rest_admin_role", restAdminPermissionsPayload, restApiAdminHeader); + Assert.assertEquals(HttpStatus.SC_CREATED, response.getStatusCode()); + response = rh.executePutRequest( + ENDPOINT + "/roles/rest_admin_role_to_delete", restAdminPermissionsPayload, restApiAdminHeader); + Assert.assertEquals(HttpStatus.SC_CREATED, response.getStatusCode()); + + // attempt to create a new rest admin role by admin + response = rh.executePutRequest( + ENDPOINT + "/roles/some_rest_admin_role", + restAdminPermissionsPayload, + adminHeader); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); + + // attempt to update exiting admin role + response = rh.executePutRequest( + ENDPOINT + "/roles/new_rest_admin_role", + restAdminPermissionsPayload, + adminHeader); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); + + // attempt to patch exiting admin role + response = rh.executePatchRequest( + ENDPOINT + "/roles/new_rest_admin_role", + createPatchRestAdminPermissionsPayload("replace"), + adminHeader); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); + + // attempt to update exiting admin role + response = rh.executePutRequest( + ENDPOINT + "/roles/new_rest_admin_role", + restAdminPermissionsPayload, + restApiHeader); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); + + // attempt to create a new rest admin role by admin + response = rh.executePutRequest( + ENDPOINT + "/roles/some_rest_admin_role", + restAdminPermissionsPayload, + restApiHeader); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); + + // attempt to patch exiting admin role and crate a new one + response = rh.executePatchRequest( + ENDPOINT + "/roles", + createPatchRestAdminPermissionsPayload("replace"), + restApiHeader); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); + + response = rh.executePatchRequest( + ENDPOINT + "/roles", + createPatchRestAdminPermissionsPayload("add"), + restApiHeader); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); + response = rh.executePatchRequest( + ENDPOINT + "/roles", + createPatchRestAdminPermissionsPayload("remove"), + restApiHeader); + System.out.println("RESPONSE: " + response.getBody()); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); + } + + @Test + public void testDeleteRestApiAdminRoleForbiddenForNonSuperAdmin() throws Exception { + setupWithRestRoles(); + rh.sendAdminCertificate = false; + + final Header restApiAdminHeader = encodeBasicHeader("rest_api_admin_user", "rest_api_admin_user"); + final Header adminHeader = encodeBasicHeader("admin", "admin"); + final Header restApiHeader = encodeBasicHeader("test", "test"); + + final String allRestAdminPermissionsPayload = createRestAdminPermissionsPayload("cluster/*"); + HttpResponse response = rh.executePutRequest( + ENDPOINT + "/roles/new_rest_admin_role", allRestAdminPermissionsPayload, restApiAdminHeader); + Assert.assertEquals(HttpStatus.SC_CREATED, response.getStatusCode()); + + // attempt to update exiting admin role + response = rh.executeDeleteRequest( + ENDPOINT + "/roles/new_rest_admin_role", + adminHeader); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); + + //true to change + response = rh.executeDeleteRequest( + ENDPOINT + "/roles/new_rest_admin_role", + allRestAdminPermissionsPayload, + restApiHeader); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); + } + + + private String createPatchRestAdminPermissionsPayload(final String op) throws JsonProcessingException { + final ArrayNode rootNode = (ArrayNode) DefaultObjectMapper.objectMapper.createArrayNode(); + final ObjectNode opAddObjectNode = DefaultObjectMapper.objectMapper.createObjectNode(); + final ObjectNode clusterPermissionsNode = DefaultObjectMapper.objectMapper.createObjectNode(); + clusterPermissionsNode.set("cluster_permissions", clusterPermissionsForRestAdmin("cluster/*")); + if ("add".equals(op)) { + opAddObjectNode + .put("op", "add") + .put("path", "/some_rest_admin_role") + .set("value", clusterPermissionsNode); + rootNode.add(opAddObjectNode); + } + + if ("remove".equals(op)) { + final ObjectNode opRemoveObjectNode = DefaultObjectMapper.objectMapper.createObjectNode(); + opRemoveObjectNode + .put("op", "remove") + .put("path", "/rest_admin_role_to_delete"); + rootNode.add(opRemoveObjectNode); + } + + if ("replace".equals(op)) { + final ObjectNode replaceRemoveObjectNode = DefaultObjectMapper.objectMapper.createObjectNode(); + replaceRemoveObjectNode + .put("op", "replace") + .put("path", "/new_rest_admin_role/cluster_permissions") + .set("value", clusterPermissionsForRestAdmin("*")); + + rootNode.add(replaceRemoveObjectNode); + } + return DefaultObjectMapper.objectMapper.writeValueAsString(rootNode); + } + + @Test + public void testRolesApiForNonSuperAdmin() throws Exception { setupWithRestRoles(); rh.keystore = "restapi/kirk-keystore.jks"; rh.sendAdminCertificate = false; rh.sendHTTPClientCredentials = true; + checkNonSuperAdminRoles(new Header[0]); + } + void checkNonSuperAdminRoles(final Header[] header) throws Exception { HttpResponse response; // Delete read only roles - response = rh.executeDeleteRequest(ENDPOINT + "/roles/opendistro_security_transport_client" , new Header[0]); + response = rh.executeDeleteRequest(ENDPOINT + "/roles/opendistro_security_transport_client", header); Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); // Put read only roles - response = rh.executePutRequest( ENDPOINT + "/roles/opendistro_security_transport_client", - FileHelper.loadFile("restapi/roles_captains.json"), new Header[0]); + response = rh.executePutRequest(ENDPOINT + "/roles/opendistro_security_transport_client", + FileHelper.loadFile("restapi/roles_captains.json"), header); Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); // Patch single read only roles - response = rh.executePatchRequest(ENDPOINT + "/roles/opendistro_security_transport_client", "[{ \"op\": \"replace\", \"path\": \"/description\", \"value\": \"foo\" }]", new Header[0]); + response = rh.executePatchRequest( + ENDPOINT + "/roles/opendistro_security_transport_client", + "[{ \"op\": \"replace\", \"path\": \"/description\", \"value\": \"foo\" }]", + header); Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); // Patch multiple read only roles - response = rh.executePatchRequest(ENDPOINT + "/roles/", "[{ \"op\": \"add\", \"path\": \"/opendistro_security_transport_client/description\", \"value\": \"foo\" }]", new Header[0]); + response = rh.executePatchRequest(ENDPOINT + "/roles/", + "[{ \"op\": \"add\", \"path\": \"/opendistro_security_transport_client/description\", \"value\": \"foo\" }]", + header); Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); // get hidden role - response = rh.executeGetRequest(ENDPOINT + "/roles/opendistro_security_internal"); + response = rh.executeGetRequest(ENDPOINT + "/roles/opendistro_security_internal", header); Assert.assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatusCode()); // delete hidden role - response = rh.executeDeleteRequest(ENDPOINT + "/roles/opendistro_security_internal" , new Header[0]); + response = rh.executeDeleteRequest(ENDPOINT + "/roles/opendistro_security_internal", header); Assert.assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatusCode()); // put hidden role String body = FileHelper.loadFile("restapi/roles_captains.json"); - response = rh.executePutRequest( ENDPOINT+ "/roles/opendistro_security_internal", body, new Header[0]); - Assert.assertEquals(org.apache.http.HttpStatus.SC_NOT_FOUND, response.getStatusCode()); + response = rh.executePutRequest(ENDPOINT + "/roles/opendistro_security_internal", body, header); + Assert.assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatusCode()); // Patch single hidden roles - response = rh.executePatchRequest(ENDPOINT + "/roles/opendistro_security_internal", "[{ \"op\": \"replace\", \"path\": \"/description\", \"value\": \"foo\" }]", new Header[0]); + response = rh.executePatchRequest(ENDPOINT + "/roles/opendistro_security_internal", + "[{ \"op\": \"replace\", \"path\": \"/description\", \"value\": \"foo\" }]", header); Assert.assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatusCode()); // Patch multiple hidden roles - response = rh.executePatchRequest(ENDPOINT + "/roles/", "[{ \"op\": \"add\", \"path\": \"/opendistro_security_internal/description\", \"value\": \"foo\" }]", new Header[0]); + response = rh.executePatchRequest(ENDPOINT + "/roles/", + "[{ \"op\": \"add\", \"path\": \"/opendistro_security_internal/description\", \"value\": \"foo\" }]", + header); Assert.assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatusCode()); - } @Test - public void checkNullElementsInArray() throws Exception{ + public void checkNullElementsInArray() throws Exception { setup(); rh.keystore = "restapi/kirk-keystore.jks"; rh.sendAdminCertificate = true; @@ -516,7 +766,7 @@ public void checkNullElementsInArray() throws Exception{ Assert.assertEquals(AbstractConfigurationValidator.ErrorType.NULL_ARRAY_ELEMENT.getMessage(), settings.get("reason")); body = FileHelper.loadFile("restapi/roles_null_array_element_index_permissions.json"); - response = rh.executePutRequest(ENDPOINT+ "/roles/opendistro_security_role_starfleet", body, new Header[0]); + response = rh.executePutRequest(ENDPOINT + "/roles/opendistro_security_role_starfleet", body, new Header[0]); settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); Assert.assertEquals(AbstractConfigurationValidator.ErrorType.NULL_ARRAY_ELEMENT.getMessage(), settings.get("reason")); diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/RolesMappingApiTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/RolesMappingApiTest.java index 2d1f10736d..19174a115b 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/RolesMappingApiTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/RolesMappingApiTest.java @@ -13,6 +13,9 @@ import java.util.List; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import org.apache.http.Header; import org.apache.http.HttpStatus; import org.junit.Assert; @@ -20,6 +23,7 @@ import org.opensearch.common.settings.Settings; import org.opensearch.common.xcontent.XContentType; +import org.opensearch.security.DefaultObjectMapper; import org.opensearch.security.dlic.rest.validation.AbstractConfigurationValidator; import org.opensearch.security.test.helper.file.FileHelper; import org.opensearch.security.test.helper.rest.RestHelper.HttpResponse; @@ -44,12 +48,114 @@ public void testRolesMappingApi() throws Exception { rh.keystore = "restapi/kirk-keystore.jks"; rh.sendAdminCertificate = true; + // create index + setupStarfleetIndex(); + // add user picard, role captains initially maps to + // opendistro_security_role_starfleet_captains and opendistro_security_role_starfleet + addUserWithPassword("picard", "picard", new String[] { "captains" }, HttpStatus.SC_CREATED); + checkWriteAccess(HttpStatus.SC_CREATED, "picard", "picard", "sf", "_doc", 1); + // TODO: only one doctype allowed for ES6 + //checkWriteAccess(HttpStatus.SC_CREATED, "picard", "picard", "sf", "_doc", 1); + rh.sendAdminCertificate = true; + verifyGetForSuperAdmin(new Header[0]); + rh.sendAdminCertificate = true; + verifyDeleteForSuperAdmin(new Header[0], true); + rh.sendAdminCertificate = true; + verifyPutForSuperAdmin(new Header[0]); + verifyPatchForSuperAdmin(new Header[0]); + // mapping with several backend roles, one of the is captain + deleteAndputNewMapping(new Header[0],"rolesmapping_backendroles_captains_list.json", true); + checkAllSfAllowed(); + + // mapping with one backend role, captain + deleteAndputNewMapping(new Header[0],"rolesmapping_backendroles_captains_single.json", true); + checkAllSfAllowed(); + + // mapping with several users, one is picard + deleteAndputNewMapping(new Header[0],"rolesmapping_users_picard_list.json", true); + checkAllSfAllowed(); + + // just user picard + deleteAndputNewMapping(new Header[0],"rolesmapping_users_picard_single.json", true); + checkAllSfAllowed(); + + // hosts + deleteAndputNewMapping(new Header[0],"rolesmapping_hosts_list.json", true); + checkAllSfAllowed(); + + // hosts + deleteAndputNewMapping(new Header[0],"rolesmapping_hosts_single.json", true); + checkAllSfAllowed(); + + // full settings, access + deleteAndputNewMapping(new Header[0],"rolesmapping_all_access.json", true); + checkAllSfAllowed(); + + // full settings, no access + deleteAndputNewMapping(new Header[0],"rolesmapping_all_noaccess.json", true); + checkAllSfForbidden(); + } + + @Test + public void testRolesMappingApiWithFullPermissions() throws Exception { + setupWithRestRoles(); + rh.sendAdminCertificate = false; + + final Header restApiAdminHeader = encodeBasicHeader("rest_api_admin_user", "rest_api_admin_user"); + // create index + setupStarfleetIndex(); + // add user picard, role captains initially maps to + // opendistro_security_role_starfleet_captains and opendistro_security_role_starfleet + addUserWithPassword("picard", "picard", new String[] { "captains" }, HttpStatus.SC_CREATED); + checkWriteAccess(HttpStatus.SC_CREATED, "picard", "picard", "sf", "_doc", 1); + // TODO: only one doctype allowed for ES6 + //checkWriteAccess(HttpStatus.SC_CREATED, "picard", "picard", "sf", "_doc", 1); + + verifyGetForSuperAdmin(new Header[]{restApiAdminHeader}); + verifyDeleteForSuperAdmin(new Header[]{restApiAdminHeader}, false); + verifyPutForSuperAdmin(new Header[]{restApiAdminHeader}); + verifyPatchForSuperAdmin(new Header[]{restApiAdminHeader}); + // mapping with several backend roles, one of the is captain + deleteAndputNewMapping(new Header[]{restApiAdminHeader}, "rolesmapping_backendroles_captains_list.json", false); + checkAllSfAllowed(); + + // mapping with one backend role, captain + deleteAndputNewMapping(new Header[]{restApiAdminHeader},"rolesmapping_backendroles_captains_single.json", true); + checkAllSfAllowed(); + + // mapping with several users, one is picard + deleteAndputNewMapping(new Header[]{restApiAdminHeader},"rolesmapping_users_picard_list.json", true); + checkAllSfAllowed(); + + // just user picard + deleteAndputNewMapping(new Header[]{restApiAdminHeader},"rolesmapping_users_picard_single.json", true); + checkAllSfAllowed(); + + // hosts + deleteAndputNewMapping(new Header[]{restApiAdminHeader},"rolesmapping_hosts_list.json", true); + checkAllSfAllowed(); + + // hosts + deleteAndputNewMapping(new Header[]{restApiAdminHeader},"rolesmapping_hosts_single.json", true); + checkAllSfAllowed(); + + // full settings, access + deleteAndputNewMapping(new Header[]{restApiAdminHeader},"rolesmapping_all_access.json", true); + checkAllSfAllowed(); + + // full settings, no access + deleteAndputNewMapping(new Header[]{restApiAdminHeader},"rolesmapping_all_noaccess.json", true); + checkAllSfForbidden(); + + } + + void verifyGetForSuperAdmin(final Header[] header) throws Exception { // check rolesmapping exists, old config api - HttpResponse response = rh.executeGetRequest(ENDPOINT + "/rolesmapping"); + HttpResponse response = rh.executeGetRequest(ENDPOINT + "/rolesmapping", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); // check rolesmapping exists, new API - response = rh.executeGetRequest(ENDPOINT + "/rolesmapping"); + response = rh.executeGetRequest(ENDPOINT + "/rolesmapping", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); Assert.assertTrue(response.getContentType(), response.isJsonContentType()); @@ -61,9 +167,8 @@ public void testRolesMappingApi() throws Exception { // -- GET - // GET opendistro_security_role_starfleet, exists - response = rh.executeGetRequest(ENDPOINT + "/rolesmapping/opendistro_security_role_starfleet", new Header[0]); + response = rh.executeGetRequest(ENDPOINT + "/rolesmapping/opendistro_security_role_starfleet", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); Assert.assertTrue(response.getContentType(), response.isJsonContentType()); Settings settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); @@ -73,55 +178,42 @@ public void testRolesMappingApi() throws Exception { Assert.assertEquals("nagilum", settings.getAsList("opendistro_security_role_starfleet.users").get(0)); // GET, rolesmapping does not exist - response = rh.executeGetRequest(ENDPOINT + "/rolesmapping/nothinghthere", new Header[0]); + response = rh.executeGetRequest(ENDPOINT + "/rolesmapping/nothinghthere", header); Assert.assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatusCode()); // GET, new URL endpoint in security - response = rh.executeGetRequest(ENDPOINT + "/rolesmapping/", new Header[0]); + response = rh.executeGetRequest(ENDPOINT + "/rolesmapping/", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); Assert.assertTrue(response.getContentType(), response.isJsonContentType()); // GET, new URL endpoint in security - response = rh.executeGetRequest(ENDPOINT + "/rolesmapping", new Header[0]); + response = rh.executeGetRequest(ENDPOINT + "/rolesmapping", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); Assert.assertTrue(response.getContentType(), response.isJsonContentType()); // Super admin should be able to describe particular hidden rolemapping - response = rh.executeGetRequest(ENDPOINT + "/rolesmapping/opendistro_security_internal", new Header[0]); + response = rh.executeGetRequest(ENDPOINT + "/rolesmapping/opendistro_security_internal", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); Assert.assertTrue(response.getBody().contains("\"hidden\":true")); + } - // create index - setupStarfleetIndex(); - - // add user picard, role captains initially maps to - // opendistro_security_role_starfleet_captains and opendistro_security_role_starfleet - addUserWithPassword("picard", "picard", new String[] { "captains" }, HttpStatus.SC_CREATED); - checkWriteAccess(HttpStatus.SC_CREATED, "picard", "picard", "sf", "_doc", 1); - - // TODO: only one doctype allowed for ES6 - //checkWriteAccess(HttpStatus.SC_CREATED, "picard", "picard", "sf", "_doc", 1); - - // --- DELETE - - rh.sendAdminCertificate = true; - + void verifyDeleteForSuperAdmin(final Header[] header, final boolean useAdminCert) throws Exception { // Non-existing role - response = rh.executeDeleteRequest(ENDPOINT + "/rolesmapping/idonotexist", new Header[0]); + HttpResponse response = rh.executeDeleteRequest(ENDPOINT + "/rolesmapping/idonotexist", header); Assert.assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatusCode()); // read only role // SuperAdmin can delete read only role - response = rh.executeDeleteRequest(ENDPOINT + "/rolesmapping/opendistro_security_role_starfleet_library", new Header[0]); + response = rh.executeDeleteRequest(ENDPOINT + "/rolesmapping/opendistro_security_role_starfleet_library", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); // hidden role - response = rh.executeDeleteRequest(ENDPOINT + "/rolesmapping/opendistro_security_internal", new Header[0]); + response = rh.executeDeleteRequest(ENDPOINT + "/rolesmapping/opendistro_security_internal", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); Assert.assertTrue(response.getBody().contains("'opendistro_security_internal' deleted.")); // remove complete role mapping for opendistro_security_role_starfleet_captains - response = rh.executeDeleteRequest(ENDPOINT + "/rolesmapping/opendistro_security_role_starfleet_captains", new Header[0]); + response = rh.executeDeleteRequest(ENDPOINT + "/rolesmapping/opendistro_security_role_starfleet_captains", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); response = rh.executeGetRequest(ENDPOINT + "/configuration/rolesmapping"); rh.sendAdminCertificate = false; @@ -134,32 +226,30 @@ public void testRolesMappingApi() throws Exception { // checkWriteAccess(HttpStatus.SC_OK, "picard", "picard", "sf", "_doc", 1); // remove also opendistro_security_role_starfleet, poor picard has no mapping left - rh.sendAdminCertificate = true; - response = rh.executeDeleteRequest(ENDPOINT + "/rolesmapping/opendistro_security_role_starfleet", new Header[0]); + rh.sendAdminCertificate = useAdminCert; + response = rh.executeDeleteRequest(ENDPOINT + "/rolesmapping/opendistro_security_role_starfleet", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); rh.sendAdminCertificate = false; checkAllSfForbidden(); + } - rh.sendAdminCertificate = true; - - // --- PUT - + void verifyPutForSuperAdmin(final Header[] header) throws Exception { // put with empty mapping, must fail - response = rh.executePutRequest(ENDPOINT + "/rolesmapping/opendistro_security_role_starfleet_captains", "", new Header[0]); + HttpResponse response = rh.executePutRequest(ENDPOINT + "/rolesmapping/opendistro_security_role_starfleet_captains", "", header); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); + Settings settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); Assert.assertEquals(AbstractConfigurationValidator.ErrorType.PAYLOAD_MANDATORY.getMessage(), settings.get("reason")); // put new configuration with invalid payload, must fail response = rh.executePutRequest(ENDPOINT + "/rolesmapping/opendistro_security_role_starfleet_captains", - FileHelper.loadFile("restapi/rolesmapping_not_parseable.json"), new Header[0]); + FileHelper.loadFile("restapi/rolesmapping_not_parseable.json"), header); settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); Assert.assertEquals(AbstractConfigurationValidator.ErrorType.BODY_NOT_PARSEABLE.getMessage(), settings.get("reason")); // put new configuration with invalid keys, must fail response = rh.executePutRequest(ENDPOINT + "/rolesmapping/opendistro_security_role_starfleet_captains", - FileHelper.loadFile("restapi/rolesmapping_invalid_keys.json"), new Header[0]); + FileHelper.loadFile("restapi/rolesmapping_invalid_keys.json"), header); settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); Assert.assertEquals(AbstractConfigurationValidator.ErrorType.INVALID_CONFIGURATION.getMessage(), settings.get("reason")); @@ -170,7 +260,7 @@ public void testRolesMappingApi() throws Exception { // wrong datatypes response = rh.executePutRequest(ENDPOINT + "/rolesmapping/opendistro_security_role_starfleet_captains", - FileHelper.loadFile("restapi/rolesmapping_backendroles_captains_single_wrong_datatype.json"), new Header[0]); + FileHelper.loadFile("restapi/rolesmapping_backendroles_captains_single_wrong_datatype.json"), header); settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); Assert.assertEquals(AbstractConfigurationValidator.ErrorType.WRONG_DATATYPE.getMessage(), settings.get("reason")); @@ -179,7 +269,7 @@ public void testRolesMappingApi() throws Exception { Assert.assertTrue(settings.get("users") == null); response = rh.executePutRequest(ENDPOINT + "/rolesmapping/opendistro_security_role_starfleet_captains", - FileHelper.loadFile("restapi/rolesmapping_hosts_single_wrong_datatype.json"), new Header[0]); + FileHelper.loadFile("restapi/rolesmapping_hosts_single_wrong_datatype.json"), header); settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); Assert.assertEquals(AbstractConfigurationValidator.ErrorType.WRONG_DATATYPE.getMessage(), settings.get("reason")); @@ -188,7 +278,7 @@ public void testRolesMappingApi() throws Exception { Assert.assertTrue(settings.get("users") == null); response = rh.executePutRequest(ENDPOINT + "/rolesmapping/opendistro_security_role_starfleet_captains", - FileHelper.loadFile("restapi/rolesmapping_users_picard_single_wrong_datatype.json"), new Header[0]); + FileHelper.loadFile("restapi/rolesmapping_users_picard_single_wrong_datatype.json"), header); settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); Assert.assertEquals(AbstractConfigurationValidator.ErrorType.WRONG_DATATYPE.getMessage(), settings.get("reason")); @@ -199,81 +289,83 @@ public void testRolesMappingApi() throws Exception { // Read only role mapping // SuperAdmin can add read only roles - mappings response = rh.executePutRequest(ENDPOINT + "/rolesmapping/opendistro_security_role_starfleet_library", - FileHelper.loadFile("restapi/rolesmapping_all_access.json"), new Header[0]); + FileHelper.loadFile("restapi/rolesmapping_all_access.json"), header); Assert.assertEquals(HttpStatus.SC_CREATED, response.getStatusCode()); // hidden role, allowed for super admin response = rh.executePutRequest(ENDPOINT + "/rolesmapping/opendistro_security_internal", - FileHelper.loadFile("restapi/rolesmapping_all_access.json"), new Header[0]); + FileHelper.loadFile("restapi/rolesmapping_all_access.json"), header); Assert.assertEquals(HttpStatus.SC_CREATED, response.getStatusCode()); response = rh.executePutRequest(ENDPOINT + "/rolesmapping/opendistro_security_role_starfleet_captains", - FileHelper.loadFile("restapi/rolesmapping_all_access.json"), new Header[0]); + FileHelper.loadFile("restapi/rolesmapping_all_access.json"), header); Assert.assertEquals(HttpStatus.SC_CREATED, response.getStatusCode()); + } - // -- PATCH + void verifyPatchForSuperAdmin(final Header[] header) throws Exception { // PATCH on non-existing resource - rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT + "/rolesmapping/imnothere", "[{ \"op\": \"add\", \"path\": \"/a/b/c\", \"value\": [ \"foo\", \"bar\" ] }]", new Header[0]); + HttpResponse response = rh.executePatchRequest(ENDPOINT + "/rolesmapping/imnothere", "[{ \"op\": \"add\", \"path\": \"/a/b/c\", \"value\": [ \"foo\", \"bar\" ] }]", header); Assert.assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatusCode()); // PATCH read only resource, must be forbidden // SuperAdmin can patch read-only resource - rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT + "/rolesmapping/opendistro_security_role_starfleet_library", "[{ \"op\": \"add\", \"path\": \"/description\", \"value\": \"foo\"] }]", new Header[0]); + response = rh.executePatchRequest(ENDPOINT + "/rolesmapping/opendistro_security_role_starfleet_library", + "[{ \"op\": \"add\", \"path\": \"/description\", \"value\": \"foo\"] }]", header); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); // PATCH hidden resource, must be not found, can be found by super admin - rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT + "/rolesmapping/opendistro_security_internal", "[{ \"op\": \"add\", \"path\": \"/a/b/c\", \"value\": [ " + - "\"foo\", \"bar\" ] }]", new Header[0]); + response = rh.executePatchRequest(ENDPOINT + "/rolesmapping/opendistro_security_internal", + "[{ \"op\": \"add\", \"path\": \"/a/b/c\", \"value\": [ " + + "\"foo\", \"bar\" ] }]", header); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); // PATCH value of hidden flag, must fail with validation error - rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT + "/rolesmapping/opendistro_security_role_vulcans", "[{ \"op\": \"add\", \"path\": \"/hidden\", \"value\": true }]", new Header[0]); + response = rh.executePatchRequest(ENDPOINT + "/rolesmapping/opendistro_security_role_vulcans", + "[{ \"op\": \"add\", \"path\": \"/hidden\", \"value\": true }]", header); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); Assert.assertTrue(response.getBody().matches(".*\"invalid_keys\"\\s*:\\s*\\{\\s*\"keys\"\\s*:\\s*\"hidden\"\\s*\\}.*")); // PATCH - rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT + "/rolesmapping/opendistro_security_role_vulcans", "[{ \"op\": \"add\", \"path\": \"/backend_roles/-\", \"value\": \"spring\" }]", new Header[0]); + response = rh.executePatchRequest(ENDPOINT + "/rolesmapping/opendistro_security_role_vulcans", + "[{ \"op\": \"add\", \"path\": \"/backend_roles/-\", \"value\": \"spring\" }]", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - response = rh.executeGetRequest(ENDPOINT + "/rolesmapping/opendistro_security_role_vulcans", new Header[0]); + response = rh.executeGetRequest(ENDPOINT + "/rolesmapping/opendistro_security_role_vulcans", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); + Settings settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); List permissions = settings.getAsList("opendistro_security_role_vulcans.backend_roles"); Assert.assertNotNull(permissions); Assert.assertTrue(permissions.contains("spring")); // -- PATCH on whole config resource // PATCH on non-existing resource - rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT + "/rolesmapping", "[{ \"op\": \"add\", \"path\": \"/imnothere/a\", \"value\": [ \"foo\", \"bar\" ] }]", new Header[0]); + response = rh.executePatchRequest(ENDPOINT + "/rolesmapping", + "[{ \"op\": \"add\", \"path\": \"/imnothere/a\", \"value\": [ \"foo\", \"bar\" ] }]", header); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); // PATCH read only resource, must be forbidden // SuperAdmin can patch read only resource rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT + "/rolesmapping", "[{ \"op\": \"add\", \"path\": \"/opendistro_security_role_starfleet_library/description\", \"value\": \"foo\" }]", new Header[0]); + response = rh.executePatchRequest(ENDPOINT + "/rolesmapping", + "[{ \"op\": \"add\", \"path\": \"/opendistro_security_role_starfleet_library/description\", \"value\": \"foo\" }]", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); // PATCH hidden resource, must be bad request rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT + "/rolesmapping", "[{ \"op\": \"add\", \"path\": \"/opendistro_security_internal/a\", \"value\": [ \"foo\", \"bar\" ] }]", new Header[0]); + response = rh.executePatchRequest(ENDPOINT + "/rolesmapping", "[{ \"op\": \"add\", \"path\": \"/opendistro_security_internal/a\", \"value\": [ \"foo\", \"bar\" ] }]", header); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); // PATCH value of hidden flag, must fail with validation error rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT + "/rolesmapping", "[{ \"op\": \"add\", \"path\": \"/opendistro_security_role_vulcans/hidden\", \"value\": true }]", new Header[0]); + response = rh.executePatchRequest(ENDPOINT + "/rolesmapping", + "[{ \"op\": \"add\", \"path\": \"/opendistro_security_role_vulcans/hidden\", \"value\": true }]", header); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); Assert.assertTrue(response.getBody().matches(".*\"invalid_keys\"\\s*:\\s*\\{\\s*\"keys\"\\s*:\\s*\"hidden\"\\s*\\}.*")); // PATCH - rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT + "/rolesmapping", "[{ \"op\": \"add\", \"path\": \"/bulknew1\", \"value\": { \"backend_roles\":[\"vulcanadmin\"]} }]", new Header[0]); + response = rh.executePatchRequest(ENDPOINT + "/rolesmapping", + "[{ \"op\": \"add\", \"path\": \"/bulknew1\", \"value\": { \"backend_roles\":[\"vulcanadmin\"]} }]", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - response = rh.executeGetRequest(ENDPOINT + "/rolesmapping/bulknew1", new Header[0]); + response = rh.executeGetRequest(ENDPOINT + "/rolesmapping/bulknew1", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); permissions = settings.getAsList("bulknew1.backend_roles"); @@ -281,45 +373,10 @@ public void testRolesMappingApi() throws Exception { Assert.assertTrue(permissions.contains("vulcanadmin")); // PATCH delete - rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT + "/rolesmapping", "[{ \"op\": \"remove\", \"path\": \"/bulknew1\"}]", new Header[0]); + response = rh.executePatchRequest(ENDPOINT + "/rolesmapping", "[{ \"op\": \"remove\", \"path\": \"/bulknew1\"}]", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - response = rh.executeGetRequest(ENDPOINT + "/rolesmapping/bulknew1", new Header[0]); + response = rh.executeGetRequest(ENDPOINT + "/rolesmapping/bulknew1", header); Assert.assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatusCode()); - - - // mapping with several backend roles, one of the is captain - deleteAndputNewMapping("rolesmapping_backendroles_captains_list.json"); - checkAllSfAllowed(); - - // mapping with one backend role, captain - deleteAndputNewMapping("rolesmapping_backendroles_captains_single.json"); - checkAllSfAllowed(); - - // mapping with several users, one is picard - deleteAndputNewMapping("rolesmapping_users_picard_list.json"); - checkAllSfAllowed(); - - // just user picard - deleteAndputNewMapping("rolesmapping_users_picard_single.json"); - checkAllSfAllowed(); - - // hosts - deleteAndputNewMapping("rolesmapping_hosts_list.json"); - checkAllSfAllowed(); - - // hosts - deleteAndputNewMapping("rolesmapping_hosts_single.json"); - checkAllSfAllowed(); - - // full settings, access - deleteAndputNewMapping("rolesmapping_all_access.json"); - checkAllSfAllowed(); - - // full settings, no access - deleteAndputNewMapping("rolesmapping_all_noaccess.json"); - checkAllSfForbidden(); - } private void checkAllSfAllowed() throws Exception { @@ -334,13 +391,13 @@ private void checkAllSfForbidden() throws Exception { checkWriteAccess(HttpStatus.SC_FORBIDDEN, "picard", "picard", "sf", "_doc", 1); } - private HttpResponse deleteAndputNewMapping(String fileName) throws Exception { - rh.sendAdminCertificate = true; + private HttpResponse deleteAndputNewMapping(final Header[] header, final String fileName, final boolean useAdminCert) throws Exception { + rh.sendAdminCertificate = useAdminCert; HttpResponse response = rh.executeDeleteRequest(ENDPOINT + "/rolesmapping/opendistro_security_role_starfleet_captains", - new Header[0]); + header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); response = rh.executePutRequest(ENDPOINT + "/rolesmapping/opendistro_security_role_starfleet_captains", - FileHelper.loadFile("restapi/"+fileName), new Header[0]); + FileHelper.loadFile("restapi/"+fileName), header); Assert.assertEquals(HttpStatus.SC_CREATED, response.getStatusCode()); rh.sendAdminCertificate = false; return response; @@ -355,46 +412,142 @@ public void testRolesMappingApiForNonSuperAdmin() throws Exception { rh.sendAdminCertificate = false; rh.sendHTTPClientCredentials = true; + verifyNonSuperAdminUser(new Header[0]); + } + + @Test + public void testRolesMappingApiForNonSuperAdminRestApiUser() throws Exception { + setupWithRestRoles(); + rh.sendAdminCertificate = false; + final Header restApiHeader = encodeBasicHeader("test", "test"); + verifyNonSuperAdminUser(new Header[] {restApiHeader}); + } + + void verifyNonSuperAdminUser(final Header[] header) throws Exception { HttpResponse response; // Delete read only roles mapping - response = rh.executeDeleteRequest(ENDPOINT + "/rolesmapping/opendistro_security_role_starfleet_library" , new Header[0]); + response = rh.executeDeleteRequest(ENDPOINT + "/rolesmapping/opendistro_security_role_starfleet_library" , header); Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); // Put read only roles mapping response = rh.executePutRequest(ENDPOINT + "/rolesmapping/opendistro_security_role_starfleet_library", - FileHelper.loadFile("restapi/rolesmapping_all_access.json"), new Header[0]); + FileHelper.loadFile("restapi/rolesmapping_all_access.json"), header); Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); // Patch single read only roles mapping - response = rh.executePatchRequest(ENDPOINT + "/rolesmapping/opendistro_security_role_starfleet_library", "[{ \"op\": \"add\", \"path\": \"/description\", \"value\": \"foo\" }]", new Header[0]); + response = rh.executePatchRequest(ENDPOINT + "/rolesmapping/opendistro_security_role_starfleet_library", "[{ \"op\": \"add\", \"path\": \"/description\", \"value\": \"foo\" }]", header); Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); // Patch multiple read only roles mapping - response = rh.executePatchRequest(ENDPOINT + "/rolesmapping", "[{ \"op\": \"add\", \"path\": \"/opendistro_security_role_starfleet_library/description\", \"value\": \"foo\" }]", new Header[0]); + response = rh.executePatchRequest(ENDPOINT + "/rolesmapping", "[{ \"op\": \"add\", \"path\": \"/opendistro_security_role_starfleet_library/description\", \"value\": \"foo\" }]", header); Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); // GET, rolesmapping is hidden, allowed for super admin - response = rh.executeGetRequest(ENDPOINT + "/rolesmapping/opendistro_security_internal", new Header[0]); + response = rh.executeGetRequest(ENDPOINT + "/rolesmapping/opendistro_security_internal", header); Assert.assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatusCode()); // Delete hidden roles mapping - response = rh.executeDeleteRequest(ENDPOINT + "/rolesmapping/opendistro_security_internal" , new Header[0]); + response = rh.executeDeleteRequest(ENDPOINT + "/rolesmapping/opendistro_security_internal" , header); Assert.assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatusCode()); // Put hidden roles mapping response = rh.executePutRequest(ENDPOINT + "/rolesmapping/opendistro_security_internal", - FileHelper.loadFile("restapi/rolesmapping_all_access.json"), new Header[0]); + FileHelper.loadFile("restapi/rolesmapping_all_access.json"), header); Assert.assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatusCode()); // Patch hidden roles mapping - response = rh.executePatchRequest(ENDPOINT + "/rolesmapping/opendistro_security_internal", "[{ \"op\": \"add\", \"path\": \"/description\", \"value\": \"foo\" }]", new Header[0]); + response = rh.executePatchRequest(ENDPOINT + "/rolesmapping/opendistro_security_internal", "[{ \"op\": \"add\", \"path\": \"/description\", \"value\": \"foo\" }]", header); Assert.assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatusCode()); // Patch multiple hidden roles mapping - response = rh.executePatchRequest(ENDPOINT + "/rolesmapping", "[{ \"op\": \"add\", \"path\": \"/opendistro_security_internal/description\", \"value\": \"foo\" }]", new Header[0]); - Assert.assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatusCode()); + response = rh.executePatchRequest(ENDPOINT + "/rolesmapping", "[{ \"op\": \"add\", \"path\": \"/opendistro_security_internal/description\", \"value\": \"foo\" }]", header); + Assert.assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatusCode()); } + + @Test + public void testChangeRestApiAdminRoleMappingForbiddenForNonSuperAdmin() throws Exception { + setupWithRestRoles(); + rh.sendAdminCertificate = false; + + final Header restApiAdminHeader = encodeBasicHeader("rest_api_admin_user", "rest_api_admin_user"); + final Header adminHeader = encodeBasicHeader("admin", "admin"); + final Header restApiHeader = encodeBasicHeader("test", "test"); + + HttpResponse response = rh.executePutRequest( + ENDPOINT + "/roles/new_rest_api_role", + createRestAdminPermissionsPayload(), restApiAdminHeader); + Assert.assertEquals(HttpStatus.SC_CREATED, response.getStatusCode()); + response = rh.executePutRequest( + ENDPOINT + "/roles/new_rest_api_role_without_mapping", + createRestAdminPermissionsPayload(), restApiAdminHeader); + Assert.assertEquals(HttpStatus.SC_CREATED, response.getStatusCode()); + response = rh.executePutRequest( + ENDPOINT + "/rolesmapping/new_rest_api_role", + createUsersPayload("a", "b", "c"), restApiAdminHeader); + Assert.assertEquals(HttpStatus.SC_CREATED, response.getStatusCode()); + + verifyRestApiPutAndDeleteForNonRestApiAdmin(adminHeader); + verifyRestApiPutAndDeleteForNonRestApiAdmin(restApiHeader); + verifyRestApiPatchForNonRestApiAdmin(adminHeader, false); + verifyRestApiPatchForNonRestApiAdmin(restApiHeader, false); + verifyRestApiPatchForNonRestApiAdmin(adminHeader, true); + verifyRestApiPatchForNonRestApiAdmin(restApiHeader, true); + } + + private void verifyRestApiPutAndDeleteForNonRestApiAdmin(final Header header) throws Exception { + HttpResponse response = rh.executePutRequest( + ENDPOINT + "/rolesmapping/new_rest_api_role", createUsersPayload("a", "b", "c"), header); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); + + response = rh.executeDeleteRequest( + ENDPOINT + "/rolesmapping/new_rest_api_role", "", header); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); + } + + private void verifyRestApiPatchForNonRestApiAdmin(final Header header, boolean bulk) throws Exception { + String path = ENDPOINT + "/rolesmapping"; + if (!bulk) { + path += "/new_rest_api_role"; + } + HttpResponse response = rh.executePatchRequest(path, createPathPayload("add"), header); + System.err.println(response.getBody()); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); + + response = rh.executePatchRequest(path, createPathPayload("replace"), header); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); + + response = rh.executePatchRequest(path, createPathPayload("remove"), header); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); + } + + private ObjectNode createUsersObjectNode(final String... users) { + final ArrayNode usersArray = DefaultObjectMapper.objectMapper.createArrayNode(); + for (final String user : users) { + usersArray.add(user); + } + return DefaultObjectMapper.objectMapper.createObjectNode().set("users", usersArray); + } + + private String createUsersPayload(final String... users) throws JsonProcessingException { + return DefaultObjectMapper.objectMapper.writeValueAsString(createUsersObjectNode(users)); + } + private String createPathPayload(final String op) throws JsonProcessingException { + final ArrayNode arrayNode = DefaultObjectMapper.objectMapper.createArrayNode(); + final ObjectNode opNode = DefaultObjectMapper.objectMapper.createObjectNode(); + opNode.put("op", op); + if ("add".equals(op)) { + opNode.put("path", "/new_rest_api_role_without_mapping"); + opNode.set("value", createUsersObjectNode("d", "e", "f")); + } + if ("replace".equals(op)) { + opNode.put("path", "/new_rest_api_role"); + opNode.set("value", createUsersObjectNode("g", "h", "i")); + } + if ("remove".equals(op)) { + opNode.put("path", "/new_rest_api_role"); + } + return DefaultObjectMapper.objectMapper.writeValueAsString(arrayNode.add(opNode)); } @Test diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/SslCertsApiTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/SslCertsApiTest.java new file mode 100644 index 0000000000..2df1fdfaf7 --- /dev/null +++ b/src/test/java/org/opensearch/security/dlic/rest/api/SslCertsApiTest.java @@ -0,0 +1,174 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.dlic.rest.api; + +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.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.support.ConfigConstants; +import org.opensearch.security.test.helper.rest.RestHelper.HttpResponse; + +import static org.opensearch.security.OpenSearchSecurityPlugin.PLUGINS_PREFIX; + +public class SslCertsApiTest extends AbstractRestApiUnitTest { + + static final String HTTP_CERTS = "http"; + + static final String TRANSPORT_CERTS = "transport"; + + private final static List> EXPECTED_CERTIFICATES = + ImmutableList.of( + ImmutableMap.of( + "issuer_dn", "CN=Example Com Inc. Signing CA,OU=Example Com Inc. Signing CA,O=Example Com Inc.,DC=example,DC=com", + "subject_dn", "CN=node-0.example.com,OU=SSL,O=Test,L=Test,C=DE", + "san", "[[2, node-0.example.com], [2, localhost], [7, 127.0.0.1], [8, 1.2.3.4.5.5]]", + "not_before", "2018-05-05T14:37:09Z", + "not_after", "2028-05-02T14:37:09Z" + ), + ImmutableMap.of( + "issuer_dn", "CN=Example Com Inc. Root CA,OU=Example Com Inc. Root CA,O=Example Com Inc.,DC=example,DC=com", + "subject_dn", "CN=Example Com Inc. Signing CA,OU=Example Com Inc. Signing CA,O=Example Com Inc.,DC=example,DC=com", + "san", "", + "not_before", "2018-05-05T14:37:08Z", + "not_after", "2028-05-04T14:37:08Z" + ) + ); + + private final static String EXPECTED_CERTIFICATES_BY_TYPE; + static { + try { + EXPECTED_CERTIFICATES_BY_TYPE = DefaultObjectMapper.objectMapper.writeValueAsString( + ImmutableMap.of( + "http_certificates_list", EXPECTED_CERTIFICATES, + "transport_certificates_list", EXPECTED_CERTIFICATES + ) + ); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + private final Header restApiAdminHeader = encodeBasicHeader("rest_api_admin_user", "rest_api_admin_user"); + private final Header restApiCertsInfoAdminHeader = encodeBasicHeader("rest_api_admin_ssl_info", "rest_api_admin_ssl_info"); + + private final Header restApiReloadCertsAdminHeader = encodeBasicHeader("rest_api_admin_ssl_reloadcerts", "rest_api_admin_ssl_reloadcerts"); + + private final Header restApiHeader = encodeBasicHeader("test", "test"); + + + public String certsInfoEndpoint() { + return PLUGINS_PREFIX + "/api/ssl/certs"; + } + + public String certsReloadEndpoint(final String certType) { + return String.format("%s/api/ssl/%s/reloadcerts", PLUGINS_PREFIX, certType); + } + + @Test + public void testCertsInfo() throws Exception { + setupWithRestRoles(); + final Header adminCredsHeader = encodeBasicHeader("admin", "admin"); + // No creds, no admin certificate - UNAUTHORIZED + rh.sendAdminCertificate = false; + HttpResponse response = rh.executeGetRequest(certsInfoEndpoint()); + Assert.assertEquals(response.getBody(), HttpStatus.SC_UNAUTHORIZED, response.getStatusCode()); + + rh.sendAdminCertificate = false; + response = rh.executeGetRequest(certsInfoEndpoint(), adminCredsHeader); + Assert.assertEquals(response.getBody(), HttpStatus.SC_FORBIDDEN, response.getStatusCode()); + + sendAdminCert(); + response = rh.executeGetRequest(certsInfoEndpoint()); + Assert.assertEquals(response.getBody(), HttpStatus.SC_OK, response.getStatusCode()); + Assert.assertEquals(EXPECTED_CERTIFICATES_BY_TYPE, response.getBody()); + + rh.sendAdminCertificate = false; + Assert.assertEquals(EXPECTED_CERTIFICATES_BY_TYPE, loadCerts(restApiAdminHeader)); + Assert.assertEquals(EXPECTED_CERTIFICATES_BY_TYPE, loadCerts(restApiCertsInfoAdminHeader)); + + response = rh.executeGetRequest(certsInfoEndpoint(), restApiHeader); + Assert.assertEquals(response.getBody(), HttpStatus.SC_FORBIDDEN, response.getStatusCode()); + } + + private String loadCerts(final Header... header) throws Exception { + HttpResponse response = rh.executeGetRequest(certsInfoEndpoint(), restApiAdminHeader); + Assert.assertEquals(response.getBody(), HttpStatus.SC_OK, response.getStatusCode()); + return response.getBody(); + } + + @Test + public void testReloadCertsNotAvailableByDefault() throws Exception { + setupWithRestRoles(); + + sendAdminCert(); + verifyReloadCertsNotAvailable(); + + rh.sendAdminCertificate = false; + verifyReloadCertsNotAvailable(restApiAdminHeader); + verifyReloadCertsNotAvailable(restApiReloadCertsAdminHeader); + + HttpResponse response = rh.executePutRequest(certsReloadEndpoint(HTTP_CERTS), "{}", restApiHeader); + Assert.assertEquals(response.getBody(), HttpStatus.SC_FORBIDDEN, response.getStatusCode()); + response = rh.executePutRequest(certsReloadEndpoint(TRANSPORT_CERTS), "{}", restApiHeader); + Assert.assertEquals(response.getBody(), HttpStatus.SC_FORBIDDEN, response.getStatusCode()); + } + + private void verifyReloadCertsNotAvailable(final Header... header) { + HttpResponse response = rh.executePutRequest(certsReloadEndpoint(HTTP_CERTS), "{}", header); + Assert.assertEquals(response.getBody(), HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); + response = rh.executePutRequest(certsReloadEndpoint(TRANSPORT_CERTS), "{}", header); + Assert.assertEquals(response.getBody(), HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); + } + + @Test + public void testReloadCertsWrongCertsType() throws Exception { + setupWithRestRoles(reloadEnabled()); + sendAdminCert(); + HttpResponse response = rh.executePutRequest(certsReloadEndpoint("aaaaa"), "{}"); + Assert.assertEquals(response.getBody(), HttpStatus.SC_FORBIDDEN, response.getStatusCode()); + + rh.sendAdminCertificate = false; + response = rh.executePutRequest(certsReloadEndpoint("bbbb"), "{}", restApiAdminHeader); + Assert.assertEquals(response.getBody(), HttpStatus.SC_FORBIDDEN, response.getStatusCode()); + response = rh.executePutRequest(certsReloadEndpoint("cccc"), "{}", restApiReloadCertsAdminHeader); + Assert.assertEquals(response.getBody(), HttpStatus.SC_FORBIDDEN, response.getStatusCode()); + + } + + @Test + public void testReloadCerts() throws Exception { + setupWithRestRoles(reloadEnabled()); + } + + + private void sendAdminCert() { + rh.keystore = "restapi/kirk-keystore.jks"; + rh.sendAdminCertificate = true; + } + + Settings reloadEnabled() { + return Settings.builder() + .put(ConfigConstants.SECURITY_SSL_CERT_RELOAD_ENABLED, true) + .build(); + } + +} diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/UserApiTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/UserApiTest.java index cb844c7b62..c69cd845b0 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/UserApiTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/UserApiTest.java @@ -58,7 +58,7 @@ public void testSecurityRoles() throws Exception { .executeGetRequest(ENDPOINT + "/" + CType.INTERNALUSERS.toLCString()); Assert.assertEquals(response.getBody(), HttpStatus.SC_OK, response.getStatusCode()); Settings settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); - Assert.assertEquals(56, settings.size()); + Assert.assertEquals(133, settings.size()); response = rh.executePatchRequest(ENDPOINT + "/internalusers", "[{ \"op\": \"add\", \"path\": \"/newuser\", \"value\": {\"password\": \"newuser\", \"opendistro_security_roles\": [\"opendistro_security_all_access\"] } }]", new Header[0]); Assert.assertEquals(response.getBody(), HttpStatus.SC_OK, response.getStatusCode()); @@ -104,50 +104,57 @@ public void testUserApi() throws Exception { rh.keystore = "restapi/kirk-keystore.jks"; rh.sendAdminCertificate = true; - // initial configuration, 6 users - HttpResponse response = rh - .executeGetRequest(ENDPOINT + "/" + CType.INTERNALUSERS.toLCString()); + // initial configuration + HttpResponse response = rh.executeGetRequest(ENDPOINT + "/" + CType.INTERNALUSERS.toLCString()); Assert.assertEquals(response.getBody(), HttpStatus.SC_OK, response.getStatusCode()); Settings settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); - Assert.assertEquals(56, settings.size()); - // --- GET + Assert.assertEquals(133, settings.size()); + verifyGet(); + verifyPut(); + verifyPatch(true); + // create index first + setupStarfleetIndex(); + verifyRoles(true); + } + private void verifyGet(final Header... header) throws Exception { + // --- GET // GET, user admin, exists - response = rh.executeGetRequest(ENDPOINT + "/internalusers/admin", new Header[0]); + HttpResponse response = rh.executeGetRequest(ENDPOINT + "/internalusers/admin", header); Assert.assertEquals(response.getBody(), HttpStatus.SC_OK, response.getStatusCode()); - settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); + Settings settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); Assert.assertEquals(7, settings.size()); // hash must be filtered Assert.assertEquals("", settings.get("admin.hash")); // GET, user does not exist - response = rh.executeGetRequest(ENDPOINT + "/internalusers/nothinghthere", new Header[0]); + response = rh.executeGetRequest(ENDPOINT + "/internalusers/nothinghthere", header); Assert.assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatusCode()); // GET, new URL endpoint in security - response = rh.executeGetRequest(ENDPOINT + "/user/", new Header[0]); + response = rh.executeGetRequest(ENDPOINT + "/user/", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); // GET, new URL endpoint in security - response = rh.executeGetRequest(ENDPOINT + "/user", new Header[0]); + response = rh.executeGetRequest(ENDPOINT + "/user", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); + } + private void verifyPut(final Header... header) throws Exception { // -- PUT - // no username given - response = rh.executePutRequest(ENDPOINT + "/internalusers/", "{\"hash\": \"123\"}", new Header[0]); + HttpResponse response = rh.executePutRequest(ENDPOINT + "/internalusers/", "{\"hash\": \"123\"}", header); Assert.assertEquals(HttpStatus.SC_METHOD_NOT_ALLOWED, response.getStatusCode()); // Faulty JSON payload response = rh.executePutRequest(ENDPOINT + "/internalusers/nagilum", "{some: \"thing\" asd other: \"thing\"}", - new Header[0]); + header); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); + Settings settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); Assert.assertEquals(settings.get("reason"), AbstractConfigurationValidator.ErrorType.BODY_NOT_PARSEABLE.getMessage()); // Missing quotes in JSON - parseable in 6.x, but wrong config keys - response = rh.executePutRequest(ENDPOINT + "/internalusers/nagilum", "{some: \"thing\", other: \"thing\"}", - new Header[0]); + response = rh.executePutRequest(ENDPOINT + "/internalusers/nagilum", "{some: \"thing\", other: \"thing\"}", header); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); //JK: this should be "Could not parse content of request." because JSON is truly invalid @@ -156,102 +163,105 @@ public void testUserApi() throws Exception { //Assert.assertTrue(settings.get(AbstractConfigurationValidator.INVALID_KEYS_KEY + ".keys").contains("other")); // Get hidden role - response = rh.executeGetRequest(ENDPOINT + "/internalusers/hide" , new Header[0]); + response = rh.executeGetRequest(ENDPOINT + "/internalusers/hide" , header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); Assert.assertTrue(response.getBody().contains("\"hidden\":true")); // Associating with hidden role is allowed (for superadmin) response = rh.executePutRequest(ENDPOINT + "/internalusers/test", "{ \"opendistro_security_roles\": " + - "[\"opendistro_security_hidden\"]}", new Header[0]); + "[\"opendistro_security_hidden\"]}", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); // Associating with reserved role is allowed (for superadmin) response = rh.executePutRequest(ENDPOINT + "/internalusers/test", "{ \"opendistro_security_roles\": [\"opendistro_security_reserved\"], " + - "\"hash\": \"123\"}", - new Header[0]); + "\"hash\": \"123\"}", + header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); // Associating with non-existent role is not allowed response = rh.executePutRequest(ENDPOINT + "/internalusers/nagilum", "{ \"opendistro_security_roles\": [\"non_existent\"]}", - new Header[0]); + header); Assert.assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatusCode()); settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); Assert.assertEquals(settings.get("message"), "Role 'non_existent' is not available for role-mapping."); // Wrong config keys response = rh.executePutRequest(ENDPOINT + "/internalusers/nagilum", "{\"some\": \"thing\", \"other\": \"thing\"}", - new Header[0]); + header); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); Assert.assertEquals(settings.get("reason"), AbstractConfigurationValidator.ErrorType.INVALID_CONFIGURATION.getMessage()); Assert.assertTrue(settings.get(AbstractConfigurationValidator.INVALID_KEYS_KEY + ".keys").contains("some")); Assert.assertTrue(settings.get(AbstractConfigurationValidator.INVALID_KEYS_KEY + ".keys").contains("other")); + } + private void verifyPatch(final boolean sendAdminCert, Header... restAdminHeader) throws Exception { // -- PATCH // PATCH on non-existing resource - rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT + "/internalusers/imnothere", "[{ \"op\": \"add\", \"path\": \"/a/b/c\", \"value\": [ \"foo\", \"bar\" ] }]", new Header[0]); + rh.sendAdminCertificate = sendAdminCert; + HttpResponse response = rh.executePatchRequest(ENDPOINT + "/internalusers/imnothere", "[{ \"op\": \"add\", \"path\": \"/a/b/c\", \"value\": [ \"foo\", \"bar\" ] }]", restAdminHeader); Assert.assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatusCode()); // PATCH read only resource, must be forbidden, // but SuperAdmin can PATCH read-only resource - rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT + "/internalusers/sarek", "[{ \"op\": \"add\", \"path\": \"/description\", \"value\": \"foo\" }]", new Header[0]); + rh.sendAdminCertificate = sendAdminCert; + response = rh.executePatchRequest(ENDPOINT + "/internalusers/sarek", "[{ \"op\": \"add\", \"path\": \"/description\", \"value\": \"foo\" }]", restAdminHeader); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); // PATCH hidden resource, must be not found, can be found for super admin - rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT + "/internalusers/q", "[{ \"op\": \"add\", \"path\": \"/a/b/c\", \"value\": [ \"foo\", \"bar\" ] }]", new Header[0]); + rh.sendAdminCertificate = sendAdminCert; + response = rh.executePatchRequest(ENDPOINT + "/internalusers/q", "[{ \"op\": \"add\", \"path\": \"/a/b/c\", \"value\": [ \"foo\", \"bar\" ] }]", restAdminHeader); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); // PATCH value of hidden flag, must fail with validation error - rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT + "/internalusers/test", "[{ \"op\": \"add\", \"path\": \"/hidden\", \"value\": true }]", new Header[0]); + rh.sendAdminCertificate = sendAdminCert; + response = rh.executePatchRequest(ENDPOINT + "/internalusers/test", "[{ \"op\": \"add\", \"path\": \"/hidden\", \"value\": true }]", restAdminHeader); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); Assert.assertTrue(response.getBody().matches(".*\"invalid_keys\"\\s*:\\s*\\{\\s*\"keys\"\\s*:\\s*\"hidden\"\\s*\\}.*")); // PATCH password - rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT + "/internalusers/test", "[{ \"op\": \"add\", \"path\": \"/password\", \"value\": \"neu\" }]", new Header[0]); + rh.sendAdminCertificate = sendAdminCert; + response = rh.executePatchRequest(ENDPOINT + "/internalusers/test", "[{ \"op\": \"add\", \"path\": \"/password\", \"value\": \"neu\" }]", restAdminHeader); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - response = rh.executeGetRequest(ENDPOINT + "/internalusers/test", new Header[0]); + response = rh.executeGetRequest(ENDPOINT + "/internalusers/test", restAdminHeader); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); + Settings settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); Assert.assertFalse(settings.hasValue("test.password")); Assert.assertTrue(settings.hasValue("test.hash")); // -- PATCH on whole config resource // PATCH on non-existing resource - rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT + "/internalusers", "[{ \"op\": \"add\", \"path\": \"/imnothere/a\", \"value\": [ \"foo\", \"bar\" ] }]", new Header[0]); + rh.sendAdminCertificate = sendAdminCert; + response = rh.executePatchRequest(ENDPOINT + "/internalusers", "[{ \"op\": \"add\", \"path\": \"/imnothere/a\", \"value\": [ \"foo\", \"bar\" ] }]", restAdminHeader); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); // PATCH read only resource, must be forbidden, // but SuperAdmin can PATCH read only resouce - rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT + "/internalusers", "[{ \"op\": \"add\", \"path\": \"/sarek/description\", \"value\": \"foo\" }]", new Header[0]); + rh.sendAdminCertificate = sendAdminCert; + response = rh.executePatchRequest(ENDPOINT + "/internalusers", "[{ \"op\": \"add\", \"path\": \"/sarek/description\", \"value\": \"foo\" }]", restAdminHeader); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); rh.sendAdminCertificate = false; - response = rh.executePatchRequest(ENDPOINT + "/internalusers", "[{ \"op\": \"add\", \"path\": \"/sarek/a\", \"value\": [ \"foo\", \"bar\" ] }]", new Header[0]); + response = rh.executePatchRequest(ENDPOINT + "/internalusers", "[{ \"op\": \"add\", \"path\": \"/sarek/a\", \"value\": [ \"foo\", \"bar\" ] }]"); Assert.assertEquals(HttpStatus.SC_UNAUTHORIZED, response.getStatusCode()); // PATCH hidden resource, must be bad request - rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT + "/internalusers", "[{ \"op\": \"add\", \"path\": \"/q/a\", \"value\": [ \"foo\", \"bar\" ] }]", new Header[0]); + rh.sendAdminCertificate = sendAdminCert; + response = rh.executePatchRequest(ENDPOINT + "/internalusers", "[{ \"op\": \"add\", \"path\": \"/q/a\", \"value\": [ \"foo\", \"bar\" ] }]", restAdminHeader); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); // PATCH value of hidden flag, must fail with validation error - rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT + "/internalusers", "[{ \"op\": \"add\", \"path\": \"/test/hidden\", \"value\": true }]", new Header[0]); + rh.sendAdminCertificate = sendAdminCert; + response = rh.executePatchRequest(ENDPOINT + "/internalusers", "[{ \"op\": \"add\", \"path\": \"/test/hidden\", \"value\": true }]", restAdminHeader); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); Assert.assertTrue(response.getBody().matches(".*\"invalid_keys\"\\s*:\\s*\\{\\s*\"keys\"\\s*:\\s*\"hidden\"\\s*\\}.*")); // PATCH - rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT + "/internalusers", "[{ \"op\": \"add\", \"path\": \"/bulknew1\", \"value\": {\"password\": \"bla\", \"backend_roles\": [\"vulcan\"] } }]", new Header[0]); + rh.sendAdminCertificate = sendAdminCert; + response = rh.executePatchRequest(ENDPOINT + "/internalusers", + "[{ \"op\": \"add\", \"path\": \"/bulknew1\", \"value\": {\"password\": \"bla\", \"backend_roles\": [\"vulcan\"] } }]", restAdminHeader); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - response = rh.executeGetRequest(ENDPOINT + "/internalusers/bulknew1", new Header[0]); + response = rh.executeGetRequest(ENDPOINT + "/internalusers/bulknew1", restAdminHeader); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); Assert.assertFalse(settings.hasValue("bulknew1.password")); @@ -267,17 +277,17 @@ public void testUserApi() throws Exception { // add/update user, user is read only, forbidden // SuperAdmin can add read only users - rh.sendAdminCertificate = true; + rh.sendAdminCertificate = sendAdminCert; addUserWithHash("sarek", "$2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m", HttpStatus.SC_OK); // add/update user, user is hidden, forbidden, allowed for super admin - rh.sendAdminCertificate = true; + rh.sendAdminCertificate = sendAdminCert; addUserWithHash("q", "$2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m", HttpStatus.SC_OK); // add users - rh.sendAdminCertificate = true; + rh.sendAdminCertificate = sendAdminCert; addUserWithHash("nagilum", "$2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m", HttpStatus.SC_CREATED); @@ -285,20 +295,20 @@ public void testUserApi() throws Exception { checkGeneralAccess(HttpStatus.SC_OK, "nagilum", "nagilum"); // try remove user, no username - rh.sendAdminCertificate = true; - response = rh.executeDeleteRequest(ENDPOINT + "/internalusers", new Header[0]); + rh.sendAdminCertificate = sendAdminCert; + response = rh.executeDeleteRequest(ENDPOINT + "/internalusers", restAdminHeader); Assert.assertEquals(HttpStatus.SC_METHOD_NOT_ALLOWED, response.getStatusCode()); // try remove user, nonexisting user - response = rh.executeDeleteRequest(ENDPOINT + "/internalusers/picard", new Header[0]); + response = rh.executeDeleteRequest(ENDPOINT + "/internalusers/picard", restAdminHeader); Assert.assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatusCode()); // try remove readonly user - response = rh.executeDeleteRequest(ENDPOINT + "/internalusers/sarek", new Header[0]); + response = rh.executeDeleteRequest(ENDPOINT + "/internalusers/sarek", restAdminHeader); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); // try remove hidden user, allowed for super admin - response = rh.executeDeleteRequest(ENDPOINT + "/internalusers/q", new Header[0]); + response = rh.executeDeleteRequest(ENDPOINT + "/internalusers/q", restAdminHeader); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); Assert.assertTrue(response.getBody().contains("'q' deleted.")); // now really remove user @@ -309,7 +319,7 @@ public void testUserApi() throws Exception { checkGeneralAccess(HttpStatus.SC_UNAUTHORIZED, "nagilum", "nagilum"); // use password instead of hash - rh.sendAdminCertificate = true; + rh.sendAdminCertificate = sendAdminCert; addUserWithPassword("nagilum", "correctpassword", HttpStatus.SC_CREATED); rh.sendAdminCertificate = false; @@ -319,7 +329,7 @@ public void testUserApi() throws Exception { deleteUser("nagilum"); // Check unchanged password functionality - rh.sendAdminCertificate = true; + rh.sendAdminCertificate = sendAdminCert; // new user, password or hash is mandatory addUserWithoutPasswordOrHash("nagilum", new String[]{"starfleet"}, HttpStatus.SC_BAD_REQUEST); @@ -329,35 +339,32 @@ public void testUserApi() throws Exception { // update user, do not specify hash or password, hash must remain the same addUserWithoutPasswordOrHash("nagilum", new String[]{"starfleet"}, HttpStatus.SC_OK); // get user, check hash, must be untouched - response = rh.executeGetRequest(ENDPOINT + "/internalusers/nagilum", new Header[0]); + response = rh.executeGetRequest(ENDPOINT + "/internalusers/nagilum", restAdminHeader); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); Assert.assertTrue(settings.get("nagilum.hash").equals("")); + } - - // ROLES - // create index first - setupStarfleetIndex(); - + private void verifyRoles(final boolean sendAdminCert, Header... header) throws Exception { // wrong datatypes in roles file - rh.sendAdminCertificate = true; - response = rh.executePutRequest(ENDPOINT + "/internalusers/picard", FileHelper.loadFile("restapi/users_wrong_datatypes.json"), new Header[0]); - settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); + rh.sendAdminCertificate = sendAdminCert; + HttpResponse response = rh.executePutRequest(ENDPOINT + "/internalusers/picard", FileHelper.loadFile("restapi/users_wrong_datatypes.json"), header); + Settings settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); Assert.assertEquals(AbstractConfigurationValidator.ErrorType.WRONG_DATATYPE.getMessage(), settings.get("reason")); Assert.assertTrue(settings.get("backend_roles").equals("Array expected")); rh.sendAdminCertificate = false; - rh.sendAdminCertificate = true; - response = rh.executePutRequest(ENDPOINT + "/internalusers/picard", FileHelper.loadFile("restapi/users_wrong_datatypes.json"), new Header[0]); + rh.sendAdminCertificate = sendAdminCert; + response = rh.executePutRequest(ENDPOINT + "/internalusers/picard", FileHelper.loadFile("restapi/users_wrong_datatypes.json"), header); settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); Assert.assertEquals(AbstractConfigurationValidator.ErrorType.WRONG_DATATYPE.getMessage(), settings.get("reason")); Assert.assertTrue(settings.get("backend_roles").equals("Array expected")); rh.sendAdminCertificate = false; - rh.sendAdminCertificate = true; - response = rh.executePutRequest(ENDPOINT + "/internalusers/picard", FileHelper.loadFile("restapi/users_wrong_datatypes2.json"), new Header[0]); + rh.sendAdminCertificate = sendAdminCert; + response = rh.executePutRequest(ENDPOINT + "/internalusers/picard", FileHelper.loadFile("restapi/users_wrong_datatypes2.json"), header); settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); Assert.assertEquals(AbstractConfigurationValidator.ErrorType.WRONG_DATATYPE.getMessage(), settings.get("reason")); @@ -365,8 +372,8 @@ public void testUserApi() throws Exception { Assert.assertTrue(settings.get("backend_roles") == null); rh.sendAdminCertificate = false; - rh.sendAdminCertificate = true; - response = rh.executePutRequest(ENDPOINT + "/internalusers/picard", FileHelper.loadFile("restapi/users_wrong_datatypes3.json"), new Header[0]); + rh.sendAdminCertificate = sendAdminCert; + response = rh.executePutRequest(ENDPOINT + "/internalusers/picard", FileHelper.loadFile("restapi/users_wrong_datatypes3.json"), header); settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); Assert.assertEquals(AbstractConfigurationValidator.ErrorType.WRONG_DATATYPE.getMessage(), settings.get("reason")); @@ -395,12 +402,12 @@ public void testUserApi() throws Exception { checkReadAccess(HttpStatus.SC_OK, "picard", "picard", "sf", "_doc", 0); checkWriteAccess(HttpStatus.SC_CREATED, "picard", "picard", "sf", "_doc", 1); - rh.sendAdminCertificate = true; - response = rh.executeGetRequest(ENDPOINT + "/internalusers/picard", new Header[0]); + rh.sendAdminCertificate = sendAdminCert; + response = rh.executeGetRequest(ENDPOINT + "/internalusers/picard", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); Assert.assertEquals("", settings.get("picard.hash")); - roles = settings.getAsList("picard.backend_roles"); + List roles = settings.getAsList("picard.backend_roles"); Assert.assertNotNull(roles); Assert.assertEquals(2, roles.size()); Assert.assertTrue(roles.contains("starfleet")); @@ -411,10 +418,46 @@ public void testUserApi() throws Exception { // check tabs in json - response = rh.executePutRequest(ENDPOINT + "/internalusers/userwithtabs", "\t{\"hash\": \t \"123\"\t} ", new Header[0]); + response = rh.executePutRequest(ENDPOINT + "/internalusers/userwithtabs", "\t{\"hash\": \t \"123\"\t} ", header); Assert.assertEquals(response.getBody(), HttpStatus.SC_CREATED, response.getStatusCode()); } + @Test + public void testUserApiWithRestAdminPermissions() throws Exception { + setupWithRestRoles(); + rh.sendAdminCertificate = false; + final Header restApiAdminHeader = encodeBasicHeader("rest_api_admin_user", "rest_api_admin_user"); + // initial configuration + HttpResponse response = rh.executeGetRequest(ENDPOINT + "/" + CType.INTERNALUSERS.toLCString(), restApiAdminHeader); + Assert.assertEquals(response.getBody(), HttpStatus.SC_OK, response.getStatusCode()); + Settings settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); + Assert.assertEquals(133, settings.size()); + verifyGet(restApiAdminHeader); + verifyPut(restApiAdminHeader); + verifyPatch(false, restApiAdminHeader); + // create index first + setupStarfleetIndex(); + verifyRoles(false, restApiAdminHeader); + } + + @Test + public void testUserApiWithRestInternalUsersAdminPermissions() throws Exception { + setupWithRestRoles(); + rh.sendAdminCertificate = false; + final Header restApiInternalUsersAdminHeader = encodeBasicHeader("rest_api_admin_internalusers", "rest_api_admin_internalusers"); + // initial configuration + HttpResponse response = rh.executeGetRequest(ENDPOINT + "/" + CType.INTERNALUSERS.toLCString(), restApiInternalUsersAdminHeader); + Assert.assertEquals(response.getBody(), HttpStatus.SC_OK, response.getStatusCode()); + Settings settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); + Assert.assertEquals(133, settings.size()); + verifyGet(restApiInternalUsersAdminHeader); + verifyPut(restApiInternalUsersAdminHeader); + verifyPatch(false, restApiInternalUsersAdminHeader); + // create index first + setupStarfleetIndex(); + verifyRoles(false, restApiInternalUsersAdminHeader); + } + @Test public void testPasswordRules() throws Exception { @@ -436,7 +479,7 @@ public void testPasswordRules() throws Exception { Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); System.out.println(response.getBody()); Settings settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); - Assert.assertEquals(56, settings.size()); + Assert.assertEquals(133, settings.size()); addUserWithPassword("tooshoort", "", HttpStatus.SC_BAD_REQUEST); addUserWithPassword("tooshoort", "123", HttpStatus.SC_BAD_REQUEST); @@ -516,7 +559,7 @@ public void testUserApiWithDots() throws Exception { .executeGetRequest(ENDPOINT + "/" + CType.INTERNALUSERS.toLCString()); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); Settings settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); - Assert.assertEquals(56, settings.size()); + Assert.assertEquals(133, settings.size()); addUserWithPassword(".my.dotuser0", "$2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m", HttpStatus.SC_CREATED); diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/legacy/LegacySslCertsApiTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/legacy/LegacySslCertsApiTest.java new file mode 100644 index 0000000000..5d1c3ae538 --- /dev/null +++ b/src/test/java/org/opensearch/security/dlic/rest/api/legacy/LegacySslCertsApiTest.java @@ -0,0 +1,29 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.dlic.rest.api.legacy; + +import org.opensearch.security.dlic.rest.api.SslCertsApiTest; + +import static org.opensearch.security.OpenSearchSecurityPlugin.LEGACY_OPENDISTRO_PREFIX; + +public class LegacySslCertsApiTest extends SslCertsApiTest { + + @Override + public String certsInfoEndpoint() { + return LEGACY_OPENDISTRO_PREFIX + "/api/ssl/certs"; + } + + @Override + public String certsReloadEndpoint(String certType) { + return String.format("%s/api/ssl/%s/reloadcerts", LEGACY_OPENDISTRO_PREFIX, certType); + } +} diff --git a/src/test/java/org/opensearch/security/securityconf/SecurityRolesPermissionsTest.java b/src/test/java/org/opensearch/security/securityconf/SecurityRolesPermissionsTest.java new file mode 100644 index 0000000000..010b453b85 --- /dev/null +++ b/src/test/java/org/opensearch/security/securityconf/SecurityRolesPermissionsTest.java @@ -0,0 +1,274 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.securityconf; + +import java.io.IOException; +import java.util.AbstractMap.SimpleEntry; +import java.util.Arrays; +import java.util.Collection; +import java.util.Locale; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +import org.opensearch.common.settings.Settings; +import org.opensearch.security.DefaultObjectMapper; +import org.opensearch.security.dlic.rest.api.Endpoint; +import org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.PermissionBuilder; +import org.opensearch.security.securityconf.impl.CType; +import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; + +import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.CERTS_INFO_ACTION; +import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.ENDPOINTS_WITH_PERMISSIONS; +import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.RELOAD_CERTS_ACTION; + +public class SecurityRolesPermissionsTest { + + static final Map NO_REST_ADMIN_PERMISSIONS_ROLES = + ImmutableMap.builder() + .put( + "all_access", + role("*")) + .put( + "all_cluster_and_indices", + role("custer:*", "indices:*") + ).build(); + + static final Map REST_ADMIN_PERMISSIONS_FULL_ACCESS_ROLES = + ImmutableMap.builder() + .put( + "security_rest_api_full_access", + role(allRestApiPermissions())) + .put( + "security_rest_api_full_access_with_star", + role("restapi:admin/*")) + .build(); + + + static String restAdminApiRoleName(final String endpoint) { + return String.format("security_rest_api_%s_only", endpoint); + } + + static final Map REST_ADMIN_PERMISSIONS_ROLES = + ENDPOINTS_WITH_PERMISSIONS + .entrySet() + .stream() + .flatMap(e -> { + final String endpoint = e.getKey().name().toLowerCase(Locale.ROOT); + final PermissionBuilder pb = e.getValue(); + if (e.getKey() == Endpoint.SSL) { + return Stream.of( + new SimpleEntry<>( + restAdminApiRoleName(CERTS_INFO_ACTION), + role(pb.build(CERTS_INFO_ACTION)) + ), + new SimpleEntry<>( + restAdminApiRoleName(RELOAD_CERTS_ACTION), + role(pb.build(RELOAD_CERTS_ACTION)) + ) + ); + } else { + return Stream.of( + new SimpleEntry<>(restAdminApiRoleName(endpoint), role(pb.build())) + ); + } + }).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + + static ObjectNode role(final String... clusterPermissions) { + final ArrayNode clusterPermissionsArrayNode = DefaultObjectMapper.objectMapper.createArrayNode(); + Arrays.stream(clusterPermissions).forEach(clusterPermissionsArrayNode::add); + return DefaultObjectMapper.objectMapper + .createObjectNode() + .put("reserved", true) + .set("cluster_permissions", clusterPermissionsArrayNode); + } + + static String[] allRestApiPermissions() { + return ENDPOINTS_WITH_PERMISSIONS + .entrySet() + .stream() + .flatMap(entry -> { + if (entry.getKey() == Endpoint.SSL) { + return Stream.of(entry.getValue().build(CERTS_INFO_ACTION), entry.getValue().build(RELOAD_CERTS_ACTION)); + } else { + return Stream.of(entry.getValue().build()); + } + }).toArray(String[]::new); + } + + final ConfigModel configModel; + + public SecurityRolesPermissionsTest() throws IOException { + this.configModel = + new ConfigModelV7( + createRolesConfig(), + createRoleMappingsConfig(), + createActionGroupsConfig(), + createTenantsConfig(), + Mockito.mock(DynamicConfigModel.class), + Settings.EMPTY + ); + } + + @Test + public void hasNoExplicitClusterPermissionPermissionForRestAdmin() { + for (final String role : NO_REST_ADMIN_PERMISSIONS_ROLES.keySet()) { + final SecurityRoles securityRolesForRole = configModel.getSecurityRoles().filter(ImmutableSet.of(role)); + for (final Map.Entry entry : ENDPOINTS_WITH_PERMISSIONS.entrySet()) { + final Endpoint endpoint = entry.getKey(); + final PermissionBuilder permissionBuilder = entry.getValue(); + if (endpoint == Endpoint.SSL) { + Assert.assertFalse( + endpoint.name(), + securityRolesForRole.hasExplicitClusterPermissionPermission(permissionBuilder.build(CERTS_INFO_ACTION)) + ); + Assert.assertFalse( + endpoint.name(), + securityRolesForRole.hasExplicitClusterPermissionPermission(permissionBuilder.build(RELOAD_CERTS_ACTION)) + ); + } else { + Assert.assertFalse( + endpoint.name(), + securityRolesForRole.hasExplicitClusterPermissionPermission(permissionBuilder.build()) + ); + } + } + } + } + + @Test + public void hasExplicitClusterPermissionPermissionForRestAdminWitFullAccess() { + for (final String role : REST_ADMIN_PERMISSIONS_FULL_ACCESS_ROLES.keySet()) { + final SecurityRoles securityRolesForRole = configModel.getSecurityRoles().filter(ImmutableSet.of(role)); + for (final Map.Entry entry : ENDPOINTS_WITH_PERMISSIONS.entrySet()) { + final Endpoint endpoint = entry.getKey(); + final PermissionBuilder permissionBuilder = entry.getValue(); + if (endpoint == Endpoint.SSL) { + Assert.assertTrue(endpoint.name() + "/" + CERTS_INFO_ACTION, securityRolesForRole.hasExplicitClusterPermissionPermission(permissionBuilder.build(CERTS_INFO_ACTION))); + Assert.assertTrue(endpoint.name() + "/" + CERTS_INFO_ACTION, securityRolesForRole.hasExplicitClusterPermissionPermission(permissionBuilder.build(RELOAD_CERTS_ACTION))); + } else { + Assert.assertTrue(endpoint.name(), securityRolesForRole.hasExplicitClusterPermissionPermission(permissionBuilder.build())); + } + } + } + } + + @Test + public void hasExplicitClusterPermissionPermissionForRestAdmin() { + // verify all endpoint except SSL + final Collection noSslEndpoints = + ENDPOINTS_WITH_PERMISSIONS.keySet().stream() + .filter(e -> e != Endpoint.SSL).collect(Collectors.toList()); + for (final Endpoint endpoint : noSslEndpoints) { + final String permission = ENDPOINTS_WITH_PERMISSIONS.get(endpoint).build(); + final SecurityRoles allowOnePermissionRole = + configModel.getSecurityRoles().filter( + ImmutableSet.of(restAdminApiRoleName(endpoint.name().toLowerCase(Locale.ROOT)))); + Assert.assertTrue(endpoint.name(), allowOnePermissionRole.hasExplicitClusterPermissionPermission(permission)); + assertHasNoPermissionsForRestApiAdminOnePermissionRole( + endpoint, + allowOnePermissionRole + ); + } + // verify SSL endpoint with 2 actions + for (final String sslAction : ImmutableSet.of(CERTS_INFO_ACTION, RELOAD_CERTS_ACTION)) { + final SecurityRoles sslAllowRole = + configModel.getSecurityRoles().filter(ImmutableSet.of(restAdminApiRoleName(sslAction))); + final PermissionBuilder permissionBuilder = ENDPOINTS_WITH_PERMISSIONS.get(Endpoint.SSL); + Assert.assertTrue( + Endpoint.SSL + "/" + sslAction, + sslAllowRole.hasExplicitClusterPermissionPermission(permissionBuilder.build(sslAction)) + ); + assertHasNoPermissionsForRestApiAdminOnePermissionRole(Endpoint.SSL, sslAllowRole); + } + } + + void assertHasNoPermissionsForRestApiAdminOnePermissionRole(final Endpoint allowEndpoint, final SecurityRoles allowOnlyRoleForRole) { + final Collection noPermissionEndpoints = + ENDPOINTS_WITH_PERMISSIONS.keySet().stream() + .filter(e -> e != allowEndpoint) + .collect(Collectors.toList()); + for (final Endpoint endpoint : noPermissionEndpoints) { + final PermissionBuilder permissionBuilder = ENDPOINTS_WITH_PERMISSIONS.get(endpoint); + if (endpoint == Endpoint.SSL) { + Assert.assertFalse( + endpoint.name(), + allowOnlyRoleForRole.hasExplicitClusterPermissionPermission(permissionBuilder.build(CERTS_INFO_ACTION))); + Assert.assertFalse( + endpoint.name(), + allowOnlyRoleForRole.hasExplicitClusterPermissionPermission(permissionBuilder.build(RELOAD_CERTS_ACTION))); + } else { + Assert.assertFalse( + endpoint.name(), + allowOnlyRoleForRole.hasExplicitClusterPermissionPermission(permissionBuilder.build())); + } + } + } + + static ObjectNode meta(final String type) { + return DefaultObjectMapper.objectMapper + .createObjectNode() + .put("type", type) + .put("config_version", 2); + } + + static SecurityDynamicConfiguration createRolesConfig() throws IOException { + final ObjectNode rolesNode = DefaultObjectMapper.objectMapper.createObjectNode(); + rolesNode.set("_meta", meta("roles")); + NO_REST_ADMIN_PERMISSIONS_ROLES.forEach(rolesNode::set); + REST_ADMIN_PERMISSIONS_FULL_ACCESS_ROLES.forEach(rolesNode::set); + REST_ADMIN_PERMISSIONS_ROLES.forEach(rolesNode::set); + return SecurityDynamicConfiguration.fromNode(rolesNode, CType.ROLES, 2, 0, 0); + } + + static SecurityDynamicConfiguration createRoleMappingsConfig() throws IOException { + final ObjectNode metaNode = DefaultObjectMapper.objectMapper.createObjectNode(); + metaNode.set("_meta", meta("rolesmapping")); + return SecurityDynamicConfiguration.fromNode(metaNode, CType.ROLESMAPPING, 2, 0, 0); + } + + static SecurityDynamicConfiguration createActionGroupsConfig() throws IOException { + final ObjectNode metaNode = DefaultObjectMapper.objectMapper.createObjectNode(); + metaNode.set("_meta", meta("actiongroups")); + return SecurityDynamicConfiguration.fromNode(metaNode, CType.ACTIONGROUPS, 2, 0, 0); + } + + static SecurityDynamicConfiguration createTenantsConfig() throws IOException { + final ObjectNode metaNode = DefaultObjectMapper.objectMapper.createObjectNode(); + metaNode.set("_meta", meta("tenants")); + return SecurityDynamicConfiguration.fromNode(metaNode, CType.TENANTS, 2, 0, 0); + } + +} diff --git a/src/test/java/org/opensearch/security/ssl/SecuritySSLCertsInfoActionTests.java b/src/test/java/org/opensearch/security/ssl/SecuritySSLCertsInfoActionTests.java deleted file mode 100644 index c9618e6463..0000000000 --- a/src/test/java/org/opensearch/security/ssl/SecuritySSLCertsInfoActionTests.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.security.ssl; - -import java.util.List; -import java.util.Map; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import net.minidev.json.JSONObject; -import org.junit.Assert; -import org.junit.Test; - -import org.opensearch.common.settings.Settings; -import org.opensearch.security.ssl.util.SSLConfigConstants; -import org.opensearch.security.support.ConfigConstants; -import org.opensearch.security.test.SingleClusterTest; -import org.opensearch.security.test.helper.file.FileHelper; -import org.opensearch.security.test.helper.rest.RestHelper; - -import static org.opensearch.security.OpenSearchSecurityPlugin.LEGACY_OPENDISTRO_PREFIX; -import static org.opensearch.security.OpenSearchSecurityPlugin.PLUGINS_PREFIX; - -public class SecuritySSLCertsInfoActionTests extends SingleClusterTest { - private final List> NODE_CERT_DETAILS = ImmutableList.of( - ImmutableMap.of( - "issuer_dn", "CN=Example Com Inc. Signing CA,OU=Example Com Inc. Signing CA,O=Example Com Inc.,DC=example,DC=com", - "subject_dn", "CN=node-0.example.com,OU=SSL,O=Test,L=Test,C=DE", - "san", "[[2, node-0.example.com], [2, localhost], [7, 127.0.0.1], [8, 1.2.3.4.5.5]]", - "not_before","2018-05-05T14:37:09Z", - "not_after","2028-05-02T14:37:09Z" - )); - - @Test - public void testCertInfo_Legacy_Pass() throws Exception { - certInfo_Pass(LEGACY_OPENDISTRO_PREFIX + "/api/ssl/certs"); - } - - @Test - public void testCertInfo_Pass() throws Exception { - certInfo_Pass(PLUGINS_PREFIX + "/api/ssl/certs"); - } - - public void certInfo_Pass(final String endpoint) throws Exception { - initTestCluster(); - final RestHelper rh = restHelper(); - rh.enableHTTPClientSSL = true; - rh.trustHTTPServerCertificate = true; - rh.sendAdminCertificate = true; - rh.keystore = "kirk-keystore.jks"; - - final RestHelper.HttpResponse transportInfoRestResponse = rh.executeGetRequest(endpoint); - JSONObject expectedJsonResponse = new JSONObject(); - expectedJsonResponse.appendField("http_certificates_list", NODE_CERT_DETAILS); - expectedJsonResponse.appendField("transport_certificates_list", NODE_CERT_DETAILS); - Assert.assertEquals(expectedJsonResponse.toString(), transportInfoRestResponse.getBody()); - } - - @Test - public void testCertInfoFail_Legacy_NonAdmin() throws Exception { - certInfoFail_NonAdmin(LEGACY_OPENDISTRO_PREFIX + "/api/ssl/certs"); - } - - @Test - public void testCertInfoFail_NonAdmin() throws Exception { - certInfoFail_NonAdmin(PLUGINS_PREFIX + "/api/ssl/certs"); - } - - public void certInfoFail_NonAdmin(final String endpoint) throws Exception { - initTestCluster(); - final RestHelper rh = restHelper(); - rh.enableHTTPClientSSL = true; - rh.trustHTTPServerCertificate = true; - rh.sendAdminCertificate = true; - rh.keystore = "spock-keystore.jks"; - - final RestHelper.HttpResponse transportInfoRestResponse = rh.executeGetRequest(endpoint); - Assert.assertEquals(401, transportInfoRestResponse.getStatusCode()); // Forbidden for non-admin - Assert.assertEquals("Unauthorized", transportInfoRestResponse.getStatusReason()); - } - - /** - * Helper method to initialize test cluster for CertInfoAction Tests - */ - private void initTestCluster() throws Exception { - final Settings settings = Settings.builder() - .put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENABLED, true) - .put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_PEMCERT_FILEPATH, FileHelper. getAbsoluteFilePathFromClassPath("ssl/node-0.crt.pem")) - .put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_PEMKEY_FILEPATH, FileHelper. getAbsoluteFilePathFromClassPath("ssl/node-0.key.pem")) - .put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_PEMTRUSTEDCAS_FILEPATH, FileHelper. getAbsoluteFilePathFromClassPath("ssl/root-ca.pem")) - .put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION, false) - .put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION_RESOLVE_HOST_NAME, false) - .put(SSLConfigConstants.SECURITY_SSL_HTTP_ENABLED, true) - .put(SSLConfigConstants.SECURITY_SSL_HTTP_PEMCERT_FILEPATH, FileHelper.getAbsoluteFilePathFromClassPath("ssl/node-0.crt.pem")) - .put(SSLConfigConstants.SECURITY_SSL_HTTP_PEMKEY_FILEPATH, FileHelper.getAbsoluteFilePathFromClassPath("ssl/node-0.key.pem")) - .put(SSLConfigConstants.SECURITY_SSL_HTTP_PEMTRUSTEDCAS_FILEPATH, FileHelper.getAbsoluteFilePathFromClassPath("ssl/root-ca.pem")) - .put(ConfigConstants.SECURITY_SSL_CERT_RELOAD_ENABLED, true) - .build(); - setup(settings); - } -} diff --git a/src/test/java/org/opensearch/security/ssl/SecuritySSLReloadCertsActionTests.java b/src/test/java/org/opensearch/security/ssl/SecuritySSLReloadCertsActionTests.java index ea78ee043c..f7684a9c51 100644 --- a/src/test/java/org/opensearch/security/ssl/SecuritySSLReloadCertsActionTests.java +++ b/src/test/java/org/opensearch/security/ssl/SecuritySSLReloadCertsActionTests.java @@ -127,32 +127,6 @@ public void testReloadHttpSSLCertsPass() throws Exception { assertReloadCertificateSuccess(rh, "http", getUpdatedCertDetailsExpectedResponse("http")); } - @Test - public void testReloadHttpSSLCerts_FailWrongUri() throws Exception { - initClusterWithTestCerts(); - RestHelper rh = getRestHelperAdminUser(); - - RestHelper.HttpResponse reloadCertsResponse = rh.executePutRequest("_opendistro/_security/api/ssl/wrong/reloadcerts", null); - JSONObject expectedResponse = new JSONObject(); - // Note: toString and toJSONString replace / with \/. This helps get rid of the additional \ character. - expectedResponse.put("message", "invalid uri path, please use /_opendistro/_security/api/ssl/http/reload or /_opendistro/_security/api/ssl/transport/reload"); - final String expectedResponseString = expectedResponse.toString().replace("\\", ""); - Assert.assertEquals(expectedResponseString, reloadCertsResponse.getBody()); - } - - - @Test - public void testSSLReloadFail_UnAuthorizedUser() throws Exception { - initClusterWithTestCerts(); - // Test endpoint for non-admin user - RestHelper rh = getRestHelperNonAdminUser(); - - final RestHelper.HttpResponse reloadCertsResponse = rh.executePutRequest(RELOAD_TRANSPORT_CERTS_ENDPOINT, null); - Assert.assertEquals(401, reloadCertsResponse.getStatusCode()); - Assert.assertEquals("Unauthorized", reloadCertsResponse.getStatusReason()); - } - - @Test public void testSSLReloadFail_InvalidDNAndDate() throws Exception { initClusterWithTestCerts(); @@ -169,24 +143,6 @@ public void testSSLReloadFail_InvalidDNAndDate() throws Exception { Assert.assertEquals(expectedResponse.toString(), reloadCertsResponse.getBody()); } - @Test - public void testSSLReloadFail_NoReloadSet() throws Exception { - updateFiles(defaultCertFilePath, pemCertFilePath); - updateFiles(defaultKeyFilePath, pemKeyFilePath); - // This is when SSLCertReload property is set to false - initTestCluster(pemCertFilePath, pemKeyFilePath, pemCertFilePath, pemKeyFilePath, false); - - RestHelper rh = getRestHelperAdminUser(); - - final RestHelper.HttpResponse reloadCertsResponse = rh.executePutRequest(RELOAD_TRANSPORT_CERTS_ENDPOINT, null); - Assert.assertEquals(400, reloadCertsResponse.getStatusCode()); - JSONObject expectedResponse = new JSONObject(); - expectedResponse.appendField("error", "no handler found for uri [/_opendistro/_security/api/ssl/transport/reloadcerts] and method [PUT]"); - // Note: toString and toJSONString replace / with \/. This helps get rid of the additional \ character. - final String expectedResponseString = expectedResponse.toString().replace("\\", ""); - Assert.assertEquals(expectedResponseString, reloadCertsResponse.getBody()); - } - @Test public void testReloadTransportSSLSameCertsPass() throws Exception { initClusterWithTestCerts(); diff --git a/src/test/resources/restapi/internal_users.yml b/src/test/resources/restapi/internal_users.yml index 0049ab8c86..658d3f3aa1 100644 --- a/src/test/resources/restapi/internal_users.yml +++ b/src/test/resources/restapi/internal_users.yml @@ -61,3 +61,69 @@ admin_all_access: - "vulcan" attributes: {} description: "sample user with all_access, used to test whitelisting" +rest_api_admin_user: + hash: "$2y$12$X5ZamIheHYc2bihGTbK66Oe1.1vJ19akH0OFGF7TvI2BhbbED.KcO" + reserved: false + hidden: false + backend_roles: [] + description: "REST API Full Access admin user" +rest_api_admin_actiongroups: + hash: "$2y$12$XE9zXOgyYBBxnzoWMowIsO1w6o6oIc5w3vgwYjdEE44m9MxFZEuR." + reserved: false + hidden: false + backend_roles: [] + description: "REST API Action groups admin user" +rest_api_admin_allowlist: + hash: "$2y$12$W5AdCO/j08KiDu7EF/1Zf.nkcQM/7s.TtAdN2pRpbDM31xXcIIJUq" + reserved: false + hidden: false + backend_roles: [] + description: "REST API Allow list admin user" +rest_api_admin_audit: + hash: "$2y$12$UEbBqz9S6xuEefbK3LDvge5MwX4V1GvJYzUP8M24ItkfXMXg/NSh6" + reserved: false + hidden: false + backend_roles: [] + description: "REST API Audit admin user" +rest_api_admin_internalusers: + hash: "$2y$12$pUn1a6jdIeR.stkvEqNe5uK3rOY7Dj3uQfE8Cvd2bjNjTQ2HbsBMK" + reserved: false + hidden: false + backend_roles: [] + description: "REST API Internal users admin user" +rest_api_admin_nodesdn: + hash: "$2y$12$xFUIepz0vILRMzMkZMGY1Ow1P1eJo8TJ2oGiaFXaenGrOMsmDnKZS" + reserved: false + hidden: false + backend_roles: [] + description: "REST API NodesDN admin user" +rest_api_admin_roles: + hash: "$2y$12$BR.CBsElNLj8v2dzpHJ7bOKVLwWKWjKDhlEvBIvAe9b6/m0xWy2Bq" + reserved: false + hidden: false + backend_roles: [] + description: "REST API Roles admin user" +rest_api_admin_rolesmapping: + hash: "$2y$12$WQb7PsnRRr04zxjuZsDwU.F7QEr7W0f/rJLjUNLf50hpoJuTqqnaS" + reserved: false + hidden: false + backend_roles: [] + description: "REST API Roles Mapping admin user" +rest_api_admin_ssl_info: + hash: "$2y$12$irI4k0eKE8z9OXEd1jO4eeQfPV8WRMfttzutAhEeRBWy5XNXOlpr." + reserved: false + hidden: false + backend_roles: [] + description: "REST API SSL Certs admin user" +rest_api_admin_ssl_reloadcerts: + hash: "$2y$12$DxNdaBBMvTq5wO5XlnwlTeGSaC7yNoFoJt2N5TVtraopxPnGjMol2" + reserved: false + hidden: false + backend_roles: [] + description: "REST API SSL Reload Certs admin user" +rest_api_admin_tenants: + hash: "$2y$12$q05T7m7DFtkLLj.MVJ6jjuZkAywG4ZwaNi9fiYn6XCJelN2TUXCy2" + reserved: false + hidden: false + backend_roles: [] + description: "REST API Tenats admin user" diff --git a/src/test/resources/restapi/roles.yml b/src/test/resources/restapi/roles.yml index d82382e4f6..6deb194e9b 100644 --- a/src/test/resources/restapi/roles.yml +++ b/src/test/resources/restapi/roles.yml @@ -393,3 +393,51 @@ opendistro_security_role_starfleet_captains: allowed_actions: - "CRUD_UT" tenant_permissions: [] +rest_api_admin_full_access: + reserved: true + cluster_permissions: + - 'restapi:admin/actiongroups' + - 'restapi:admin/allowlist' + - 'restapi:admin/internalusers' + - 'restapi:admin/nodesdn' + - 'restapi:admin/roles' + - 'restapi:admin/rolesmapping' + - 'restapi:admin/ssl/certs/info' + - 'restapi:admin/ssl/certs/reload' + - 'restapi:admin/tenants' +rest_api_admin_actiongroups_only: + reserved: true + cluster_permissions: + - 'restapi:admin/actiongroups' +rest_api_admin_allowlist_only: + reserved: true + cluster_permissions: + - 'restapi:admin/allowlist' +rest_api_admin_internalusers_only: + reserved: true + cluster_permissions: + - 'restapi:admin/internalusers' +rest_api_admin_nodesdn_only: + reserved: true + cluster_permissions: + - 'restapi:admin/nodesdn' +rest_api_admin_roles_only: + reserved: true + cluster_permissions: + - 'restapi:admin/roles' +rest_api_admin_rolesmapping_only: + reserved: true + cluster_permissions: + - 'restapi:admin/rolesmapping' +rest_api_admin_ssl_info_only: + reserved: true + cluster_permissions: + - 'restapi:admin/ssl/certs/info' +rest_api_admin_ssl_reloadcerts_only: + reserved: true + cluster_permissions: + - 'restapi:admin/ssl/certs/reload' +rest_api_admin_tenants_only: + reserved: true + cluster_permissions: + - 'restapi:admin/tenants' diff --git a/src/test/resources/restapi/roles_mapping.yml b/src/test/resources/restapi/roles_mapping.yml index 8c46942854..a87287d5ff 100644 --- a/src/test/resources/restapi/roles_mapping.yml +++ b/src/test/resources/restapi/roles_mapping.yml @@ -185,6 +185,16 @@ opendistro_security_test: hosts: [] users: - "test" + - "rest_api_admin_user" + - "rest_api_admin_nodesdn" + - "rest_api_admin_allowlist" + - "rest_api_admin_roles" + - "rest_api_admin_rolesmapping" + - "rest_api_admin_actiongroups" + - "rest_api_admin_internalusers" + - "rest_api_admin_tenants" + - "rest_api_admin_ssl_info" + - "rest_api_admin_ssl_reloadcerts" and_backend_roles: [] description: "Migrated from v6" opendistro_security_role_starfleet_captains: @@ -206,3 +216,47 @@ opendistro_security_role_host2: - "opendistro_security_host_localhost" and_backend_roles: [] description: "Migrated from v6" +rest_api_admin_full_access: + reserved: false + hidden: true + users: [rest_api_admin_user] +rest_api_admin_actiongroups_only: + reserved: false + hidden: true + users: [rest_api_admin_actiongroups] +rest_api_admin_allowlist_only: + reserved: false + hidden: true + users: [rest_api_admin_allowlist] +rest_api_admin_audit_only: + reserved: false + hidden: true + users: [rest_api_admin_audit] +rest_api_admin_internalusers_only: + reserved: false + hidden: true + users: [rest_api_admin_internalusers] +rest_api_admin_nodesdn_only: + reserved: false + hidden: true + users: [rest_api_admin_nodesdn] +rest_api_admin_roles_only: + reserved: false + hidden: true + users: [rest_api_admin_roles] +rest_api_admin_rolesmapping_only: + reserved: false + hidden: true + users: [rest_api_admin_rolesmapping] +rest_api_admin_ssl_info_only: + reserved: false + hidden: true + users: [rest_api_admin_ssl_info] +rest_api_admin_ssl_reloadcerts_only: + reserved: false + hidden: true + users: [rest_api_admin_ssl_reloadcerts] +rest_api_admin_tenants_only: + reserved: false + hidden: true + users: [rest_api_admin_tenants] diff --git a/src/test/resources/restapi/roles_tenants.yml b/src/test/resources/restapi/roles_tenants.yml index 93b510dd16..e9b724e342 100644 --- a/src/test/resources/restapi/roles_tenants.yml +++ b/src/test/resources/restapi/roles_tenants.yml @@ -2,3 +2,13 @@ _meta: type: "tenants" config_version: 2 +some_admin_tenant: + reserved: false + description: "Demo tenant for admin user" +hidden_tenant: + reserved: true + hidden: true + description: "Hidden tenant" +reserved_tenant: + reserved: true + description: "Reserved tenant"