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 233eacda4e..eedf42a4d4 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 @@ -11,22 +11,11 @@ package org.opensearch.security.dlic.rest.api; -import java.io.IOException; -import java.io.UncheckedIOException; -import java.nio.file.Path; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.function.Predicate; - import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.JsonNode; import org.apache.commons.lang3.tuple.Pair; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; - import org.opensearch.ExceptionsHelper; import org.opensearch.action.index.IndexRequest; import org.opensearch.action.index.IndexResponse; @@ -38,8 +27,10 @@ import org.opensearch.common.util.concurrent.ThreadContext.StoredContext; import org.opensearch.common.xcontent.XContentHelper; import org.opensearch.common.xcontent.XContentType; +import org.opensearch.core.action.ActionListener; import org.opensearch.core.common.Strings; import org.opensearch.core.common.transport.TransportAddress; +import org.opensearch.core.rest.RestStatus; import org.opensearch.core.xcontent.ToXContent; import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.index.engine.VersionConflictEngineException; @@ -49,8 +40,6 @@ import org.opensearch.rest.RestController; import org.opensearch.rest.RestRequest; import org.opensearch.rest.RestRequest.Method; -import org.opensearch.core.action.ActionListener; -import org.opensearch.core.rest.RestStatus; import org.opensearch.security.action.configupdate.ConfigUpdateAction; import org.opensearch.security.action.configupdate.ConfigUpdateRequest; import org.opensearch.security.action.configupdate.ConfigUpdateResponse; @@ -58,6 +47,7 @@ import org.opensearch.security.configuration.AdminDNs; import org.opensearch.security.configuration.ConfigurationRepository; import org.opensearch.security.dlic.rest.support.Utils; +import org.opensearch.security.dlic.rest.validation.ConfigurationValidators; import org.opensearch.security.dlic.rest.validation.RequestContentValidator; import org.opensearch.security.dlic.rest.validation.ValidationResult; import org.opensearch.security.privileges.PrivilegesEvaluator; @@ -69,10 +59,16 @@ import org.opensearch.security.user.User; import org.opensearch.threadpool.ThreadPool; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + import static org.opensearch.security.dlic.rest.api.RequestHandler.methodNotImplementedHandler; import static org.opensearch.security.dlic.rest.api.Responses.badRequestMessage; import static org.opensearch.security.dlic.rest.api.Responses.forbiddenMessage; -import static org.opensearch.security.dlic.rest.api.Responses.notFoundMessage; import static org.opensearch.security.dlic.rest.api.Responses.payload; import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_ADMIN_ENABLED; @@ -89,6 +85,8 @@ public abstract class AbstractApiAction extends BaseRestHandler { protected final AuditLog auditLog; protected final Settings settings; + protected final ConfigurationValidators configurationValidators; + private Map requestHandlers; protected final RequestHandler.RequestHandlersBuilder requestHandlersBuilder; @@ -131,6 +129,8 @@ protected AbstractApiAction( settings.getAsBoolean(SECURITY_RESTAPI_ADMIN_ENABLED, false) ); this.auditLog = auditLog; + this.requestHandlers = null; + this.configurationValidators = new ConfigurationValidators(getEndpoint(), this.restApiAdminPrivilegesEvaluator); this.requestHandlersBuilder = new RequestHandler.RequestHandlersBuilder(); this.requestHandlersBuilder.configureRequestHandlers(this::buildDefaultRequestHandlers); } @@ -145,18 +145,43 @@ private void buildDefaultRequestHandlers(final RequestHandler.RequestHandlersBui } protected final ValidationResult processDeleteRequest(final RestRequest request) throws IOException { - return withRequiredResourceName(request).map(name -> loadSecurityConfiguration(name, false)) - .map(this::resourceExists) - .map(this::resourceCanBeChanged); + return withRequiredEntityName(request).map( + entityName -> loadConfiguration(getConfigurationType(), false, false).map( + configuration -> toSecurityConfiguration(entityName, configuration) + ) + ).map(this::deleteRequestConfigurationValidator).map(this::removeEntityFromConfiguration); } - protected final ValidationResult processGetRequest(final RestRequest request) throws IOException { - return loadFilteredConfiguration(nameParam(request)).map(this::resourceExists); + protected final ValidationResult deleteRequestConfigurationValidator( + final SecurityConfiguration securityConfiguration + ) throws IOException { + return configurationHasEntity(securityConfiguration).map(configurationValidators::hasRightsToChangeImmutableEntity); + } + + protected final ValidationResult configurationHasEntity(final SecurityConfiguration securityConfiguration) { + return configurationValidators.entityExists(getConfigurationName(), securityConfiguration); + } + + protected final ValidationResult> processGetRequest(final RestRequest request) throws IOException { + return loadConfiguration(getConfigurationType(), true, true).map( + configuration -> toSecurityConfiguration(nameParam(request), configuration) + ).map(this::configurationHasEntity).map(configurationValidators::hasRightsToLoadOrChangeHiddenEntity).map(securityConfiguration -> { + final var configuration = securityConfiguration.configuration(); + if (securityConfiguration.maybeEntityName().isPresent()) { + configuration.removeOthers(securityConfiguration.entityName()); + } + return ValidationResult.success(configuration); + }); } protected final ValidationResult processPutRequest(final RestRequest request) throws IOException { - return withRequiredResourceName(request).map(name -> loadSecurityConfigurationWithRequestContent(name, request)) - .map(this::resourceCanBeChanged); + return withRequiredEntityName(request).map(name -> loadConfigurationWithRequestContent(name, request)) + .map( + contentAndSecurityConfiguration -> configurationValidators.hasRightsToChangeImmutableEntity( + contentAndSecurityConfiguration.getRight() + ).map(ignore -> ValidationResult.success(contentAndSecurityConfiguration)) + ) + .map(this::addEntityToConfiguration); } final void saveOrUpdateConfiguration( @@ -164,7 +189,7 @@ final void saveOrUpdateConfiguration( final SecurityDynamicConfiguration configuration, final OnSucessActionListener onSucessActionListener ) { - saveAndUpdateConfigs(this.securityIndexName, client, getConfigName(), configuration, onSucessActionListener); + saveAndUpdateConfigs(this.securityIndexName, client, getConfigurationType(), configuration, onSucessActionListener); } protected final String nameParam(final RestRequest request) { @@ -175,49 +200,61 @@ protected final String nameParam(final RestRequest request) { return name; } - protected final ValidationResult withRequiredResourceName(final RestRequest request) { + protected final ValidationResult withRequiredEntityName(final RestRequest request) { final var resourceName = nameParam(request); if (resourceName == null) { - return ValidationResult.error(RestStatus.BAD_REQUEST, badRequestMessage("No " + getResourceName() + " specified.")); + return ValidationResult.error(RestStatus.BAD_REQUEST, badRequestMessage("No " + getConfigurationName() + " specified.")); } return ValidationResult.success(resourceName); } - protected final ValidationResult loadFilteredConfiguration(final String resourceName) throws IOException { - return loadSecurityConfiguration(resourceName, true).map(this::resourceNotHidden).map(securityConfiguration -> { - final var configuration = securityConfiguration.configuration(); - if (!isSuperAdmin()) { - configuration.removeHidden(); - } - configuration.clearHashes(); - configuration.set_meta(null); - return ValidationResult.success(securityConfiguration); - }); + public ValidationResult> withUserAndRemoteAddress() { + final var userAndRemoteAddress = Utils.userAndRemoteAddressFrom(threadPool.getThreadContext()); + if (userAndRemoteAddress.getLeft() == null) { + return ValidationResult.error(RestStatus.UNAUTHORIZED, payload(RestStatus.UNAUTHORIZED, "Unauthorized")); + } + return ValidationResult.success(userAndRemoteAddress); } - protected final ValidationResult loadSecurityConfigurationWithRequestContent( - final String resourceName, + protected final ValidationResult> loadConfigurationWithRequestContent( + final String entityName, final RestRequest request ) throws IOException { return createValidator().validate(request) .map( - content -> loadConfiguration(getConfigName(), false).map( - configuration -> ValidationResult.success(SecurityConfiguration.of(resourceName, content, configuration)) - ) + content -> loadConfiguration(getConfigurationType(), false, false).map( + configuration -> toSecurityConfiguration(entityName, configuration) + ).map(securityConfiguration -> ValidationResult.success(Pair.of(content, securityConfiguration))) ); } - protected final ValidationResult loadSecurityConfiguration( - final String resourceName, - final boolean logComplianceEvent + protected final ValidationResult toSecurityConfiguration( + final String entityName, + final SecurityDynamicConfiguration configuration + ) { + return ValidationResult.success(SecurityConfiguration.of(entityName, configuration)); + } + + protected final ValidationResult addEntityToConfiguration( + final Pair contentAndSecurityConfiguration ) throws IOException { - return loadConfiguration(getConfigName(), logComplianceEvent).map( - configuration -> ValidationResult.success(SecurityConfiguration.of(resourceName, configuration)) - ); + final var securityConfiguration = contentAndSecurityConfiguration.getRight(); + final var configuration = securityConfiguration.configuration(); + final var configObject = Utils.toConfigObject(contentAndSecurityConfiguration.getLeft(), configuration.getImplementingClass()); + configuration.putCObject(securityConfiguration.entityName(), configObject); + return ValidationResult.success(securityConfiguration); + } + + protected final ValidationResult removeEntityFromConfiguration( + final SecurityConfiguration securityConfiguration + ) { + securityConfiguration.configuration().remove(securityConfiguration.entityName()); + return ValidationResult.success(securityConfiguration); } protected final ValidationResult> loadConfiguration( final CType cType, + final boolean omitSensitiveData, final boolean logComplianceEvent ) { final var configuration = load(cType, logComplianceEvent); @@ -225,118 +262,29 @@ protected final ValidationResult> loadConfigurat return ValidationResult.error( RestStatus.FORBIDDEN, forbiddenMessage( - "Security index need to be updated to support '" + getConfigName().toLCString() + "'. Use SecurityAdmin to populate." + "Security index need to be updated to support '" + + getConfigurationType().toLCString() + + "'. Use SecurityAdmin to populate." ) ); } - return ValidationResult.success(configuration); - } - - protected final ValidationResult resourceExists(final SecurityConfiguration securityConfiguration) { - return resourceExists(getResourceName(), securityConfiguration); - } - - protected final ValidationResult resourceExists( - final String resourceName, - final SecurityConfiguration securityConfiguration - ) { - if (securityConfiguration.maybeResourceName().isPresent()) { - if (!securityConfiguration.resourceExists()) { - return ValidationResult.error( - RestStatus.NOT_FOUND, - notFoundMessage(resourceName + " '" + securityConfiguration.resourceName() + "' not found.") - ); - } - return ValidationResult.success(securityConfiguration); - } - return ValidationResult.success(securityConfiguration); - } - - protected final ValidationResult resourceCanBeChanged(final SecurityConfiguration securityConfiguration) - throws IOException { - return resourceNotHidden(securityConfiguration).map(ignore -> resourceNotReadOnly(securityConfiguration)); - } - - protected final ValidationResult> withUserAndRemoteAddress() { - final var userAndRemoteAddress = Utils.userAndRemoteAddressFrom(threadPool.getThreadContext()); - if (userAndRemoteAddress.getLeft() == null) { - return ValidationResult.error(RestStatus.UNAUTHORIZED, payload(RestStatus.UNAUTHORIZED, "Unauthorized")); - } - return ValidationResult.success(userAndRemoteAddress); - } - - protected final ValidationResult canChangeObjectWithRestAdminPermissions( - final SecurityConfiguration securityConfiguration - ) throws IOException { - if (securityConfiguration.resourceExists()) { - final var configuration = securityConfiguration.configuration(); - final var existingActionGroup = configuration.getCEntry(securityConfiguration.resourceName()); - if (restApiAdminPrivilegesEvaluator.containsRestApiAdminPermissions(existingActionGroup)) { - return ValidationResult.error(RestStatus.FORBIDDEN, forbiddenMessage("Access denied")); - } - } else { - final var reducedRequestContent = securityConfiguration.contentAsConfigObject(); - if (restApiAdminPrivilegesEvaluator.containsRestApiAdminPermissions(reducedRequestContent)) { - return ValidationResult.error(RestStatus.FORBIDDEN, forbiddenMessage("Access denied")); + if (omitSensitiveData) { + if (!isSuperAdmin()) { + configuration.removeHidden(); } + configuration.clearHashes(); + configuration.set_meta(null); } - return ValidationResult.success(securityConfiguration); - } - - protected final ValidationResult validateRoles( - final SecurityConfiguration securityConfiguration, - final List roles - ) throws IOException { - return loadConfiguration(CType.ROLES, false).map(rolesConfiguration -> { - if (roles != null) { - final var maybeNotValidRole = roles.stream().map(role -> { - try { - return resourceExists("role", SecurityConfiguration.of(role, rolesConfiguration)).map(this::resourceCanBeChanged); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - }).filter(Predicate.not(ValidationResult::isValid)).findFirst(); - if (maybeNotValidRole.isPresent()) { - return maybeNotValidRole.get(); - } - } - return ValidationResult.success(securityConfiguration); - }); + return ValidationResult.success(configuration); } protected abstract RequestContentValidator createValidator(final Object... params); - protected abstract String getResourceName(); + protected abstract String getConfigurationName(); - protected abstract CType getConfigName(); + protected abstract CType getConfigurationType(); - protected void handleApiRequest(final RestChannel channel, final RestRequest request, final Client client) throws IOException { - final var handlers = Optional.ofNullable(requestHandlers).orElseGet(requestHandlersBuilder::build); - try { - switch (request.method()) { - case DELETE: - handlers.get(Method.DELETE).handle(channel, request, client); - break; - case POST: - handlers.get(Method.POST).handle(channel, request, client); - break; - case PUT: - handlers.get(Method.PUT).handle(channel, request, client); - break; - case GET: - handlers.get(Method.GET).handle(channel, request, client); - break; - default: - throw new IllegalArgumentException(request.method() + " not supported"); - } - } catch (JsonMappingException jme) { - throw jme; - // TODO strip source - // if(jme.getLocation() == null || jme.getLocation().getSourceRef() == null) { - // throw jme; - // } else throw new JsonMappingException(null, jme.getMessage()); - } - } + protected void handleApiRequest(final RestChannel channel, final RestRequest request, final Client client) throws IOException {} protected boolean hasPermissionsToCreate( final SecurityDynamicConfiguration dynamicConfigFactory, @@ -372,11 +320,6 @@ protected boolean isReadonlyFieldUpdated(final JsonNode existingResource, final return false; } - protected boolean isReadonlyFieldUpdated(final SecurityDynamicConfiguration configuration, final JsonNode targetResource) { - // Default is false. Override function for additional logic - return false; - } - abstract static class OnSucessActionListener implements ActionListener { private final RestChannel channel; @@ -501,8 +444,18 @@ protected final RestChannelConsumer prepareRequest(RestRequest request, NodeClie threadPool.getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, originalUser); threadPool.getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS, originalRemoteAddress); threadPool.getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN, originalOrigin); - - handleApiRequest(channel, request, client); + requestHandlers = Optional.ofNullable(requestHandlers).orElseGet(requestHandlersBuilder::build); + if (request.method() == Method.PATCH) { + handleApiRequest(channel, request, client); + } else { + final var handler = requestHandlers.getOrDefault(request.method(), methodNotImplementedHandler); + try { + handler.handle(channel, request, client); + } catch (JsonMappingException jme) { + throw jme; + // TODO strip source + } + } } catch (Exception e) { LOGGER.error("Error processing request {}", request, e); try { @@ -589,26 +542,6 @@ protected void notImplemented(RestChannel channel, Method method) { response(channel, RestStatus.NOT_IMPLEMENTED, "Method " + method.name() + " not supported for this action."); } - protected final ValidationResult resourceNotHidden(final SecurityConfiguration securityConfiguration) { - if (isHidden(securityConfiguration.configuration(), securityConfiguration.resourceName())) { - return ValidationResult.error( - RestStatus.NOT_FOUND, - notFoundMessage("Resource '" + securityConfiguration.resourceName() + "' is not available.") - ); - } - return ValidationResult.success(securityConfiguration); - } - - protected final ValidationResult resourceNotReadOnly(final SecurityConfiguration securityConfiguration) { - if (isReadOnly(securityConfiguration.configuration(), securityConfiguration.resourceName())) { - return ValidationResult.error( - RestStatus.FORBIDDEN, - forbiddenMessage("Resource '" + securityConfiguration.resourceName() + "' is read-only.") - ); - } - return ValidationResult.success(securityConfiguration); - } - protected final boolean isReserved(SecurityDynamicConfiguration configuration, String resourceName) { return configuration.isStatic(resourceName) || configuration.isReserved(resourceName); } @@ -638,7 +571,7 @@ public String getName() { protected abstract Endpoint getEndpoint(); protected boolean isSuperAdmin() { - return restApiAdminPrivilegesEvaluator.isCurrentUserRestApiAdminFor(getEndpoint()); + return restApiAdminPrivilegesEvaluator.isCurrentUserAdminFor(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 15c3a72811..5ea4b4513d 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 @@ -11,9 +11,12 @@ package org.opensearch.security.dlic.rest.api; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import org.apache.commons.lang3.tuple.Pair; import org.apache.commons.lang3.tuple.Triple; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -106,7 +109,7 @@ public List routes() { } @Override - protected String getResourceName() { + protected String getConfigurationName() { return RESOURCE_NAME; } @@ -116,7 +119,7 @@ protected Endpoint getEndpoint() { } @Override - protected CType getConfigName() { + protected CType getConfigurationType() { return CType.INTERNALUSERS; } @@ -126,7 +129,7 @@ private void accountApiRequestHandlers(RequestHandler.RequestHandlersBuilder req .override(Method.GET, (channel, request, client) -> withUserAndRemoteAddress().map( userAndRemoteAddress -> - loadConfiguration(getConfigName(), false) + loadConfiguration(getConfigurationType(), false, false) .map(configuration -> ValidationResult.success( Triple.of( @@ -145,9 +148,12 @@ private void accountApiRequestHandlers(RequestHandler.RequestHandlersBuilder req withUserAndRemoteAddress() .map(userAndRemoteAddress -> { final var user = userAndRemoteAddress.getLeft(); - return loadSecurityConfigurationWithRequestContent(user.getName(), request) - .map(this::resourceExists) - .map(this::resourceCanBeChanged); + return loadConfigurationWithRequestContent(user.getName(), request) + .map(contentAndConfiguration -> + configurationHasEntity(contentAndConfiguration.getRight()) + .map(configurationValidators::hasRightsToChangeImmutableEntity) + .map(ignore -> ValidationResult.success(contentAndConfiguration)) + ); }).map(this::passwordCanBeValidated) .valid(securityConfiguration -> updateUserPassword(channel, client, securityConfiguration)) .error((status, toXContent) -> Responses.response(channel, status, toXContent)) @@ -178,21 +184,29 @@ private void userAccount( ); } - private ValidationResult passwordCanBeValidated(final SecurityConfiguration securityConfiguration) { - final var username = securityConfiguration.resourceName(); - final var content = securityConfiguration.requestContent(); + private ValidationResult> passwordCanBeValidated( + final Pair contentAndSecurityConfiguration + ) { + final var securityConfiguration = contentAndSecurityConfiguration.getRight(); + final var username = securityConfiguration.entityName(); + final ObjectNode content = (ObjectNode) contentAndSecurityConfiguration.getLeft(); final var currentPassword = content.get("current_password").asText(); final var internalUserEntry = (Hashed) securityConfiguration.configuration().getCEntry(username); final var currentHash = internalUserEntry.getHash(); if (currentHash == null || !OpenBSDBCrypt.checkPassword(currentHash, currentPassword.toCharArray())) { return ValidationResult.error(RestStatus.BAD_REQUEST, badRequestMessage("Could not validate your current password.")); } - return ValidationResult.success(securityConfiguration); + return ValidationResult.success(contentAndSecurityConfiguration); } - private void updateUserPassword(final RestChannel channel, final Client client, final SecurityConfiguration securityConfiguration) { - final var username = securityConfiguration.resourceName(); - final var securityJsonNode = new SecurityJsonNode(securityConfiguration.requestContent()); + private void updateUserPassword( + final RestChannel channel, + final Client client, + final Pair contentAndSecurityConfiguration + ) { + final var securityConfiguration = contentAndSecurityConfiguration.getRight(); + final var username = securityConfiguration.entityName(); + final var securityJsonNode = new SecurityJsonNode(contentAndSecurityConfiguration.getLeft()); final var internalUserEntry = (Hashed) securityConfiguration.configuration().getCEntry(username); // if password is set, it takes precedence over hash final var password = securityJsonNode.get("password").asString(); 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 8145dc33b6..4f1dddd591 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 @@ -101,9 +101,17 @@ private void actionGroupsApiRequestHandlers(RequestHandler.RequestHandlersBuilde processPutRequest(request) .map(this::actionGroupNameIsNotSameAsRoleName) .map(this::hasSelfReference) - .map(this::canChangeObjectWithRestAdminPermissions)) + .map(configurationValidators::canChangeObjectWithRestAdminPermissions)) .onChangeRequest(Method.DELETE, request -> - processDeleteRequest(request).map(this::canChangeObjectWithRestAdminPermissions)) + // process delete by default removed entity after validation. + // in this an additional check needs to be added for REST API + withRequiredEntityName(request) + .map(entityName -> + loadConfiguration(getConfigurationType(), false, false) + .map(configuration -> toSecurityConfiguration(entityName, configuration))) + .map(this::deleteRequestConfigurationValidator) + .map(configurationValidators::canChangeObjectWithRestAdminPermissions) + .map(this::removeEntityFromConfiguration)) .override(Method.POST, methodNotImplementedHandler); // spotless:on } @@ -111,7 +119,7 @@ private void actionGroupsApiRequestHandlers(RequestHandler.RequestHandlersBuilde private ValidationResult actionGroupNameIsNotSameAsRoleName(final SecurityConfiguration securityConfiguration) throws IOException { // Prevent the case where action group and role share a same name. - return loadConfiguration(CType.ROLES, false).map( + return loadConfiguration(CType.ROLES, false, false).map( rolesConfiguration -> actionGroupNameIsNotSameAsRoleName(securityConfiguration, rolesConfiguration) ); } @@ -120,12 +128,11 @@ private ValidationResult actionGroupNameIsNotSameAsRoleNa final SecurityConfiguration securityConfiguration, final SecurityDynamicConfiguration rolesConfiguration ) { - if (rolesConfiguration.getCEntries().containsKey(securityConfiguration.resourceName())) { + if (rolesConfiguration.getCEntries().containsKey(securityConfiguration.entityName())) { return ValidationResult.error( RestStatus.BAD_REQUEST, badRequestMessage( - securityConfiguration.resourceName() - + " is an existing role. A action group cannot be named with an existing role name." + securityConfiguration.entityName() + " is an existing role. A action group cannot be named with an existing role name." ) ); } @@ -134,23 +141,16 @@ private ValidationResult actionGroupNameIsNotSameAsRoleNa private ValidationResult hasSelfReference(final SecurityConfiguration securityConfiguration) throws IOException { // Prevent the case where action group references to itself in the allowed_actions. - return loadConfiguration(getConfigName(), false).map(actionGroupsConfig -> { - final var actionGroupName = securityConfiguration.resourceName(); - final var actionGroup = securityConfiguration.contentAsConfigObject(); - actionGroupsConfig.putCObject(securityConfiguration.resourceName(), actionGroup); - if (hasSelfReference(securityConfiguration.resourceName(), actionGroupsConfig)) { - return ValidationResult.error( - RestStatus.BAD_REQUEST, - badRequestMessage(actionGroupName + " cannot be an allowed_action of itself") - ); - } - return ValidationResult.success(securityConfiguration); - }); - } - - private boolean hasSelfReference(final String name, final SecurityDynamicConfiguration configuration) { - List allowedActions = ((ActionGroupsV7) configuration.getCEntry(name)).getAllowed_actions(); - return allowedActions.contains(name); + final var actionGroupName = securityConfiguration.entityName(); + final var configuration = securityConfiguration.configuration(); + final var allowedActions = ((ActionGroupsV7) configuration.getCEntry(actionGroupName)).getAllowed_actions(); + if (allowedActions.contains(actionGroupName)) { + return ValidationResult.error( + RestStatus.BAD_REQUEST, + badRequestMessage(actionGroupName + " cannot be an allowed_action of itself") + ); + } + return ValidationResult.success(securityConfiguration); } @Override @@ -186,12 +186,12 @@ public Set mandatoryKeys() { } @Override - protected CType getConfigName() { + protected CType getConfigurationType() { return CType.ACTIONGROUPS; } @Override - protected String getResourceName() { + protected String getConfigurationName() { return "actiongroup"; } 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 06319181eb..8410f0d684 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 @@ -87,7 +87,7 @@ public class AllowlistApiAction extends PatchableResourceApiAction { new Route(RestRequest.Method.PATCH, "/_plugins/_security/api/allowlist") ); - private static final String RESOURCE_NAME = "config"; + private static final String CONFIGURATION_NAME = "config"; @Inject public AllowlistApiAction( @@ -127,7 +127,10 @@ protected void handleApiRequest(final RestChannel channel, final RestRequest req private void allowlistApiRequestHandlers(RequestHandler.RequestHandlersBuilder requestHandlersBuilder) { requestHandlersBuilder.verifyAccessForAllMethods() - .onChangeRequest(RestRequest.Method.PUT, request -> loadSecurityConfigurationWithRequestContent(RESOURCE_NAME, request)) + .onChangeRequest( + RestRequest.Method.PUT, + request -> loadConfigurationWithRequestContent(CONFIGURATION_NAME, request).map(this::addEntityToConfiguration) + ) .override(RestRequest.Method.DELETE, methodNotImplementedHandler); } @@ -137,12 +140,12 @@ public List routes() { } @Override - protected String getResourceName() { - return RESOURCE_NAME; + protected String getConfigurationName() { + return CONFIGURATION_NAME; } @Override - protected CType getConfigName() { + protected CType getConfigurationType() { return CType.ALLOWLIST; } 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 d0fa4d78ad..d3eae168b8 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 @@ -17,6 +17,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import org.apache.commons.lang3.tuple.Pair; import org.opensearch.client.Client; import org.opensearch.cluster.service.ClusterService; import org.opensearch.common.settings.Settings; @@ -139,7 +140,7 @@ public class AuditApiAction extends PatchableResourceApiAction { ) ); - private static final String RESOURCE_NAME = "config"; + private static final String CONFIGURATION_NAME = "config"; @VisibleForTesting public static final String READONLY_FIELD = "_readonly"; @VisibleForTesting @@ -261,8 +262,8 @@ protected void handleApiRequest(final RestChannel channel, final RestRequest req } @Override - protected String getResourceName() { - return RESOURCE_NAME; + protected String getConfigurationName() { + return CONFIGURATION_NAME; } @Override @@ -271,7 +272,7 @@ protected Endpoint getEndpoint() { } @Override - protected CType getConfigName() { + protected CType getConfigurationType() { return CType.AUDIT; } @@ -280,15 +281,18 @@ private void auditApiRequestHandlers(RequestHandler.RequestHandlersBuilder reque requestHandlersBuilder .onGetRequest(request -> processGetRequest(request) - .map(securityConfiguration -> { - final var configuration = securityConfiguration.configuration(); + .map(configuration -> { configuration.putCObject(READONLY_FIELD, readonlyFields); - return ValidationResult.success(securityConfiguration); - })) + return ValidationResult.success(configuration); + }) + ) .onChangeRequest(RestRequest.Method.PUT, request -> withConfigResourceNameOnly(request) - .map(ignore -> processPutRequest(request)) - .map(this::verifyNotReadonlyFieldUpdated)) + .map(config -> loadConfigurationWithRequestContent(config, request)) + .map(this::verifyNotReadonlyFieldUpdated) + .map(this::addEntityToConfiguration) + .map(configurationValidators::hasRightsToChangeImmutableEntity) + ) .override(RestRequest.Method.POST, methodNotImplementedHandler) .override(RestRequest.Method.DELETE, methodNotImplementedHandler); // spotless:on @@ -296,22 +300,24 @@ private void auditApiRequestHandlers(RequestHandler.RequestHandlersBuilder reque private ValidationResult withConfigResourceNameOnly(final RestRequest request) { final var name = nameParam(request); - if (!RESOURCE_NAME.equals(name)) { - return ValidationResult.error(RestStatus.BAD_REQUEST, badRequestMessage("name must be config")); + if (!CONFIGURATION_NAME.equals(name)) { + return ValidationResult.error(RestStatus.BAD_REQUEST, badRequestMessage("name must be `" + CONFIGURATION_NAME + "`")); } return ValidationResult.success(name); } - private ValidationResult verifyNotReadonlyFieldUpdated(final SecurityConfiguration securityConfiguration) - throws IOException { + private ValidationResult> verifyNotReadonlyFieldUpdated( + final Pair contentAndSecurityConfiguration + ) { if (!isSuperAdmin()) { - final var existingResource = securityConfiguration.configurationAsJsonNode().get(getResourceName()); - final var targetResource = securityConfiguration.requestContent(); + final var configuration = contentAndSecurityConfiguration.getRight().configuration(); + final var existingResource = Utils.convertJsonToJackson(configuration, false).get(getConfigurationName()); + final var targetResource = contentAndSecurityConfiguration.getLeft(); if (readonlyFields.stream().anyMatch(path -> !existingResource.at(path).equals(targetResource.at(path)))) { return ValidationResult.error(RestStatus.CONFLICT, conflictMessage("Attempted to update read-only property.")); } } - return ValidationResult.success(securityConfiguration); + return ValidationResult.success(contentAndSecurityConfiguration); } @Override @@ -342,8 +348,4 @@ protected boolean isReadonlyFieldUpdated(final JsonNode existingResource, final return false; } - @Override - protected boolean isReadonlyFieldUpdated(final SecurityDynamicConfiguration configuration, final JsonNode targetResource) { - return isReadonlyFieldUpdated(Utils.convertJsonToJackson(configuration, false).get(getResourceName()), targetResource); - } } 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 906581114e..5394b4506e 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 @@ -79,12 +79,12 @@ protected RequestContentValidator createValidator(Object... params) { } @Override - protected String getResourceName() { + protected String getConfigurationName() { return "authtoken"; } @Override - protected CType getConfigName() { + protected CType getConfigurationType() { return null; } 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 5502f7e756..2f8d8689d9 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 @@ -130,13 +130,13 @@ protected RequestContentValidator createValidator(final Object... params) { } @Override - protected String getResourceName() { + protected String getConfigurationName() { // not needed return null; } @Override - protected CType getConfigName() { + protected CType getConfigurationType() { return null; } 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 4d71cdeb4b..a0b4c59b8f 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 @@ -11,17 +11,12 @@ package org.opensearch.security.dlic.rest.api; -import java.io.IOException; -import java.nio.file.Path; -import java.util.List; -import java.util.Map; - import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.TextNode; import com.google.common.collect.ImmutableList; - import com.google.common.collect.ImmutableMap; +import org.apache.commons.lang3.tuple.Pair; import org.opensearch.client.Client; import org.opensearch.cluster.service.ClusterService; import org.opensearch.common.inject.Inject; @@ -49,6 +44,11 @@ import org.opensearch.security.user.UserServiceException; import org.opensearch.threadpool.ThreadPool; +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; + import static org.opensearch.security.dlic.rest.api.Responses.badRequest; import static org.opensearch.security.dlic.rest.api.Responses.badRequestMessage; import static org.opensearch.security.dlic.rest.api.Responses.methodNotImplementedMessage; @@ -123,34 +123,39 @@ protected Endpoint getEndpoint() { } @Override - protected String getResourceName() { + protected String getConfigurationName() { return "user"; } @Override - protected CType getConfigName() { + protected CType getConfigurationType() { return CType.INTERNALUSERS; } private void internalUsersApiRequestHandlers(RequestHandler.RequestHandlersBuilder requestHandlersBuilder) { // spotless:off - // Overrides the GET request functionality to allow for the special case of requesting an auth token. requestHandlersBuilder + // Overrides the GET request functionality to allow for the special case of requesting an auth token. .override(Method.POST, (channel, request, client) -> withAuthTokenPath(request) - .map(this::loadFilteredConfiguration) - .map(this::resourceExists) + .map(username -> + loadConfiguration(getConfigurationType(), true, true) + .map(configuration -> toSecurityConfiguration(username, configuration)) + ) + .map(this::configurationHasEntity) + .map(configurationValidators::hasRightsToLoadOrChangeHiddenEntity) .valid(securityConfiguration -> generateAuthToken(channel, securityConfiguration)) .error((status, toXContent) -> Responses.response(channel, status, toXContent))) .onChangeRequest(Method.PUT, request -> - processPutRequest(request) - .map(securityConfiguration -> createOrUpdateUser(request, securityConfiguration)) + withRequiredEntityName(request) + .map(entityName -> loadConfigurationWithRequestContent(entityName, request)) + .map(contentAndConfiguration -> createOrUpdateUser(request, contentAndConfiguration)) ); // spotless:on } private ValidationResult withAuthTokenPath(final RestRequest request) throws IOException { - return withRequiredResourceName(request).map(username -> { + return withRequiredEntityName(request).map(username -> { // Handle auth token fetching if (!(request.uri().contains("/internalusers/" + username + "/authtoken") && request.uri().endsWith("/authtoken"))) { return ValidationResult.error(RestStatus.NOT_IMPLEMENTED, methodNotImplementedMessage(request.method())); @@ -161,7 +166,7 @@ private ValidationResult withAuthTokenPath(final RestRequest request) th private void generateAuthToken(final RestChannel channel, final SecurityConfiguration securityConfiguration) throws IOException { try { - final var username = securityConfiguration.resourceName(); + final var username = securityConfiguration.entityName(); final var authToken = userService.generateAuthToken(username); if (!Strings.isNullOrEmpty(authToken)) { ok(channel, "'" + username + "' authtoken generated " + authToken); @@ -175,21 +180,24 @@ private void generateAuthToken(final RestChannel channel, final SecurityConfigur private ValidationResult createOrUpdateUser( final RestRequest request, - final SecurityConfiguration securityConfiguration + final Pair contentAndConfiguration ) throws IOException { - final var username = securityConfiguration.resourceName(); - final ObjectNode contentAsNode = (ObjectNode) securityConfiguration.requestContent(); - final SecurityJsonNode securityJsonNode = new SecurityJsonNode(contentAsNode); + final var securityConfiguration = contentAndConfiguration.getRight(); + final var username = securityConfiguration.entityName(); + final ObjectNode requestContent = (ObjectNode) contentAndConfiguration.getLeft(); + final SecurityJsonNode securityJsonNode = new SecurityJsonNode(requestContent); // FIXME do we need to verify roles as well? final var securityRoles = securityJsonNode.get("opendistro_security_roles").asList(); // Don't allow user to add non-existent role or a role for which role-mapping is hidden or reserved // spotless:off - return validateRoles(securityConfiguration, securityRoles) - .map(ignore -> createOrUpdateAccount(request, securityConfiguration)) + return configurationValidators.hasRightsToChangeImmutableEntity(securityConfiguration) + .map(ignore -> loadConfiguration(CType.ROLES, false, false)) + .map(rolesConfiguration -> configurationValidators.validateRoles(securityRoles, rolesConfiguration)) + .map(ignore -> createOrUpdateAccount(request, requestContent, securityConfiguration)) .map(updatedSecurityConfiguration -> { // when updating an existing user password hash can be blank, which means no changes // for existing users, hash is optional - if (updatedSecurityConfiguration.resourceExists() && securityJsonNode.get("hash").asString() == null) { + if (updatedSecurityConfiguration.entityExists() && securityJsonNode.get("hash").asString() == null) { final String hash = ((Hashed) updatedSecurityConfiguration.configuration().getCEntry(username)).getHash(); if (Strings.isNullOrEmpty(hash)) { return ValidationResult.error( @@ -200,32 +208,32 @@ private ValidationResult createOrUpdateUser( ) ); } - contentAsNode.put("hash", hash); + requestContent.put("hash", hash); } - return ValidationResult.success(updatedSecurityConfiguration); + return addEntityToConfiguration(contentAndConfiguration); }); // spotless:on } private ValidationResult createOrUpdateAccount( final RestRequest request, + final ObjectNode requestContent, final SecurityConfiguration securityConfiguration ) throws IOException { try { - final var username = securityConfiguration.resourceName(); - final var content = securityConfiguration.requestContent(); + final var username = securityConfiguration.entityName(); if (request.hasParam("service")) { - ((ObjectNode) content).put("service", request.param("service")); + requestContent.put("service", request.param("service")); } if (request.hasParam("enabled")) { - ((ObjectNode) content).put("enabled", request.param("enabled")); + requestContent.put("enabled", request.param("enabled")); } - ((ObjectNode) content).put("name", username); + requestContent.put("name", username); // FIXME add better solution for account and internal users - final var updateConfiguration = userService.createOrUpdateAccount((ObjectNode) content); + final var updateConfiguration = userService.createOrUpdateAccount(requestContent); // remove extra user in case we deal with the new one. not nice better to redesign account users. - if (!securityConfiguration.resourceExists()) updateConfiguration.remove(securityConfiguration.resourceName()); - return ValidationResult.success(SecurityConfiguration.of(username, content, updateConfiguration)); + if (!securityConfiguration.entityExists()) updateConfiguration.remove(securityConfiguration.entityName()); + return ValidationResult.success(SecurityConfiguration.of(username, updateConfiguration)); } catch (UserServiceException ex) { return ValidationResult.error(RestStatus.BAD_REQUEST, badRequestMessage(ex.getMessage())); } 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 409f7d21d3..a12a3f1086 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 @@ -291,13 +291,13 @@ protected RequestContentValidator createValidator(final Object... params) { } @Override - protected String getResourceName() { + protected String getConfigurationName() { // not needed return null; } @Override - protected CType getConfigName() { + protected CType getConfigurationType() { return null; } diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/MultiTenancyConfigApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/MultiTenancyConfigApiAction.java index 1e3aec16e4..270a36572b 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/MultiTenancyConfigApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/MultiTenancyConfigApiAction.java @@ -11,21 +11,10 @@ package org.opensearch.security.dlic.rest.api; -import java.io.IOException; -import java.nio.file.Path; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; - import com.fasterxml.jackson.databind.JsonNode; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; - -import org.apache.commons.lang3.tuple.Pair; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.opensearch.action.index.IndexResponse; @@ -40,7 +29,6 @@ import org.opensearch.security.configuration.ConfigurationRepository; import org.opensearch.security.dlic.rest.validation.RequestContentValidator; import org.opensearch.security.dlic.rest.validation.RequestContentValidator.DataType; -import org.opensearch.security.dlic.rest.validation.ValidationResult; import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; @@ -49,6 +37,15 @@ import org.opensearch.security.support.ConfigConstants; import org.opensearch.threadpool.ThreadPool; +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + import static org.opensearch.rest.RestRequest.Method.GET; import static org.opensearch.rest.RestRequest.Method.PUT; import static org.opensearch.security.dlic.rest.api.Responses.ok; @@ -105,12 +102,12 @@ protected Endpoint getEndpoint() { } @Override - protected String getResourceName() { + protected String getConfigurationName() { return null; } @Override - protected CType getConfigName() { + protected CType getConfigurationType() { return CType.CONFIG; } @@ -151,21 +148,19 @@ private ToXContent multitenancyContent(final ConfigV7 config) { private void multiTenancyConfigApiRequestHandlers(RequestHandler.RequestHandlersBuilder requestHandlersBuilder) { requestHandlersBuilder.allMethodsNotImplemented() - .override(GET, (channel, request, client) -> loadConfiguration(getConfigName(), false).valid(configuration -> { + .override(GET, (channel, request, client) -> loadConfiguration(getConfigurationType(), false, false).valid(configuration -> { final var config = (ConfigV7) configuration.getCEntry(CType.CONFIG.toLCString()); ok(channel, multitenancyContent(config)); }).error((status, toXContent) -> Responses.response(channel, status, toXContent))) .override(PUT, (channel, request, client) -> { - createValidator().validate(request) - .map( - content -> loadConfiguration(getConfigName(), false).map( - configuration -> ValidationResult.success(Pair.of(content, configuration)) - ) + loadConfigurationWithRequestContent("config", request).valid( + contentAndConfig -> updateMultitenancy( + channel, + client, + contentAndConfig.getRight().configuration(), + contentAndConfig.getLeft() ) - .valid(contentAndConfig -> { - updateMultitenancy(channel, client, contentAndConfig.getRight(), contentAndConfig.getLeft()); - }) - .error((status, toXContent) -> Responses.response(channel, status, toXContent)); + ).error((status, toXContent) -> Responses.response(channel, status, toXContent)); }); } 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 19c1c74136..f463da79f6 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 @@ -122,7 +122,7 @@ protected void handleApiRequest(RestChannel channel, RestRequest request, Client } protected void consumeParameters(final RestRequest request) { - request.param("name"); + super.consumeParameters(request); request.param("show_all"); } @@ -130,21 +130,21 @@ private void nodesDnApiRequestHandlers(RequestHandler.RequestHandlersBuilder req // spotless:off requestHandlersBuilder.verifyAccessForAllMethods() .onChangeRequest(Method.PUT, request -> - withRequiredResourceName(request) + withRequiredEntityName(request) .map(this::notStaticNodesDn) - .map(ignore -> processPutRequest(request))) + .map(entityName -> processPutRequest(request)) + ) .onChangeRequest(Method.DELETE, request -> - withRequiredResourceName(request) + withRequiredEntityName(request) .map(this::notStaticNodesDn) .map(ignore -> processDeleteRequest(request))) .onGetRequest(request -> processGetRequest(request) - .map(securityConfiguration -> { + .map(configuration -> { if (request.paramAsBoolean("show_all", false)) { - final var configuration = securityConfiguration.configuration(); addStaticNodesDn(configuration); } - return ValidationResult.success(securityConfiguration); + return ValidationResult.success(configuration); }) ); // spotless:on @@ -182,12 +182,12 @@ protected Endpoint getEndpoint() { } @Override - protected String getResourceName() { + protected String getConfigurationName() { return "nodesdn"; } @Override - protected CType getConfigName() { + protected CType getConfigurationType() { return CType.NODESDN; } 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 e2b9e83afb..5715098977 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 @@ -74,10 +74,10 @@ private void handlePatch(RestChannel channel, final RestRequest request, final C } String name = request.param("name"); - SecurityDynamicConfiguration existingConfiguration = load(getConfigName(), false); + SecurityDynamicConfiguration existingConfiguration = load(getConfigurationType(), false); if (existingConfiguration.getSeqNo() < 0) { - forbidden(channel, "Config '" + getConfigName().toLCString() + "' isn't configured. Use SecurityAdmin to populate."); + forbidden(channel, "Config '" + getConfigurationType().toLCString() + "' isn't configured. Use SecurityAdmin to populate."); return; } @@ -94,7 +94,7 @@ private void handlePatch(RestChannel channel, final RestRequest request, final C JsonNode existingAsJsonNode = Utils.convertJsonToJackson(existingConfiguration, true); if (!(existingAsJsonNode instanceof ObjectNode)) { - internalErrorResponse(channel, "Config " + getConfigName() + " is malformed"); + internalErrorResponse(channel, "Config " + getConfigurationType() + " is malformed"); return; } @@ -121,7 +121,7 @@ private void handleSinglePatch( } if (!existingConfiguration.exists(name)) { - notFound(channel, getResourceName() + " " + name + " not found."); + notFound(channel, getConfigurationName() + " " + name + " not found."); return; } @@ -184,14 +184,20 @@ private void handleSinglePatch( } } - saveAndUpdateConfigs(this.securityIndexName, client, getConfigName(), mdc, new OnSucessActionListener(channel) { + saveAndUpdateConfigs( + this.securityIndexName, + client, + getConfigurationType(), + mdc, + new OnSucessActionListener(channel) { - @Override - public void onResponse(IndexResponse response) { - successResponse(channel, "'" + name + "' updated."); + @Override + public void onResponse(IndexResponse response) { + successResponse(channel, "'" + name + "' updated."); + } } - }); + ); } private void handleBulkPatch( @@ -282,13 +288,19 @@ private void handleBulkPatch( } } - saveAndUpdateConfigs(this.securityIndexName, client, getConfigName(), mdc, new OnSucessActionListener(channel) { + saveAndUpdateConfigs( + this.securityIndexName, + client, + getConfigurationType(), + mdc, + new OnSucessActionListener(channel) { - @Override - public void onResponse(IndexResponse response) { - successResponse(channel, "Resource updated."); + @Override + public void onResponse(IndexResponse response) { + successResponse(channel, "Resource updated."); + } } - }); + ); } diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/RequestHandler.java b/src/main/java/org/opensearch/security/dlic/rest/api/RequestHandler.java index 84382f7f98..2b2ab99a6e 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/RequestHandler.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/RequestHandler.java @@ -129,16 +129,15 @@ RequestHandlersBuilder withSaveOrUpdateConfigurationHandler( } public RequestHandlersBuilder onGetRequest( - final CheckedFunction, IOException> mapper + final CheckedFunction>, IOException> mapper ) { Objects.requireNonNull(mapper, "onGetRequest request handler can't be null"); - add(RestRequest.Method.GET, (channel, request, client) -> mapper.apply(request).valid(securityConfiguration -> { - securityConfiguration.maybeResourceName() - .ifPresentOrElse( - name -> ok(channel, securityConfiguration.resourceConfiguration()), - () -> ok(channel, securityConfiguration.configuration()) - ); - }).error((status, toXContent) -> response(channel, status, toXContent))); + add( + RestRequest.Method.GET, + (channel, request, client) -> mapper.apply(request) + .valid(configuration -> ok(channel, configuration)) + .error((status, toXContent) -> response(channel, status, toXContent)) + ); return this; } @@ -163,8 +162,8 @@ public RequestHandlersBuilder onChangeRequest( new AbstractApiAction.OnSucessActionListener<>(channel) { @Override public void onResponse(IndexResponse indexResponse) { - if (securityConfiguration.maybeResourceName().isPresent()) { - ok(channel, "'" + securityConfiguration.resourceName() + "' updated."); + if (securityConfiguration.maybeEntityName().isPresent()) { + ok(channel, "'" + securityConfiguration.entityName() + "' updated."); } else { ok(channel, "Resource updated."); } @@ -177,39 +176,38 @@ public void onResponse(IndexResponse indexResponse) { case PUT: add(method, (channel, request, client) -> mapper.apply(request) - .valid(securityConfiguration -> { - final var resourceExists = securityConfiguration.resourceExists(); - saveOrUpdateConfigurationHandler.apply( - client, - securityConfiguration.createOrUpdateResource(), - new AbstractApiAction.OnSucessActionListener<>(channel) { - @Override - public void onResponse(IndexResponse response) { - if (resourceExists) { - ok(channel, "'" + securityConfiguration.resourceName() + "' updated."); - } else { - created(channel, "'" + securityConfiguration.resourceName() + "' created."); - } + .valid(securityConfiguration -> saveOrUpdateConfigurationHandler.apply( + client, + securityConfiguration.configuration(), + new AbstractApiAction.OnSucessActionListener<>(channel) { + @Override + public void onResponse(IndexResponse response) { + if (securityConfiguration.entityExists()) { + ok(channel, "'" + securityConfiguration.entityName() + "' updated."); + } else { + created(channel, "'" + securityConfiguration.entityName() + "' created."); } } - ); - }).error((status, toXContent) -> response(channel, status, toXContent))); + } + )).error((status, toXContent) -> response(channel, status, toXContent))); break; case DELETE: Objects.requireNonNull(mapper, "onDeleteRequest request handler can't be null"); add(RestRequest.Method.DELETE, (channel, request, client) -> mapper.apply(request) - .valid(securityConfiguration -> - saveOrUpdateConfigurationHandler.apply( - client, - securityConfiguration.deleteResource(), - new AbstractApiAction.OnSucessActionListener<>(channel) { - @Override - public void onResponse(IndexResponse response) { - ok(channel, "'" + securityConfiguration.resourceName() + "' deleted."); + .valid(securityConfiguration -> { + securityConfiguration.configuration().remove(securityConfiguration.entityName()); + saveOrUpdateConfigurationHandler.apply( + client, + securityConfiguration.configuration(), + new AbstractApiAction.OnSucessActionListener<>(channel) { + @Override + public void onResponse(IndexResponse response) { + ok(channel, "'" + securityConfiguration.entityName() + "' deleted."); + } } - } - ) + ); + } ) .error((status, toXContent) -> response(channel, status, toXContent)) ); 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 index c72e69876d..d05803be50 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/RestApiAdminPrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/RestApiAdminPrivilegesEvaluator.java @@ -98,7 +98,7 @@ public RestApiAdminPrivilegesEvaluator( this.restapiAdminEnabled = restapiAdminEnabled; } - public boolean isCurrentUserRestApiAdminFor(final Endpoint endpoint, final String action) { + public boolean isCurrentUserAdminFor(final Endpoint endpoint, final String action) { final Pair userAndRemoteAddress = Utils.userAndRemoteAddressFrom(threadContext); if (userAndRemoteAddress.getLeft() == null) { return false; @@ -154,8 +154,8 @@ public boolean containsRestApiAdminPermissions(final Object configObject) { } } - public boolean isCurrentUserRestApiAdminFor(final Endpoint endpoint) { - return isCurrentUserRestApiAdminFor(endpoint, null); + public boolean isCurrentUserAdminFor(final Endpoint endpoint) { + return isCurrentUserAdminFor(endpoint, null); } private static String buildEndpointActionPermission(final Endpoint endpoint, final String action) { 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 2c451a97ca..88fba33a1f 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 @@ -136,17 +136,33 @@ private void rolesApiRequestHandlers(RequestHandler.RequestHandlersBuilder reque .onChangeRequest(Method.PUT, request -> processPutRequest(request).map(this::canChangeRolesRestAdminPermissions)) .onChangeRequest(Method.DELETE, request -> - processDeleteRequest(request).map(this::canChangeRolesRestAdminPermissions)) + // process delete by default removed entity after validation. + // in this an additional check needs to be added for REST API + withRequiredEntityName(request) + .map(entityName -> + loadConfiguration(getConfigurationType(), false, false) + .map(configuration -> toSecurityConfiguration(entityName, configuration)) + ) + .map(this::deleteRequestConfigurationValidator) + .map(this::canChangeRolesRestAdminPermissions) + .map(this::removeEntityFromConfiguration) + ) .override(Method.POST, methodNotImplementedHandler); // spotless:on } - private ValidationResult canChangeRolesRestAdminPermissions(final SecurityConfiguration securityConfiguration) + private ValidationResult rolesConfigurationValidator(final SecurityConfiguration securityConfiguration) throws IOException { + return configurationHasEntity(securityConfiguration).map(configurationValidators::hasRightsToChangeImmutableEntity) + .map(this::canChangeRolesRestAdminPermissions) + .map(this::removeEntityFromConfiguration); + } + + private ValidationResult canChangeRolesRestAdminPermissions(final SecurityConfiguration securityConfiguration) { if (isSuperAdmin()) { return ValidationResult.success(securityConfiguration); } - return canChangeObjectWithRestAdminPermissions(securityConfiguration); + return configurationValidators.canChangeObjectWithRestAdminPermissions(securityConfiguration); } @Override @@ -176,12 +192,12 @@ public Map allowedKeys() { } @Override - protected String getResourceName() { + protected String getConfigurationName() { return "role"; } @Override - protected CType getConfigName() { + protected CType getConfigurationType() { return CType.ROLES; } 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 ad542a72ee..43d18ed27f 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 @@ -11,14 +11,7 @@ package org.opensearch.security.dlic.rest.api; -import java.io.IOException; -import java.nio.file.Path; -import java.util.List; -import java.util.Map; -import java.util.Set; - import com.google.common.collect.ImmutableList; - import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import org.opensearch.client.Client; @@ -39,6 +32,12 @@ import org.opensearch.security.ssl.transport.PrincipalExtractor; import org.opensearch.threadpool.ThreadPool; +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.Set; + import static org.opensearch.security.dlic.rest.api.RequestHandler.methodNotImplementedHandler; import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; @@ -77,28 +76,42 @@ private void rolesMappingApiRequestHandlers(RequestHandler.RequestHandlersBuilde requestHandlersBuilder .onChangeRequest(Method.PUT, request -> processPutRequest(request) - .map(securityConfiguration -> - validateRoles( - securityConfiguration, - List.of(securityConfiguration.resourceName()) - ) - ) + .map(this::roleValidForRoleMapping) .map(this::canChangeRolesMappingRestAdminPermissions)) .onChangeRequest(Method.DELETE, request -> - processDeleteRequest(request).map(this::canChangeRolesMappingRestAdminPermissions)) + // process delete by default removed entity after validation. + // in this an additional check needs to be added for REST API + withRequiredEntityName(request) + .map(entityName -> + loadConfiguration(getConfigurationType(), false, false) + .map(configuration -> toSecurityConfiguration(entityName, configuration)) + ) + .map(this::deleteRequestConfigurationValidator) + .map(this::canChangeRolesMappingRestAdminPermissions) + .map(this::removeEntityFromConfiguration) + ) .override(Method.POST, methodNotImplementedHandler); // spotless:on } + private ValidationResult roleValidForRoleMapping(final SecurityConfiguration securityConfiguration) + throws IOException { + return loadConfiguration(CType.ROLES, false, false).map( + rolesConfiguration -> configurationValidators.validateRole( + SecurityConfiguration.of(securityConfiguration.entityName(), rolesConfiguration) + ).map(ignore -> ValidationResult.success(securityConfiguration)) + ); + } + private ValidationResult canChangeRolesMappingRestAdminPermissions( final SecurityConfiguration securityConfiguration ) throws IOException { - return loadConfiguration(CType.ROLES, false).map(rolesConfiguration -> { + return loadConfiguration(CType.ROLES, false, false).map(rolesConfiguration -> { if (isSuperAdmin()) { return ValidationResult.success(securityConfiguration); } - return canChangeObjectWithRestAdminPermissions( - SecurityConfiguration.of(securityConfiguration.resourceName(), rolesConfiguration) + return configurationValidators.canChangeObjectWithRestAdminPermissions( + SecurityConfiguration.of(securityConfiguration.entityName(), rolesConfiguration) ); }).map(ignore -> ValidationResult.success(securityConfiguration)); } @@ -170,12 +183,12 @@ public Map allowedKeys() { } @Override - protected String getResourceName() { + protected String getConfigurationName() { return "rolesmapping"; } @Override - protected CType getConfigName() { + protected CType getConfigurationType() { return CType.ROLESMAPPING; } diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/SecurityConfigApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityConfigApiAction.java index 0ca014a5fa..aede516575 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/SecurityConfigApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityConfigApiAction.java @@ -85,7 +85,7 @@ public List routes() { } @Override - protected CType getConfigName() { + protected CType getConfigurationType() { return CType.CONFIG; } @@ -95,7 +95,7 @@ protected Endpoint getEndpoint() { } @Override - protected String getResourceName() { + protected String getConfigurationName() { // not needed, no single resource return null; } @@ -125,10 +125,7 @@ private void securityConfigApiActionRequestHandlers(RequestHandler.RequestHandle } else { return withConfigResourceNameOnly(request).map(ignore -> processPutRequest(request)); } - }) - .override(Method.POST, methodNotImplementedHandler) - .override(Method.DELETE, methodNotImplementedHandler) - .override(Method.POST, methodNotImplementedHandler); + }).override(Method.POST, methodNotImplementedHandler).override(Method.DELETE, methodNotImplementedHandler); } private ValidationResult withConfigResourceNameOnly(final RestRequest request) { diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/SecurityConfiguration.java b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityConfiguration.java index 418a617786..06c6d8914a 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/SecurityConfiguration.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityConfiguration.java @@ -11,39 +11,26 @@ package org.opensearch.security.dlic.rest.api; -import com.fasterxml.jackson.databind.JsonNode; -import org.opensearch.core.common.bytes.BytesReference; -import org.opensearch.core.xcontent.MediaTypeRegistry; -import org.opensearch.core.xcontent.ToXContent; -import org.opensearch.core.xcontent.XContentHelper; -import org.opensearch.security.DefaultObjectMapper; -import org.opensearch.security.dlic.rest.support.Utils; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; -import java.io.IOException; -import java.util.Map; import java.util.Objects; import java.util.Optional; public class SecurityConfiguration { - private final String resourceName; + private final String entityName; - private final JsonNode requestContent; + private final boolean entityExists; private final SecurityDynamicConfiguration configuration; - private SecurityConfiguration(final String resourceName, final SecurityDynamicConfiguration configuration) { - this(resourceName, null, configuration); - } - private SecurityConfiguration( - final String resourceName, - final JsonNode requestContent, + final String entityName, + final boolean entityExists, final SecurityDynamicConfiguration configuration ) { - this.resourceName = resourceName; - this.requestContent = requestContent; + this.entityName = entityName; + this.entityExists = entityExists; this.configuration = configuration; } @@ -51,71 +38,21 @@ public SecurityDynamicConfiguration configuration() { return configuration; } - public JsonNode requestContent() { - return requestContent; - } - - public Object contentAsConfigObject() throws IOException { - return Utils.toConfigObject(requestContent, configuration.getImplementingClass()); - } - - public JsonNode configurationAsJsonNode() throws IOException { - return configurationAsJsonNode(false); - } - - public JsonNode configurationAsJsonNode(final boolean omitDefaults) throws IOException { - final BytesReference bytes = XContentHelper.toXContent( - configuration, - MediaTypeRegistry.JSON, - new ToXContent.MapParams(Map.of("omit_defaults", Boolean.valueOf(omitDefaults).toString())), - false - ); - return DefaultObjectMapper.readTree(bytes.utf8ToString()); - } - - public SecurityDynamicConfiguration resourceConfiguration() { - if (resourceName != null) { - configuration.removeOthers(resourceName); - } - return configuration; - } - - public boolean resourceExists() { - return configuration.exists(resourceName); - } - - public SecurityDynamicConfiguration deleteResource() { - if (resourceName != null) { - configuration.remove(resourceName); - } - return configuration; - } - - public SecurityDynamicConfiguration createOrUpdateResource() throws IOException { - configuration.putCObject(resourceName, contentAsConfigObject()); - return configuration; - } - - public Optional maybeResourceName() { - return Optional.ofNullable(resourceName); + public boolean entityExists() { + return entityExists; } - public String resourceName() { - return resourceName; + public Optional maybeEntityName() { + return Optional.ofNullable(entityName); } - public static SecurityConfiguration of(final String resourceName, final SecurityDynamicConfiguration configuration) { - Objects.requireNonNull(configuration); - return new SecurityConfiguration(resourceName, configuration); + public String entityName() { + return maybeEntityName().orElse("empty"); } - public static SecurityConfiguration of( - final String resourceName, - final JsonNode requestContent, - final SecurityDynamicConfiguration configuration - ) { + public static SecurityConfiguration of(final String entityName, final SecurityDynamicConfiguration configuration) { Objects.requireNonNull(configuration); - return new SecurityConfiguration(resourceName, requestContent, configuration); + return new SecurityConfiguration(entityName, configuration.exists(entityName), configuration); } } 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 index 758c4a86e1..3884718ae4 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/SecuritySSLCertsAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/SecuritySSLCertsAction.java @@ -132,9 +132,9 @@ private void securitySSLCertsRequestHandlers(RequestHandler.RequestHandlersBuild private boolean accessHandler(final RestRequest request) { switch (request.method()) { case GET: - return restApiAdminPrivilegesEvaluator.isCurrentUserRestApiAdminFor(getEndpoint(), "certs"); + return restApiAdminPrivilegesEvaluator.isCurrentUserAdminFor(getEndpoint(), "certs"); case PUT: - return restApiAdminPrivilegesEvaluator.isCurrentUserRestApiAdminFor(getEndpoint(), "reloadcerts"); + return restApiAdminPrivilegesEvaluator.isCurrentUserAdminFor(getEndpoint(), "reloadcerts"); default: return false; } @@ -236,12 +236,12 @@ protected void consumeParameters(RestRequest request) { } @Override - protected String getResourceName() { + protected String getConfigurationName() { return null; } @Override - protected CType getConfigName() { + protected CType getConfigurationType() { 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 f0c9be3ee9..4215dd34e4 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 @@ -127,12 +127,12 @@ public Map allowedKeys() { } @Override - protected CType getConfigName() { + protected CType getConfigurationType() { return CType.TENANTS; } @Override - protected String getResourceName() { + protected String getConfigurationName() { return "tenant"; } 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 eb46c76e1d..be582809b4 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 @@ -156,13 +156,13 @@ protected RequestContentValidator createValidator(final Object... params) { } @Override - protected String getResourceName() { + protected String getConfigurationName() { // not needed return null; } @Override - protected CType getConfigName() { + protected CType getConfigurationType() { return null; } diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/WhitelistApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/WhitelistApiAction.java index d3bc92959c..601f5eef0a 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/WhitelistApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/WhitelistApiAction.java @@ -126,7 +126,7 @@ protected Endpoint getEndpoint() { } @Override - protected CType getConfigName() { + protected CType getConfigurationType() { return CType.WHITELIST; } diff --git a/src/main/java/org/opensearch/security/dlic/rest/support/Utils.java b/src/main/java/org/opensearch/security/dlic/rest/support/Utils.java index 57cf080b95..3853ee5f46 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 @@ -12,6 +12,7 @@ package org.opensearch.security.dlic.rest.support; import java.io.IOException; +import java.io.UncheckedIOException; import java.security.AccessController; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; @@ -35,6 +36,7 @@ import org.opensearch.ExceptionsHelper; import org.opensearch.OpenSearchParseException; import org.opensearch.SpecialPermission; +import org.opensearch.common.CheckedSupplier; import org.opensearch.core.common.bytes.BytesReference; import org.opensearch.core.common.transport.TransportAddress; import org.opensearch.common.util.concurrent.ThreadContext; @@ -268,4 +270,12 @@ public static Pair userAndRemoteAddressFrom(final Thread return Pair.of(user, remoteAddress); } + public static T withIOException(final CheckedSupplier action) { + try { + return action.get(); + } catch (final IOException ioe) { + throw new UncheckedIOException(ioe); + } + } + } diff --git a/src/main/java/org/opensearch/security/dlic/rest/validation/ConfigurationValidators.java b/src/main/java/org/opensearch/security/dlic/rest/validation/ConfigurationValidators.java new file mode 100644 index 0000000000..be0d85bccf --- /dev/null +++ b/src/main/java/org/opensearch/security/dlic/rest/validation/ConfigurationValidators.java @@ -0,0 +1,136 @@ +/* + * 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.validation; + +import org.opensearch.core.rest.RestStatus; +import org.opensearch.security.dlic.rest.api.Endpoint; +import org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator; +import org.opensearch.security.dlic.rest.api.SecurityConfiguration; +import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; + +import java.io.IOException; +import java.util.List; + +import static java.util.function.Predicate.not; +import static org.opensearch.security.dlic.rest.api.Responses.forbiddenMessage; +import static org.opensearch.security.dlic.rest.api.Responses.notFoundMessage; +import static org.opensearch.security.dlic.rest.support.Utils.withIOException; + +public final class ConfigurationValidators { + + private final Endpoint endpoint; + + private final RestApiAdminPrivilegesEvaluator restApiAdminPrivilegesEvaluator; + + public ConfigurationValidators(final Endpoint endpoint, final RestApiAdminPrivilegesEvaluator restApiAdminPrivilegesEvaluator) { + this.endpoint = endpoint; + this.restApiAdminPrivilegesEvaluator = restApiAdminPrivilegesEvaluator; + } + + public ValidationResult entityExists( + final String configurationName, + final SecurityConfiguration securityConfiguration + ) { + return securityConfiguration.maybeEntityName().>map(entityName -> { + if (!securityConfiguration.entityExists()) { + return ValidationResult.error( + RestStatus.NOT_FOUND, + notFoundMessage(configurationName + " '" + securityConfiguration.entityName() + "' not found.") + ); + } + return ValidationResult.success(securityConfiguration); + }).orElseGet(() -> ValidationResult.success(securityConfiguration)); + } + + public ValidationResult hasRightsToChangeImmutableEntity(final SecurityConfiguration securityConfiguration) + throws IOException { + final var immutableCheck = entityImmutable(securityConfiguration); + if (!immutableCheck.isValid() && !restApiAdminPrivilegesEvaluator.isCurrentUserAdminFor(endpoint)) { + return immutableCheck; + } + return ValidationResult.success(securityConfiguration); + } + + public ValidationResult hasRightsToLoadOrChangeHiddenEntity(final SecurityConfiguration securityConfiguration) { + final var hiddenCheck = entityHidden(securityConfiguration); + if (!hiddenCheck.isValid() && !restApiAdminPrivilegesEvaluator.isCurrentUserAdminFor(endpoint)) { + return hiddenCheck; + } + return ValidationResult.success(securityConfiguration); + } + + public ValidationResult entityImmutable(final SecurityConfiguration securityConfiguration) throws IOException { + return entityHidden(securityConfiguration).map(this::entityStatic).map(this::entityReserved); + } + + public ValidationResult entityStatic(final SecurityConfiguration securityConfiguration) { + final var configuration = securityConfiguration.configuration(); + final var entityName = securityConfiguration.entityName(); + if (configuration.isStatic(entityName)) { + return ValidationResult.error(RestStatus.FORBIDDEN, forbiddenMessage("Resource '" + entityName + "' is static.")); + } + return ValidationResult.success(securityConfiguration); + } + + public ValidationResult entityReserved(final SecurityConfiguration securityConfiguration) { + final var configuration = securityConfiguration.configuration(); + final var entityName = securityConfiguration.entityName(); + if (configuration.isReserved(entityName)) { + return ValidationResult.error(RestStatus.FORBIDDEN, forbiddenMessage("Resource '" + entityName + "' is reserved.")); + } + return ValidationResult.success(securityConfiguration); + } + + public ValidationResult entityHidden(final SecurityConfiguration securityConfiguration) { + final var configuration = securityConfiguration.configuration(); + final var entityName = securityConfiguration.entityName(); + if (configuration.isHidden(entityName)) { + return ValidationResult.error(RestStatus.NOT_FOUND, notFoundMessage("Resource '" + entityName + "' is not available.")); + } + return ValidationResult.success(securityConfiguration); + } + + public ValidationResult validateRole(final SecurityConfiguration securityConfiguration) throws IOException { + return validateRoles(List.of(securityConfiguration.entityName()), securityConfiguration.configuration()).map( + ignore -> ValidationResult.success(securityConfiguration) + ); + } + + public ValidationResult> validateRoles( + final List roles, + final SecurityDynamicConfiguration rolesConfiguration + ) { + final var rolesToCheck = roles == null ? List.of() : roles; + return rolesToCheck.stream().map(role -> withIOException(() -> { + final var roleSecConfig = SecurityConfiguration.of(role, rolesConfiguration); + return entityExists("role", roleSecConfig).map(this::hasRightsToChangeImmutableEntity); + })) + .filter(not(ValidationResult::isValid)) + .findFirst() + .>>map( + result -> ValidationResult.error(result.status(), result.errorMessage()) + ) + .orElseGet(() -> ValidationResult.success(rolesConfiguration)); + } + + public ValidationResult canChangeObjectWithRestAdminPermissions( + final SecurityConfiguration securityConfiguration + ) { + final var configuration = securityConfiguration.configuration(); + final var entity = configuration.getCEntry(securityConfiguration.entityName()); + if (restApiAdminPrivilegesEvaluator.containsRestApiAdminPermissions(entity)) { + return ValidationResult.error(RestStatus.FORBIDDEN, forbiddenMessage("Access denied")); + } + return ValidationResult.success(securityConfiguration); + } + +} diff --git a/src/main/java/org/opensearch/security/dlic/rest/validation/ValidationResult.java b/src/main/java/org/opensearch/security/dlic/rest/validation/ValidationResult.java index e8a9557fac..7fb91d8913 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/validation/ValidationResult.java +++ b/src/main/java/org/opensearch/security/dlic/rest/validation/ValidationResult.java @@ -71,6 +71,10 @@ public ValidationResult valid(final CheckedConsumer mapper) t return this; } + public RestStatus status() { + return status; + } + public boolean isValid() { return errorMessage == null; } diff --git a/src/main/java/org/opensearch/security/rest/TenantInfoAction.java b/src/main/java/org/opensearch/security/rest/TenantInfoAction.java index f3afc0f006..b2fbe08f37 100644 --- a/src/main/java/org/opensearch/security/rest/TenantInfoAction.java +++ b/src/main/java/org/opensearch/security/rest/TenantInfoAction.java @@ -177,7 +177,7 @@ private boolean isAuthorized() { return false; } - private final SecurityDynamicConfiguration load(final CType config, boolean logComplianceEvent) { + private SecurityDynamicConfiguration load(final CType config, boolean logComplianceEvent) { SecurityDynamicConfiguration loaded = configurationRepository.getConfigurationsFromIndex( Collections.singleton(config), logComplianceEvent 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 ab0cf53237..27a1360a65 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 @@ -77,7 +77,7 @@ private void checkGetAndPutAllowlistPermissions(final int expectedStatus, final } // FORBIDDEN FOR NON SUPER ADMIN if (expectedStatus == HttpStatus.SC_FORBIDDEN) { - assertTrue(response.getBody().contains("API allowed only for super admin.")); + assertTrue(response.getBody().contains("Access denied")); } // CHECK PUT REQUEST response = rh.executePutRequest( 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 4cfe057388..096db95dcd 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 @@ -302,7 +302,7 @@ void verifyPutForSuperAdmin(final Header[] header) throws Exception { Assert.assertTrue(settings.get("backend_roles").equals("Array expected")); // Read only role mapping - // SuperAdmin can add read only roles - mappings + // SuperAdmin can add read-only roles - mappings response = rh.executePutRequest( ENDPOINT + "/rolesmapping/opendistro_security_role_starfleet_library", FileHelper.loadFile("restapi/rolesmapping_all_access.json"), 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 4b78e15278..66c29acbde 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 @@ -961,7 +961,7 @@ public void testUserApiForNonSuperAdmin() throws Exception { ); Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); Settings settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); - Assert.assertEquals(settings.get("message"), "Resource 'opendistro_security_reserved' is read-only."); + Assert.assertEquals(settings.get("message"), "Resource 'opendistro_security_reserved' is reserved."); // Patch single hidden user response = rh.executePatchRequest( diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/WhitelistApiTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/WhitelistApiTest.java index 8a1d8ce83f..398489b788 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/WhitelistApiTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/WhitelistApiTest.java @@ -84,7 +84,7 @@ private void checkGetAndPutWhitelistPermissions(final int expectedStatus, final } // FORBIDDEN FOR NON SUPER ADMIN if (expectedStatus == HttpStatus.SC_FORBIDDEN) { - assertTrue(response.getBody().contains("API allowed only for super admin.")); + assertTrue(response.getBody().contains("Access denied")); } // CHECK PUT REQUEST response = rh.executePutRequest( diff --git a/src/test/java/org/opensearch/security/dlic/rest/validation/ConfigurationValidatorsTest.java b/src/test/java/org/opensearch/security/dlic/rest/validation/ConfigurationValidatorsTest.java new file mode 100644 index 0000000000..cc0963f868 --- /dev/null +++ b/src/test/java/org/opensearch/security/dlic/rest/validation/ConfigurationValidatorsTest.java @@ -0,0 +1,377 @@ +/* + * 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.validation; + +import org.apache.commons.lang3.tuple.Triple; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; +import org.opensearch.core.rest.RestStatus; +import org.opensearch.security.dlic.rest.api.Endpoint; +import org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator; +import org.opensearch.security.dlic.rest.api.SecurityConfiguration; +import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; +import org.opensearch.security.securityconf.impl.v7.ActionGroupsV7; +import org.opensearch.security.securityconf.impl.v7.RoleV7; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class ConfigurationValidatorsTest { + + @Mock + SecurityDynamicConfiguration configuration; + + @Mock + RestApiAdminPrivilegesEvaluator restApiAdminPrivilegesEvaluator; + + private ConfigurationValidators configurationValidators; + + @Before + public void createConfigurationValidator() { + configurationValidators = new ConfigurationValidators(Endpoint.INTERNALUSERS, restApiAdminPrivilegesEvaluator); + } + + @Test + public void entityDoesNotExist() { + when(configuration.exists("some_role")).thenReturn(false); + final var validationResult = configurationValidators.entityExists("role", SecurityConfiguration.of("some_role", configuration)); + assertFalse(validationResult.isValid()); + assertEquals(RestStatus.NOT_FOUND, validationResult.status()); + } + + @Test + public void entityExists() { + when(configuration.exists("some_role")).thenReturn(true); + final var validationResult = configurationValidators.entityExists("role", SecurityConfiguration.of("some_role", configuration)); + assertTrue(validationResult.isValid()); + assertEquals(RestStatus.OK, validationResult.status()); + } + + @Test + public void entityExistsSkipEmptyEntityName() { + when(configuration.exists(null)).thenReturn(false); + final var validationResult = configurationValidators.entityExists("role", SecurityConfiguration.of(null, configuration)); + assertTrue(validationResult.isValid()); + assertEquals(RestStatus.OK, validationResult.status()); + } + + @Test + public void entityHidden() { + when(configuration.isHidden("some_entity")).thenReturn(true); + final var validationResult = configurationValidators.entityHidden( SecurityConfiguration.of("some_entity", configuration)); + assertFalse(validationResult.isValid()); + assertEquals(RestStatus.NOT_FOUND, validationResult.status()); + } + + @Test + public void entityNotHidden() { + when(configuration.isHidden("some_entity")).thenReturn(false); + final var validationResult = configurationValidators.entityHidden( SecurityConfiguration.of("some_entity", configuration)); + assertTrue(validationResult.isValid()); + assertEquals(RestStatus.OK, validationResult.status()); + } + + @Test + public void entityReserved() { + when(configuration.isReserved("some_entity")).thenReturn(true); + final var validationResult = configurationValidators.entityReserved( SecurityConfiguration.of("some_entity", configuration)); + assertFalse(validationResult.isValid()); + assertEquals(RestStatus.FORBIDDEN, validationResult.status()); + } + + @Test + public void entityNotReserved() { + when(configuration.isReserved("some_entity")).thenReturn(false); + final var validationResult = configurationValidators.entityReserved( SecurityConfiguration.of("some_entity", configuration)); + assertTrue(validationResult.isValid()); + assertEquals(RestStatus.OK, validationResult.status()); + } + + @Test + public void entityStatic() { + when(configuration.isStatic("some_entity")).thenReturn(true); + final var validationResult = configurationValidators.entityStatic( SecurityConfiguration.of("some_entity", configuration)); + assertFalse(validationResult.isValid()); + assertEquals(RestStatus.FORBIDDEN, validationResult.status()); + } + + @Test + public void entityNotStatic() { + when(configuration.isStatic("some_entity")).thenReturn(false); + final var validationResult = configurationValidators.entityStatic( SecurityConfiguration.of("some_entity", configuration)); + assertTrue(validationResult.isValid()); + assertEquals(RestStatus.OK, validationResult.status()); + } + + @Test + public void hiddenEntityImmutable() throws Exception { + when(configuration.isHidden("some_entity")).thenReturn(true); + + var validationResult = configurationValidators.entityImmutable( SecurityConfiguration.of("some_entity", configuration)); + assertFalse(validationResult.isValid()); + assertEquals(RestStatus.NOT_FOUND, validationResult.status()); + } + + @Test + public void staticEntityImmutable() throws Exception { + when(configuration.isHidden("some_entity")).thenReturn(false); + when(configuration.isStatic("some_entity")).thenReturn(true); + final var validationResult = configurationValidators.entityImmutable( SecurityConfiguration.of("some_entity", configuration)); + assertFalse(validationResult.isValid()); + assertEquals(RestStatus.FORBIDDEN, validationResult.status()); + } + + @Test + public void reservedEntityImmutable() throws Exception { + when(configuration.isHidden("some_entity")).thenReturn(false); + when(configuration.isStatic("some_entity")).thenReturn(false); + when(configuration.isReserved("some_entity")).thenReturn(true); + final var validationResult = configurationValidators.entityImmutable( SecurityConfiguration.of("some_entity", configuration)); + assertFalse(validationResult.isValid()); + assertEquals(RestStatus.FORBIDDEN, validationResult.status()); + } + + @Test + public void hasRightsToChangeImmutableEntity() throws Exception { + configImmutableEntities(false); + var result = configurationValidators.hasRightsToChangeImmutableEntity(SecurityConfiguration.of("hidden_entity", configuration)); + assertFalse(result.isValid()); + assertEquals(RestStatus.NOT_FOUND, result.status()); + + result = configurationValidators.hasRightsToChangeImmutableEntity(SecurityConfiguration.of("static_entity", configuration)); + assertFalse(result.isValid()); + assertEquals(RestStatus.FORBIDDEN, result.status()); + + result = configurationValidators.hasRightsToChangeImmutableEntity(SecurityConfiguration.of("reserved_entity", configuration)); + assertFalse(result.isValid()); + assertEquals(RestStatus.FORBIDDEN, result.status()); + + result = configurationValidators.hasRightsToChangeImmutableEntity(SecurityConfiguration.of("just_entity", configuration)); + assertTrue(result.isValid()); + assertEquals(RestStatus.OK, result.status()); + } + + @Test + public void hasRightsToChangeImmutableEntityForAdmin() throws Exception { + configImmutableEntities(true); + + var result = configurationValidators.hasRightsToChangeImmutableEntity(SecurityConfiguration.of("hidden_entity", configuration)); + assertTrue(result.isValid()); + assertEquals(RestStatus.OK, result.status()); + + result = configurationValidators.hasRightsToChangeImmutableEntity(SecurityConfiguration.of("static_entity", configuration)); + assertTrue(result.isValid()); + assertEquals(RestStatus.OK, result.status()); + + result = configurationValidators.hasRightsToChangeImmutableEntity(SecurityConfiguration.of("reserved_entity", configuration)); + assertTrue(result.isValid()); + assertEquals(RestStatus.OK, result.status()); + + result = configurationValidators.hasRightsToChangeImmutableEntity(SecurityConfiguration.of("just_entity", configuration)); + assertTrue(result.isValid()); + assertEquals(RestStatus.OK, result.status()); + } + + @Test + public void hasRightsToLoadOrChangeHiddenEntityForRegularUser() throws Exception { + configImmutableEntities(false); + + var result = configurationValidators.hasRightsToLoadOrChangeHiddenEntity(SecurityConfiguration.of("hidden_entity", configuration)); + assertFalse(result.isValid()); + assertEquals(RestStatus.NOT_FOUND, result.status()); + + result = configurationValidators.hasRightsToLoadOrChangeHiddenEntity(SecurityConfiguration.of("just_entity", configuration)); + assertTrue(result.isValid()); + assertEquals(RestStatus.OK, result.status()); + } + + @Test + public void hasRightsToLoadOrChangeHiddenEntityForAdmin() throws Exception { + configImmutableEntities(true); + + var result = configurationValidators.hasRightsToLoadOrChangeHiddenEntity(SecurityConfiguration.of("hidden_entity", configuration)); + assertTrue(result.isValid()); + assertEquals(RestStatus.OK, result.status()); + + result = configurationValidators.hasRightsToLoadOrChangeHiddenEntity(SecurityConfiguration.of("just_entity", configuration)); + assertTrue(result.isValid()); + assertEquals(RestStatus.OK, result.status()); + } + + private void configImmutableEntities(final boolean isAdmin) { + when(restApiAdminPrivilegesEvaluator.isCurrentUserAdminFor(any(Endpoint.class))).thenReturn(isAdmin); + when(configuration.isHidden("just_entity")).thenReturn(false); + when(configuration.isStatic("just_entity")).thenReturn(false); + when(configuration.isReserved("just_entity")).thenReturn(false); + + when(configuration.isHidden("hidden_entity")).thenReturn(true); + when(configuration.isStatic("static_entity")).thenReturn(true); + when(configuration.isReserved("reserved_entity")).thenReturn(true); + } + + @Test + public void entityNotImmutable() throws Exception { + when(configuration.isHidden("some_entity")).thenReturn(false); + when(configuration.isStatic("some_entity")).thenReturn(false); + when(configuration.isReserved("some_entity")).thenReturn(false); + + var validationResult = configurationValidators.entityImmutable( SecurityConfiguration.of("some_entity", configuration)); + assertTrue(validationResult.isValid()); + assertEquals(RestStatus.OK, validationResult.status()); + } + + @Test + public void roleDoeNotExist() throws Exception { + final var validationResult = configurationValidators.validateRole(SecurityConfiguration.of("some_role", configuration)); + assertFalse(validationResult.isValid()); + assertEquals(RestStatus.NOT_FOUND, validationResult.status()); + } + + @Test + public void hiddenRoleInvalid() throws Exception { + when(configuration.exists("some_role")).thenReturn(true); + when(configuration.isHidden("some_role")).thenReturn(true); + + final var validationResult = configurationValidators.validateRole(SecurityConfiguration.of("some_role", configuration)); + assertFalse(validationResult.isValid()); + assertEquals(RestStatus.NOT_FOUND, validationResult.status()); + } + + @Test + public void staticRoleInvalid() throws Exception { + when(configuration.exists("some_role")).thenReturn(true); + when(configuration.isHidden("some_role")).thenReturn(false); + when(configuration.isStatic("some_role")).thenReturn(true); + + final var validationResult = configurationValidators.validateRole(SecurityConfiguration.of("some_role", configuration)); + assertFalse(validationResult.isValid()); + assertEquals(RestStatus.FORBIDDEN, validationResult.status()); + } + + @Test + public void reservedRoleInvalid() throws Exception { + when(configuration.exists("some_role")).thenReturn(true); + when(configuration.isHidden("some_role")).thenReturn(false); + when(configuration.isStatic("some_role")).thenReturn(false); + when(configuration.isReserved("some_role")).thenReturn(true); + + final var validationResult = configurationValidators.validateRole(SecurityConfiguration.of("some_role", configuration)); + assertFalse(validationResult.isValid()); + assertEquals(RestStatus.FORBIDDEN, validationResult.status()); + } + + @Test + public void validateRolesForAdmin() { + configureRoles(true); + final var expectedResultForRoles = List.of( + Triple.of("valid_role", true, RestStatus.OK), + Triple.of("reserved_role", true, RestStatus.OK), + Triple.of("static_role", true, RestStatus.OK), + Triple.of("hidden_role", true, RestStatus.OK), + Triple.of("non_existing_role", false, RestStatus.NOT_FOUND) + ); + + for (final var roleWithExpectedResults : expectedResultForRoles) { + final var validationResult = configurationValidators.validateRoles(List.of(roleWithExpectedResults.getLeft()), configuration); + assertEquals(roleWithExpectedResults.getMiddle(), validationResult.isValid()); + assertEquals(roleWithExpectedResults.getRight(), validationResult.status()); + } + + } + + @Test + public void validateRolesForRegularUser() { + configureRoles(false); + final var expectedResultForRoles = List.of( + Triple.of("valid_role", true, RestStatus.OK), + Triple.of("reserved_role", false, RestStatus.FORBIDDEN), + Triple.of("static_role", false, RestStatus.FORBIDDEN), + Triple.of("hidden_role", false, RestStatus.NOT_FOUND), + Triple.of("non_existing_role", false, RestStatus.NOT_FOUND) + ); + + for (final var roleWithExpectedResults : expectedResultForRoles) { + final var validationResult = configurationValidators.validateRoles(List.of(roleWithExpectedResults.getLeft()), configuration); + assertEquals(roleWithExpectedResults.getMiddle(), validationResult.isValid()); + assertEquals(roleWithExpectedResults.getRight(), validationResult.status()); + } + + } + + private void configureRoles(final boolean isAdmin) { + when(restApiAdminPrivilegesEvaluator.isCurrentUserAdminFor(any(Endpoint.class))).thenReturn(isAdmin); + + when(configuration.exists("non_existing_role")).thenReturn(false); + + when(configuration.exists("hidden_role")).thenReturn(true); + when(configuration.isHidden("hidden_role")).thenReturn(true); + + when(configuration.exists("static_role")).thenReturn(true); + when(configuration.isHidden("static_role")).thenReturn(false); + when(configuration.isStatic("static_role")).thenReturn(true); + + when(configuration.exists("reserved_role")).thenReturn(true); + when(configuration.isHidden("reserved_role")).thenReturn(false); + when(configuration.isStatic("reserved_role")).thenReturn(false); + when(configuration.isReserved("reserved_role")).thenReturn(true); + + when(configuration.exists("valid_role")).thenReturn(true); + when(configuration.isHidden("valid_role")).thenReturn(false); + when(configuration.isStatic("valid_role")).thenReturn(false); + when(configuration.isReserved("valid_role")).thenReturn(false); + } + + @Test + public void regularUserCanNotChangeObjectWithRestAdminPermissions() { + final var restAdminPermissions = List.of( + "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" + ); + + final var actionGroups = new ActionGroupsV7("some_ag", restAdminPermissions); + final var role = new RoleV7(); + role.setCluster_permissions(restAdminPermissions); + + when(restApiAdminPrivilegesEvaluator.containsRestApiAdminPermissions(any(Object.class))).thenCallRealMethod(); + Mockito.when(configuration.getCEntry("some_ag")).thenReturn(actionGroups); + Mockito.when(configuration.getCEntry("some_role")).thenReturn(role); + final var agCheckResult = configurationValidators.canChangeObjectWithRestAdminPermissions( + SecurityConfiguration.of("some_ag", configuration) + ); + assertFalse(agCheckResult.isValid()); + assertEquals(RestStatus.FORBIDDEN, agCheckResult.status()); + + final var roleCheckResult = configurationValidators.canChangeObjectWithRestAdminPermissions( + SecurityConfiguration.of("some_role", configuration) + ); + assertFalse(roleCheckResult.isValid()); + assertEquals(RestStatus.FORBIDDEN, roleCheckResult.status()); + } + +}