From 9f695fa65336bd07638638567cb872367854d978 Mon Sep 17 00:00:00 2001 From: Derek Ho Date: Tue, 24 Dec 2024 12:20:02 -0500 Subject: [PATCH 01/20] Naive cluster permission authz and authc based on token validity Signed-off-by: Derek Ho --- .../security/filter/SecurityRestFilter.java | 9 + .../security/http/ApiTokenAuthenticator.java | 286 ++++++++++++++++++ .../security/privileges/ActionPrivileges.java | 5 + .../PrivilegesEvaluationContext.java | 10 + .../privileges/PrivilegesEvaluator.java | 10 + .../securityconf/DynamicConfigModelV7.java | 18 ++ 6 files changed, 338 insertions(+) create mode 100644 src/main/java/org/opensearch/security/http/ApiTokenAuthenticator.java diff --git a/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java b/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java index c9d10ee2fa..e1efe65409 100644 --- a/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java +++ b/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java @@ -76,6 +76,8 @@ public class SecurityRestFilter { protected final Logger log = LogManager.getLogger(this.getClass()); + public static final String API_TOKEN_CLUSTERPERM_KEY = "security.api_token.clusterperm"; + public static final String API_TOKEN_INDEXPERM_KEY = "security.api_token.indexperm"; private final BackendRegistry registry; private final RestLayerPrivilegesEvaluator evaluator; private final AuditLog auditLog; @@ -232,6 +234,13 @@ void authorizeRequest(RestHandler original, SecurityRequestChannel request, User .addAll(route.actionNames() != null ? route.actionNames() : Collections.emptySet()) .add(route.name()) .build(); + + log.info("API token context value: " + threadContext.getTransient(API_TOKEN_CLUSTERPERM_KEY).toString()); + + if (threadContext.getTransient(API_TOKEN_CLUSTERPERM_KEY) != null) { + return; + } + pres = evaluator.evaluate(user, route.name(), actionNames); if (log.isDebugEnabled()) { diff --git a/src/main/java/org/opensearch/security/http/ApiTokenAuthenticator.java b/src/main/java/org/opensearch/security/http/ApiTokenAuthenticator.java new file mode 100644 index 0000000000..bc6b25116d --- /dev/null +++ b/src/main/java/org/opensearch/security/http/ApiTokenAuthenticator.java @@ -0,0 +1,286 @@ +/* + * 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.http; + +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.hc.core5.http.HttpHeaders; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.OpenSearchException; +import org.opensearch.OpenSearchSecurityException; +import org.opensearch.SpecialPermission; +import org.opensearch.common.settings.Settings; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.security.DefaultObjectMapper; +import org.opensearch.security.auth.HTTPAuthenticator; +import org.opensearch.security.authtoken.jwt.EncryptionDecryptionUtil; +import org.opensearch.security.filter.SecurityRequest; +import org.opensearch.security.filter.SecurityResponse; +import org.opensearch.security.ssl.util.ExceptionUtils; +import org.opensearch.security.user.AuthCredentials; +import org.opensearch.security.util.KeyUtils; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtParser; +import io.jsonwebtoken.JwtParserBuilder; +import io.jsonwebtoken.security.WeakKeyException; + +import static org.opensearch.security.OpenSearchSecurityPlugin.LEGACY_OPENDISTRO_PREFIX; +import static org.opensearch.security.OpenSearchSecurityPlugin.PLUGINS_PREFIX; +import static org.opensearch.security.filter.SecurityRestFilter.API_TOKEN_CLUSTERPERM_KEY; +import static org.opensearch.security.util.AuthTokenUtils.isAccessToRestrictedEndpoints; + +public class ApiTokenAuthenticator implements HTTPAuthenticator { + + private static final int MINIMUM_SIGNING_KEY_BIT_LENGTH = 512; + private static final String REGEX_PATH_PREFIX = "/(" + LEGACY_OPENDISTRO_PREFIX + "|" + PLUGINS_PREFIX + ")/" + "(.*)"; + private static final Pattern PATTERN_PATH_PREFIX = Pattern.compile(REGEX_PATH_PREFIX); + + protected final Logger log = LogManager.getLogger(this.getClass()); + + private static final Pattern BEARER = Pattern.compile("^\\s*Bearer\\s.*", Pattern.CASE_INSENSITIVE); + private static final String BEARER_PREFIX = "bearer "; + + private final JwtParser jwtParser; + private final String encryptionKey; + private final Boolean apiTokenEnabled; + private final String clusterName; + + private final EncryptionDecryptionUtil encryptionUtil; + + @SuppressWarnings("removal") + public ApiTokenAuthenticator(Settings settings, String clusterName) { + String apiTokenEnabledSetting = settings.get("enabled", "true"); + apiTokenEnabled = Boolean.parseBoolean(apiTokenEnabledSetting); + encryptionKey = settings.get("encryption_key"); + + final SecurityManager sm = System.getSecurityManager(); + if (sm != null) { + sm.checkPermission(new SpecialPermission()); + } + jwtParser = AccessController.doPrivileged(new PrivilegedAction() { + @Override + public JwtParser run() { + JwtParserBuilder builder = initParserBuilder(settings.get("signing_key")); + return builder.build(); + } + }); + this.clusterName = clusterName; + this.encryptionUtil = new EncryptionDecryptionUtil(encryptionKey); + } + + private JwtParserBuilder initParserBuilder(final String signingKey) { + if (signingKey == null) { + throw new OpenSearchSecurityException("Unable to find api token authenticator signing_key"); + } + + final int signingKeyLengthBits = signingKey.length() * 8; + if (signingKeyLengthBits < MINIMUM_SIGNING_KEY_BIT_LENGTH) { + throw new OpenSearchSecurityException( + "Signing key size was " + + signingKeyLengthBits + + " bits, which is not secure enough. Please use a signing_key with a size >= " + + MINIMUM_SIGNING_KEY_BIT_LENGTH + + " bits." + ); + } + JwtParserBuilder jwtParserBuilder = KeyUtils.createJwtParserBuilderFromSigningKey(signingKey, log); + + return jwtParserBuilder; + } + + private String extractSecurityRolesFromClaims(Claims claims) { + Object cp = claims.get("cp"); + Object ip = claims.get("ip"); + String rolesClaim = ""; + + if (cp != null) { + rolesClaim = encryptionUtil.decrypt(cp.toString()); + } else { + log.warn("This is a malformed Api Token"); + } + + return rolesClaim; + } + + private String[] extractBackendRolesFromClaims(Claims claims) { + Object backendRolesObject = claims.get("br"); + String[] backendRoles; + + if (backendRolesObject == null) { + backendRoles = new String[0]; + } else { + // Extracting roles based on the compatibility mode + backendRoles = Arrays.stream(backendRolesObject.toString().split(",")).map(String::trim).toArray(String[]::new); + } + + return backendRoles; + } + + @Override + @SuppressWarnings("removal") + public AuthCredentials extractCredentials(final SecurityRequest request, final ThreadContext context) + throws OpenSearchSecurityException { + final SecurityManager sm = System.getSecurityManager(); + + if (sm != null) { + sm.checkPermission(new SpecialPermission()); + } + + AuthCredentials creds = AccessController.doPrivileged(new PrivilegedAction() { + @Override + public AuthCredentials run() { + return extractCredentials0(request, context); + } + }); + + return creds; + } + + private AuthCredentials extractCredentials0(final SecurityRequest request, final ThreadContext context) { + if (!apiTokenEnabled) { + log.error("Api token authentication is disabled"); + return null; + } + + String jwtToken = extractJwtFromHeader(request); + if (jwtToken == null) { + return null; + } + + if (!isRequestAllowed(request)) { + return null; + } + + try { + final Claims claims = jwtParser.parseClaimsJws(jwtToken).getBody(); + + final String subject = claims.getSubject(); + if (subject == null) { + log.error("Valid jwt on behalf of token with no subject"); + return null; + } + + final Set audience = claims.getAudience(); + if (audience == null || audience.isEmpty()) { + log.error("Valid jwt on behalf of token with no audience"); + return null; + } + + final String issuer = claims.getIssuer(); + if (!clusterName.equals(issuer)) { + log.error("The issuer of this OBO does not match the current cluster identifier"); + return null; + } + + String clusterPermissions = extractSecurityRolesFromClaims(claims); + String[] backendRoles = extractBackendRolesFromClaims(claims); + + final AuthCredentials ac = new AuthCredentials(subject, List.of(), backendRoles).markComplete(); + + for (Entry claim : claims.entrySet()) { + String key = "attr.jwt." + claim.getKey(); + Object value = claim.getValue(); + + if (value instanceof Collection) { + try { + // Convert the list to a JSON array string + String jsonValue = DefaultObjectMapper.writeValueAsString(value, false); + ac.addAttribute(key, jsonValue); + } catch (Exception e) { + log.warn("Failed to convert list claim to JSON for key: " + key, e); + // Fallback to string representation + ac.addAttribute(key, String.valueOf(value)); + } + } else { + ac.addAttribute(key, String.valueOf(value)); + } + } + + context.putTransient(API_TOKEN_CLUSTERPERM_KEY, clusterPermissions); + + return ac; + + } catch (WeakKeyException e) { + log.error("Cannot authenticate user with JWT because of ", e); + return null; + } catch (Exception e) { + if (log.isDebugEnabled()) { + log.debug("Invalid or expired JWT token.", e); + } + } + + // Return null for the authentication failure + return null; + } + + private String extractJwtFromHeader(SecurityRequest request) { + String jwtToken = request.header(HttpHeaders.AUTHORIZATION); + + if (jwtToken == null || jwtToken.isEmpty()) { + logDebug("No JWT token found in '{}' header", HttpHeaders.AUTHORIZATION); + return null; + } + + if (!BEARER.matcher(jwtToken).matches() || !jwtToken.toLowerCase().contains(BEARER_PREFIX)) { + logDebug("No Bearer scheme found in header"); + return null; + } + + jwtToken = jwtToken.substring(jwtToken.toLowerCase().indexOf(BEARER_PREFIX) + BEARER_PREFIX.length()); + + return jwtToken; + } + + private void logDebug(String message, Object... args) { + if (log.isDebugEnabled()) { + log.debug(message, args); + } + } + + public Boolean isRequestAllowed(final SecurityRequest request) { + Matcher matcher = PATTERN_PATH_PREFIX.matcher(request.path()); + final String suffix = matcher.matches() ? matcher.group(2) : null; + if (isAccessToRestrictedEndpoints(request, suffix)) { + final OpenSearchException exception = ExceptionUtils.invalidUsageOfOBOTokenException(); + log.error(exception.toString()); + return false; + } + return true; + } + + @Override + public Optional reRequestAuthentication(final SecurityRequest response, AuthCredentials creds) { + return Optional.empty(); + } + + @Override + public String getType() { + return "onbehalfof_jwt"; + } + + @Override + public boolean supportsImpersonation() { + return false; + } +} diff --git a/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java b/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java index 87ac32d090..7f019c86db 100644 --- a/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java +++ b/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java @@ -407,6 +407,11 @@ PrivilegesEvaluatorResponse providesPrivilege(PrivilegesEvaluationContext contex } } + // 4: Evaluate api tokens + if (context.getClusterPermissions().contains(action)) { + return PrivilegesEvaluatorResponse.ok(); + } + return PrivilegesEvaluatorResponse.insufficient(action); } diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java index f7e5d6de7d..686f38a686 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java @@ -11,6 +11,7 @@ package org.opensearch.security.privileges; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.function.Supplier; @@ -45,6 +46,7 @@ public class PrivilegesEvaluationContext { private final IndexResolverReplacer indexResolverReplacer; private final IndexNameExpressionResolver indexNameExpressionResolver; private final Supplier clusterStateSupplier; + private List clusterPermissions; /** * This caches the ready to use WildcardMatcher instances for the current request. Many index patterns have @@ -172,4 +174,12 @@ public String toString() { + mappedRoles + '}'; } + + public void setClusterPermissions(List clusterPermissions) { + this.clusterPermissions = clusterPermissions; + } + + public List getClusterPermissions() { + return clusterPermissions; + } } diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java index 36666972ec..faea38b81c 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java @@ -109,6 +109,7 @@ import org.greenrobot.eventbus.Subscribe; import static org.opensearch.security.OpenSearchSecurityPlugin.traceAction; +import static org.opensearch.security.filter.SecurityRestFilter.API_TOKEN_CLUSTERPERM_KEY; import static org.opensearch.security.support.ConfigConstants.OPENDISTRO_SECURITY_USER_INFO_THREAD_CONTEXT; public class PrivilegesEvaluator { @@ -342,6 +343,15 @@ public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) context.setMappedRoles(mappedRoles); } + // Extract cluster and index permissions from the api token thread context + // TODO: Add decryption here to make sure it is not injectable by anyone? + // TODO: This is only a naive implementation that does not support * + final String apiTokenClusterPermissions = threadContext.getTransient(API_TOKEN_CLUSTERPERM_KEY); + if (apiTokenClusterPermissions != null) { + List clusterPermissions = Arrays.asList(apiTokenClusterPermissions.split(",")); + context.setClusterPermissions(clusterPermissions); + } + // Add the security roles for this user so that they can be used for DLS parameter substitution. user.addSecurityRoles(mappedRoles); setUserInfoInThreadContext(user); diff --git a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java index 9c90e2341f..b57b422653 100644 --- a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java +++ b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java @@ -59,6 +59,7 @@ import org.opensearch.security.auth.internal.InternalAuthenticationBackend; import org.opensearch.security.auth.internal.NoOpAuthenticationBackend; import org.opensearch.security.configuration.ClusterInfoHolder; +import org.opensearch.security.http.ApiTokenAuthenticator; import org.opensearch.security.http.OnBehalfOfAuthenticator; import org.opensearch.security.securityconf.impl.DashboardSignInOption; import org.opensearch.security.securityconf.impl.v7.ConfigV7; @@ -377,6 +378,23 @@ private void buildAAA() { } } + /* + * If the Api token authentication is configured: + * Add the ApiToken authbackend in to the auth domains + * Challenge: false - no need to iterate through the auth domains again when ApiToken authentication failed + * order: -2 - prioritize the Api token authentication when it gets enabled + */ + Settings apiTokenSettings = getDynamicApiTokenSettings(); + if (!isKeyNull(apiTokenSettings, "signing_key") && !isKeyNull(apiTokenSettings, "encryption_key")) { + final AuthDomain _ad = new AuthDomain( + new NoOpAuthenticationBackend(Settings.EMPTY, null), + new ApiTokenAuthenticator(getDynamicApiTokenSettings(), this.cih.getClusterName()), + false, + -2 + ); + restAuthDomains0.add(_ad); + } + /* * If the OnBehalfOf (OBO) authentication is configured: * Add the OBO authbackend in to the auth domains From d3fcc4aadcd83b8c087337189dfbad49ce675f8c Mon Sep 17 00:00:00 2001 From: Derek Ho Date: Tue, 24 Dec 2024 16:16:46 -0500 Subject: [PATCH 02/20] Crude index permissions authz Signed-off-by: Derek Ho --- .../security/authtoken/jwt/JwtVendor.java | 15 ++- .../security/filter/SecurityRestFilter.java | 9 +- .../security/http/ApiTokenAuthenticator.java | 107 +++++++++++++++--- .../security/privileges/ActionPrivileges.java | 9 ++ .../PrivilegesEvaluationContext.java | 18 +++ .../privileges/PrivilegesEvaluator.java | 17 +++ .../securityconf/DynamicConfigModelV7.java | 1 + 7 files changed, 146 insertions(+), 30 deletions(-) diff --git a/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java b/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java index 75ce45912a..575bba7964 100644 --- a/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java +++ b/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java @@ -15,7 +15,6 @@ import java.security.AccessController; import java.security.PrivilegedAction; import java.text.ParseException; -import java.util.ArrayList; import java.util.Base64; import java.util.Date; import java.util.List; @@ -30,6 +29,7 @@ import org.opensearch.common.settings.Settings; import org.opensearch.common.xcontent.XContentFactory; import org.opensearch.core.xcontent.ToXContent; +import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.security.action.apitokens.ApiToken; import com.nimbusds.jose.JOSEException; @@ -184,12 +184,17 @@ public ExpiringBearerAuthToken createJwt( } if (indexPermissions != null) { - List permissionStrings = new ArrayList<>(); + XContentBuilder builder = XContentFactory.jsonBuilder(); + builder.startArray(); for (ApiToken.IndexPermission permission : indexPermissions) { - permissionStrings.add(permission.toXContent(XContentFactory.jsonBuilder(), ToXContent.EMPTY_PARAMS).toString()); + // Add each permission to the array + permission.toXContent(builder, ToXContent.EMPTY_PARAMS); } - final String listOfIndexPermissions = String.join(",", permissionStrings); - claimsBuilder.claim("ip", encryptString(listOfIndexPermissions)); + builder.endArray(); + + // Encrypt the entire JSON array + String jsonArray = builder.toString(); + claimsBuilder.claim("ip", encryptString(jsonArray)); } final JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.parse(signingKey.getAlgorithm().getName())).build(); diff --git a/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java b/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java index e1efe65409..04a2489b7b 100644 --- a/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java +++ b/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java @@ -77,7 +77,8 @@ public class SecurityRestFilter { protected final Logger log = LogManager.getLogger(this.getClass()); public static final String API_TOKEN_CLUSTERPERM_KEY = "security.api_token.clusterperm"; - public static final String API_TOKEN_INDEXPERM_KEY = "security.api_token.indexperm"; + public static final String API_TOKEN_INDEXACTIONS_KEY = "security.api_token.indexactions"; + public static final String API_TOKEN_INDICES_KEY = "security.api_token.indices"; private final BackendRegistry registry; private final RestLayerPrivilegesEvaluator evaluator; private final AuditLog auditLog; @@ -235,12 +236,6 @@ void authorizeRequest(RestHandler original, SecurityRequestChannel request, User .add(route.name()) .build(); - log.info("API token context value: " + threadContext.getTransient(API_TOKEN_CLUSTERPERM_KEY).toString()); - - if (threadContext.getTransient(API_TOKEN_CLUSTERPERM_KEY) != null) { - return; - } - pres = evaluator.evaluate(user, route.name(), actionNames); if (log.isDebugEnabled()) { diff --git a/src/main/java/org/opensearch/security/http/ApiTokenAuthenticator.java b/src/main/java/org/opensearch/security/http/ApiTokenAuthenticator.java index bc6b25116d..0d37c51355 100644 --- a/src/main/java/org/opensearch/security/http/ApiTokenAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/ApiTokenAuthenticator.java @@ -11,9 +11,10 @@ package org.opensearch.security.http; +import java.io.IOException; import java.security.AccessController; import java.security.PrivilegedAction; -import java.util.Arrays; +import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map.Entry; @@ -31,7 +32,12 @@ import org.opensearch.SpecialPermission; import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.common.xcontent.XContentType; +import org.opensearch.core.xcontent.DeprecationHandler; +import org.opensearch.core.xcontent.NamedXContentRegistry; +import org.opensearch.core.xcontent.XContentParser; import org.opensearch.security.DefaultObjectMapper; +import org.opensearch.security.action.apitokens.ApiToken; import org.opensearch.security.auth.HTTPAuthenticator; import org.opensearch.security.authtoken.jwt.EncryptionDecryptionUtil; import org.opensearch.security.filter.SecurityRequest; @@ -48,6 +54,8 @@ import static org.opensearch.security.OpenSearchSecurityPlugin.LEGACY_OPENDISTRO_PREFIX; import static org.opensearch.security.OpenSearchSecurityPlugin.PLUGINS_PREFIX; import static org.opensearch.security.filter.SecurityRestFilter.API_TOKEN_CLUSTERPERM_KEY; +import static org.opensearch.security.filter.SecurityRestFilter.API_TOKEN_INDEXACTIONS_KEY; +import static org.opensearch.security.filter.SecurityRestFilter.API_TOKEN_INDICES_KEY; import static org.opensearch.security.util.AuthTokenUtils.isAccessToRestrictedEndpoints; public class ApiTokenAuthenticator implements HTTPAuthenticator { @@ -109,32 +117,88 @@ private JwtParserBuilder initParserBuilder(final String signingKey) { return jwtParserBuilder; } - private String extractSecurityRolesFromClaims(Claims claims) { + private String extractClusterPermissionsFromClaims(Claims claims) { Object cp = claims.get("cp"); - Object ip = claims.get("ip"); - String rolesClaim = ""; + String clusterPermissions = ""; if (cp != null) { - rolesClaim = encryptionUtil.decrypt(cp.toString()); + clusterPermissions = encryptionUtil.decrypt(cp.toString()); } else { log.warn("This is a malformed Api Token"); } - return rolesClaim; + return clusterPermissions; } - private String[] extractBackendRolesFromClaims(Claims claims) { - Object backendRolesObject = claims.get("br"); - String[] backendRoles; + private String extractAllowedActionsFromClaims(Claims claims) throws IOException { + Object ip = claims.get("ip"); + + if (ip != null) { + String decryptedPermissions = encryptionUtil.decrypt(ip.toString()); + + try ( + XContentParser parser = XContentType.JSON.xContent() + .createParser(NamedXContentRegistry.EMPTY, DeprecationHandler.THROW_UNSUPPORTED_OPERATION, decryptedPermissions) + ) { + + // Use built-in array parsing + List permissions = new ArrayList<>(); + + // Move to start of array + parser.nextToken(); // START_ARRAY + while (parser.nextToken() != XContentParser.Token.END_ARRAY) { + permissions.add(ApiToken.IndexPermission.fromXContent(parser)); + } + // Get first permission's actions + if (!permissions.isEmpty() && !permissions.get(0).getAllowedActions().isEmpty()) { + return permissions.get(0).getAllowedActions().get(0); + } + + return ""; + } catch (Exception e) { + log.error("Error extracting allowed actions", e); + return ""; + } + + } + + return ""; + } + + private String extractIndicesFromClaims(Claims claims) throws IOException { + Object ip = claims.get("ip"); + + if (ip != null) { + String decryptedPermissions = encryptionUtil.decrypt(ip.toString()); + + try ( + XContentParser parser = XContentType.JSON.xContent() + .createParser(NamedXContentRegistry.EMPTY, DeprecationHandler.THROW_UNSUPPORTED_OPERATION, decryptedPermissions) + ) { + + // Use built-in array parsing + List permissions = new ArrayList<>(); + + // Move to start of array + parser.nextToken(); // START_ARRAY + while (parser.nextToken() != XContentParser.Token.END_ARRAY) { + permissions.add(ApiToken.IndexPermission.fromXContent(parser)); + } + + // Get first permission's actions + if (!permissions.isEmpty() && !permissions.get(0).getIndexPatterns().isEmpty()) { + return permissions.get(0).getIndexPatterns().get(0); + } + + return ""; + } catch (Exception e) { + log.error("Error extracting indices", e); + return ""; + } - if (backendRolesObject == null) { - backendRoles = new String[0]; - } else { - // Extracting roles based on the compatibility mode - backendRoles = Arrays.stream(backendRolesObject.toString().split(",")).map(String::trim).toArray(String[]::new); } - return backendRoles; + return ""; } @Override @@ -193,10 +257,15 @@ private AuthCredentials extractCredentials0(final SecurityRequest request, final return null; } - String clusterPermissions = extractSecurityRolesFromClaims(claims); - String[] backendRoles = extractBackendRolesFromClaims(claims); + log.info("before extraction"); + + String clusterPermissions = extractClusterPermissionsFromClaims(claims); + String allowedActions = extractAllowedActionsFromClaims(claims); + String indices = extractIndicesFromClaims(claims); + + log.info(clusterPermissions + allowedActions + indices); - final AuthCredentials ac = new AuthCredentials(subject, List.of(), backendRoles).markComplete(); + final AuthCredentials ac = new AuthCredentials(subject, List.of(), new String[0]).markComplete(); for (Entry claim : claims.entrySet()) { String key = "attr.jwt." + claim.getKey(); @@ -218,6 +287,8 @@ private AuthCredentials extractCredentials0(final SecurityRequest request, final } context.putTransient(API_TOKEN_CLUSTERPERM_KEY, clusterPermissions); + context.putTransient(API_TOKEN_INDEXACTIONS_KEY, allowedActions); + context.putTransient(API_TOKEN_INDICES_KEY, indices); return ac; diff --git a/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java b/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java index 7f019c86db..d32df0eb56 100644 --- a/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java +++ b/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java @@ -14,6 +14,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -161,6 +162,14 @@ public PrivilegesEvaluatorResponse hasIndexPrivilege( return response; } + // API Token Authz + // TODO: this is very naive implementation + if (context.getIndices() != null && new HashSet<>(context.getIndices()).containsAll(resolvedIndices.getAllIndices())) { + if (new HashSet<>(context.getAllowedActions()).containsAll(actions)) { + return PrivilegesEvaluatorResponse.ok(); + } + } + if (!resolvedIndices.isLocalAll() && resolvedIndices.getAllIndices().isEmpty()) { // This is necessary for requests which operate on remote indices. // Access control for the remote indices will be performed on the remote cluster. diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java index 686f38a686..65b69f5b53 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java @@ -47,6 +47,8 @@ public class PrivilegesEvaluationContext { private final IndexNameExpressionResolver indexNameExpressionResolver; private final Supplier clusterStateSupplier; private List clusterPermissions; + private List allowedActions; + private List indices; /** * This caches the ready to use WildcardMatcher instances for the current request. Many index patterns have @@ -182,4 +184,20 @@ public void setClusterPermissions(List clusterPermissions) { public List getClusterPermissions() { return clusterPermissions; } + + public List getAllowedActions() { + return allowedActions; + } + + public void setAllowedActions(List allowedActions) { + this.allowedActions = allowedActions; + } + + public List getIndices() { + return indices; + } + + public void setIndices(List indices) { + this.indices = indices; + } } diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java index faea38b81c..0871633a25 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java @@ -110,6 +110,8 @@ import static org.opensearch.security.OpenSearchSecurityPlugin.traceAction; import static org.opensearch.security.filter.SecurityRestFilter.API_TOKEN_CLUSTERPERM_KEY; +import static org.opensearch.security.filter.SecurityRestFilter.API_TOKEN_INDEXACTIONS_KEY; +import static org.opensearch.security.filter.SecurityRestFilter.API_TOKEN_INDICES_KEY; import static org.opensearch.security.support.ConfigConstants.OPENDISTRO_SECURITY_USER_INFO_THREAD_CONTEXT; public class PrivilegesEvaluator { @@ -352,6 +354,21 @@ public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) context.setClusterPermissions(clusterPermissions); } + final String apiTokenIndexAllowedActions = threadContext.getTransient(API_TOKEN_INDEXACTIONS_KEY); + if (apiTokenIndexAllowedActions != null) { + List allowedactions = Arrays.asList(apiTokenIndexAllowedActions.split(",")); + context.setAllowedActions(allowedactions); + } + + final String apiTokenIndices = threadContext.getTransient(API_TOKEN_INDICES_KEY); + if (apiTokenIndices != null) { + List indices = Arrays.asList(apiTokenIndices.split(",")); + context.setIndices(indices); + } + + log.info("API Tokens actions" + apiTokenIndexAllowedActions); + log.info("API Tokens indices" + apiTokenIndices); + // Add the security roles for this user so that they can be used for DLS parameter substitution. user.addSecurityRoles(mappedRoles); setUserInfoInThreadContext(user); diff --git a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java index b57b422653..a6e4ff4734 100644 --- a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java +++ b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java @@ -386,6 +386,7 @@ private void buildAAA() { */ Settings apiTokenSettings = getDynamicApiTokenSettings(); if (!isKeyNull(apiTokenSettings, "signing_key") && !isKeyNull(apiTokenSettings, "encryption_key")) { + log.info("we initialized the api tokenauthenticator"); final AuthDomain _ad = new AuthDomain( new NoOpAuthenticationBackend(Settings.EMPTY, null), new ApiTokenAuthenticator(getDynamicApiTokenSettings(), this.cih.getClusterName()), From 6904317c7ff2d55916943449a031e300bbc7c809 Mon Sep 17 00:00:00 2001 From: Derek Ho Date: Thu, 26 Dec 2024 11:18:00 -0500 Subject: [PATCH 03/20] Fix tests Signed-off-by: Derek Ho --- .../opensearch/security/privileges/ActionPrivileges.java | 2 +- .../opensearch/security/authtoken/jwt/JwtVendorTest.java | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java b/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java index d32df0eb56..2a0f572f0e 100644 --- a/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java +++ b/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java @@ -417,7 +417,7 @@ PrivilegesEvaluatorResponse providesPrivilege(PrivilegesEvaluationContext contex } // 4: Evaluate api tokens - if (context.getClusterPermissions().contains(action)) { + if (context.getClusterPermissions() != null && context.getClusterPermissions().contains(action)) { return PrivilegesEvaluatorResponse.ok(); } diff --git a/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java b/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java index 48aae6f9b8..ee11c17e13 100644 --- a/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java +++ b/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java @@ -289,8 +289,9 @@ public void testCreateJwtForApiTokenSuccess() throws Exception { ApiToken.IndexPermission indexPermission = new ApiToken.IndexPermission(List.of("*"), List.of("read")); final List indexPermissions = List.of(indexPermission); final String expectedClusterPermissions = "cluster:admin/*"; - final String expectedIndexPermissions = indexPermission.toXContent(XContentFactory.jsonBuilder(), ToXContent.EMPTY_PARAMS) - .toString(); + final String expectedIndexPermissions = "[" + + indexPermission.toXContent(XContentFactory.jsonBuilder(), ToXContent.EMPTY_PARAMS).toString() + + "]"; LongSupplier currentTime = () -> (long) 100; String claimsEncryptionKey = "1234567890123456"; @@ -319,6 +320,7 @@ public void testCreateJwtForApiTokenSuccess() throws Exception { encryptionUtil.decrypt(signedJWT.getJWTClaimsSet().getClaims().get("cp").toString()), equalTo(expectedClusterPermissions) ); + assertThat(encryptionUtil.decrypt(signedJWT.getJWTClaimsSet().getClaims().get("ip").toString()), equalTo(expectedIndexPermissions)); XContentParser parser = XContentType.JSON.xContent() @@ -327,6 +329,9 @@ public void testCreateJwtForApiTokenSuccess() throws Exception { DeprecationHandler.THROW_UNSUPPORTED_OPERATION, encryptionUtil.decrypt(signedJWT.getJWTClaimsSet().getClaims().get("ip").toString()) ); + // Parse first item of the list + parser.nextToken(); + parser.nextToken(); ApiToken.IndexPermission indexPermission1 = ApiToken.IndexPermission.fromXContent(parser); // Index permission deserialization works as expected From 17bca93a6524ff59eda1f070b8f018e294c7ec46 Mon Sep 17 00:00:00 2001 From: Derek Ho Date: Thu, 26 Dec 2024 12:09:00 -0500 Subject: [PATCH 04/20] Revert mis-merge in abstractauditlog Signed-off-by: Derek Ho --- .../auditlog/impl/AbstractAuditLog.java | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/src/main/java/org/opensearch/security/auditlog/impl/AbstractAuditLog.java b/src/main/java/org/opensearch/security/auditlog/impl/AbstractAuditLog.java index 8bf8f63dde..9a16cd8bfd 100644 --- a/src/main/java/org/opensearch/security/auditlog/impl/AbstractAuditLog.java +++ b/src/main/java/org/opensearch/security/auditlog/impl/AbstractAuditLog.java @@ -584,22 +584,24 @@ public void logDocumentWritten(ShardId shardId, GetResult originalResult, Index originalSource = "{}"; } if (securityIndicesMatcher.test(shardId.getIndexName())) { - try ( - XContentParser parser = XContentHelper.createParser( - NamedXContentRegistry.EMPTY, - THROW_UNSUPPORTED_OPERATION, - originalResult.internalSourceRef(), - XContentType.JSON - ) - ) { - Object base64 = parser.map().values().iterator().next(); - if (base64 instanceof String) { - originalSource = (new String(BaseEncoding.base64().decode((String) base64), StandardCharsets.UTF_8)); - } else { - originalSource = XContentHelper.convertToJson(originalResult.internalSourceRef(), false, XContentType.JSON); + if (originalSource == null) { + try ( + XContentParser parser = XContentHelper.createParser( + NamedXContentRegistry.EMPTY, + THROW_UNSUPPORTED_OPERATION, + originalResult.internalSourceRef(), + XContentType.JSON + ) + ) { + Object base64 = parser.map().values().iterator().next(); + if (base64 instanceof String) { + originalSource = (new String(BaseEncoding.base64().decode((String) base64), StandardCharsets.UTF_8)); + } else { + originalSource = XContentHelper.convertToJson(originalResult.internalSourceRef(), false, XContentType.JSON); + } + } catch (Exception e) { + log.error(e.toString()); } - } catch (Exception e) { - log.error(e.toString()); } try ( @@ -640,7 +642,7 @@ public void logDocumentWritten(ShardId shardId, GetResult originalResult, Index } } - if (!complianceConfig.shouldLogWriteMetadataOnly()) { + if (!complianceConfig.shouldLogWriteMetadataOnly() && !complianceConfig.shouldLogDiffsForWrite()) { if (securityIndicesMatcher.test(shardId.getIndexName())) { // current source, normally not null or empty try ( From 92d4e60302b70f2ed0a6ca4b37010f9399443581 Mon Sep 17 00:00:00 2001 From: Derek Ho Date: Fri, 27 Dec 2024 12:34:12 -0500 Subject: [PATCH 05/20] Add allowlist for authc, add basic test showing it works Signed-off-by: Derek Ho --- .../security/OpenSearchSecurityPlugin.java | 10 ++ .../apitokens/ApiTokenIndexListenerCache.java | 112 ++++++++++++++++++ .../security/http/ApiTokenAuthenticator.java | 22 ++-- .../securityconf/DynamicConfigModelV7.java | 1 - .../security/ssl/util/ExceptionUtils.java | 4 + .../security/util/AuthTokenUtils.java | 4 + .../apitokens/ApiTokenAuthenticatorTest.java | 99 ++++++++++++++++ 7 files changed, 242 insertions(+), 10 deletions(-) create mode 100644 src/main/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCache.java create mode 100644 src/test/java/org/opensearch/security/action/apitokens/ApiTokenAuthenticatorTest.java diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 063088fcc9..efe51d2e74 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -132,6 +132,7 @@ import org.opensearch.search.internal.SearchContext; import org.opensearch.search.query.QuerySearchResult; import org.opensearch.security.action.apitokens.ApiTokenAction; +import org.opensearch.security.action.apitokens.ApiTokenIndexListenerCache; import org.opensearch.security.action.configupdate.ConfigUpdateAction; import org.opensearch.security.action.configupdate.TransportConfigUpdateAction; import org.opensearch.security.action.onbehalf.CreateOnBehalfOfTokenAction; @@ -717,6 +718,15 @@ public void onIndexModule(IndexModule indexModule) { dlsFlsBaseContext ) ); + + // TODO: Is there a higher level approach that makes more sense here? Does this cover unsuccessful index ops? + if (ConfigConstants.OPENSEARCH_API_TOKENS_INDEX.equals(indexModule.getIndex().getName())) { + ApiTokenIndexListenerCache apiTokenIndexListenerCacher = ApiTokenIndexListenerCache.getInstance(); + apiTokenIndexListenerCacher.initialize(); + indexModule.addIndexOperationListener(apiTokenIndexListenerCacher); + log.warn("Security plugin started listening to operations on index {}", ConfigConstants.OPENSEARCH_API_TOKENS_INDEX); + } + indexModule.forceQueryCacheProvider((indexSettings, nodeCache) -> new QueryCache() { @Override diff --git a/src/main/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCache.java b/src/main/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCache.java new file mode 100644 index 0000000000..68ec995c60 --- /dev/null +++ b/src/main/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCache.java @@ -0,0 +1,112 @@ +/* + * 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. + */ + +package org.opensearch.security.action.apitokens; + +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.common.xcontent.LoggingDeprecationHandler; +import org.opensearch.common.xcontent.XContentType; +import org.opensearch.core.common.bytes.BytesReference; +import org.opensearch.core.index.shard.ShardId; +import org.opensearch.core.xcontent.NamedXContentRegistry; +import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.index.engine.Engine; +import org.opensearch.index.shard.IndexingOperationListener; + +/** + * This class implements an index operation listener for operations performed on api tokens + * These indices are defined on bootstrap and configured to listen in OpenSearchSecurityPlugin.java + */ +public class ApiTokenIndexListenerCache implements IndexingOperationListener { + + private final static Logger log = LogManager.getLogger(ApiTokenIndexListenerCache.class); + + private static final ApiTokenIndexListenerCache INSTANCE = new ApiTokenIndexListenerCache(); + private final ConcurrentHashMap idToJtiMap = new ConcurrentHashMap<>(); + + private Set jtis = new HashSet(); + + private boolean initialized; + + private ApiTokenIndexListenerCache() {} + + public static ApiTokenIndexListenerCache getInstance() { + return ApiTokenIndexListenerCache.INSTANCE; + } + + /** + * Initializes the ApiTokenIndexListenerCache. + * This method is called during the plugin's initialization process. + * + */ + public void initialize() { + + if (initialized) { + return; + } + + initialized = true; + + } + + public boolean isInitialized() { + return initialized; + } + + /** + * This method is called after an index operation is performed. + * It adds the JTI of the indexed document to the cache and maps the document ID to the JTI (for deletion handling). + * @param shardId The shard ID of the index where the operation was performed. + * @param index The index where the operation was performed. + * @param result The result of the index operation. + */ + @Override + public void postIndex(ShardId shardId, Engine.Index index, Engine.IndexResult result) { + BytesReference sourceRef = index.source(); + + try { + XContentParser parser = XContentType.JSON.xContent() + .createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, sourceRef.streamInput()); + + ApiToken token = ApiToken.fromXContent(parser); + jtis.add(token.getJti()); + idToJtiMap.put(index.id(), token.getJti()); + + } catch (IOException e) { + log.error("Failed to parse indexed document", e); + } + } + + /** + * This method is called after a delete operation is performed. + * It deletes the corresponding document id in the map and the corresponding jti from the cache. + * @param shardId The shard ID of the index where the delete operation was performed. + * @param delete The delete operation that was performed. + * @param result The result of the delete operation. + */ + @Override + public void postDelete(ShardId shardId, Engine.Delete delete, Engine.DeleteResult result) { + String docId = delete.id(); + String jti = idToJtiMap.remove(docId); + if (jti != null) { + jtis.remove(jti); + log.debug("Removed token with ID {} and JTI {} from cache", docId, jti); + } + } + + public Set getJtis() { + return jtis; + } +} diff --git a/src/main/java/org/opensearch/security/http/ApiTokenAuthenticator.java b/src/main/java/org/opensearch/security/http/ApiTokenAuthenticator.java index 0d37c51355..cae78a8415 100644 --- a/src/main/java/org/opensearch/security/http/ApiTokenAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/ApiTokenAuthenticator.java @@ -38,6 +38,7 @@ import org.opensearch.core.xcontent.XContentParser; import org.opensearch.security.DefaultObjectMapper; import org.opensearch.security.action.apitokens.ApiToken; +import org.opensearch.security.action.apitokens.ApiTokenIndexListenerCache; import org.opensearch.security.auth.HTTPAuthenticator; import org.opensearch.security.authtoken.jwt.EncryptionDecryptionUtil; import org.opensearch.security.filter.SecurityRequest; @@ -226,6 +227,7 @@ private AuthCredentials extractCredentials0(final SecurityRequest request, final log.error("Api token authentication is disabled"); return null; } + ApiTokenIndexListenerCache cache = ApiTokenIndexListenerCache.getInstance(); String jwtToken = extractJwtFromHeader(request); if (jwtToken == null) { @@ -236,35 +238,37 @@ private AuthCredentials extractCredentials0(final SecurityRequest request, final return null; } + // TODO: handle revocation different from deletion? + if (!cache.getJtis().contains(encryptionUtil.encrypt(jwtToken))) { + log.debug("Token is not allowlisted"); + return null; + } + try { final Claims claims = jwtParser.parseClaimsJws(jwtToken).getBody(); final String subject = claims.getSubject(); if (subject == null) { - log.error("Valid jwt on behalf of token with no subject"); + log.error("Valid jwt api token with no subject"); return null; } final Set audience = claims.getAudience(); if (audience == null || audience.isEmpty()) { - log.error("Valid jwt on behalf of token with no audience"); + log.error("Valid jwt api token with no audience"); return null; } final String issuer = claims.getIssuer(); if (!clusterName.equals(issuer)) { - log.error("The issuer of this OBO does not match the current cluster identifier"); + log.error("The issuer of this api token does not match the current cluster identifier"); return null; } - log.info("before extraction"); - String clusterPermissions = extractClusterPermissionsFromClaims(claims); String allowedActions = extractAllowedActionsFromClaims(claims); String indices = extractIndicesFromClaims(claims); - log.info(clusterPermissions + allowedActions + indices); - final AuthCredentials ac = new AuthCredentials(subject, List.of(), new String[0]).markComplete(); for (Entry claim : claims.entrySet()) { @@ -333,7 +337,7 @@ public Boolean isRequestAllowed(final SecurityRequest request) { Matcher matcher = PATTERN_PATH_PREFIX.matcher(request.path()); final String suffix = matcher.matches() ? matcher.group(2) : null; if (isAccessToRestrictedEndpoints(request, suffix)) { - final OpenSearchException exception = ExceptionUtils.invalidUsageOfOBOTokenException(); + final OpenSearchException exception = ExceptionUtils.invalidUsageOfApiTokenException(); log.error(exception.toString()); return false; } @@ -347,7 +351,7 @@ public Optional reRequestAuthentication(final SecurityRequest @Override public String getType() { - return "onbehalfof_jwt"; + return "apitoken_jwt"; } @Override diff --git a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java index a6e4ff4734..b57b422653 100644 --- a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java +++ b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java @@ -386,7 +386,6 @@ private void buildAAA() { */ Settings apiTokenSettings = getDynamicApiTokenSettings(); if (!isKeyNull(apiTokenSettings, "signing_key") && !isKeyNull(apiTokenSettings, "encryption_key")) { - log.info("we initialized the api tokenauthenticator"); final AuthDomain _ad = new AuthDomain( new NoOpAuthenticationBackend(Settings.EMPTY, null), new ApiTokenAuthenticator(getDynamicApiTokenSettings(), this.cih.getClusterName()), diff --git a/src/main/java/org/opensearch/security/ssl/util/ExceptionUtils.java b/src/main/java/org/opensearch/security/ssl/util/ExceptionUtils.java index 4683075f1d..32a70a468f 100644 --- a/src/main/java/org/opensearch/security/ssl/util/ExceptionUtils.java +++ b/src/main/java/org/opensearch/security/ssl/util/ExceptionUtils.java @@ -68,6 +68,10 @@ public static OpenSearchException invalidUsageOfOBOTokenException() { return new OpenSearchException("On-Behalf-Of Token is not allowed to be used for accessing this endpoint."); } + public static OpenSearchException invalidUsageOfApiTokenException() { + return new OpenSearchException("Api Tokens are not allowed to be used for accessing this endpoint."); + } + public static OpenSearchException createJwkCreationException() { return new OpenSearchException("An error occurred during the creation of Jwk."); } diff --git a/src/main/java/org/opensearch/security/util/AuthTokenUtils.java b/src/main/java/org/opensearch/security/util/AuthTokenUtils.java index 3884bf75fe..caccb91407 100644 --- a/src/main/java/org/opensearch/security/util/AuthTokenUtils.java +++ b/src/main/java/org/opensearch/security/util/AuthTokenUtils.java @@ -20,6 +20,7 @@ public class AuthTokenUtils { private static final String ON_BEHALF_OF_SUFFIX = "api/generateonbehalfoftoken"; private static final String ACCOUNT_SUFFIX = "api/account"; + private static final String API_TOKEN_SUFFIX = "api/apitokens"; public static Boolean isAccessToRestrictedEndpoints(final SecurityRequest request, final String suffix) { if (suffix == null) { @@ -28,6 +29,9 @@ public static Boolean isAccessToRestrictedEndpoints(final SecurityRequest reques switch (suffix) { case ON_BEHALF_OF_SUFFIX: return request.method() == POST; + case API_TOKEN_SUFFIX: + // Don't want to allow any api token access + return true; case ACCOUNT_SUFFIX: return request.method() == PUT; default: diff --git a/src/test/java/org/opensearch/security/action/apitokens/ApiTokenAuthenticatorTest.java b/src/test/java/org/opensearch/security/action/apitokens/ApiTokenAuthenticatorTest.java new file mode 100644 index 0000000000..358bf746d2 --- /dev/null +++ b/src/test/java/org/opensearch/security/action/apitokens/ApiTokenAuthenticatorTest.java @@ -0,0 +1,99 @@ +/* + * 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.action.apitokens; + +import org.apache.logging.log4j.Logger; +import org.junit.Before; +import org.junit.Test; + +import org.opensearch.common.settings.Settings; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.security.filter.SecurityRequest; +import org.opensearch.security.http.ApiTokenAuthenticator; +import org.opensearch.security.user.AuthCredentials; + +import org.mockito.Mock; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ApiTokenAuthenticatorTest { + + private ApiTokenAuthenticator authenticator; + private ApiTokenIndexListenerCache cache; + private String testJti = "test-jti"; + @Mock + private Logger log; + + @Before + public void setUp() { + // Setup basic settings + Settings settings = Settings.builder() + .put("enabled", "true") + .put("signing_key", "U3VwZXJTZWNyZXRLZXlUaGF0SXNFeGFjdGx5NjRCeXRlc0xvbmdBbmRXaWxsV29ya1dpdGhIUzUxMkFsZ29yaXRobSEhCgo=") + .put("encryption_key", "MTIzNDU2Nzg5MDEyMzQ1Ng==") + .build(); + + authenticator = new ApiTokenAuthenticator(settings, "opensearch-cluster"); + cache = ApiTokenIndexListenerCache.getInstance(); + } + + @Test + public void testAuthenticationFailsWhenJtiNotInCache() { + String testJti = "test-jti-not-in-cache"; + ApiTokenIndexListenerCache cache = ApiTokenIndexListenerCache.getInstance(); + assertFalse(cache.getJtis().contains(testJti)); + + SecurityRequest request = mock(SecurityRequest.class); + when(request.header("Authorization")).thenReturn("Bearer " + testJti); + when(request.path()).thenReturn("/test"); + + ThreadContext threadContext = new ThreadContext(Settings.EMPTY); + + AuthCredentials credentials = authenticator.extractCredentials(request, threadContext); + + // It should return null when JTI is not in cache + assertNull("Should return null when JTI is not in allowlist cache", credentials); + } + + @Test + public void testExtractCredentialsPassWhenJtiInCache() { + // Given: A JTI that is in the cache + String testJti = + "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ0ZXN0LXRva2VuIiwiYXVkIjoidGVzdC10b2tlbiIsIm5iZiI6MTczNTMxODI5NywiaXAiOiJnaGlWTXVnWlBtcHZJMjZIT2hmUTRnaEJ1Qkh0Y2x6c1REYVlxQjVBRklyTkE4SzJCVTdxc2toMVBCOEMzQWpTdVBaREM0THVSM2pjZkdpLzlkU2ZicDBuQTNGMkhtSi9jaDA3cDY2ZWJ6OD0iLCJpc3MiOiJvcGVuc2VhcmNoLWNsdXN0ZXIiLCJleHAiOjE3Mzc5MTAyOTcsImlhdCI6MTczNTMxODI5NywiY3AiOiI5T0tzeGhUWmdaZjlTdWpKa0cxV1ViM1QvUVU2eGJmU3Noa25ZZFVIa2hzPSJ9.xdoDZiGBbqaqcH2evoMEV5384oTyRg04_gO3akQpO4c502c8bV8W5TF_5SxUvkXKDeuQEBFH-4c44VVhCnUQIw"; + String encryptedTestJti = + "k3JQNRXR57Y4V4W1LNkpEP+QzcDra5+/fFfQrr0Rncr/RhYAnYEDgN9RKcJ2Wmjf63NCQa9+HjeFcn0H2wKhMm7pl9bd3ya9FO+LUD7t9Sj+IKBsThVo93sUmnxJh/llglMsKsKQVkuY+YKa6A6dT8bMqmt7kIrer7w8TRENr9J8x41TGb/cDDWDvJLME7QkFzJjMxYDgKNiEevMbOpC8yjIZdK08jPe3Thq+xm+JYruoYeyI5g8QjkJA9ZOs1f6eXTAvPxhseuPqgIKykRE25fuWjl5n9tJ9W+jpl+iET7zzOLXSPEU5UepL/COkVd6xW63Ay72oMOewqveDXzyR8S8LAfgRuKgYZWms7yT37XcGg0c6Y7M62KVPo+1XQ+FGLtty3eDKwaSopFqLNcISFMiPml9XYv7V1AndJGINbH4KUDyeSQYUh4d+sOxjg9prGzW0nvKE22jzyQlW9t0wpDiB0visInvKVZAqKLPUp0x0pFbAVV12sJJkw6DFkD6+VL+8d2L/Z8kxJXO3uHHjhO3u3RWAe6UhLGncLhJciH57MEw8zFdNturr+tJREL5WbWyiEzKTOBzO8R5Ec92XyCDshIXzVxQv/QOM5meFxPcrkBAgKa6ztWCCmQqa2M1MdKkwKUGn3w6ixOTZ55nZQ=="; + ApiTokenIndexListenerCache cache = ApiTokenIndexListenerCache.getInstance(); + cache.getJtis().add(encryptedTestJti); + assertTrue(cache.getJtis().contains(encryptedTestJti)); + + // Create a mock request with the JWT token and path + SecurityRequest request = mock(SecurityRequest.class); + when(request.header("Authorization")).thenReturn("Bearer " + testJti); + when(request.path()).thenReturn("/test"); + + // Create ThreadContext + Settings settings = Settings.builder().build(); + ThreadContext threadContext = new ThreadContext(settings); + + AuthCredentials ac = authenticator.extractCredentials(request, threadContext); + + // Verify the exception message if needed + assertNotNull("Should return null when JTI is not in allowlist cache", ac); + + } + +} From 22cfbe87fce8a11394adfbfd4f09e31f8491101a Mon Sep 17 00:00:00 2001 From: Derek Ho Date: Fri, 27 Dec 2024 15:37:02 -0500 Subject: [PATCH 06/20] Add more extensive tests for authenticator, switch to list of indexPermissions Signed-off-by: Derek Ho --- .../security/filter/SecurityRestFilter.java | 3 +- .../security/http/ApiTokenAuthenticator.java | 95 ++----------- .../security/privileges/ActionPrivileges.java | 13 +- .../PrivilegesEvaluationContext.java | 20 +-- .../privileges/PrivilegesEvaluator.java | 20 +-- .../apitokens/ApiTokenAuthenticatorTest.java | 129 +++++++++++++++--- .../authtoken/jwt/AuthTokenUtilsTest.java | 11 ++ 7 files changed, 156 insertions(+), 135 deletions(-) diff --git a/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java b/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java index 04a2489b7b..c214075a42 100644 --- a/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java +++ b/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java @@ -77,8 +77,7 @@ public class SecurityRestFilter { protected final Logger log = LogManager.getLogger(this.getClass()); public static final String API_TOKEN_CLUSTERPERM_KEY = "security.api_token.clusterperm"; - public static final String API_TOKEN_INDEXACTIONS_KEY = "security.api_token.indexactions"; - public static final String API_TOKEN_INDICES_KEY = "security.api_token.indices"; + public static final String API_TOKEN_INDEXPERM_KEY = "security.api_token.indexactions"; private final BackendRegistry registry; private final RestLayerPrivilegesEvaluator evaluator; private final AuditLog auditLog; diff --git a/src/main/java/org/opensearch/security/http/ApiTokenAuthenticator.java b/src/main/java/org/opensearch/security/http/ApiTokenAuthenticator.java index cae78a8415..3b5715ecf0 100644 --- a/src/main/java/org/opensearch/security/http/ApiTokenAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/ApiTokenAuthenticator.java @@ -15,11 +15,8 @@ import java.security.AccessController; import java.security.PrivilegedAction; import java.util.ArrayList; -import java.util.Collection; import java.util.List; -import java.util.Map.Entry; import java.util.Optional; -import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -36,7 +33,6 @@ import org.opensearch.core.xcontent.DeprecationHandler; import org.opensearch.core.xcontent.NamedXContentRegistry; import org.opensearch.core.xcontent.XContentParser; -import org.opensearch.security.DefaultObjectMapper; import org.opensearch.security.action.apitokens.ApiToken; import org.opensearch.security.action.apitokens.ApiTokenIndexListenerCache; import org.opensearch.security.auth.HTTPAuthenticator; @@ -55,8 +51,7 @@ import static org.opensearch.security.OpenSearchSecurityPlugin.LEGACY_OPENDISTRO_PREFIX; import static org.opensearch.security.OpenSearchSecurityPlugin.PLUGINS_PREFIX; import static org.opensearch.security.filter.SecurityRestFilter.API_TOKEN_CLUSTERPERM_KEY; -import static org.opensearch.security.filter.SecurityRestFilter.API_TOKEN_INDEXACTIONS_KEY; -import static org.opensearch.security.filter.SecurityRestFilter.API_TOKEN_INDICES_KEY; +import static org.opensearch.security.filter.SecurityRestFilter.API_TOKEN_INDEXPERM_KEY; import static org.opensearch.security.util.AuthTokenUtils.isAccessToRestrictedEndpoints; public class ApiTokenAuthenticator implements HTTPAuthenticator { @@ -65,7 +60,7 @@ public class ApiTokenAuthenticator implements HTTPAuthenticator { private static final String REGEX_PATH_PREFIX = "/(" + LEGACY_OPENDISTRO_PREFIX + "|" + PLUGINS_PREFIX + ")/" + "(.*)"; private static final Pattern PATTERN_PATH_PREFIX = Pattern.compile(REGEX_PATH_PREFIX); - protected final Logger log = LogManager.getLogger(this.getClass()); + public Logger log = LogManager.getLogger(this.getClass()); private static final Pattern BEARER = Pattern.compile("^\\s*Bearer\\s.*", Pattern.CASE_INSENSITIVE); private static final String BEARER_PREFIX = "bearer "; @@ -131,7 +126,7 @@ private String extractClusterPermissionsFromClaims(Claims claims) { return clusterPermissions; } - private String extractAllowedActionsFromClaims(Claims claims) throws IOException { + private List extractIndexPermissionFromClaims(Claims claims) throws IOException { Object ip = claims.get("ip"); if (ip != null) { @@ -150,56 +145,15 @@ private String extractAllowedActionsFromClaims(Claims claims) throws IOException while (parser.nextToken() != XContentParser.Token.END_ARRAY) { permissions.add(ApiToken.IndexPermission.fromXContent(parser)); } - // Get first permission's actions - if (!permissions.isEmpty() && !permissions.get(0).getAllowedActions().isEmpty()) { - return permissions.get(0).getAllowedActions().get(0); - } - - return ""; - } catch (Exception e) { - log.error("Error extracting allowed actions", e); - return ""; - } - - } - - return ""; - } - - private String extractIndicesFromClaims(Claims claims) throws IOException { - Object ip = claims.get("ip"); - - if (ip != null) { - String decryptedPermissions = encryptionUtil.decrypt(ip.toString()); - - try ( - XContentParser parser = XContentType.JSON.xContent() - .createParser(NamedXContentRegistry.EMPTY, DeprecationHandler.THROW_UNSUPPORTED_OPERATION, decryptedPermissions) - ) { - - // Use built-in array parsing - List permissions = new ArrayList<>(); - - // Move to start of array - parser.nextToken(); // START_ARRAY - while (parser.nextToken() != XContentParser.Token.END_ARRAY) { - permissions.add(ApiToken.IndexPermission.fromXContent(parser)); - } - - // Get first permission's actions - if (!permissions.isEmpty() && !permissions.get(0).getIndexPatterns().isEmpty()) { - return permissions.get(0).getIndexPatterns().get(0); - } - - return ""; + return permissions; } catch (Exception e) { - log.error("Error extracting indices", e); - return ""; + log.error("Error extracting index permissions", e); + return List.of(); } } - return ""; + return List.of(); } @Override @@ -253,12 +207,6 @@ private AuthCredentials extractCredentials0(final SecurityRequest request, final return null; } - final Set audience = claims.getAudience(); - if (audience == null || audience.isEmpty()) { - log.error("Valid jwt api token with no audience"); - return null; - } - final String issuer = claims.getIssuer(); if (!clusterName.equals(issuer)) { log.error("The issuer of this api token does not match the current cluster identifier"); @@ -266,33 +214,12 @@ private AuthCredentials extractCredentials0(final SecurityRequest request, final } String clusterPermissions = extractClusterPermissionsFromClaims(claims); - String allowedActions = extractAllowedActionsFromClaims(claims); - String indices = extractIndicesFromClaims(claims); - - final AuthCredentials ac = new AuthCredentials(subject, List.of(), new String[0]).markComplete(); - - for (Entry claim : claims.entrySet()) { - String key = "attr.jwt." + claim.getKey(); - Object value = claim.getValue(); - - if (value instanceof Collection) { - try { - // Convert the list to a JSON array string - String jsonValue = DefaultObjectMapper.writeValueAsString(value, false); - ac.addAttribute(key, jsonValue); - } catch (Exception e) { - log.warn("Failed to convert list claim to JSON for key: " + key, e); - // Fallback to string representation - ac.addAttribute(key, String.valueOf(value)); - } - } else { - ac.addAttribute(key, String.valueOf(value)); - } - } + List indexPermissions = extractIndexPermissionFromClaims(claims); + + final AuthCredentials ac = new AuthCredentials(subject, List.of(), "").markComplete(); context.putTransient(API_TOKEN_CLUSTERPERM_KEY, clusterPermissions); - context.putTransient(API_TOKEN_INDEXACTIONS_KEY, allowedActions); - context.putTransient(API_TOKEN_INDICES_KEY, indices); + context.putTransient(API_TOKEN_INDEXPERM_KEY, indexPermissions); return ac; diff --git a/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java b/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java index 2a0f572f0e..56c798f6f5 100644 --- a/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java +++ b/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java @@ -36,6 +36,7 @@ import org.opensearch.common.settings.Settings; import org.opensearch.core.common.unit.ByteSizeUnit; import org.opensearch.core.common.unit.ByteSizeValue; +import org.opensearch.security.action.apitokens.ApiToken; import org.opensearch.security.resolver.IndexResolverReplacer; import org.opensearch.security.securityconf.FlattenedActionGroups; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; @@ -164,8 +165,16 @@ public PrivilegesEvaluatorResponse hasIndexPrivilege( // API Token Authz // TODO: this is very naive implementation - if (context.getIndices() != null && new HashSet<>(context.getIndices()).containsAll(resolvedIndices.getAllIndices())) { - if (new HashSet<>(context.getAllowedActions()).containsAll(actions)) { + if (context.getIndexPermissions() != null) { + List indexPermissions = context.getIndexPermissions(); + + boolean hasPermission = indexPermissions.stream().anyMatch(permission -> { + boolean hasAllActions = new HashSet<>(permission.getAllowedActions()).containsAll(actions); + boolean hasAllIndices = new HashSet<>(permission.getIndexPatterns()).containsAll(resolvedIndices.getAllIndices()); + return hasAllActions && hasAllIndices; + }); + + if (hasPermission) { return PrivilegesEvaluatorResponse.ok(); } } diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java index 65b69f5b53..b41bc366da 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java @@ -21,6 +21,7 @@ import org.opensearch.cluster.ClusterState; import org.opensearch.cluster.metadata.IndexAbstraction; import org.opensearch.cluster.metadata.IndexNameExpressionResolver; +import org.opensearch.security.action.apitokens.ApiToken; import org.opensearch.security.resolver.IndexResolverReplacer; import org.opensearch.security.support.WildcardMatcher; import org.opensearch.security.user.User; @@ -47,8 +48,7 @@ public class PrivilegesEvaluationContext { private final IndexNameExpressionResolver indexNameExpressionResolver; private final Supplier clusterStateSupplier; private List clusterPermissions; - private List allowedActions; - private List indices; + private List indexPermissions; /** * This caches the ready to use WildcardMatcher instances for the current request. Many index patterns have @@ -185,19 +185,11 @@ public List getClusterPermissions() { return clusterPermissions; } - public List getAllowedActions() { - return allowedActions; + public List getIndexPermissions() { + return indexPermissions; } - public void setAllowedActions(List allowedActions) { - this.allowedActions = allowedActions; - } - - public List getIndices() { - return indices; - } - - public void setIndices(List indices) { - this.indices = indices; + public void setIndexPermissions(List indexPermissions) { + this.indexPermissions = indexPermissions; } } diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java index 0871633a25..a5af967861 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java @@ -86,6 +86,7 @@ import org.opensearch.core.xcontent.NamedXContentRegistry; import org.opensearch.index.reindex.ReindexAction; import org.opensearch.script.mustache.RenderSearchTemplateAction; +import org.opensearch.security.action.apitokens.ApiToken; import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.configuration.ClusterInfoHolder; import org.opensearch.security.configuration.ConfigurationRepository; @@ -110,8 +111,7 @@ import static org.opensearch.security.OpenSearchSecurityPlugin.traceAction; import static org.opensearch.security.filter.SecurityRestFilter.API_TOKEN_CLUSTERPERM_KEY; -import static org.opensearch.security.filter.SecurityRestFilter.API_TOKEN_INDEXACTIONS_KEY; -import static org.opensearch.security.filter.SecurityRestFilter.API_TOKEN_INDICES_KEY; +import static org.opensearch.security.filter.SecurityRestFilter.API_TOKEN_INDEXPERM_KEY; import static org.opensearch.security.support.ConfigConstants.OPENDISTRO_SECURITY_USER_INFO_THREAD_CONTEXT; public class PrivilegesEvaluator { @@ -354,21 +354,11 @@ public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) context.setClusterPermissions(clusterPermissions); } - final String apiTokenIndexAllowedActions = threadContext.getTransient(API_TOKEN_INDEXACTIONS_KEY); - if (apiTokenIndexAllowedActions != null) { - List allowedactions = Arrays.asList(apiTokenIndexAllowedActions.split(",")); - context.setAllowedActions(allowedactions); + final List apiTokenIndexPermissions = threadContext.getTransient(API_TOKEN_INDEXPERM_KEY); + if (apiTokenIndexPermissions != null) { + context.setIndexPermissions(apiTokenIndexPermissions); } - final String apiTokenIndices = threadContext.getTransient(API_TOKEN_INDICES_KEY); - if (apiTokenIndices != null) { - List indices = Arrays.asList(apiTokenIndices.split(",")); - context.setIndices(indices); - } - - log.info("API Tokens actions" + apiTokenIndexAllowedActions); - log.info("API Tokens indices" + apiTokenIndices); - // Add the security roles for this user so that they can be used for DLS parameter substitution. user.addSecurityRoles(mappedRoles); setUserInfoInThreadContext(user); diff --git a/src/test/java/org/opensearch/security/action/apitokens/ApiTokenAuthenticatorTest.java b/src/test/java/org/opensearch/security/action/apitokens/ApiTokenAuthenticatorTest.java index 358bf746d2..67293e0347 100644 --- a/src/test/java/org/opensearch/security/action/apitokens/ApiTokenAuthenticatorTest.java +++ b/src/test/java/org/opensearch/security/action/apitokens/ApiTokenAuthenticatorTest.java @@ -14,6 +14,7 @@ import org.apache.logging.log4j.Logger; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; @@ -21,26 +22,31 @@ import org.opensearch.security.http.ApiTokenAuthenticator; import org.opensearch.security.user.AuthCredentials; +import io.jsonwebtoken.ExpiredJwtException; import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +@RunWith(MockitoJUnitRunner.class) public class ApiTokenAuthenticatorTest { private ApiTokenAuthenticator authenticator; - private ApiTokenIndexListenerCache cache; - private String testJti = "test-jti"; @Mock private Logger log; + private ThreadContext threadcontext; + @Before public void setUp() { - // Setup basic settings Settings settings = Settings.builder() .put("enabled", "true") .put("signing_key", "U3VwZXJTZWNyZXRLZXlUaGF0SXNFeGFjdGx5NjRCeXRlc0xvbmdBbmRXaWxsV29ya1dpdGhIUzUxMkFsZ29yaXRobSEhCgo=") @@ -48,7 +54,9 @@ public void setUp() { .build(); authenticator = new ApiTokenAuthenticator(settings, "opensearch-cluster"); - cache = ApiTokenIndexListenerCache.getInstance(); + authenticator.log = log; + when(log.isDebugEnabled()).thenReturn(true); + threadcontext = new ThreadContext(Settings.EMPTY); } @Test @@ -61,39 +69,124 @@ public void testAuthenticationFailsWhenJtiNotInCache() { when(request.header("Authorization")).thenReturn("Bearer " + testJti); when(request.path()).thenReturn("/test"); - ThreadContext threadContext = new ThreadContext(Settings.EMPTY); - - AuthCredentials credentials = authenticator.extractCredentials(request, threadContext); + AuthCredentials credentials = authenticator.extractCredentials(request, threadcontext); - // It should return null when JTI is not in cache assertNull("Should return null when JTI is not in allowlist cache", credentials); } @Test public void testExtractCredentialsPassWhenJtiInCache() { - // Given: A JTI that is in the cache String testJti = - "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ0ZXN0LXRva2VuIiwiYXVkIjoidGVzdC10b2tlbiIsIm5iZiI6MTczNTMxODI5NywiaXAiOiJnaGlWTXVnWlBtcHZJMjZIT2hmUTRnaEJ1Qkh0Y2x6c1REYVlxQjVBRklyTkE4SzJCVTdxc2toMVBCOEMzQWpTdVBaREM0THVSM2pjZkdpLzlkU2ZicDBuQTNGMkhtSi9jaDA3cDY2ZWJ6OD0iLCJpc3MiOiJvcGVuc2VhcmNoLWNsdXN0ZXIiLCJleHAiOjE3Mzc5MTAyOTcsImlhdCI6MTczNTMxODI5NywiY3AiOiI5T0tzeGhUWmdaZjlTdWpKa0cxV1ViM1QvUVU2eGJmU3Noa25ZZFVIa2hzPSJ9.xdoDZiGBbqaqcH2evoMEV5384oTyRg04_gO3akQpO4c502c8bV8W5TF_5SxUvkXKDeuQEBFH-4c44VVhCnUQIw"; + "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ0ZXN0LXRva2VuIiwiYXVkIjoidGVzdC10b2tlbiIsIm5iZiI6MTczNTMyNjM0NywiaXAiOiJnaGlWTXVnWlBtcHZJMjZIT2hmUTRnaEJ1Qkh0Y2x6c1REYVlxQjVBRklyTkE4SzJCVTdxc2toMVBCOEMzQWpTdVBaREM0THVSM2pjZkdpLzlkU2ZicDBuQTNGMkhtSi9jaDA3cDY2ZWJ6OD0iLCJpc3MiOiJvcGVuc2VhcmNoLWNsdXN0ZXIiLCJleHAiOjIyMTg3NjU2NDMzMCwiaWF0IjoxNzM1MzI2MzQ3LCJjcCI6IjlPS3N4aFRaZ1pmOVN1akprRzFXVWIzVC9RVTZ4YmZTc2hrbllkVUhraHM9In0.kqMSnn5YwhLmeiI_8iIBQ5uhmI52n2MNniAa52Zpfs3TiE_PXKiNbDNs08hNqzGYW772gT7lfvp6kZnFxQ4v2Q"; String encryptedTestJti = - "k3JQNRXR57Y4V4W1LNkpEP+QzcDra5+/fFfQrr0Rncr/RhYAnYEDgN9RKcJ2Wmjf63NCQa9+HjeFcn0H2wKhMm7pl9bd3ya9FO+LUD7t9Sj+IKBsThVo93sUmnxJh/llglMsKsKQVkuY+YKa6A6dT8bMqmt7kIrer7w8TRENr9J8x41TGb/cDDWDvJLME7QkFzJjMxYDgKNiEevMbOpC8yjIZdK08jPe3Thq+xm+JYruoYeyI5g8QjkJA9ZOs1f6eXTAvPxhseuPqgIKykRE25fuWjl5n9tJ9W+jpl+iET7zzOLXSPEU5UepL/COkVd6xW63Ay72oMOewqveDXzyR8S8LAfgRuKgYZWms7yT37XcGg0c6Y7M62KVPo+1XQ+FGLtty3eDKwaSopFqLNcISFMiPml9XYv7V1AndJGINbH4KUDyeSQYUh4d+sOxjg9prGzW0nvKE22jzyQlW9t0wpDiB0visInvKVZAqKLPUp0x0pFbAVV12sJJkw6DFkD6+VL+8d2L/Z8kxJXO3uHHjhO3u3RWAe6UhLGncLhJciH57MEw8zFdNturr+tJREL5WbWyiEzKTOBzO8R5Ec92XyCDshIXzVxQv/QOM5meFxPcrkBAgKa6ztWCCmQqa2M1MdKkwKUGn3w6ixOTZ55nZQ=="; + "k3JQNRXR57Y4V4W1LNkpEP+QzcDra5+/fFfQrr0Rncr/RhYAnYEDgN9RKcJ2Wmjf63NCQa9+HjeFcn0H2wKhMm7pl9bd3ya9FO+LUD7t9Sih4DOjUt0t7ee4ROC0eRK5glMsKsKQVkuY+YKa6A6dT8bMqmt7kIrer7w8TRENr9J8x41TGb/cDDWDvJLME7QkFzJjMxYDgKNiEevMbOpC8yjIZdK08jPe3Thq+xm+JYruoYeyI5g8QjkJA9ZOs1f6eXTAvPxhseuPqgIKykRE25fuWjl5n9tJ9W+jpl+iET7zzOLXSPEU5UepL/COkVd6xW63Ay72oMOewqveDXzyR8S8LAfgRuKgYZWms7yT37XcGg0c6Y7M62KVPo+1XQ+F+K5bgddkd8G+I9KHf561jIMzBcIodgGRj659954W16D1C92+PF/YWPQoTv2hVK4f60H82ga1YSiz3r9UrFV8d7gLJwtyJT9HNPuXO2VZ7xPhre+n1Wv7No0kH2S/r3nqKK6Bk/kn1ZbAmjLxuw13c95lIir6avlKE7XX4PiQDfcGeAyeXOw/36kLW8wH7kjXWdBspld1AiI4fCOaszNXF+7gcuTxIhECl+mEyrJbMI88EWllq+LbydiOrVLFXXRMiCbvj+VTYjzimgJPp+Vuvg=="; ApiTokenIndexListenerCache cache = ApiTokenIndexListenerCache.getInstance(); cache.getJtis().add(encryptedTestJti); assertTrue(cache.getJtis().contains(encryptedTestJti)); - // Create a mock request with the JWT token and path SecurityRequest request = mock(SecurityRequest.class); when(request.header("Authorization")).thenReturn("Bearer " + testJti); when(request.path()).thenReturn("/test"); - // Create ThreadContext - Settings settings = Settings.builder().build(); - ThreadContext threadContext = new ThreadContext(settings); + AuthCredentials ac = authenticator.extractCredentials(request, threadcontext); - AuthCredentials ac = authenticator.extractCredentials(request, threadContext); + assertNotNull("Should not be null when JTI is in allowlist cache", ac); + } + + @Test + public void testExtractCredentialsFailWhenTokenIsExpired() { + String testJti = + "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ0ZXN0LXRva2VuIiwiYXVkIjoidGVzdC10b2tlbiIsIm5iZiI6MTczNTMyNjU4MiwiaXAiOiJnaGlWTXVnWlBtcHZJMjZIT2hmUTRnaEJ1Qkh0Y2x6c1REYVlxQjVBRklyTkE4SzJCVTdxc2toMVBCOEMzQWpTdVBaREM0THVSM2pjZkdpLzlkU2ZicDBuQTNGMkhtSi9jaDA3cDY2ZWJ6OD0iLCJpc3MiOiJvcGVuc2VhcmNoLWNsdXN0ZXIiLCJleHAiOjI5MDI5NDksImlhdCI6MTczNTMyNjU4MiwiY3AiOiI5T0tzeGhUWmdaZjlTdWpKa0cxV1ViM1QvUVU2eGJmU3Noa25ZZFVIa2hzPSJ9.-f45IAU4jE8EbDuthsPFm-TxtJCk8Q_uRmnG4sEkfLtjmp8mHUbSaS109YRGxKDVr3uEMgFwvkSKEFt7DHhf9A"; + String encryptedTestJti = + "k3JQNRXR57Y4V4W1LNkpEP+QzcDra5+/fFfQrr0Rncr/RhYAnYEDgN9RKcJ2Wmjf63NCQa9+HjeFcn0H2wKhMm7pl9bd3ya9FO+LUD7t9ShsbOyBUkpFSVuQwrXLatY+glMsKsKQVkuY+YKa6A6dT8bMqmt7kIrer7w8TRENr9J8x41TGb/cDDWDvJLME7QkFzJjMxYDgKNiEevMbOpC8yjIZdK08jPe3Thq+xm+JYruoYeyI5g8QjkJA9ZOs1f6eXTAvPxhseuPqgIKykRE25fuWjl5n9tJ9W+jpl+iET7zzOLXSPEU5UepL/COkVd6xW63Ay72oMOewqveDXzyR8S8LAfgRuKgYZWms7yT37XcGg0c6Y7M62KVPo+1XQ+Fu193YtvS4vqt9G8jHiq51VCRxNHYVlAsratxzvECD8AKBilR9/7dUKyOQDBIzPG4ws+kgI680SgdMgGuLANQPGzal9US8GsWzTbQWCgtObaSVKB02U4gh16wvy3XrXtPz2Z0ZAxoU2Z8opX8hcvB5MG5UUEf+tpgTtVPcbuJyCL42yD3FIc3v/LCYlG/hFvflXBx5c1r+4Tij8Qc/NkYb7/03xiJsVH6eduSqR9M0QBpLm7xg2TgqVMvC/+n96x2V3lS4via4lAK6xuYeRY0ng=="; + ApiTokenIndexListenerCache cache = ApiTokenIndexListenerCache.getInstance(); + cache.getJtis().add(encryptedTestJti); + assertTrue(cache.getJtis().contains(encryptedTestJti)); + + SecurityRequest request = mock(SecurityRequest.class); + when(request.header("Authorization")).thenReturn("Bearer " + testJti); + when(request.path()).thenReturn("/test"); + + AuthCredentials ac = authenticator.extractCredentials(request, threadcontext); + + assertNull("Should return null when JTI is expired", ac); + verify(log).debug(eq("Invalid or expired JWT token."), any(ExpiredJwtException.class)); + + } + + @Test + public void testExtractCredentialsFailWhenIssuerDoesNotMatch() { + String testJti = + "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ0ZXN0LXRva2VuIiwiYXVkIjoidGVzdC10b2tlbiIsIm5iZiI6MTczNTMyNjM0NywiaXAiOiJnaGlWTXVnWlBtcHZJMjZIT2hmUTRnaEJ1Qkh0Y2x6c1REYVlxQjVBRklyTkE4SzJCVTdxc2toMVBCOEMzQWpTdVBaREM0THVSM2pjZkdpLzlkU2ZicDBuQTNGMkhtSi9jaDA3cDY2ZWJ6OD0iLCJpc3MiOiJvcGVuc2VhcmNoLWNsdXN0ZXIiLCJleHAiOjIyMTg3NjU2NDMzMCwiaWF0IjoxNzM1MzI2MzQ3LCJjcCI6IjlPS3N4aFRaZ1pmOVN1akprRzFXVWIzVC9RVTZ4YmZTc2hrbllkVUhraHM9In0.kqMSnn5YwhLmeiI_8iIBQ5uhmI52n2MNniAa52Zpfs3TiE_PXKiNbDNs08hNqzGYW772gT7lfvp6kZnFxQ4v2Q"; + String encryptedTestJti = + "k3JQNRXR57Y4V4W1LNkpEP+QzcDra5+/fFfQrr0Rncr/RhYAnYEDgN9RKcJ2Wmjf63NCQa9+HjeFcn0H2wKhMm7pl9bd3ya9FO+LUD7t9Sih4DOjUt0t7ee4ROC0eRK5glMsKsKQVkuY+YKa6A6dT8bMqmt7kIrer7w8TRENr9J8x41TGb/cDDWDvJLME7QkFzJjMxYDgKNiEevMbOpC8yjIZdK08jPe3Thq+xm+JYruoYeyI5g8QjkJA9ZOs1f6eXTAvPxhseuPqgIKykRE25fuWjl5n9tJ9W+jpl+iET7zzOLXSPEU5UepL/COkVd6xW63Ay72oMOewqveDXzyR8S8LAfgRuKgYZWms7yT37XcGg0c6Y7M62KVPo+1XQ+F+K5bgddkd8G+I9KHf561jIMzBcIodgGRj659954W16D1C92+PF/YWPQoTv2hVK4f60H82ga1YSiz3r9UrFV8d7gLJwtyJT9HNPuXO2VZ7xPhre+n1Wv7No0kH2S/r3nqKK6Bk/kn1ZbAmjLxuw13c95lIir6avlKE7XX4PiQDfcGeAyeXOw/36kLW8wH7kjXWdBspld1AiI4fCOaszNXF+7gcuTxIhECl+mEyrJbMI88EWllq+LbydiOrVLFXXRMiCbvj+VTYjzimgJPp+Vuvg=="; + ApiTokenIndexListenerCache cache = ApiTokenIndexListenerCache.getInstance(); + cache.getJtis().add(encryptedTestJti); + assertTrue(cache.getJtis().contains(encryptedTestJti)); + + SecurityRequest request = mock(SecurityRequest.class); + when(request.header("Authorization")).thenReturn("Bearer " + testJti); + when(request.path()).thenReturn("/test"); + + Settings settings = Settings.builder() + .put("enabled", "true") + .put("signing_key", "U3VwZXJTZWNyZXRLZXlUaGF0SXNFeGFjdGx5NjRCeXRlc0xvbmdBbmRXaWxsV29ya1dpdGhIUzUxMkFsZ29yaXRobSEhCgo=") + .put("encryption_key", "MTIzNDU2Nzg5MDEyMzQ1Ng==") + .build(); + + authenticator = new ApiTokenAuthenticator(settings, "opensearch-cluster-name-mismatch"); + authenticator.log = log; + + AuthCredentials ac = authenticator.extractCredentials(request, threadcontext); + + assertNull("Should return null when issuer does not match cluster", ac); + verify(log).error(eq("The issuer of this api token does not match the current cluster identifier")); + } + + @Test + public void testExtractCredentialsFailWhenAccessingRestrictedEndpoint() { + String testJti = + "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ0ZXN0LXRva2VuIiwiYXVkIjoidGVzdC10b2tlbiIsIm5iZiI6MTczNTMyNjM0NywiaXAiOiJnaGlWTXVnWlBtcHZJMjZIT2hmUTRnaEJ1Qkh0Y2x6c1REYVlxQjVBRklyTkE4SzJCVTdxc2toMVBCOEMzQWpTdVBaREM0THVSM2pjZkdpLzlkU2ZicDBuQTNGMkhtSi9jaDA3cDY2ZWJ6OD0iLCJpc3MiOiJvcGVuc2VhcmNoLWNsdXN0ZXIiLCJleHAiOjIyMTg3NjU2NDMzMCwiaWF0IjoxNzM1MzI2MzQ3LCJjcCI6IjlPS3N4aFRaZ1pmOVN1akprRzFXVWIzVC9RVTZ4YmZTc2hrbllkVUhraHM9In0.kqMSnn5YwhLmeiI_8iIBQ5uhmI52n2MNniAa52Zpfs3TiE_PXKiNbDNs08hNqzGYW772gT7lfvp6kZnFxQ4v2Q"; + String encryptedTestJti = + "k3JQNRXR57Y4V4W1LNkpEP+QzcDra5+/fFfQrr0Rncr/RhYAnYEDgN9RKcJ2Wmjf63NCQa9+HjeFcn0H2wKhMm7pl9bd3ya9FO+LUD7t9Sih4DOjUt0t7ee4ROC0eRK5glMsKsKQVkuY+YKa6A6dT8bMqmt7kIrer7w8TRENr9J8x41TGb/cDDWDvJLME7QkFzJjMxYDgKNiEevMbOpC8yjIZdK08jPe3Thq+xm+JYruoYeyI5g8QjkJA9ZOs1f6eXTAvPxhseuPqgIKykRE25fuWjl5n9tJ9W+jpl+iET7zzOLXSPEU5UepL/COkVd6xW63Ay72oMOewqveDXzyR8S8LAfgRuKgYZWms7yT37XcGg0c6Y7M62KVPo+1XQ+F+K5bgddkd8G+I9KHf561jIMzBcIodgGRj659954W16D1C92+PF/YWPQoTv2hVK4f60H82ga1YSiz3r9UrFV8d7gLJwtyJT9HNPuXO2VZ7xPhre+n1Wv7No0kH2S/r3nqKK6Bk/kn1ZbAmjLxuw13c95lIir6avlKE7XX4PiQDfcGeAyeXOw/36kLW8wH7kjXWdBspld1AiI4fCOaszNXF+7gcuTxIhECl+mEyrJbMI88EWllq+LbydiOrVLFXXRMiCbvj+VTYjzimgJPp+Vuvg=="; + ApiTokenIndexListenerCache cache = ApiTokenIndexListenerCache.getInstance(); + cache.getJtis().add(encryptedTestJti); + assertTrue(cache.getJtis().contains(encryptedTestJti)); - // Verify the exception message if needed - assertNotNull("Should return null when JTI is not in allowlist cache", ac); + SecurityRequest request = mock(SecurityRequest.class); + when(request.header("Authorization")).thenReturn("Bearer " + testJti); + when(request.path()).thenReturn("/_plugins/_security/api/apitokens"); + + AuthCredentials ac = authenticator.extractCredentials(request, threadcontext); + + assertNull("Should return null when JTI is being used to access restricted endpoint", ac); + verify(log).error("OpenSearchException[Api Tokens are not allowed to be used for accessing this endpoint.]"); } + @Test + public void testAuthenticatorNotEnabled() { + String encryptedTestJti = + "k3JQNRXR57Y4V4W1LNkpEP+QzcDra5+/fFfQrr0Rncr/RhYAnYEDgN9RKcJ2Wmjf63NCQa9+HjeFcn0H2wKhMm7pl9bd3ya9FO+LUD7t9Sih4DOjUt0t7ee4ROC0eRK5glMsKsKQVkuY+YKa6A6dT8bMqmt7kIrer7w8TRENr9J8x41TGb/cDDWDvJLME7QkFzJjMxYDgKNiEevMbOpC8yjIZdK08jPe3Thq+xm+JYruoYeyI5g8QjkJA9ZOs1f6eXTAvPxhseuPqgIKykRE25fuWjl5n9tJ9W+jpl+iET7zzOLXSPEU5UepL/COkVd6xW63Ay72oMOewqveDXzyR8S8LAfgRuKgYZWms7yT37XcGg0c6Y7M62KVPo+1XQ+F+K5bgddkd8G+I9KHf561jIMzBcIodgGRj659954W16D1C92+PF/YWPQoTv2hVK4f60H82ga1YSiz3r9UrFV8d7gLJwtyJT9HNPuXO2VZ7xPhre+n1Wv7No0kH2S/r3nqKK6Bk/kn1ZbAmjLxuw13c95lIir6avlKE7XX4PiQDfcGeAyeXOw/36kLW8wH7kjXWdBspld1AiI4fCOaszNXF+7gcuTxIhECl+mEyrJbMI88EWllq+LbydiOrVLFXXRMiCbvj+VTYjzimgJPp+Vuvg=="; + ApiTokenIndexListenerCache cache = ApiTokenIndexListenerCache.getInstance(); + cache.getJtis().add(encryptedTestJti); + assertTrue(cache.getJtis().contains(encryptedTestJti)); + + SecurityRequest request = mock(SecurityRequest.class); + + Settings settings = Settings.builder() + .put("enabled", "false") + .put("signing_key", "U3VwZXJTZWNyZXRLZXlUaGF0SXNFeGFjdGx5NjRCeXRlc0xvbmdBbmRXaWxsV29ya1dpdGhIUzUxMkFsZ29yaXRobSEhCgo=") + .put("encryption_key", "MTIzNDU2Nzg5MDEyMzQ1Ng==") + .build(); + ThreadContext threadContext = new ThreadContext(settings); + + authenticator = new ApiTokenAuthenticator(settings, "opensearch-cluster-name-mismatch"); + authenticator.log = log; + + AuthCredentials ac = authenticator.extractCredentials(request, threadContext); + + assertNull("Should return null when api tokens auth is not enabled", ac); + verify(log).error(eq("Api token authentication is disabled")); + } } diff --git a/src/test/java/org/opensearch/security/authtoken/jwt/AuthTokenUtilsTest.java b/src/test/java/org/opensearch/security/authtoken/jwt/AuthTokenUtilsTest.java index e0026155de..2ab7b9da8e 100644 --- a/src/test/java/org/opensearch/security/authtoken/jwt/AuthTokenUtilsTest.java +++ b/src/test/java/org/opensearch/security/authtoken/jwt/AuthTokenUtilsTest.java @@ -27,6 +27,17 @@ public class AuthTokenUtilsTest { + @Test + public void testIsAccessToRestrictedEndpointsForApiToken() { + NamedXContentRegistry namedXContentRegistry = new NamedXContentRegistry(Collections.emptyList()); + + FakeRestRequest request = new FakeRestRequest.Builder(namedXContentRegistry).withPath("/api/apitokens") + .withMethod(RestRequest.Method.POST) + .build(); + + assertTrue(AuthTokenUtils.isAccessToRestrictedEndpoints(SecurityRequestFactory.from(request), "api/generateonbehalfoftoken")); + } + @Test public void testIsAccessToRestrictedEndpointsForOnBehalfOfToken() { NamedXContentRegistry namedXContentRegistry = new NamedXContentRegistry(Collections.emptyList()); From 665b9e916a3b43561f3ec2111040e7c1aa6e0b78 Mon Sep 17 00:00:00 2001 From: Derek Ho Date: Mon, 30 Dec 2024 15:17:54 -0500 Subject: [PATCH 07/20] Directly store permissions in the cache Signed-off-by: Derek Ho --- .../apitokens/ApiTokenIndexListenerCache.java | 10 +-- .../action/apitokens/Permissions.java | 40 +++++++++++ .../security/filter/SecurityRestFilter.java | 3 - .../security/http/ApiTokenAuthenticator.java | 69 ++----------------- .../security/privileges/ActionPrivileges.java | 54 +++++++++++---- .../PrivilegesEvaluationContext.java | 23 ++----- .../privileges/PrivilegesEvaluator.java | 17 ----- .../apitokens/ApiTokenAuthenticatorTest.java | 22 +++--- 8 files changed, 106 insertions(+), 132 deletions(-) create mode 100644 src/main/java/org/opensearch/security/action/apitokens/Permissions.java diff --git a/src/main/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCache.java b/src/main/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCache.java index 68ec995c60..8b87f2fa03 100644 --- a/src/main/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCache.java +++ b/src/main/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCache.java @@ -9,8 +9,7 @@ package org.opensearch.security.action.apitokens; import java.io.IOException; -import java.util.HashSet; -import java.util.Set; +import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.apache.logging.log4j.LogManager; @@ -36,7 +35,7 @@ public class ApiTokenIndexListenerCache implements IndexingOperationListener { private static final ApiTokenIndexListenerCache INSTANCE = new ApiTokenIndexListenerCache(); private final ConcurrentHashMap idToJtiMap = new ConcurrentHashMap<>(); - private Set jtis = new HashSet(); + private Map jtis = new ConcurrentHashMap<>(); private boolean initialized; @@ -81,7 +80,7 @@ public void postIndex(ShardId shardId, Engine.Index index, Engine.IndexResult re .createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, sourceRef.streamInput()); ApiToken token = ApiToken.fromXContent(parser); - jtis.add(token.getJti()); + jtis.put(token.getJti(), new Permissions(token.getClusterPermissions(), token.getIndexPermissions())); idToJtiMap.put(index.id(), token.getJti()); } catch (IOException e) { @@ -106,7 +105,8 @@ public void postDelete(ShardId shardId, Engine.Delete delete, Engine.DeleteResul } } - public Set getJtis() { + public Map getJtis() { return jtis; } + } diff --git a/src/main/java/org/opensearch/security/action/apitokens/Permissions.java b/src/main/java/org/opensearch/security/action/apitokens/Permissions.java new file mode 100644 index 0000000000..cb1478b9ae --- /dev/null +++ b/src/main/java/org/opensearch/security/action/apitokens/Permissions.java @@ -0,0 +1,40 @@ +/* + * 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. + */ + +package org.opensearch.security.action.apitokens; + +import java.util.List; + +public class Permissions { + private List clusterPerm; + private List indexPermission; + + // Constructor + public Permissions(List clusterPerm, List indexPermission) { + this.clusterPerm = clusterPerm; + this.indexPermission = indexPermission; + } + + // Getters and setters + public List getClusterPerm() { + return clusterPerm; + } + + public void setClusterPerm(List clusterPerm) { + this.clusterPerm = clusterPerm; + } + + public List getIndexPermission() { + return indexPermission; + } + + public void setIndexPermission(List indexPermission) { + this.indexPermission = indexPermission; + } + +} diff --git a/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java b/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java index c214075a42..c9d10ee2fa 100644 --- a/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java +++ b/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java @@ -76,8 +76,6 @@ public class SecurityRestFilter { protected final Logger log = LogManager.getLogger(this.getClass()); - public static final String API_TOKEN_CLUSTERPERM_KEY = "security.api_token.clusterperm"; - public static final String API_TOKEN_INDEXPERM_KEY = "security.api_token.indexactions"; private final BackendRegistry registry; private final RestLayerPrivilegesEvaluator evaluator; private final AuditLog auditLog; @@ -234,7 +232,6 @@ void authorizeRequest(RestHandler original, SecurityRequestChannel request, User .addAll(route.actionNames() != null ? route.actionNames() : Collections.emptySet()) .add(route.name()) .build(); - pres = evaluator.evaluate(user, route.name(), actionNames); if (log.isDebugEnabled()) { diff --git a/src/main/java/org/opensearch/security/http/ApiTokenAuthenticator.java b/src/main/java/org/opensearch/security/http/ApiTokenAuthenticator.java index 3b5715ecf0..61ba2cd8e3 100644 --- a/src/main/java/org/opensearch/security/http/ApiTokenAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/ApiTokenAuthenticator.java @@ -11,10 +11,8 @@ package org.opensearch.security.http; -import java.io.IOException; import java.security.AccessController; import java.security.PrivilegedAction; -import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.regex.Matcher; @@ -29,11 +27,6 @@ import org.opensearch.SpecialPermission; import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; -import org.opensearch.common.xcontent.XContentType; -import org.opensearch.core.xcontent.DeprecationHandler; -import org.opensearch.core.xcontent.NamedXContentRegistry; -import org.opensearch.core.xcontent.XContentParser; -import org.opensearch.security.action.apitokens.ApiToken; import org.opensearch.security.action.apitokens.ApiTokenIndexListenerCache; import org.opensearch.security.auth.HTTPAuthenticator; import org.opensearch.security.authtoken.jwt.EncryptionDecryptionUtil; @@ -50,8 +43,6 @@ import static org.opensearch.security.OpenSearchSecurityPlugin.LEGACY_OPENDISTRO_PREFIX; import static org.opensearch.security.OpenSearchSecurityPlugin.PLUGINS_PREFIX; -import static org.opensearch.security.filter.SecurityRestFilter.API_TOKEN_CLUSTERPERM_KEY; -import static org.opensearch.security.filter.SecurityRestFilter.API_TOKEN_INDEXPERM_KEY; import static org.opensearch.security.util.AuthTokenUtils.isAccessToRestrictedEndpoints; public class ApiTokenAuthenticator implements HTTPAuthenticator { @@ -113,49 +104,6 @@ private JwtParserBuilder initParserBuilder(final String signingKey) { return jwtParserBuilder; } - private String extractClusterPermissionsFromClaims(Claims claims) { - Object cp = claims.get("cp"); - String clusterPermissions = ""; - - if (cp != null) { - clusterPermissions = encryptionUtil.decrypt(cp.toString()); - } else { - log.warn("This is a malformed Api Token"); - } - - return clusterPermissions; - } - - private List extractIndexPermissionFromClaims(Claims claims) throws IOException { - Object ip = claims.get("ip"); - - if (ip != null) { - String decryptedPermissions = encryptionUtil.decrypt(ip.toString()); - - try ( - XContentParser parser = XContentType.JSON.xContent() - .createParser(NamedXContentRegistry.EMPTY, DeprecationHandler.THROW_UNSUPPORTED_OPERATION, decryptedPermissions) - ) { - - // Use built-in array parsing - List permissions = new ArrayList<>(); - - // Move to start of array - parser.nextToken(); // START_ARRAY - while (parser.nextToken() != XContentParser.Token.END_ARRAY) { - permissions.add(ApiToken.IndexPermission.fromXContent(parser)); - } - return permissions; - } catch (Exception e) { - log.error("Error extracting index permissions", e); - return List.of(); - } - - } - - return List.of(); - } - @Override @SuppressWarnings("removal") public AuthCredentials extractCredentials(final SecurityRequest request, final ThreadContext context) @@ -193,8 +141,8 @@ private AuthCredentials extractCredentials0(final SecurityRequest request, final } // TODO: handle revocation different from deletion? - if (!cache.getJtis().contains(encryptionUtil.encrypt(jwtToken))) { - log.debug("Token is not allowlisted"); + if (!cache.getJtis().containsKey(encryptionUtil.encrypt(jwtToken))) { + log.error("Token is not allowlisted"); return null; } @@ -213,13 +161,8 @@ private AuthCredentials extractCredentials0(final SecurityRequest request, final return null; } - String clusterPermissions = extractClusterPermissionsFromClaims(claims); - List indexPermissions = extractIndexPermissionFromClaims(claims); - - final AuthCredentials ac = new AuthCredentials(subject, List.of(), "").markComplete(); - - context.putTransient(API_TOKEN_CLUSTERPERM_KEY, clusterPermissions); - context.putTransient(API_TOKEN_INDEXPERM_KEY, indexPermissions); + final AuthCredentials ac = new AuthCredentials("apitoken_" + subject + ":" + encryptionUtil.encrypt(jwtToken), List.of(), "") + .markComplete(); return ac; @@ -227,9 +170,7 @@ private AuthCredentials extractCredentials0(final SecurityRequest request, final log.error("Cannot authenticate user with JWT because of ", e); return null; } catch (Exception e) { - if (log.isDebugEnabled()) { - log.debug("Invalid or expired JWT token.", e); - } + log.error("Invalid or expired JWT token.", e); } // Return null for the authentication failure diff --git a/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java b/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java index 56c798f6f5..b9ff9f125e 100644 --- a/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java +++ b/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java @@ -165,17 +165,23 @@ public PrivilegesEvaluatorResponse hasIndexPrivilege( // API Token Authz // TODO: this is very naive implementation - if (context.getIndexPermissions() != null) { - List indexPermissions = context.getIndexPermissions(); - - boolean hasPermission = indexPermissions.stream().anyMatch(permission -> { - boolean hasAllActions = new HashSet<>(permission.getAllowedActions()).containsAll(actions); - boolean hasAllIndices = new HashSet<>(permission.getIndexPatterns()).containsAll(resolvedIndices.getAllIndices()); - return hasAllActions && hasAllIndices; - }); - - if (hasPermission) { - return PrivilegesEvaluatorResponse.ok(); + if (context.getUser().getName().startsWith("apitoken")) { + String jti = context.getUser().getName().split(":")[1]; + if (context.getApiTokenIndexListenerCache().getJtis().get(jti) != null) { + List indexPermissions = context.getApiTokenIndexListenerCache() + .getJtis() + .get(jti) + .getIndexPermission(); + + boolean hasPermission = indexPermissions.stream().anyMatch(permission -> { + boolean hasAllActions = new HashSet<>(permission.getAllowedActions()).containsAll(actions); + boolean hasAllIndices = new HashSet<>(permission.getIndexPatterns()).containsAll(resolvedIndices.getAllIndices()); + return hasAllActions && hasAllIndices; + }); + + if (hasPermission) { + return PrivilegesEvaluatorResponse.ok(); + } } } @@ -426,8 +432,14 @@ PrivilegesEvaluatorResponse providesPrivilege(PrivilegesEvaluationContext contex } // 4: Evaluate api tokens - if (context.getClusterPermissions() != null && context.getClusterPermissions().contains(action)) { - return PrivilegesEvaluatorResponse.ok(); + if (context.getUser().getName().startsWith("apitoken")) { + String jti = context.getUser().getName().split(":")[1]; + log.info(context.getApiTokenIndexListenerCache().getJtis().get(jti).getClusterPerm().toString()); + + if (context.getApiTokenIndexListenerCache().getJtis().get(jti) != null + && context.getApiTokenIndexListenerCache().getJtis().get(jti).getClusterPerm().contains(action)) { + return PrivilegesEvaluatorResponse.ok(); + } } return PrivilegesEvaluatorResponse.insufficient(action); @@ -463,6 +475,14 @@ PrivilegesEvaluatorResponse providesExplicitPrivilege(PrivilegesEvaluationContex } } + if (context.getUser().getName().startsWith("apitoken")) { + String jti = context.getUser().getName().split(":")[1]; + if (context.getApiTokenIndexListenerCache().getJtis().get(jti) != null + && context.getApiTokenIndexListenerCache().getJtis().get(jti).getClusterPerm().contains(action)) { + return PrivilegesEvaluatorResponse.ok(); + } + } + return PrivilegesEvaluatorResponse.insufficient(action); } @@ -499,6 +519,14 @@ PrivilegesEvaluatorResponse providesAnyPrivilege(PrivilegesEvaluationContext con } } + if (context.getUser().getName().startsWith("apitoken")) { + String jti = context.getUser().getName().split(":")[1]; + if (context.getApiTokenIndexListenerCache().getJtis().get(jti) != null + && context.getApiTokenIndexListenerCache().getJtis().get(jti).getClusterPerm().stream().anyMatch(actions::contains)) { + return PrivilegesEvaluatorResponse.ok(); + } + } + if (actions.size() == 1) { return PrivilegesEvaluatorResponse.insufficient(actions.iterator().next()); } else { diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java index b41bc366da..c0352484da 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java @@ -11,7 +11,6 @@ package org.opensearch.security.privileges; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.function.Supplier; @@ -21,7 +20,7 @@ import org.opensearch.cluster.ClusterState; import org.opensearch.cluster.metadata.IndexAbstraction; import org.opensearch.cluster.metadata.IndexNameExpressionResolver; -import org.opensearch.security.action.apitokens.ApiToken; +import org.opensearch.security.action.apitokens.ApiTokenIndexListenerCache; import org.opensearch.security.resolver.IndexResolverReplacer; import org.opensearch.security.support.WildcardMatcher; import org.opensearch.security.user.User; @@ -47,9 +46,7 @@ public class PrivilegesEvaluationContext { private final IndexResolverReplacer indexResolverReplacer; private final IndexNameExpressionResolver indexNameExpressionResolver; private final Supplier clusterStateSupplier; - private List clusterPermissions; - private List indexPermissions; - + private final ApiTokenIndexListenerCache apiTokenIndexListenerCache = ApiTokenIndexListenerCache.getInstance(); /** * This caches the ready to use WildcardMatcher instances for the current request. Many index patterns have * to be executed several times per request (for example first for action privileges, later for DLS). Thus, @@ -177,19 +174,7 @@ public String toString() { + '}'; } - public void setClusterPermissions(List clusterPermissions) { - this.clusterPermissions = clusterPermissions; - } - - public List getClusterPermissions() { - return clusterPermissions; - } - - public List getIndexPermissions() { - return indexPermissions; - } - - public void setIndexPermissions(List indexPermissions) { - this.indexPermissions = indexPermissions; + public ApiTokenIndexListenerCache getApiTokenIndexListenerCache() { + return apiTokenIndexListenerCache; } } diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java index a5af967861..36666972ec 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java @@ -86,7 +86,6 @@ import org.opensearch.core.xcontent.NamedXContentRegistry; import org.opensearch.index.reindex.ReindexAction; import org.opensearch.script.mustache.RenderSearchTemplateAction; -import org.opensearch.security.action.apitokens.ApiToken; import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.configuration.ClusterInfoHolder; import org.opensearch.security.configuration.ConfigurationRepository; @@ -110,8 +109,6 @@ import org.greenrobot.eventbus.Subscribe; import static org.opensearch.security.OpenSearchSecurityPlugin.traceAction; -import static org.opensearch.security.filter.SecurityRestFilter.API_TOKEN_CLUSTERPERM_KEY; -import static org.opensearch.security.filter.SecurityRestFilter.API_TOKEN_INDEXPERM_KEY; import static org.opensearch.security.support.ConfigConstants.OPENDISTRO_SECURITY_USER_INFO_THREAD_CONTEXT; public class PrivilegesEvaluator { @@ -345,20 +342,6 @@ public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) context.setMappedRoles(mappedRoles); } - // Extract cluster and index permissions from the api token thread context - // TODO: Add decryption here to make sure it is not injectable by anyone? - // TODO: This is only a naive implementation that does not support * - final String apiTokenClusterPermissions = threadContext.getTransient(API_TOKEN_CLUSTERPERM_KEY); - if (apiTokenClusterPermissions != null) { - List clusterPermissions = Arrays.asList(apiTokenClusterPermissions.split(",")); - context.setClusterPermissions(clusterPermissions); - } - - final List apiTokenIndexPermissions = threadContext.getTransient(API_TOKEN_INDEXPERM_KEY); - if (apiTokenIndexPermissions != null) { - context.setIndexPermissions(apiTokenIndexPermissions); - } - // Add the security roles for this user so that they can be used for DLS parameter substitution. user.addSecurityRoles(mappedRoles); setUserInfoInThreadContext(user); diff --git a/src/test/java/org/opensearch/security/action/apitokens/ApiTokenAuthenticatorTest.java b/src/test/java/org/opensearch/security/action/apitokens/ApiTokenAuthenticatorTest.java index 67293e0347..93109b49d3 100644 --- a/src/test/java/org/opensearch/security/action/apitokens/ApiTokenAuthenticatorTest.java +++ b/src/test/java/org/opensearch/security/action/apitokens/ApiTokenAuthenticatorTest.java @@ -63,7 +63,7 @@ public void setUp() { public void testAuthenticationFailsWhenJtiNotInCache() { String testJti = "test-jti-not-in-cache"; ApiTokenIndexListenerCache cache = ApiTokenIndexListenerCache.getInstance(); - assertFalse(cache.getJtis().contains(testJti)); + assertFalse(cache.getJtis().containsKey(testJti)); SecurityRequest request = mock(SecurityRequest.class); when(request.header("Authorization")).thenReturn("Bearer " + testJti); @@ -81,8 +81,8 @@ public void testExtractCredentialsPassWhenJtiInCache() { String encryptedTestJti = "k3JQNRXR57Y4V4W1LNkpEP+QzcDra5+/fFfQrr0Rncr/RhYAnYEDgN9RKcJ2Wmjf63NCQa9+HjeFcn0H2wKhMm7pl9bd3ya9FO+LUD7t9Sih4DOjUt0t7ee4ROC0eRK5glMsKsKQVkuY+YKa6A6dT8bMqmt7kIrer7w8TRENr9J8x41TGb/cDDWDvJLME7QkFzJjMxYDgKNiEevMbOpC8yjIZdK08jPe3Thq+xm+JYruoYeyI5g8QjkJA9ZOs1f6eXTAvPxhseuPqgIKykRE25fuWjl5n9tJ9W+jpl+iET7zzOLXSPEU5UepL/COkVd6xW63Ay72oMOewqveDXzyR8S8LAfgRuKgYZWms7yT37XcGg0c6Y7M62KVPo+1XQ+F+K5bgddkd8G+I9KHf561jIMzBcIodgGRj659954W16D1C92+PF/YWPQoTv2hVK4f60H82ga1YSiz3r9UrFV8d7gLJwtyJT9HNPuXO2VZ7xPhre+n1Wv7No0kH2S/r3nqKK6Bk/kn1ZbAmjLxuw13c95lIir6avlKE7XX4PiQDfcGeAyeXOw/36kLW8wH7kjXWdBspld1AiI4fCOaszNXF+7gcuTxIhECl+mEyrJbMI88EWllq+LbydiOrVLFXXRMiCbvj+VTYjzimgJPp+Vuvg=="; ApiTokenIndexListenerCache cache = ApiTokenIndexListenerCache.getInstance(); - cache.getJtis().add(encryptedTestJti); - assertTrue(cache.getJtis().contains(encryptedTestJti)); + cache.getJtis().put(encryptedTestJti, null); + assertTrue(cache.getJtis().containsKey(encryptedTestJti)); SecurityRequest request = mock(SecurityRequest.class); when(request.header("Authorization")).thenReturn("Bearer " + testJti); @@ -100,8 +100,8 @@ public void testExtractCredentialsFailWhenTokenIsExpired() { String encryptedTestJti = "k3JQNRXR57Y4V4W1LNkpEP+QzcDra5+/fFfQrr0Rncr/RhYAnYEDgN9RKcJ2Wmjf63NCQa9+HjeFcn0H2wKhMm7pl9bd3ya9FO+LUD7t9ShsbOyBUkpFSVuQwrXLatY+glMsKsKQVkuY+YKa6A6dT8bMqmt7kIrer7w8TRENr9J8x41TGb/cDDWDvJLME7QkFzJjMxYDgKNiEevMbOpC8yjIZdK08jPe3Thq+xm+JYruoYeyI5g8QjkJA9ZOs1f6eXTAvPxhseuPqgIKykRE25fuWjl5n9tJ9W+jpl+iET7zzOLXSPEU5UepL/COkVd6xW63Ay72oMOewqveDXzyR8S8LAfgRuKgYZWms7yT37XcGg0c6Y7M62KVPo+1XQ+Fu193YtvS4vqt9G8jHiq51VCRxNHYVlAsratxzvECD8AKBilR9/7dUKyOQDBIzPG4ws+kgI680SgdMgGuLANQPGzal9US8GsWzTbQWCgtObaSVKB02U4gh16wvy3XrXtPz2Z0ZAxoU2Z8opX8hcvB5MG5UUEf+tpgTtVPcbuJyCL42yD3FIc3v/LCYlG/hFvflXBx5c1r+4Tij8Qc/NkYb7/03xiJsVH6eduSqR9M0QBpLm7xg2TgqVMvC/+n96x2V3lS4via4lAK6xuYeRY0ng=="; ApiTokenIndexListenerCache cache = ApiTokenIndexListenerCache.getInstance(); - cache.getJtis().add(encryptedTestJti); - assertTrue(cache.getJtis().contains(encryptedTestJti)); + cache.getJtis().put(encryptedTestJti, null); + assertTrue(cache.getJtis().containsKey(encryptedTestJti)); SecurityRequest request = mock(SecurityRequest.class); when(request.header("Authorization")).thenReturn("Bearer " + testJti); @@ -121,8 +121,8 @@ public void testExtractCredentialsFailWhenIssuerDoesNotMatch() { String encryptedTestJti = "k3JQNRXR57Y4V4W1LNkpEP+QzcDra5+/fFfQrr0Rncr/RhYAnYEDgN9RKcJ2Wmjf63NCQa9+HjeFcn0H2wKhMm7pl9bd3ya9FO+LUD7t9Sih4DOjUt0t7ee4ROC0eRK5glMsKsKQVkuY+YKa6A6dT8bMqmt7kIrer7w8TRENr9J8x41TGb/cDDWDvJLME7QkFzJjMxYDgKNiEevMbOpC8yjIZdK08jPe3Thq+xm+JYruoYeyI5g8QjkJA9ZOs1f6eXTAvPxhseuPqgIKykRE25fuWjl5n9tJ9W+jpl+iET7zzOLXSPEU5UepL/COkVd6xW63Ay72oMOewqveDXzyR8S8LAfgRuKgYZWms7yT37XcGg0c6Y7M62KVPo+1XQ+F+K5bgddkd8G+I9KHf561jIMzBcIodgGRj659954W16D1C92+PF/YWPQoTv2hVK4f60H82ga1YSiz3r9UrFV8d7gLJwtyJT9HNPuXO2VZ7xPhre+n1Wv7No0kH2S/r3nqKK6Bk/kn1ZbAmjLxuw13c95lIir6avlKE7XX4PiQDfcGeAyeXOw/36kLW8wH7kjXWdBspld1AiI4fCOaszNXF+7gcuTxIhECl+mEyrJbMI88EWllq+LbydiOrVLFXXRMiCbvj+VTYjzimgJPp+Vuvg=="; ApiTokenIndexListenerCache cache = ApiTokenIndexListenerCache.getInstance(); - cache.getJtis().add(encryptedTestJti); - assertTrue(cache.getJtis().contains(encryptedTestJti)); + cache.getJtis().put(encryptedTestJti, null); + assertTrue(cache.getJtis().containsKey(encryptedTestJti)); SecurityRequest request = mock(SecurityRequest.class); when(request.header("Authorization")).thenReturn("Bearer " + testJti); @@ -150,8 +150,8 @@ public void testExtractCredentialsFailWhenAccessingRestrictedEndpoint() { String encryptedTestJti = "k3JQNRXR57Y4V4W1LNkpEP+QzcDra5+/fFfQrr0Rncr/RhYAnYEDgN9RKcJ2Wmjf63NCQa9+HjeFcn0H2wKhMm7pl9bd3ya9FO+LUD7t9Sih4DOjUt0t7ee4ROC0eRK5glMsKsKQVkuY+YKa6A6dT8bMqmt7kIrer7w8TRENr9J8x41TGb/cDDWDvJLME7QkFzJjMxYDgKNiEevMbOpC8yjIZdK08jPe3Thq+xm+JYruoYeyI5g8QjkJA9ZOs1f6eXTAvPxhseuPqgIKykRE25fuWjl5n9tJ9W+jpl+iET7zzOLXSPEU5UepL/COkVd6xW63Ay72oMOewqveDXzyR8S8LAfgRuKgYZWms7yT37XcGg0c6Y7M62KVPo+1XQ+F+K5bgddkd8G+I9KHf561jIMzBcIodgGRj659954W16D1C92+PF/YWPQoTv2hVK4f60H82ga1YSiz3r9UrFV8d7gLJwtyJT9HNPuXO2VZ7xPhre+n1Wv7No0kH2S/r3nqKK6Bk/kn1ZbAmjLxuw13c95lIir6avlKE7XX4PiQDfcGeAyeXOw/36kLW8wH7kjXWdBspld1AiI4fCOaszNXF+7gcuTxIhECl+mEyrJbMI88EWllq+LbydiOrVLFXXRMiCbvj+VTYjzimgJPp+Vuvg=="; ApiTokenIndexListenerCache cache = ApiTokenIndexListenerCache.getInstance(); - cache.getJtis().add(encryptedTestJti); - assertTrue(cache.getJtis().contains(encryptedTestJti)); + cache.getJtis().put(encryptedTestJti, null); + assertTrue(cache.getJtis().containsKey(encryptedTestJti)); SecurityRequest request = mock(SecurityRequest.class); when(request.header("Authorization")).thenReturn("Bearer " + testJti); @@ -169,8 +169,8 @@ public void testAuthenticatorNotEnabled() { String encryptedTestJti = "k3JQNRXR57Y4V4W1LNkpEP+QzcDra5+/fFfQrr0Rncr/RhYAnYEDgN9RKcJ2Wmjf63NCQa9+HjeFcn0H2wKhMm7pl9bd3ya9FO+LUD7t9Sih4DOjUt0t7ee4ROC0eRK5glMsKsKQVkuY+YKa6A6dT8bMqmt7kIrer7w8TRENr9J8x41TGb/cDDWDvJLME7QkFzJjMxYDgKNiEevMbOpC8yjIZdK08jPe3Thq+xm+JYruoYeyI5g8QjkJA9ZOs1f6eXTAvPxhseuPqgIKykRE25fuWjl5n9tJ9W+jpl+iET7zzOLXSPEU5UepL/COkVd6xW63Ay72oMOewqveDXzyR8S8LAfgRuKgYZWms7yT37XcGg0c6Y7M62KVPo+1XQ+F+K5bgddkd8G+I9KHf561jIMzBcIodgGRj659954W16D1C92+PF/YWPQoTv2hVK4f60H82ga1YSiz3r9UrFV8d7gLJwtyJT9HNPuXO2VZ7xPhre+n1Wv7No0kH2S/r3nqKK6Bk/kn1ZbAmjLxuw13c95lIir6avlKE7XX4PiQDfcGeAyeXOw/36kLW8wH7kjXWdBspld1AiI4fCOaszNXF+7gcuTxIhECl+mEyrJbMI88EWllq+LbydiOrVLFXXRMiCbvj+VTYjzimgJPp+Vuvg=="; ApiTokenIndexListenerCache cache = ApiTokenIndexListenerCache.getInstance(); - cache.getJtis().add(encryptedTestJti); - assertTrue(cache.getJtis().contains(encryptedTestJti)); + cache.getJtis().put(encryptedTestJti, null); + assertTrue(cache.getJtis().containsKey(encryptedTestJti)); SecurityRequest request = mock(SecurityRequest.class); From e39df0d01ee58bbdb0f9238cb7c23acc6494fd07 Mon Sep 17 00:00:00 2001 From: Derek Ho Date: Mon, 30 Dec 2024 15:51:11 -0500 Subject: [PATCH 08/20] Remove permissions from jti Signed-off-by: Derek Ho --- .../action/apitokens/ApiTokenRepository.java | 2 +- .../security/authtoken/jwt/JwtVendor.java | 34 ++---------------- .../security/http/ApiTokenAuthenticator.java | 4 ++- .../identity/SecurityTokenManager.java | 11 ++---- .../apitokens/ApiTokenAuthenticatorTest.java | 12 ++++--- .../apitokens/ApiTokenRepositoryTest.java | 4 +-- .../security/authtoken/jwt/JwtVendorTest.java | 36 +------------------ .../identity/SecurityTokenManagerTest.java | 8 ++--- 8 files changed, 22 insertions(+), 89 deletions(-) diff --git a/src/main/java/org/opensearch/security/action/apitokens/ApiTokenRepository.java b/src/main/java/org/opensearch/security/action/apitokens/ApiTokenRepository.java index ce81aceb4b..be336f3582 100644 --- a/src/main/java/org/opensearch/security/action/apitokens/ApiTokenRepository.java +++ b/src/main/java/org/opensearch/security/action/apitokens/ApiTokenRepository.java @@ -49,7 +49,7 @@ public String createApiToken( ) { apiTokenIndexHandler.createApiTokenIndexIfAbsent(); // TODO: Add validation on whether user is creating a token with a subset of their permissions - ExpiringBearerAuthToken token = securityTokenManager.issueApiToken(name, expiration, clusterPermissions, indexPermissions); + ExpiringBearerAuthToken token = securityTokenManager.issueApiToken(name, expiration); ApiToken apiToken = new ApiToken( name, securityTokenManager.encryptToken(token.getCompleteToken()), diff --git a/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java b/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java index 575bba7964..0c91b3c093 100644 --- a/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java +++ b/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java @@ -11,7 +11,6 @@ package org.opensearch.security.authtoken.jwt; -import java.io.IOException; import java.security.AccessController; import java.security.PrivilegedAction; import java.text.ParseException; @@ -27,10 +26,6 @@ import org.opensearch.OpenSearchException; import org.opensearch.common.collect.Tuple; import org.opensearch.common.settings.Settings; -import org.opensearch.common.xcontent.XContentFactory; -import org.opensearch.core.xcontent.ToXContent; -import org.opensearch.core.xcontent.XContentBuilder; -import org.opensearch.security.action.apitokens.ApiToken; import com.nimbusds.jose.JOSEException; import com.nimbusds.jose.JWSAlgorithm; @@ -157,14 +152,8 @@ public ExpiringBearerAuthToken createJwt( } @SuppressWarnings("removal") - public ExpiringBearerAuthToken createJwt( - final String issuer, - final String subject, - final String audience, - final long expiration, - final List clusterPermissions, - final List indexPermissions - ) throws JOSEException, ParseException, IOException { + public ExpiringBearerAuthToken createJwt(final String issuer, final String subject, final String audience, final long expiration) + throws JOSEException, ParseException { final long currentTimeMs = timeProvider.getAsLong(); final Date now = new Date(currentTimeMs); @@ -178,25 +167,6 @@ public ExpiringBearerAuthToken createJwt( final Date expiryTime = new Date(expiration); claimsBuilder.expirationTime(expiryTime); - if (clusterPermissions != null) { - final String listOfClusterPermissions = String.join(",", clusterPermissions); - claimsBuilder.claim("cp", encryptString(listOfClusterPermissions)); - } - - if (indexPermissions != null) { - XContentBuilder builder = XContentFactory.jsonBuilder(); - builder.startArray(); - for (ApiToken.IndexPermission permission : indexPermissions) { - // Add each permission to the array - permission.toXContent(builder, ToXContent.EMPTY_PARAMS); - } - builder.endArray(); - - // Encrypt the entire JSON array - String jsonArray = builder.toString(); - claimsBuilder.claim("ip", encryptString(jsonArray)); - } - final JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.parse(signingKey.getAlgorithm().getName())).build(); final SignedJWT signedJwt = AccessController.doPrivileged( diff --git a/src/main/java/org/opensearch/security/http/ApiTokenAuthenticator.java b/src/main/java/org/opensearch/security/http/ApiTokenAuthenticator.java index 61ba2cd8e3..0da8d5447d 100644 --- a/src/main/java/org/opensearch/security/http/ApiTokenAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/ApiTokenAuthenticator.java @@ -170,7 +170,9 @@ private AuthCredentials extractCredentials0(final SecurityRequest request, final log.error("Cannot authenticate user with JWT because of ", e); return null; } catch (Exception e) { - log.error("Invalid or expired JWT token.", e); + if (log.isDebugEnabled()) { + log.debug("Invalid or expired JWT token.", e); + } } // Return null for the authentication failure diff --git a/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java b/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java index ca5a17b6f7..aeee248f25 100644 --- a/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java +++ b/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java @@ -12,7 +12,6 @@ package org.opensearch.security.identity; import java.util.ArrayList; -import java.util.List; import java.util.Optional; import java.util.Set; @@ -28,7 +27,6 @@ import org.opensearch.identity.tokens.AuthToken; import org.opensearch.identity.tokens.OnBehalfOfClaims; import org.opensearch.identity.tokens.TokenManager; -import org.opensearch.security.action.apitokens.ApiToken; import org.opensearch.security.authtoken.jwt.ExpiringBearerAuthToken; import org.opensearch.security.authtoken.jwt.JwtVendor; import org.opensearch.security.securityconf.ConfigModel; @@ -141,16 +139,11 @@ public ExpiringBearerAuthToken issueOnBehalfOfToken(final Subject subject, final } } - public ExpiringBearerAuthToken issueApiToken( - final String name, - final Long expiration, - final List clusterPermissions, - final List indexPermissions - ) { + public ExpiringBearerAuthToken issueApiToken(final String name, final Long expiration) { final User user = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); try { - return apiTokenJwtVendor.createJwt(cs.getClusterName().value(), name, name, expiration, clusterPermissions, indexPermissions); + return apiTokenJwtVendor.createJwt(cs.getClusterName().value(), name, name, expiration); } catch (final Exception ex) { logger.error("Error creating Api Token for " + user.getName(), ex); throw new OpenSearchSecurityException("Unable to generate Api Token"); diff --git a/src/test/java/org/opensearch/security/action/apitokens/ApiTokenAuthenticatorTest.java b/src/test/java/org/opensearch/security/action/apitokens/ApiTokenAuthenticatorTest.java index 93109b49d3..3de70d1302 100644 --- a/src/test/java/org/opensearch/security/action/apitokens/ApiTokenAuthenticatorTest.java +++ b/src/test/java/org/opensearch/security/action/apitokens/ApiTokenAuthenticatorTest.java @@ -11,6 +11,8 @@ package org.opensearch.security.action.apitokens; +import java.util.List; + import org.apache.logging.log4j.Logger; import org.junit.Before; import org.junit.Test; @@ -81,7 +83,7 @@ public void testExtractCredentialsPassWhenJtiInCache() { String encryptedTestJti = "k3JQNRXR57Y4V4W1LNkpEP+QzcDra5+/fFfQrr0Rncr/RhYAnYEDgN9RKcJ2Wmjf63NCQa9+HjeFcn0H2wKhMm7pl9bd3ya9FO+LUD7t9Sih4DOjUt0t7ee4ROC0eRK5glMsKsKQVkuY+YKa6A6dT8bMqmt7kIrer7w8TRENr9J8x41TGb/cDDWDvJLME7QkFzJjMxYDgKNiEevMbOpC8yjIZdK08jPe3Thq+xm+JYruoYeyI5g8QjkJA9ZOs1f6eXTAvPxhseuPqgIKykRE25fuWjl5n9tJ9W+jpl+iET7zzOLXSPEU5UepL/COkVd6xW63Ay72oMOewqveDXzyR8S8LAfgRuKgYZWms7yT37XcGg0c6Y7M62KVPo+1XQ+F+K5bgddkd8G+I9KHf561jIMzBcIodgGRj659954W16D1C92+PF/YWPQoTv2hVK4f60H82ga1YSiz3r9UrFV8d7gLJwtyJT9HNPuXO2VZ7xPhre+n1Wv7No0kH2S/r3nqKK6Bk/kn1ZbAmjLxuw13c95lIir6avlKE7XX4PiQDfcGeAyeXOw/36kLW8wH7kjXWdBspld1AiI4fCOaszNXF+7gcuTxIhECl+mEyrJbMI88EWllq+LbydiOrVLFXXRMiCbvj+VTYjzimgJPp+Vuvg=="; ApiTokenIndexListenerCache cache = ApiTokenIndexListenerCache.getInstance(); - cache.getJtis().put(encryptedTestJti, null); + cache.getJtis().put(encryptedTestJti, new Permissions(List.of(), List.of())); assertTrue(cache.getJtis().containsKey(encryptedTestJti)); SecurityRequest request = mock(SecurityRequest.class); @@ -100,7 +102,7 @@ public void testExtractCredentialsFailWhenTokenIsExpired() { String encryptedTestJti = "k3JQNRXR57Y4V4W1LNkpEP+QzcDra5+/fFfQrr0Rncr/RhYAnYEDgN9RKcJ2Wmjf63NCQa9+HjeFcn0H2wKhMm7pl9bd3ya9FO+LUD7t9ShsbOyBUkpFSVuQwrXLatY+glMsKsKQVkuY+YKa6A6dT8bMqmt7kIrer7w8TRENr9J8x41TGb/cDDWDvJLME7QkFzJjMxYDgKNiEevMbOpC8yjIZdK08jPe3Thq+xm+JYruoYeyI5g8QjkJA9ZOs1f6eXTAvPxhseuPqgIKykRE25fuWjl5n9tJ9W+jpl+iET7zzOLXSPEU5UepL/COkVd6xW63Ay72oMOewqveDXzyR8S8LAfgRuKgYZWms7yT37XcGg0c6Y7M62KVPo+1XQ+Fu193YtvS4vqt9G8jHiq51VCRxNHYVlAsratxzvECD8AKBilR9/7dUKyOQDBIzPG4ws+kgI680SgdMgGuLANQPGzal9US8GsWzTbQWCgtObaSVKB02U4gh16wvy3XrXtPz2Z0ZAxoU2Z8opX8hcvB5MG5UUEf+tpgTtVPcbuJyCL42yD3FIc3v/LCYlG/hFvflXBx5c1r+4Tij8Qc/NkYb7/03xiJsVH6eduSqR9M0QBpLm7xg2TgqVMvC/+n96x2V3lS4via4lAK6xuYeRY0ng=="; ApiTokenIndexListenerCache cache = ApiTokenIndexListenerCache.getInstance(); - cache.getJtis().put(encryptedTestJti, null); + cache.getJtis().put(encryptedTestJti, new Permissions(List.of(), List.of())); assertTrue(cache.getJtis().containsKey(encryptedTestJti)); SecurityRequest request = mock(SecurityRequest.class); @@ -121,7 +123,7 @@ public void testExtractCredentialsFailWhenIssuerDoesNotMatch() { String encryptedTestJti = "k3JQNRXR57Y4V4W1LNkpEP+QzcDra5+/fFfQrr0Rncr/RhYAnYEDgN9RKcJ2Wmjf63NCQa9+HjeFcn0H2wKhMm7pl9bd3ya9FO+LUD7t9Sih4DOjUt0t7ee4ROC0eRK5glMsKsKQVkuY+YKa6A6dT8bMqmt7kIrer7w8TRENr9J8x41TGb/cDDWDvJLME7QkFzJjMxYDgKNiEevMbOpC8yjIZdK08jPe3Thq+xm+JYruoYeyI5g8QjkJA9ZOs1f6eXTAvPxhseuPqgIKykRE25fuWjl5n9tJ9W+jpl+iET7zzOLXSPEU5UepL/COkVd6xW63Ay72oMOewqveDXzyR8S8LAfgRuKgYZWms7yT37XcGg0c6Y7M62KVPo+1XQ+F+K5bgddkd8G+I9KHf561jIMzBcIodgGRj659954W16D1C92+PF/YWPQoTv2hVK4f60H82ga1YSiz3r9UrFV8d7gLJwtyJT9HNPuXO2VZ7xPhre+n1Wv7No0kH2S/r3nqKK6Bk/kn1ZbAmjLxuw13c95lIir6avlKE7XX4PiQDfcGeAyeXOw/36kLW8wH7kjXWdBspld1AiI4fCOaszNXF+7gcuTxIhECl+mEyrJbMI88EWllq+LbydiOrVLFXXRMiCbvj+VTYjzimgJPp+Vuvg=="; ApiTokenIndexListenerCache cache = ApiTokenIndexListenerCache.getInstance(); - cache.getJtis().put(encryptedTestJti, null); + cache.getJtis().put(encryptedTestJti, new Permissions(List.of(), List.of())); assertTrue(cache.getJtis().containsKey(encryptedTestJti)); SecurityRequest request = mock(SecurityRequest.class); @@ -150,7 +152,7 @@ public void testExtractCredentialsFailWhenAccessingRestrictedEndpoint() { String encryptedTestJti = "k3JQNRXR57Y4V4W1LNkpEP+QzcDra5+/fFfQrr0Rncr/RhYAnYEDgN9RKcJ2Wmjf63NCQa9+HjeFcn0H2wKhMm7pl9bd3ya9FO+LUD7t9Sih4DOjUt0t7ee4ROC0eRK5glMsKsKQVkuY+YKa6A6dT8bMqmt7kIrer7w8TRENr9J8x41TGb/cDDWDvJLME7QkFzJjMxYDgKNiEevMbOpC8yjIZdK08jPe3Thq+xm+JYruoYeyI5g8QjkJA9ZOs1f6eXTAvPxhseuPqgIKykRE25fuWjl5n9tJ9W+jpl+iET7zzOLXSPEU5UepL/COkVd6xW63Ay72oMOewqveDXzyR8S8LAfgRuKgYZWms7yT37XcGg0c6Y7M62KVPo+1XQ+F+K5bgddkd8G+I9KHf561jIMzBcIodgGRj659954W16D1C92+PF/YWPQoTv2hVK4f60H82ga1YSiz3r9UrFV8d7gLJwtyJT9HNPuXO2VZ7xPhre+n1Wv7No0kH2S/r3nqKK6Bk/kn1ZbAmjLxuw13c95lIir6avlKE7XX4PiQDfcGeAyeXOw/36kLW8wH7kjXWdBspld1AiI4fCOaszNXF+7gcuTxIhECl+mEyrJbMI88EWllq+LbydiOrVLFXXRMiCbvj+VTYjzimgJPp+Vuvg=="; ApiTokenIndexListenerCache cache = ApiTokenIndexListenerCache.getInstance(); - cache.getJtis().put(encryptedTestJti, null); + cache.getJtis().put(encryptedTestJti, new Permissions(List.of(), List.of())); assertTrue(cache.getJtis().containsKey(encryptedTestJti)); SecurityRequest request = mock(SecurityRequest.class); @@ -169,7 +171,7 @@ public void testAuthenticatorNotEnabled() { String encryptedTestJti = "k3JQNRXR57Y4V4W1LNkpEP+QzcDra5+/fFfQrr0Rncr/RhYAnYEDgN9RKcJ2Wmjf63NCQa9+HjeFcn0H2wKhMm7pl9bd3ya9FO+LUD7t9Sih4DOjUt0t7ee4ROC0eRK5glMsKsKQVkuY+YKa6A6dT8bMqmt7kIrer7w8TRENr9J8x41TGb/cDDWDvJLME7QkFzJjMxYDgKNiEevMbOpC8yjIZdK08jPe3Thq+xm+JYruoYeyI5g8QjkJA9ZOs1f6eXTAvPxhseuPqgIKykRE25fuWjl5n9tJ9W+jpl+iET7zzOLXSPEU5UepL/COkVd6xW63Ay72oMOewqveDXzyR8S8LAfgRuKgYZWms7yT37XcGg0c6Y7M62KVPo+1XQ+F+K5bgddkd8G+I9KHf561jIMzBcIodgGRj659954W16D1C92+PF/YWPQoTv2hVK4f60H82ga1YSiz3r9UrFV8d7gLJwtyJT9HNPuXO2VZ7xPhre+n1Wv7No0kH2S/r3nqKK6Bk/kn1ZbAmjLxuw13c95lIir6avlKE7XX4PiQDfcGeAyeXOw/36kLW8wH7kjXWdBspld1AiI4fCOaszNXF+7gcuTxIhECl+mEyrJbMI88EWllq+LbydiOrVLFXXRMiCbvj+VTYjzimgJPp+Vuvg=="; ApiTokenIndexListenerCache cache = ApiTokenIndexListenerCache.getInstance(); - cache.getJtis().put(encryptedTestJti, null); + cache.getJtis().put(encryptedTestJti, new Permissions(List.of(), List.of())); assertTrue(cache.getJtis().containsKey(encryptedTestJti)); SecurityRequest request = mock(SecurityRequest.class); diff --git a/src/test/java/org/opensearch/security/action/apitokens/ApiTokenRepositoryTest.java b/src/test/java/org/opensearch/security/action/apitokens/ApiTokenRepositoryTest.java index 03a2e2c30e..a6dae60400 100644 --- a/src/test/java/org/opensearch/security/action/apitokens/ApiTokenRepositoryTest.java +++ b/src/test/java/org/opensearch/security/action/apitokens/ApiTokenRepositoryTest.java @@ -84,13 +84,13 @@ public void testCreateApiToken() { String encryptedToken = "encrypted-token"; ExpiringBearerAuthToken bearerToken = mock(ExpiringBearerAuthToken.class); when(bearerToken.getCompleteToken()).thenReturn(completeToken); - when(securityTokenManager.issueApiToken(any(), any(), any(), any())).thenReturn(bearerToken); + when(securityTokenManager.issueApiToken(any(), any())).thenReturn(bearerToken); when(securityTokenManager.encryptToken(completeToken)).thenReturn(encryptedToken); String result = repository.createApiToken(tokenName, clusterPermissions, indexPermissions, expiration); verify(apiTokenIndexHandler).createApiTokenIndexIfAbsent(); - verify(securityTokenManager).issueApiToken(any(), any(), any(), any()); + verify(securityTokenManager).issueApiToken(any(), any()); verify(securityTokenManager).encryptToken(completeToken); verify(apiTokenIndexHandler).indexTokenMetadata( argThat( diff --git a/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java b/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java index ee11c17e13..ec37898687 100644 --- a/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java +++ b/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java @@ -32,11 +32,7 @@ import org.opensearch.common.collect.Tuple; import org.opensearch.common.settings.Settings; import org.opensearch.common.xcontent.XContentFactory; -import org.opensearch.common.xcontent.XContentType; -import org.opensearch.core.xcontent.DeprecationHandler; -import org.opensearch.core.xcontent.NamedXContentRegistry; import org.opensearch.core.xcontent.ToXContent; -import org.opensearch.core.xcontent.XContentParser; import org.opensearch.security.action.apitokens.ApiToken; import org.opensearch.security.support.ConfigConstants; @@ -297,14 +293,7 @@ public void testCreateJwtForApiTokenSuccess() throws Exception { String claimsEncryptionKey = "1234567890123456"; Settings settings = Settings.builder().put("signing_key", signingKeyB64Encoded).put("encryption_key", claimsEncryptionKey).build(); final JwtVendor jwtVendor = new JwtVendor(settings, Optional.of(currentTime)); - final ExpiringBearerAuthToken authToken = jwtVendor.createJwt( - issuer, - subject, - audience, - Long.MAX_VALUE, - clusterPermissions, - indexPermissions - ); + final ExpiringBearerAuthToken authToken = jwtVendor.createJwt(issuer, subject, audience, Long.MAX_VALUE); SignedJWT signedJWT = SignedJWT.parse(authToken.getCompleteToken()); @@ -314,29 +303,6 @@ public void testCreateJwtForApiTokenSuccess() throws Exception { assertThat(signedJWT.getJWTClaimsSet().getClaims().get("iat"), is(notNullValue())); // Allow for millisecond to second conversion flexibility assertThat(((Date) signedJWT.getJWTClaimsSet().getClaims().get("exp")).getTime() / 1000, equalTo(Long.MAX_VALUE / 1000)); - - EncryptionDecryptionUtil encryptionUtil = new EncryptionDecryptionUtil(claimsEncryptionKey); - assertThat( - encryptionUtil.decrypt(signedJWT.getJWTClaimsSet().getClaims().get("cp").toString()), - equalTo(expectedClusterPermissions) - ); - - assertThat(encryptionUtil.decrypt(signedJWT.getJWTClaimsSet().getClaims().get("ip").toString()), equalTo(expectedIndexPermissions)); - - XContentParser parser = XContentType.JSON.xContent() - .createParser( - NamedXContentRegistry.EMPTY, - DeprecationHandler.THROW_UNSUPPORTED_OPERATION, - encryptionUtil.decrypt(signedJWT.getJWTClaimsSet().getClaims().get("ip").toString()) - ); - // Parse first item of the list - parser.nextToken(); - parser.nextToken(); - ApiToken.IndexPermission indexPermission1 = ApiToken.IndexPermission.fromXContent(parser); - - // Index permission deserialization works as expected - assertThat(indexPermission1.getIndexPatterns(), equalTo(indexPermission.getIndexPatterns())); - assertThat(indexPermission1.getAllowedActions(), equalTo(indexPermission.getAllowedActions())); } @Test diff --git a/src/test/java/org/opensearch/security/identity/SecurityTokenManagerTest.java b/src/test/java/org/opensearch/security/identity/SecurityTokenManagerTest.java index 7ecbb6da34..f6679a95b7 100644 --- a/src/test/java/org/opensearch/security/identity/SecurityTokenManagerTest.java +++ b/src/test/java/org/opensearch/security/identity/SecurityTokenManagerTest.java @@ -261,8 +261,8 @@ public void issueApiToken_success() throws Exception { createMockJwtVendorInTokenManager(); final ExpiringBearerAuthToken authToken = mock(ExpiringBearerAuthToken.class); - when(jwtVendor.createJwt(anyString(), anyString(), anyString(), anyLong(), any(), any())).thenReturn(authToken); - final AuthToken returnedToken = tokenManager.issueApiToken("elmo", Long.MAX_VALUE, List.of("*"), List.of()); + when(jwtVendor.createJwt(anyString(), anyString(), anyString(), anyLong())).thenReturn(authToken); + final AuthToken returnedToken = tokenManager.issueApiToken("elmo", Long.MAX_VALUE); assertThat(returnedToken, equalTo(authToken)); @@ -282,8 +282,8 @@ public void encryptCallsJwtEncrypt() throws Exception { createMockJwtVendorInTokenManager(); final ExpiringBearerAuthToken authToken = mock(ExpiringBearerAuthToken.class); - when(jwtVendor.createJwt(anyString(), anyString(), anyString(), anyLong(), any(), any())).thenReturn(authToken); - final AuthToken returnedToken = tokenManager.issueApiToken("elmo", Long.MAX_VALUE, List.of("*"), List.of()); + when(jwtVendor.createJwt(anyString(), anyString(), anyString(), anyLong())).thenReturn(authToken); + final AuthToken returnedToken = tokenManager.issueApiToken("elmo", Long.MAX_VALUE); assertThat(returnedToken, equalTo(authToken)); From ad6397425d5df8889f70f884e1325431983a7f4f Mon Sep 17 00:00:00 2001 From: Derek Ho Date: Mon, 30 Dec 2024 17:24:06 -0500 Subject: [PATCH 09/20] Onboard onto clusterPrivileges Signed-off-by: Derek Ho --- .../security/privileges/ActionPrivileges.java | 85 ++++++++++++------- 1 file changed, 56 insertions(+), 29 deletions(-) diff --git a/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java b/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java index b9ff9f125e..45a17d1b97 100644 --- a/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java +++ b/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java @@ -13,6 +13,7 @@ import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -323,6 +324,8 @@ static class ClusterPrivileges { private final ImmutableSet wellKnownClusterActions; + private final FlattenedActionGroups actionGroups; + /** * Creates pre-computed cluster privileges based on the given parameters. *

@@ -399,6 +402,7 @@ static class ClusterPrivileges { this.rolesWithWildcardPermissions = rolesWithWildcardPermissions.build(); this.rolesToActionMatcher = rolesToActionMatcher.build(); this.wellKnownClusterActions = wellKnownClusterActions; + this.actionGroups = actionGroups; } /** @@ -432,17 +436,60 @@ PrivilegesEvaluatorResponse providesPrivilege(PrivilegesEvaluationContext contex } // 4: Evaluate api tokens - if (context.getUser().getName().startsWith("apitoken")) { - String jti = context.getUser().getName().split(":")[1]; - log.info(context.getApiTokenIndexListenerCache().getJtis().get(jti).getClusterPerm().toString()); + return providesClusterPrivilegeForApiToken(context, Set.of(action), false); + } - if (context.getApiTokenIndexListenerCache().getJtis().get(jti) != null - && context.getApiTokenIndexListenerCache().getJtis().get(jti).getClusterPerm().contains(action)) { + /** + * Evaluates cluster privileges for api tokens. It does so by checking exact match, regex match, * match, and action group match in a non-optimized, naive way. + * First it expands all action groups to get all the actions and patterns of actions. Then it checks * if not an explicit check, then for exact match, then for pattern match. + */ + PrivilegesEvaluatorResponse providesClusterPrivilegeForApiToken( + PrivilegesEvaluationContext context, + Set actions, + Boolean explicit + ) { + String userName = context.getUser().getName(); + String jti = context.getUser().getName().split(":")[1]; + if (userName.startsWith("apitoken") && context.getApiTokenIndexListenerCache().getJtis().get(jti) != null) { + List clusterPermissions = context.getApiTokenIndexListenerCache().getJtis().get(jti).getClusterPerm(); + // Expand the action groups + ImmutableSet resolvedClusterPermissions = actionGroups.resolve( + context.getApiTokenIndexListenerCache().getJtis().get(jti).getClusterPerm() + ); + log.info(resolvedClusterPermissions); + + // Check for wildcard permission + if (!explicit) { + if (resolvedClusterPermissions.contains("*")) { + return PrivilegesEvaluatorResponse.ok(); + } + } + + // Check for exact match + if (!Collections.disjoint(resolvedClusterPermissions, actions)) { return PrivilegesEvaluatorResponse.ok(); } - } - return PrivilegesEvaluatorResponse.insufficient(action); + // Check for pattern matches (like "cluster:*") + for (String permission : resolvedClusterPermissions) { + // Skip exact matches as we already checked those + if (!permission.contains("*")) { + continue; + } + + WildcardMatcher permissionMatcher = WildcardMatcher.from(permission); + for (String action : actions) { + if (permissionMatcher.test(action)) { + return PrivilegesEvaluatorResponse.ok(); + } + } + } + } + if (actions.size() == 1) { + return PrivilegesEvaluatorResponse.insufficient(actions.iterator().next()); + } else { + return PrivilegesEvaluatorResponse.insufficient("any of " + actions); + } } /** @@ -475,15 +522,7 @@ PrivilegesEvaluatorResponse providesExplicitPrivilege(PrivilegesEvaluationContex } } - if (context.getUser().getName().startsWith("apitoken")) { - String jti = context.getUser().getName().split(":")[1]; - if (context.getApiTokenIndexListenerCache().getJtis().get(jti) != null - && context.getApiTokenIndexListenerCache().getJtis().get(jti).getClusterPerm().contains(action)) { - return PrivilegesEvaluatorResponse.ok(); - } - } - - return PrivilegesEvaluatorResponse.insufficient(action); + return providesClusterPrivilegeForApiToken(context, Set.of(action), true); } /** @@ -519,19 +558,7 @@ PrivilegesEvaluatorResponse providesAnyPrivilege(PrivilegesEvaluationContext con } } - if (context.getUser().getName().startsWith("apitoken")) { - String jti = context.getUser().getName().split(":")[1]; - if (context.getApiTokenIndexListenerCache().getJtis().get(jti) != null - && context.getApiTokenIndexListenerCache().getJtis().get(jti).getClusterPerm().stream().anyMatch(actions::contains)) { - return PrivilegesEvaluatorResponse.ok(); - } - } - - if (actions.size() == 1) { - return PrivilegesEvaluatorResponse.insufficient(actions.iterator().next()); - } else { - return PrivilegesEvaluatorResponse.insufficient("any of " + actions); - } + return providesClusterPrivilegeForApiToken(context, actions, false); } } From 73eb2ab50ef28019d7f3d064c8fdddeb21597ade Mon Sep 17 00:00:00 2001 From: Derek Ho Date: Tue, 31 Dec 2024 15:11:12 -0500 Subject: [PATCH 10/20] Add index permissions api token eval Signed-off-by: Derek Ho --- .../security/privileges/ActionPrivileges.java | 172 +++++++++++------- 1 file changed, 111 insertions(+), 61 deletions(-) diff --git a/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java b/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java index 45a17d1b97..08b2d6aa5c 100644 --- a/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java +++ b/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java @@ -164,28 +164,6 @@ public PrivilegesEvaluatorResponse hasIndexPrivilege( return response; } - // API Token Authz - // TODO: this is very naive implementation - if (context.getUser().getName().startsWith("apitoken")) { - String jti = context.getUser().getName().split(":")[1]; - if (context.getApiTokenIndexListenerCache().getJtis().get(jti) != null) { - List indexPermissions = context.getApiTokenIndexListenerCache() - .getJtis() - .get(jti) - .getIndexPermission(); - - boolean hasPermission = indexPermissions.stream().anyMatch(permission -> { - boolean hasAllActions = new HashSet<>(permission.getAllowedActions()).containsAll(actions); - boolean hasAllIndices = new HashSet<>(permission.getIndexPatterns()).containsAll(resolvedIndices.getAllIndices()); - return hasAllActions && hasAllIndices; - }); - - if (hasPermission) { - return PrivilegesEvaluatorResponse.ok(); - } - } - } - if (!resolvedIndices.isLocalAll() && resolvedIndices.getAllIndices().isEmpty()) { // This is necessary for requests which operate on remote indices. // Access control for the remote indices will be performed on the remote cluster. @@ -436,54 +414,55 @@ PrivilegesEvaluatorResponse providesPrivilege(PrivilegesEvaluationContext contex } // 4: Evaluate api tokens - return providesClusterPrivilegeForApiToken(context, Set.of(action), false); + return apiTokenProvidesClusterPrivilege(context, Set.of(action), false); } /** * Evaluates cluster privileges for api tokens. It does so by checking exact match, regex match, * match, and action group match in a non-optimized, naive way. * First it expands all action groups to get all the actions and patterns of actions. Then it checks * if not an explicit check, then for exact match, then for pattern match. */ - PrivilegesEvaluatorResponse providesClusterPrivilegeForApiToken( + PrivilegesEvaluatorResponse apiTokenProvidesClusterPrivilege( PrivilegesEvaluationContext context, Set actions, Boolean explicit ) { String userName = context.getUser().getName(); - String jti = context.getUser().getName().split(":")[1]; - if (userName.startsWith("apitoken") && context.getApiTokenIndexListenerCache().getJtis().get(jti) != null) { - List clusterPermissions = context.getApiTokenIndexListenerCache().getJtis().get(jti).getClusterPerm(); - // Expand the action groups - ImmutableSet resolvedClusterPermissions = actionGroups.resolve( - context.getApiTokenIndexListenerCache().getJtis().get(jti).getClusterPerm() - ); - log.info(resolvedClusterPermissions); + if (userName.startsWith("apitoken") && userName.contains(":")) { + String jti = context.getUser().getName().split(":")[1]; + if (context.getApiTokenIndexListenerCache().getJtis().get(jti) != null) { + // Expand the action groups + ImmutableSet resolvedClusterPermissions = actionGroups.resolve( + context.getApiTokenIndexListenerCache().getJtis().get(jti).getClusterPerm() + ); - // Check for wildcard permission - if (!explicit) { - if (resolvedClusterPermissions.contains("*")) { - return PrivilegesEvaluatorResponse.ok(); + // Check for wildcard permission + if (!explicit) { + if (resolvedClusterPermissions.contains("*")) { + return PrivilegesEvaluatorResponse.ok(); + } } - } - - // Check for exact match - if (!Collections.disjoint(resolvedClusterPermissions, actions)) { - return PrivilegesEvaluatorResponse.ok(); - } - // Check for pattern matches (like "cluster:*") - for (String permission : resolvedClusterPermissions) { - // Skip exact matches as we already checked those - if (!permission.contains("*")) { - continue; + // Check for exact match + if (!Collections.disjoint(resolvedClusterPermissions, actions)) { + return PrivilegesEvaluatorResponse.ok(); } - WildcardMatcher permissionMatcher = WildcardMatcher.from(permission); - for (String action : actions) { - if (permissionMatcher.test(action)) { - return PrivilegesEvaluatorResponse.ok(); + // Check for pattern matches (like "cluster:*") + for (String permission : resolvedClusterPermissions) { + // Skip exact matches as we already checked those + if (!permission.contains("*")) { + continue; + } + + WildcardMatcher permissionMatcher = WildcardMatcher.from(permission); + for (String action : actions) { + if (permissionMatcher.test(action)) { + return PrivilegesEvaluatorResponse.ok(); + } } } } + } if (actions.size() == 1) { return PrivilegesEvaluatorResponse.insufficient(actions.iterator().next()); @@ -522,7 +501,7 @@ PrivilegesEvaluatorResponse providesExplicitPrivilege(PrivilegesEvaluationContex } } - return providesClusterPrivilegeForApiToken(context, Set.of(action), true); + return apiTokenProvidesClusterPrivilege(context, Set.of(action), true); } /** @@ -558,7 +537,7 @@ PrivilegesEvaluatorResponse providesAnyPrivilege(PrivilegesEvaluationContext con } } - return providesClusterPrivilegeForApiToken(context, actions, false); + return apiTokenProvidesClusterPrivilege(context, actions, false); } } @@ -617,6 +596,8 @@ static class IndexPrivileges { */ private final ImmutableMap> rolesToExplicitActionToIndexPattern; + private final FlattenedActionGroups actionGroups; + /** * Creates pre-computed index privileges based on the given parameters. *

@@ -754,6 +735,7 @@ static class IndexPrivileges { this.wellKnownIndexActions = wellKnownIndexActions; this.explicitlyRequiredIndexActions = explicitlyRequiredIndexActions; + this.actionGroups = actionGroups; } /** @@ -856,13 +838,7 @@ PrivilegesEvaluatorResponse providesPrivilege( return PrivilegesEvaluatorResponse.partiallyOk(availableIndices, checkTable).evaluationExceptions(exceptions); } - return PrivilegesEvaluatorResponse.insufficient(checkTable) - .reason( - resolvedIndices.getAllIndices().size() == 1 - ? "Insufficient permissions for the referenced index" - : "None of " + resolvedIndices.getAllIndices().size() + " referenced indices has sufficient permissions" - ) - .evaluationExceptions(exceptions); + return apiTokenProvidesIndexPrivilege(checkTable, context, exceptions, resolvedIndices, actions, false); } /** @@ -928,8 +904,82 @@ PrivilegesEvaluatorResponse providesExplicitPrivilege( } } + return apiTokenProvidesIndexPrivilege(checkTable, context, exceptions, resolvedIndices, actions, true); + } + + PrivilegesEvaluatorResponse apiTokenProvidesIndexPrivilege( + CheckTable checkTable, + PrivilegesEvaluationContext context, + List exceptions, + IndexResolverReplacer.Resolved resolvedIndices, + Set actions, + Boolean explicit + ) { + String userName = context.getUser().getName(); + if (userName.startsWith("apitoken") && userName.contains(":")) { + String jti = context.getUser().getName().split(":")[1]; + if (context.getApiTokenIndexListenerCache().getJtis().get(jti) != null) { + List indexPermissions = context.getApiTokenIndexListenerCache() + .getJtis() + .get(jti) + .getIndexPermission(); + + for (String concreteIndex : resolvedIndices.getAllIndices()) { + boolean indexHasAllPermissions = false; + + // Check each index permission + for (ApiToken.IndexPermission indexPermission : indexPermissions) { + // First check if this permission applies to this index + boolean indexMatched = false; + for (String pattern : indexPermission.getIndexPatterns()) { + if (WildcardMatcher.from(pattern).test(concreteIndex)) { + indexMatched = true; + break; + } + } + + if (!indexMatched) { + continue; + } + + // Index matched, now check if this permission covers all actions + Set remainingActions = new HashSet<>(actions); + ImmutableSet resolvedIndexPermissions = actionGroups.resolve(indexPermission.getAllowedActions()); + + for (String permission : resolvedIndexPermissions) { + // Skip global wildcard if explicit is true + if (explicit && permission.equals("*")) { + continue; + } + + WildcardMatcher permissionMatcher = WildcardMatcher.from(permission); + remainingActions.removeIf(action -> permissionMatcher.test(action)); + + if (remainingActions.isEmpty()) { + indexHasAllPermissions = true; + break; + } + } + + if (indexHasAllPermissions) { + break; // Found a permission that covers all actions for this index + } + } + + if (!indexHasAllPermissions) { + return PrivilegesEvaluatorResponse.insufficient("Insufficient permissions for index"); + } + } + // If we get here, all indices had sufficient permissions + return PrivilegesEvaluatorResponse.ok(); + } + } return PrivilegesEvaluatorResponse.insufficient(checkTable) - .reason("No explicit privileges have been provided for the referenced indices.") + .reason( + resolvedIndices.getAllIndices().size() == 1 + ? "Insufficient permissions for the referenced index" + : "None of " + resolvedIndices.getAllIndices().size() + " referenced indices has sufficient permissions" + ) .evaluationExceptions(exceptions); } } From 641822660162ffb781324bbf8ae7871f206596a7 Mon Sep 17 00:00:00 2001 From: Derek Ho Date: Tue, 31 Dec 2024 16:49:42 -0500 Subject: [PATCH 11/20] Add testing for cluster and index priv Signed-off-by: Derek Ho --- .../privileges/ActionPrivilegesTest.java | 116 +++++++++++++++++- .../security/privileges/ActionPrivileges.java | 31 +++-- 2 files changed, 136 insertions(+), 11 deletions(-) diff --git a/src/integrationTest/java/org/opensearch/security/privileges/ActionPrivilegesTest.java b/src/integrationTest/java/org/opensearch/security/privileges/ActionPrivilegesTest.java index 7807dae748..ecd76b127c 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/ActionPrivilegesTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/ActionPrivilegesTest.java @@ -38,15 +38,19 @@ import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.core.common.unit.ByteSizeUnit; import org.opensearch.core.common.unit.ByteSizeValue; +import org.opensearch.security.action.apitokens.ApiToken; +import org.opensearch.security.action.apitokens.Permissions; import org.opensearch.security.resolver.IndexResolverReplacer; import org.opensearch.security.securityconf.FlattenedActionGroups; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; +import org.opensearch.security.securityconf.impl.v7.ActionGroupsV7; import org.opensearch.security.securityconf.impl.v7.RoleV7; import org.opensearch.security.user.User; import org.opensearch.security.util.MockIndexMetadataBuilder; import static org.hamcrest.MatcherAssert.assertThat; +import static org.opensearch.security.privileges.ActionPrivilegesTest.IndexPrivileges.IndicesAndAliases.resolved; import static org.opensearch.security.privileges.PrivilegeEvaluatorResponseMatcher.isAllowed; import static org.opensearch.security.privileges.PrivilegeEvaluatorResponseMatcher.isForbidden; import static org.opensearch.security.privileges.PrivilegeEvaluatorResponseMatcher.isPartiallyOk; @@ -258,6 +262,69 @@ public void hasAny_wildcard() throws Exception { isForbidden(missingPrivileges("cluster:whatever")) ); } + + @Test + public void apiToken_explicit_failsWithWildcard() throws Exception { + SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromYaml("test_role:\n" + // + " cluster_permissions:\n" + // + " - '*'", CType.ROLES); + ActionPrivileges subject = new ActionPrivileges(roles, FlattenedActionGroups.EMPTY, null, Settings.EMPTY); + String token = "blah"; + PrivilegesEvaluationContext context = ctxWithUserName("apitoken_test:" + token); + context.getApiTokenIndexListenerCache().getJtis().put(token, new Permissions(List.of("*"), List.of())); + // Explicit fails + assertThat( + subject.hasExplicitClusterPrivilege(context, "cluster:whatever"), + isForbidden(missingPrivileges("cluster:whatever")) + ); + // Not explicit succeeds + assertThat(subject.hasClusterPrivilege(context, "cluster:whatever"), isAllowed()); + // Any succeeds + assertThat(subject.hasAnyClusterPrivilege(context, ImmutableSet.of("cluster:whatever")), isAllowed()); + } + + @Test + public void apiToken_succeedsWithExactMatch() throws Exception { + SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromYaml("test_role:\n" + // + " cluster_permissions:\n" + // + " - '*'", CType.ROLES); + ActionPrivileges subject = new ActionPrivileges(roles, FlattenedActionGroups.EMPTY, null, Settings.EMPTY); + String token = "blah"; + PrivilegesEvaluationContext context = ctxWithUserName("apitoken_test:" + token); + context.getApiTokenIndexListenerCache().getJtis().put(token, new Permissions(List.of("cluster:whatever"), List.of())); + // Explicit succeeds + assertThat(subject.hasExplicitClusterPrivilege(context, "cluster:whatever"), isAllowed()); + // Not explicit succeeds + assertThat(subject.hasClusterPrivilege(context, "cluster:whatever"), isAllowed()); + // Any succeeds + assertThat(subject.hasAnyClusterPrivilege(context, ImmutableSet.of("cluster:whatever")), isAllowed()); + // Any succeeds + assertThat(subject.hasAnyClusterPrivilege(context, ImmutableSet.of("cluster:whatever", "cluster:other")), isAllowed()); + } + + @Test + public void apiToken_succeedsWithActionGroupsExapnded() throws Exception { + SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromYaml("test_role:\n" + // + " cluster_permissions:\n" + // + " - '*'", CType.ROLES); + + SecurityDynamicConfiguration config = SecurityDynamicConfiguration.fromYaml( + "CLUSTER_ALL:\n allowed_actions:\n - \"cluster:*\"", + CType.ACTIONGROUPS + ); + + FlattenedActionGroups actionGroups = new FlattenedActionGroups(config); + ActionPrivileges subject = new ActionPrivileges(roles, actionGroups, null, Settings.EMPTY); + String token = "blah"; + PrivilegesEvaluationContext context = ctxWithUserName("apitoken_test:" + token); + context.getApiTokenIndexListenerCache().getJtis().put(token, new Permissions(List.of("CLUSTER_ALL"), List.of())); + // Explicit succeeds + assertThat(subject.hasExplicitClusterPrivilege(context, "cluster:whatever"), isAllowed()); + // Not explicit succeeds + assertThat(subject.hasClusterPrivilege(context, "cluster:whatever"), isAllowed()); + // Any succeeds + assertThat(subject.hasClusterPrivilege(context, "cluster:monitor/main"), isAllowed()); + } } /** @@ -292,6 +359,20 @@ public void positive_full() throws Exception { assertThat(result, isAllowed()); } + @Test + public void apiTokens_positive_full() throws Exception { + String token = "blah"; + PrivilegesEvaluationContext context = ctxWithUserName("apitoken_test:" + token); + context.getApiTokenIndexListenerCache() + .getJtis() + .put( + token, + new Permissions(List.of("index_a11"), List.of(new ApiToken.IndexPermission(List.of("index_a11"), List.of("*")))) + ); + PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege(context, requiredActions, resolved("index_a11")); + assertThat(result, isAllowed()); + } + @Test public void positive_partial() throws Exception { PrivilegesEvaluationContext ctx = ctx("test_role"); @@ -346,6 +427,18 @@ public void negative_wrongRole() throws Exception { assertThat(result, isForbidden(missingPrivileges(requiredActions))); } + @Test + public void apiToken_negative_noPermissions() throws Exception { + String token = "blah"; + PrivilegesEvaluationContext context = ctxWithUserName("apitoken_test:" + token); + context.getApiTokenIndexListenerCache() + .getJtis() + .put(token, new Permissions(List.of(), List.of(new ApiToken.IndexPermission(List.of(), List.of())))); + + PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege(context, requiredActions, resolved("index_a11")); + assertThat(result, isForbidden(missingPrivileges(requiredActions))); + } + @Test public void negative_wrongAction() throws Exception { PrivilegesEvaluationContext ctx = ctx("test_role"); @@ -375,6 +468,23 @@ public void positive_hasExplicit_full() { } } + @Test + public void apiTokens_positive_hasExplicit_full() { + String token = "blah"; + PrivilegesEvaluationContext context = ctxWithUserName("apitoken_test:" + token); + context.getApiTokenIndexListenerCache() + .getJtis() + .put( + token, + new Permissions(List.of("index_a11"), List.of(new ApiToken.IndexPermission(List.of("index_a11"), List.of("*")))) + ); + + PrivilegesEvaluatorResponse result = subject.hasExplicitIndexPrivilege(context, requiredActions, resolved("index_a11")); + + assertThat(result, isForbidden(missingPrivileges(requiredActions))); + + } + private boolean covers(PrivilegesEvaluationContext ctx, String... indices) { for (String index : indices) { if (!indexSpec.covers(ctx.getUser(), index)) { @@ -1017,7 +1127,11 @@ static SecurityDynamicConfiguration createRoles(int numberOfRoles, int n } static PrivilegesEvaluationContext ctx(String... roles) { - User user = new User("test_user"); + return ctxWithUserName("test-user", roles); + } + + static PrivilegesEvaluationContext ctxWithUserName(String userName, String... roles) { + User user = new User(userName); user.addAttributes(ImmutableMap.of("attrs.dept_no", "a11")); return new PrivilegesEvaluationContext( user, diff --git a/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java b/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java index 08b2d6aa5c..d722231796 100644 --- a/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java +++ b/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java @@ -431,7 +431,7 @@ PrivilegesEvaluatorResponse apiTokenProvidesClusterPrivilege( String jti = context.getUser().getName().split(":")[1]; if (context.getApiTokenIndexListenerCache().getJtis().get(jti) != null) { // Expand the action groups - ImmutableSet resolvedClusterPermissions = actionGroups.resolve( + Set resolvedClusterPermissions = actionGroups.resolve( context.getApiTokenIndexListenerCache().getJtis().get(jti).getClusterPerm() ); @@ -449,15 +449,18 @@ PrivilegesEvaluatorResponse apiTokenProvidesClusterPrivilege( // Check for pattern matches (like "cluster:*") for (String permission : resolvedClusterPermissions) { - // Skip exact matches as we already checked those - if (!permission.contains("*")) { - continue; - } + // skip pure *, which was evaluated above + if (permission != "*") { + // Skip exact matches as we already checked those + if (!permission.contains("*")) { + continue; + } - WildcardMatcher permissionMatcher = WildcardMatcher.from(permission); - for (String action : actions) { - if (permissionMatcher.test(action)) { - return PrivilegesEvaluatorResponse.ok(); + WildcardMatcher permissionMatcher = WildcardMatcher.from(permission); + for (String action : actions) { + if (permissionMatcher.test(action)) { + return PrivilegesEvaluatorResponse.ok(); + } } } } @@ -967,7 +970,15 @@ PrivilegesEvaluatorResponse apiTokenProvidesIndexPrivilege( } if (!indexHasAllPermissions) { - return PrivilegesEvaluatorResponse.insufficient("Insufficient permissions for index"); + return PrivilegesEvaluatorResponse.insufficient(checkTable) + .reason( + resolvedIndices.getAllIndices().size() == 1 + ? "Insufficient permissions for the referenced index" + : "None of " + + resolvedIndices.getAllIndices().size() + + " referenced indices has sufficient permissions" + ) + .evaluationExceptions(exceptions); } } // If we get here, all indices had sufficient permissions From bc8aacf42eb1ee1d85b8bfa46c798430d24f7291 Mon Sep 17 00:00:00 2001 From: Derek Ho Date: Mon, 6 Jan 2025 12:42:34 -0500 Subject: [PATCH 12/20] Use transport action Signed-off-by: Derek Ho --- .../security/OpenSearchSecurityPlugin.java | 12 +- .../action/apitokens/ApiTokenAction.java | 87 +++++++--- .../apitokens/ApiTokenIndexListenerCache.java | 162 +++++++++++------- .../apitokens/ApiTokenUpdateAction.java | 24 +++ .../apitokens/ApiTokenUpdateNodeResponse.java | 28 +++ .../apitokens/ApiTokenUpdateRequest.java | 35 ++++ .../apitokens/ApiTokenUpdateResponse.java | 60 +++++++ .../TransportApiTokenUpdateAction.java | 104 +++++++++++ .../security/http/ApiTokenAuthenticator.java | 2 +- .../security/privileges/ActionPrivileges.java | 9 +- .../apitokens/ApiTokenAuthenticatorTest.java | 24 +-- 11 files changed, 427 insertions(+), 120 deletions(-) create mode 100644 src/main/java/org/opensearch/security/action/apitokens/ApiTokenUpdateAction.java create mode 100644 src/main/java/org/opensearch/security/action/apitokens/ApiTokenUpdateNodeResponse.java create mode 100644 src/main/java/org/opensearch/security/action/apitokens/ApiTokenUpdateRequest.java create mode 100644 src/main/java/org/opensearch/security/action/apitokens/ApiTokenUpdateResponse.java create mode 100644 src/main/java/org/opensearch/security/action/apitokens/TransportApiTokenUpdateAction.java diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index efe51d2e74..048fa1fea9 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -133,6 +133,8 @@ import org.opensearch.search.query.QuerySearchResult; import org.opensearch.security.action.apitokens.ApiTokenAction; import org.opensearch.security.action.apitokens.ApiTokenIndexListenerCache; +import org.opensearch.security.action.apitokens.ApiTokenUpdateAction; +import org.opensearch.security.action.apitokens.TransportApiTokenUpdateAction; import org.opensearch.security.action.configupdate.ConfigUpdateAction; import org.opensearch.security.action.configupdate.TransportConfigUpdateAction; import org.opensearch.security.action.onbehalf.CreateOnBehalfOfTokenAction; @@ -686,6 +688,7 @@ public UnaryOperator getRestHandlerWrapper(final ThreadContext thre List> actions = new ArrayList<>(1); if (!disabled && !SSLConfig.isSslOnlyMode()) { actions.add(new ActionHandler<>(ConfigUpdateAction.INSTANCE, TransportConfigUpdateAction.class)); + actions.add(new ActionHandler<>(ApiTokenUpdateAction.INSTANCE, TransportApiTokenUpdateAction.class)); // external storage does not support reload and does not provide SSL certs info if (!ExternalSecurityKeyStore.hasExternalSslContext(settings)) { actions.add(new ActionHandler<>(CertificatesActionType.INSTANCE, TransportCertificatesInfoNodesAction.class)); @@ -719,14 +722,6 @@ public void onIndexModule(IndexModule indexModule) { ) ); - // TODO: Is there a higher level approach that makes more sense here? Does this cover unsuccessful index ops? - if (ConfigConstants.OPENSEARCH_API_TOKENS_INDEX.equals(indexModule.getIndex().getName())) { - ApiTokenIndexListenerCache apiTokenIndexListenerCacher = ApiTokenIndexListenerCache.getInstance(); - apiTokenIndexListenerCacher.initialize(); - indexModule.addIndexOperationListener(apiTokenIndexListenerCacher); - log.warn("Security plugin started listening to operations on index {}", ConfigConstants.OPENSEARCH_API_TOKENS_INDEX); - } - indexModule.forceQueryCacheProvider((indexSettings, nodeCache) -> new QueryCache() { @Override @@ -1105,6 +1100,7 @@ public Collection createComponents( adminDns = new AdminDNs(settings); cr = ConfigurationRepository.create(settings, this.configPath, threadPool, localClient, clusterService, auditLog); + ApiTokenIndexListenerCache.getInstance().initialize(clusterService, localClient); this.passwordHasher = PasswordHasherFactory.createPasswordHasher(settings); diff --git a/src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java b/src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java index e2e373812f..75bf3ffa01 100644 --- a/src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java +++ b/src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java @@ -20,14 +20,18 @@ import java.util.stream.Collectors; import com.google.common.collect.ImmutableList; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.opensearch.client.Client; import org.opensearch.client.node.NodeClient; import org.opensearch.cluster.service.ClusterService; +import org.opensearch.core.action.ActionListener; import org.opensearch.core.rest.RestStatus; import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.rest.BaseRestHandler; import org.opensearch.rest.BytesRestResponse; +import org.opensearch.rest.RestChannel; import org.opensearch.rest.RestHandler; import org.opensearch.rest.RestRequest; import org.opensearch.security.identity.SecurityTokenManager; @@ -48,6 +52,7 @@ public class ApiTokenAction extends BaseRestHandler { private final ApiTokenRepository apiTokenRepository; + public Logger log = LogManager.getLogger(this.getClass()); private static final List ROUTES = addRoutesPrefix( ImmutableList.of( @@ -133,20 +138,32 @@ private RestChannelConsumer handlePost(RestRequest request, NodeClient client) { (Long) requestBody.getOrDefault(EXPIRATION_FIELD, Instant.now().toEpochMilli() + TimeUnit.DAYS.toMillis(30)) ); - builder.startObject(); - builder.field("Api Token: ", token); - builder.endObject(); - - response = new BytesRestResponse(RestStatus.OK, builder); + // Then trigger the update action + ApiTokenUpdateRequest updateRequest = new ApiTokenUpdateRequest(); + client.execute(ApiTokenUpdateAction.INSTANCE, updateRequest, new ActionListener() { + @Override + public void onResponse(ApiTokenUpdateResponse updateResponse) { + try { + XContentBuilder builder = channel.newBuilder(); + builder.startObject(); + builder.field("Api Token: ", token); + builder.endObject(); + + BytesRestResponse response = new BytesRestResponse(RestStatus.OK, builder); + channel.sendResponse(response); + } catch (IOException e) { + sendErrorResponse(channel, RestStatus.INTERNAL_SERVER_ERROR, "Failed to send response after token creation"); + } + } + + @Override + public void onFailure(Exception e) { + sendErrorResponse(channel, RestStatus.INTERNAL_SERVER_ERROR, "Failed to propagate token creation"); + } + }); } catch (final Exception exception) { - builder.startObject() - .field("error", "An unexpected error occurred. Please check the input and try again.") - .field("message", exception.getMessage()) - .endObject(); - response = new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, builder); + sendErrorResponse(channel, RestStatus.INTERNAL_SERVER_ERROR, exception.getMessage()); } - builder.close(); - channel.sendResponse(response); }; } @@ -239,22 +256,46 @@ private RestChannelConsumer handleDelete(RestRequest request, NodeClient client) validateRequestParameters(requestBody); apiTokenRepository.deleteApiToken((String) requestBody.get(NAME_FIELD)); - builder.startObject(); - builder.field("message", "token " + requestBody.get(NAME_FIELD) + " deleted successfully."); - builder.endObject(); - - response = new BytesRestResponse(RestStatus.OK, builder); + ApiTokenUpdateRequest updateRequest = new ApiTokenUpdateRequest(); + client.execute(ApiTokenUpdateAction.INSTANCE, updateRequest, new ActionListener() { + @Override + public void onResponse(ApiTokenUpdateResponse updateResponse) { + try { + XContentBuilder builder = channel.newBuilder(); + builder.startObject(); + builder.field("message", "token " + requestBody.get(NAME_FIELD) + " deleted successfully."); + builder.endObject(); + + BytesRestResponse response = new BytesRestResponse(RestStatus.OK, builder); + channel.sendResponse(response); + } catch (Exception e) { + sendErrorResponse(channel, RestStatus.INTERNAL_SERVER_ERROR, "Failed to send response after token update"); + } + } + + @Override + public void onFailure(Exception e) { + sendErrorResponse(channel, RestStatus.INTERNAL_SERVER_ERROR, "Failed to propagate token deletion"); + } + }); } catch (final ApiTokenException exception) { - builder.startObject().field("error", exception.getMessage()).endObject(); - response = new BytesRestResponse(RestStatus.NOT_FOUND, builder); + sendErrorResponse(channel, RestStatus.NOT_FOUND, exception.getMessage()); } catch (final Exception exception) { - builder.startObject().field("error", exception.getMessage()).endObject(); - response = new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, builder); + sendErrorResponse(channel, RestStatus.INTERNAL_SERVER_ERROR, exception.getMessage()); } - builder.close(); - channel.sendResponse(response); }; } + private void sendErrorResponse(RestChannel channel, RestStatus status, String errorMessage) { + try { + XContentBuilder builder = channel.newBuilder(); + builder.startObject().field("error", errorMessage).endObject(); + BytesRestResponse response = new BytesRestResponse(status, builder); + channel.sendResponse(response); + } catch (Exception e) { + log.error("Failed to send error response", e); + } + } + } diff --git a/src/main/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCache.java b/src/main/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCache.java index 8b87f2fa03..a27c1e06db 100644 --- a/src/main/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCache.java +++ b/src/main/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCache.java @@ -8,105 +8,137 @@ package org.opensearch.security.action.apitokens; -import java.io.IOException; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.opensearch.common.xcontent.LoggingDeprecationHandler; -import org.opensearch.common.xcontent.XContentType; -import org.opensearch.core.common.bytes.BytesReference; -import org.opensearch.core.index.shard.ShardId; -import org.opensearch.core.xcontent.NamedXContentRegistry; -import org.opensearch.core.xcontent.XContentParser; -import org.opensearch.index.engine.Engine; -import org.opensearch.index.shard.IndexingOperationListener; - -/** - * This class implements an index operation listener for operations performed on api tokens - * These indices are defined on bootstrap and configured to listen in OpenSearchSecurityPlugin.java - */ -public class ApiTokenIndexListenerCache implements IndexingOperationListener { +import org.opensearch.client.Client; +import org.opensearch.cluster.ClusterChangedEvent; +import org.opensearch.cluster.ClusterStateListener; +import org.opensearch.cluster.metadata.IndexMetadata; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.core.rest.RestStatus; +import org.opensearch.index.query.QueryBuilders; +import org.opensearch.security.support.ConfigConstants; - private final static Logger log = LogManager.getLogger(ApiTokenIndexListenerCache.class); +public class ApiTokenIndexListenerCache implements ClusterStateListener { + private static final Logger log = LogManager.getLogger(ApiTokenIndexListenerCache.class); private static final ApiTokenIndexListenerCache INSTANCE = new ApiTokenIndexListenerCache(); - private final ConcurrentHashMap idToJtiMap = new ConcurrentHashMap<>(); - private Map jtis = new ConcurrentHashMap<>(); + private final ConcurrentHashMap idToJtiMap = new ConcurrentHashMap<>(); + private final Map jtis = new ConcurrentHashMap<>(); - private boolean initialized; + private final AtomicBoolean initialized = new AtomicBoolean(false); + private ClusterService clusterService; + private Client client; private ApiTokenIndexListenerCache() {} public static ApiTokenIndexListenerCache getInstance() { - return ApiTokenIndexListenerCache.INSTANCE; + return INSTANCE; + } + + public void initialize(ClusterService clusterService, Client client) { + if (initialized.compareAndSet(false, true)) { + this.clusterService = clusterService; + this.client = client; + + // Register as cluster state listener + this.clusterService.addListener(this); + } } - /** - * Initializes the ApiTokenIndexListenerCache. - * This method is called during the plugin's initialization process. - * - */ - public void initialize() { + @Override + public void clusterChanged(ClusterChangedEvent event) { + // Reload cache if the security index has changed + IndexMetadata securityIndex = event.state().metadata().index(getSecurityIndexName()); + if (securityIndex != null) { + reloadApiTokensFromIndex(); + } + } - if (initialized) { + void reloadApiTokensFromIndex() { + if (!initialized.get()) { + log.debug("Cache not yet initialized or client is null, skipping reload"); return; } - initialized = true; + if (clusterService.state() != null && clusterService.state().blocks().hasGlobalBlockWithStatus(RestStatus.SERVICE_UNAVAILABLE)) { + log.debug("Cluster not yet ready, skipping API tokens cache reload"); + return; + } + try { + // Clear existing caches + log.info("Reloading API tokens cache from index: {}", jtis.entrySet().toString()); + + idToJtiMap.clear(); + jtis.clear(); + + // Search request to get all API tokens from the security index + client.prepareSearch(getSecurityIndexName()) + .setQuery(QueryBuilders.matchAllQuery()) + .execute() + .actionGet() + .getHits() + .forEach(hit -> { + // Parse the document and update the cache + Map source = hit.getSourceAsMap(); + String id = hit.getId(); + String jti = (String) source.get("jti"); + Permissions permissions = parsePermissions(source); + + idToJtiMap.put(id, jti); + jtis.put(jti, permissions); + }); + + log.debug("Successfully reloaded API tokens cache"); + } catch (Exception e) { + log.error("Failed to reload API tokens cache", e); + } } - public boolean isInitialized() { - return initialized; + private String getSecurityIndexName() { + // Return the name of your security index + return ConfigConstants.OPENSEARCH_API_TOKENS_INDEX; } - /** - * This method is called after an index operation is performed. - * It adds the JTI of the indexed document to the cache and maps the document ID to the JTI (for deletion handling). - * @param shardId The shard ID of the index where the operation was performed. - * @param index The index where the operation was performed. - * @param result The result of the index operation. - */ - @Override - public void postIndex(ShardId shardId, Engine.Index index, Engine.IndexResult result) { - BytesReference sourceRef = index.source(); - - try { - XContentParser parser = XContentType.JSON.xContent() - .createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, sourceRef.streamInput()); + @SuppressWarnings("unchecked") + private Permissions parsePermissions(Map source) { + // Implement parsing logic for permissions from the document + return new Permissions( + (List) source.get(ApiToken.CLUSTER_PERMISSIONS_FIELD), + (List) source.get(ApiToken.INDEX_PERMISSIONS_FIELD) + ); + } - ApiToken token = ApiToken.fromXContent(parser); - jtis.put(token.getJti(), new Permissions(token.getClusterPermissions(), token.getIndexPermissions())); - idToJtiMap.put(index.id(), token.getJti()); + // Getter methods for cached data + public String getJtiForId(String id) { + return idToJtiMap.get(id); + } - } catch (IOException e) { - log.error("Failed to parse indexed document", e); - } + public Permissions getPermissionsForJti(String jti) { + return jtis.get(jti); } - /** - * This method is called after a delete operation is performed. - * It deletes the corresponding document id in the map and the corresponding jti from the cache. - * @param shardId The shard ID of the index where the delete operation was performed. - * @param delete The delete operation that was performed. - * @param result The result of the delete operation. - */ - @Override - public void postDelete(ShardId shardId, Engine.Delete delete, Engine.DeleteResult result) { - String docId = delete.id(); - String jti = idToJtiMap.remove(docId); - if (jti != null) { - jtis.remove(jti); - log.debug("Removed token with ID {} and JTI {} from cache", docId, jti); - } + // Method to check if a token is valid + public boolean isValidToken(String jti) { + return jtis.containsKey(jti); } public Map getJtis() { return jtis; } + // Cleanup method + public void close() { + if (clusterService != null) { + clusterService.removeListener(this); + } + } } diff --git a/src/main/java/org/opensearch/security/action/apitokens/ApiTokenUpdateAction.java b/src/main/java/org/opensearch/security/action/apitokens/ApiTokenUpdateAction.java new file mode 100644 index 0000000000..c9d324c52f --- /dev/null +++ b/src/main/java/org/opensearch/security/action/apitokens/ApiTokenUpdateAction.java @@ -0,0 +1,24 @@ +/* + * 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.action.apitokens; + +import org.opensearch.action.ActionType; + +public class ApiTokenUpdateAction extends ActionType { + + public static final ApiTokenUpdateAction INSTANCE = new ApiTokenUpdateAction(); + public static final String NAME = "cluster:admin/opendistro_security/apitoken/update"; + + protected ApiTokenUpdateAction() { + super(NAME, ApiTokenUpdateResponse::new); + } +} diff --git a/src/main/java/org/opensearch/security/action/apitokens/ApiTokenUpdateNodeResponse.java b/src/main/java/org/opensearch/security/action/apitokens/ApiTokenUpdateNodeResponse.java new file mode 100644 index 0000000000..429310d966 --- /dev/null +++ b/src/main/java/org/opensearch/security/action/apitokens/ApiTokenUpdateNodeResponse.java @@ -0,0 +1,28 @@ +/* + * 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.action.apitokens; + +import java.io.IOException; + +import org.opensearch.action.support.nodes.BaseNodeResponse; +import org.opensearch.cluster.node.DiscoveryNode; +import org.opensearch.core.common.io.stream.StreamInput; + +public class ApiTokenUpdateNodeResponse extends BaseNodeResponse { + public ApiTokenUpdateNodeResponse(StreamInput in) throws IOException { + super(in); + } + + public ApiTokenUpdateNodeResponse(DiscoveryNode node) { + super(node); + } +} diff --git a/src/main/java/org/opensearch/security/action/apitokens/ApiTokenUpdateRequest.java b/src/main/java/org/opensearch/security/action/apitokens/ApiTokenUpdateRequest.java new file mode 100644 index 0000000000..f78c0370d5 --- /dev/null +++ b/src/main/java/org/opensearch/security/action/apitokens/ApiTokenUpdateRequest.java @@ -0,0 +1,35 @@ +/* + * 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.action.apitokens; + +import java.io.IOException; + +import org.opensearch.action.support.nodes.BaseNodesRequest; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; + +public class ApiTokenUpdateRequest extends BaseNodesRequest { + + public ApiTokenUpdateRequest(StreamInput in) throws IOException { + super(in); + } + + public ApiTokenUpdateRequest() throws IOException { + super(new String[0]); + } + + @Override + public void writeTo(final StreamOutput out) throws IOException { + super.writeTo(out); + } + +} diff --git a/src/main/java/org/opensearch/security/action/apitokens/ApiTokenUpdateResponse.java b/src/main/java/org/opensearch/security/action/apitokens/ApiTokenUpdateResponse.java new file mode 100644 index 0000000000..99d94bd578 --- /dev/null +++ b/src/main/java/org/opensearch/security/action/apitokens/ApiTokenUpdateResponse.java @@ -0,0 +1,60 @@ +/* + * 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.action.apitokens; + +import java.io.IOException; +import java.util.List; + +import org.opensearch.action.FailedNodeException; +import org.opensearch.action.support.nodes.BaseNodesResponse; +import org.opensearch.cluster.ClusterName; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.xcontent.ToXContentObject; +import org.opensearch.core.xcontent.XContentBuilder; + +public class ApiTokenUpdateResponse extends BaseNodesResponse implements ToXContentObject { + + public ApiTokenUpdateResponse(StreamInput in) throws IOException { + super(in); + } + + public ApiTokenUpdateResponse( + final ClusterName clusterName, + List nodes, + List failures + ) { + super(clusterName, nodes, failures); + } + + @Override + public List readNodesFrom(final StreamInput in) throws IOException { + return in.readList(ApiTokenUpdateNodeResponse::new); + } + + @Override + public void writeNodesTo(final StreamOutput out, List nodes) throws IOException { + out.writeList(nodes); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject("ApiTokenupdate_response"); + builder.field("nodes", getNodesMap()); + builder.field("node_size", getNodes().size()); + builder.field("has_failures", hasFailures()); + builder.field("failures_size", failures().size()); + builder.endObject(); + + return builder; + } +} diff --git a/src/main/java/org/opensearch/security/action/apitokens/TransportApiTokenUpdateAction.java b/src/main/java/org/opensearch/security/action/apitokens/TransportApiTokenUpdateAction.java new file mode 100644 index 0000000000..f47bdfad81 --- /dev/null +++ b/src/main/java/org/opensearch/security/action/apitokens/TransportApiTokenUpdateAction.java @@ -0,0 +1,104 @@ +/* + * 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.action.apitokens; + +import java.io.IOException; +import java.util.List; + +import org.opensearch.action.FailedNodeException; +import org.opensearch.action.support.ActionFilters; +import org.opensearch.action.support.nodes.TransportNodesAction; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.inject.Inject; +import org.opensearch.common.settings.Settings; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.threadpool.ThreadPool; +import org.opensearch.transport.TransportRequest; +import org.opensearch.transport.TransportService; + +public class TransportApiTokenUpdateAction extends TransportNodesAction< + ApiTokenUpdateRequest, + ApiTokenUpdateResponse, + TransportApiTokenUpdateAction.NodeApiTokenUpdateRequest, + ApiTokenUpdateNodeResponse> { + + private final ApiTokenIndexListenerCache apiTokenCache; + private final ClusterService clusterService; + + @Inject + public TransportApiTokenUpdateAction( + Settings settings, + ThreadPool threadPool, + ClusterService clusterService, + TransportService transportService, + ActionFilters actionFilters + ) { + super( + ApiTokenUpdateAction.NAME, + threadPool, + clusterService, + transportService, + actionFilters, + ApiTokenUpdateRequest::new, + TransportApiTokenUpdateAction.NodeApiTokenUpdateRequest::new, + ThreadPool.Names.MANAGEMENT, + ApiTokenUpdateNodeResponse.class + ); + this.apiTokenCache = ApiTokenIndexListenerCache.getInstance(); + this.clusterService = clusterService; + } + + public static class NodeApiTokenUpdateRequest extends TransportRequest { + ApiTokenUpdateRequest request; + + public NodeApiTokenUpdateRequest(ApiTokenUpdateRequest request) { + this.request = request; + } + + public NodeApiTokenUpdateRequest(StreamInput streamInput) throws IOException { + super(streamInput); + this.request = new ApiTokenUpdateRequest(streamInput); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + request.writeTo(out); + } + } + + @Override + protected ApiTokenUpdateNodeResponse newNodeResponse(StreamInput in) throws IOException { + return new ApiTokenUpdateNodeResponse(in); + } + + @Override + protected ApiTokenUpdateResponse newResponse( + ApiTokenUpdateRequest request, + List responses, + List failures + ) { + return new ApiTokenUpdateResponse(this.clusterService.getClusterName(), responses, failures); + } + + @Override + protected NodeApiTokenUpdateRequest newNodeRequest(ApiTokenUpdateRequest request) { + return new NodeApiTokenUpdateRequest(request); + } + + @Override + protected ApiTokenUpdateNodeResponse nodeOperation(final NodeApiTokenUpdateRequest request) { + apiTokenCache.reloadApiTokensFromIndex(); + return new ApiTokenUpdateNodeResponse(clusterService.localNode()); + } +} diff --git a/src/main/java/org/opensearch/security/http/ApiTokenAuthenticator.java b/src/main/java/org/opensearch/security/http/ApiTokenAuthenticator.java index 0da8d5447d..482cb39ff0 100644 --- a/src/main/java/org/opensearch/security/http/ApiTokenAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/ApiTokenAuthenticator.java @@ -141,7 +141,7 @@ private AuthCredentials extractCredentials0(final SecurityRequest request, final } // TODO: handle revocation different from deletion? - if (!cache.getJtis().containsKey(encryptionUtil.encrypt(jwtToken))) { + if (!cache.isValidToken(encryptionUtil.encrypt(jwtToken))) { log.error("Token is not allowlisted"); return null; } diff --git a/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java b/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java index d722231796..13d515ab10 100644 --- a/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java +++ b/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java @@ -429,10 +429,10 @@ PrivilegesEvaluatorResponse apiTokenProvidesClusterPrivilege( String userName = context.getUser().getName(); if (userName.startsWith("apitoken") && userName.contains(":")) { String jti = context.getUser().getName().split(":")[1]; - if (context.getApiTokenIndexListenerCache().getJtis().get(jti) != null) { + if (context.getApiTokenIndexListenerCache().isValidToken(jti)) { // Expand the action groups Set resolvedClusterPermissions = actionGroups.resolve( - context.getApiTokenIndexListenerCache().getJtis().get(jti).getClusterPerm() + context.getApiTokenIndexListenerCache().getPermissionsForJti(jti).getClusterPerm() ); // Check for wildcard permission @@ -921,10 +921,9 @@ PrivilegesEvaluatorResponse apiTokenProvidesIndexPrivilege( String userName = context.getUser().getName(); if (userName.startsWith("apitoken") && userName.contains(":")) { String jti = context.getUser().getName().split(":")[1]; - if (context.getApiTokenIndexListenerCache().getJtis().get(jti) != null) { + if (context.getApiTokenIndexListenerCache().isValidToken(jti)) { List indexPermissions = context.getApiTokenIndexListenerCache() - .getJtis() - .get(jti) + .getPermissionsForJti(jti) .getIndexPermission(); for (String concreteIndex : resolvedIndices.getAllIndices()) { diff --git a/src/test/java/org/opensearch/security/action/apitokens/ApiTokenAuthenticatorTest.java b/src/test/java/org/opensearch/security/action/apitokens/ApiTokenAuthenticatorTest.java index 3de70d1302..0ee0ef3e5c 100644 --- a/src/test/java/org/opensearch/security/action/apitokens/ApiTokenAuthenticatorTest.java +++ b/src/test/java/org/opensearch/security/action/apitokens/ApiTokenAuthenticatorTest.java @@ -31,7 +31,6 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.any; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.mock; @@ -65,7 +64,7 @@ public void setUp() { public void testAuthenticationFailsWhenJtiNotInCache() { String testJti = "test-jti-not-in-cache"; ApiTokenIndexListenerCache cache = ApiTokenIndexListenerCache.getInstance(); - assertFalse(cache.getJtis().containsKey(testJti)); + assertFalse(cache.isValidToken(testJti)); SecurityRequest request = mock(SecurityRequest.class); when(request.header("Authorization")).thenReturn("Bearer " + testJti); @@ -82,9 +81,7 @@ public void testExtractCredentialsPassWhenJtiInCache() { "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ0ZXN0LXRva2VuIiwiYXVkIjoidGVzdC10b2tlbiIsIm5iZiI6MTczNTMyNjM0NywiaXAiOiJnaGlWTXVnWlBtcHZJMjZIT2hmUTRnaEJ1Qkh0Y2x6c1REYVlxQjVBRklyTkE4SzJCVTdxc2toMVBCOEMzQWpTdVBaREM0THVSM2pjZkdpLzlkU2ZicDBuQTNGMkhtSi9jaDA3cDY2ZWJ6OD0iLCJpc3MiOiJvcGVuc2VhcmNoLWNsdXN0ZXIiLCJleHAiOjIyMTg3NjU2NDMzMCwiaWF0IjoxNzM1MzI2MzQ3LCJjcCI6IjlPS3N4aFRaZ1pmOVN1akprRzFXVWIzVC9RVTZ4YmZTc2hrbllkVUhraHM9In0.kqMSnn5YwhLmeiI_8iIBQ5uhmI52n2MNniAa52Zpfs3TiE_PXKiNbDNs08hNqzGYW772gT7lfvp6kZnFxQ4v2Q"; String encryptedTestJti = "k3JQNRXR57Y4V4W1LNkpEP+QzcDra5+/fFfQrr0Rncr/RhYAnYEDgN9RKcJ2Wmjf63NCQa9+HjeFcn0H2wKhMm7pl9bd3ya9FO+LUD7t9Sih4DOjUt0t7ee4ROC0eRK5glMsKsKQVkuY+YKa6A6dT8bMqmt7kIrer7w8TRENr9J8x41TGb/cDDWDvJLME7QkFzJjMxYDgKNiEevMbOpC8yjIZdK08jPe3Thq+xm+JYruoYeyI5g8QjkJA9ZOs1f6eXTAvPxhseuPqgIKykRE25fuWjl5n9tJ9W+jpl+iET7zzOLXSPEU5UepL/COkVd6xW63Ay72oMOewqveDXzyR8S8LAfgRuKgYZWms7yT37XcGg0c6Y7M62KVPo+1XQ+F+K5bgddkd8G+I9KHf561jIMzBcIodgGRj659954W16D1C92+PF/YWPQoTv2hVK4f60H82ga1YSiz3r9UrFV8d7gLJwtyJT9HNPuXO2VZ7xPhre+n1Wv7No0kH2S/r3nqKK6Bk/kn1ZbAmjLxuw13c95lIir6avlKE7XX4PiQDfcGeAyeXOw/36kLW8wH7kjXWdBspld1AiI4fCOaszNXF+7gcuTxIhECl+mEyrJbMI88EWllq+LbydiOrVLFXXRMiCbvj+VTYjzimgJPp+Vuvg=="; - ApiTokenIndexListenerCache cache = ApiTokenIndexListenerCache.getInstance(); - cache.getJtis().put(encryptedTestJti, new Permissions(List.of(), List.of())); - assertTrue(cache.getJtis().containsKey(encryptedTestJti)); + ApiTokenIndexListenerCache.getInstance().getJtis().put(encryptedTestJti, new Permissions(List.of(), List.of())); SecurityRequest request = mock(SecurityRequest.class); when(request.header("Authorization")).thenReturn("Bearer " + testJti); @@ -101,9 +98,7 @@ public void testExtractCredentialsFailWhenTokenIsExpired() { "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ0ZXN0LXRva2VuIiwiYXVkIjoidGVzdC10b2tlbiIsIm5iZiI6MTczNTMyNjU4MiwiaXAiOiJnaGlWTXVnWlBtcHZJMjZIT2hmUTRnaEJ1Qkh0Y2x6c1REYVlxQjVBRklyTkE4SzJCVTdxc2toMVBCOEMzQWpTdVBaREM0THVSM2pjZkdpLzlkU2ZicDBuQTNGMkhtSi9jaDA3cDY2ZWJ6OD0iLCJpc3MiOiJvcGVuc2VhcmNoLWNsdXN0ZXIiLCJleHAiOjI5MDI5NDksImlhdCI6MTczNTMyNjU4MiwiY3AiOiI5T0tzeGhUWmdaZjlTdWpKa0cxV1ViM1QvUVU2eGJmU3Noa25ZZFVIa2hzPSJ9.-f45IAU4jE8EbDuthsPFm-TxtJCk8Q_uRmnG4sEkfLtjmp8mHUbSaS109YRGxKDVr3uEMgFwvkSKEFt7DHhf9A"; String encryptedTestJti = "k3JQNRXR57Y4V4W1LNkpEP+QzcDra5+/fFfQrr0Rncr/RhYAnYEDgN9RKcJ2Wmjf63NCQa9+HjeFcn0H2wKhMm7pl9bd3ya9FO+LUD7t9ShsbOyBUkpFSVuQwrXLatY+glMsKsKQVkuY+YKa6A6dT8bMqmt7kIrer7w8TRENr9J8x41TGb/cDDWDvJLME7QkFzJjMxYDgKNiEevMbOpC8yjIZdK08jPe3Thq+xm+JYruoYeyI5g8QjkJA9ZOs1f6eXTAvPxhseuPqgIKykRE25fuWjl5n9tJ9W+jpl+iET7zzOLXSPEU5UepL/COkVd6xW63Ay72oMOewqveDXzyR8S8LAfgRuKgYZWms7yT37XcGg0c6Y7M62KVPo+1XQ+Fu193YtvS4vqt9G8jHiq51VCRxNHYVlAsratxzvECD8AKBilR9/7dUKyOQDBIzPG4ws+kgI680SgdMgGuLANQPGzal9US8GsWzTbQWCgtObaSVKB02U4gh16wvy3XrXtPz2Z0ZAxoU2Z8opX8hcvB5MG5UUEf+tpgTtVPcbuJyCL42yD3FIc3v/LCYlG/hFvflXBx5c1r+4Tij8Qc/NkYb7/03xiJsVH6eduSqR9M0QBpLm7xg2TgqVMvC/+n96x2V3lS4via4lAK6xuYeRY0ng=="; - ApiTokenIndexListenerCache cache = ApiTokenIndexListenerCache.getInstance(); - cache.getJtis().put(encryptedTestJti, new Permissions(List.of(), List.of())); - assertTrue(cache.getJtis().containsKey(encryptedTestJti)); + ApiTokenIndexListenerCache.getInstance().getJtis().put(encryptedTestJti, new Permissions(List.of(), List.of())); SecurityRequest request = mock(SecurityRequest.class); when(request.header("Authorization")).thenReturn("Bearer " + testJti); @@ -122,9 +117,7 @@ public void testExtractCredentialsFailWhenIssuerDoesNotMatch() { "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ0ZXN0LXRva2VuIiwiYXVkIjoidGVzdC10b2tlbiIsIm5iZiI6MTczNTMyNjM0NywiaXAiOiJnaGlWTXVnWlBtcHZJMjZIT2hmUTRnaEJ1Qkh0Y2x6c1REYVlxQjVBRklyTkE4SzJCVTdxc2toMVBCOEMzQWpTdVBaREM0THVSM2pjZkdpLzlkU2ZicDBuQTNGMkhtSi9jaDA3cDY2ZWJ6OD0iLCJpc3MiOiJvcGVuc2VhcmNoLWNsdXN0ZXIiLCJleHAiOjIyMTg3NjU2NDMzMCwiaWF0IjoxNzM1MzI2MzQ3LCJjcCI6IjlPS3N4aFRaZ1pmOVN1akprRzFXVWIzVC9RVTZ4YmZTc2hrbllkVUhraHM9In0.kqMSnn5YwhLmeiI_8iIBQ5uhmI52n2MNniAa52Zpfs3TiE_PXKiNbDNs08hNqzGYW772gT7lfvp6kZnFxQ4v2Q"; String encryptedTestJti = "k3JQNRXR57Y4V4W1LNkpEP+QzcDra5+/fFfQrr0Rncr/RhYAnYEDgN9RKcJ2Wmjf63NCQa9+HjeFcn0H2wKhMm7pl9bd3ya9FO+LUD7t9Sih4DOjUt0t7ee4ROC0eRK5glMsKsKQVkuY+YKa6A6dT8bMqmt7kIrer7w8TRENr9J8x41TGb/cDDWDvJLME7QkFzJjMxYDgKNiEevMbOpC8yjIZdK08jPe3Thq+xm+JYruoYeyI5g8QjkJA9ZOs1f6eXTAvPxhseuPqgIKykRE25fuWjl5n9tJ9W+jpl+iET7zzOLXSPEU5UepL/COkVd6xW63Ay72oMOewqveDXzyR8S8LAfgRuKgYZWms7yT37XcGg0c6Y7M62KVPo+1XQ+F+K5bgddkd8G+I9KHf561jIMzBcIodgGRj659954W16D1C92+PF/YWPQoTv2hVK4f60H82ga1YSiz3r9UrFV8d7gLJwtyJT9HNPuXO2VZ7xPhre+n1Wv7No0kH2S/r3nqKK6Bk/kn1ZbAmjLxuw13c95lIir6avlKE7XX4PiQDfcGeAyeXOw/36kLW8wH7kjXWdBspld1AiI4fCOaszNXF+7gcuTxIhECl+mEyrJbMI88EWllq+LbydiOrVLFXXRMiCbvj+VTYjzimgJPp+Vuvg=="; - ApiTokenIndexListenerCache cache = ApiTokenIndexListenerCache.getInstance(); - cache.getJtis().put(encryptedTestJti, new Permissions(List.of(), List.of())); - assertTrue(cache.getJtis().containsKey(encryptedTestJti)); + ApiTokenIndexListenerCache.getInstance().getJtis().put(encryptedTestJti, new Permissions(List.of(), List.of())); SecurityRequest request = mock(SecurityRequest.class); when(request.header("Authorization")).thenReturn("Bearer " + testJti); @@ -151,9 +144,7 @@ public void testExtractCredentialsFailWhenAccessingRestrictedEndpoint() { "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ0ZXN0LXRva2VuIiwiYXVkIjoidGVzdC10b2tlbiIsIm5iZiI6MTczNTMyNjM0NywiaXAiOiJnaGlWTXVnWlBtcHZJMjZIT2hmUTRnaEJ1Qkh0Y2x6c1REYVlxQjVBRklyTkE4SzJCVTdxc2toMVBCOEMzQWpTdVBaREM0THVSM2pjZkdpLzlkU2ZicDBuQTNGMkhtSi9jaDA3cDY2ZWJ6OD0iLCJpc3MiOiJvcGVuc2VhcmNoLWNsdXN0ZXIiLCJleHAiOjIyMTg3NjU2NDMzMCwiaWF0IjoxNzM1MzI2MzQ3LCJjcCI6IjlPS3N4aFRaZ1pmOVN1akprRzFXVWIzVC9RVTZ4YmZTc2hrbllkVUhraHM9In0.kqMSnn5YwhLmeiI_8iIBQ5uhmI52n2MNniAa52Zpfs3TiE_PXKiNbDNs08hNqzGYW772gT7lfvp6kZnFxQ4v2Q"; String encryptedTestJti = "k3JQNRXR57Y4V4W1LNkpEP+QzcDra5+/fFfQrr0Rncr/RhYAnYEDgN9RKcJ2Wmjf63NCQa9+HjeFcn0H2wKhMm7pl9bd3ya9FO+LUD7t9Sih4DOjUt0t7ee4ROC0eRK5glMsKsKQVkuY+YKa6A6dT8bMqmt7kIrer7w8TRENr9J8x41TGb/cDDWDvJLME7QkFzJjMxYDgKNiEevMbOpC8yjIZdK08jPe3Thq+xm+JYruoYeyI5g8QjkJA9ZOs1f6eXTAvPxhseuPqgIKykRE25fuWjl5n9tJ9W+jpl+iET7zzOLXSPEU5UepL/COkVd6xW63Ay72oMOewqveDXzyR8S8LAfgRuKgYZWms7yT37XcGg0c6Y7M62KVPo+1XQ+F+K5bgddkd8G+I9KHf561jIMzBcIodgGRj659954W16D1C92+PF/YWPQoTv2hVK4f60H82ga1YSiz3r9UrFV8d7gLJwtyJT9HNPuXO2VZ7xPhre+n1Wv7No0kH2S/r3nqKK6Bk/kn1ZbAmjLxuw13c95lIir6avlKE7XX4PiQDfcGeAyeXOw/36kLW8wH7kjXWdBspld1AiI4fCOaszNXF+7gcuTxIhECl+mEyrJbMI88EWllq+LbydiOrVLFXXRMiCbvj+VTYjzimgJPp+Vuvg=="; - ApiTokenIndexListenerCache cache = ApiTokenIndexListenerCache.getInstance(); - cache.getJtis().put(encryptedTestJti, new Permissions(List.of(), List.of())); - assertTrue(cache.getJtis().containsKey(encryptedTestJti)); + ApiTokenIndexListenerCache.getInstance().getJtis().put(encryptedTestJti, new Permissions(List.of(), List.of())); SecurityRequest request = mock(SecurityRequest.class); when(request.header("Authorization")).thenReturn("Bearer " + testJti); @@ -163,16 +154,13 @@ public void testExtractCredentialsFailWhenAccessingRestrictedEndpoint() { assertNull("Should return null when JTI is being used to access restricted endpoint", ac); verify(log).error("OpenSearchException[Api Tokens are not allowed to be used for accessing this endpoint.]"); - } @Test public void testAuthenticatorNotEnabled() { String encryptedTestJti = "k3JQNRXR57Y4V4W1LNkpEP+QzcDra5+/fFfQrr0Rncr/RhYAnYEDgN9RKcJ2Wmjf63NCQa9+HjeFcn0H2wKhMm7pl9bd3ya9FO+LUD7t9Sih4DOjUt0t7ee4ROC0eRK5glMsKsKQVkuY+YKa6A6dT8bMqmt7kIrer7w8TRENr9J8x41TGb/cDDWDvJLME7QkFzJjMxYDgKNiEevMbOpC8yjIZdK08jPe3Thq+xm+JYruoYeyI5g8QjkJA9ZOs1f6eXTAvPxhseuPqgIKykRE25fuWjl5n9tJ9W+jpl+iET7zzOLXSPEU5UepL/COkVd6xW63Ay72oMOewqveDXzyR8S8LAfgRuKgYZWms7yT37XcGg0c6Y7M62KVPo+1XQ+F+K5bgddkd8G+I9KHf561jIMzBcIodgGRj659954W16D1C92+PF/YWPQoTv2hVK4f60H82ga1YSiz3r9UrFV8d7gLJwtyJT9HNPuXO2VZ7xPhre+n1Wv7No0kH2S/r3nqKK6Bk/kn1ZbAmjLxuw13c95lIir6avlKE7XX4PiQDfcGeAyeXOw/36kLW8wH7kjXWdBspld1AiI4fCOaszNXF+7gcuTxIhECl+mEyrJbMI88EWllq+LbydiOrVLFXXRMiCbvj+VTYjzimgJPp+Vuvg=="; - ApiTokenIndexListenerCache cache = ApiTokenIndexListenerCache.getInstance(); - cache.getJtis().put(encryptedTestJti, new Permissions(List.of(), List.of())); - assertTrue(cache.getJtis().containsKey(encryptedTestJti)); + ApiTokenIndexListenerCache.getInstance().getJtis().put(encryptedTestJti, new Permissions(List.of(), List.of())); SecurityRequest request = mock(SecurityRequest.class); From b90bae9de87bd8be3e697fa1732f6c573c10acf3 Mon Sep 17 00:00:00 2001 From: Derek Ho Date: Mon, 6 Jan 2025 14:30:09 -0500 Subject: [PATCH 13/20] Cleanup tests and constants Signed-off-by: Derek Ho --- .../apitokens/ApiTokenIndexListenerCache.java | 11 -- .../security/http/ApiTokenAuthenticator.java | 6 +- .../security/privileges/ActionPrivileges.java | 10 +- .../apitokens/ApiTokenAuthenticatorTest.java | 112 ++++++++++++------ 4 files changed, 81 insertions(+), 58 deletions(-) diff --git a/src/main/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCache.java b/src/main/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCache.java index a27c1e06db..501638e9d4 100644 --- a/src/main/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCache.java +++ b/src/main/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCache.java @@ -74,13 +74,9 @@ void reloadApiTokensFromIndex() { } try { - // Clear existing caches - log.info("Reloading API tokens cache from index: {}", jtis.entrySet().toString()); - idToJtiMap.clear(); jtis.clear(); - // Search request to get all API tokens from the security index client.prepareSearch(getSecurityIndexName()) .setQuery(QueryBuilders.matchAllQuery()) .execute() @@ -104,24 +100,17 @@ void reloadApiTokensFromIndex() { } private String getSecurityIndexName() { - // Return the name of your security index return ConfigConstants.OPENSEARCH_API_TOKENS_INDEX; } @SuppressWarnings("unchecked") private Permissions parsePermissions(Map source) { - // Implement parsing logic for permissions from the document return new Permissions( (List) source.get(ApiToken.CLUSTER_PERMISSIONS_FIELD), (List) source.get(ApiToken.INDEX_PERMISSIONS_FIELD) ); } - // Getter methods for cached data - public String getJtiForId(String id) { - return idToJtiMap.get(id); - } - public Permissions getPermissionsForJti(String jti) { return jtis.get(jti); } diff --git a/src/main/java/org/opensearch/security/http/ApiTokenAuthenticator.java b/src/main/java/org/opensearch/security/http/ApiTokenAuthenticator.java index 482cb39ff0..86086eee1e 100644 --- a/src/main/java/org/opensearch/security/http/ApiTokenAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/ApiTokenAuthenticator.java @@ -60,6 +60,7 @@ public class ApiTokenAuthenticator implements HTTPAuthenticator { private final String encryptionKey; private final Boolean apiTokenEnabled; private final String clusterName; + public static final String API_TOKEN_USER_PREFIX = "apitoken:"; private final EncryptionDecryptionUtil encryptionUtil; @@ -161,10 +162,7 @@ private AuthCredentials extractCredentials0(final SecurityRequest request, final return null; } - final AuthCredentials ac = new AuthCredentials("apitoken_" + subject + ":" + encryptionUtil.encrypt(jwtToken), List.of(), "") - .markComplete(); - - return ac; + return new AuthCredentials(API_TOKEN_USER_PREFIX + encryptionUtil.encrypt(jwtToken), List.of(), "").markComplete(); } catch (WeakKeyException e) { log.error("Cannot authenticate user with JWT because of ", e); diff --git a/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java b/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java index 13d515ab10..a3bb2dc3ad 100644 --- a/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java +++ b/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java @@ -49,6 +49,8 @@ import com.selectivem.collections.DeduplicatingCompactSubSetBuilder; import com.selectivem.collections.ImmutableCompactSubSet; +import static org.opensearch.security.http.ApiTokenAuthenticator.API_TOKEN_USER_PREFIX; + /** * This class converts role configuration into pre-computed, optimized data structures for checking privileges. *

@@ -427,8 +429,8 @@ PrivilegesEvaluatorResponse apiTokenProvidesClusterPrivilege( Boolean explicit ) { String userName = context.getUser().getName(); - if (userName.startsWith("apitoken") && userName.contains(":")) { - String jti = context.getUser().getName().split(":")[1]; + if (userName.startsWith(API_TOKEN_USER_PREFIX)) { + String jti = context.getUser().getName().split(API_TOKEN_USER_PREFIX)[1]; if (context.getApiTokenIndexListenerCache().isValidToken(jti)) { // Expand the action groups Set resolvedClusterPermissions = actionGroups.resolve( @@ -919,8 +921,8 @@ PrivilegesEvaluatorResponse apiTokenProvidesIndexPrivilege( Boolean explicit ) { String userName = context.getUser().getName(); - if (userName.startsWith("apitoken") && userName.contains(":")) { - String jti = context.getUser().getName().split(":")[1]; + if (userName.startsWith(API_TOKEN_USER_PREFIX)) { + String jti = context.getUser().getName().split(API_TOKEN_USER_PREFIX)[1]; if (context.getApiTokenIndexListenerCache().isValidToken(jti)) { List indexPermissions = context.getApiTokenIndexListenerCache() .getPermissionsForJti(jti) diff --git a/src/test/java/org/opensearch/security/action/apitokens/ApiTokenAuthenticatorTest.java b/src/test/java/org/opensearch/security/action/apitokens/ApiTokenAuthenticatorTest.java index 0ee0ef3e5c..916b7d86fc 100644 --- a/src/test/java/org/opensearch/security/action/apitokens/ApiTokenAuthenticatorTest.java +++ b/src/test/java/org/opensearch/security/action/apitokens/ApiTokenAuthenticatorTest.java @@ -11,6 +11,11 @@ package org.opensearch.security.action.apitokens; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Base64; +import java.util.Date; import java.util.List; import org.apache.logging.log4j.Logger; @@ -20,11 +25,14 @@ import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.security.authtoken.jwt.EncryptionDecryptionUtil; import org.opensearch.security.filter.SecurityRequest; import org.opensearch.security.http.ApiTokenAuthenticator; import org.opensearch.security.user.AuthCredentials; import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; @@ -45,13 +53,17 @@ public class ApiTokenAuthenticatorTest { private Logger log; private ThreadContext threadcontext; + private final String signingKey = Base64.getEncoder() + .encodeToString("jwt signing key long enough for secure api token authentication testing".getBytes(StandardCharsets.UTF_8)); + private final String encryptionKey = Base64.getEncoder().encodeToString("123456678910".getBytes(StandardCharsets.UTF_8)); + private final EncryptionDecryptionUtil encryptionUtil = new EncryptionDecryptionUtil(encryptionKey); @Before public void setUp() { Settings settings = Settings.builder() .put("enabled", "true") - .put("signing_key", "U3VwZXJTZWNyZXRLZXlUaGF0SXNFeGFjdGx5NjRCeXRlc0xvbmdBbmRXaWxsV29ya1dpdGhIUzUxMkFsZ29yaXRobSEhCgo=") - .put("encryption_key", "MTIzNDU2Nzg5MDEyMzQ1Ng==") + .put("signing_key", signingKey) + .put("encryption_key", encryptionKey) .build(); authenticator = new ApiTokenAuthenticator(settings, "opensearch-cluster"); @@ -77,14 +89,20 @@ public void testAuthenticationFailsWhenJtiNotInCache() { @Test public void testExtractCredentialsPassWhenJtiInCache() { - String testJti = - "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ0ZXN0LXRva2VuIiwiYXVkIjoidGVzdC10b2tlbiIsIm5iZiI6MTczNTMyNjM0NywiaXAiOiJnaGlWTXVnWlBtcHZJMjZIT2hmUTRnaEJ1Qkh0Y2x6c1REYVlxQjVBRklyTkE4SzJCVTdxc2toMVBCOEMzQWpTdVBaREM0THVSM2pjZkdpLzlkU2ZicDBuQTNGMkhtSi9jaDA3cDY2ZWJ6OD0iLCJpc3MiOiJvcGVuc2VhcmNoLWNsdXN0ZXIiLCJleHAiOjIyMTg3NjU2NDMzMCwiaWF0IjoxNzM1MzI2MzQ3LCJjcCI6IjlPS3N4aFRaZ1pmOVN1akprRzFXVWIzVC9RVTZ4YmZTc2hrbllkVUhraHM9In0.kqMSnn5YwhLmeiI_8iIBQ5uhmI52n2MNniAa52Zpfs3TiE_PXKiNbDNs08hNqzGYW772gT7lfvp6kZnFxQ4v2Q"; - String encryptedTestJti = - "k3JQNRXR57Y4V4W1LNkpEP+QzcDra5+/fFfQrr0Rncr/RhYAnYEDgN9RKcJ2Wmjf63NCQa9+HjeFcn0H2wKhMm7pl9bd3ya9FO+LUD7t9Sih4DOjUt0t7ee4ROC0eRK5glMsKsKQVkuY+YKa6A6dT8bMqmt7kIrer7w8TRENr9J8x41TGb/cDDWDvJLME7QkFzJjMxYDgKNiEevMbOpC8yjIZdK08jPe3Thq+xm+JYruoYeyI5g8QjkJA9ZOs1f6eXTAvPxhseuPqgIKykRE25fuWjl5n9tJ9W+jpl+iET7zzOLXSPEU5UepL/COkVd6xW63Ay72oMOewqveDXzyR8S8LAfgRuKgYZWms7yT37XcGg0c6Y7M62KVPo+1XQ+F+K5bgddkd8G+I9KHf561jIMzBcIodgGRj659954W16D1C92+PF/YWPQoTv2hVK4f60H82ga1YSiz3r9UrFV8d7gLJwtyJT9HNPuXO2VZ7xPhre+n1Wv7No0kH2S/r3nqKK6Bk/kn1ZbAmjLxuw13c95lIir6avlKE7XX4PiQDfcGeAyeXOw/36kLW8wH7kjXWdBspld1AiI4fCOaszNXF+7gcuTxIhECl+mEyrJbMI88EWllq+LbydiOrVLFXXRMiCbvj+VTYjzimgJPp+Vuvg=="; - ApiTokenIndexListenerCache.getInstance().getJtis().put(encryptedTestJti, new Permissions(List.of(), List.of())); + String token = Jwts.builder() + .setIssuer("opensearch-cluster") + .setSubject("test-token") + .setAudience("test-token") + .setIssuedAt(Date.from(Instant.now())) + .setExpiration(Date.from(Instant.now().plus(1, ChronoUnit.DAYS))) + .signWith(SignatureAlgorithm.HS512, signingKey) + .compact(); + + String encryptedToken = encryptionUtil.encrypt(token); + ApiTokenIndexListenerCache.getInstance().getJtis().put(encryptedToken, new Permissions(List.of(), List.of())); SecurityRequest request = mock(SecurityRequest.class); - when(request.header("Authorization")).thenReturn("Bearer " + testJti); + when(request.header("Authorization")).thenReturn("Bearer " + token); when(request.path()).thenReturn("/test"); AuthCredentials ac = authenticator.extractCredentials(request, threadcontext); @@ -94,14 +112,20 @@ public void testExtractCredentialsPassWhenJtiInCache() { @Test public void testExtractCredentialsFailWhenTokenIsExpired() { - String testJti = - "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ0ZXN0LXRva2VuIiwiYXVkIjoidGVzdC10b2tlbiIsIm5iZiI6MTczNTMyNjU4MiwiaXAiOiJnaGlWTXVnWlBtcHZJMjZIT2hmUTRnaEJ1Qkh0Y2x6c1REYVlxQjVBRklyTkE4SzJCVTdxc2toMVBCOEMzQWpTdVBaREM0THVSM2pjZkdpLzlkU2ZicDBuQTNGMkhtSi9jaDA3cDY2ZWJ6OD0iLCJpc3MiOiJvcGVuc2VhcmNoLWNsdXN0ZXIiLCJleHAiOjI5MDI5NDksImlhdCI6MTczNTMyNjU4MiwiY3AiOiI5T0tzeGhUWmdaZjlTdWpKa0cxV1ViM1QvUVU2eGJmU3Noa25ZZFVIa2hzPSJ9.-f45IAU4jE8EbDuthsPFm-TxtJCk8Q_uRmnG4sEkfLtjmp8mHUbSaS109YRGxKDVr3uEMgFwvkSKEFt7DHhf9A"; - String encryptedTestJti = - "k3JQNRXR57Y4V4W1LNkpEP+QzcDra5+/fFfQrr0Rncr/RhYAnYEDgN9RKcJ2Wmjf63NCQa9+HjeFcn0H2wKhMm7pl9bd3ya9FO+LUD7t9ShsbOyBUkpFSVuQwrXLatY+glMsKsKQVkuY+YKa6A6dT8bMqmt7kIrer7w8TRENr9J8x41TGb/cDDWDvJLME7QkFzJjMxYDgKNiEevMbOpC8yjIZdK08jPe3Thq+xm+JYruoYeyI5g8QjkJA9ZOs1f6eXTAvPxhseuPqgIKykRE25fuWjl5n9tJ9W+jpl+iET7zzOLXSPEU5UepL/COkVd6xW63Ay72oMOewqveDXzyR8S8LAfgRuKgYZWms7yT37XcGg0c6Y7M62KVPo+1XQ+Fu193YtvS4vqt9G8jHiq51VCRxNHYVlAsratxzvECD8AKBilR9/7dUKyOQDBIzPG4ws+kgI680SgdMgGuLANQPGzal9US8GsWzTbQWCgtObaSVKB02U4gh16wvy3XrXtPz2Z0ZAxoU2Z8opX8hcvB5MG5UUEf+tpgTtVPcbuJyCL42yD3FIc3v/LCYlG/hFvflXBx5c1r+4Tij8Qc/NkYb7/03xiJsVH6eduSqR9M0QBpLm7xg2TgqVMvC/+n96x2V3lS4via4lAK6xuYeRY0ng=="; - ApiTokenIndexListenerCache.getInstance().getJtis().put(encryptedTestJti, new Permissions(List.of(), List.of())); + String token = Jwts.builder() + .setIssuer("opensearch-cluster") + .setSubject("test-token") + .setAudience("test-token") + .setIssuedAt(Date.from(Instant.now())) + .setExpiration(Date.from(Instant.now().minus(1, ChronoUnit.DAYS))) + .signWith(SignatureAlgorithm.HS512, signingKey) + .compact(); + + String encryptedToken = encryptionUtil.encrypt(token); + ApiTokenIndexListenerCache.getInstance().getJtis().put(encryptedToken, new Permissions(List.of(), List.of())); SecurityRequest request = mock(SecurityRequest.class); - when(request.header("Authorization")).thenReturn("Bearer " + testJti); + when(request.header("Authorization")).thenReturn("Bearer " + token); when(request.path()).thenReturn("/test"); AuthCredentials ac = authenticator.extractCredentials(request, threadcontext); @@ -113,25 +137,22 @@ public void testExtractCredentialsFailWhenTokenIsExpired() { @Test public void testExtractCredentialsFailWhenIssuerDoesNotMatch() { - String testJti = - "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ0ZXN0LXRva2VuIiwiYXVkIjoidGVzdC10b2tlbiIsIm5iZiI6MTczNTMyNjM0NywiaXAiOiJnaGlWTXVnWlBtcHZJMjZIT2hmUTRnaEJ1Qkh0Y2x6c1REYVlxQjVBRklyTkE4SzJCVTdxc2toMVBCOEMzQWpTdVBaREM0THVSM2pjZkdpLzlkU2ZicDBuQTNGMkhtSi9jaDA3cDY2ZWJ6OD0iLCJpc3MiOiJvcGVuc2VhcmNoLWNsdXN0ZXIiLCJleHAiOjIyMTg3NjU2NDMzMCwiaWF0IjoxNzM1MzI2MzQ3LCJjcCI6IjlPS3N4aFRaZ1pmOVN1akprRzFXVWIzVC9RVTZ4YmZTc2hrbllkVUhraHM9In0.kqMSnn5YwhLmeiI_8iIBQ5uhmI52n2MNniAa52Zpfs3TiE_PXKiNbDNs08hNqzGYW772gT7lfvp6kZnFxQ4v2Q"; - String encryptedTestJti = - "k3JQNRXR57Y4V4W1LNkpEP+QzcDra5+/fFfQrr0Rncr/RhYAnYEDgN9RKcJ2Wmjf63NCQa9+HjeFcn0H2wKhMm7pl9bd3ya9FO+LUD7t9Sih4DOjUt0t7ee4ROC0eRK5glMsKsKQVkuY+YKa6A6dT8bMqmt7kIrer7w8TRENr9J8x41TGb/cDDWDvJLME7QkFzJjMxYDgKNiEevMbOpC8yjIZdK08jPe3Thq+xm+JYruoYeyI5g8QjkJA9ZOs1f6eXTAvPxhseuPqgIKykRE25fuWjl5n9tJ9W+jpl+iET7zzOLXSPEU5UepL/COkVd6xW63Ay72oMOewqveDXzyR8S8LAfgRuKgYZWms7yT37XcGg0c6Y7M62KVPo+1XQ+F+K5bgddkd8G+I9KHf561jIMzBcIodgGRj659954W16D1C92+PF/YWPQoTv2hVK4f60H82ga1YSiz3r9UrFV8d7gLJwtyJT9HNPuXO2VZ7xPhre+n1Wv7No0kH2S/r3nqKK6Bk/kn1ZbAmjLxuw13c95lIir6avlKE7XX4PiQDfcGeAyeXOw/36kLW8wH7kjXWdBspld1AiI4fCOaszNXF+7gcuTxIhECl+mEyrJbMI88EWllq+LbydiOrVLFXXRMiCbvj+VTYjzimgJPp+Vuvg=="; - ApiTokenIndexListenerCache.getInstance().getJtis().put(encryptedTestJti, new Permissions(List.of(), List.of())); + String token = Jwts.builder() + .setIssuer("not-opensearch-cluster") + .setSubject("test-token") + .setAudience("test-token") + .setIssuedAt(Date.from(Instant.now())) + .setExpiration(Date.from(Instant.now().plus(1, ChronoUnit.DAYS))) + .signWith(SignatureAlgorithm.HS512, signingKey) + .compact(); + + String encryptedToken = encryptionUtil.encrypt(token); + ApiTokenIndexListenerCache.getInstance().getJtis().put(encryptedToken, new Permissions(List.of(), List.of())); SecurityRequest request = mock(SecurityRequest.class); - when(request.header("Authorization")).thenReturn("Bearer " + testJti); + when(request.header("Authorization")).thenReturn("Bearer " + token); when(request.path()).thenReturn("/test"); - Settings settings = Settings.builder() - .put("enabled", "true") - .put("signing_key", "U3VwZXJTZWNyZXRLZXlUaGF0SXNFeGFjdGx5NjRCeXRlc0xvbmdBbmRXaWxsV29ya1dpdGhIUzUxMkFsZ29yaXRobSEhCgo=") - .put("encryption_key", "MTIzNDU2Nzg5MDEyMzQ1Ng==") - .build(); - - authenticator = new ApiTokenAuthenticator(settings, "opensearch-cluster-name-mismatch"); - authenticator.log = log; - AuthCredentials ac = authenticator.extractCredentials(request, threadcontext); assertNull("Should return null when issuer does not match cluster", ac); @@ -140,14 +161,20 @@ public void testExtractCredentialsFailWhenIssuerDoesNotMatch() { @Test public void testExtractCredentialsFailWhenAccessingRestrictedEndpoint() { - String testJti = - "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ0ZXN0LXRva2VuIiwiYXVkIjoidGVzdC10b2tlbiIsIm5iZiI6MTczNTMyNjM0NywiaXAiOiJnaGlWTXVnWlBtcHZJMjZIT2hmUTRnaEJ1Qkh0Y2x6c1REYVlxQjVBRklyTkE4SzJCVTdxc2toMVBCOEMzQWpTdVBaREM0THVSM2pjZkdpLzlkU2ZicDBuQTNGMkhtSi9jaDA3cDY2ZWJ6OD0iLCJpc3MiOiJvcGVuc2VhcmNoLWNsdXN0ZXIiLCJleHAiOjIyMTg3NjU2NDMzMCwiaWF0IjoxNzM1MzI2MzQ3LCJjcCI6IjlPS3N4aFRaZ1pmOVN1akprRzFXVWIzVC9RVTZ4YmZTc2hrbllkVUhraHM9In0.kqMSnn5YwhLmeiI_8iIBQ5uhmI52n2MNniAa52Zpfs3TiE_PXKiNbDNs08hNqzGYW772gT7lfvp6kZnFxQ4v2Q"; - String encryptedTestJti = - "k3JQNRXR57Y4V4W1LNkpEP+QzcDra5+/fFfQrr0Rncr/RhYAnYEDgN9RKcJ2Wmjf63NCQa9+HjeFcn0H2wKhMm7pl9bd3ya9FO+LUD7t9Sih4DOjUt0t7ee4ROC0eRK5glMsKsKQVkuY+YKa6A6dT8bMqmt7kIrer7w8TRENr9J8x41TGb/cDDWDvJLME7QkFzJjMxYDgKNiEevMbOpC8yjIZdK08jPe3Thq+xm+JYruoYeyI5g8QjkJA9ZOs1f6eXTAvPxhseuPqgIKykRE25fuWjl5n9tJ9W+jpl+iET7zzOLXSPEU5UepL/COkVd6xW63Ay72oMOewqveDXzyR8S8LAfgRuKgYZWms7yT37XcGg0c6Y7M62KVPo+1XQ+F+K5bgddkd8G+I9KHf561jIMzBcIodgGRj659954W16D1C92+PF/YWPQoTv2hVK4f60H82ga1YSiz3r9UrFV8d7gLJwtyJT9HNPuXO2VZ7xPhre+n1Wv7No0kH2S/r3nqKK6Bk/kn1ZbAmjLxuw13c95lIir6avlKE7XX4PiQDfcGeAyeXOw/36kLW8wH7kjXWdBspld1AiI4fCOaszNXF+7gcuTxIhECl+mEyrJbMI88EWllq+LbydiOrVLFXXRMiCbvj+VTYjzimgJPp+Vuvg=="; - ApiTokenIndexListenerCache.getInstance().getJtis().put(encryptedTestJti, new Permissions(List.of(), List.of())); + String token = Jwts.builder() + .setIssuer("opensearch-cluster") + .setSubject("test-token") + .setAudience("test-token") + .setIssuedAt(Date.from(Instant.now())) + .setExpiration(Date.from(Instant.now().plus(1, ChronoUnit.DAYS))) + .signWith(SignatureAlgorithm.HS512, signingKey) + .compact(); + + String encryptedToken = encryptionUtil.encrypt(token); + ApiTokenIndexListenerCache.getInstance().getJtis().put(encryptedToken, new Permissions(List.of(), List.of())); SecurityRequest request = mock(SecurityRequest.class); - when(request.header("Authorization")).thenReturn("Bearer " + testJti); + when(request.header("Authorization")).thenReturn("Bearer " + token); when(request.path()).thenReturn("/_plugins/_security/api/apitokens"); AuthCredentials ac = authenticator.extractCredentials(request, threadcontext); @@ -158,9 +185,16 @@ public void testExtractCredentialsFailWhenAccessingRestrictedEndpoint() { @Test public void testAuthenticatorNotEnabled() { - String encryptedTestJti = - "k3JQNRXR57Y4V4W1LNkpEP+QzcDra5+/fFfQrr0Rncr/RhYAnYEDgN9RKcJ2Wmjf63NCQa9+HjeFcn0H2wKhMm7pl9bd3ya9FO+LUD7t9Sih4DOjUt0t7ee4ROC0eRK5glMsKsKQVkuY+YKa6A6dT8bMqmt7kIrer7w8TRENr9J8x41TGb/cDDWDvJLME7QkFzJjMxYDgKNiEevMbOpC8yjIZdK08jPe3Thq+xm+JYruoYeyI5g8QjkJA9ZOs1f6eXTAvPxhseuPqgIKykRE25fuWjl5n9tJ9W+jpl+iET7zzOLXSPEU5UepL/COkVd6xW63Ay72oMOewqveDXzyR8S8LAfgRuKgYZWms7yT37XcGg0c6Y7M62KVPo+1XQ+F+K5bgddkd8G+I9KHf561jIMzBcIodgGRj659954W16D1C92+PF/YWPQoTv2hVK4f60H82ga1YSiz3r9UrFV8d7gLJwtyJT9HNPuXO2VZ7xPhre+n1Wv7No0kH2S/r3nqKK6Bk/kn1ZbAmjLxuw13c95lIir6avlKE7XX4PiQDfcGeAyeXOw/36kLW8wH7kjXWdBspld1AiI4fCOaszNXF+7gcuTxIhECl+mEyrJbMI88EWllq+LbydiOrVLFXXRMiCbvj+VTYjzimgJPp+Vuvg=="; - ApiTokenIndexListenerCache.getInstance().getJtis().put(encryptedTestJti, new Permissions(List.of(), List.of())); + String token = Jwts.builder() + .setIssuer("opensearch-cluster") + .setSubject("test-token") + .setAudience("test-token") + .setIssuedAt(Date.from(Instant.now())) + .setExpiration(Date.from(Instant.now().plus(1, ChronoUnit.DAYS))) + .signWith(SignatureAlgorithm.HS512, signingKey) + .compact(); + String encryptedToken = encryptionUtil.encrypt(token); + ApiTokenIndexListenerCache.getInstance().getJtis().put(encryptedToken, new Permissions(List.of(), List.of())); SecurityRequest request = mock(SecurityRequest.class); @@ -171,7 +205,7 @@ public void testAuthenticatorNotEnabled() { .build(); ThreadContext threadContext = new ThreadContext(settings); - authenticator = new ApiTokenAuthenticator(settings, "opensearch-cluster-name-mismatch"); + authenticator = new ApiTokenAuthenticator(settings, "opensearch-cluster"); authenticator.log = log; AuthCredentials ac = authenticator.extractCredentials(request, threadContext); From 552aedadf7fc88649b54e213fe8351c3a449d5ae Mon Sep 17 00:00:00 2001 From: Derek Ho Date: Mon, 6 Jan 2025 14:48:47 -0500 Subject: [PATCH 14/20] Fix test Signed-off-by: Derek Ho --- .../security/privileges/ActionPrivilegesTest.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/integrationTest/java/org/opensearch/security/privileges/ActionPrivilegesTest.java b/src/integrationTest/java/org/opensearch/security/privileges/ActionPrivilegesTest.java index ecd76b127c..7bdb0980c1 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/ActionPrivilegesTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/ActionPrivilegesTest.java @@ -270,7 +270,7 @@ public void apiToken_explicit_failsWithWildcard() throws Exception { " - '*'", CType.ROLES); ActionPrivileges subject = new ActionPrivileges(roles, FlattenedActionGroups.EMPTY, null, Settings.EMPTY); String token = "blah"; - PrivilegesEvaluationContext context = ctxWithUserName("apitoken_test:" + token); + PrivilegesEvaluationContext context = ctxWithUserName("apitoken:" + token); context.getApiTokenIndexListenerCache().getJtis().put(token, new Permissions(List.of("*"), List.of())); // Explicit fails assertThat( @@ -290,7 +290,7 @@ public void apiToken_succeedsWithExactMatch() throws Exception { " - '*'", CType.ROLES); ActionPrivileges subject = new ActionPrivileges(roles, FlattenedActionGroups.EMPTY, null, Settings.EMPTY); String token = "blah"; - PrivilegesEvaluationContext context = ctxWithUserName("apitoken_test:" + token); + PrivilegesEvaluationContext context = ctxWithUserName("apitoken:" + token); context.getApiTokenIndexListenerCache().getJtis().put(token, new Permissions(List.of("cluster:whatever"), List.of())); // Explicit succeeds assertThat(subject.hasExplicitClusterPrivilege(context, "cluster:whatever"), isAllowed()); @@ -316,7 +316,7 @@ public void apiToken_succeedsWithActionGroupsExapnded() throws Exception { FlattenedActionGroups actionGroups = new FlattenedActionGroups(config); ActionPrivileges subject = new ActionPrivileges(roles, actionGroups, null, Settings.EMPTY); String token = "blah"; - PrivilegesEvaluationContext context = ctxWithUserName("apitoken_test:" + token); + PrivilegesEvaluationContext context = ctxWithUserName("apitoken:" + token); context.getApiTokenIndexListenerCache().getJtis().put(token, new Permissions(List.of("CLUSTER_ALL"), List.of())); // Explicit succeeds assertThat(subject.hasExplicitClusterPrivilege(context, "cluster:whatever"), isAllowed()); @@ -362,7 +362,7 @@ public void positive_full() throws Exception { @Test public void apiTokens_positive_full() throws Exception { String token = "blah"; - PrivilegesEvaluationContext context = ctxWithUserName("apitoken_test:" + token); + PrivilegesEvaluationContext context = ctxWithUserName("apitoken:" + token); context.getApiTokenIndexListenerCache() .getJtis() .put( @@ -430,7 +430,7 @@ public void negative_wrongRole() throws Exception { @Test public void apiToken_negative_noPermissions() throws Exception { String token = "blah"; - PrivilegesEvaluationContext context = ctxWithUserName("apitoken_test:" + token); + PrivilegesEvaluationContext context = ctxWithUserName("apitoken:" + token); context.getApiTokenIndexListenerCache() .getJtis() .put(token, new Permissions(List.of(), List.of(new ApiToken.IndexPermission(List.of(), List.of())))); @@ -471,7 +471,7 @@ public void positive_hasExplicit_full() { @Test public void apiTokens_positive_hasExplicit_full() { String token = "blah"; - PrivilegesEvaluationContext context = ctxWithUserName("apitoken_test:" + token); + PrivilegesEvaluationContext context = ctxWithUserName("apitoken:" + token); context.getApiTokenIndexListenerCache() .getJtis() .put( From aa506e78eb5699d2580e28d4bb0f0060971449e1 Mon Sep 17 00:00:00 2001 From: Derek Ho Date: Mon, 6 Jan 2025 16:06:40 -0500 Subject: [PATCH 15/20] Remove unecessary id to jti map since we are reloading every time and write test Signed-off-by: Derek Ho --- .../apitokens/ApiTokenIndexListenerCache.java | 21 --- .../ApiTokenIndexListenerCacheTest.java | 165 ++++++++++++++++++ 2 files changed, 165 insertions(+), 21 deletions(-) create mode 100644 src/test/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCacheTest.java diff --git a/src/main/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCache.java b/src/main/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCache.java index 501638e9d4..9c2a10802b 100644 --- a/src/main/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCache.java +++ b/src/main/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCache.java @@ -21,7 +21,6 @@ import org.opensearch.cluster.ClusterStateListener; import org.opensearch.cluster.metadata.IndexMetadata; import org.opensearch.cluster.service.ClusterService; -import org.opensearch.core.rest.RestStatus; import org.opensearch.index.query.QueryBuilders; import org.opensearch.security.support.ConfigConstants; @@ -63,18 +62,7 @@ public void clusterChanged(ClusterChangedEvent event) { } void reloadApiTokensFromIndex() { - if (!initialized.get()) { - log.debug("Cache not yet initialized or client is null, skipping reload"); - return; - } - - if (clusterService.state() != null && clusterService.state().blocks().hasGlobalBlockWithStatus(RestStatus.SERVICE_UNAVAILABLE)) { - log.debug("Cluster not yet ready, skipping API tokens cache reload"); - return; - } - try { - idToJtiMap.clear(); jtis.clear(); client.prepareSearch(getSecurityIndexName()) @@ -88,8 +76,6 @@ void reloadApiTokensFromIndex() { String id = hit.getId(); String jti = (String) source.get("jti"); Permissions permissions = parsePermissions(source); - - idToJtiMap.put(id, jti); jtis.put(jti, permissions); }); @@ -123,11 +109,4 @@ public boolean isValidToken(String jti) { public Map getJtis() { return jtis; } - - // Cleanup method - public void close() { - if (clusterService != null) { - clusterService.removeListener(this); - } - } } diff --git a/src/test/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCacheTest.java b/src/test/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCacheTest.java new file mode 100644 index 0000000000..0df9f63427 --- /dev/null +++ b/src/test/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCacheTest.java @@ -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. + */ + +package org.opensearch.security.action.apitokens; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +import org.apache.lucene.search.TotalHits; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.opensearch.action.search.SearchRequestBuilder; +import org.opensearch.action.search.SearchResponse; +import org.opensearch.client.Client; +import org.opensearch.cluster.ClusterChangedEvent; +import org.opensearch.cluster.ClusterState; +import org.opensearch.cluster.metadata.IndexMetadata; +import org.opensearch.cluster.metadata.Metadata; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.action.ActionFuture; +import org.opensearch.common.xcontent.XContentFactory; +import org.opensearch.core.common.bytes.BytesReference; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.search.SearchHit; +import org.opensearch.search.SearchHits; +import org.opensearch.security.support.ConfigConstants; + +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class ApiTokenIndexListenerCacheTest { + + private ApiTokenIndexListenerCache cache; + + @Mock + private ClusterService clusterService; + + @Mock + private Client client; + + @Mock + private ClusterChangedEvent event; + + @Mock + private ClusterState clusterState; + + @Mock + private IndexMetadata indexMetadata; + @Mock + private SearchResponse searchResponse; + + @Mock + private SearchRequestBuilder searchRequestBuilder; + + @Mock + private ActionFuture actionFuture; + + @Before + public void setUp() { + ApiTokenIndexListenerCache.getInstance().initialize(clusterService, client); + cache = ApiTokenIndexListenerCache.getInstance(); + } + + @Test + public void testSingleton() { + ApiTokenIndexListenerCache instance1 = ApiTokenIndexListenerCache.getInstance(); + ApiTokenIndexListenerCache instance2 = ApiTokenIndexListenerCache.getInstance(); + assertSame("getInstance should always return the same instance", instance1, instance2); + } + + @Test + public void testJtisOperations() { + String jti = "testJti"; + Permissions permissions = new Permissions(List.of("read"), List.of(new ApiToken.IndexPermission(List.of(), List.of()))); + + cache.getJtis().put(jti, permissions); + assertEquals("Should retrieve correct permissions", permissions, cache.getJtis().get(jti)); + + cache.getJtis().remove(jti); + assertNull("Should return null after removal", cache.getJtis().get(jti)); + } + + @Test + public void testClearJtis() { + cache.getJtis().put("testJti", new Permissions(List.of("read"), List.of(new ApiToken.IndexPermission(List.of(), List.of())))); + cache.reloadApiTokensFromIndex(); + + assertTrue("Jtis should be empty after clear", cache.getJtis().isEmpty()); + } + + @Test + public void testClusterChangedInvokesReloadTokens() { + ClusterState clusterState = mock(ClusterState.class); + Metadata metadata = mock(Metadata.class); + when(clusterState.metadata()).thenReturn(metadata); + when(metadata.index(ConfigConstants.OPENSEARCH_API_TOKENS_INDEX)).thenReturn(indexMetadata); + when(event.state()).thenReturn(clusterState); + + ApiTokenIndexListenerCache cacheSpy = spy(cache); + cacheSpy.clusterChanged(event); + + verify(cacheSpy).reloadApiTokensFromIndex(); + } + + @Test + public void testReloadApiTokensFromIndexAndParse() throws IOException { + SearchHit hit = createSearchHitFromApiToken("1", "testJti", Arrays.asList("cluster:monitor"), List.of()); + + SearchHits searchHits = new SearchHits(new SearchHit[] { hit }, new TotalHits(1, TotalHits.Relation.EQUAL_TO), 1.0f); + + // Mock the search response + when(searchResponse.getHits()).thenReturn(searchHits); + when(client.prepareSearch(any())).thenReturn(searchRequestBuilder); + when(searchRequestBuilder.setQuery(any())).thenReturn(searchRequestBuilder); + when(searchRequestBuilder.execute()).thenReturn(actionFuture); + when(actionFuture.actionGet()).thenReturn(searchResponse); + + // Execute the reload + cache.reloadApiTokensFromIndex(); + + // Verify the cache was updated + assertFalse("Jtis should not be empty after reload", cache.getJtis().isEmpty()); + assertEquals("Should have one JTI entry", 1, cache.getJtis().size()); + assertTrue("Should contain testJti", cache.getJtis().containsKey("testJti")); + // Verify extraction works + assertEquals("Should have one cluster action", List.of("cluster:monitor"), cache.getJtis().get("testJti").getClusterPerm()); + assertEquals("Should have no index actions", List.of(), cache.getJtis().get("testJti").getIndexPermission()); + } + + private SearchHit createSearchHitFromApiToken( + String id, + String jti, + List allowedActions, + List prohibitedActions + ) throws IOException { + ApiToken apiToken = new ApiToken("test", jti, allowedActions, prohibitedActions, Long.MAX_VALUE); + XContentBuilder builder = XContentFactory.jsonBuilder(); + apiToken.toXContent(builder, null); + + SearchHit hit = new SearchHit(Integer.parseInt(id), id, null, null, null); + hit.sourceRef(BytesReference.bytes(builder)); + return hit; + } + +} From d9cf78d644022c079b66a203930fe032154c8c15 Mon Sep 17 00:00:00 2001 From: Derek Ho Date: Tue, 14 Jan 2025 11:19:29 -0500 Subject: [PATCH 16/20] PR review Signed-off-by: Derek Ho --- .../privileges/ActionPrivilegesTest.java | 25 +++++--------- .../security/action/apitokens/ApiToken.java | 20 ++--------- .../apitokens/ApiTokenIndexListenerCache.java | 8 ++--- .../action/apitokens/ApiTokenRepository.java | 8 +---- .../security/http/ApiTokenAuthenticator.java | 18 ++++------ .../identity/SecurityTokenManager.java | 13 +++---- .../security/privileges/ActionPrivileges.java | 8 ++--- .../PrivilegesEvaluationContext.java | 2 +- .../securityconf/impl/v7/ConfigV7.java | 12 +------ .../apitokens/ApiTokenIndexHandlerTest.java | 3 -- .../ApiTokenIndexListenerCacheTest.java | 12 +++---- .../apitokens/ApiTokenRepositoryTest.java | 6 ++-- .../security/authtoken/jwt/JwtVendorTest.java | 34 ------------------- .../identity/SecurityTokenManagerTest.java | 13 ------- 14 files changed, 39 insertions(+), 143 deletions(-) diff --git a/src/integrationTest/java/org/opensearch/security/privileges/ActionPrivilegesTest.java b/src/integrationTest/java/org/opensearch/security/privileges/ActionPrivilegesTest.java index 7bdb0980c1..c8c2489c61 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/ActionPrivilegesTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/ActionPrivilegesTest.java @@ -50,7 +50,6 @@ import org.opensearch.security.util.MockIndexMetadataBuilder; import static org.hamcrest.MatcherAssert.assertThat; -import static org.opensearch.security.privileges.ActionPrivilegesTest.IndexPrivileges.IndicesAndAliases.resolved; import static org.opensearch.security.privileges.PrivilegeEvaluatorResponseMatcher.isAllowed; import static org.opensearch.security.privileges.PrivilegeEvaluatorResponseMatcher.isForbidden; import static org.opensearch.security.privileges.PrivilegeEvaluatorResponseMatcher.isPartiallyOk; @@ -265,13 +264,11 @@ public void hasAny_wildcard() throws Exception { @Test public void apiToken_explicit_failsWithWildcard() throws Exception { - SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromYaml("test_role:\n" + // - " cluster_permissions:\n" + // - " - '*'", CType.ROLES); + SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.empty(CType.ROLES); ActionPrivileges subject = new ActionPrivileges(roles, FlattenedActionGroups.EMPTY, null, Settings.EMPTY); String token = "blah"; PrivilegesEvaluationContext context = ctxWithUserName("apitoken:" + token); - context.getApiTokenIndexListenerCache().getJtis().put(token, new Permissions(List.of("*"), List.of())); + context.getApiTokenRepository().getJtis().put(token, new Permissions(List.of("*"), List.of())); // Explicit fails assertThat( subject.hasExplicitClusterPrivilege(context, "cluster:whatever"), @@ -285,13 +282,11 @@ public void apiToken_explicit_failsWithWildcard() throws Exception { @Test public void apiToken_succeedsWithExactMatch() throws Exception { - SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromYaml("test_role:\n" + // - " cluster_permissions:\n" + // - " - '*'", CType.ROLES); + SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.empty(CType.ROLES); ActionPrivileges subject = new ActionPrivileges(roles, FlattenedActionGroups.EMPTY, null, Settings.EMPTY); String token = "blah"; PrivilegesEvaluationContext context = ctxWithUserName("apitoken:" + token); - context.getApiTokenIndexListenerCache().getJtis().put(token, new Permissions(List.of("cluster:whatever"), List.of())); + context.getApiTokenRepository().getJtis().put(token, new Permissions(List.of("cluster:whatever"), List.of())); // Explicit succeeds assertThat(subject.hasExplicitClusterPrivilege(context, "cluster:whatever"), isAllowed()); // Not explicit succeeds @@ -304,9 +299,7 @@ public void apiToken_succeedsWithExactMatch() throws Exception { @Test public void apiToken_succeedsWithActionGroupsExapnded() throws Exception { - SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromYaml("test_role:\n" + // - " cluster_permissions:\n" + // - " - '*'", CType.ROLES); + SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.empty(CType.ROLES); SecurityDynamicConfiguration config = SecurityDynamicConfiguration.fromYaml( "CLUSTER_ALL:\n allowed_actions:\n - \"cluster:*\"", @@ -317,7 +310,7 @@ public void apiToken_succeedsWithActionGroupsExapnded() throws Exception { ActionPrivileges subject = new ActionPrivileges(roles, actionGroups, null, Settings.EMPTY); String token = "blah"; PrivilegesEvaluationContext context = ctxWithUserName("apitoken:" + token); - context.getApiTokenIndexListenerCache().getJtis().put(token, new Permissions(List.of("CLUSTER_ALL"), List.of())); + context.getApiTokenRepository().getJtis().put(token, new Permissions(List.of("CLUSTER_ALL"), List.of())); // Explicit succeeds assertThat(subject.hasExplicitClusterPrivilege(context, "cluster:whatever"), isAllowed()); // Not explicit succeeds @@ -363,7 +356,7 @@ public void positive_full() throws Exception { public void apiTokens_positive_full() throws Exception { String token = "blah"; PrivilegesEvaluationContext context = ctxWithUserName("apitoken:" + token); - context.getApiTokenIndexListenerCache() + context.getApiTokenRepository() .getJtis() .put( token, @@ -431,7 +424,7 @@ public void negative_wrongRole() throws Exception { public void apiToken_negative_noPermissions() throws Exception { String token = "blah"; PrivilegesEvaluationContext context = ctxWithUserName("apitoken:" + token); - context.getApiTokenIndexListenerCache() + context.getApiTokenRepository() .getJtis() .put(token, new Permissions(List.of(), List.of(new ApiToken.IndexPermission(List.of(), List.of())))); @@ -472,7 +465,7 @@ public void positive_hasExplicit_full() { public void apiTokens_positive_hasExplicit_full() { String token = "blah"; PrivilegesEvaluationContext context = ctxWithUserName("apitoken:" + token); - context.getApiTokenIndexListenerCache() + context.getApiTokenRepository() .getJtis() .put( token, diff --git a/src/main/java/org/opensearch/security/action/apitokens/ApiToken.java b/src/main/java/org/opensearch/security/action/apitokens/ApiToken.java index d8be267da3..b1aa775ed9 100644 --- a/src/main/java/org/opensearch/security/action/apitokens/ApiToken.java +++ b/src/main/java/org/opensearch/security/action/apitokens/ApiToken.java @@ -16,15 +16,12 @@ import java.util.ArrayList; import java.util.List; -import com.fasterxml.jackson.annotation.JsonIgnore; - import org.opensearch.core.xcontent.ToXContent; import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.core.xcontent.XContentParser; public class ApiToken implements ToXContent { public static final String NAME_FIELD = "name"; - public static final String JTI_FIELD = "jti"; public static final String CREATION_TIME_FIELD = "creation_time"; public static final String CLUSTER_PERMISSIONS_FIELD = "cluster_permissions"; public static final String INDEX_PERMISSIONS_FIELD = "index_permissions"; @@ -33,15 +30,13 @@ public class ApiToken implements ToXContent { public static final String EXPIRATION_FIELD = "expiration"; private final String name; - private final String jti; private final Instant creationTime; private final List clusterPermissions; private final List indexPermissions; private final long expiration; - public ApiToken(String name, String jti, List clusterPermissions, List indexPermissions, Long expiration) { + public ApiToken(String name, List clusterPermissions, List indexPermissions, Long expiration) { this.creationTime = Instant.now(); - this.jti = jti; this.name = name; this.clusterPermissions = clusterPermissions; this.indexPermissions = indexPermissions; @@ -50,14 +45,12 @@ public ApiToken(String name, String jti, List clusterPermissions, List clusterPermissions, List indexPermissions, Instant creationTime, Long expiration ) { this.name = name; - this.jti = jti; this.clusterPermissions = clusterPermissions; this.indexPermissions = indexPermissions; this.creationTime = creationTime; @@ -157,9 +150,6 @@ public static ApiToken fromXContent(XContentParser parser) throws IOException { case NAME_FIELD: name = parser.text(); break; - case JTI_FIELD: - jti = parser.text(); - break; case CREATION_TIME_FIELD: creationTime = Instant.ofEpochMilli(parser.longValue()); break; @@ -185,7 +175,7 @@ public static ApiToken fromXContent(XContentParser parser) throws IOException { } } - return new ApiToken(name, jti, clusterPermissions, indexPermissions, creationTime, expiration); + return new ApiToken(name, clusterPermissions, indexPermissions, creationTime, expiration); } private static IndexPermission parseIndexPermission(XContentParser parser) throws IOException { @@ -224,11 +214,6 @@ public Long getExpiration() { return expiration; } - @JsonIgnore - public String getJti() { - return jti; - } - public Instant getCreationTime() { return creationTime; } @@ -241,7 +226,6 @@ public List getClusterPermissions() { public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params) throws IOException { xContentBuilder.startObject(); xContentBuilder.field(NAME_FIELD, name); - xContentBuilder.field(JTI_FIELD, jti); xContentBuilder.field(CLUSTER_PERMISSIONS_FIELD, clusterPermissions); xContentBuilder.field(INDEX_PERMISSIONS_FIELD, indexPermissions); xContentBuilder.field(CREATION_TIME_FIELD, creationTime.toEpochMilli()); diff --git a/src/main/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCache.java b/src/main/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCache.java index 9c2a10802b..6cdcc18010 100644 --- a/src/main/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCache.java +++ b/src/main/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCache.java @@ -24,6 +24,8 @@ import org.opensearch.index.query.QueryBuilders; import org.opensearch.security.support.ConfigConstants; +import static org.opensearch.security.action.apitokens.ApiToken.NAME_FIELD; + public class ApiTokenIndexListenerCache implements ClusterStateListener { private static final Logger log = LogManager.getLogger(ApiTokenIndexListenerCache.class); @@ -64,7 +66,6 @@ public void clusterChanged(ClusterChangedEvent event) { void reloadApiTokensFromIndex() { try { jtis.clear(); - client.prepareSearch(getSecurityIndexName()) .setQuery(QueryBuilders.matchAllQuery()) .execute() @@ -73,13 +74,10 @@ void reloadApiTokensFromIndex() { .forEach(hit -> { // Parse the document and update the cache Map source = hit.getSourceAsMap(); - String id = hit.getId(); - String jti = (String) source.get("jti"); + String jti = (String) source.get(NAME_FIELD); Permissions permissions = parsePermissions(source); jtis.put(jti, permissions); }); - - log.debug("Successfully reloaded API tokens cache"); } catch (Exception e) { log.error("Failed to reload API tokens cache", e); } diff --git a/src/main/java/org/opensearch/security/action/apitokens/ApiTokenRepository.java b/src/main/java/org/opensearch/security/action/apitokens/ApiTokenRepository.java index be336f3582..cdad23f32d 100644 --- a/src/main/java/org/opensearch/security/action/apitokens/ApiTokenRepository.java +++ b/src/main/java/org/opensearch/security/action/apitokens/ApiTokenRepository.java @@ -50,13 +50,7 @@ public String createApiToken( apiTokenIndexHandler.createApiTokenIndexIfAbsent(); // TODO: Add validation on whether user is creating a token with a subset of their permissions ExpiringBearerAuthToken token = securityTokenManager.issueApiToken(name, expiration); - ApiToken apiToken = new ApiToken( - name, - securityTokenManager.encryptToken(token.getCompleteToken()), - clusterPermissions, - indexPermissions, - expiration - ); + ApiToken apiToken = new ApiToken(name, clusterPermissions, indexPermissions, expiration); apiTokenIndexHandler.indexTokenMetadata(apiToken); return token.getCompleteToken(); } diff --git a/src/main/java/org/opensearch/security/http/ApiTokenAuthenticator.java b/src/main/java/org/opensearch/security/http/ApiTokenAuthenticator.java index 86086eee1e..517717b874 100644 --- a/src/main/java/org/opensearch/security/http/ApiTokenAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/ApiTokenAuthenticator.java @@ -29,7 +29,6 @@ import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.security.action.apitokens.ApiTokenIndexListenerCache; import org.opensearch.security.auth.HTTPAuthenticator; -import org.opensearch.security.authtoken.jwt.EncryptionDecryptionUtil; import org.opensearch.security.filter.SecurityRequest; import org.opensearch.security.filter.SecurityResponse; import org.opensearch.security.ssl.util.ExceptionUtils; @@ -62,8 +61,6 @@ public class ApiTokenAuthenticator implements HTTPAuthenticator { private final String clusterName; public static final String API_TOKEN_USER_PREFIX = "apitoken:"; - private final EncryptionDecryptionUtil encryptionUtil; - @SuppressWarnings("removal") public ApiTokenAuthenticator(Settings settings, String clusterName) { String apiTokenEnabledSetting = settings.get("enabled", "true"); @@ -82,7 +79,6 @@ public JwtParser run() { } }); this.clusterName = clusterName; - this.encryptionUtil = new EncryptionDecryptionUtil(encryptionKey); } private JwtParserBuilder initParserBuilder(final String signingKey) { @@ -141,12 +137,6 @@ private AuthCredentials extractCredentials0(final SecurityRequest request, final return null; } - // TODO: handle revocation different from deletion? - if (!cache.isValidToken(encryptionUtil.encrypt(jwtToken))) { - log.error("Token is not allowlisted"); - return null; - } - try { final Claims claims = jwtParser.parseClaimsJws(jwtToken).getBody(); @@ -156,13 +146,19 @@ private AuthCredentials extractCredentials0(final SecurityRequest request, final return null; } + // TODO: handle revocation different from deletion? + if (!cache.isValidToken(subject)) { + log.error("Token is not allowlisted"); + return null; + } + final String issuer = claims.getIssuer(); if (!clusterName.equals(issuer)) { log.error("The issuer of this api token does not match the current cluster identifier"); return null; } - return new AuthCredentials(API_TOKEN_USER_PREFIX + encryptionUtil.encrypt(jwtToken), List.of(), "").markComplete(); + return new AuthCredentials(API_TOKEN_USER_PREFIX + subject, List.of(), "").markComplete(); } catch (WeakKeyException e) { log.error("Cannot authenticate user with JWT because of ", e); diff --git a/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java b/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java index aeee248f25..430cac995c 100644 --- a/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java +++ b/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java @@ -140,6 +140,11 @@ public ExpiringBearerAuthToken issueOnBehalfOfToken(final Subject subject, final } public ExpiringBearerAuthToken issueApiToken(final String name, final Long expiration) { + if (!issueApiTokenAllowed()) { + throw new OpenSearchSecurityException( + "Api token generation is not enabled." + ); + } final User user = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); try { @@ -150,14 +155,6 @@ public ExpiringBearerAuthToken issueApiToken(final String name, final Long expir } } - public String encryptToken(final String token) { - return apiTokenJwtVendor.encryptString(token); - } - - public String decryptString(final String input) { - return apiTokenJwtVendor.decryptString(input); - } - @Override public AuthToken issueServiceAccountToken(final String serviceId) { try { diff --git a/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java b/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java index a3bb2dc3ad..be952385d8 100644 --- a/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java +++ b/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java @@ -431,10 +431,10 @@ PrivilegesEvaluatorResponse apiTokenProvidesClusterPrivilege( String userName = context.getUser().getName(); if (userName.startsWith(API_TOKEN_USER_PREFIX)) { String jti = context.getUser().getName().split(API_TOKEN_USER_PREFIX)[1]; - if (context.getApiTokenIndexListenerCache().isValidToken(jti)) { + if (context.getApiTokenRepository().isValidToken(jti)) { // Expand the action groups Set resolvedClusterPermissions = actionGroups.resolve( - context.getApiTokenIndexListenerCache().getPermissionsForJti(jti).getClusterPerm() + context.getApiTokenRepository().getPermissionsForJti(jti).getClusterPerm() ); // Check for wildcard permission @@ -923,8 +923,8 @@ PrivilegesEvaluatorResponse apiTokenProvidesIndexPrivilege( String userName = context.getUser().getName(); if (userName.startsWith(API_TOKEN_USER_PREFIX)) { String jti = context.getUser().getName().split(API_TOKEN_USER_PREFIX)[1]; - if (context.getApiTokenIndexListenerCache().isValidToken(jti)) { - List indexPermissions = context.getApiTokenIndexListenerCache() + if (context.getApiTokenRepository().isValidToken(jti)) { + List indexPermissions = context.getApiTokenRepository() .getPermissionsForJti(jti) .getIndexPermission(); diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java index c0352484da..1762e99083 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java @@ -174,7 +174,7 @@ public String toString() { + '}'; } - public ApiTokenIndexListenerCache getApiTokenIndexListenerCache() { + public ApiTokenIndexListenerCache getApiTokenRepository() { return apiTokenIndexListenerCache; } } diff --git a/src/main/java/org/opensearch/security/securityconf/impl/v7/ConfigV7.java b/src/main/java/org/opensearch/security/securityconf/impl/v7/ConfigV7.java index 6555c0838d..0818ec5530 100644 --- a/src/main/java/org/opensearch/security/securityconf/impl/v7/ConfigV7.java +++ b/src/main/java/org/opensearch/security/securityconf/impl/v7/ConfigV7.java @@ -503,8 +503,6 @@ public static class ApiTokenSettings { private Boolean enabled = Boolean.FALSE; @JsonProperty("signing_key") private String signingKey; - @JsonProperty("encryption_key") - private String encryptionKey; @JsonIgnore public String configAsJson() { @@ -531,17 +529,9 @@ public void setSigningKey(String signingKey) { this.signingKey = signingKey; } - public String getEncryptionKey() { - return encryptionKey; - } - - public void setEncryptionKey(String encryptionKey) { - this.encryptionKey = encryptionKey; - } - @Override public String toString() { - return "ApiTokenSettings [ enabled=" + enabled + ", signing_key=" + signingKey + ", encryption_key=" + encryptionKey + "]"; + return "ApiTokenSettings [ enabled=" + enabled + ", signing_key=" + signingKey + "]"; } } diff --git a/src/test/java/org/opensearch/security/action/apitokens/ApiTokenIndexHandlerTest.java b/src/test/java/org/opensearch/security/action/apitokens/ApiTokenIndexHandlerTest.java index 7e03c14851..abb194df37 100644 --- a/src/test/java/org/opensearch/security/action/apitokens/ApiTokenIndexHandlerTest.java +++ b/src/test/java/org/opensearch/security/action/apitokens/ApiTokenIndexHandlerTest.java @@ -192,7 +192,6 @@ public void testIndexTokenStoresTokenPayload() { ); ApiToken token = new ApiToken( "test-token-description", - "test-token-jti", clusterPermissions, indexPermissions, Instant.now(), @@ -245,7 +244,6 @@ public void testGetTokenPayloads() throws IOException { // First token ApiToken token1 = new ApiToken( "token1-description", - "token1-jti", Arrays.asList("cluster:admin/something"), Arrays.asList(new ApiToken.IndexPermission( Arrays.asList("index1-*"), @@ -258,7 +256,6 @@ public void testGetTokenPayloads() throws IOException { // Second token ApiToken token2 = new ApiToken( "token2-description", - "token2-jti", Arrays.asList("cluster:admin/other"), Arrays.asList(new ApiToken.IndexPermission( Arrays.asList("index2-*"), diff --git a/src/test/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCacheTest.java b/src/test/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCacheTest.java index 0df9f63427..93c03a3159 100644 --- a/src/test/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCacheTest.java +++ b/src/test/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCacheTest.java @@ -124,7 +124,7 @@ public void testClusterChangedInvokesReloadTokens() { @Test public void testReloadApiTokensFromIndexAndParse() throws IOException { - SearchHit hit = createSearchHitFromApiToken("1", "testJti", Arrays.asList("cluster:monitor"), List.of()); + SearchHit hit = createSearchHitFromApiToken("1", Arrays.asList("cluster:monitor"), List.of()); SearchHits searchHits = new SearchHits(new SearchHit[] { hit }, new TotalHits(1, TotalHits.Relation.EQUAL_TO), 1.0f); @@ -147,13 +147,9 @@ public void testReloadApiTokensFromIndexAndParse() throws IOException { assertEquals("Should have no index actions", List.of(), cache.getJtis().get("testJti").getIndexPermission()); } - private SearchHit createSearchHitFromApiToken( - String id, - String jti, - List allowedActions, - List prohibitedActions - ) throws IOException { - ApiToken apiToken = new ApiToken("test", jti, allowedActions, prohibitedActions, Long.MAX_VALUE); + private SearchHit createSearchHitFromApiToken(String id, List allowedActions, List prohibitedActions) + throws IOException { + ApiToken apiToken = new ApiToken("test", allowedActions, prohibitedActions, Long.MAX_VALUE); XContentBuilder builder = XContentFactory.jsonBuilder(); apiToken.toXContent(builder, null); diff --git a/src/test/java/org/opensearch/security/action/apitokens/ApiTokenRepositoryTest.java b/src/test/java/org/opensearch/security/action/apitokens/ApiTokenRepositoryTest.java index a6dae60400..3060ecd290 100644 --- a/src/test/java/org/opensearch/security/action/apitokens/ApiTokenRepositoryTest.java +++ b/src/test/java/org/opensearch/security/action/apitokens/ApiTokenRepositoryTest.java @@ -62,7 +62,7 @@ public void testDeleteApiToken() throws ApiTokenException { @Test public void testGetApiTokens() throws IndexNotFoundException { Map expectedTokens = new HashMap<>(); - expectedTokens.put("token1", new ApiToken("token1", "token1-jti", Arrays.asList("perm1"), Arrays.asList(), Long.MAX_VALUE)); + expectedTokens.put("token1", new ApiToken("token1", Arrays.asList("perm1"), Arrays.asList(), Long.MAX_VALUE)); when(apiTokenIndexHandler.getTokenMetadatas()).thenReturn(expectedTokens); Map result = repository.getApiTokens(); @@ -85,19 +85,17 @@ public void testCreateApiToken() { ExpiringBearerAuthToken bearerToken = mock(ExpiringBearerAuthToken.class); when(bearerToken.getCompleteToken()).thenReturn(completeToken); when(securityTokenManager.issueApiToken(any(), any())).thenReturn(bearerToken); - when(securityTokenManager.encryptToken(completeToken)).thenReturn(encryptedToken); String result = repository.createApiToken(tokenName, clusterPermissions, indexPermissions, expiration); verify(apiTokenIndexHandler).createApiTokenIndexIfAbsent(); verify(securityTokenManager).issueApiToken(any(), any()); - verify(securityTokenManager).encryptToken(completeToken); verify(apiTokenIndexHandler).indexTokenMetadata( argThat( token -> token.getName().equals(tokenName) - && token.getJti().equals(encryptedToken) && token.getClusterPermissions().equals(clusterPermissions) && token.getIndexPermissions().equals(indexPermissions) + && token.getExpiration().equals(expiration) ) ); assertThat(result, equalTo(completeToken)); diff --git a/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java b/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java index ec37898687..c766d5a4a4 100644 --- a/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java +++ b/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java @@ -318,40 +318,6 @@ public void testEncryptJwtCorrectly() { assertThat(jwtVendor.encryptString(token), equalTo(expectedEncryptedToken)); } - @Test - public void testEncryptDecryptClusterIndexPermissionsCorrectly() throws IOException { - String claimsEncryptionKey = BaseEncoding.base64().encode("1234567890123456".getBytes(StandardCharsets.UTF_8)); - String clusterPermissions = "cluster:admin/*,cluster:*"; - String encryptedClusterPermissions = "P+KGUkpANJHzHGKVSqJhIyHOKS+JCLOanxCOBWSgZNk="; - // "{\"index_pattern\":[\"*\"],\"allowed_actions\":[\"read\"]},{\"index_pattern\":[\".*\"],\"allowed_actions\":[\"write\"]}" - String indexPermissions = Strings.join( - List.of( - new ApiToken.IndexPermission(List.of("*"), List.of("read")).toXContent( - XContentFactory.jsonBuilder(), - ToXContent.EMPTY_PARAMS - ).toString(), - new ApiToken.IndexPermission(List.of(".*"), List.of("write")).toXContent( - XContentFactory.jsonBuilder(), - ToXContent.EMPTY_PARAMS - ).toString() - ), - "," - ); - String encryptedIndexPermissions = - "Y9ssHcl6spHC2/zy+L1P0y8e2+T+jGgXcP02DWGeTMk/3KiI4Ik0Df7oXMf9l/Ba0emk9LClnHsJi8iFwRh7ii1Pxb3CTHS/d+p7a3bA6rtJjgOjGlbjdWTdj4+87uBJynsR5CAlUMLeTrjbPe/nWw=="; - Settings settings = Settings.builder().put("signing_key", signingKeyB64Encoded).put("encryption_key", claimsEncryptionKey).build(); - LongSupplier currentTime = () -> (long) 100; - JwtVendor jwtVendor = new JwtVendor(settings, Optional.of(currentTime)); - - // encrypt decrypt cluster permissions - assertThat(jwtVendor.encryptString(clusterPermissions), equalTo(encryptedClusterPermissions)); - assertThat(jwtVendor.decryptString(encryptedClusterPermissions), equalTo(clusterPermissions)); - - // encrypt decrypt index permissions - assertThat(jwtVendor.encryptString(indexPermissions), equalTo(encryptedIndexPermissions)); - assertThat(jwtVendor.decryptString(encryptedIndexPermissions), equalTo(indexPermissions)); - } - @Test public void testKeyTooShortThrowsException() { String claimsEncryptionKey = RandomStringUtils.randomAlphanumeric(16); diff --git a/src/test/java/org/opensearch/security/identity/SecurityTokenManagerTest.java b/src/test/java/org/opensearch/security/identity/SecurityTokenManagerTest.java index f6679a95b7..7a11e3dc6e 100644 --- a/src/test/java/org/opensearch/security/identity/SecurityTokenManagerTest.java +++ b/src/test/java/org/opensearch/security/identity/SecurityTokenManagerTest.java @@ -290,17 +290,4 @@ public void encryptCallsJwtEncrypt() throws Exception { verify(cs).getClusterName(); verify(threadPool).getThreadContext(); } - - @Test - public void testEncryptTokenCallsJwtEncrypt() throws Exception { - String tokenToEncrypt = "test-token"; - String encryptedToken = "encrypted-test-token"; - createMockJwtVendorInTokenManager(); - when(jwtVendor.encryptString(tokenToEncrypt)).thenReturn(encryptedToken); - - String result = tokenManager.encryptToken(tokenToEncrypt); - - assertThat(result, equalTo(encryptedToken)); - verify(jwtVendor).encryptString(tokenToEncrypt); - } } From d7b7e478e49824b72040d6e819f725b369b20faa Mon Sep 17 00:00:00 2001 From: Derek Ho Date: Tue, 14 Jan 2025 11:23:24 -0500 Subject: [PATCH 17/20] Spotless Signed-off-by: Derek Ho --- .../opensearch/security/identity/SecurityTokenManager.java | 4 +--- .../org/opensearch/security/authtoken/jwt/JwtVendorTest.java | 2 -- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java b/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java index 430cac995c..7c85684e18 100644 --- a/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java +++ b/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java @@ -141,9 +141,7 @@ public ExpiringBearerAuthToken issueOnBehalfOfToken(final Subject subject, final public ExpiringBearerAuthToken issueApiToken(final String name, final Long expiration) { if (!issueApiTokenAllowed()) { - throw new OpenSearchSecurityException( - "Api token generation is not enabled." - ); + throw new OpenSearchSecurityException("Api token generation is not enabled."); } final User user = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); diff --git a/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java b/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java index c766d5a4a4..162f5b35d2 100644 --- a/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java +++ b/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java @@ -11,7 +11,6 @@ package org.opensearch.security.authtoken.jwt; -import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Date; import java.util.List; @@ -39,7 +38,6 @@ import com.nimbusds.jose.JWSSigner; import com.nimbusds.jose.jwk.JWK; import com.nimbusds.jwt.SignedJWT; -import joptsimple.internal.Strings; import org.mockito.ArgumentCaptor; import static org.hamcrest.MatcherAssert.assertThat; From 6571d9d45d92bda4a2c1e97c03ee0af65e773a12 Mon Sep 17 00:00:00 2001 From: Derek Ho Date: Tue, 14 Jan 2025 11:52:01 -0500 Subject: [PATCH 18/20] Fix tests Signed-off-by: Derek Ho --- .../apitokens/ApiTokenAuthenticatorTest.java | 45 +++++++------------ .../apitokens/ApiTokenIndexHandlerTest.java | 1 - 2 files changed, 17 insertions(+), 29 deletions(-) diff --git a/src/test/java/org/opensearch/security/action/apitokens/ApiTokenAuthenticatorTest.java b/src/test/java/org/opensearch/security/action/apitokens/ApiTokenAuthenticatorTest.java index 916b7d86fc..ac0379d0e6 100644 --- a/src/test/java/org/opensearch/security/action/apitokens/ApiTokenAuthenticatorTest.java +++ b/src/test/java/org/opensearch/security/action/apitokens/ApiTokenAuthenticatorTest.java @@ -25,7 +25,6 @@ import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; -import org.opensearch.security.authtoken.jwt.EncryptionDecryptionUtil; import org.opensearch.security.filter.SecurityRequest; import org.opensearch.security.http.ApiTokenAuthenticator; import org.opensearch.security.user.AuthCredentials; @@ -55,16 +54,11 @@ public class ApiTokenAuthenticatorTest { private ThreadContext threadcontext; private final String signingKey = Base64.getEncoder() .encodeToString("jwt signing key long enough for secure api token authentication testing".getBytes(StandardCharsets.UTF_8)); - private final String encryptionKey = Base64.getEncoder().encodeToString("123456678910".getBytes(StandardCharsets.UTF_8)); - private final EncryptionDecryptionUtil encryptionUtil = new EncryptionDecryptionUtil(encryptionKey); + private final String tokenName = "test-token"; @Before public void setUp() { - Settings settings = Settings.builder() - .put("enabled", "true") - .put("signing_key", signingKey) - .put("encryption_key", encryptionKey) - .build(); + Settings settings = Settings.builder().put("enabled", "true").put("signing_key", signingKey).build(); authenticator = new ApiTokenAuthenticator(settings, "opensearch-cluster"); authenticator.log = log; @@ -91,15 +85,14 @@ public void testAuthenticationFailsWhenJtiNotInCache() { public void testExtractCredentialsPassWhenJtiInCache() { String token = Jwts.builder() .setIssuer("opensearch-cluster") - .setSubject("test-token") - .setAudience("test-token") + .setSubject(tokenName) + .setAudience(tokenName) .setIssuedAt(Date.from(Instant.now())) .setExpiration(Date.from(Instant.now().plus(1, ChronoUnit.DAYS))) .signWith(SignatureAlgorithm.HS512, signingKey) .compact(); - String encryptedToken = encryptionUtil.encrypt(token); - ApiTokenIndexListenerCache.getInstance().getJtis().put(encryptedToken, new Permissions(List.of(), List.of())); + ApiTokenIndexListenerCache.getInstance().getJtis().put(tokenName, new Permissions(List.of(), List.of())); SecurityRequest request = mock(SecurityRequest.class); when(request.header("Authorization")).thenReturn("Bearer " + token); @@ -114,15 +107,14 @@ public void testExtractCredentialsPassWhenJtiInCache() { public void testExtractCredentialsFailWhenTokenIsExpired() { String token = Jwts.builder() .setIssuer("opensearch-cluster") - .setSubject("test-token") - .setAudience("test-token") + .setSubject(tokenName) + .setAudience(tokenName) .setIssuedAt(Date.from(Instant.now())) .setExpiration(Date.from(Instant.now().minus(1, ChronoUnit.DAYS))) .signWith(SignatureAlgorithm.HS512, signingKey) .compact(); - String encryptedToken = encryptionUtil.encrypt(token); - ApiTokenIndexListenerCache.getInstance().getJtis().put(encryptedToken, new Permissions(List.of(), List.of())); + ApiTokenIndexListenerCache.getInstance().getJtis().put(tokenName, new Permissions(List.of(), List.of())); SecurityRequest request = mock(SecurityRequest.class); when(request.header("Authorization")).thenReturn("Bearer " + token); @@ -139,15 +131,14 @@ public void testExtractCredentialsFailWhenTokenIsExpired() { public void testExtractCredentialsFailWhenIssuerDoesNotMatch() { String token = Jwts.builder() .setIssuer("not-opensearch-cluster") - .setSubject("test-token") - .setAudience("test-token") + .setSubject(tokenName) + .setAudience(tokenName) .setIssuedAt(Date.from(Instant.now())) .setExpiration(Date.from(Instant.now().plus(1, ChronoUnit.DAYS))) .signWith(SignatureAlgorithm.HS512, signingKey) .compact(); - String encryptedToken = encryptionUtil.encrypt(token); - ApiTokenIndexListenerCache.getInstance().getJtis().put(encryptedToken, new Permissions(List.of(), List.of())); + ApiTokenIndexListenerCache.getInstance().getJtis().put(tokenName, new Permissions(List.of(), List.of())); SecurityRequest request = mock(SecurityRequest.class); when(request.header("Authorization")).thenReturn("Bearer " + token); @@ -163,15 +154,14 @@ public void testExtractCredentialsFailWhenIssuerDoesNotMatch() { public void testExtractCredentialsFailWhenAccessingRestrictedEndpoint() { String token = Jwts.builder() .setIssuer("opensearch-cluster") - .setSubject("test-token") - .setAudience("test-token") + .setSubject(tokenName) + .setAudience(tokenName) .setIssuedAt(Date.from(Instant.now())) .setExpiration(Date.from(Instant.now().plus(1, ChronoUnit.DAYS))) .signWith(SignatureAlgorithm.HS512, signingKey) .compact(); - String encryptedToken = encryptionUtil.encrypt(token); - ApiTokenIndexListenerCache.getInstance().getJtis().put(encryptedToken, new Permissions(List.of(), List.of())); + ApiTokenIndexListenerCache.getInstance().getJtis().put(tokenName, new Permissions(List.of(), List.of())); SecurityRequest request = mock(SecurityRequest.class); when(request.header("Authorization")).thenReturn("Bearer " + token); @@ -187,14 +177,13 @@ public void testExtractCredentialsFailWhenAccessingRestrictedEndpoint() { public void testAuthenticatorNotEnabled() { String token = Jwts.builder() .setIssuer("opensearch-cluster") - .setSubject("test-token") - .setAudience("test-token") + .setSubject(tokenName) + .setAudience(tokenName) .setIssuedAt(Date.from(Instant.now())) .setExpiration(Date.from(Instant.now().plus(1, ChronoUnit.DAYS))) .signWith(SignatureAlgorithm.HS512, signingKey) .compact(); - String encryptedToken = encryptionUtil.encrypt(token); - ApiTokenIndexListenerCache.getInstance().getJtis().put(encryptedToken, new Permissions(List.of(), List.of())); + ApiTokenIndexListenerCache.getInstance().getJtis().put(tokenName, new Permissions(List.of(), List.of())); SecurityRequest request = mock(SecurityRequest.class); diff --git a/src/test/java/org/opensearch/security/action/apitokens/ApiTokenIndexHandlerTest.java b/src/test/java/org/opensearch/security/action/apitokens/ApiTokenIndexHandlerTest.java index abb194df37..9b3b8638e2 100644 --- a/src/test/java/org/opensearch/security/action/apitokens/ApiTokenIndexHandlerTest.java +++ b/src/test/java/org/opensearch/security/action/apitokens/ApiTokenIndexHandlerTest.java @@ -230,7 +230,6 @@ public void testIndexTokenStoresTokenPayload() { String source = capturedRequest.source().utf8ToString(); assertThat(source, containsString("test-token-description")); assertThat(source, containsString("cluster:admin/something")); - assertThat(source, containsString("test-token-jti")); assertThat(source, containsString("test-index-*")); } From 9fcb7209db014e6f76dde717f8260806ef6edba8 Mon Sep 17 00:00:00 2001 From: Derek Ho Date: Tue, 14 Jan 2025 15:31:48 -0500 Subject: [PATCH 19/20] PR review Signed-off-by: Derek Ho --- .../privileges/ActionPrivilegesTest.java | 63 ++++--- .../security/OpenSearchSecurityPlugin.java | 6 +- .../apitokens/ApiTokenIndexListenerCache.java | 110 ------------ .../action/apitokens/ApiTokenRepository.java | 75 +++++++- .../TransportApiTokenUpdateAction.java | 9 +- .../security/http/ApiTokenAuthenticator.java | 13 +- .../security/privileges/ActionPrivileges.java | 150 ++++++++-------- .../PrivilegesEvaluationContext.java | 50 +++++- .../securityconf/DynamicConfigModelV7.java | 7 +- .../action/apitokens/ApiTokenActionTest.java | 5 +- .../apitokens/ApiTokenAuthenticatorTest.java | 19 +-- .../ApiTokenIndexListenerCacheTest.java | 161 ------------------ .../apitokens/ApiTokenRepositoryTest.java | 112 +++++++++++- 13 files changed, 368 insertions(+), 412 deletions(-) delete mode 100644 src/main/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCache.java delete mode 100644 src/test/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCacheTest.java diff --git a/src/integrationTest/java/org/opensearch/security/privileges/ActionPrivilegesTest.java b/src/integrationTest/java/org/opensearch/security/privileges/ActionPrivilegesTest.java index c8c2489c61..d4f8f202cc 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/ActionPrivilegesTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/ActionPrivilegesTest.java @@ -39,6 +39,7 @@ import org.opensearch.core.common.unit.ByteSizeUnit; import org.opensearch.core.common.unit.ByteSizeValue; import org.opensearch.security.action.apitokens.ApiToken; +import org.opensearch.security.action.apitokens.ApiTokenRepository; import org.opensearch.security.action.apitokens.Permissions; import org.opensearch.security.resolver.IndexResolverReplacer; import org.opensearch.security.securityconf.FlattenedActionGroups; @@ -49,6 +50,8 @@ import org.opensearch.security.user.User; import org.opensearch.security.util.MockIndexMetadataBuilder; +import org.mockito.Mockito; + import static org.hamcrest.MatcherAssert.assertThat; import static org.opensearch.security.privileges.PrivilegeEvaluatorResponseMatcher.isAllowed; import static org.opensearch.security.privileges.PrivilegeEvaluatorResponseMatcher.isForbidden; @@ -267,8 +270,7 @@ public void apiToken_explicit_failsWithWildcard() throws Exception { SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.empty(CType.ROLES); ActionPrivileges subject = new ActionPrivileges(roles, FlattenedActionGroups.EMPTY, null, Settings.EMPTY); String token = "blah"; - PrivilegesEvaluationContext context = ctxWithUserName("apitoken:" + token); - context.getApiTokenRepository().getJtis().put(token, new Permissions(List.of("*"), List.of())); + PrivilegesEvaluationContext context = ctxForApiToken("apitoken:" + token, new Permissions(List.of("*"), List.of())); // Explicit fails assertThat( subject.hasExplicitClusterPrivilege(context, "cluster:whatever"), @@ -285,8 +287,10 @@ public void apiToken_succeedsWithExactMatch() throws Exception { SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.empty(CType.ROLES); ActionPrivileges subject = new ActionPrivileges(roles, FlattenedActionGroups.EMPTY, null, Settings.EMPTY); String token = "blah"; - PrivilegesEvaluationContext context = ctxWithUserName("apitoken:" + token); - context.getApiTokenRepository().getJtis().put(token, new Permissions(List.of("cluster:whatever"), List.of())); + PrivilegesEvaluationContext context = ctxForApiToken( + "apitoken:" + token, + new Permissions(List.of("cluster:whatever"), List.of()) + ); // Explicit succeeds assertThat(subject.hasExplicitClusterPrivilege(context, "cluster:whatever"), isAllowed()); // Not explicit succeeds @@ -309,8 +313,7 @@ public void apiToken_succeedsWithActionGroupsExapnded() throws Exception { FlattenedActionGroups actionGroups = new FlattenedActionGroups(config); ActionPrivileges subject = new ActionPrivileges(roles, actionGroups, null, Settings.EMPTY); String token = "blah"; - PrivilegesEvaluationContext context = ctxWithUserName("apitoken:" + token); - context.getApiTokenRepository().getJtis().put(token, new Permissions(List.of("CLUSTER_ALL"), List.of())); + PrivilegesEvaluationContext context = ctxForApiToken("apitoken:" + token, new Permissions(List.of("CLUSTER_ALL"), List.of())); // Explicit succeeds assertThat(subject.hasExplicitClusterPrivilege(context, "cluster:whatever"), isAllowed()); // Not explicit succeeds @@ -355,13 +358,10 @@ public void positive_full() throws Exception { @Test public void apiTokens_positive_full() throws Exception { String token = "blah"; - PrivilegesEvaluationContext context = ctxWithUserName("apitoken:" + token); - context.getApiTokenRepository() - .getJtis() - .put( - token, - new Permissions(List.of("index_a11"), List.of(new ApiToken.IndexPermission(List.of("index_a11"), List.of("*")))) - ); + PrivilegesEvaluationContext context = ctxForApiToken( + "apitoken:" + token, + new Permissions(List.of("index_a11"), List.of(new ApiToken.IndexPermission(List.of("index_a11"), List.of("*")))) + ); PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege(context, requiredActions, resolved("index_a11")); assertThat(result, isAllowed()); } @@ -423,10 +423,10 @@ public void negative_wrongRole() throws Exception { @Test public void apiToken_negative_noPermissions() throws Exception { String token = "blah"; - PrivilegesEvaluationContext context = ctxWithUserName("apitoken:" + token); - context.getApiTokenRepository() - .getJtis() - .put(token, new Permissions(List.of(), List.of(new ApiToken.IndexPermission(List.of(), List.of())))); + PrivilegesEvaluationContext context = ctxForApiToken( + "apitoken:" + token, + new Permissions(List.of(), List.of(new ApiToken.IndexPermission(List.of(), List.of()))) + ); PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege(context, requiredActions, resolved("index_a11")); assertThat(result, isForbidden(missingPrivileges(requiredActions))); @@ -464,13 +464,10 @@ public void positive_hasExplicit_full() { @Test public void apiTokens_positive_hasExplicit_full() { String token = "blah"; - PrivilegesEvaluationContext context = ctxWithUserName("apitoken:" + token); - context.getApiTokenRepository() - .getJtis() - .put( - token, - new Permissions(List.of("index_a11"), List.of(new ApiToken.IndexPermission(List.of("index_a11"), List.of("*")))) - ); + PrivilegesEvaluationContext context = ctxForApiToken( + "apitoken:" + token, + new Permissions(List.of("index_a11"), List.of(new ApiToken.IndexPermission(List.of("index_a11"), List.of("*")))) + ); PrivilegesEvaluatorResponse result = subject.hasExplicitIndexPrivilege(context, requiredActions, resolved("index_a11")); @@ -1126,6 +1123,7 @@ static PrivilegesEvaluationContext ctx(String... roles) { static PrivilegesEvaluationContext ctxWithUserName(String userName, String... roles) { User user = new User(userName); user.addAttributes(ImmutableMap.of("attrs.dept_no", "a11")); + ApiTokenRepository mockRepository = Mockito.mock(ApiTokenRepository.class); return new PrivilegesEvaluationContext( user, ImmutableSet.copyOf(roles), @@ -1137,4 +1135,21 @@ static PrivilegesEvaluationContext ctxWithUserName(String userName, String... ro null ); } + + static PrivilegesEvaluationContext ctxForApiToken(String userName, Permissions permissions) { + User user = new User(userName); + user.addAttributes(ImmutableMap.of("attrs.dept_no", "a11")); + ApiTokenRepository mockRepository = Mockito.mock(ApiTokenRepository.class); + return new PrivilegesEvaluationContext( + user, + ImmutableSet.of(), + null, + null, + null, + null, + new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), + null, + permissions + ); + } } diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 048fa1fea9..f7e7216197 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -132,7 +132,7 @@ import org.opensearch.search.internal.SearchContext; import org.opensearch.search.query.QuerySearchResult; import org.opensearch.security.action.apitokens.ApiTokenAction; -import org.opensearch.security.action.apitokens.ApiTokenIndexListenerCache; +import org.opensearch.security.action.apitokens.ApiTokenRepository; import org.opensearch.security.action.apitokens.ApiTokenUpdateAction; import org.opensearch.security.action.apitokens.TransportApiTokenUpdateAction; import org.opensearch.security.action.configupdate.ConfigUpdateAction; @@ -257,6 +257,7 @@ public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin private volatile UserService userService; private volatile RestLayerPrivilegesEvaluator restLayerEvaluator; private volatile ConfigurationRepository cr; + private volatile ApiTokenRepository ar; private volatile AdminDNs adminDns; private volatile ClusterService cs; private volatile AtomicReference localNode = new AtomicReference<>(); @@ -1100,7 +1101,7 @@ public Collection createComponents( adminDns = new AdminDNs(settings); cr = ConfigurationRepository.create(settings, this.configPath, threadPool, localClient, clusterService, auditLog); - ApiTokenIndexListenerCache.getInstance().initialize(clusterService, localClient); + ar = new ApiTokenRepository(localClient, clusterService, tokenManager); this.passwordHasher = PasswordHasherFactory.createPasswordHasher(settings); @@ -1216,6 +1217,7 @@ public Collection createComponents( components.add(dcf); components.add(userService); components.add(passwordHasher); + components.add(ar); components.add(sslSettingsManager); if (isSslCertReloadEnabled(settings) && sslCertificatesHotReloadEnabled(settings)) { diff --git a/src/main/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCache.java b/src/main/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCache.java deleted file mode 100644 index 6cdcc18010..0000000000 --- a/src/main/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCache.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * 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. - */ - -package org.opensearch.security.action.apitokens; - -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicBoolean; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import org.opensearch.client.Client; -import org.opensearch.cluster.ClusterChangedEvent; -import org.opensearch.cluster.ClusterStateListener; -import org.opensearch.cluster.metadata.IndexMetadata; -import org.opensearch.cluster.service.ClusterService; -import org.opensearch.index.query.QueryBuilders; -import org.opensearch.security.support.ConfigConstants; - -import static org.opensearch.security.action.apitokens.ApiToken.NAME_FIELD; - -public class ApiTokenIndexListenerCache implements ClusterStateListener { - - private static final Logger log = LogManager.getLogger(ApiTokenIndexListenerCache.class); - private static final ApiTokenIndexListenerCache INSTANCE = new ApiTokenIndexListenerCache(); - - private final ConcurrentHashMap idToJtiMap = new ConcurrentHashMap<>(); - private final Map jtis = new ConcurrentHashMap<>(); - - private final AtomicBoolean initialized = new AtomicBoolean(false); - private ClusterService clusterService; - private Client client; - - private ApiTokenIndexListenerCache() {} - - public static ApiTokenIndexListenerCache getInstance() { - return INSTANCE; - } - - public void initialize(ClusterService clusterService, Client client) { - if (initialized.compareAndSet(false, true)) { - this.clusterService = clusterService; - this.client = client; - - // Register as cluster state listener - this.clusterService.addListener(this); - } - } - - @Override - public void clusterChanged(ClusterChangedEvent event) { - // Reload cache if the security index has changed - IndexMetadata securityIndex = event.state().metadata().index(getSecurityIndexName()); - if (securityIndex != null) { - reloadApiTokensFromIndex(); - } - } - - void reloadApiTokensFromIndex() { - try { - jtis.clear(); - client.prepareSearch(getSecurityIndexName()) - .setQuery(QueryBuilders.matchAllQuery()) - .execute() - .actionGet() - .getHits() - .forEach(hit -> { - // Parse the document and update the cache - Map source = hit.getSourceAsMap(); - String jti = (String) source.get(NAME_FIELD); - Permissions permissions = parsePermissions(source); - jtis.put(jti, permissions); - }); - } catch (Exception e) { - log.error("Failed to reload API tokens cache", e); - } - } - - private String getSecurityIndexName() { - return ConfigConstants.OPENSEARCH_API_TOKENS_INDEX; - } - - @SuppressWarnings("unchecked") - private Permissions parsePermissions(Map source) { - return new Permissions( - (List) source.get(ApiToken.CLUSTER_PERMISSIONS_FIELD), - (List) source.get(ApiToken.INDEX_PERMISSIONS_FIELD) - ); - } - - public Permissions getPermissionsForJti(String jti) { - return jtis.get(jti); - } - - // Method to check if a token is valid - public boolean isValidToken(String jti) { - return jtis.containsKey(jti); - } - - public Map getJtis() { - return jtis; - } -} diff --git a/src/main/java/org/opensearch/security/action/apitokens/ApiTokenRepository.java b/src/main/java/org/opensearch/security/action/apitokens/ApiTokenRepository.java index cdad23f32d..a16987bef4 100644 --- a/src/main/java/org/opensearch/security/action/apitokens/ApiTokenRepository.java +++ b/src/main/java/org/opensearch/security/action/apitokens/ApiTokenRepository.java @@ -13,32 +13,99 @@ import java.util.List; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import com.google.common.annotations.VisibleForTesting; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.opensearch.client.Client; +import org.opensearch.cluster.ClusterChangedEvent; +import org.opensearch.cluster.ClusterStateListener; +import org.opensearch.cluster.metadata.IndexMetadata; import org.opensearch.cluster.service.ClusterService; import org.opensearch.index.IndexNotFoundException; +import org.opensearch.index.query.QueryBuilders; import org.opensearch.security.authtoken.jwt.ExpiringBearerAuthToken; import org.opensearch.security.identity.SecurityTokenManager; +import org.opensearch.security.support.ConfigConstants; -public class ApiTokenRepository { +import static org.opensearch.security.action.apitokens.ApiToken.NAME_FIELD; + +public class ApiTokenRepository implements ClusterStateListener { private final ApiTokenIndexHandler apiTokenIndexHandler; private final SecurityTokenManager securityTokenManager; + private static final Logger log = LogManager.getLogger(ApiTokenRepository.class); + + private final Map jtis = new ConcurrentHashMap<>(); + + private Client client; + + void reloadApiTokensFromIndex() { + try { + jtis.clear(); + client.prepareSearch(ConfigConstants.OPENSEARCH_API_TOKENS_INDEX) + .setQuery(QueryBuilders.matchAllQuery()) + .execute() + .actionGet() + .getHits() + .forEach(hit -> { + // Parse the document and update the cache + Map source = hit.getSourceAsMap(); + String jti = (String) source.get(NAME_FIELD); + Permissions permissions = parsePermissions(source); + jtis.put(jti, permissions); + }); + } catch (Exception e) { + log.error("Failed to reload API tokens cache", e); + } + } + + @SuppressWarnings("unchecked") + private Permissions parsePermissions(Map source) { + return new Permissions( + (List) source.get(ApiToken.CLUSTER_PERMISSIONS_FIELD), + (List) source.get(ApiToken.INDEX_PERMISSIONS_FIELD) + ); + } + + public Permissions getPermissionsForJti(String jti) { + return jtis.get(jti); + } + + // Method to check if a token is valid + public boolean isValidToken(String jti) { + return jtis.containsKey(jti); + } + + public Map getJtis() { + return jtis; + } public ApiTokenRepository(Client client, ClusterService clusterService, SecurityTokenManager tokenManager) { apiTokenIndexHandler = new ApiTokenIndexHandler(client, clusterService); securityTokenManager = tokenManager; + clusterService.addListener(this); } - private ApiTokenRepository(ApiTokenIndexHandler apiTokenIndexHandler, SecurityTokenManager securityTokenManager) { + private ApiTokenRepository(ApiTokenIndexHandler apiTokenIndexHandler, SecurityTokenManager securityTokenManager, Client client) { this.apiTokenIndexHandler = apiTokenIndexHandler; this.securityTokenManager = securityTokenManager; + this.client = client; } @VisibleForTesting - static ApiTokenRepository forTest(ApiTokenIndexHandler apiTokenIndexHandler, SecurityTokenManager securityTokenManager) { - return new ApiTokenRepository(apiTokenIndexHandler, securityTokenManager); + static ApiTokenRepository forTest(ApiTokenIndexHandler apiTokenIndexHandler, SecurityTokenManager securityTokenManager, Client client) { + return new ApiTokenRepository(apiTokenIndexHandler, securityTokenManager, client); + } + + @Override + public void clusterChanged(ClusterChangedEvent event) { + // Reload cache if the security index has changed + IndexMetadata securityIndex = event.state().metadata().index(ConfigConstants.OPENSEARCH_API_TOKENS_INDEX); + if (securityIndex != null) { + reloadApiTokensFromIndex(); + } } public String createApiToken( diff --git a/src/main/java/org/opensearch/security/action/apitokens/TransportApiTokenUpdateAction.java b/src/main/java/org/opensearch/security/action/apitokens/TransportApiTokenUpdateAction.java index f47bdfad81..c486deab71 100644 --- a/src/main/java/org/opensearch/security/action/apitokens/TransportApiTokenUpdateAction.java +++ b/src/main/java/org/opensearch/security/action/apitokens/TransportApiTokenUpdateAction.java @@ -32,7 +32,7 @@ public class TransportApiTokenUpdateAction extends TransportNodesAction< TransportApiTokenUpdateAction.NodeApiTokenUpdateRequest, ApiTokenUpdateNodeResponse> { - private final ApiTokenIndexListenerCache apiTokenCache; + private final ApiTokenRepository apiTokenRepository; private final ClusterService clusterService; @Inject @@ -41,7 +41,8 @@ public TransportApiTokenUpdateAction( ThreadPool threadPool, ClusterService clusterService, TransportService transportService, - ActionFilters actionFilters + ActionFilters actionFilters, + ApiTokenRepository apiTokenRepository ) { super( ApiTokenUpdateAction.NAME, @@ -54,7 +55,7 @@ public TransportApiTokenUpdateAction( ThreadPool.Names.MANAGEMENT, ApiTokenUpdateNodeResponse.class ); - this.apiTokenCache = ApiTokenIndexListenerCache.getInstance(); + this.apiTokenRepository = apiTokenRepository; this.clusterService = clusterService; } @@ -98,7 +99,7 @@ protected NodeApiTokenUpdateRequest newNodeRequest(ApiTokenUpdateRequest request @Override protected ApiTokenUpdateNodeResponse nodeOperation(final NodeApiTokenUpdateRequest request) { - apiTokenCache.reloadApiTokensFromIndex(); + apiTokenRepository.reloadApiTokensFromIndex(); return new ApiTokenUpdateNodeResponse(clusterService.localNode()); } } diff --git a/src/main/java/org/opensearch/security/http/ApiTokenAuthenticator.java b/src/main/java/org/opensearch/security/http/ApiTokenAuthenticator.java index 517717b874..154c5b7798 100644 --- a/src/main/java/org/opensearch/security/http/ApiTokenAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/ApiTokenAuthenticator.java @@ -25,9 +25,10 @@ import org.opensearch.OpenSearchException; import org.opensearch.OpenSearchSecurityException; import org.opensearch.SpecialPermission; +import org.opensearch.common.inject.Inject; import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; -import org.opensearch.security.action.apitokens.ApiTokenIndexListenerCache; +import org.opensearch.security.action.apitokens.ApiTokenRepository; import org.opensearch.security.auth.HTTPAuthenticator; import org.opensearch.security.filter.SecurityRequest; import org.opensearch.security.filter.SecurityResponse; @@ -56,16 +57,16 @@ public class ApiTokenAuthenticator implements HTTPAuthenticator { private static final String BEARER_PREFIX = "bearer "; private final JwtParser jwtParser; - private final String encryptionKey; private final Boolean apiTokenEnabled; private final String clusterName; public static final String API_TOKEN_USER_PREFIX = "apitoken:"; + private final ApiTokenRepository apiTokenRepository; @SuppressWarnings("removal") - public ApiTokenAuthenticator(Settings settings, String clusterName) { + @Inject + public ApiTokenAuthenticator(Settings settings, String clusterName, ApiTokenRepository apiTokenRepository) { String apiTokenEnabledSetting = settings.get("enabled", "true"); apiTokenEnabled = Boolean.parseBoolean(apiTokenEnabledSetting); - encryptionKey = settings.get("encryption_key"); final SecurityManager sm = System.getSecurityManager(); if (sm != null) { @@ -79,6 +80,7 @@ public JwtParser run() { } }); this.clusterName = clusterName; + this.apiTokenRepository = apiTokenRepository; } private JwtParserBuilder initParserBuilder(final String signingKey) { @@ -126,7 +128,6 @@ private AuthCredentials extractCredentials0(final SecurityRequest request, final log.error("Api token authentication is disabled"); return null; } - ApiTokenIndexListenerCache cache = ApiTokenIndexListenerCache.getInstance(); String jwtToken = extractJwtFromHeader(request); if (jwtToken == null) { @@ -147,7 +148,7 @@ private AuthCredentials extractCredentials0(final SecurityRequest request, final } // TODO: handle revocation different from deletion? - if (!cache.isValidToken(subject)) { + if (!apiTokenRepository.isValidToken(subject)) { log.error("Token is not allowlisted"); return null; } diff --git a/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java b/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java index be952385d8..6d2b3316e9 100644 --- a/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java +++ b/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java @@ -38,6 +38,7 @@ import org.opensearch.core.common.unit.ByteSizeUnit; import org.opensearch.core.common.unit.ByteSizeValue; import org.opensearch.security.action.apitokens.ApiToken; +import org.opensearch.security.action.apitokens.Permissions; import org.opensearch.security.resolver.IndexResolverReplacer; import org.opensearch.security.securityconf.FlattenedActionGroups; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; @@ -430,44 +431,38 @@ PrivilegesEvaluatorResponse apiTokenProvidesClusterPrivilege( ) { String userName = context.getUser().getName(); if (userName.startsWith(API_TOKEN_USER_PREFIX)) { - String jti = context.getUser().getName().split(API_TOKEN_USER_PREFIX)[1]; - if (context.getApiTokenRepository().isValidToken(jti)) { - // Expand the action groups - Set resolvedClusterPermissions = actionGroups.resolve( - context.getApiTokenRepository().getPermissionsForJti(jti).getClusterPerm() - ); - - // Check for wildcard permission - if (!explicit) { - if (resolvedClusterPermissions.contains("*")) { - return PrivilegesEvaluatorResponse.ok(); - } - } + Permissions permissions = context.getPermissionsForApiToken(); + Set resolvedClusterPermissions = actionGroups.resolve(permissions.getClusterPerm()); - // Check for exact match - if (!Collections.disjoint(resolvedClusterPermissions, actions)) { + // Check for wildcard permission + if (!explicit) { + if (resolvedClusterPermissions.contains("*")) { return PrivilegesEvaluatorResponse.ok(); } + } - // Check for pattern matches (like "cluster:*") - for (String permission : resolvedClusterPermissions) { - // skip pure *, which was evaluated above - if (permission != "*") { - // Skip exact matches as we already checked those - if (!permission.contains("*")) { - continue; - } + // Check for exact match + if (!Collections.disjoint(resolvedClusterPermissions, actions)) { + return PrivilegesEvaluatorResponse.ok(); + } - WildcardMatcher permissionMatcher = WildcardMatcher.from(permission); - for (String action : actions) { - if (permissionMatcher.test(action)) { - return PrivilegesEvaluatorResponse.ok(); - } + // Check for pattern matches (like "cluster:*") + for (String permission : resolvedClusterPermissions) { + // skip pure *, which was evaluated above + if (permission != "*") { + // Skip exact matches as we already checked those + if (!permission.contains("*")) { + continue; + } + + WildcardMatcher permissionMatcher = WildcardMatcher.from(permission); + for (String action : actions) { + if (permissionMatcher.test(action)) { + return PrivilegesEvaluatorResponse.ok(); } } } } - } if (actions.size() == 1) { return PrivilegesEvaluatorResponse.insufficient(actions.iterator().next()); @@ -922,70 +917,65 @@ PrivilegesEvaluatorResponse apiTokenProvidesIndexPrivilege( ) { String userName = context.getUser().getName(); if (userName.startsWith(API_TOKEN_USER_PREFIX)) { - String jti = context.getUser().getName().split(API_TOKEN_USER_PREFIX)[1]; - if (context.getApiTokenRepository().isValidToken(jti)) { - List indexPermissions = context.getApiTokenRepository() - .getPermissionsForJti(jti) - .getIndexPermission(); - - for (String concreteIndex : resolvedIndices.getAllIndices()) { - boolean indexHasAllPermissions = false; - - // Check each index permission - for (ApiToken.IndexPermission indexPermission : indexPermissions) { - // First check if this permission applies to this index - boolean indexMatched = false; - for (String pattern : indexPermission.getIndexPatterns()) { - if (WildcardMatcher.from(pattern).test(concreteIndex)) { - indexMatched = true; - break; - } + Permissions permissions = context.getPermissionsForApiToken(); + List indexPermissions = permissions.getIndexPermission(); + + for (String concreteIndex : resolvedIndices.getAllIndices()) { + boolean indexHasAllPermissions = false; + + // Check each index permission + for (ApiToken.IndexPermission indexPermission : indexPermissions) { + // First check if this permission applies to this index + boolean indexMatched = false; + for (String pattern : indexPermission.getIndexPatterns()) { + if (WildcardMatcher.from(pattern).test(concreteIndex)) { + indexMatched = true; + break; } + } - if (!indexMatched) { - continue; - } - - // Index matched, now check if this permission covers all actions - Set remainingActions = new HashSet<>(actions); - ImmutableSet resolvedIndexPermissions = actionGroups.resolve(indexPermission.getAllowedActions()); - - for (String permission : resolvedIndexPermissions) { - // Skip global wildcard if explicit is true - if (explicit && permission.equals("*")) { - continue; - } + if (!indexMatched) { + continue; + } - WildcardMatcher permissionMatcher = WildcardMatcher.from(permission); - remainingActions.removeIf(action -> permissionMatcher.test(action)); + // Index matched, now check if this permission covers all actions + Set remainingActions = new HashSet<>(actions); + ImmutableSet resolvedIndexPermissions = actionGroups.resolve(indexPermission.getAllowedActions()); - if (remainingActions.isEmpty()) { - indexHasAllPermissions = true; - break; - } + for (String permission : resolvedIndexPermissions) { + // Skip global wildcard if explicit is true + if (explicit && permission.equals("*")) { + continue; } - if (indexHasAllPermissions) { - break; // Found a permission that covers all actions for this index + WildcardMatcher permissionMatcher = WildcardMatcher.from(permission); + remainingActions.removeIf(action -> permissionMatcher.test(action)); + + if (remainingActions.isEmpty()) { + indexHasAllPermissions = true; + break; } } - if (!indexHasAllPermissions) { - return PrivilegesEvaluatorResponse.insufficient(checkTable) - .reason( - resolvedIndices.getAllIndices().size() == 1 - ? "Insufficient permissions for the referenced index" - : "None of " - + resolvedIndices.getAllIndices().size() - + " referenced indices has sufficient permissions" - ) - .evaluationExceptions(exceptions); + if (indexHasAllPermissions) { + break; // Found a permission that covers all actions for this index } } - // If we get here, all indices had sufficient permissions - return PrivilegesEvaluatorResponse.ok(); + + if (!indexHasAllPermissions) { + return PrivilegesEvaluatorResponse.insufficient(checkTable) + .reason( + resolvedIndices.getAllIndices().size() == 1 + ? "Insufficient permissions for the referenced index" + : "None of " + resolvedIndices.getAllIndices().size() + " referenced indices has sufficient permissions" + ) + .evaluationExceptions(exceptions); + } } + // If we get here, all indices had sufficient permissions + return PrivilegesEvaluatorResponse.ok(); } + return PrivilegesEvaluatorResponse.insufficient(checkTable) .reason( resolvedIndices.getAllIndices().size() == 1 diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java index 1762e99083..c4c76dfd3a 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java @@ -11,21 +11,27 @@ package org.opensearch.security.privileges; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.function.Supplier; +import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableSet; import org.opensearch.action.ActionRequest; import org.opensearch.cluster.ClusterState; import org.opensearch.cluster.metadata.IndexAbstraction; import org.opensearch.cluster.metadata.IndexNameExpressionResolver; -import org.opensearch.security.action.apitokens.ApiTokenIndexListenerCache; +import org.opensearch.common.inject.Inject; +import org.opensearch.security.action.apitokens.ApiTokenRepository; +import org.opensearch.security.action.apitokens.Permissions; import org.opensearch.security.resolver.IndexResolverReplacer; import org.opensearch.security.support.WildcardMatcher; import org.opensearch.security.user.User; import org.opensearch.tasks.Task; +import static org.opensearch.security.http.ApiTokenAuthenticator.API_TOKEN_USER_PREFIX; + /** * Request-scoped context information for privilege evaluation. *

@@ -46,7 +52,9 @@ public class PrivilegesEvaluationContext { private final IndexResolverReplacer indexResolverReplacer; private final IndexNameExpressionResolver indexNameExpressionResolver; private final Supplier clusterStateSupplier; - private final ApiTokenIndexListenerCache apiTokenIndexListenerCache = ApiTokenIndexListenerCache.getInstance(); + @Inject + private ApiTokenRepository apiTokenRepository; + private Permissions permissionsForApiToken; /** * This caches the ready to use WildcardMatcher instances for the current request. Many index patterns have * to be executed several times per request (for example first for action privileges, later for DLS). Thus, @@ -72,6 +80,7 @@ public PrivilegesEvaluationContext( this.indexResolverReplacer = indexResolverReplacer; this.indexNameExpressionResolver = indexNameExpressionResolver; this.task = task; + this.permissionsForApiToken = extractApiTokenPermissionsForUser(); } public User getUser() { @@ -174,7 +183,40 @@ public String toString() { + '}'; } - public ApiTokenIndexListenerCache getApiTokenRepository() { - return apiTokenIndexListenerCache; + public Permissions getPermissionsForApiToken() { + return permissionsForApiToken; + } + + @VisibleForTesting + PrivilegesEvaluationContext( + User user, + ImmutableSet mappedRoles, + String action, + ActionRequest request, + Task task, + IndexResolverReplacer indexResolverReplacer, + IndexNameExpressionResolver indexNameExpressionResolver, + Supplier clusterStateSupplier, + Permissions permissions + ) { + this.user = user; + this.mappedRoles = mappedRoles; + this.action = action; + this.request = request; + this.clusterStateSupplier = clusterStateSupplier; + this.indexResolverReplacer = indexResolverReplacer; + this.indexNameExpressionResolver = indexNameExpressionResolver; + this.task = task; + this.permissionsForApiToken = permissions; + } + + private Permissions extractApiTokenPermissionsForUser() { + if (user.getName().startsWith(API_TOKEN_USER_PREFIX)) { + String jti = user.getName().split(API_TOKEN_USER_PREFIX)[1]; + if (apiTokenRepository.isValidToken(jti)) { + return apiTokenRepository.getPermissionsForJti(jti); + } + } + return new Permissions(List.of(), List.of()); } } diff --git a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java index b57b422653..facfa4f075 100644 --- a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java +++ b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java @@ -47,8 +47,10 @@ import com.google.common.collect.Multimaps; import org.opensearch.SpecialPermission; +import org.opensearch.common.inject.Inject; import org.opensearch.common.settings.Settings; import org.opensearch.common.xcontent.XContentType; +import org.opensearch.security.action.apitokens.ApiTokenRepository; import org.opensearch.security.auth.AuthDomain; import org.opensearch.security.auth.AuthFailureListener; import org.opensearch.security.auth.AuthenticationBackend; @@ -242,6 +244,9 @@ public Settings getDynamicApiTokenSettings() { .build(); } + @Inject + private ApiTokenRepository apiTokenRepository; + private void buildAAA() { final SortedSet restAuthDomains0 = new TreeSet<>(); @@ -388,7 +393,7 @@ private void buildAAA() { if (!isKeyNull(apiTokenSettings, "signing_key") && !isKeyNull(apiTokenSettings, "encryption_key")) { final AuthDomain _ad = new AuthDomain( new NoOpAuthenticationBackend(Settings.EMPTY, null), - new ApiTokenAuthenticator(getDynamicApiTokenSettings(), this.cih.getClusterName()), + new ApiTokenAuthenticator(getDynamicApiTokenSettings(), this.cih.getClusterName(), apiTokenRepository), false, -2 ); diff --git a/src/test/java/org/opensearch/security/action/apitokens/ApiTokenActionTest.java b/src/test/java/org/opensearch/security/action/apitokens/ApiTokenActionTest.java index 483fe7c9d7..32fadda93e 100644 --- a/src/test/java/org/opensearch/security/action/apitokens/ApiTokenActionTest.java +++ b/src/test/java/org/opensearch/security/action/apitokens/ApiTokenActionTest.java @@ -19,14 +19,17 @@ import org.junit.Test; +import org.opensearch.cluster.service.ClusterService; + import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.mock; public class ApiTokenActionTest { - private final ApiTokenAction apiTokenAction = new ApiTokenAction(null, null, null); + private final ApiTokenAction apiTokenAction = new ApiTokenAction(mock(ClusterService.class), null, null); @Test public void testCreateIndexPermission() { diff --git a/src/test/java/org/opensearch/security/action/apitokens/ApiTokenAuthenticatorTest.java b/src/test/java/org/opensearch/security/action/apitokens/ApiTokenAuthenticatorTest.java index ac0379d0e6..75278f2dbf 100644 --- a/src/test/java/org/opensearch/security/action/apitokens/ApiTokenAuthenticatorTest.java +++ b/src/test/java/org/opensearch/security/action/apitokens/ApiTokenAuthenticatorTest.java @@ -16,7 +16,6 @@ import java.time.temporal.ChronoUnit; import java.util.Base64; import java.util.Date; -import java.util.List; import org.apache.logging.log4j.Logger; import org.junit.Before; @@ -35,7 +34,6 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; -import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.mockito.Mockito.any; @@ -50,6 +48,8 @@ public class ApiTokenAuthenticatorTest { private ApiTokenAuthenticator authenticator; @Mock private Logger log; + @Mock + private ApiTokenRepository apiTokenRepository; private ThreadContext threadcontext; private final String signingKey = Base64.getEncoder() @@ -60,7 +60,7 @@ public class ApiTokenAuthenticatorTest { public void setUp() { Settings settings = Settings.builder().put("enabled", "true").put("signing_key", signingKey).build(); - authenticator = new ApiTokenAuthenticator(settings, "opensearch-cluster"); + authenticator = new ApiTokenAuthenticator(settings, "opensearch-cluster", apiTokenRepository); authenticator.log = log; when(log.isDebugEnabled()).thenReturn(true); threadcontext = new ThreadContext(Settings.EMPTY); @@ -69,8 +69,6 @@ public void setUp() { @Test public void testAuthenticationFailsWhenJtiNotInCache() { String testJti = "test-jti-not-in-cache"; - ApiTokenIndexListenerCache cache = ApiTokenIndexListenerCache.getInstance(); - assertFalse(cache.isValidToken(testJti)); SecurityRequest request = mock(SecurityRequest.class); when(request.header("Authorization")).thenReturn("Bearer " + testJti); @@ -92,7 +90,7 @@ public void testExtractCredentialsPassWhenJtiInCache() { .signWith(SignatureAlgorithm.HS512, signingKey) .compact(); - ApiTokenIndexListenerCache.getInstance().getJtis().put(tokenName, new Permissions(List.of(), List.of())); + when(apiTokenRepository.isValidToken(tokenName)).thenReturn(true); SecurityRequest request = mock(SecurityRequest.class); when(request.header("Authorization")).thenReturn("Bearer " + token); @@ -114,8 +112,6 @@ public void testExtractCredentialsFailWhenTokenIsExpired() { .signWith(SignatureAlgorithm.HS512, signingKey) .compact(); - ApiTokenIndexListenerCache.getInstance().getJtis().put(tokenName, new Permissions(List.of(), List.of())); - SecurityRequest request = mock(SecurityRequest.class); when(request.header("Authorization")).thenReturn("Bearer " + token); when(request.path()).thenReturn("/test"); @@ -138,7 +134,7 @@ public void testExtractCredentialsFailWhenIssuerDoesNotMatch() { .signWith(SignatureAlgorithm.HS512, signingKey) .compact(); - ApiTokenIndexListenerCache.getInstance().getJtis().put(tokenName, new Permissions(List.of(), List.of())); + when(apiTokenRepository.isValidToken(tokenName)).thenReturn(true); SecurityRequest request = mock(SecurityRequest.class); when(request.header("Authorization")).thenReturn("Bearer " + token); @@ -161,8 +157,6 @@ public void testExtractCredentialsFailWhenAccessingRestrictedEndpoint() { .signWith(SignatureAlgorithm.HS512, signingKey) .compact(); - ApiTokenIndexListenerCache.getInstance().getJtis().put(tokenName, new Permissions(List.of(), List.of())); - SecurityRequest request = mock(SecurityRequest.class); when(request.header("Authorization")).thenReturn("Bearer " + token); when(request.path()).thenReturn("/_plugins/_security/api/apitokens"); @@ -183,7 +177,6 @@ public void testAuthenticatorNotEnabled() { .setExpiration(Date.from(Instant.now().plus(1, ChronoUnit.DAYS))) .signWith(SignatureAlgorithm.HS512, signingKey) .compact(); - ApiTokenIndexListenerCache.getInstance().getJtis().put(tokenName, new Permissions(List.of(), List.of())); SecurityRequest request = mock(SecurityRequest.class); @@ -194,7 +187,7 @@ public void testAuthenticatorNotEnabled() { .build(); ThreadContext threadContext = new ThreadContext(settings); - authenticator = new ApiTokenAuthenticator(settings, "opensearch-cluster"); + authenticator = new ApiTokenAuthenticator(settings, "opensearch-cluster", apiTokenRepository); authenticator.log = log; AuthCredentials ac = authenticator.extractCredentials(request, threadContext); diff --git a/src/test/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCacheTest.java b/src/test/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCacheTest.java deleted file mode 100644 index 93c03a3159..0000000000 --- a/src/test/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCacheTest.java +++ /dev/null @@ -1,161 +0,0 @@ -/* - * 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. - */ - -package org.opensearch.security.action.apitokens; - -import java.io.IOException; -import java.util.Arrays; -import java.util.List; - -import org.apache.lucene.search.TotalHits; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -import org.opensearch.action.search.SearchRequestBuilder; -import org.opensearch.action.search.SearchResponse; -import org.opensearch.client.Client; -import org.opensearch.cluster.ClusterChangedEvent; -import org.opensearch.cluster.ClusterState; -import org.opensearch.cluster.metadata.IndexMetadata; -import org.opensearch.cluster.metadata.Metadata; -import org.opensearch.cluster.service.ClusterService; -import org.opensearch.common.action.ActionFuture; -import org.opensearch.common.xcontent.XContentFactory; -import org.opensearch.core.common.bytes.BytesReference; -import org.opensearch.core.xcontent.XContentBuilder; -import org.opensearch.search.SearchHit; -import org.opensearch.search.SearchHits; -import org.opensearch.security.support.ConfigConstants; - -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -@RunWith(MockitoJUnitRunner.class) -public class ApiTokenIndexListenerCacheTest { - - private ApiTokenIndexListenerCache cache; - - @Mock - private ClusterService clusterService; - - @Mock - private Client client; - - @Mock - private ClusterChangedEvent event; - - @Mock - private ClusterState clusterState; - - @Mock - private IndexMetadata indexMetadata; - @Mock - private SearchResponse searchResponse; - - @Mock - private SearchRequestBuilder searchRequestBuilder; - - @Mock - private ActionFuture actionFuture; - - @Before - public void setUp() { - ApiTokenIndexListenerCache.getInstance().initialize(clusterService, client); - cache = ApiTokenIndexListenerCache.getInstance(); - } - - @Test - public void testSingleton() { - ApiTokenIndexListenerCache instance1 = ApiTokenIndexListenerCache.getInstance(); - ApiTokenIndexListenerCache instance2 = ApiTokenIndexListenerCache.getInstance(); - assertSame("getInstance should always return the same instance", instance1, instance2); - } - - @Test - public void testJtisOperations() { - String jti = "testJti"; - Permissions permissions = new Permissions(List.of("read"), List.of(new ApiToken.IndexPermission(List.of(), List.of()))); - - cache.getJtis().put(jti, permissions); - assertEquals("Should retrieve correct permissions", permissions, cache.getJtis().get(jti)); - - cache.getJtis().remove(jti); - assertNull("Should return null after removal", cache.getJtis().get(jti)); - } - - @Test - public void testClearJtis() { - cache.getJtis().put("testJti", new Permissions(List.of("read"), List.of(new ApiToken.IndexPermission(List.of(), List.of())))); - cache.reloadApiTokensFromIndex(); - - assertTrue("Jtis should be empty after clear", cache.getJtis().isEmpty()); - } - - @Test - public void testClusterChangedInvokesReloadTokens() { - ClusterState clusterState = mock(ClusterState.class); - Metadata metadata = mock(Metadata.class); - when(clusterState.metadata()).thenReturn(metadata); - when(metadata.index(ConfigConstants.OPENSEARCH_API_TOKENS_INDEX)).thenReturn(indexMetadata); - when(event.state()).thenReturn(clusterState); - - ApiTokenIndexListenerCache cacheSpy = spy(cache); - cacheSpy.clusterChanged(event); - - verify(cacheSpy).reloadApiTokensFromIndex(); - } - - @Test - public void testReloadApiTokensFromIndexAndParse() throws IOException { - SearchHit hit = createSearchHitFromApiToken("1", Arrays.asList("cluster:monitor"), List.of()); - - SearchHits searchHits = new SearchHits(new SearchHit[] { hit }, new TotalHits(1, TotalHits.Relation.EQUAL_TO), 1.0f); - - // Mock the search response - when(searchResponse.getHits()).thenReturn(searchHits); - when(client.prepareSearch(any())).thenReturn(searchRequestBuilder); - when(searchRequestBuilder.setQuery(any())).thenReturn(searchRequestBuilder); - when(searchRequestBuilder.execute()).thenReturn(actionFuture); - when(actionFuture.actionGet()).thenReturn(searchResponse); - - // Execute the reload - cache.reloadApiTokensFromIndex(); - - // Verify the cache was updated - assertFalse("Jtis should not be empty after reload", cache.getJtis().isEmpty()); - assertEquals("Should have one JTI entry", 1, cache.getJtis().size()); - assertTrue("Should contain testJti", cache.getJtis().containsKey("testJti")); - // Verify extraction works - assertEquals("Should have one cluster action", List.of("cluster:monitor"), cache.getJtis().get("testJti").getClusterPerm()); - assertEquals("Should have no index actions", List.of(), cache.getJtis().get("testJti").getIndexPermission()); - } - - private SearchHit createSearchHitFromApiToken(String id, List allowedActions, List prohibitedActions) - throws IOException { - ApiToken apiToken = new ApiToken("test", allowedActions, prohibitedActions, Long.MAX_VALUE); - XContentBuilder builder = XContentFactory.jsonBuilder(); - apiToken.toXContent(builder, null); - - SearchHit hit = new SearchHit(Integer.parseInt(id), id, null, null, null); - hit.sourceRef(BytesReference.bytes(builder)); - return hit; - } - -} diff --git a/src/test/java/org/opensearch/security/action/apitokens/ApiTokenRepositoryTest.java b/src/test/java/org/opensearch/security/action/apitokens/ApiTokenRepositoryTest.java index 3060ecd290..43dd2f2542 100644 --- a/src/test/java/org/opensearch/security/action/apitokens/ApiTokenRepositoryTest.java +++ b/src/test/java/org/opensearch/security/action/apitokens/ApiTokenRepositoryTest.java @@ -11,35 +11,73 @@ package org.opensearch.security.action.apitokens; +import java.io.IOException; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +import org.apache.lucene.search.TotalHits; import org.junit.Before; import org.junit.Test; - +import org.junit.runner.RunWith; + +import org.opensearch.action.search.SearchRequestBuilder; +import org.opensearch.action.search.SearchResponse; +import org.opensearch.client.Client; +import org.opensearch.cluster.ClusterChangedEvent; +import org.opensearch.cluster.ClusterState; +import org.opensearch.cluster.metadata.IndexMetadata; +import org.opensearch.cluster.metadata.Metadata; +import org.opensearch.common.action.ActionFuture; +import org.opensearch.common.xcontent.XContentFactory; +import org.opensearch.core.common.bytes.BytesReference; +import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.index.IndexNotFoundException; +import org.opensearch.search.SearchHit; +import org.opensearch.search.SearchHits; import org.opensearch.security.authtoken.jwt.ExpiringBearerAuthToken; import org.opensearch.security.identity.SecurityTokenManager; +import org.opensearch.security.support.ConfigConstants; import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.any; import static org.mockito.Mockito.argThat; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +@RunWith(MockitoJUnitRunner.class) public class ApiTokenRepositoryTest { @Mock private SecurityTokenManager securityTokenManager; @Mock private ApiTokenIndexHandler apiTokenIndexHandler; + @Mock + private IndexMetadata indexMetadata; + @Mock + private SearchResponse searchResponse; + + @Mock + private SearchRequestBuilder searchRequestBuilder; + + @Mock + private ActionFuture actionFuture; + @Mock + private Client client; + @Mock + private ClusterChangedEvent event; private ApiTokenRepository repository; @@ -47,7 +85,7 @@ public class ApiTokenRepositoryTest { public void setUp() { apiTokenIndexHandler = mock(ApiTokenIndexHandler.class); securityTokenManager = mock(SecurityTokenManager.class); - repository = ApiTokenRepository.forTest(apiTokenIndexHandler, securityTokenManager); + repository = ApiTokenRepository.forTest(apiTokenIndexHandler, securityTokenManager, client); } @Test @@ -116,4 +154,74 @@ public void testDeleteApiTokenThrowsApiTokenException() throws ApiTokenException repository.deleteApiToken(tokenName); } + + @Test + public void testJtisOperations() { + String jti = "testJti"; + Permissions permissions = new Permissions(List.of("read"), List.of(new ApiToken.IndexPermission(List.of(), List.of()))); + + repository.getJtis().put(jti, permissions); + assertEquals("Should retrieve correct permissions", permissions, repository.getJtis().get(jti)); + + repository.getJtis().remove(jti); + assertNull("Should return null after removal", repository.getJtis().get(jti)); + } + + @Test + public void testClearJtis() { + repository.getJtis().put("testJti", new Permissions(List.of("read"), List.of(new ApiToken.IndexPermission(List.of(), List.of())))); + repository.reloadApiTokensFromIndex(); + + assertTrue("Jtis should be empty after clear", repository.getJtis().isEmpty()); + } + + @Test + public void testClusterChangedInvokesReloadTokens() { + ClusterState clusterState = mock(ClusterState.class); + Metadata metadata = mock(Metadata.class); + when(clusterState.metadata()).thenReturn(metadata); + when(metadata.index(ConfigConstants.OPENSEARCH_API_TOKENS_INDEX)).thenReturn(indexMetadata); + when(event.state()).thenReturn(clusterState); + + ApiTokenRepository cacheSpy = spy(repository); + cacheSpy.clusterChanged(event); + + verify(cacheSpy).reloadApiTokensFromIndex(); + } + + @Test + public void testReloadApiTokensFromIndexAndParse() throws IOException { + SearchHit hit = createSearchHitFromApiToken("1", Arrays.asList("cluster:monitor"), List.of()); + + SearchHits searchHits = new SearchHits(new SearchHit[] { hit }, new TotalHits(1, TotalHits.Relation.EQUAL_TO), 1.0f); + + // Mock the search response + when(searchResponse.getHits()).thenReturn(searchHits); + when(client.prepareSearch(any())).thenReturn(searchRequestBuilder); + when(searchRequestBuilder.setQuery(any())).thenReturn(searchRequestBuilder); + when(searchRequestBuilder.execute()).thenReturn(actionFuture); + when(actionFuture.actionGet()).thenReturn(searchResponse); + + // Execute the reload + repository.reloadApiTokensFromIndex(); + + // Verify the cache was updated + assertFalse("Jtis should not be empty after reload", repository.getJtis().isEmpty()); + assertEquals("Should have one JTI entry", 1, repository.getJtis().size()); + assertTrue("Should contain testJti", repository.getJtis().containsKey("test")); + // Verify extraction works + assertEquals("Should have one cluster action", List.of("cluster:monitor"), repository.getJtis().get("test").getClusterPerm()); + assertEquals("Should have no index actions", List.of(), repository.getJtis().get("test").getIndexPermission()); + } + + private SearchHit createSearchHitFromApiToken(String id, List allowedActions, List prohibitedActions) + throws IOException { + ApiToken apiToken = new ApiToken("test", allowedActions, prohibitedActions, Long.MAX_VALUE); + XContentBuilder builder = XContentFactory.jsonBuilder(); + apiToken.toXContent(builder, null); + + SearchHit hit = new SearchHit(Integer.parseInt(id), id, null, null, null); + hit.sourceRef(BytesReference.bytes(builder)); + return hit; + } } From e44072cbde2e1ff2702d5c08ed93431b863d508b Mon Sep 17 00:00:00 2001 From: Derek Ho Date: Tue, 14 Jan 2025 18:07:21 -0500 Subject: [PATCH 20/20] Inject Signed-off-by: Derek Ho --- .../dlsfls/DlsFlsLegacyHeadersTest.java | 18 +- .../dlsfls/DocumentPrivilegesTest.java | 13 +- .../dlsfls/FieldPrivilegesTest.java | 5 +- .../security/OpenSearchSecurityPlugin.java | 7 +- .../security/action/apitokens/ApiToken.java | 1 - .../action/apitokens/ApiTokenAction.java | 11 +- .../action/apitokens/ApiTokenRepository.java | 3 + .../authtoken/jwt/ApiTokenJwtVendor.java | 91 ++++++++++ .../security/authtoken/jwt/JwtVendor.java | 170 +++--------------- .../security/authtoken/jwt/OBOJwtVendor.java | 120 +++++++++++++ .../identity/SecurityTokenManager.java | 30 +++- .../PrivilegesEvaluationContext.java | 6 +- .../privileges/PrivilegesEvaluator.java | 9 +- .../action/apitokens/ApiTokenActionTest.java | 2 +- ...tVendorTest.java => OBOJwtVendorTest.java} | 46 ++--- .../identity/SecurityTokenManagerTest.java | 14 +- 16 files changed, 342 insertions(+), 204 deletions(-) create mode 100644 src/main/java/org/opensearch/security/authtoken/jwt/ApiTokenJwtVendor.java create mode 100644 src/main/java/org/opensearch/security/authtoken/jwt/OBOJwtVendor.java rename src/test/java/org/opensearch/security/authtoken/jwt/{JwtVendorTest.java => OBOJwtVendorTest.java} (87%) diff --git a/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/DlsFlsLegacyHeadersTest.java b/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/DlsFlsLegacyHeadersTest.java index 2c8e6de587..1224c1bc23 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/DlsFlsLegacyHeadersTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/DlsFlsLegacyHeadersTest.java @@ -35,6 +35,7 @@ import org.opensearch.index.query.RangeQueryBuilder; import org.opensearch.index.query.TermQueryBuilder; import org.opensearch.search.internal.ShardSearchRequest; +import org.opensearch.security.action.apitokens.ApiTokenRepository; import org.opensearch.security.privileges.PrivilegesEvaluationContext; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; import org.opensearch.security.securityconf.impl.v7.RoleV7; @@ -47,6 +48,7 @@ import org.mockito.Mockito; +import static org.mockito.Mockito.mock; import static org.opensearch.security.Song.ARTIST_STRING; import static org.opensearch.security.Song.ARTIST_TWINS; import static org.opensearch.security.Song.FIELD_ARTIST; @@ -255,11 +257,11 @@ public void performHeaderDecoration_oldNode() throws Exception { Metadata metadata = exampleMetadata(); DlsFlsProcessedConfig dlsFlsProcessedConfig = dlsFlsProcessedConfig(exampleRolesConfig(), metadata); - Transport.Connection connection = Mockito.mock(Transport.Connection.class); + Transport.Connection connection = mock(Transport.Connection.class); Mockito.when(connection.getVersion()).thenReturn(Version.V_2_0_0); // ShardSearchRequest does not extend ActionRequest, thus the headers must be set - ShardSearchRequest request = Mockito.mock(ShardSearchRequest.class); + ShardSearchRequest request = mock(ShardSearchRequest.class); Map headerSink = new HashMap<>(); @@ -277,7 +279,7 @@ public void performHeaderDecoration_actionRequest() throws Exception { Metadata metadata = exampleMetadata(); DlsFlsProcessedConfig dlsFlsProcessedConfig = dlsFlsProcessedConfig(exampleRolesConfig(), metadata); - Transport.Connection connection = Mockito.mock(Transport.Connection.class); + Transport.Connection connection = mock(Transport.Connection.class); Mockito.when(connection.getVersion()).thenReturn(Version.V_2_0_0); // SearchRequest does extend ActionRequest, thus the headers must not be set @@ -296,11 +298,11 @@ public void performHeaderDecoration_newNode() throws Exception { Metadata metadata = exampleMetadata(); DlsFlsProcessedConfig dlsFlsProcessedConfig = dlsFlsProcessedConfig(exampleRolesConfig(), metadata); - Transport.Connection connection = Mockito.mock(Transport.Connection.class); + Transport.Connection connection = mock(Transport.Connection.class); Mockito.when(connection.getVersion()).thenReturn(Version.V_3_0_0); // ShardSearchRequest does not extend ActionRequest, thus the headers must be set - ShardSearchRequest request = Mockito.mock(ShardSearchRequest.class); + ShardSearchRequest request = mock(ShardSearchRequest.class); Map headerSink = new HashMap<>(); @@ -345,7 +347,8 @@ public void prepare_ccs() throws Exception { null, null, new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), - () -> clusterState + () -> clusterState, + mock(ApiTokenRepository.class) ); DlsFlsLegacyHeaders.prepare(threadContext, ctx, dlsFlsProcessedConfig(exampleRolesConfig(), metadata), metadata, false); @@ -364,7 +367,8 @@ static PrivilegesEvaluationContext ctx(Metadata metadata, String... roles) { null, null, new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), - () -> clusterState + () -> clusterState, + mock(ApiTokenRepository.class) ); } diff --git a/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/DocumentPrivilegesTest.java b/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/DocumentPrivilegesTest.java index 97a0ddb69e..262498ff9e 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/DocumentPrivilegesTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/DocumentPrivilegesTest.java @@ -51,6 +51,7 @@ import org.opensearch.index.query.QueryBuilder; import org.opensearch.index.query.QueryBuilders; import org.opensearch.index.query.TermQueryBuilder; +import org.opensearch.security.action.apitokens.ApiTokenRepository; import org.opensearch.security.privileges.PrivilegesConfigurationValidationException; import org.opensearch.security.privileges.PrivilegesEvaluationContext; import org.opensearch.security.privileges.PrivilegesEvaluationException; @@ -61,6 +62,7 @@ import org.opensearch.test.framework.TestSecurityConfig; import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.Mockito.mock; import static org.opensearch.security.util.MockIndexMetadataBuilder.dataStreams; import static org.opensearch.security.util.MockIndexMetadataBuilder.indices; import static org.junit.Assert.assertEquals; @@ -526,7 +528,8 @@ public IndicesAndAliases_getRestriction( null, null, null, - () -> CLUSTER_STATE + () -> CLUSTER_STATE, + mock(ApiTokenRepository.class) ); this.statefulness = statefulness; this.dfmEmptyOverridesAll = dfmEmptyOverridesAll == DfmEmptyOverridesAll.DFM_EMPTY_OVERRIDES_ALL_TRUE; @@ -841,7 +844,8 @@ public IndicesRequest indices(String... strings) { null, RESOLVER_REPLACER, INDEX_NAME_EXPRESSION_RESOLVER, - () -> CLUSTER_STATE + () -> CLUSTER_STATE, + mock(ApiTokenRepository.class) ); this.statefulness = statefulness; this.dfmEmptyOverridesAll = dfmEmptyOverridesAll == DfmEmptyOverridesAll.DFM_EMPTY_OVERRIDES_ALL_TRUE; @@ -1126,7 +1130,8 @@ public DataStreams_getRestriction( null, null, null, - () -> CLUSTER_STATE + () -> CLUSTER_STATE, + mock(ApiTokenRepository.class) ); this.statefulness = statefulness; this.dfmEmptyOverridesAll = dfmEmptyOverridesAll == DfmEmptyOverridesAll.DFM_EMPTY_OVERRIDES_ALL_TRUE; @@ -1146,7 +1151,7 @@ public void invalidQuery() throws Exception { @Test(expected = PrivilegesEvaluationException.class) public void invalidTemplatedQuery() throws Exception { DocumentPrivileges.DlsQuery.create("{\"invalid\": \"totally ${attr.foo}\"}", xContentRegistry) - .evaluate(new PrivilegesEvaluationContext(new User("test_user"), ImmutableSet.of(), null, null, null, null, null, null)); + .evaluate(new PrivilegesEvaluationContext(new User("test_user"), ImmutableSet.of(), null, null, null, null, null, null, mock(ApiTokenRepository.class))); } @Test diff --git a/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/FieldPrivilegesTest.java b/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/FieldPrivilegesTest.java index 54a32e9972..394296b7d3 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/FieldPrivilegesTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/FieldPrivilegesTest.java @@ -21,6 +21,7 @@ import org.opensearch.cluster.ClusterState; import org.opensearch.cluster.metadata.Metadata; import org.opensearch.common.settings.Settings; +import org.opensearch.security.action.apitokens.ApiTokenRepository; import org.opensearch.security.privileges.PrivilegesConfigurationValidationException; import org.opensearch.security.privileges.PrivilegesEvaluationContext; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; @@ -29,6 +30,7 @@ import org.opensearch.security.user.User; import org.opensearch.test.framework.TestSecurityConfig; +import static org.mockito.Mockito.mock; import static org.opensearch.security.util.MockIndexMetadataBuilder.indices; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -158,7 +160,8 @@ static PrivilegesEvaluationContext ctx(String... roles) { null, null, null, - () -> CLUSTER_STATE + () -> CLUSTER_STATE, + mock(ApiTokenRepository.class) ); } } diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index f7e7216197..45b61423ab 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -647,7 +647,7 @@ public List getRestHandlers( ) ); handlers.add(new CreateOnBehalfOfTokenAction(tokenManager)); - handlers.add(new ApiTokenAction(cs, localClient, tokenManager)); + handlers.add(new ApiTokenAction(ar)); handlers.addAll( SecurityRestApiActions.getHandler( settings, @@ -1101,7 +1101,6 @@ public Collection createComponents( adminDns = new AdminDNs(settings); cr = ConfigurationRepository.create(settings, this.configPath, threadPool, localClient, clusterService, auditLog); - ar = new ApiTokenRepository(localClient, clusterService, tokenManager); this.passwordHasher = PasswordHasherFactory.createPasswordHasher(settings); @@ -1110,6 +1109,7 @@ public Collection createComponents( final XFFResolver xffResolver = new XFFResolver(threadPool); backendRegistry = new BackendRegistry(settings, adminDns, xffResolver, auditLog, threadPool); tokenManager = new SecurityTokenManager(cs, threadPool, userService); + ar = new ApiTokenRepository(localClient, clusterService, tokenManager); final CompatConfig compatConfig = new CompatConfig(environment, transportPassiveAuthSetting); @@ -1125,7 +1125,8 @@ public Collection createComponents( privilegesInterceptor, cih, irr, - namedXContentRegistry.get() + namedXContentRegistry.get(), + ar ); dlsFlsBaseContext = new DlsFlsBaseContext(evaluator, threadPool.getThreadContext(), adminDns); diff --git a/src/main/java/org/opensearch/security/action/apitokens/ApiToken.java b/src/main/java/org/opensearch/security/action/apitokens/ApiToken.java index b1aa775ed9..6a81ad9f4d 100644 --- a/src/main/java/org/opensearch/security/action/apitokens/ApiToken.java +++ b/src/main/java/org/opensearch/security/action/apitokens/ApiToken.java @@ -133,7 +133,6 @@ public static IndexPermission fromXContent(XContentParser parser) throws IOExcep */ public static ApiToken fromXContent(XContentParser parser) throws IOException { String name = null; - String jti = null; List clusterPermissions = new ArrayList<>(); List indexPermissions = new ArrayList<>(); Instant creationTime = null; diff --git a/src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java b/src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java index 75bf3ffa01..d690083ba1 100644 --- a/src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java +++ b/src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java @@ -26,6 +26,7 @@ import org.opensearch.client.Client; import org.opensearch.client.node.NodeClient; import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.inject.Inject; import org.opensearch.core.action.ActionListener; import org.opensearch.core.rest.RestStatus; import org.opensearch.core.xcontent.XContentBuilder; @@ -51,9 +52,11 @@ import static org.opensearch.security.util.ParsingUtils.safeStringList; public class ApiTokenAction extends BaseRestHandler { - private final ApiTokenRepository apiTokenRepository; + private ApiTokenRepository apiTokenRepository; public Logger log = LogManager.getLogger(this.getClass()); + + private static final List ROUTES = addRoutesPrefix( ImmutableList.of( new RestHandler.Route(POST, "/apitokens"), @@ -62,8 +65,10 @@ public class ApiTokenAction extends BaseRestHandler { ) ); - public ApiTokenAction(ClusterService clusterService, Client client, SecurityTokenManager securityTokenManager) { - this.apiTokenRepository = new ApiTokenRepository(client, clusterService, securityTokenManager); + @Inject + public ApiTokenAction(ApiTokenRepository apiTokenRepository) { + this.apiTokenRepository = apiTokenRepository; +// this.apiTokenRepository = new ApiTokenRepository(client, clusterService, securityTokenManager); } @Override diff --git a/src/main/java/org/opensearch/security/action/apitokens/ApiTokenRepository.java b/src/main/java/org/opensearch/security/action/apitokens/ApiTokenRepository.java index a16987bef4..3eabe60119 100644 --- a/src/main/java/org/opensearch/security/action/apitokens/ApiTokenRepository.java +++ b/src/main/java/org/opensearch/security/action/apitokens/ApiTokenRepository.java @@ -24,6 +24,7 @@ import org.opensearch.cluster.ClusterStateListener; import org.opensearch.cluster.metadata.IndexMetadata; import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.inject.Inject; import org.opensearch.index.IndexNotFoundException; import org.opensearch.index.query.QueryBuilders; import org.opensearch.security.authtoken.jwt.ExpiringBearerAuthToken; @@ -82,9 +83,11 @@ public Map getJtis() { return jtis; } + @Inject public ApiTokenRepository(Client client, ClusterService clusterService, SecurityTokenManager tokenManager) { apiTokenIndexHandler = new ApiTokenIndexHandler(client, clusterService); securityTokenManager = tokenManager; + this.client = client; clusterService.addListener(this); } diff --git a/src/main/java/org/opensearch/security/authtoken/jwt/ApiTokenJwtVendor.java b/src/main/java/org/opensearch/security/authtoken/jwt/ApiTokenJwtVendor.java new file mode 100644 index 0000000000..da07d6e087 --- /dev/null +++ b/src/main/java/org/opensearch/security/authtoken/jwt/ApiTokenJwtVendor.java @@ -0,0 +1,91 @@ +/* + * 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.authtoken.jwt; + +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.text.ParseException; +import java.util.Base64; +import java.util.Date; +import java.util.List; +import java.util.Optional; +import java.util.function.LongSupplier; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.OpenSearchException; +import org.opensearch.common.collect.Tuple; +import org.opensearch.common.settings.Settings; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.JWSSigner; +import com.nimbusds.jose.KeyLengthException; +import com.nimbusds.jose.crypto.MACSigner; +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.KeyUse; +import com.nimbusds.jose.jwk.OctetSequenceKey; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; + +import static org.opensearch.security.authtoken.jwt.JwtVendor.createJwkFromSettings; +import static org.opensearch.security.util.AuthTokenUtils.isKeyNull; + +public class ApiTokenJwtVendor extends JwtVendor { + private static final Logger logger = LogManager.getLogger(ApiTokenJwtVendor.class); + + private final JWK signingKey; + private final JWSSigner signer; + private final LongSupplier timeProvider; + private static final Integer MAX_EXPIRY_SECONDS = 600; + + public ApiTokenJwtVendor(final Settings settings, final Optional timeProvider) { + final Tuple tuple = createJwkFromSettings(settings); + signingKey = tuple.v1(); + signer = tuple.v2(); + + this.timeProvider = timeProvider.orElse(System::currentTimeMillis); + } + + @Override + @SuppressWarnings("removal") + public ExpiringBearerAuthToken createJwt(final String issuer, final String subject, final String audience, final long expiration) + throws JOSEException, ParseException { + final long currentTimeMs = timeProvider.getAsLong(); + final Date now = new Date(currentTimeMs); + + final JWTClaimsSet.Builder claimsBuilder = new JWTClaimsSet.Builder(); + claimsBuilder.issuer(issuer); + claimsBuilder.issueTime(now); + claimsBuilder.subject(subject); + claimsBuilder.audience(audience); + claimsBuilder.notBeforeTime(now); + final Date expiryTime = new Date(expiration); + claimsBuilder.expirationTime(expiryTime); + + final JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.parse(signingKey.getAlgorithm().getName())).build(); + + final SignedJWT signedJwt = AccessController.doPrivileged( + (PrivilegedAction) () -> new SignedJWT(header, claimsBuilder.build()) + ); + // Sign the JWT so it can be serialized + signedJwt.sign(signer); + if (logger.isDebugEnabled()) { + logger.debug( + "Created JWT: " + signedJwt.serialize() + "\n" + signedJwt.getHeader().toJSONObject() + "\n" + signedJwt.getJWTClaimsSet() + ); + } + return new ExpiringBearerAuthToken(signedJwt.serialize(), subject, expiryTime); + } +} diff --git a/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java b/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java index 0c91b3c093..c66fdd2254 100644 --- a/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java +++ b/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java @@ -11,58 +11,43 @@ package org.opensearch.security.authtoken.jwt; -import java.security.AccessController; -import java.security.PrivilegedAction; -import java.text.ParseException; -import java.util.Base64; -import java.util.Date; -import java.util.List; -import java.util.Optional; -import java.util.function.LongSupplier; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import org.opensearch.OpenSearchException; -import org.opensearch.common.collect.Tuple; -import org.opensearch.common.settings.Settings; - import com.nimbusds.jose.JOSEException; import com.nimbusds.jose.JWSAlgorithm; -import com.nimbusds.jose.JWSHeader; import com.nimbusds.jose.JWSSigner; import com.nimbusds.jose.KeyLengthException; import com.nimbusds.jose.crypto.MACSigner; import com.nimbusds.jose.jwk.JWK; import com.nimbusds.jose.jwk.KeyUse; import com.nimbusds.jose.jwk.OctetSequenceKey; -import com.nimbusds.jwt.JWTClaimsSet; -import com.nimbusds.jwt.SignedJWT; - -import static org.opensearch.security.util.AuthTokenUtils.isKeyNull; - -public class JwtVendor { - private static final Logger logger = LogManager.getLogger(JwtVendor.class); +import org.opensearch.OpenSearchException; +import org.opensearch.common.collect.Tuple; +import org.opensearch.common.settings.Settings; - private final JWK signingKey; - private final JWSSigner signer; - private final LongSupplier timeProvider; - private final EncryptionDecryptionUtil encryptionDecryptionUtil; - private static final Integer MAX_EXPIRY_SECONDS = 600; +import java.text.ParseException; +import java.util.Base64; +import java.util.List; - public JwtVendor(final Settings settings, final Optional timeProvider) { - final Tuple tuple = createJwkFromSettings(settings); - signingKey = tuple.v1(); - signer = tuple.v2(); +import static org.opensearch.security.util.AuthTokenUtils.isKeyNull; - if (isKeyNull(settings, "encryption_key")) { - throw new IllegalArgumentException("encryption_key cannot be null"); - } else { - this.encryptionDecryptionUtil = new EncryptionDecryptionUtil(settings.get("encryption_key")); - } - this.timeProvider = timeProvider.orElse(System::currentTimeMillis); +public abstract class JwtVendor { + public ExpiringBearerAuthToken createJwt( + final String issuer, + final String subject, + final String audience, + final long requestedExpirySeconds, + final List roles, + final List backendRoles, + final boolean includeBackendRoles + ) throws JOSEException, ParseException { + throw new UnsupportedOperationException("createJwt with given params is not supported."); } + public ExpiringBearerAuthToken createJwt( + final String issuer, final String subject, final String audience, final long expiration + ) throws JOSEException, ParseException { + throw new UnsupportedOperationException("createJwt with given params is not supported."); + }; + /* * The default configuration of this web key should be: * KeyType: OCTET @@ -74,21 +59,21 @@ static Tuple createJwkFromSettings(final Settings settings) { if (!isKeyNull(settings, "signing_key")) { final String signingKey = settings.get("signing_key"); key = new OctetSequenceKey.Builder(Base64.getDecoder().decode(signingKey)).algorithm(JWSAlgorithm.HS512) - .keyUse(KeyUse.SIGNATURE) - .build(); + .keyUse(KeyUse.SIGNATURE) + .build(); } else { final Settings jwkSettings = settings.getAsSettings("jwt").getAsSettings("key"); if (jwkSettings.isEmpty()) { throw new OpenSearchException( - "Settings for signing key is missing. Please specify at least the option signing_key with a shared secret." + "Settings for signing key is missing. Please specify at least the option signing_key with a shared secret." ); } final String signingKey = jwkSettings.get("k"); key = new OctetSequenceKey.Builder(Base64.getDecoder().decode(signingKey)).algorithm(JWSAlgorithm.HS512) - .keyUse(KeyUse.SIGNATURE) - .build(); + .keyUse(KeyUse.SIGNATURE) + .build(); } try { @@ -97,101 +82,4 @@ static Tuple createJwkFromSettings(final Settings settings) { throw new OpenSearchException(kle); } } - - public ExpiringBearerAuthToken createJwt( - final String issuer, - final String subject, - final String audience, - final long requestedExpirySeconds, - final List roles, - final List backendRoles, - final boolean includeBackendRoles - ) throws JOSEException, ParseException { - final long currentTimeMs = timeProvider.getAsLong(); - final Date now = new Date(currentTimeMs); - - final JWTClaimsSet.Builder claimsBuilder = new JWTClaimsSet.Builder(); - claimsBuilder.issuer(issuer); - claimsBuilder.issueTime(now); - claimsBuilder.subject(subject); - claimsBuilder.audience(audience); - claimsBuilder.notBeforeTime(now); - - final long expirySeconds = Math.min(requestedExpirySeconds, MAX_EXPIRY_SECONDS); - if (expirySeconds <= 0) { - throw new IllegalArgumentException("The expiration time should be a positive integer"); - } - final Date expiryTime = new Date(currentTimeMs + expirySeconds * 1000); - claimsBuilder.expirationTime(expiryTime); - - if (roles != null) { - final String listOfRoles = String.join(",", roles); - claimsBuilder.claim("er", encryptionDecryptionUtil.encrypt(listOfRoles)); - } else { - throw new IllegalArgumentException("Roles cannot be null"); - } - - if (includeBackendRoles && backendRoles != null) { - final String listOfBackendRoles = String.join(",", backendRoles); - claimsBuilder.claim("br", listOfBackendRoles); - } - - final JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.parse(signingKey.getAlgorithm().getName())).build(); - final SignedJWT signedJwt = new SignedJWT(header, claimsBuilder.build()); - - // Sign the JWT so it can be serialized - signedJwt.sign(signer); - - if (logger.isDebugEnabled()) { - logger.debug( - "Created JWT: " + signedJwt.serialize() + "\n" + signedJwt.getHeader().toJSONObject() + "\n" + signedJwt.getJWTClaimsSet() - ); - } - - return new ExpiringBearerAuthToken(signedJwt.serialize(), subject, expiryTime, expirySeconds); - } - - @SuppressWarnings("removal") - public ExpiringBearerAuthToken createJwt(final String issuer, final String subject, final String audience, final long expiration) - throws JOSEException, ParseException { - final long currentTimeMs = timeProvider.getAsLong(); - final Date now = new Date(currentTimeMs); - - final JWTClaimsSet.Builder claimsBuilder = new JWTClaimsSet.Builder(); - claimsBuilder.issuer(issuer); - claimsBuilder.issueTime(now); - claimsBuilder.subject(subject); - claimsBuilder.audience(audience); - claimsBuilder.notBeforeTime(now); - - final Date expiryTime = new Date(expiration); - claimsBuilder.expirationTime(expiryTime); - - final JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.parse(signingKey.getAlgorithm().getName())).build(); - - final SignedJWT signedJwt = AccessController.doPrivileged( - (PrivilegedAction) () -> new SignedJWT(header, claimsBuilder.build()) - ); - - // Sign the JWT so it can be serialized - signedJwt.sign(signer); - - if (logger.isDebugEnabled()) { - logger.debug( - "Created JWT: " + signedJwt.serialize() + "\n" + signedJwt.getHeader().toJSONObject() + "\n" + signedJwt.getJWTClaimsSet() - ); - } - - return new ExpiringBearerAuthToken(signedJwt.serialize(), subject, expiryTime); - } - - /* Returns the encrypted string based on encryption settings */ - public String encryptString(final String input) { - return encryptionDecryptionUtil.encrypt(input); - } - - /* Returns the decrypted string based on encryption settings */ - public String decryptString(final String input) { - return encryptionDecryptionUtil.decrypt(input); - } } diff --git a/src/main/java/org/opensearch/security/authtoken/jwt/OBOJwtVendor.java b/src/main/java/org/opensearch/security/authtoken/jwt/OBOJwtVendor.java new file mode 100644 index 0000000000..bde3d1980c --- /dev/null +++ b/src/main/java/org/opensearch/security/authtoken/jwt/OBOJwtVendor.java @@ -0,0 +1,120 @@ +/* + * 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.authtoken.jwt; + +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.text.ParseException; +import java.util.Base64; +import java.util.Date; +import java.util.List; +import java.util.Optional; +import java.util.function.LongSupplier; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.OpenSearchException; +import org.opensearch.common.collect.Tuple; +import org.opensearch.common.settings.Settings; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.JWSSigner; +import com.nimbusds.jose.KeyLengthException; +import com.nimbusds.jose.crypto.MACSigner; +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.KeyUse; +import com.nimbusds.jose.jwk.OctetSequenceKey; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; + +import static org.opensearch.security.authtoken.jwt.JwtVendor.createJwkFromSettings; +import static org.opensearch.security.util.AuthTokenUtils.isKeyNull; + +public class OBOJwtVendor extends JwtVendor { + private static final Logger logger = LogManager.getLogger(OBOJwtVendor.class); + + private final JWK signingKey; + private final JWSSigner signer; + private final LongSupplier timeProvider; + private final EncryptionDecryptionUtil encryptionDecryptionUtil; + private static final Integer MAX_EXPIRY_SECONDS = 600; + + public OBOJwtVendor(final Settings settings, final Optional timeProvider) { + final Tuple tuple = createJwkFromSettings(settings); + signingKey = tuple.v1(); + signer = tuple.v2(); + + if (isKeyNull(settings, "encryption_key")) { + throw new IllegalArgumentException("encryption_key cannot be null"); + } else { + this.encryptionDecryptionUtil = new EncryptionDecryptionUtil(settings.get("encryption_key")); + } + this.timeProvider = timeProvider.orElse(System::currentTimeMillis); + } + + @Override + public ExpiringBearerAuthToken createJwt( + final String issuer, + final String subject, + final String audience, + final long requestedExpirySeconds, + final List roles, + final List backendRoles, + final boolean includeBackendRoles + ) throws JOSEException, ParseException { + final long currentTimeMs = timeProvider.getAsLong(); + final Date now = new Date(currentTimeMs); + + final JWTClaimsSet.Builder claimsBuilder = new JWTClaimsSet.Builder(); + claimsBuilder.issuer(issuer); + claimsBuilder.issueTime(now); + claimsBuilder.subject(subject); + claimsBuilder.audience(audience); + claimsBuilder.notBeforeTime(now); + + final long expirySeconds = Math.min(requestedExpirySeconds, MAX_EXPIRY_SECONDS); + if (expirySeconds <= 0) { + throw new IllegalArgumentException("The expiration time should be a positive integer"); + } + final Date expiryTime = new Date(currentTimeMs + expirySeconds * 1000); + claimsBuilder.expirationTime(expiryTime); + + if (roles != null) { + final String listOfRoles = String.join(",", roles); + claimsBuilder.claim("er", encryptionDecryptionUtil.encrypt(listOfRoles)); + } else { + throw new IllegalArgumentException("Roles cannot be null"); + } + + if (includeBackendRoles && backendRoles != null) { + final String listOfBackendRoles = String.join(",", backendRoles); + claimsBuilder.claim("br", listOfBackendRoles); + } + + final JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.parse(signingKey.getAlgorithm().getName())).build(); + final SignedJWT signedJwt = new SignedJWT(header, claimsBuilder.build()); + + // Sign the JWT so it can be serialized + signedJwt.sign(signer); + + if (logger.isDebugEnabled()) { + logger.debug( + "Created JWT: " + signedJwt.serialize() + "\n" + signedJwt.getHeader().toJSONObject() + "\n" + signedJwt.getJWTClaimsSet() + ); + } + + return new ExpiringBearerAuthToken(signedJwt.serialize(), subject, expiryTime, expirySeconds); + } +} diff --git a/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java b/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java index 7c85684e18..3ff3c4e2d8 100644 --- a/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java +++ b/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java @@ -27,8 +27,9 @@ import org.opensearch.identity.tokens.AuthToken; import org.opensearch.identity.tokens.OnBehalfOfClaims; import org.opensearch.identity.tokens.TokenManager; +import org.opensearch.security.authtoken.jwt.ApiTokenJwtVendor; import org.opensearch.security.authtoken.jwt.ExpiringBearerAuthToken; -import org.opensearch.security.authtoken.jwt.JwtVendor; +import org.opensearch.security.authtoken.jwt.OBOJwtVendor; import org.opensearch.security.securityconf.ConfigModel; import org.opensearch.security.securityconf.DynamicConfigModel; import org.opensearch.security.support.ConfigConstants; @@ -50,8 +51,8 @@ public class SecurityTokenManager implements TokenManager { private final ThreadPool threadPool; private final UserService userService; - private JwtVendor oboJwtVendor = null; - private JwtVendor apiTokenJwtVendor = null; + private OBOJwtVendor oboJwtVendor = null; + private ApiTokenJwtVendor apiTokenOBOJwtVendor = null; private ConfigModel configModel = null; public SecurityTokenManager(final ClusterService cs, final ThreadPool threadPool, final UserService userService) { @@ -70,31 +71,42 @@ public void onDynamicConfigModelChanged(final DynamicConfigModel dcm) { final Settings oboSettings = dcm.getDynamicOnBehalfOfSettings(); final Boolean oboEnabled = oboSettings.getAsBoolean("enabled", false); if (oboEnabled) { - oboJwtVendor = createJwtVendor(oboSettings); + oboJwtVendor = createOboJwtVendor(oboSettings); } final Settings apiTokenSettings = dcm.getDynamicApiTokenSettings(); final Boolean apiTokenEnabled = apiTokenSettings.getAsBoolean("enabled", false); if (apiTokenEnabled) { - apiTokenJwtVendor = createJwtVendor(apiTokenSettings); + apiTokenOBOJwtVendor = createApiTokenJwtVendor(apiTokenSettings); } } /** For testing */ - JwtVendor createJwtVendor(final Settings settings) { + OBOJwtVendor createOboJwtVendor(final Settings settings) { try { - return new JwtVendor(settings, Optional.empty()); + return new OBOJwtVendor(settings, Optional.empty()); } catch (final Exception ex) { logger.error("Unable to create the JwtVendor instance", ex); return null; } } + /** For testing */ + ApiTokenJwtVendor createApiTokenJwtVendor(final Settings settings) { + try { + return new ApiTokenJwtVendor(settings, Optional.empty()); + } catch (final Exception ex) { + logger.error("Unable to create the JwtVendor instance", ex); + return null; + } + } + + public boolean issueOnBehalfOfTokenAllowed() { return oboJwtVendor != null && configModel != null; } public boolean issueApiTokenAllowed() { - return apiTokenJwtVendor != null && configModel != null; + return apiTokenOBOJwtVendor != null && configModel != null; } @Override @@ -146,7 +158,7 @@ public ExpiringBearerAuthToken issueApiToken(final String name, final Long expir final User user = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); try { - return apiTokenJwtVendor.createJwt(cs.getClusterName().value(), name, name, expiration); + return apiTokenOBOJwtVendor.createJwt(cs.getClusterName().value(), name, name, expiration); } catch (final Exception ex) { logger.error("Error creating Api Token for " + user.getName(), ex); throw new OpenSearchSecurityException("Unable to generate Api Token"); diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java index c4c76dfd3a..40c668a164 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java @@ -52,7 +52,6 @@ public class PrivilegesEvaluationContext { private final IndexResolverReplacer indexResolverReplacer; private final IndexNameExpressionResolver indexNameExpressionResolver; private final Supplier clusterStateSupplier; - @Inject private ApiTokenRepository apiTokenRepository; private Permissions permissionsForApiToken; /** @@ -62,6 +61,7 @@ public class PrivilegesEvaluationContext { */ private final Map renderedPatternTemplateCache = new HashMap<>(); + @Inject public PrivilegesEvaluationContext( User user, ImmutableSet mappedRoles, @@ -70,7 +70,8 @@ public PrivilegesEvaluationContext( Task task, IndexResolverReplacer indexResolverReplacer, IndexNameExpressionResolver indexNameExpressionResolver, - Supplier clusterStateSupplier + Supplier clusterStateSupplier, + ApiTokenRepository apiTokenRepository ) { this.user = user; this.mappedRoles = mappedRoles; @@ -80,6 +81,7 @@ public PrivilegesEvaluationContext( this.indexResolverReplacer = indexResolverReplacer; this.indexNameExpressionResolver = indexNameExpressionResolver; this.task = task; + this.apiTokenRepository = apiTokenRepository; this.permissionsForApiToken = extractApiTokenPermissionsForUser(); } diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java index 36666972ec..7fb61355e7 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java @@ -79,6 +79,7 @@ import org.opensearch.cluster.metadata.IndexNameExpressionResolver; import org.opensearch.cluster.metadata.Metadata; import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.inject.Inject; import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.core.common.Strings; @@ -86,6 +87,7 @@ import org.opensearch.core.xcontent.NamedXContentRegistry; import org.opensearch.index.reindex.ReindexAction; import org.opensearch.script.mustache.RenderSearchTemplateAction; +import org.opensearch.security.action.apitokens.ApiTokenRepository; import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.configuration.ClusterInfoHolder; import org.opensearch.security.configuration.ConfigurationRepository; @@ -153,7 +155,9 @@ public class PrivilegesEvaluator { private final NamedXContentRegistry namedXContentRegistry; private final Settings settings; private final AtomicReference actionPrivileges = new AtomicReference<>(); + private ApiTokenRepository apiTokenRepository; + @Inject public PrivilegesEvaluator( final ClusterService clusterService, Supplier clusterStateSupplier, @@ -166,7 +170,8 @@ public PrivilegesEvaluator( final PrivilegesInterceptor privilegesInterceptor, final ClusterInfoHolder clusterInfoHolder, final IndexResolverReplacer irr, - NamedXContentRegistry namedXContentRegistry + NamedXContentRegistry namedXContentRegistry, + ApiTokenRepository apiTokenRepository ) { super(); @@ -298,7 +303,7 @@ public PrivilegesEvaluationContext createContext( TransportAddress caller = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS); ImmutableSet mappedRoles = ImmutableSet.copyOf((injectedRoles == null) ? mapRoles(user, caller) : injectedRoles); - return new PrivilegesEvaluationContext(user, mappedRoles, action0, request, task, irr, resolver, clusterStateSupplier); + return new PrivilegesEvaluationContext(user, mappedRoles, action0, request, task, irr, resolver, clusterStateSupplier, apiTokenRepository); } public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) { diff --git a/src/test/java/org/opensearch/security/action/apitokens/ApiTokenActionTest.java b/src/test/java/org/opensearch/security/action/apitokens/ApiTokenActionTest.java index 32fadda93e..ef40d3c1bc 100644 --- a/src/test/java/org/opensearch/security/action/apitokens/ApiTokenActionTest.java +++ b/src/test/java/org/opensearch/security/action/apitokens/ApiTokenActionTest.java @@ -29,7 +29,7 @@ public class ApiTokenActionTest { - private final ApiTokenAction apiTokenAction = new ApiTokenAction(mock(ClusterService.class), null, null); + private final ApiTokenAction apiTokenAction = new ApiTokenAction(mock(ApiTokenRepository.class)); @Test public void testCreateIndexPermission() { diff --git a/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java b/src/test/java/org/opensearch/security/authtoken/jwt/OBOJwtVendorTest.java similarity index 87% rename from src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java rename to src/test/java/org/opensearch/security/authtoken/jwt/OBOJwtVendorTest.java index 162f5b35d2..a723c356a2 100644 --- a/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java +++ b/src/test/java/org/opensearch/security/authtoken/jwt/OBOJwtVendorTest.java @@ -54,7 +54,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -public class JwtVendorTest { +public class OBOJwtVendorTest { private Appender mockAppender; private ArgumentCaptor logEventCaptor; @@ -66,7 +66,7 @@ public class JwtVendorTest { public void testCreateJwkFromSettings() { final Settings settings = Settings.builder().put("signing_key", signingKeyB64Encoded).build(); - final Tuple jwk = JwtVendor.createJwkFromSettings(settings); + final Tuple jwk = OBOJwtVendor.createJwkFromSettings(settings); assertThat(jwk.v1().getAlgorithm().getName(), is("HS512")); assertThat(jwk.v1().getKeyUse().toString(), is("sig")); Assert.assertTrue(jwk.v1().toOctetSequenceKey().getKeyValue().decodeToString().startsWith(signingKey)); @@ -75,14 +75,14 @@ public void testCreateJwkFromSettings() { @Test public void testCreateJwkFromSettingsWithWeakKey() { Settings settings = Settings.builder().put("signing_key", "abcd1234").build(); - Throwable exception = Assert.assertThrows(OpenSearchException.class, () -> JwtVendor.createJwkFromSettings(settings)); + Throwable exception = Assert.assertThrows(OpenSearchException.class, () -> OBOJwtVendor.createJwkFromSettings(settings)); assertThat(exception.getMessage(), containsString("The secret length must be at least 256 bits")); } @Test public void testCreateJwkFromSettingsWithoutSigningKey() { Settings settings = Settings.builder().put("jwt", "").build(); - Throwable exception = Assert.assertThrows(RuntimeException.class, () -> JwtVendor.createJwkFromSettings(settings)); + Throwable exception = Assert.assertThrows(RuntimeException.class, () -> OBOJwtVendor.createJwkFromSettings(settings)); assertThat( exception.getMessage(), equalTo("Settings for signing key is missing. Please specify at least the option signing_key with a shared secret.") @@ -103,8 +103,8 @@ public void testCreateJwtWithRoles() throws Exception { String claimsEncryptionKey = "1234567890123456"; Settings settings = Settings.builder().put("signing_key", signingKeyB64Encoded).put("encryption_key", claimsEncryptionKey).build(); - JwtVendor jwtVendor = new JwtVendor(settings, Optional.of(currentTime)); - final ExpiringBearerAuthToken authToken = jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles, backendRoles, false); + OBOJwtVendor OBOJwtVendor = new OBOJwtVendor(settings, Optional.of(currentTime)); + final ExpiringBearerAuthToken authToken = OBOJwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles, backendRoles, false); SignedJWT signedJWT = SignedJWT.parse(authToken.getCompleteToken()); @@ -140,8 +140,8 @@ public void testCreateJwtWithBackendRolesIncluded() throws Exception { .put(ConfigConstants.EXTENSIONS_BWC_PLUGIN_MODE, true) // CS-ENFORCE-SINGLE .build(); - final JwtVendor jwtVendor = new JwtVendor(settings, Optional.of(currentTime)); - final ExpiringBearerAuthToken authToken = jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles, backendRoles, true); + final OBOJwtVendor OBOJwtVendor = new OBOJwtVendor(settings, Optional.of(currentTime)); + final ExpiringBearerAuthToken authToken = OBOJwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles, backendRoles, true); SignedJWT signedJWT = SignedJWT.parse(authToken.getCompleteToken()); @@ -166,11 +166,11 @@ public void testCreateJwtWithNegativeExpiry() { Integer expirySeconds = -300; String claimsEncryptionKey = RandomStringUtils.randomAlphanumeric(16); Settings settings = Settings.builder().put("signing_key", signingKeyB64Encoded).put("encryption_key", claimsEncryptionKey).build(); - JwtVendor jwtVendor = new JwtVendor(settings, Optional.empty()); + OBOJwtVendor OBOJwtVendor = new OBOJwtVendor(settings, Optional.empty()); final Throwable exception = assertThrows(RuntimeException.class, () -> { try { - jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles, List.of(), true); + OBOJwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles, List.of(), true); } catch (final Exception e) { throw new RuntimeException(e); } @@ -189,9 +189,9 @@ public void testCreateJwtWithExceededExpiry() throws Exception { LongSupplier currentTime = () -> (long) 100; String claimsEncryptionKey = RandomStringUtils.randomAlphanumeric(16); Settings settings = Settings.builder().put("signing_key", signingKeyB64Encoded).put("encryption_key", claimsEncryptionKey).build(); - JwtVendor jwtVendor = new JwtVendor(settings, Optional.of(currentTime)); + OBOJwtVendor OBOJwtVendor = new OBOJwtVendor(settings, Optional.of(currentTime)); - final ExpiringBearerAuthToken authToken = jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles, backendRoles, true); + final ExpiringBearerAuthToken authToken = OBOJwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles, backendRoles, true); // Expiry is a hint, the max value is controlled by the JwtVendor and reduced as is seen fit. assertThat(authToken.getExpiresInSeconds(), not(equalTo(expirySeconds))); assertThat(authToken.getExpiresInSeconds(), equalTo(600L)); @@ -209,7 +209,7 @@ public void testCreateJwtWithBadEncryptionKey() { final Throwable exception = assertThrows(RuntimeException.class, () -> { try { - new JwtVendor(settings, Optional.empty()).createJwt(issuer, subject, audience, expirySeconds, roles, List.of(), true); + new OBOJwtVendor(settings, Optional.empty()).createJwt(issuer, subject, audience, expirySeconds, roles, List.of(), true); } catch (final Exception e) { throw new RuntimeException(e); } @@ -226,11 +226,11 @@ public void testCreateJwtWithBadRoles() { Integer expirySeconds = 300; String claimsEncryptionKey = RandomStringUtils.randomAlphanumeric(16); Settings settings = Settings.builder().put("signing_key", signingKeyB64Encoded).put("encryption_key", claimsEncryptionKey).build(); - JwtVendor jwtVendor = new JwtVendor(settings, Optional.empty()); + OBOJwtVendor OBOJwtVendor = new OBOJwtVendor(settings, Optional.empty()); final Throwable exception = assertThrows(RuntimeException.class, () -> { try { - jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles, List.of(), true); + OBOJwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles, List.of(), true); } catch (final Exception e) { throw new RuntimeException(e); } @@ -244,7 +244,7 @@ public void testCreateJwtLogsCorrectly() throws Exception { logEventCaptor = ArgumentCaptor.forClass(LogEvent.class); when(mockAppender.getName()).thenReturn("MockAppender"); when(mockAppender.isStarted()).thenReturn(true); - final Logger logger = (Logger) LogManager.getLogger(JwtVendor.class); + final Logger logger = (Logger) LogManager.getLogger(OBOJwtVendor.class); logger.addAppender(mockAppender); logger.setLevel(Level.DEBUG); @@ -260,9 +260,9 @@ public void testCreateJwtLogsCorrectly() throws Exception { final List backendRoles = List.of("Sales", "Support"); final int expirySeconds = 300; - final JwtVendor jwtVendor = new JwtVendor(settings, Optional.of(currentTime)); + final OBOJwtVendor OBOJwtVendor = new OBOJwtVendor(settings, Optional.of(currentTime)); - jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles, backendRoles, false); + OBOJwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles, backendRoles, false); verify(mockAppender, times(1)).append(logEventCaptor.capture()); @@ -290,8 +290,8 @@ public void testCreateJwtForApiTokenSuccess() throws Exception { LongSupplier currentTime = () -> (long) 100; String claimsEncryptionKey = "1234567890123456"; Settings settings = Settings.builder().put("signing_key", signingKeyB64Encoded).put("encryption_key", claimsEncryptionKey).build(); - final JwtVendor jwtVendor = new JwtVendor(settings, Optional.of(currentTime)); - final ExpiringBearerAuthToken authToken = jwtVendor.createJwt(issuer, subject, audience, Long.MAX_VALUE); + final OBOJwtVendor OBOJwtVendor = new OBOJwtVendor(settings, Optional.of(currentTime)); + final ExpiringBearerAuthToken authToken = OBOJwtVendor.createJwt(issuer, subject, audience, Long.MAX_VALUE); SignedJWT signedJWT = SignedJWT.parse(authToken.getCompleteToken()); @@ -312,8 +312,8 @@ public void testEncryptJwtCorrectly() { "k3JQNRXR57Y4V4W1LNkpEP7FTJZos7fySJDJDGuBQXe7pi9aiEIGJ7JqjezssGRZ1AZGD/QTPQ0jjaV+rEICxBO9oyfTYWIoDdnAg5LijqPAzaULp48hi+/dqXXAAhi1zIlCSjqTDoZMTyjFxq4aRlPLjjQFuVxR3gIDMNnAUnvmFu5xh5AiVeKa1dwGy5X34Ou2i9pnQzmEDJDnf6mh7w2ODkDThJGh8JUlsUlfZEq6NwVN1XNyOr2IhPd3IZYUMgN3vWHyfjs6uwQNyHKHHcxIj4P8bJXLIGxJy3+LV5Y="; Settings settings = Settings.builder().put("signing_key", signingKeyB64Encoded).put("encryption_key", claimsEncryptionKey).build(); LongSupplier currentTime = () -> (long) 100; - JwtVendor jwtVendor = new JwtVendor(settings, Optional.of(currentTime)); - assertThat(jwtVendor.encryptString(token), equalTo(expectedEncryptedToken)); + OBOJwtVendor OBOJwtVendor = new OBOJwtVendor(settings, Optional.of(currentTime)); + assertThat(OBOJwtVendor.encryptString(token), equalTo(expectedEncryptedToken)); } @Test @@ -321,7 +321,7 @@ public void testKeyTooShortThrowsException() { String claimsEncryptionKey = RandomStringUtils.randomAlphanumeric(16); String tooShortKey = BaseEncoding.base64().encode("short_key".getBytes()); Settings settings = Settings.builder().put("signing_key", tooShortKey).put("encryption_key", claimsEncryptionKey).build(); - final Throwable exception = assertThrows(OpenSearchException.class, () -> { new JwtVendor(settings, Optional.empty()); }); + final Throwable exception = assertThrows(OpenSearchException.class, () -> { new OBOJwtVendor(settings, Optional.empty()); }); assertThat(exception.getMessage(), containsString("The secret length must be at least 256 bits")); } diff --git a/src/test/java/org/opensearch/security/identity/SecurityTokenManagerTest.java b/src/test/java/org/opensearch/security/identity/SecurityTokenManagerTest.java index 7a11e3dc6e..4c3efc4aab 100644 --- a/src/test/java/org/opensearch/security/identity/SecurityTokenManagerTest.java +++ b/src/test/java/org/opensearch/security/identity/SecurityTokenManagerTest.java @@ -29,7 +29,7 @@ import org.opensearch.identity.tokens.AuthToken; import org.opensearch.identity.tokens.OnBehalfOfClaims; import org.opensearch.security.authtoken.jwt.ExpiringBearerAuthToken; -import org.opensearch.security.authtoken.jwt.JwtVendor; +import org.opensearch.security.authtoken.jwt.OBOJwtVendor; import org.opensearch.security.securityconf.ConfigModel; import org.opensearch.security.securityconf.DynamicConfigModel; import org.opensearch.security.support.ConfigConstants; @@ -61,7 +61,7 @@ public class SecurityTokenManagerTest { private SecurityTokenManager tokenManager; @Mock - private JwtVendor jwtVendor; + private OBOJwtVendor OBOJwtVendor; @Mock private ClusterService cs; @Mock @@ -122,7 +122,7 @@ private DynamicConfigModel createMockJwtVendorInTokenManager() { final DynamicConfigModel dcm = mock(DynamicConfigModel.class); when(dcm.getDynamicOnBehalfOfSettings()).thenReturn(settings); when(dcm.getDynamicApiTokenSettings()).thenReturn(settings); - doAnswer((invocation) -> jwtVendor).when(tokenManager).createJwtVendor(settings); + doAnswer((invocation) -> OBOJwtVendor).when(tokenManager).createJwtVendor(settings); tokenManager.onDynamicConfigModelChanged(dcm); return dcm; } @@ -213,7 +213,7 @@ public void issueOnBehalfOfToken_jwtGenerationFailure() throws Exception { createMockJwtVendorInTokenManager(); - when(jwtVendor.createJwt(any(), anyString(), anyString(), anyLong(), any(), any(), anyBoolean())).thenThrow( + when(OBOJwtVendor.createJwt(any(), anyString(), anyString(), anyLong(), any(), any(), anyBoolean())).thenThrow( new RuntimeException("foobar") ); final OpenSearchSecurityException exception = assertThrows( @@ -240,7 +240,7 @@ public void issueOnBehalfOfToken_success() throws Exception { createMockJwtVendorInTokenManager(); final ExpiringBearerAuthToken authToken = mock(ExpiringBearerAuthToken.class); - when(jwtVendor.createJwt(any(), anyString(), anyString(), anyLong(), any(), any(), anyBoolean())).thenReturn(authToken); + when(OBOJwtVendor.createJwt(any(), anyString(), anyString(), anyLong(), any(), any(), anyBoolean())).thenReturn(authToken); final AuthToken returnedToken = tokenManager.issueOnBehalfOfToken(null, new OnBehalfOfClaims("elmo", 450L)); assertThat(returnedToken, equalTo(authToken)); @@ -261,7 +261,7 @@ public void issueApiToken_success() throws Exception { createMockJwtVendorInTokenManager(); final ExpiringBearerAuthToken authToken = mock(ExpiringBearerAuthToken.class); - when(jwtVendor.createJwt(anyString(), anyString(), anyString(), anyLong())).thenReturn(authToken); + when(OBOJwtVendor.createJwt(anyString(), anyString(), anyString(), anyLong())).thenReturn(authToken); final AuthToken returnedToken = tokenManager.issueApiToken("elmo", Long.MAX_VALUE); assertThat(returnedToken, equalTo(authToken)); @@ -282,7 +282,7 @@ public void encryptCallsJwtEncrypt() throws Exception { createMockJwtVendorInTokenManager(); final ExpiringBearerAuthToken authToken = mock(ExpiringBearerAuthToken.class); - when(jwtVendor.createJwt(anyString(), anyString(), anyString(), anyLong())).thenReturn(authToken); + when(OBOJwtVendor.createJwt(anyString(), anyString(), anyString(), anyLong())).thenReturn(authToken); final AuthToken returnedToken = tokenManager.issueApiToken("elmo", Long.MAX_VALUE); assertThat(returnedToken, equalTo(authToken));