Skip to content

Combine RestApiPrivilegesEvaluator and RestApiAdminPrivilegesEvaluator to RestApiAuthorizationEvaluator#6072

Open
cwperks wants to merge 3 commits intoopensearch-project:mainfrom
cwperks:rest-api-refactor
Open

Combine RestApiPrivilegesEvaluator and RestApiAdminPrivilegesEvaluator to RestApiAuthorizationEvaluator#6072
cwperks wants to merge 3 commits intoopensearch-project:mainfrom
cwperks:rest-api-refactor

Conversation

@cwperks
Copy link
Copy Markdown
Member

@cwperks cwperks commented Apr 6, 2026

Description

This PR contains a refactoring to simplify authz for security APIs.

Currently, authorization is split into 2 files:

  1. RestApiPrivilegesEvaluator - For when plugins.security.restapi.admin.enabled is set to true which authorizes security APIs based on whether the user has explicitly been granted the requisite restapi:* permission
  2. RestApiAdminPrivilegesEvaluator - For when plugins.security.restapi.roles_enabled is set which authorizes security APIs based on the user's roles
  • Category

Refactoring

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.

cwperks added 2 commits April 5, 2026 23:27
…r to RestApiAuthorizationEvaluator

Signed-off-by: Craig Perkins <craig5008@gmail.com>
Signed-off-by: Craig Perkins <craig5008@gmail.com>
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 6, 2026

Codecov Report

❌ Patch coverage is 67.97386% with 49 lines in your changes missing coverage. Please review.
✅ Project coverage is 74.01%. Comparing base (db9efb6) to head (05ae95d).
⚠️ Report is 5 commits behind head on main.

Files with missing lines Patch % Lines
...y/dlic/rest/api/RestApiAuthorizationEvaluator.java 69.74% 22 Missing and 14 partials ⚠️
.../security/dlic/rest/api/PermissionsInfoAction.java 33.33% 2 Missing ⚠️
...i/migrate/MigrateResourceSharingInfoApiAction.java 0.00% 2 Missing ⚠️
...arch/security/dlic/rest/api/AbstractApiAction.java 75.00% 1 Missing ⚠️
...rch/security/dlic/rest/api/AllowlistApiAction.java 0.00% 1 Missing ⚠️
...security/dlic/rest/api/ConfigUpgradeApiAction.java 0.00% 1 Missing ⚠️
...ity/dlic/rest/api/MultiTenancyConfigApiAction.java 0.00% 1 Missing ⚠️
...earch/security/dlic/rest/api/NodesDnApiAction.java 0.00% 1 Missing ⚠️
.../security/dlic/rest/api/RateLimitersApiAction.java 0.00% 1 Missing ⚠️
...curity/dlic/rest/api/RollbackVersionApiAction.java 0.00% 1 Missing ⚠️
... and 2 more
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #6072      +/-   ##
==========================================
+ Coverage   73.98%   74.01%   +0.03%     
==========================================
  Files         441      440       -1     
  Lines       27413    27447      +34     
  Branches     4110     4115       +5     
==========================================
+ Hits        20281    20316      +35     
+ Misses       5193     5192       -1     
  Partials     1939     1939              
Files with missing lines Coverage Δ
.../opensearch/security/OpenSearchSecurityPlugin.java 84.91% <ø> (+0.03%) ⬆️
...earch/security/dlic/rest/api/AccountApiAction.java 98.68% <100.00%> (ø)
.../security/dlic/rest/api/ActionGroupsApiAction.java 98.27% <100.00%> (ø)
...nsearch/security/dlic/rest/api/AuditApiAction.java 91.17% <100.00%> (ø)
.../security/dlic/rest/api/CertificatesApiAction.java 80.00% <100.00%> (ø)
...security/dlic/rest/api/InternalUsersApiAction.java 93.54% <100.00%> (ø)
...nsearch/security/dlic/rest/api/RolesApiAction.java 96.07% <100.00%> (ø)
.../security/dlic/rest/api/RolesMappingApiAction.java 97.29% <100.00%> (ø)
...ecurity/dlic/rest/api/SecurityApiDependencies.java 93.33% <100.00%> (-0.79%) ⬇️
...security/dlic/rest/api/SecurityRestApiActions.java 88.88% <100.00%> (-1.12%) ⬇️
... and 15 more

... and 5 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Signed-off-by: Craig Perkins <cwperx@amazon.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 1, 2026

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

🧪 PR contains tests
🔒 No security concerns identified
✅ No TODO sections
🔀 Multiple PR themes

Sub-PR theme: Merge RestApiPrivilegesEvaluator and RestApiAdminPrivilegesEvaluator into RestApiAuthorizationEvaluator

Relevant files:

  • src/main/java/org/opensearch/security/dlic/rest/api/RestApiAuthorizationEvaluator.java
  • src/main/java/org/opensearch/security/dlic/rest/api/SecurityApiDependencies.java
  • src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java
  • src/test/java/org/opensearch/security/dlic/rest/api/RestApiAuthorizationEvaluatorTest.java

Sub-PR theme: Update all API action classes to use RestApiAuthorizationEvaluator

Relevant files:

  • src/main/java/org/opensearch/security/dlic/rest/api/AbstractApiAction.java
  • src/main/java/org/opensearch/security/dlic/rest/api/ActionGroupsApiAction.java
  • src/main/java/org/opensearch/security/dlic/rest/api/AllowlistApiAction.java
  • src/main/java/org/opensearch/security/dlic/rest/api/AuditApiAction.java
  • src/main/java/org/opensearch/security/dlic/rest/api/CertificatesApiAction.java
  • src/main/java/org/opensearch/security/dlic/rest/api/ConfigUpgradeApiAction.java
  • src/main/java/org/opensearch/security/dlic/rest/api/InternalUsersApiAction.java
  • src/main/java/org/opensearch/security/dlic/rest/api/MultiTenancyConfigApiAction.java
  • src/main/java/org/opensearch/security/dlic/rest/api/NodesDnApiAction.java
  • src/main/java/org/opensearch/security/dlic/rest/api/RateLimitersApiAction.java
  • src/main/java/org/opensearch/security/dlic/rest/api/RolesApiAction.java
  • src/main/java/org/opensearch/security/dlic/rest/api/RolesMappingApiAction.java
  • src/main/java/org/opensearch/security/dlic/rest/api/RollbackVersionApiAction.java
  • src/main/java/org/opensearch/security/dlic/rest/api/SecurityConfigApiAction.java
  • src/main/java/org/opensearch/security/dlic/rest/api/SecuritySSLCertsApiAction.java
  • src/main/java/org/opensearch/security/dlic/rest/api/TenantsApiAction.java
  • src/main/java/org/opensearch/security/dlic/rest/api/ViewVersionApiAction.java
  • src/main/java/org/opensearch/security/dlic/rest/validation/EndpointValidator.java
  • src/main/java/org/opensearch/security/dlic/rest/api/PermissionsInfoAction.java
  • src/main/java/org/opensearch/security/resources/api/migrate/MigrateResourceSharingInfoApiAction.java

⚡ Recommended focus areas for review

Null PrivilegesConfiguration

The privilegesConfiguration parameter can be null (as seen in the test setup where null is passed). In isCurrentUserAdminFor, privilegesConfiguration.privilegesEvaluator() is called without a null check, which will throw a NullPointerException when restapiAdminEnabled is true and a non-admin user accesses an endpoint.

final PrivilegesEvaluationContext context = privilegesConfiguration.privilegesEvaluator()
    .createContext(userAndRemoteAddress.getLeft(), permission);
final boolean hasAccess = context.getActionPrivileges().hasExplicitClusterPrivilege(context, permission).isAllowed();
Missing continue on error

In parseDisabledEndpoints, when value.getValue() is not a Collection, only an error is logged but execution continues into the for loop that casts value.getValue() to Collection, which will throw a ClassCastException. A continue statement should be added after the error log.

if (value.getValue() instanceof Collection == false) {
    logger.error(
        "Disabled HTTP methods of endpoint '{}' must be an array, actually is '{}', skipping.",
        endpointString,
        (value.getValue().toString())
    );
}

final List<Method> disabledMethods = new LinkedList<>();
for (Object disabledMethodObj : (Collection) value.getValue()) {
Thread-safety concern

The disabledEndpointsForUsers map is used as a cache and written to without synchronization. In a multi-threaded environment, concurrent requests for the same user could result in race conditions. The original code had the same issue, but this refactoring is a good opportunity to address it.

if (disabledEndpointsForUsers.containsKey(userPrincipal)) {
    return disabledEndpointsForUsers.get(userPrincipal);
}

if (!currentUserHasRestApiAccess(userRoles)) {
    return this.allEndpoints;
}

final Map<Endpoint, List<Method>> finalEndpoints = new HashMap<>();
final List<Endpoint> remainingEndpoints = new LinkedList<>(Arrays.asList(Endpoint.values()));

boolean hasDisabledEndpoints = false;
for (String userRole : userRoles) {
    final Map<Endpoint, List<Method>> endpointsForRole = disabledEndpointsForRoles.get(userRole);
    if (endpointsForRole == null || endpointsForRole.isEmpty()) {
        continue;
    }
    remainingEndpoints.retainAll(endpointsForRole.keySet());
    hasDisabledEndpoints = true;
}

if (isDebugEnabled) {
    logger.debug("Remaining endpoints for user {} after retaining all : {}", userPrincipal, remainingEndpoints);
}

if (hasDisabledEndpoints == false) {
    if (isDebugEnabled) {
        logger.debug(
            "No disabled endpoints for user {} at all,  only globally disabledendpoints apply.",
            userPrincipal,
            remainingEndpoints
        );
    }
    disabledEndpointsForUsers.put(userPrincipal, addGloballyDisabledEndpoints(finalEndpoints));
    return finalEndpoints;
}

for (Endpoint endpoint : remainingEndpoints) {
    final List<Method> remainingMethodsForEndpoint = new LinkedList<>(Arrays.asList(Method.values()));
    for (String userRole : userRoles) {
        final Map<Endpoint, List<Method>> endpoints = disabledEndpointsForRoles.get(userRole);
        if (endpoints != null && endpoints.isEmpty() == false) {
            remainingMethodsForEndpoint.retainAll(endpoints.get(endpoint));
        }
    }

    finalEndpoints.put(endpoint, remainingMethodsForEndpoint);
}

if (isDebugEnabled) {
    logger.debug("Disabled endpoints for user {} after retaining all : {}", userPrincipal, finalEndpoints);
}

addGloballyDisabledEndpoints(finalEndpoints);
disabledEndpointsForUsers.put(userPrincipal, finalEndpoints);

if (isDebugEnabled) {
    logger.debug(
        "Disabled endpoints for user {} after retaining all : {}",
        userPrincipal,
        disabledEndpointsForUsers.get(userPrincipal)
    );
}

return disabledEndpointsForUsers.get(userPrincipal);

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 1, 2026

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Add missing continue to prevent ClassCastException

When value.getValue() is not a Collection, the code logs an error but does not
continue to skip the entry. The subsequent unchecked cast (Collection)
value.getValue() will throw a ClassCastException at runtime. A continue statement
should be added after the error log.

src/main/java/org/opensearch/security/dlic/rest/api/RestApiAuthorizationEvaluator.java [368-377]

 if (value.getValue() instanceof Collection == false) {
     logger.error(
         "Disabled HTTP methods of endpoint '{}' must be an array, actually is '{}', skipping.",
         endpointString,
         (value.getValue().toString())
     );
+    continue;
 }
 
 final List<Method> disabledMethods = new LinkedList<>();
 for (Object disabledMethodObj : (Collection) value.getValue()) {
Suggestion importance[1-10]: 8

__

Why: This is a real bug carried over from the original RestApiPrivilegesEvaluator code - when value.getValue() is not a Collection, the error is logged but execution falls through to an unchecked (Collection) cast that will throw a ClassCastException. Adding continue is necessary to match the intended "skipping" behavior described in the error message.

Medium
Replace null with a mock for required dependency

Passing null for privilegesConfiguration in the test constructor will cause a
NullPointerException if any test exercises isCurrentUserAdminFor, since the merged
class now calls privilegesConfiguration.privilegesEvaluator(). A mock of
PrivilegesConfiguration should be provided instead.

src/test/java/org/opensearch/security/dlic/rest/api/RestApiAuthorizationEvaluatorTest.java [36-44]

+final PrivilegesConfiguration privilegesConfiguration = mock(PrivilegesConfiguration.class);
 this.privilegesEvaluator = new RestApiAuthorizationEvaluator(
     Settings.EMPTY,
     mock(AdminDNs.class),
     (user, caller) -> user.getSecurityRoles(),
     mock(PrincipalExtractor.class),
     mock(Path.class),
     mock(ThreadPool.class),
-    null
+    privilegesConfiguration
 );
Suggestion importance[1-10]: 6

__

Why: Passing null for privilegesConfiguration could cause a NullPointerException if tests exercise isCurrentUserAdminFor. However, the existing tests in this file may only test role-based access methods that don't call privilegesConfiguration, so the actual impact depends on test coverage. The suggestion is valid but may only matter for specific test scenarios.

Low

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.

1 participant