Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/*
* 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.api;

import java.util.Map;

import org.apache.http.HttpStatus;
import org.junit.Before;
import org.junit.Test;

import org.opensearch.test.framework.cluster.TestRestClient;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.isOneOf;
import static org.opensearch.security.OpenSearchSecurityPlugin.PLUGINS_PREFIX;
import static org.opensearch.security.support.ConfigConstants.EXPERIMENTAL_SECURITY_CONFIGURATIONS_VERSIONS_ENABLED;

public class RollbackVersionApiIntegrationTest extends AbstractApiIntegrationTest {

private String endpointPrefix() {
return PLUGINS_PREFIX + "/api";
}

private String RollbackBase() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If these are all constants can we declare them as constant fields at the top of this file?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack!

return endpointPrefix() + "/rollback";
}

private String RollbackVersion(String versionId) {
return RollbackBase() + "/version/" + versionId;
}

@Override
protected Map<String, Object> getClusterSettings() {
Map<String, Object> settings = super.getClusterSettings();
settings.put(EXPERIMENTAL_SECURITY_CONFIGURATIONS_VERSIONS_ENABLED, true);
return settings;
}

@Before
public void setupConfigVersionsIndex() throws Exception {
try (TestRestClient client = localCluster.getRestClient(ADMIN_USER_NAME, DEFAULT_PASSWORD)) {
client.get("/_cluster/health?wait_for_status=yellow&timeout=30s");
client.delete("/.opensearch_security_config_versions");
client.put("/.opensearch_security_config_versions");
client.post("/_refresh");
client.get("/_cluster/health/.opensearch_security_config_versions?wait_for_status=yellow&timeout=5s");

String bulkPayload =
"{ \"index\": { \"_index\": \".opensearch_security_config_versions\", \"_id\": \"opensearch_security_config_versions\" } }\n"
+ "{ \"versions\": ["
+ " {"
+ " \"version_id\": \"v1\","
+ " \"timestamp\": \"2025-04-03T00:00:00Z\","
+ " \"modified_by\": \"admin\","
+ " \"security_configs\": {"
+ " \"internalusers\": {"
+ " \"lastUpdated\": \"2025-04-03T00:00:00Z\","
+ " \"configData\": { \"admin\": { \"hash\": \"$2y$12$aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\" } }"
+ " }"
+ " }"
+ " },"
+ " {"
+ " \"version_id\": \"v2\","
+ " \"timestamp\": \"2025-04-04T00:00:00Z\","
+ " \"modified_by\": \"admin\","
+ " \"security_configs\": {"
+ " \"internalusers\": {"
+ " \"lastUpdated\": \"2025-04-04T00:00:00Z\","
+ " \"configData\": { \"admin\": { \"hash\": \"$2y$12$bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\" } }"
+ " }"
+ " }"
+ " }"
+ "] }\n";

var response = client.postJson("/_bulk?refresh=true", bulkPayload);
assertThat("Bulk insert failed", response.getStatusCode(), is(200));
}
}

@Test
public void testRollbackToPreviousVersion_success() throws Exception {
withUser(ADMIN_USER_NAME, DEFAULT_PASSWORD, client -> {
var response = client.post(RollbackBase());
assertThat(response.getStatusCode(), is(HttpStatus.SC_OK));
assertThat(response.getTextFromJsonBody("/status"), equalTo("OK"));
assertThat(response.getTextFromJsonBody("/message"), containsString("config rolled back to version"));
});
}

@Test
public void testRollbackToSpecificVersion_success() throws Exception {
String versionId = "v1";
withUser(ADMIN_USER_NAME, DEFAULT_PASSWORD, client -> {
var response = client.post(RollbackVersion(versionId));
assertThat(response.getStatusCode(), is(HttpStatus.SC_OK));
assertThat(response.getTextFromJsonBody("/status"), equalTo("OK"));
assertThat(response.getTextFromJsonBody("/message"), containsString("config rolled back to version " + versionId));
});
}

@Test
public void testRollbackWithNonAdmin_shouldBeUnauthorized() throws Exception {
withUser(NEW_USER, DEFAULT_PASSWORD, client -> {
var response = client.post(RollbackBase());
assertThat(response.getStatusCode(), isOneOf(HttpStatus.SC_FORBIDDEN, HttpStatus.SC_UNAUTHORIZED));
});
}

@Test
public void testRollbackToInvalidVersion_shouldReturnNotFound() throws Exception {
withUser(ADMIN_USER_NAME, DEFAULT_PASSWORD, client -> {
var response = client.post(RollbackVersion("does-not-exist"));
assertThat(response.getStatusCode(), is(HttpStatus.SC_NOT_FOUND));
assertThat(response.getTextFromJsonBody("/message"), containsString("not found"));
});
}

@Test
public void testRollbackWhenOnlyOneVersion_shouldFail() throws Exception {
withUser(ADMIN_USER_NAME, DEFAULT_PASSWORD, client -> {
client.delete("/.opensearch_security_config_versions");
client.put("/.opensearch_security_config_versions");
client.post("/_refresh");
client.get("/_cluster/health/.opensearch_security_config_versions?wait_for_status=yellow&timeout=30s");

String bulkPayload = ""
+ "{ \"index\": { \"_index\": \".opensearch_security_config_versions\", \"_id\": \"opensearch_security_config_versions\" } }\n"
+ "{ \"versions\": [ {"
+ " \"version_id\": \"v1\","
+ " \"timestamp\": \"2025-04-03T00:00:00Z\","
+ " \"modified_by\": \"admin\","
+ " \"security_configs\": {"
+ " \"config_type_1\": {"
+ " \"lastUpdated\": \"2025-04-03T00:00:00Z\","
+ " \"configData\": {"
+ " \"key1\": { \"dummy\": \"value1\" }"
+ " }"
+ " }"
+ " }"
+ "} ] }\n";

var bulkResponse = client.postJson("/_bulk?refresh=true", bulkPayload);
assertThat("Bulk insert failed: " + bulkResponse.getBody(), bulkResponse.getStatusCode(), is(200));

client.post("/_refresh");

var response = client.post(RollbackBase());
assertThat(response.getStatusCode(), is(404));
assertThat(response.getBody(), containsString("No previous version available to rollback"));
});
}

}
Original file line number Diff line number Diff line change
@@ -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.api;

import java.util.Map;

import org.junit.Before;
import org.junit.Test;

import org.opensearch.test.framework.TestSecurityConfig;
import org.opensearch.test.framework.cluster.TestRestClient;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.isOneOf;
import static org.opensearch.security.OpenSearchSecurityPlugin.PLUGINS_PREFIX;
import static org.opensearch.security.support.ConfigConstants.EXPERIMENTAL_SECURITY_CONFIGURATIONS_VERSIONS_ENABLED;

public class ViewVersionApiIntegrationTest extends AbstractApiIntegrationTest {

static {
testSecurityConfig.user(new TestSecurityConfig.User("limitedUser").password("limitedPass"));
}

private String endpointPrefix() {
return PLUGINS_PREFIX + "/api";
}

private String viewVersionBase() {
return endpointPrefix() + "/version";
}

private String viewVersion(String versionId) {
return viewVersionBase() + "/" + versionId;
}

@Override
protected Map<String, Object> getClusterSettings() {
Map<String, Object> settings = super.getClusterSettings();
settings.put(EXPERIMENTAL_SECURITY_CONFIGURATIONS_VERSIONS_ENABLED, true);
return settings;
}

@Before
public void setupIndexAndCerts() throws Exception {
try (TestRestClient client = localCluster.getRestClient(ADMIN_USER_NAME, DEFAULT_PASSWORD)) {
client.get("/_cluster/health?wait_for_status=yellow&timeout=30s");
client.delete("/.opensearch_security_config_versions");
client.put("/.opensearch_security_config_versions");
client.post("/_refresh");
client.get("/_cluster/health/.opensearch_security_config_versions?wait_for_status=yellow&timeout=5s");

String bulkPayload = ""
+ "{ \"index\": { \"_index\": \".opensearch_security_config_versions\", \"_id\": \"opensearch_security_config_versions\" } }\n"
+ "{ \"versions\": [ {"
+ " \"version_id\": \"v1\","
+ " \"timestamp\": \"2025-04-03T00:00:00Z\","
+ " \"modified_by\": \"admin\","
+ " \"security_configs\": {"
+ " \"config_type_1\": {"
+ " \"lastUpdated\": \"2025-04-03T00:00:00Z\","
+ " \"configData\": {"
+ " \"key1\": { \"dummy\": \"value1\" }"
+ " }"
+ " }"
+ " }"
+ "} ] }\n";

var bulkResponse = client.postJson("/_bulk?refresh=true", bulkPayload);
assertThat("Failed to insert config versions doc via bulk: " + bulkResponse.getBody(), bulkResponse.getStatusCode(), is(200));
}
}

@Test
public void testViewAllVersions() throws Exception {
withUser(ADMIN_USER_NAME, DEFAULT_PASSWORD, client -> {
var response = ok(() -> client.get(viewVersionBase()));
var json = response.bodyAsJsonNode();

assertThat(json.has("versions"), is(true));
var versions = json.get("versions");
assertThat(versions.isArray(), is(true));
assertThat(versions.size(), greaterThan(0));
});
}

@Test
public void testViewSpecificVersionFound() throws Exception {
withUser(ADMIN_USER_NAME, DEFAULT_PASSWORD, client -> {
var response = ok(() -> client.get(viewVersion("v1")));
var json = response.bodyAsJsonNode();

assertThat(json.has("versions"), is(true));
var versions = json.get("versions");
assertThat(versions.isArray(), is(true));
assertThat(versions.size(), is(1));

var ver = versions.get(0);
assertThat(ver.get("version_id").asText(), equalTo("v1"));
});
}

@Test
public void testViewSpecificVersionNotFound() throws Exception {
withUser(ADMIN_USER_NAME, DEFAULT_PASSWORD, client -> {
var response = notFound(() -> client.get(viewVersion("does-not-exist")));
var json = response.bodyAsJsonNode();

assertThat(json.has("status"), is(true));
assertThat(json.get("status").asText(), equalTo("NOT_FOUND"));

assertThat(json.has("message"), is(true));
assertThat(json.get("message").asText(), containsString("not found"));
});
}

@Test
public void testViewAllVersions_forbiddenWithoutAdminCert() throws Exception {
withUser("limitedUser", "limitedPass", client -> {
var response = client.get(viewVersionBase());
assertThat(response.getStatusCode(), isOneOf(401, 403));
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,7 @@ public enum Endpoint {
VALIDATE,
ALLOWLIST,
NODESDN,
SSL;
SSL,
VIEW_VERSION,
ROLLBACK_VERSION;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two should be grouped as version? A singular permission that allows rest-admin privileges to the view and rollback. Similar to other endpoints already present here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

View API is just GET request, while rollback API is a POST request, hence kept them separate. But will look into this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just version may not sound similar to for ex: say roles because GET /role/{}, PUT /role/{} is intuitive to understand but GET /version/{}, PUT /version/{} is not that intuitive. PUT/POST /version/{} is not updating its entity, here it is doing rolling back which is not intuitive to understand

}
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ default String build() {
.put(Endpoint.ROLESMAPPING, action -> buildEndpointPermission(Endpoint.ROLESMAPPING))
.put(Endpoint.TENANTS, action -> buildEndpointPermission(Endpoint.TENANTS))
.put(Endpoint.SSL, action -> buildEndpointActionPermission(Endpoint.SSL, action))
.put(Endpoint.VIEW_VERSION, action -> buildEndpointPermission(Endpoint.VIEW_VERSION))
.put(Endpoint.ROLLBACK_VERSION, action -> buildEndpointPermission(Endpoint.ROLLBACK_VERSION))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similar to the comment above, this should be clubbed as one permission.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack

.build();

private final ThreadContext threadContext;
Expand Down
Loading
Loading