diff --git a/src/integrationTest/java/org/opensearch/security/DefaultConfigurationTests.java b/src/integrationTest/java/org/opensearch/security/DefaultConfigurationTests.java index 8bb5b96145..b254e3182a 100644 --- a/src/integrationTest/java/org/opensearch/security/DefaultConfigurationTests.java +++ b/src/integrationTest/java/org/opensearch/security/DefaultConfigurationTests.java @@ -11,17 +11,22 @@ import java.io.IOException; import java.nio.file.Path; +import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import com.fasterxml.jackson.databind.JsonNode; + import org.apache.commons.io.FileUtils; import org.awaitility.Awaitility; import org.junit.AfterClass; import org.junit.ClassRule; import org.junit.Test; import org.junit.runner.RunWith; - +import org.opensearch.test.framework.TestSecurityConfig.User; import org.opensearch.test.framework.cluster.ClusterManager; import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; @@ -38,10 +43,9 @@ public class DefaultConfigurationTests { private final static Path configurationFolder = ConfigurationFiles.createConfigurationDirectory(); - public static final String ADMIN_USER_NAME = "admin"; - public static final String DEFAULT_PASSWORD = "secret"; - public static final String NEW_USER = "new-user"; - public static final String LIMITED_USER = "limited-user"; + private static final User ADMIN_USER = new User("admin"); + private static final User NEW_USER = new User("new-user"); + private static final User LIMITED_USER = new User("limited-user"); @ClassRule public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) @@ -62,17 +66,60 @@ public static void cleanConfigurationDirectory() throws IOException { FileUtils.deleteDirectory(configurationFolder.toFile()); } + // @Test + // public void shouldLoadDefaultConfiguration() { + // try (TestRestClient client = cluster.getRestClient(NEW_USER)) { + // Awaitility.await().alias("Load default configuration").until(() -> client.getAuthInfo().getStatusCode(), equalTo(200)); + // } + // try (TestRestClient client = cluster.getRestClient(ADMIN_USER)) { + // client.confirmCorrectCredentials(ADMIN_USER.getName()); + // HttpResponse response = client.get("_plugins/_security/api/internalusers"); + // response.assertStatusCode(200); + // Map users = response.getBodyAs(Map.class); + // assertThat(users, allOf(aMapWithSize(3), hasKey(ADMIN_USER.getName()), hasKey(NEW_USER.getName()), hasKey(LIMITED_USER.getName()))); + // } + // } + + @Test - public void shouldLoadDefaultConfiguration() { - try (TestRestClient client = cluster.getRestClient(NEW_USER, DEFAULT_PASSWORD)) { + public void securityRolesUgrade() throws Exception { + try (var client = cluster.getRestClient(ADMIN_USER)) { Awaitility.await().alias("Load default configuration").until(() -> client.getAuthInfo().getStatusCode(), equalTo(200)); + + final var defaultRolesResponse = client.get("_plugins/_security/api/roles/"); + final var roles = defaultRolesResponse.getBodyAs(JsonNode.class); + final var rolesCount = extractFieldNames(roles).size(); + + final var checkForUpgrade = client.get("_plugins/_security/api/_upgrade_check"); + System.out.println("checkForUpgrade Response: " + checkForUpgrade.getBody()); + + + final var roleToDelete = "flow_framework_full_access"; + final var deleteRoleResponse = client.delete("_plugins/_security/api/roles/" + roleToDelete); + deleteRoleResponse.assertStatusCode(200); + + final var checkForUpgrade3 = client.get("_plugins/_security/api/_upgrade_check"); + System.out.println("checkForUpgrade3 Response: " + checkForUpgrade3.getBody()); + + final var roleToAlter = "flow_framework_read_access"; + final String patchBody = "[{ \"op\": \"replace\", \"path\": \"/cluster_permissions\", \"value\":" + + "[\"a\",\"b\",\"c\"]" + + "},{ \"op\": \"add\", \"path\": \"/index_permissions\", \"value\":" + + "[{\"index_patterns\":[\"*\"],\"allowed_actions\":[\"*\"]}]" + + "}]"; + final var updateRoleResponse = client.patch("_plugins/_security/api/roles/" + roleToAlter, patchBody); + updateRoleResponse.assertStatusCode(200); + System.out.println("Updated Role Response: " +updateRoleResponse.getBody()); + + final var checkForUpgrade2 = client.get("_plugins/_security/api/_upgrade_check"); + System.out.println("checkForUpgrade2 Response: " + checkForUpgrade2.getBody()); + } - try (TestRestClient client = cluster.getRestClient(ADMIN_USER_NAME, DEFAULT_PASSWORD)) { - client.confirmCorrectCredentials(ADMIN_USER_NAME); - HttpResponse response = client.get("_plugins/_security/api/internalusers"); - response.assertStatusCode(200); - Map users = response.getBodyAs(Map.class); - assertThat(users, allOf(aMapWithSize(3), hasKey(ADMIN_USER_NAME), hasKey(NEW_USER), hasKey(LIMITED_USER))); - } + } + + private List extractFieldNames(final JsonNode json) { + final var list = new ArrayList(); + json.fieldNames().forEachRemaining(list::add); + return list; } } diff --git a/src/integrationTest/resources/roles.yml b/src/integrationTest/resources/roles.yml index 02de9bf3d5..2ea7548ad6 100644 --- a/src/integrationTest/resources/roles.yml +++ b/src/integrationTest/resources/roles.yml @@ -17,3 +17,21 @@ user_limited-user__limited-role: allowed_actions: - "indices:data/read/get" - "indices:data/read/search" +flow_framework_full_access: + cluster_permissions: + - 'cluster:admin/opensearch/flow_framework/*' + - 'cluster_monitor' + index_permissions: + - index_patterns: + - '*' + allowed_actions: + - 'indices:admin/aliases/get' + - 'indices:admin/mappings/get' + - 'indices_monitor' +flow_framework_read_access: + cluster_permissions: + - 'cluster:admin/opensearch/flow_framework/workflow/get' + - 'cluster:admin/opensearch/flow_framework/workflow/search' + - 'cluster:admin/opensearch/flow_framework/workflow_state/get' + - 'cluster:admin/opensearch/flow_framework/workflow_state/search' + - 'cluster:admin/opensearch/flow_framework/workflow_step/get' diff --git a/src/main/java/org/opensearch/security/configuration/ConfigurationRepository.java b/src/main/java/org/opensearch/security/configuration/ConfigurationRepository.java index dfbeb16cb3..353286fc4a 100644 --- a/src/main/java/org/opensearch/security/configuration/ConfigurationRepository.java +++ b/src/main/java/org/opensearch/security/configuration/ConfigurationRepository.java @@ -123,6 +123,14 @@ private ConfigurationRepository( configCache = CacheBuilder.newBuilder().build(); } + public String getConfigDirectory() { + String lookupDir = System.getProperty("security.default_init.dir"); + final String cd = lookupDir != null + ? (lookupDir + "/") + : new Environment(settings, configPath).configDir().toAbsolutePath().toString() + "/opensearch-security/"; + return cd; + } + private void initalizeClusterConfiguration(final boolean installDefaultConfig) { try { LOGGER.info("Background init thread started. Install default config?: " + installDefaultConfig); @@ -135,10 +143,7 @@ private void initalizeClusterConfiguration(final boolean installDefaultConfig) { if (installDefaultConfig) { try { - String lookupDir = System.getProperty("security.default_init.dir"); - final String cd = lookupDir != null - ? (lookupDir + "/") - : new Environment(settings, configPath).configDir().toAbsolutePath().toString() + "/opensearch-security/"; + final String cd = getConfigDirectory(); File confFile = new File(cd + "config.yml"); if (confFile.exists()) { final ThreadContext threadContext = threadPool.getThreadContext(); diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/ConfigUpgradeApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/ConfigUpgradeApiAction.java new file mode 100644 index 0000000000..4090a6b38c --- /dev/null +++ b/src/main/java/org/opensearch/security/dlic/rest/api/ConfigUpgradeApiAction.java @@ -0,0 +1,172 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.dlic.rest.api; + + + +import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; +import static org.opensearch.security.dlic.rest.support.Utils.withIOException; + +import java.io.IOException; +import java.security.AccessController; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.EnumSet; +import java.util.List; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.opensearch.client.Client; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.inject.Inject; +import org.opensearch.common.xcontent.XContentType; +import org.opensearch.core.rest.RestStatus; +import org.opensearch.rest.BytesRestResponse; +import org.opensearch.rest.RestChannel; +import org.opensearch.rest.RestRequest; +import org.opensearch.rest.RestRequest.Method; +import org.opensearch.security.configuration.ConfigurationRepository; +import org.opensearch.security.dlic.rest.support.Utils; +import org.opensearch.security.dlic.rest.validation.ValidationResult; +import org.opensearch.security.securityconf.impl.CType; +import org.opensearch.security.support.ConfigHelper; +import org.opensearch.threadpool.ThreadPool; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.flipkart.zjsonpatch.DiffFlags; +import com.flipkart.zjsonpatch.JsonDiff; +import com.google.common.collect.ImmutableList; + +public class ConfigUpgradeApiAction extends AbstractApiAction { + + private final static Logger LOGGER = LogManager.getLogger(ConfigUpgradeApiAction.class); + + private static final List routes = addRoutesPrefix( + ImmutableList.of( + new Route(Method.GET, "/_upgrade_check") + ) + ); + + @Inject + public ConfigUpgradeApiAction( + final ClusterService clusterService, + final ThreadPool threadPool, + final SecurityApiDependencies securityApiDependencies + ) { + super(Endpoint.CONFIG, clusterService, threadPool, securityApiDependencies); + this.requestHandlersBuilder.configureRequestHandlers(rhb -> { + rhb.add(Method.GET, this::handleRolesCanUpgrade); + }); + } + + public void handleRolesCanUpgrade(final RestChannel channel, final RestRequest request, final Client client) { + try { + withIOException(() -> computeDifferenceToUpdate(CType.ROLES, "roles.yml") + .map(differences -> { + final var canUpgrade = differences.size() > 0; + + // Step 4: Return a response indicating if an upgrade can be performed + ObjectNode response = JsonNodeFactory.instance.objectNode(); + response.put("can_upgrade", canUpgrade); + + if (canUpgrade) { + // Optionally include the differences in the response + response.set("differences", differences); + } + return ValidationResult.success(response); + })); +// Handle how this is returned! + channel.sendResponse(new BytesRestResponse(RestStatus.OK, XContentType.JSON.mediaType(), response.toPrettyString())); + } catch (Exception e) { + // Handle other exceptions + LOGGER.error("Unexpected error during upgrade check", e); + channel.sendResponse(new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, "{\"error\":\"Unexpected error checking for upgrade\"}")); + } + } + + private ValidationResult computeDifferenceToUpdate(final CType configType, final String configName) throws IOException { + return + loadConfiguration(configType, false, false) + .map(activeRoles -> { + final var activeRolesJson = Utils.convertJsonToJackson(activeRoles, false); + final var defaultRolesJson = loadConfigFileAsJson(configName, configType); + final var rawDiff = JsonDiff.asJson(activeRolesJson, defaultRolesJson, EnumSet.of(DiffFlags.OMIT_VALUE_ON_REMOVE)); + return ValidationResult.success(filterRemoveOperations(rawDiff)); + }); + } + + private JsonNode filterRemoveOperations(final JsonNode diff) { + final ArrayNode filteredDiff = JsonNodeFactory.instance.arrayNode(); + diff.forEach(node -> { + if (!isRemoveOperation(node)) { + filteredDiff.add(node); + return; + } else { + if (!hasRootLevelPath(node)) { + filteredDiff.add(node); + } + } + }); + return filteredDiff; + } + + private boolean hasRootLevelPath(final JsonNode node) { + final var jsonPath = node.get("path").asText(); + return jsonPath.charAt(0 ) == '/' && !jsonPath.substring(1).contains("/"); + } + private boolean isRemoveOperation(final JsonNode node) { + return node.get("op").asText().equals("remove"); + } + + public JsonNode loadConfigFileAsJson(final String fileName, final CType cType) { + final var cd = securityApiDependencies.configurationRepository().getConfigDirectory(); + final var filepath = cd + fileName; + try { + return AccessController.doPrivileged((PrivilegedExceptionAction) () -> { + var loadedConfiguration = ConfigHelper.fromYamlFile(filepath, cType, ConfigurationRepository.DEFAULT_CONFIG_VERSION, 0, 0); + return Utils.convertJsonToJackson(loadedConfiguration, false); + }); + } catch (PrivilegedActionException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + throw new RuntimeException(e); + } + } + + + @Override + public List routes() { + return routes; + } + + @Override + protected CType getConfigType() { + return CType.ROLES; + } + + // private void rolesApiRequestHandlers(RequestHandler.RequestHandlersBuilder requestHandlersBuilder) { + // requestHandlersBuilder.add(null, methodNotImplementedHandler).onChangeRequest(Method.POST, this::processUpgrade); + // } + + // protected final ValidationResult processUpgrade(final RestRequest request) throws IOException { + // return loadConfiguration(nameParam(request), false).map( + // securityConfiguration -> { + // final int existingRolesConfig = securityConfiguration.configuration().getCEntry(getConfigType()); + // return ValidationResult.success("Upgrade Complete"); + // } + // ); + // } + +} diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/ReservedRolesApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/ReservedRolesApiAction.java deleted file mode 100644 index 02e43f1a87..0000000000 --- a/src/main/java/org/opensearch/security/dlic/rest/api/ReservedRolesApiAction.java +++ /dev/null @@ -1,162 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.security.dlic.rest.api; - -import static org.opensearch.security.dlic.rest.api.AbstractApiAction.LOGGER; -import static org.opensearch.security.dlic.rest.api.RequestHandler.methodNotImplementedHandler; -import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; - -import java.io.IOException; -import java.security.AccessController; -import java.security.PrivilegedExceptionAction; -import java.util.List; -import java.util.Map; - -import org.opensearch.cluster.service.ClusterService; -import org.opensearch.common.inject.Inject; -import org.opensearch.common.settings.Settings; -import org.opensearch.rest.RestRequest; -import org.opensearch.rest.RestRequest.Method; -import org.opensearch.security.dlic.rest.api.RolesApiAction.RoleRequestContentValidator; -import org.opensearch.security.dlic.rest.validation.EndpointValidator; -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.securityconf.impl.CType; -import org.opensearch.security.support.ConfigHelper; -import org.opensearch.threadpool.ThreadPool; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; - -public class ReservedRolesApiAction extends AbstractApiAction { - - private static final List routes = addRoutesPrefix( - ImmutableList.of( - new Route(Method.GET, "/roles/_reserved/_upgrade_check"), - new Route(Method.POST, "/roles/_reserved/_upgrade_apply") - ) - ); - - @Inject - public ReservedRolesApiAction( - final ClusterService clusterService, - final ThreadPool threadPool, - final SecurityApiDependencies securityApiDependencies - ) { - super(Endpoint.ROLES, clusterService, threadPool, securityApiDependencies); - this.requestHandlersBuilder.configureRequestHandlers(rhb -> { - rhb.onGetRequest(request -> - withIOException(() -> - loadConfiguration(getConfigType(), false, false) - .map(securityConfiguration -> { - try { - final var entries = securityConfiguration.getCEntries(); - final var entriesJson = convertJsonToJackson(entries); - - Utils. - } catch (final Exception ex) { - // log it ? - } - return (JsonNode)null; - })))); - } - - public static void uploadFile( - String filepath, - String index, - CType cType, - ) { - final String configType = cType.toLCString(); - - var fromDisk = AccessController.doPrivileged((PrivilegedExceptionAction) () -> - ConfigHelper.fromYamlFile(filepath, cType, configVersion, 0, 0)); - } - - - @Override - public List routes() { - return routes; - } - - @Override - protected CType getConfigType() { - return CType.ROLES; - } - - private void rolesApiRequestHandlers(RequestHandler.RequestHandlersBuilder requestHandlersBuilder) { - requestHandlersBuilder.add(null, methodNotImplementedHandler).onChangeRequest(Method.POST, this::processUpgrade); - } - - protected final ValidationResult processUpgrade(final RestRequest request) throws IOException { - return loadConfiguration(nameParam(request), false).map( - securityConfiguration -> { - final int existingRolesConfig = securityConfiguration.configuration().getCEntry(getConfigType()); - return ValidationResult.success("Upgrade Complete"); - } - ); - } - - @Override - protected EndpointValidator createEndpointValidator() { - return new EndpointValidator() { - - @Override - public Endpoint endpoint() { - return endpoint; - } - - @Override - public RestApiAdminPrivilegesEvaluator restApiAdminPrivilegesEvaluator() { - return securityApiDependencies.restApiAdminPrivilegesEvaluator(); - } - - @Override - public ValidationResult isAllowedToChangeImmutableEntity(SecurityConfiguration securityConfiguration) - throws IOException { - return EndpointValidator.super.isAllowedToChangeImmutableEntity(securityConfiguration).map(ignore -> { - if (isCurrentUserAdmin()) { - return ValidationResult.success(securityConfiguration); - } - return isAllowedToChangeEntityWithRestAdminPermissions(securityConfiguration); - }); - } - - @Override - public RequestContentValidator createRequestContentValidator(Object... params) { - return new RoleRequestContentValidator(new RequestContentValidator.ValidationContext() { - @Override - public Object[] params() { - return params; - } - - @Override - public Settings settings() { - return securityApiDependencies.settings(); - } - - @Override - public Map allowedKeys() { - final ImmutableMap.Builder allowedKeys = ImmutableMap.builder(); - if (isCurrentUserAdmin()) allowedKeys.put("reserved", DataType.BOOLEAN); - return allowedKeys.put("cluster_permissions", DataType.ARRAY) - .put("tenant_permissions", DataType.ARRAY) - .put("index_permissions", DataType.ARRAY) - .put("description", DataType.STRING) - .build(); - } - }); - } - }; - } - -} diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java index b0d46f8774..f38cf0580d 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java @@ -95,7 +95,8 @@ public static Collection getHandler( new AllowlistApiAction(Endpoint.ALLOWLIST, clusterService, threadPool, securityApiDependencies), new AuditApiAction(clusterService, threadPool, securityApiDependencies), new MultiTenancyConfigApiAction(clusterService, threadPool, securityApiDependencies), - new SecuritySSLCertsApiAction(clusterService, threadPool, securityKeyStore, certificatesReloadEnabled, securityApiDependencies) + new SecuritySSLCertsApiAction(clusterService, threadPool, securityKeyStore, certificatesReloadEnabled, securityApiDependencies), + new ConfigUpgradeApiAction(clusterService, threadPool, securityApiDependencies) ); } 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 ea782ea504..8398fbd7a4 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 @@ -83,4 +83,7 @@ public ToXContent errorMessage() { return errorMessage; } + public C getContent(){ + return content; + } }