diff --git a/build.gradle b/build.gradle index ee2e33131b..b50a730307 100644 --- a/build.gradle +++ b/build.gradle @@ -289,7 +289,6 @@ configurations.all { } dependencies { - implementation 'jakarta.annotation:jakarta.annotation-api:1.3.5' implementation "org.opensearch.plugin:transport-netty4-client:${opensearch_version}" implementation "org.opensearch.client:opensearch-rest-high-level-client:${opensearch_version}" implementation 'com.google.guava:guava:30.0-jre' diff --git a/config/roles.yml b/config/roles.yml index 841af9dede..d03b47ab28 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 878b37d4dc..22c272065d 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -158,8 +158,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; @@ -473,16 +471,11 @@ 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))); - } - + 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, @@ -494,7 +487,9 @@ public List getRestHandlers(Settings settings, RestController restC evaluator, threadPool, Objects.requireNonNull(auditLog), - Objects.requireNonNull(userService)) + Objects.requireNonNull(userService), + 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 3c7f969e6c..26fcef3b3e 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 securityIndexName; 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.securityIndexName = 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); AbstractApiAction.saveAndUpdateConfigs(this.securityIndexName, client, 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{ @@ -445,7 +457,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(); @@ -556,8 +567,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 39f6e0c40b..5e4799d655 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 54c79b6675..cefaeb5c6c 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 e0cc5d2301..bf50523242 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 @@ -84,6 +84,13 @@ public InternalUsersApiAction(final Settings settings, final Path configPath, fi this.userService = userService; } + @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 970408e3d1..4bf3df16c9 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 @@ -94,6 +94,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 2d141a4318..deb56a69c7 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 4ed4ce48ba..72928cd0ad 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())); saveAndUpdateConfigs(this.securityIndexName,client, 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 bbe653d3ec..8c6a7656e1 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,6 +26,7 @@ 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.security.user.UserService; import org.opensearch.threadpool.ThreadPool; @@ -43,7 +44,9 @@ public static Collection getHandler(final Settings settings, final PrivilegesEvaluator evaluator, final ThreadPool threadPool, final AuditLog auditLog, - final UserService userService) { + final UserService userService, + 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, userService, auditLog)); handlers.add(new RolesMappingApiAction(settings, configPath, controller, client, adminDns, cr, cs, principalExtractor, evaluator, threadPool, auditLog)); @@ -62,6 +65,7 @@ public static Collection getHandler(final Settings settings, 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 MultiTenancyConfigApiAction(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..4168bf4109 --- /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.core.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 a305c9f8dd..aba2807846 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.XContentHelper; import org.opensearch.common.xcontent.XContentType; import org.opensearch.common.xcontent.json.JsonXContent; @@ -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.core.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 195b8b758e..2c0e7ac7c0 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 3638960c50..987b8fac64 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 ec75bbd08f..8b8dd9f3e3 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 ecd29cb20c..de7afbc27b 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 4e13f6f49c..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.core.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 c737e67398..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.core.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 20eb782853..7f05bfb18e 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 @@ -17,10 +17,14 @@ import java.util.HashMap; import java.util.Map; import java.util.Objects; +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; @@ -96,18 +100,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); @@ -120,6 +113,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; @@ -156,7 +163,7 @@ protected void addUserWithPassword(String username, String password, String[] ro } payload += "]}"; HttpResponse response = rh.executePutRequest("/_opendistro/_security/api/internalusers/" + username, payload, new Header[0]); - Assert.assertEquals(status, response.getStatusCode()); + Assert.assertEquals(response.getBody(), status, response.getStatusCode()); rh.sendAdminCertificate = sendAdminCertificate; } @@ -210,30 +217,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(); } @@ -273,4 +273,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 0e8a0a715f..d2df19997d 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", "picardpicardpicard", new String[] { "starfleet" }, HttpStatus.SC_CREATED); + checkReadAccess(HttpStatus.SC_OK, "picard", "picardpicardpicard", "sf", "_doc", 0); + checkWriteAccess(HttpStatus.SC_FORBIDDEN, "picard", "picardpicardpicard", "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,56 +78,46 @@ 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", "picardpicardpicard", new String[] { "starfleet" }, HttpStatus.SC_CREATED); - checkReadAccess(HttpStatus.SC_OK, "picard", "picardpicardpicard", "sf", "_doc", 0); - // TODO: only one doctype allowed for ES6 - // checkReadAccess(HttpStatus.SC_OK, "picard", "picard", "sf", "public", 0); - checkWriteAccess(HttpStatus.SC_FORBIDDEN, "picard", "picardpicardpicard", "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; @@ -125,24 +137,25 @@ public void testActionGroupsApi() throws Exception { rh.sendAdminCertificate = false; checkWriteAccess(HttpStatus.SC_FORBIDDEN, "picard", "picardpicardpicard", "sf", "_doc", 0); checkReadAccess(HttpStatus.SC_FORBIDDEN, "picard", "picardpicardpicard", "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 +165,8 @@ public void testActionGroupsApi() throws Exception { checkWriteAccess(HttpStatus.SC_OK, "picard", "picardpicardpicard", "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 +175,106 @@ public void testActionGroupsApi() throws Exception { checkWriteAccess(HttpStatus.SC_OK, "picard", "picardpicardpicard", "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 +285,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 +337,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 +346,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 +361,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", "picardpicardpicard", new String[] { "starfleet" }, HttpStatus.SC_CREATED); + checkReadAccess(HttpStatus.SC_OK, "picard", "picardpicardpicard", "sf", "_doc", 0); + checkWriteAccess(HttpStatus.SC_FORBIDDEN, "picard", "picardpicardpicard", "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", "picardpicardpicard", new String[] { "starfleet" }, HttpStatus.SC_CREATED); + checkReadAccess(HttpStatus.SC_OK, "picard", "picardpicardpicard", "sf", "_doc", 0); + checkWriteAccess(HttpStatus.SC_FORBIDDEN, "picard", "picardpicardpicard", "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 e2359ecd8e..06858c685b 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,72 +144,78 @@ 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", "picardpicardpicardpicard", new String[]{"starfleet", "captains"}, HttpStatus.SC_CREATED); + checkReadAccess(HttpStatus.SC_OK, "picard", "picardpicardpicardpicard", "sf", "_doc", 0); + checkWriteAccess(HttpStatus.SC_OK, "picard", "picardpicardpicardpicard", "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", "picardpicardpicardpicard", new String[] { "starfleet", "captains" }, HttpStatus.SC_CREATED); - checkReadAccess(HttpStatus.SC_OK, "picard", "picardpicardpicardpicard", "sf", "_doc", 0); - checkWriteAccess(HttpStatus.SC_OK, "picard", "picardpicardpicardpicard", "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", "picardpicardpicardpicard", "sf", "_doc", 1); @@ -194,30 +223,32 @@ public void testRolesApi() throws Exception { // So we also get a 403 FORBIDDEN when tring to add new document type checkWriteAccess(HttpStatus.SC_FORBIDDEN, "picard", "picardpicardpicardpicard", "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", "picardpicardpicardpicard", "sf", "_doc", 0); checkWriteAccess(HttpStatus.SC_FORBIDDEN, "picard", "picardpicardpicardpicard", "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 +258,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 +267,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", "picardpicardpicardpicard", "sf", "_doc", 0); @@ -258,41 +289,41 @@ public void testRolesApi() throws Exception { // - 'indices:*' checkWriteAccess(HttpStatus.SC_OK, "picard", "picardpicardpicardpicard", "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", "picardpicardpicardpicard", "sf", "_doc", 0); checkWriteAccess(HttpStatus.SC_OK, "picard", "picardpicardpicardpicard", "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 +336,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 +358,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 +372,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 +434,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", "picardpicardpicardpicard", new String[]{"starfleet", "captains"}, HttpStatus.SC_CREATED); + checkReadAccess(HttpStatus.SC_OK, "picard", "picardpicardpicardpicard", "sf", "_doc", 0); + checkWriteAccess(HttpStatus.SC_OK, "picard", "picardpicardpicardpicard", "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", "picardpicardpicardpicard", new String[]{"starfleet", "captains"}, HttpStatus.SC_CREATED); + checkReadAccess(HttpStatus.SC_OK, "picard", "picardpicardpicardpicard", "sf", "_doc", 0); + checkWriteAccess(HttpStatus.SC_OK, "picard", "picardpicardpicardpicard", "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 +767,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 6be5ceddb7..ee7a3a6cf5 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", "picardpicardpicardpicard", new String[] { "captains" }, HttpStatus.SC_CREATED); + checkWriteAccess(HttpStatus.SC_CREATED, "picard", "picardpicardpicardpicard", "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", "picardpicardpicardpicard", new String[] { "captains" }, HttpStatus.SC_CREATED); + checkWriteAccess(HttpStatus.SC_CREATED, "picard", "picardpicardpicardpicard", "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", "picardpicardpicardpicard", new String[] { "captains" }, HttpStatus.SC_CREATED); - checkWriteAccess(HttpStatus.SC_CREATED, "picard", "picardpicardpicardpicard", "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,34 +289,34 @@ 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 @@ -236,44 +326,46 @@ public void testRolesMappingApi() throws Exception { 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", "picardpicardpicardpicard", "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..e2b649770a --- /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.http.Header; +import org.apache.http.HttpStatus; +import org.junit.Assert; +import org.junit.Test; + +import org.opensearch.common.settings.Settings; +import org.opensearch.security.DefaultObjectMapper; +import org.opensearch.security.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 0c509e6bbd..c778a8f8f7 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 @@ -43,7 +43,7 @@ protected String getEndpointPrefix() { } - final int USER_SETTING_SIZE = 56; // Lines per account entry * number of accounts + final int USER_SETTING_SIZE = 133; // Lines per account entry * number of accounts private static final String ENABLED_SERVICE_ACCOUNT_BODY = "{" + " \"attributes\": { \"service\": \"true\", " @@ -168,7 +168,6 @@ private void verifyGet(final Header... header) throws Exception { // GET, new URL endpoint in security response = rh.executeGetRequest(ENDPOINT + "/user", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - } private void verifyPut(final Header... header) throws Exception { @@ -179,7 +178,7 @@ private void verifyPut(final Header... header) throws Exception { // Faulty JSON payload response = rh.executePutRequest(ENDPOINT + "/internalusers/nagilum", "{some: \"thing\" asd other: \"thing\"}", - header); + header); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); Settings settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); Assert.assertEquals(settings.get("reason"), AbstractConfigurationValidator.ErrorType.BODY_NOT_PARSEABLE.getMessage()); @@ -200,31 +199,30 @@ private void verifyPut(final Header... header) throws Exception { // Associating with hidden role is allowed (for superadmin) response = rh.executePutRequest(ENDPOINT + "/internalusers/test", "{ \"opendistro_security_roles\": " + - "[\"opendistro_security_hidden\"]}", header); + "[\"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\"}", - header); + "\"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\"]}", - header); + 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\"}", - header); + 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 { @@ -452,6 +450,7 @@ private void verifyAuthToken(final boolean sendAdminCert, Header... restAdminHea } private void verifyRoles(final boolean sendAdminCert, Header... header) throws Exception { + // wrong datatypes in roles file rh.sendAdminCertificate = sendAdminCert; HttpResponse response = rh.executePutRequest(ENDPOINT + "/internalusers/picard", FileHelper.loadFile("restapi/users_wrong_datatypes.json"), header); @@ -527,6 +526,42 @@ private void verifyRoles(final boolean sendAdminCert, Header... header) throws E 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 testRegExpPasswordRules() throws Exception { Settings nodeSettings = 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 36446f3d05..e1c8ec7282 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"