Skip to content

Conversation

divyanshi-0402
Copy link
Contributor

Description

Implementing View API and Rollback API for Versioned Security Configurations, build upon the changes from the PR : Versioned Security Configuration Management #5357.

API Design

View Security Configuration Versions

Purpose: View the complete history of security configuration changes, or any specific version of the security configurations.

Endpoint:

  • To view all the versions : GET /_plugins/_security/api/version
  • To view a specific version : GET /_plugins/_security/api/version/{versionId}

Access Control: Admin/Security Manager permissions required

Response Format for specific version:

curl -XGET "https://localhost:9200/_plugins/_security/api/version/v2?pretty" -u 'admin:Divvv@admin123' --insecure

{
  "versions": [
    {
      "version_id": "v2",
      "timestamp": "2025-05-23T06:56:20.081933886Z",
      "modified_by": "admin",
      "security_configs": {
        "rolesmapping": {
          "lastUpdated": "2025-05-23T06:56:20.081933886Z",
          "configData": {
             "_meta" : {
              "type" : "rolesmapping",
              "config_version" : 2
            },
            "all_access": {
              "backend_roles": ["admin"],
              "users": [],
              "hosts": [],
              "reserved": false,
              "hidden": false,
              "description": "Maps admin to all_access",
              "and_backend_roles": []
            },
            ...
          }
        },
        "roles": {
          "lastUpdated": "2025-05-23T06:56:20.081933886Z",
          "configData": {
            "_meta" : {
              "type" : "roles",
              "config_version" : 2
            },
            "alerting_full_access": {
              "cluster_permissions": [
                "cluster:admin/opendistro/alerting/*",
                "cluster:admin/opensearch/alerting/*",
                ...
              ],
              "index_permissions": [
                {
                  "index_patterns": ["*"],
                  "allowed_actions": ["indices:admin/mappings/get", ...]
                }
              ],
              "reserved": true,
              "hidden": false,
              "static": false
            },
            ...
          }
        },
        "internalusers": {
          "lastUpdated": "2025-05-23T06:56:20.081933886Z",
          "configData": {
            "_meta" : {
              "type" : "internalusers",
              "config_version" : 2
            },
            "admin": {
              "backend_roles": ["admin"],
              "reserved": true,
              "description": "Demo admin user",
              ...
            },
            "testuser": {
              "backend_roles": [],
              "reserved": false,
              ...
            },
            ...
          }
        },
        ...
      }
    }
  ]
}

Response Format for all versions :

curl -XGET "https://localhost:9200/_plugins/_security/api/version?pretty" -u 'admin:Divvv@admin123' --insecure

{
  "versions": [
    {
      "version_id": "v1",
      "timestamp": "2025-05-22T08:46:11.887620466Z",
      "modified_by": "system",
      "security_configs": {
        "rolesmapping": {
          "lastUpdated": "2025-05-22T08:46:11.887620466Z",
          "configData": {
            "_meta" : {
              "type" : "rolesmapping",
              "config_version" : 2
            },
            "all_access": {
              "backend_roles": ["admin"],
              "users": [],
              "hosts": [],
              "reserved": false,
              "hidden": false,
              "description": "Maps admin to all_access",
              "and_backend_roles": []
            },
            ...
          }
        },
        "internalusers": {
          "lastUpdated": "2025-05-22T08:46:11.887620466Z",
          "configData": {
             "_meta" : {
              "type" : "internalusers",
              "config_version" : 2
            },
            "admin": {
              "backend_roles": ["admin"],
              "reserved": true,
              "hidden": false,
              "description": "Demo admin user",
              ...
            },
            ...
          }
        },
        ...
      }
    },
    {
      "version_id": "v2",
      "timestamp": "2025-05-23T06:56:20.081933886Z",
      "modified_by": "admin",
      "security_configs": {
        "rolesmapping": {
          "lastUpdated": "2025-05-23T06:56:20.081933886Z",
          "configData": {
             "_meta" : {
              "type" : "rolesmapping",
              "config_version" : 2
            },
            "all_access": {
              "backend_roles": ["admin"],
              "users": [],
              "hosts": [],
              "reserved": false,
              "hidden": false,
              "description": "Maps admin to all_access",
              "and_backend_roles": []
            },
            ...
          }
        },
        "internalusers": {
          "lastUpdated": "2025-05-23T06:56:20.081933886Z",
          "configData": {
            "_meta" : {
              "type" : "internalusers",
              "config_version" : 2
            },
            "admin": {
              "backend_roles": ["admin"],
              "reserved": true,
              "hidden": false,
              "description": "Demo admin user",
              ...
            },
            "testuser": {
              "backend_roles": [],
              "reserved": false,
              "hidden": false,
              ...
            },
            ...
          }
        },
        ...
      }
    },
    ...
  ]
}

Response format if the version doesn't exist :

curl -XGET "https://localhost:9200/_plugins/_security/api/version/v3?pretty" -u 'admin:Divvv@admin123' --insecure
{
  "status" : "NOT_FOUND",
  "message" : "Version v3 not found"
}

Response if try to access with non admin role :

curl -XGET "https://localhost:9200/_plugins/_security/api/version?pretty" -u 'test_user:TestAPI@123' --insecure                       
{
  "status" : "FORBIDDEN",
  "message" : "No permission to access REST API: User test_user with Security roles [read_only_role, own_index] does not have any role privileged for admin access. No client TLS certificate found in request"
}

Rollback to desired Security Configuration Versions

Purpose : Allows rollback to immediate or any desired version of security

Endpoint:

  • To rollback to immediate previous version : POST /_plugins/_security/api/rollback
  • To rollback to a specific version : POST /_plugins/_security/api/rollback/version/{versionID}

Access Control: Admin/Security Manager permissions required

Response format to rollback to immediate previous version :

[.opendistro_security_config_versions] index has 8 versions 

curl -XPOST "https://localhost:9200/_plugins/_security/api/rollback" -u 'admin:Divvv@admin123' --insecure           
{
  "status" : "OK",
  "message" : "config rolled back to version v7"
}


 Detected changes in security configuration: [{"op":"add","path":"/internalusers/testuser","value":{"attributes":{},"backend_roles":[],"hash":"$2y$12$3q6lxV8DBxvifcRJ4tMuUeclwyJzawJTzhY.JcKEpL1TdWy.JeUPu","hidden":false,"opendistro_security_roles":[],"reserved":false,"static":false}}]
 Successfully saved version v9 to .opendistro_security_config_versions

Response format to rollback to a specific version :

curl -XPOST "https://localhost:9200/_plugins/_security/api/rollback/version/v3" -u 'admin:Divvv@admin123' --insecure
{
  "status" : "OK",
  "message" : "config rolled back to version v3"
}% 


Detected changes in security configuration: [{"op":"remove","path":"/internalusers/testuser"}]
Successfully saved version v10 to .opendistro_security_config_versions

Response format if the version doesn't exist :

curl -XPOST "https://localhost:9200/_plugins/_security/api/rollback/version/v13" -u 'admin:Divvv@admin123' --insecure
{
  "status" : "NOT_FOUND",
  "message" : "Version v13 not found"
}

Response if try to access with non admin role :

curl -XPOST "https://localhost:9200/_plugins/_security/api/rollback?pretty" -u 'test_user:TestAPI@123' --insecure 
{
  "status" : "FORBIDDEN",
  "message" : "No permission to access REST API: User test_user with Security roles [read_only_role, own_index] does not have any role privileged for admin access. No client TLS certificate found in request"
}

Test details for multi-node testing:

Created two nodes : opensearch-node1 and opensearch-node1

node 1 :

./bin/opensearch -E cluster.name=my-cluster -E node.name=node1 -E path.data=./data1 -E http.port=9200 -E transport.port=9300

[node1] Successfully saved version v1 to .opensearch_security_config_versions

[INFO ][o.o.s.c.ConfigurationRepository] [node1] Node 'node1' initialized

node 2 :

./bin/opensearch -E cluster.name=my-cluster -E node.name=node2 -E path.data=./data2 -E http.port=9201 -E transport.port=9301

[INFO ][o.o.s.c.ConfigurationRepository] [node2] Node 'node2' initialized

Rollback initiated from node-1:

curl -XPOST "https://172.19.119.134:9200/_plugins/_security/api/rollback" -u 'admin:admin' --insecure
{
  "status" : "OK",
  "message" : "config rolled back to version v1"
}%                   

[node1] Detected changes in security configuration: [{"op":"remove","path":"/internalusers/testuser"}]
[node1] Successfully saved version v3 to .opensearch_security_config_versions

Rollback initiated from node-2 :

curl -XPOST "https://172.19.119.134:9201/_plugins/_security/api/rollback/version/v1" -u 'admin:admin' --insecure                                       
{
  "status" : "OK",
  "message" : "config rolled back to version v1"
}

[node1] Detected changes in security configuration: [{"op":"remove","path":"/internalusers/testuser"}]
[node1] Successfully saved version v5 to .opensearch_security_config_versions

Issues Resolved

Related to #5093

Testing

Unit Tests and Integration Tests included

Check List

  • New functionality includes testing
  • New functionality has been documented
  • New Roles/Permissions have a corresponding security dashboards plugin PR
  • API changes companion pull request created
  • Commits are signed per the DCO using --signoff

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
For more information on following Developer Certificate of Origin and signing off your commits, please check here.

Signed-off-by: Divyanshi Chouksey <divvv@amazon.com>
private ValidationResult<SecurityConfiguration> viewAllVersions() throws IOException {
SecurityConfigVersionDocument doc = versionsLoader.loadFullDocument();

return ValidationResult.error(OK, (builder, params) -> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why error? Should it be ValidationResult.success?

Copy link
Member

Choose a reason for hiding this comment

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

+1. Should be ValidationResult.success 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.

ValidationResult.success only takes a SecurityConfiguration as content object, so in order to build a custom JSON I had to use this.

Copy link
Member

@DarshitChanpura DarshitChanpura Jun 25, 2025

Choose a reason for hiding this comment

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

.error() is not the correct usage. You can simply create response class that extends ToXContentObject and then return an instance of that object here.
You can refer this file: https://github.com/opensearch-project/security/pull/5389/files#diff-157c9f69cf6c49e618db3748a7803470432b527ce78fe5251228d093daa5483e

Copy link
Contributor

Choose a reason for hiding this comment

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

We cannot use ValidationResult.error, it is very misleading and wrong pattern

builder.field("security_configs");
Map<String, Object> plainConfigs = new LinkedHashMap<>();
for (Map.Entry<String, SecurityConfigVersionDocument.HistoricSecurityConfig<?>> entry : ver.getSecurity_configs().entrySet()) {
Map<String, Object> scMap = new LinkedHashMap<>();
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: Update variable name

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

Comment on lines +160 to +164
var maybeVer = doc.getVersions().stream().filter(v -> versionId.equals(v.getVersion_id())).findFirst();

if (maybeVer.isEmpty()) {
return ValidationResult.error(NOT_FOUND, payload(NOT_FOUND, "Version " + versionId + " not found"));
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need this here? We seem to be doing in rollbackCommon() as well

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, doing it once.

Copy link
Contributor

Choose a reason for hiding this comment

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

This might make more sense to keep in rollbackCommon() since you would need to check this anyway for any caller of this function

throw new IOException("Rollback aborted: Could not fetch current config for cType '" + cTypeName + "'");
}

SecurityDynamicConfiguration<?> sdc = SecurityDynamicConfiguration.empty(cType);
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: rename or expand it

Copy link
Member

Choose a reason for hiding this comment

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

since everything is in context of security you can name the variables as updateConfig or rollBackConfig since this indicates a new entry to be created from existing entry.

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!

Comment on lines 216 to 219
CType<?> cType = CType.fromString(cTypeName);
if (cType == null) {
throw new IOException("Rollback aborted: Unknown config type '" + cTypeName + "' found in version");
}
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: This check needs to be done before checking data for that cType?

Copy link
Contributor

Choose a reason for hiding this comment

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

Also exception type IOException doesn't seem appropriate. IllegalStateException?
Check in other places as well

Copy link
Member

Choose a reason for hiding this comment

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

this should never be null since we are responsible for storing the information. A Ctype will be unknown only when security plugin moves away from current CTypes. Also this should throw NPE since we use cTypes as keys. Another option would be NoSuchFieldException.

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!

sdc.setPrimaryTerm(currentConfig.getPrimaryTerm());

for (Map.Entry<String, ?> configEntry : sc.getConfigData().entrySet()) {
if ("_meta".equals(configEntry.getKey())) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we skip _meta? Good to add comments for these kinds for readers of code to understand

Copy link
Member

Choose a reason for hiding this comment

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

+1.

What is the expected behavior when config version changes? i.e. roles change from v7 to v8.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Including _meta creates a duplicate entry for the field.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Currently we have rollback functionality for same versions of config types.

Copy link
Contributor

Choose a reason for hiding this comment

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

Including _meta creates a duplicate entry for the field

Why? which other code is adding _meta field?

Comment on lines 248 to 272
for (Map.Entry<CType<?>, SecurityDynamicConfiguration<?>> entry : configsToApply.entrySet()) {
AbstractApiAction.saveAndUpdateConfigsAsync(
securityApiDependencies,
client,
entry.getKey(),
entry.getValue(),
new ActionListener<IndexResponse>() {
@Override
public void onResponse(IndexResponse r) {
latch.countDown();
}

@Override
public void onFailure(Exception e) {
log.error("Rollback: failed to write config for cType={} : {}", entry.getKey().toLCString(), e.getMessage(), e);
latch.countDown();
}
}
);
log.info("Rollback: wrote config data for cType={}", entry.getKey().toLCString());
}

if (!latch.await(20, java.util.concurrent.TimeUnit.SECONDS)) {
throw new IOException("Timeout while writing rolled-back configs");
}
Copy link
Contributor

Choose a reason for hiding this comment

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

  • Can we move this to another function?
  • Timeout value can be constant

Copy link
Member

Choose a reason for hiding this comment

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

Magic variables should be declared as class level constants.

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


for (Map.Entry<CType<?>, SecurityDynamicConfiguration<?>> entry : backups.entrySet()) {
try {
AbstractApiAction.saveAndUpdateConfigs(securityApiDependencies, client, entry.getKey(), entry.getValue()).actionGet();
Copy link
Contributor

Choose a reason for hiding this comment

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

  • High: This won't update cache
  • Can we test for this as well

Copy link
Member

Choose a reason for hiding this comment

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

Have we tested the sync vs async behavior 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.

Yes, async updates cache and triggers update for version index as well, sync doesn't.

return builder;
}

private boolean isConfigEqual(SecurityDynamicConfiguration<?> currentConfig, SecurityDynamicConfiguration<?> targetConfig) {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: This can be util function I think. Its reusable by any other code

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

}
}

private void rollbackConfigsToSecurityIndex(SecurityConfigVersionDocument.Version<?> versionData) throws IOException {
Copy link
Contributor

Choose a reason for hiding this comment

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

Variable names are not clear. Reading is bit tough here. Need to go up and down to know what variable is.
Rename such that names give meaning of that it is intended for. For ex: currentConfig used here is a good naming, easy to understand

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 ValidationResult.error(NOT_FOUND, payload(NOT_FOUND, "Version " + versionId + " not found"));
}

SecurityConfigVersionsLoader.sortVersionsById(doc.getVersions());
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need sorting 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.

Not required, removed.

@cwperks
Copy link
Member

cwperks commented Jun 23, 2025

curl -XPOST "https://172.19.119.134:9200/_plugins/_security/api/rollback" -u 'admin:admin' --insecure
{
  "status" : "OK",
  "message" : "config rolled back to version v1"
}%                   

[node1] Detected changes in security configuration: [{"op":"remove","path":"/internalusers/testuser"}]
[node1] Successfully saved version v3 to .opensearch_security_config_versions

In the context of the changes I understand why this operation will create a v3, but I do think its a bit confusing that a rollback creates a new "version".

@cwperks
Copy link
Member

cwperks commented Jun 23, 2025

Is @lastUpdated specific to the individual config type or does it apply to the entire version?

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!

private static final Logger log = LogManager.getLogger(RollbackVersionApiAction.class);

private static final List<Route> routes = addRoutesPrefix(
ImmutableList.of(new Route(POST, "/rollback"), new Route(POST, "/rollback/version/{versionID}"))
Copy link
Member

Choose a reason for hiding this comment

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

By passing versionID in does this effectively support moving forward as well?

Copy link
Member

Choose a reason for hiding this comment

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

Does it make sense to introduce a dryRun url param that would return the patch applied as its output without actually applying the patch?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's essentially rollback, providing versionID allows users to rollback to any particular version.
Yes, we are separately working on a dryRun API for previewing the patch applied and it's impact before actually applying it.

Copy link
Member

@DarshitChanpura DarshitChanpura left a comment

Choose a reason for hiding this comment

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

Thank you for adding these APIs. I have added few comments from my initial review.

One outstanding question I have is, do admin's have an option to choose a particular config to rollback when rolling back to previous version? I'm not exactly aware of a scenario, but hypothetically an admin may have removed a user and also updated a role simultaneously, this would bring us to the scenario I mentioned.

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

@@ -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

private static final Logger LOGGER = LogManager.getLogger(ViewVersionApiAction.class);

private static final List<Route> routes = addRoutesPrefix(
ImmutableList.of(new Route(Method.GET, "/version"), new Route(Method.GET, "/version/{versionID}"))
Copy link
Member

Choose a reason for hiding this comment

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

get alll versions endpoint should be changed to /versions

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

private ValidationResult<SecurityConfiguration> viewAllVersions() throws IOException {
SecurityConfigVersionDocument doc = versionsLoader.loadFullDocument();

return ValidationResult.error(OK, (builder, params) -> {
Copy link
Member

Choose a reason for hiding this comment

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

+1. Should be ValidationResult.success here.

private static final Logger log = LogManager.getLogger(RollbackVersionApiAction.class);

private static final List<Route> routes = addRoutesPrefix(
ImmutableList.of(new Route(POST, "/rollback"), new Route(POST, "/rollback/version/{versionID}"))
Copy link
Member

Choose a reason for hiding this comment

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

should be renamed to /version/rollback and /version/rollback/{versionId}

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


if (maybeVer.isEmpty()) {
return ValidationResult.error(NOT_FOUND, payload(NOT_FOUND, "Version " + versionId + " not found"));
}
Copy link
Member

Choose a reason for hiding this comment

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

this method is pretty much identical with the one above, see if you can club it together to re-use code.

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 ValidationResult.error(OK, (builder, params) -> {
XContentBuilder inner = buildVersionsJsonBuilder(List.of(maybeVer.get()));
builder.copyCurrentStructure(JsonXContent.jsonXContent.createParser(null, null, BytesReference.bytes(inner).streamInput()));
Copy link
Member

Choose a reason for hiding this comment

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

is see this pattern being repeated, 2 things:

  1. Can you use fromXContent and toXContent instead of buildVersionsJsonBuilder and copyCurrentStructure.
  2. See if you can over-load implementation of payload method in Responses.java that accepts JsonContent.

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 will check this out.

divyanshi-0402 and others added 2 commits June 24, 2025 11:42
Signed-off-by: Divyanshi Chouksey <divvv@amazon.com>
@divyanshi-0402
Copy link
Contributor Author

curl -XPOST "https://172.19.119.134:9200/_plugins/_security/api/rollback" -u 'admin:admin' --insecure
{
  "status" : "OK",
  "message" : "config rolled back to version v1"
}%                   

[node1] Detected changes in security configuration: [{"op":"remove","path":"/internalusers/testuser"}]
[node1] Successfully saved version v3 to .opensearch_security_config_versions

In the context of the changes I understand why this operation will create a v3, but I do think its a bit confusing that a rollback creates a new "version".

We are tracking all the changes in the version index, the rollback triggers an update to the security configurations hence creating a new version to track it.

@divyanshi-0402
Copy link
Contributor Author

Is @lastUpdated specific to the individual config type or does it apply to the entire version?

It's specific to individual config type

@divyanshi-0402
Copy link
Contributor Author

Thank you for adding these APIs. I have added few comments from my initial review.

One outstanding question I have is, do admin's have an option to choose a particular config to rollback when rolling back to previous version? I'm not exactly aware of a scenario, but hypothetically an admin may have removed a user and also updated a role simultaneously, this would bring us to the scenario I mentioned.

Thanks for the review!
Right now users can just rollback to a provided version for security configuration. So as of now we don't have option to choose a particular config.

Signed-off-by: Divyanshi Chouksey <77962346+divyanshi-0402@users.noreply.github.com>
SSL;
SSL,
VIEW_VERSION,
ROLLBACK_VERSION;
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

private ValidationResult<SecurityConfiguration> viewAllVersions() throws IOException {
SecurityConfigVersionDocument doc = versionsLoader.loadFullDocument();

return ValidationResult.error(OK, (builder, params) -> {
Copy link
Contributor

Choose a reason for hiding this comment

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

We cannot use ValidationResult.error, it is very misleading and wrong pattern

private ValidationResult<SecurityConfiguration> viewSpecificVersion(String versionId) throws IOException {
SecurityConfigVersionDocument doc = versionsLoader.loadFullDocument();

var maybeVer = doc.getVersions().stream().filter(v -> versionId.equals(v.getVersion_id())).findFirst();
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: what does maybeVer mean?

Comment on lines +160 to +164
var maybeVer = doc.getVersions().stream().filter(v -> versionId.equals(v.getVersion_id())).findFirst();

if (maybeVer.isEmpty()) {
return ValidationResult.error(NOT_FOUND, payload(NOT_FOUND, "Version " + versionId + " not found"));
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This might make more sense to keep in rollbackCommon() since you would need to check this anyway for any caller of this function

sdc.setPrimaryTerm(currentConfig.getPrimaryTerm());

for (Map.Entry<String, ?> configEntry : sc.getConfigData().entrySet()) {
if ("_meta".equals(configEntry.getKey())) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Including _meta creates a duplicate entry for the field

Why? which other code is adding _meta field?

@nagarajg17 nagarajg17 mentioned this pull request Jul 7, 2025
5 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants