Skip to content

Commit

Permalink
Functional diff
Browse files Browse the repository at this point in the history
Signed-off-by: Peter Nied <petern@amazon.com>
  • Loading branch information
peternied committed Mar 1, 2024
1 parent 7fb4bea commit 745eeb7
Show file tree
Hide file tree
Showing 7 changed files with 265 additions and 181 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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)
Expand All @@ -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<String, Object> 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<String, Object> users = response.getBodyAs(Map.class);
assertThat(users, allOf(aMapWithSize(3), hasKey(ADMIN_USER_NAME), hasKey(NEW_USER), hasKey(LIMITED_USER)));
}
}

private List<String> extractFieldNames(final JsonNode json) {
final var list = new ArrayList<String>();
json.fieldNames().forEachRemaining(list::add);
return list;
}
}
18 changes: 18 additions & 0 deletions src/integrationTest/resources/roles.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Route> 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<JsonNode> 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<JsonNode>) () -> {
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<Route> 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<String> 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");
// }
// );
// }

}
Loading

0 comments on commit 745eeb7

Please sign in to comment.