Skip to content

Commit

Permalink
Functional upgrade check
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 5, 2024
1 parent 33dea09 commit 90f09f6
Show file tree
Hide file tree
Showing 3 changed files with 155 additions and 60 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,52 @@

package org.opensearch.security.dlic.rest.api;



import static org.opensearch.security.dlic.rest.api.Responses.badRequestMessage;
import static org.opensearch.security.dlic.rest.api.Responses.ok;
import static org.opensearch.security.dlic.rest.api.Responses.response;
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.nio.file.Path;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

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.collect.Tuple;
import org.opensearch.common.inject.Inject;
import org.opensearch.common.settings.Settings;
import org.opensearch.core.rest.RestStatus;
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.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.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.flipkart.zjsonpatch.DiffFlags;
import com.flipkart.zjsonpatch.JsonDiff;
import com.google.common.collect.ImmutableList;
import org.opensearch.common.inject.Inject;
import org.opensearch.common.xcontent.XContentType;
import org.opensearch.core.rest.RestStatus;
Expand All @@ -49,15 +79,14 @@
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")
)
);
private static final List<Route> routes = addRoutesPrefix(ImmutableList.of(
new Route(Method.GET, "/_upgrade_check"),
new Route(Method.POST, "/_upgrade_perform")));

@Inject
public ConfigUpgradeApiAction(
Expand All @@ -67,55 +96,80 @@ public ConfigUpgradeApiAction(
) {
super(Endpoint.CONFIG, clusterService, threadPool, securityApiDependencies);
this.requestHandlersBuilder.configureRequestHandlers(rhb -> {
rhb.add(Method.GET, this::handleRolesCanUpgrade);
rhb.allMethodsNotImplemented().add(Method.GET, this::handleCanUpgrade).add(Method.POST, this::handleUpgrade);
});
}

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\"}"));
void handleCanUpgrade(final RestChannel channel, final RestRequest request, final Client client) throws IOException {
withIOException(() -> getConfigurations(request)
.map(configurations -> {
final var differencesList = new ArrayList<ValidationResult<Tuple<CType, JsonNode>>>();
for (final var configuration : configurations) {
differencesList.add(computeDifferenceToUpdate(configuration)
.map(differences -> ValidationResult.success(new Tuple<CType, JsonNode>(configuration, differences.deepCopy()))));
}
return ValidationResult.combine(differencesList);
}))
.valid(differencesList -> {
final var canUpgrade = differencesList.stream().anyMatch(entry -> entry.v2().size() > 0);

final ObjectNode response = JsonNodeFactory.instance.objectNode();
response.put("can_upgrade", canUpgrade);

if (canUpgrade) {
final ObjectNode differences = JsonNodeFactory.instance.objectNode();
differencesList.forEach(t -> {
differences.put(t.v1().toLCString(), t.v2());
});
response.put("differences", differences);
}
channel.sendResponse(new BytesRestResponse(RestStatus.OK, XContentType.JSON.mediaType(), response.toPrettyString()));
})
.error((status, toXContent) -> response(channel, status, toXContent));
}

private void handleUpgrade(final RestChannel channel, final RestRequest request, final Client client) throws IOException {
throw new UnsupportedOperationException("Unimplemented method 'handleUpgrade'");
}

private ValidationResult<JsonNode> computeDifferenceToUpdate(final CType configType) throws IOException {
return loadConfiguration(configType, false, false).map(activeRoles -> {
final var activeRolesJson = Utils.convertJsonToJackson(activeRoles, false);
final var defaultRolesJson = loadConfigFileAsJson(configType);
final var rawDiff = JsonDiff.asJson(activeRolesJson, defaultRolesJson, EnumSet.of(DiffFlags.OMIT_VALUE_ON_REMOVE));
return ValidationResult.success(filterRemoveOperations(rawDiff));
});
}

private ValidationResult<Set<CType>> getConfigurations(final RestRequest request) {
final String[] configs = request.paramAsStringArray("configs", null);

final var configurations = Optional.ofNullable(configs)
.map(CType::fromStringValues)
.orElse(supportedConfigs());

if (!configurations.stream().allMatch(supportedConfigs()::contains)) {
// Remove all supported configurations
configurations.removeAll(supportedConfigs());
return ValidationResult.error(RestStatus.BAD_REQUEST, badRequestMessage("Unsupported configurations for upgrade" + configurations));
}

return ValidationResult.success(configurations);
}

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 Set<CType> supportedConfigs() {
return Set.of(CType.ROLES);
}

private JsonNode filterRemoveOperations(final JsonNode diff) {
final ArrayNode filteredDiff = JsonNodeFactory.instance.arrayNode();
diff.forEach(node -> {
if (!isRemoveOperation(node)) {
filteredDiff.add(node);
filteredDiff.add(node.deepCopy());
return;
} else {
if (!hasRootLevelPath(node)) {
filteredDiff.add(node);
filteredDiff.add(node.deepCopy());
}
}
});
Expand All @@ -124,28 +178,28 @@ private JsonNode filterRemoveOperations(final JsonNode diff) {

private boolean hasRootLevelPath(final JsonNode node) {
final var jsonPath = node.get("path").asText();
return jsonPath.charAt(0 ) == '/' && !jsonPath.substring(1).contains("/");
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) {
public JsonNode loadConfigFileAsJson(final CType cType) {
final var cd = securityApiDependencies.configurationRepository().getConfigDirectory();
final var filepath = cd + fileName;
final var filepath = cType.configFile(Path.of(cd)).toString();
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) {
} catch (final PrivilegedActionException e) {
// TODO Auto-generated catch block
e.printStackTrace();
throw new RuntimeException(e);
}
}


@Override
public List<Route> routes() {
return routes;
Expand All @@ -156,17 +210,45 @@ protected CType getConfigType() {
return CType.ROLES;
}

// private void rolesApiRequestHandlers(RequestHandler.RequestHandlersBuilder requestHandlersBuilder) {
// requestHandlersBuilder.add(null, methodNotImplementedHandler).onChangeRequest(Method.POST, this::processUpgrade);
// }
@Override
protected EndpointValidator createEndpointValidator() {
return new EndpointValidator() {

// 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");
// }
// );
// }
@Override
public Endpoint endpoint() {
return endpoint;
}

@Override
public RestApiAdminPrivilegesEvaluator restApiAdminPrivilegesEvaluator() {
return securityApiDependencies.restApiAdminPrivilegesEvaluator();
}

@Override
public RequestContentValidator createRequestContentValidator(final Object... params) {
return RequestContentValidator.of(new RequestContentValidator.ValidationContext() {

@Override
public Set<String> mandatoryKeys() {
return Set.of("configs");
}

@Override
public Map<String, DataType> allowedKeys() {
return Map.of("configs", DataType.ARRAY);
}

@Override
public Object[] params() {
return params;
}

@Override
public Settings settings() {
return securityApiDependencies.settings();
}
});
}
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public static SecurityConfiguration of(final String entityName, final SecurityDy
public static SecurityConfiguration of(
final JsonNode requestContent,
final String entityName,
final SecurityDynamicConfiguration<?> configuration
final SecurityDynamicConfiguration<?> configuration
) {
Objects.requireNonNull(configuration, "configuration hasn't been set");
Objects.requireNonNull(requestContent, "requestContent hasn't been set");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
package org.opensearch.security.dlic.rest.validation;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

import org.opensearch.common.CheckedBiConsumer;
Expand Down Expand Up @@ -50,6 +52,17 @@ public static <L> ValidationResult<L> error(final RestStatus status, final ToXCo
return new ValidationResult<>(status, errorMessage);
}

public static <L> ValidationResult<List<L>> combine(final List<ValidationResult<L>> entries) {
final var returnList = new ArrayList<L>();
for (final var entry : entries) {
if (!entry.isValid()) {
return error(entry.status(), entry.errorMessage());
}
returnList.add(entry.content);
}
return success(returnList);
}

public <L> ValidationResult<L> map(final CheckedFunction<C, ValidationResult<L>, IOException> mapper) throws IOException {
if (content != null) {
return Objects.requireNonNull(mapper).apply(content);
Expand Down Expand Up @@ -83,7 +96,7 @@ public ToXContent errorMessage() {
return errorMessage;
}

public C getContent(){
return content;
public C getContent() {
return content;
}
}

0 comments on commit 90f09f6

Please sign in to comment.