Skip to content

Combine RestApiPrivilegesEvaluator and RestApiAdminPrivilegesEvaluator to RestApiAuthorizationEvaluator#6072

Open
cwperks wants to merge 7 commits into
opensearch-project:mainfrom
cwperks:rest-api-refactor
Open

Combine RestApiPrivilegesEvaluator and RestApiAdminPrivilegesEvaluator to RestApiAuthorizationEvaluator#6072
cwperks wants to merge 7 commits into
opensearch-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.94872% with 50 lines in your changes missing coverage. Please review.
✅ Project coverage is 74.99%. Comparing base (09de157) to head (f72336f).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
...y/dlic/rest/api/RestApiAuthorizationEvaluator.java 69.16% 23 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   74.97%   74.99%   +0.01%     
==========================================
  Files         453      451       -2     
  Lines       29112    29090      -22     
  Branches     4386     4381       -5     
==========================================
- Hits        21828    21815      -13     
+ Misses       5257     5248       -9     
  Partials     2027     2027              
Files with missing lines Coverage Δ
.../opensearch/security/OpenSearchSecurityPlugin.java 85.11% <ø> (ø)
...arch/security/action/apitokens/ApiTokenAction.java 76.31% <100.00%> (-0.31%) ⬇️
...earch/security/dlic/rest/api/AccountApiAction.java 98.75% <100.00%> (+0.06%) ⬆️
.../security/dlic/rest/api/ActionGroupsApiAction.java 98.27% <100.00%> (ø)
...nsearch/security/dlic/rest/api/AuditApiAction.java 94.02% <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%) ⬇️
... and 16 more

... and 9 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 🔍

(Review updated until commit f72336f)

Here are some key observations to aid the review process:

🧪 No relevant tests
🔒 No security concerns identified
✅ No TODO sections
🔀 No multiple PR themes
⚡ Recommended focus areas for review

Possible Issue

The constructor initializes allEndpoints as a local variable and then assigns it to this.allEndpoints, but the local variable shadows the instance field during initialization. The code creates a new HashMap and populates it, then assigns it to the instance field. However, the local variable declaration final Map<Endpoint, List<Method>> allEndpoints = new HashMap<>(); at line 145 shadows the instance field declared at line 122. This works correctly but is confusing. More critically, the allMethods list at line 147 is reused across all endpoints without creating new instances, meaning all endpoints share the same List object. If any code later modifies one endpoint's methods list, it will affect all endpoints.

final Map<Endpoint, List<Method>> allEndpoints = new HashMap<>();
for (Endpoint endpoint : Endpoint.values()) {
    final List<Method> allMethods = new LinkedList<>();
    allMethods.addAll(Arrays.asList(Method.values()));
    allEndpoints.put(endpoint, allMethods);
}
this.allEndpoints = Collections.unmodifiableMap(allEndpoints);
Logic Error

The authorization check at lines 339-340 uses a negated condition that returns an error message when the user does NOT have access. However, the logic appears inverted: it returns the error message when !(condition1 || condition2) is true, which means it returns an error when the user lacks access. But the method signature suggests it should return null for success and an error message for failure. The double negative makes this confusing and error-prone. If either authorization method returns true, the overall condition becomes true, the negation makes it false, and no error is returned (correct). But if both return false, the negation makes it true, and an error is returned (correct). The logic is actually correct but extremely difficult to verify due to the double negative.

if (!(securityApiDependencies.restApiAuthorizationEvaluator().isCurrentUserAdminFor(Endpoint.APITOKENS)
    || securityApiDependencies.restApiAuthorizationEvaluator().checkAccessPermissions(request, Endpoint.APITOKENS) == null)) {
    return "User does not have required security API access";
}

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 1, 2026

PR Code Suggestions ✨

Latest suggestions up to f72336f

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Add missing APITOKENS endpoint mapping

The ENDPOINTS_WITH_PERMISSIONS map is missing the Endpoint.APITOKENS entry that was
present in the original RestApiAdminPrivilegesEvaluator. This omission could cause
authorization failures for API token operations. Add the missing entry to maintain
feature parity.

src/main/java/org/opensearch/security/dlic/rest/api/RestApiAuthorizationEvaluator.java [88-102]

 public static final Map<Endpoint, PermissionBuilder> ENDPOINTS_WITH_PERMISSIONS = ImmutableMap.<Endpoint, PermissionBuilder>builder()
     .put(Endpoint.ACTIONGROUPS, action -> buildEndpointPermission(Endpoint.ACTIONGROUPS))
     .put(Endpoint.ALLOWLIST, action -> buildEndpointPermission(Endpoint.ALLOWLIST))
     .put(Endpoint.CONFIG, action -> buildEndpointActionPermission(Endpoint.CONFIG, action))
     .put(Endpoint.INTERNALUSERS, action -> buildEndpointPermission(Endpoint.INTERNALUSERS))
     .put(Endpoint.NODESDN, action -> buildEndpointPermission(Endpoint.NODESDN))
     .put(Endpoint.RATELIMITERS, action -> buildEndpointPermission(Endpoint.RATELIMITERS))
     .put(Endpoint.ROLES, action -> buildEndpointPermission(Endpoint.ROLES))
     .put(Endpoint.ROLESMAPPING, action -> buildEndpointPermission(Endpoint.ROLESMAPPING))
     .put(Endpoint.TENANTS, action -> buildEndpointPermission(Endpoint.TENANTS))
+    .put(Endpoint.APITOKENS, action -> buildEndpointPermission(Endpoint.APITOKENS))
     .put(Endpoint.VIEW_VERSION, action -> buildEndpointPermission(Endpoint.VIEW_VERSION))
     .put(Endpoint.ROLLBACK_VERSION, action -> buildEndpointPermission(Endpoint.ROLLBACK_VERSION))
     .put(Endpoint.SSL, action -> buildEndpointActionPermission(Endpoint.SSL, action))
     .put(Endpoint.RESOURCE_SHARING, action -> buildEndpointActionPermission(Endpoint.RESOURCE_SHARING, action))
     .build();
Suggestion importance[1-10]: 9

__

Why: The ENDPOINTS_WITH_PERMISSIONS map is missing Endpoint.APITOKENS which was present in the original RestApiAdminPrivilegesEvaluator. This is a critical omission that would break authorization for API token operations, as seen in the PR where ApiTokenAction uses this endpoint for authorization checks.

High

Previous suggestions

Suggestions up to commit f1c96f2
CategorySuggestion                                                                                                                                    Impact
Possible issue
Validate privilegesConfiguration parameter

Add null check for privilegesConfiguration parameter before using it. The
constructor accepts privilegesConfiguration but doesn't validate it, which could
lead to NullPointerException when methods like isCurrentUserAdminFor attempt to use
it.

src/main/java/org/opensearch/security/dlic/rest/api/RestApiAuthorizationEvaluator.java [126-143]

 public RestApiAuthorizationEvaluator(
     final Settings settings,
     final AdminDNs adminDNs,
     final RoleMapper roleMapper,
     final PrincipalExtractor principalExtractor,
     final Path configPath,
     final ThreadPool threadPool,
     final PrivilegesConfiguration privilegesConfiguration
 ) {
     this.adminDNs = adminDNs;
     this.roleMapper = roleMapper;
     this.principalExtractor = principalExtractor;
     this.configPath = configPath;
     this.threadPool = threadPool;
     this.threadContext = threadPool.getThreadContext();
     this.settings = settings;
-    this.privilegesConfiguration = privilegesConfiguration;
+    this.privilegesConfiguration = Objects.requireNonNull(privilegesConfiguration, "privilegesConfiguration must not be null");
     this.restapiAdminEnabled = settings.getAsBoolean(SECURITY_RESTAPI_ADMIN_ENABLED, false);
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies that privilegesConfiguration is used without null validation in the constructor. However, the test file shows it can be passed as null in some test scenarios, suggesting this might be intentional. Adding validation would improve robustness but may require adjusting test code.

Medium
Prevent NPE in authorization check

Add null check for privilegesConfiguration before calling methods on it. If
privilegesConfiguration is null, calling
privilegesConfiguration.privilegesEvaluator() will throw NullPointerException,
causing authorization checks to fail unexpectedly.

src/main/java/org/opensearch/security/dlic/rest/api/RestApiAuthorizationEvaluator.java [216-231]

 public boolean isCurrentUserAdminFor(final Endpoint endpoint, final String action) {
     final Pair<User, TransportAddress> userAndRemoteAddress = Utils.userAndRemoteAddressFrom(threadContext);
     if (userAndRemoteAddress.getLeft() == null) {
         return false;
     }
     if (adminDNs.isAdmin(userAndRemoteAddress.getLeft())) {
         return true;
     }
     if (ENDPOINTS_WITH_PERMISSIONS.containsKey(endpoint) == false) {
         logger.debug("No permission found for {} endpoint", endpoint);
         return false;
     }
+    if (privilegesConfiguration == null) {
+        logger.error("PrivilegesConfiguration is null, cannot evaluate admin permissions");
+        return false;
+    }
     final String permission = ENDPOINTS_WITH_PERMISSIONS.get(endpoint).build(action);
     final PrivilegesEvaluationContext context = privilegesConfiguration.privilegesEvaluator()
         .createContext(userAndRemoteAddress.getLeft(), permission);
Suggestion importance[1-10]: 6

__

Why: While the suggestion identifies a potential NPE risk when privilegesConfiguration is null, the issue should ideally be addressed at the constructor level (as in suggestion 1) rather than adding defensive checks in every method. The suggestion is valid but addresses the symptom rather than the root cause.

Low
Suggestions up to commit 69d1fa3
CategorySuggestion                                                                                                                                    Impact
Possible issue
Add missing continue to prevent ClassCastException

When value.getValue() is not a Collection, the code logs an error but then falls
through and attempts to cast it to Collection, which will throw a
ClassCastException. A continue statement should be added after the error log to skip
processing this entry.

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: The missing continue after the error log for non-Collection values causes a ClassCastException at runtime. This is a real bug that existed in the original RestApiPrivilegesEvaluator code and was carried over to the merged class without being fixed.

Medium
Replace null with mock to prevent NullPointerException

Passing null for privilegesConfiguration will cause a NullPointerException at
runtime when isCurrentUserAdminFor is called, 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]

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

__

Why: Passing null for privilegesConfiguration could cause a NullPointerException if isCurrentUserAdminFor is called in tests, since the merged class calls privilegesConfiguration.privilegesEvaluator(). However, the test class may only test role/cert-based access methods that don't invoke privilegesConfiguration, so the actual risk depends on which test methods exist.

Low
Suggestions up to commit 109df00
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

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 7, 2026

Persistent review updated to latest commit 69d1fa3

Copy link
Copy Markdown
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.

this is a good approach to cleaning up the code. Left a few comments.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 20, 2026

PR Code Analyzer ❗

AI-powered 'Code-Diff-Analyzer' found issues on commit f72336f.

PathLineSeverityDescription
src/main/java/org/opensearch/security/dlic/rest/api/RestApiAuthorizationEvaluator.java80mediumEndpoint.APITOKENS is present in the deleted RestApiAdminPrivilegesEvaluator.ENDPOINTS_WITH_PERMISSIONS map but is absent from the new merged RestApiAuthorizationEvaluator.ENDPOINTS_WITH_PERMISSIONS map. This causes isCurrentUserAdminFor(Endpoint.APITOKENS) to always return false, silently denying the fine-grained restapi:admin privilege path for API token administration. Regular role-based access via checkAccessPermissions still functions, but this is a behavioral regression that could mask an authorization bypass or be exploited to force fallback to a less-restrictive code path.
src/main/java/org/opensearch/security/dlic/rest/api/RestApiAuthorizationEvaluator.java349lowThe parseDisabledEndpoints method changed the log level for null/empty settings from logger.error to logger.info. A misconfigured or absent endpoint restriction setting now produces only an informational log rather than an error, reducing visibility of potential security misconfigurations in monitoring and alerting pipelines.
src/test/java/org/opensearch/security/dlic/rest/api/RestApiAuthorizationEvaluatorTest.java46lownull is passed as the privilegesConfiguration argument to RestApiAuthorizationEvaluator in the test setup. The constructor stores this null and isCurrentUserAdminFor uses it without a null guard. If test coverage expands to exercise the admin privilege path, or if this constructor usage pattern is copied to production code, it will produce a NullPointerException and could be used to deny service to admin operations.

The table above displays the top 10 most important findings.

Total: 3 | Critical: 0 | High: 0 | Medium: 1 | Low: 2


Pull Requests Author(s): Please update your Pull Request according to the report above.

Repository Maintainer(s): You can bypass diff analyzer by adding label skip-diff-analyzer after reviewing the changes carefully, then re-run failed actions. To re-enable the analyzer, remove the label, then re-run all actions.


⚠️ Note: The Code-Diff-Analyzer helps protect against potentially harmful code patterns. Please ensure you have thoroughly reviewed the changes beforehand.

Thanks.

@github-actions
Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit f1c96f2

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

Persistent review updated to latest commit f72336f

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.

2 participants