-
Notifications
You must be signed in to change notification settings - Fork 331
Added View API and Rollback API for Versioned Security Configurations #5431
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
8fa6094
7b3c979
6fac88a
a802c33
9a3dc90
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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() { | ||
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 |
---|---|---|
|
@@ -29,5 +29,7 @@ public enum Endpoint { | |
VALIDATE, | ||
ALLOWLIST, | ||
NODESDN, | ||
SSL; | ||
SSL, | ||
VIEW_VERSION, | ||
ROLLBACK_VERSION; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. similar to the comment above, this should be clubbed as one permission. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ack |
||
.build(); | ||
|
||
private final ThreadContext threadContext; | ||
|
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ack!