diff --git a/config/roles.yml b/config/roles.yml index 29f6fcbe5d..da593092a7 100644 --- a/config/roles.yml +++ b/config/roles.yml @@ -9,7 +9,21 @@ 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 + hidden: 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 e70aa01912..3b77212fa7 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -156,8 +156,6 @@ import org.opensearch.security.ssl.OpenSearchSecuritySSLPlugin; import org.opensearch.security.ssl.SslExceptionHandler; import org.opensearch.security.ssl.http.netty.ValidatingDispatcher; -import org.opensearch.security.ssl.rest.SecuritySSLCertsInfoAction; -import org.opensearch.security.ssl.rest.SecuritySSLReloadCertsAction; import org.opensearch.security.ssl.transport.DefaultPrincipalExtractor; import org.opensearch.security.ssl.transport.SecuritySSLNettyTransport; import org.opensearch.security.ssl.util.SSLConfigConstants; @@ -466,18 +464,25 @@ public List getRestHandlers(Settings settings, RestController restC if(!SSLConfig.isSslOnlyMode()) { handlers.add(new SecurityInfoAction(settings, restController, Objects.requireNonNull(evaluator), Objects.requireNonNull(threadPool))); handlers.add(new SecurityHealthAction(settings, restController, Objects.requireNonNull(backendRegistry))); - handlers.add(new SecuritySSLCertsInfoAction(settings, restController, sks, Objects.requireNonNull(threadPool), Objects.requireNonNull(adminDns))); handlers.add(new DashboardsInfoAction(settings, restController, Objects.requireNonNull(evaluator), Objects.requireNonNull(threadPool))); handlers.add(new TenantInfoAction(settings, restController, Objects.requireNonNull(evaluator), Objects.requireNonNull(threadPool), - Objects.requireNonNull(cs), Objects.requireNonNull(adminDns), Objects.requireNonNull(cr))); - handlers.add(new SecurityConfigUpdateAction(settings, restController,Objects.requireNonNull(threadPool), adminDns, configPath, principalExtractor)); - handlers.add(new SecurityWhoAmIAction(settings ,restController,Objects.requireNonNull(threadPool), adminDns, configPath, principalExtractor)); - if (sslCertReloadEnabled) { - handlers.add(new SecuritySSLReloadCertsAction(settings, restController, sks, Objects.requireNonNull(threadPool), Objects.requireNonNull(adminDns))); - } - final Collection apiHandlers = SecurityRestApiActions.getHandler(settings, configPath, restController, localClient, adminDns, cr, cs, principalExtractor, evaluator, threadPool, Objects.requireNonNull(auditLog)); - handlers.addAll(apiHandlers); - log.debug("Added {} management rest handler(s)", apiHandlers.size()); + Objects.requireNonNull(cs), Objects.requireNonNull(adminDns), Objects.requireNonNull(cr))); + handlers.add(new SecurityConfigUpdateAction(settings, restController, Objects.requireNonNull(threadPool), adminDns, configPath, principalExtractor)); + handlers.add(new SecurityWhoAmIAction(settings, restController, Objects.requireNonNull(threadPool), adminDns, configPath, principalExtractor)); + handlers.addAll( + SecurityRestApiActions.getHandler( + settings, + configPath, + restController, + localClient, + adminDns, + cr, cs, principalExtractor, + evaluator, + threadPool, + Objects.requireNonNull(auditLog), sks, + sslCertReloadEnabled) + ); + log.debug("Added {} rest handler(s)", handlers.size()); } } diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/AbstractApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/AbstractApiAction.java index 8ceb7eaaae..66c4fe85c0 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/AbstractApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/AbstractApiAction.java @@ -75,9 +75,9 @@ public abstract class AbstractApiAction extends BaseRestHandler { final ThreadPool threadPool; protected String opendistroIndex; private final RestApiPrivilegesEvaluator restApiPrivilegesEvaluator; + protected final RestApiAdminPrivilegesEvaluator restApiAdminPrivilegesEvaluator; protected final AuditLog auditLog; protected final Settings settings; - private AdminDNs adminDNs; protected AbstractApiAction(final Settings settings, final Path configPath, final RestController controller, final Client client, final AdminDNs adminDNs, final ConfigurationRepository cl, @@ -88,12 +88,13 @@ protected AbstractApiAction(final Settings settings, final Path configPath, fina this.opendistroIndex = settings.get(ConfigConstants.SECURITY_CONFIG_INDEX_NAME, ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX); - this.adminDNs = adminDNs; this.cl = cl; this.cs = cs; this.threadPool = threadPool; this.restApiPrivilegesEvaluator = new RestApiPrivilegesEvaluator(settings, adminDNs, evaluator, principalExtractor, configPath, threadPool); + this.restApiAdminPrivilegesEvaluator = + new RestApiAdminPrivilegesEvaluator(threadPool.getThreadContext(), evaluator, adminDNs); this.auditLog = auditLog; } @@ -195,7 +196,12 @@ protected void handlePut(final RestChannel channel, final RestRequest request, f } boolean existed = existingConfiguration.exists(name); - existingConfiguration.putCObject(name, DefaultObjectMapper.readTree(content, existingConfiguration.getImplementingClass())); + final Object newContent = DefaultObjectMapper.readTree(content, existingConfiguration.getImplementingClass()); + if (!hasPermissionsToCreate(existingConfiguration, newContent, getResourceName())) { + forbidden(channel, "No permissions"); + return; + } + existingConfiguration.putCObject(name, newContent); saveAnUpdateConfigs(client, request, getConfigName(), existingConfiguration, new OnSucessActionListener(channel) { @@ -212,6 +218,12 @@ public void onResponse(IndexResponse response) { } + protected boolean hasPermissionsToCreate(final SecurityDynamicConfiguration dynamicConfigFactory, + final Object content, + final String resourceName) throws IOException { + return true; + } + protected void handlePost(final RestChannel channel, final RestRequest request, final Client client, final JsonNode content) throws IOException { notImplemented(channel, Method.POST); } @@ -448,7 +460,6 @@ protected static XContentBuilder convertToJson(RestChannel channel, ToXContent t } protected void response(RestChannel channel, RestStatus status, String message) { - try { final XContentBuilder builder = channel.newBuilder(); builder.startObject(); @@ -558,8 +569,7 @@ public String getName() { protected abstract Endpoint getEndpoint(); protected boolean isSuperAdmin() { - User user = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); - return adminDNs.isAdmin(user); + return restApiAdminPrivilegesEvaluator.isCurrentUserRestApiAdminFor(getEndpoint()); } /** diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/ActionGroupsApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/ActionGroupsApiAction.java index 83d1a993ff..64b974d8d5 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,33 @@ 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 super.hasPermissionsToCreate(dynamicConfiguration, content, resourceName); + } + + @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/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/PatchableResourceApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/PatchableResourceApiAction.java index 74abb1d10a..f8b30d0231 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; } @@ -172,6 +172,10 @@ public void onResponse(IndexResponse response) { }); } + protected boolean verifyPermissionFor(final JsonNode jsonNode, final Method method) { + return true; + } + private void handleBulkPatch(RestChannel channel, RestRequest request, Client client, SecurityDynamicConfiguration existingConfiguration, ObjectNode existingAsObjectNode, JsonNode jsonPatch) throws IOException { @@ -188,7 +192,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 +209,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 +225,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..86648ae328 --- /dev/null +++ b/src/main/java/org/opensearch/security/dlic/rest/api/RestApiAdminPrivilegesEvaluator.java @@ -0,0 +1,160 @@ +/* + * 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); + + 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": + return buildEndpointActionPermission(Endpoint.SSL, "certs/info"); + case "reloadcerts": + 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..3f4604b681 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 super.hasPermissionsToCreate(dynamicConfiguration, content, resourceName); + } + } + + @Override + protected boolean isReadOnly(SecurityDynamicConfiguration existingConfiguration, String name) { + if (restApiAdminPrivilegesEvaluator.containsRestApiAdminPermissions(existingConfiguration.getCEntry(name))) { + return !isSuperAdmin(); + } else { + return super.isReadOnly(existingConfiguration, name); + } + } + } diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/RolesMappingApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/RolesMappingApiAction.java index c7e7f3d7ec..b4993340c8 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/RolesMappingApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/RolesMappingApiAction.java @@ -68,11 +68,18 @@ protected void handlePut(RestChannel channel, final RestRequest request, final C return; } + final SecurityDynamicConfiguration rolesConfiguration = load(CType.ROLES, false); final SecurityDynamicConfiguration rolesMappingConfiguration = load(getConfigName(), false); final boolean rolesMappingExists = rolesMappingConfiguration.exists(name); if (!isValidRolesMapping(channel, name)) return; + if (restApiAdminPrivilegesEvaluator.containsRestApiAdminPermissions(rolesConfiguration.getCEntry(name))) { + if (!isSuperAdmin()) { + forbidden(channel, "No permissions"); + return; + } + } rolesMappingConfiguration.putCObject(name, DefaultObjectMapper.readTree(content, rolesMappingConfiguration.getImplementingClass())); saveAnUpdateConfigs(client, request, getConfigName(), rolesMappingConfiguration, new OnSucessActionListener(channel) { @@ -89,6 +96,26 @@ public void onResponse(IndexResponse response) { }); } + @Override + protected boolean hasPermissionsToCreate(final SecurityDynamicConfiguration dynamicConfigFactory, final Object content, final String resourceName) throws IOException { + final SecurityDynamicConfiguration rolesConfiguration = load(CType.ROLES, false); + if (restApiAdminPrivilegesEvaluator.containsRestApiAdminPermissions(rolesConfiguration.getCEntry(resourceName))) { + return isSuperAdmin(); + } else { + return super.hasPermissionsToCreate(dynamicConfigFactory, content, resourceName); + } + } + + @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/SecurityRestApiActions.java b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java index 54ea46efd1..9655ba67ea 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java @@ -26,15 +26,26 @@ import org.opensearch.security.configuration.AdminDNs; import org.opensearch.security.configuration.ConfigurationRepository; import org.opensearch.security.privileges.PrivilegesEvaluator; +import org.opensearch.security.ssl.SecurityKeyStore; import org.opensearch.security.ssl.transport.PrincipalExtractor; import org.opensearch.threadpool.ThreadPool; public class SecurityRestApiActions { - public static Collection getHandler(Settings settings, Path configPath, RestController controller, Client client, - AdminDNs adminDns, ConfigurationRepository cr, ClusterService cs, PrincipalExtractor principalExtractor, - final PrivilegesEvaluator evaluator, ThreadPool threadPool, AuditLog auditLog) { - final List handlers = new ArrayList(15); + public static Collection getHandler(final Settings settings, + final Path configPath, + final RestController controller, + final Client client, + final AdminDNs adminDns, + final ConfigurationRepository cr, + final ClusterService cs, + final PrincipalExtractor principalExtractor, + final PrivilegesEvaluator evaluator, + final ThreadPool threadPool, + final AuditLog auditLog, + final SecurityKeyStore securityKeyStore, + final boolean certificatesReloadEnabled) { + final List handlers = new ArrayList(16); handlers.add(new InternalUsersApiAction(settings, configPath, controller, client, adminDns, cr, cs, principalExtractor, evaluator, threadPool, auditLog)); handlers.add(new RolesMappingApiAction(settings, configPath, controller, client, adminDns, cr, cs, principalExtractor, evaluator, threadPool, auditLog)); handlers.add(new RolesApiAction(settings, configPath, controller, client, adminDns, cr, cs, principalExtractor, evaluator, threadPool, auditLog)); @@ -51,6 +62,7 @@ public static Collection getHandler(Settings settings, Path configP handlers.add(new WhitelistApiAction(settings, configPath, controller, client, adminDns, cr, cs, principalExtractor, evaluator, threadPool, auditLog)); handlers.add(new AllowlistApiAction(settings, configPath, controller, client, adminDns, cr, cs, principalExtractor, evaluator, threadPool, auditLog)); handlers.add(new AuditApiAction(settings, configPath, controller, client, adminDns, cr, cs, principalExtractor, evaluator, threadPool, auditLog)); + handlers.add(new SecuritySSLCertsAction(settings, configPath, controller, client, adminDns, cr, cs, principalExtractor, evaluator, threadPool, auditLog, securityKeyStore, certificatesReloadEnabled)); return Collections.unmodifiableCollection(handlers); } diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/SecuritySSLCertsAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/SecuritySSLCertsAction.java new file mode 100644 index 0000000000..572b85bca4 --- /dev/null +++ b/src/main/java/org/opensearch/security/dlic/rest/api/SecuritySSLCertsAction.java @@ -0,0 +1,338 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.dlic.rest.api; + +import java.io.IOException; +import java.nio.file.Path; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.client.Client; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.bytes.BytesReference; +import org.opensearch.common.settings.Settings; +import org.opensearch.common.xcontent.XContentBuilder; +import org.opensearch.rest.BytesRestResponse; +import org.opensearch.rest.RestChannel; +import org.opensearch.rest.RestController; +import org.opensearch.rest.RestRequest; +import org.opensearch.rest.RestRequest.Method; +import org.opensearch.rest.RestStatus; +import org.opensearch.security.auditlog.AuditLog; +import org.opensearch.security.configuration.AdminDNs; +import org.opensearch.security.configuration.ConfigurationRepository; +import org.opensearch.security.dlic.rest.validation.AbstractConfigurationValidator; +import org.opensearch.security.privileges.PrivilegesEvaluator; +import org.opensearch.security.securityconf.impl.CType; +import org.opensearch.security.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 + 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 /_security/api/ssl/transport/reloadcerts + * PUT /_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/support/Utils.java b/src/main/java/org/opensearch/security/dlic/rest/support/Utils.java index 047a649718..099087523b 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/support/Utils.java +++ b/src/main/java/org/opensearch/security/dlic/rest/support/Utils.java @@ -29,12 +29,15 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; +import org.apache.commons.lang3.tuple.Pair; import org.bouncycastle.crypto.generators.OpenBSDBCrypt; import org.opensearch.ExceptionsHelper; import org.opensearch.OpenSearchParseException; import org.opensearch.SpecialPermission; import org.opensearch.common.bytes.BytesReference; +import org.opensearch.common.transport.TransportAddress; +import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.common.xcontent.NamedXContentRegistry; import org.opensearch.common.xcontent.ToXContent; import org.opensearch.common.xcontent.XContentHelper; @@ -44,6 +47,8 @@ import org.opensearch.rest.RestHandler.DeprecatedRoute; import org.opensearch.rest.RestHandler.Route; import org.opensearch.security.DefaultObjectMapper; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.user.User; import static org.opensearch.common.xcontent.DeprecationHandler.THROW_UNSUPPORTED_OPERATION; @@ -265,4 +270,11 @@ public static List addDeprecatedRoutesPrefix(List new DeprecatedRoute(r.getMethod(), p + r.getPath(), r.getDeprecationMessage()))) .collect(ImmutableList.toImmutableList()); } + + public static Pair userAndRemoteAddressFrom(final ThreadContext threadContext) { + final User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + final TransportAddress remoteAddress = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS); + return Pair.of(user, remoteAddress); + } + } diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java index df9c432827..9967859d9f 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java @@ -179,6 +179,18 @@ private SecurityRoles getSecurityRoles(Set roles) { return configModel.getSecurityRoles().filter(roles); } + public boolean hasRestAdminPermissions(final User user, + final TransportAddress remoteAddress, + final String permissions) { + final Set userRoles = mapRoles(user, remoteAddress); + return hasRestAdminPermissions(userRoles, permissions); + } + + private boolean hasRestAdminPermissions(final Set roles, String permission) { + final SecurityRoles securityRoles = getSecurityRoles(roles); + return securityRoles.hasExplicitClusterPermissionPermission(permission); + } + public boolean isInitialized() { return configModel !=null && configModel.getSecurityRoles() != null && dcm != null; } diff --git a/src/main/java/org/opensearch/security/securityconf/ConfigModelV6.java b/src/main/java/org/opensearch/security/securityconf/ConfigModelV6.java index a9ba2d7142..c401750c43 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() == 1; + } + //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 cbd7cb8301..ab3c47d911 100644 --- a/src/main/java/org/opensearch/security/securityconf/ConfigModelV7.java +++ b/src/main/java/org/opensearch/security/securityconf/ConfigModelV7.java @@ -479,10 +479,18 @@ public boolean get(Resolved resolved, User user, String[] actions, IndexNameExpr return false; } + @Override public boolean impliesClusterPermissionPermission(String action) { return roles.stream().filter(r -> r.impliesClusterPermission(action)).count() > 0; } + @Override + public boolean hasExplicitClusterPermissionPermission(String action) { + return roles.stream() + .map(r -> r.clusterPerms == WildcardMatcher.ANY ? WildcardMatcher.NONE : r.clusterPerms) + .filter(m -> m.test(action)).count() > 0; + } + //rolespan public boolean impliesTypePermGlobal(Resolved resolved, User user, String[] actions, IndexNameExpressionResolver resolver, ClusterService cs) { diff --git a/src/main/java/org/opensearch/security/securityconf/SecurityRoles.java b/src/main/java/org/opensearch/security/securityconf/SecurityRoles.java index d71b279afb..478e0f03dd 100644 --- a/src/main/java/org/opensearch/security/securityconf/SecurityRoles.java +++ b/src/main/java/org/opensearch/security/securityconf/SecurityRoles.java @@ -39,6 +39,8 @@ public interface SecurityRoles { boolean impliesClusterPermissionPermission(String action0); + boolean hasExplicitClusterPermissionPermission(String action); + Set getRoleNames(); Set reduce(Resolved requestedResolved, User user, String[] strings, IndexNameExpressionResolver resolver, ClusterService clusterService); diff --git a/src/main/java/org/opensearch/security/ssl/rest/SecuritySSLCertsInfoAction.java b/src/main/java/org/opensearch/security/ssl/rest/SecuritySSLCertsInfoAction.java deleted file mode 100644 index 6c1ec5296c..0000000000 --- a/src/main/java/org/opensearch/security/ssl/rest/SecuritySSLCertsInfoAction.java +++ /dev/null @@ -1,190 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.security.ssl.rest; - -import java.io.IOException; -import java.security.cert.X509Certificate; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import org.opensearch.client.node.NodeClient; -import org.opensearch.common.settings.Settings; -import org.opensearch.common.util.concurrent.ThreadContext; -import org.opensearch.common.xcontent.XContentBuilder; -import org.opensearch.rest.BaseRestHandler; -import org.opensearch.rest.BytesRestResponse; -import org.opensearch.rest.RestChannel; -import org.opensearch.rest.RestController; -import org.opensearch.rest.RestRequest; -import org.opensearch.rest.RestRequest.Method; -import org.opensearch.rest.RestStatus; -import org.opensearch.security.configuration.AdminDNs; -import org.opensearch.security.ssl.SecurityKeyStore; -import org.opensearch.security.support.ConfigConstants; -import org.opensearch.security.user.User; -import org.opensearch.threadpool.ThreadPool; - -import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; - - -/** - * Rest API action to get SSL certificate information related to http and transport encryption. - * Only super admin users are allowed to access this API. - * Currently this action serves GET request for _plugins/_security/api/ssl/certs endpoint - */ -public class SecuritySSLCertsInfoAction extends BaseRestHandler { - private static final List routes = addRoutesPrefix(ImmutableList.of( - new Route(Method.GET, "/ssl/certs") - )); - - private final Logger log = LogManager.getLogger(this.getClass()); - private Settings settings; - private SecurityKeyStore odsks; - private AdminDNs adminDns; - private ThreadContext threadContext; - - public SecuritySSLCertsInfoAction(final Settings settings, - final RestController restController, - final SecurityKeyStore odsks, - final ThreadPool threadPool, - final AdminDNs adminDns) { - super(); - this.settings = settings; - this.odsks = odsks; - this.adminDns = adminDns; - this.threadContext = threadPool.getThreadContext(); - } - - @Override - public List routes() { - return routes; - } - - /** - * GET request to fetch transport certificate details - * - * Sample request: - * GET _plugins/_security/api/ssl/certs - * - * Sample response: - * { - * "http_certificates_list" : [ - * { - * "issuer_dn" : "CN=Example Com Inc. Signing CA, OU=Example Com Inc. Signing CA, O=Example Com Inc., DC=example, DC=com", - * "subject_dn" : "CN=transport-0.example.com, OU=SSL, O=Test, L=Test, C=DE", - * "san", "[[2, node-0.example.com], [2, localhost], [7, 127.0.0.1], [8, 1.2.3.4.5.5]]", - * "not_before" : "2018-05-05T14:37:09.000Z", - * "not_after" : "2028-05-02T14:37:09.000Z" - * } - * "transport_certificates_list" : [ - * { - * "issuer_dn" : "CN=Example Com Inc. Signing CA, OU=Example Com Inc. Signing CA, O=Example Com Inc., DC=example, DC=com", - * "subject_dn" : "CN=transport-0.example.com, OU=SSL, O=Test, L=Test, C=DE", - * "san", "[[2, node-0.example.com], [2, localhost], [7, 127.0.0.1], [8, 1.2.3.4.5.5]]", - * "not_before" : "2018-05-05T14:37:09.000Z", - * "not_after" : "2028-05-02T14:37:09.000Z" - * } - * ] - * } - * - * @param request request to be served - * @param client client - * @throws IOException - */ - @Override - protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { - - return new RestChannelConsumer() { - - @Override - public void accept(RestChannel channel) throws Exception { - XContentBuilder builder = channel.newBuilder(); - BytesRestResponse response = null; - - // Check for Super admin user - final User user = (User)threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); - if(user == null || !adminDns.isAdmin(user)) { - response = new BytesRestResponse(RestStatus.FORBIDDEN, builder); - } else { - try { - // Check if keystore initialised - if (odsks != null) { - builder.startObject(); - builder.field("http_certificates_list", generateCertDetailList(odsks.getHttpCerts())); - builder.field("transport_certificates_list", generateCertDetailList(odsks.getTransportCerts())); - builder.endObject(); - response = new BytesRestResponse(RestStatus.OK, builder); - } else { - builder.startObject(); - builder.field("message", "keystore is not initialized"); - builder.endObject(); - response = new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, builder); - } - } catch (final Exception e1) { - log.error("Error handle request ", e1); - builder = channel.newBuilder(); - builder.startObject(); - builder.field("error", e1.toString()); - builder.endObject(); - response = new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, builder); - } finally { - if (builder != null) { - builder.close(); - } - } - } - channel.sendResponse(response); - } - - /** - * Helper that construct list of certificate details. - * @param certs list of certificates. - * @return Array containing certificate details. - */ - private List> generateCertDetailList(final X509Certificate[] certs) { - if (certs == null) { - return null; - } - return Arrays.stream(certs) - .map(cert -> { - final String issuerDn = cert != null && cert.getIssuerX500Principal() != null ? cert.getIssuerX500Principal().getName(): ""; - final String subjectDn = cert != null && cert.getSubjectX500Principal() != null ? cert.getSubjectX500Principal().getName(): ""; - - final String san = odsks.getSubjectAlternativeNames(cert); - - final String notBefore = cert != null && cert.getNotBefore() != null ? cert.getNotBefore().toInstant().toString(): ""; - final String notAfter = cert != null && cert.getNotAfter() != null ? cert.getNotAfter().toInstant().toString(): ""; - return ImmutableMap.builder() - .put("issuer_dn", issuerDn) - .put("subject_dn", subjectDn) - .put("san", san) - .put("not_before", notBefore) - .put("not_after", notAfter) - .build(); - }) - .collect(Collectors.toList()); - } - }; - } - - @Override - public String getName() { - return "SSL Certificate Information Action"; - } -} diff --git a/src/main/java/org/opensearch/security/ssl/rest/SecuritySSLReloadCertsAction.java b/src/main/java/org/opensearch/security/ssl/rest/SecuritySSLReloadCertsAction.java deleted file mode 100644 index ef4c89c625..0000000000 --- a/src/main/java/org/opensearch/security/ssl/rest/SecuritySSLReloadCertsAction.java +++ /dev/null @@ -1,155 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.security.ssl.rest; - -import java.io.IOException; -import java.util.Collections; -import java.util.List; - -import org.opensearch.client.node.NodeClient; -import org.opensearch.common.settings.Settings; -import org.opensearch.common.util.concurrent.ThreadContext; -import org.opensearch.common.xcontent.XContentBuilder; -import org.opensearch.rest.BaseRestHandler; -import org.opensearch.rest.BytesRestResponse; -import org.opensearch.rest.RestChannel; -import org.opensearch.rest.RestController; -import org.opensearch.rest.RestRequest; -import org.opensearch.rest.RestStatus; -import org.opensearch.security.configuration.AdminDNs; -import org.opensearch.security.ssl.SecurityKeyStore; -import org.opensearch.security.support.ConfigConstants; -import org.opensearch.security.user.User; -import org.opensearch.threadpool.ThreadPool; - -import static org.opensearch.rest.RestRequest.Method.PUT; - - -/** - * Rest API action to reload SSL certificates. - * Can be used to reload SSL certificates that are about to expire without restarting OpenSearch node. - * This API assumes that new certificates are in the same location specified by the security configurations in opensearch.yml - * (https://docs-beta.opensearch.org/docs/security-configuration/tls/) - * To keep sensitive certificate reload secure, this API will only allow hot reload - * with certificates issued by the same Issuer and Subject DN and SAN with expiry dates after the current one. - * Currently this action serves PUT request for /_opendistro/_security/ssl/http/reloadcerts or /_opendistro/_security/ssl/transport/reloadcerts endpoint - */ -public class SecuritySSLReloadCertsAction extends BaseRestHandler { - private static final List routes = Collections.singletonList( - new Route(PUT, "_opendistro/_security/api/ssl/{certType}/reloadcerts/") - ); - - private final Settings settings; - private final SecurityKeyStore sks; - private final ThreadContext threadContext; - private final AdminDNs adminDns; - - public SecuritySSLReloadCertsAction(final Settings settings, - final RestController restController, - final SecurityKeyStore sks, - final ThreadPool threadPool, - final AdminDNs adminDns) { - super(); - this.settings = settings; - this.sks = sks; - this.adminDns = adminDns; - this.threadContext = threadPool.getThreadContext(); - } - - @Override - public List routes() { - return routes; - } - - /** - * PUT request to reload SSL Certificates. - * - * Sample request: - * PUT _opendistro/_security/api/ssl/transport/reloadcerts - * PUT _opendistro/_security/api/ssl/http/reloadcerts - * - * NOTE: No request body is required. We will assume new certificates are loaded in the paths specified in your opensearch.yml file - * (https://docs-beta.opensearch.org/docs/security/configuration/tls/) - * - * Sample response: - * { "message": "updated http certs" } - * - * @param request request to be served - * @param client client - * @throws IOException - */ - @Override - protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { - return new RestChannelConsumer() { - - final String certType = request.param("certType").toLowerCase().trim(); - - @Override - public void accept(RestChannel channel) throws Exception { - XContentBuilder builder = channel.newBuilder(); - BytesRestResponse response = null; - - // Check for Super admin user - final User user = (User) threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); - if(user ==null||!adminDns.isAdmin(user)) { - response = new BytesRestResponse(RestStatus.FORBIDDEN, ""); - } else { - try { - builder.startObject(); - if (sks != null) { - switch (certType) { - case "http": - sks.initHttpSSLConfig(); - builder.field("message", "updated http certs"); - builder.endObject(); - response = new BytesRestResponse(RestStatus.OK, builder); - break; - case "transport": - sks.initTransportSSLConfig(); - builder.field("message", "updated transport certs"); - builder.endObject(); - response = new BytesRestResponse(RestStatus.OK, builder); - break; - default: - builder.field("message", "invalid uri path, please use /_opendistro/_security/api/ssl/http/reload or " + - "/_opendistro/_security/api/ssl/transport/reload"); - builder.endObject(); - response = new BytesRestResponse(RestStatus.FORBIDDEN, builder); - break; - } - } else { - builder.field("message", "keystore is not initialized"); - builder.endObject(); - response = new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, builder); - } - } catch (final Exception e1) { - builder = channel.newBuilder(); - builder.startObject(); - builder.field("error", e1.toString()); - builder.endObject(); - response = new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, builder); - } finally { - if (builder != null) { - builder.close(); - } - } - } - channel.sendResponse(response); - } - }; - } - - @Override - public String getName() { - return "SSL Cert Reload Action"; - } -} diff --git a/src/test/java/org/opensearch/security/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); - } -}