Skip to content
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

Api token authc/z implementation with Cache #4992

Open
wants to merge 30 commits into
base: feature/api-tokens
Choose a base branch
from

Conversation

derek-ho
Copy link
Collaborator

@derek-ho derek-ho commented Dec 24, 2024

Description

This PR implements authc/z for api tokens. For authc it is based on token validity for as well as presence in a cache, which listens to index and delete operations on the api token index. For Authz, it onboards onto action privileges class, where instead of returning not authorized for cluster and index privileges, requests go through a final api token check, where if the user is an api token, it will evaluate whether the permissions in the cache are sufficient to execute the request.

Issues Resolved

[List any issues this PR will resolve]

Is this a backport? If so, please add backport PR # and/or commits #, and remove backport-failed label from the original PR.

Do these changes introduce new permission(s) to be displayed in the static dropdown on the front-end? If so, please open a draft PR in the security dashboards plugin and link the draft PR here

Testing

[Please provide details of testing done: unit testing, integration testing and manual testing]

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.

@derek-ho derek-ho changed the title Naive cluster permission authz and authc based on token validity Naive authc+z api token implementation Dec 26, 2024
Copy link

codecov bot commented Dec 26, 2024

Codecov Report

Attention: Patch coverage is 61.69265% with 172 lines in your changes missing coverage. Please review.

Project coverage is 71.18%. Comparing base (0edba23) to head (77ad6c3).
Report is 32 commits behind head on feature/api-tokens.

Files with missing lines Patch % Lines
...arch/security/action/apitokens/ApiTokenAction.java 7.14% 39 Missing ⚠️
...pensearch/security/http/ApiTokenAuthenticator.java 58.97% 22 Missing and 10 partials ⚠️
...ensearch/security/privileges/ActionPrivileges.java 77.55% 11 Missing and 11 partials ⚠️
...urity/action/apitokens/ApiTokenUpdateResponse.java 0.00% 14 Missing ⚠️
...ction/apitokens/TransportApiTokenUpdateAction.java 22.22% 14 Missing ⚠️
...es/PermissionBasedPrivilegesEvaluationContext.java 36.36% 7 Missing ⚠️
...curity/action/apitokens/ApiTokenUpdateRequest.java 0.00% 6 Missing ⚠️
...earch/security/auditlog/impl/AbstractAuditLog.java 40.00% 3 Missing and 3 partials ⚠️
...ivileges/RoleBasedPrivilegesEvaluationContext.java 54.54% 5 Missing ⚠️
...y/action/apitokens/ApiTokenUpdateNodeResponse.java 0.00% 4 Missing ⚠️
... and 10 more
Additional details and impacted files

Impacted file tree graph

@@                  Coverage Diff                   @@
##           feature/api-tokens    #4992      +/-   ##
======================================================
- Coverage               71.46%   71.18%   -0.28%     
======================================================
  Files                     334      354      +20     
  Lines                   22552    23272     +720     
  Branches                 3590     3680      +90     
======================================================
+ Hits                    16117    16567     +450     
- Misses                   4642     4864     +222     
- Partials                 1793     1841      +48     
Files with missing lines Coverage Δ
.../opensearch/security/OpenSearchSecurityPlugin.java 83.87% <100.00%> (+0.31%) ⬆️
...opensearch/security/action/apitokens/ApiToken.java 91.20% <100.00%> (ø)
...ecurity/action/apitokens/ApiTokenUpdateAction.java 100.00% <100.00%> (ø)
...g/opensearch/security/authtoken/jwt/JwtVendor.java 82.75% <100.00%> (-6.34%) ⬇️
...rity/authtoken/jwt/claims/ApiJwtClaimsBuilder.java 100.00% <100.00%> (ø)
...curity/privileges/PrivilegesEvaluationContext.java 100.00% <100.00%> (+2.70%) ⬆️
...ch/security/securityconf/DynamicConfigFactory.java 62.74% <100.00%> (+0.24%) ⬆️
...g/opensearch/security/ssl/util/ExceptionUtils.java 45.83% <100.00%> (+2.35%) ⬆️
...a/org/opensearch/security/util/AuthTokenUtils.java 66.66% <100.00%> (+4.16%) ⬆️
.../security/action/apitokens/ApiTokenRepository.java 96.96% <94.44%> (ø)
... and 19 more

... and 12 files with indirect coverage changes

originalSource = (new String(BaseEncoding.base64().decode((String) base64), StandardCharsets.UTF_8));
} else {
originalSource = XContentHelper.convertToJson(originalResult.internalSourceRef(), false, XContentType.JSON);
if (originalSource == null) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Changes in this file are to correct a mis-merge that happened in prior PRs to this feature branch.

@derek-ho derek-ho marked this pull request as ready for review December 31, 2024 21:57
@derek-ho derek-ho changed the title Naive authc+z api token implementation Api token authc/z implementation with Cache Dec 31, 2024
Signed-off-by: Derek Ho <[email protected]>
Signed-off-by: Derek Ho <[email protected]>
Signed-off-by: Derek Ho <[email protected]>
Signed-off-by: Derek Ho <[email protected]>
Signed-off-by: Derek Ho <[email protected]>
Signed-off-by: Derek Ho <[email protected]>
Signed-off-by: Derek Ho <[email protected]>
Signed-off-by: Derek Ho <[email protected]>
Signed-off-by: Derek Ho <[email protected]>
Signed-off-by: Derek Ho <[email protected]>
}

public PrivilegesEvaluatorResponse hasAnyClusterPrivilege(PrivilegesEvaluationContext context, Set<String> actions) {
return cluster.providesAnyPrivilege(context, actions, context.getMappedRoles());
if (context instanceof RoleBasedPrivilegesEvaluationContext) {
Copy link
Member

Choose a reason for hiding this comment

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

This looks like a good contender for polymorphism.

Copy link
Collaborator

Choose a reason for hiding this comment

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

In order to avoid the if/else constructs which are very prominent in this PR, I would recommend to extract ActionPrivileges as an interface or abstract super class, and have one implementation which deals with RoleBasedPrivilegesEvaluationContext and another implementation which deals with PermissionBasedPrivilegesEvaluationContext.

return ctxWithUserName("test-user", roles);
}

static RoleBasedPrivilegesEvaluationContext ctxWithUserName(String userName, String... roles) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can the name ctxWithUserName() be somehow refined? Considering that ctxForApiToken() also accepts a userName param, it is unclear by the name what the difference to ctxWithUserName() is.

@@ -373,14 +373,14 @@ public void handleSearchContext(SearchContext searchContext, ThreadPool threadPo
return;
}

PrivilegesEvaluationContext privilegesEvaluationContext = this.dlsFlsBaseContext.getPrivilegesEvaluationContext();
if (privilegesEvaluationContext == null) {
PrivilegesEvaluationContext PrivilegesEvaluationContext = this.dlsFlsBaseContext.getPrivilegesEvaluationContext();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit: PrivilegesEvaluationContext should be lower case

@@ -110,13 +110,13 @@ public SecurityFlsDlsIndexSearcherWrapper(
protected DirectoryReader dlsFlsWrap(final DirectoryReader reader, boolean isAdmin) throws IOException {

final ShardId shardId = ShardUtils.extractShardId(reader);
PrivilegesEvaluationContext privilegesEvaluationContext = this.dlsFlsBaseContext.getPrivilegesEvaluationContext();
PrivilegesEvaluationContext PrivilegesEvaluationContext = this.dlsFlsBaseContext.getPrivilegesEvaluationContext();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit: PrivilegesEvaluationContext should be lower case

}

public PrivilegesEvaluatorResponse hasAnyClusterPrivilege(PrivilegesEvaluationContext context, Set<String> actions) {
return cluster.providesAnyPrivilege(context, actions, context.getMappedRoles());
if (context instanceof RoleBasedPrivilegesEvaluationContext) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

In order to avoid the if/else constructs which are very prominent in this PR, I would recommend to extract ActionPrivileges as an interface or abstract super class, and have one implementation which deals with RoleBasedPrivilegesEvaluationContext and another implementation which deals with PermissionBasedPrivilegesEvaluationContext.

}

if (!indexHasAllPermissions) {
return PrivilegesEvaluatorResponse.insufficient("Insufficient permissions for the index" + concreteIndex);
Copy link
Collaborator

Choose a reason for hiding this comment

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

This error message does not indicate which permission is missing. It also does not mention any other potential indices which also might have missing permissions. Thus, it is not really user friendly.

I would recommend to use the CheckTable class as it is also used by the other index privilege evaluation methods.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Additionally, the API contract for hasIndexPrivilege() which calls this method defines that situations where there are privileges available for sub-sets of indices are reported with a isPartiallyOk() response, combined with the set of available indices:

/**
* Checks whether this instance provides privileges for the combination of the provided action,
* the provided indices and the provided roles.
* <p>
* Returns a PrivilegesEvaluatorResponse with allowed=true if privileges are available.
* <p>
* If privileges are only available for a sub-set of indices, isPartiallyOk() will return true
* and the indices for which privileges are available are returned by getAvailableIndices(). This allows the
* do_not_fail_on_forbidden behaviour.
*/
public PrivilegesEvaluatorResponse hasIndexPrivilege(
PrivilegesEvaluationContext context,
Set<String> actions,
IndexResolverReplacer.Resolved resolvedIndices
) {

This is necessary for do_not_fail_on_forbidden functionality. Without that behaviour, that functionality won't work correctly for API tokens.

@@ -127,6 +128,7 @@ public final static <T> SecurityDynamicConfiguration<T> addStatics(SecurityDynam
private final Path configPath;
private final InternalAuthenticationBackend iab;
private final ClusterInfoHolder cih;
private final ApiTokenRepository ar;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit: Despite the abbreviations that are used in the existing code, I would recommend to use non-abbreviated attribute names, as it strongly improves code readability.

new NoOpAuthenticationBackend(Settings.EMPTY, null),
new ApiTokenAuthenticator(getDynamicApiTokenSettings(), this.cih.getClusterName(), ar),
false,
-2
Copy link
Collaborator

Choose a reason for hiding this comment

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

If a user has configured an auth domain with the priority -2, this will be removed in favor of the ApiTokenAuthenticator. This might be unexpected behavior.

private final Map<String, Permissions> jtis = new ConcurrentHashMap<>();

void reloadApiTokensFromIndex() {
Map<String, ApiToken> tokensFromIndex = apiTokenIndexHandler.getTokenMetadatas();
Copy link
Collaborator

Choose a reason for hiding this comment

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

ApiTokenIndexHandler seems to do a actionGet, i.e., a sync operation. Whenever we're on transport operations, I'd strongly recommend to prefer async operations using action listeners, as otherwise there's a slight deadlock risk due to thread pool exhaustion.

// First check if this permission applies to this index
boolean indexMatched = false;
for (String pattern : indexPermission.getIndexPatterns()) {
if (WildcardMatcher.from(pattern).test(concreteIndex)) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

OpenSearch has the property that privileges on aliases also grant privileges on indices contained inside the alias. Similar properties hold for data streams and backing indices. I think this support is missing here.


@Override
public ImmutableSet<String> getMappedRoles() {
return ImmutableSet.of();
Copy link
Collaborator

Choose a reason for hiding this comment

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

There are quite a few dependencies on mapped roles.

  • There's ProtectedIndexEvaluator which uses the setting plugins.security.protected_indices.roles to define roles which can access protected indices
  • PrivilegeEvaluator uses mapped roles to realize the Dashboards multi tenancy implementation
  • User attributes exposes the mapped roles via ${user.securityRoles}, thus they can be used in DLS statements

Is it anticipated that this functionality won't be available with API tokens?

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.

3 participants