From 9f695fa65336bd07638638567cb872367854d978 Mon Sep 17 00:00:00 2001 From: Derek Ho Date: Tue, 24 Dec 2024 12:20:02 -0500 Subject: [PATCH 01/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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)); From 0927d99b080dd132a23b2d46ba50c4bbfe882918 Mon Sep 17 00:00:00 2001 From: Derek Ho Date: Wed, 15 Jan 2025 16:14:46 -0500 Subject: [PATCH 21/30] Fix Signed-off-by: Derek Ho --- .../security/privileges/ActionPrivilegesTest.java | 4 +++- .../security/privileges/IndexPatternTest.java | 5 ++++- .../privileges/RestEndpointPermissionTests.java | 4 +++- .../security/privileges/dlsfls/FieldMaskingTest.java | 5 ++++- .../opensearch/security/OpenSearchSecurityPlugin.java | 2 +- .../security/action/apitokens/ApiTokenRepository.java | 1 + .../security/http/ApiTokenAuthenticator.java | 3 ++- .../privileges/PrivilegesEvaluationContext.java | 1 - .../security/privileges/PrivilegesEvaluator.java | 3 ++- .../security/securityconf/DynamicConfigFactory.java | 8 ++++++-- .../security/securityconf/DynamicConfigModelV7.java | 10 +++++++--- .../security/securityconf/impl/v7/ConfigV7.java | 4 ++-- .../privileges/SystemIndexAccessEvaluatorTest.java | 4 +++- 13 files changed, 38 insertions(+), 16 deletions(-) diff --git a/src/integrationTest/java/org/opensearch/security/privileges/ActionPrivilegesTest.java b/src/integrationTest/java/org/opensearch/security/privileges/ActionPrivilegesTest.java index d4f8f202cc..2a60d236ba 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/ActionPrivilegesTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/ActionPrivilegesTest.java @@ -53,6 +53,7 @@ import org.mockito.Mockito; import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.Mockito.mock; import static org.opensearch.security.privileges.PrivilegeEvaluatorResponseMatcher.isAllowed; import static org.opensearch.security.privileges.PrivilegeEvaluatorResponseMatcher.isForbidden; import static org.opensearch.security.privileges.PrivilegeEvaluatorResponseMatcher.isPartiallyOk; @@ -1132,7 +1133,8 @@ static PrivilegesEvaluationContext ctxWithUserName(String userName, String... ro null, null, new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), - null + null, + mock(ApiTokenRepository.class) ); } diff --git a/src/integrationTest/java/org/opensearch/security/privileges/IndexPatternTest.java b/src/integrationTest/java/org/opensearch/security/privileges/IndexPatternTest.java index e098a605e5..c63a63af93 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/IndexPatternTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/IndexPatternTest.java @@ -22,10 +22,12 @@ import org.opensearch.cluster.metadata.Metadata; import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.security.action.apitokens.ApiTokenRepository; import org.opensearch.security.resolver.IndexResolverReplacer; import org.opensearch.security.support.WildcardMatcher; import org.opensearch.security.user.User; +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; @@ -246,7 +248,8 @@ private static PrivilegesEvaluationContext ctx() { null, indexResolverReplacer, indexNameExpressionResolver, - () -> CLUSTER_STATE + () -> CLUSTER_STATE, + mock(ApiTokenRepository.class) ); } } diff --git a/src/integrationTest/java/org/opensearch/security/privileges/RestEndpointPermissionTests.java b/src/integrationTest/java/org/opensearch/security/privileges/RestEndpointPermissionTests.java index 1e61aa0206..6755347f59 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/RestEndpointPermissionTests.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/RestEndpointPermissionTests.java @@ -44,6 +44,7 @@ import org.opensearch.common.settings.Settings; import org.opensearch.security.DefaultObjectMapper; +import org.opensearch.security.action.apitokens.ApiTokenRepository; import org.opensearch.security.dlic.rest.api.Endpoint; import org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.PermissionBuilder; import org.opensearch.security.securityconf.FlattenedActionGroups; @@ -52,6 +53,7 @@ import org.opensearch.security.securityconf.impl.v7.RoleV7; import org.opensearch.security.user.User; +import static org.mockito.Mockito.mock; import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.CERTS_INFO_ACTION; import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.ENDPOINTS_WITH_PERMISSIONS; import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.RELOAD_CERTS_ACTION; @@ -251,7 +253,7 @@ static SecurityDynamicConfiguration createRolesConfig() throws IOExcepti } static PrivilegesEvaluationContext ctx(String... roles) { - return new PrivilegesEvaluationContext(new User("test_user"), ImmutableSet.copyOf(roles), null, null, null, null, null, null); + return new PrivilegesEvaluationContext(new User("test_user"), ImmutableSet.copyOf(roles), null, null, null, null, null, null, mock(ApiTokenRepository.class)); } } diff --git a/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/FieldMaskingTest.java b/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/FieldMaskingTest.java index 7f4c5bacf2..9ee43263e1 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/FieldMaskingTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/FieldMaskingTest.java @@ -22,6 +22,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; @@ -30,6 +31,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; @@ -123,7 +125,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 45b61423ab..1c4b2602db 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -1168,7 +1168,7 @@ public Collection createComponents( configPath, compatConfig ); - dcf = new DynamicConfigFactory(cr, settings, configPath, localClient, threadPool, cih, passwordHasher); + dcf = new DynamicConfigFactory(cr, settings, configPath, localClient, threadPool, cih, passwordHasher, ar); dcf.registerDCFListener(backendRegistry); dcf.registerDCFListener(compatConfig); dcf.registerDCFListener(irr); 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 3eabe60119..acbd48f2ad 100644 --- a/src/main/java/org/opensearch/security/action/apitokens/ApiTokenRepository.java +++ b/src/main/java/org/opensearch/security/action/apitokens/ApiTokenRepository.java @@ -43,6 +43,7 @@ public class ApiTokenRepository implements ClusterStateListener { private Client client; void reloadApiTokensFromIndex() { + log.info("Reloading api tokens from index. Currnet entries: " + jtis.entrySet()); try { jtis.clear(); client.prepareSearch(ConfigConstants.OPENSEARCH_API_TOKENS_INDEX) diff --git a/src/main/java/org/opensearch/security/http/ApiTokenAuthenticator.java b/src/main/java/org/opensearch/security/http/ApiTokenAuthenticator.java index 154c5b7798..9b6a5e0e34 100644 --- a/src/main/java/org/opensearch/security/http/ApiTokenAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/ApiTokenAuthenticator.java @@ -63,8 +63,8 @@ public class ApiTokenAuthenticator implements HTTPAuthenticator { private final ApiTokenRepository apiTokenRepository; @SuppressWarnings("removal") - @Inject public ApiTokenAuthenticator(Settings settings, String clusterName, ApiTokenRepository apiTokenRepository) { + log.info("We instantiating it"); String apiTokenEnabledSetting = settings.get("enabled", "true"); apiTokenEnabled = Boolean.parseBoolean(apiTokenEnabledSetting); @@ -128,6 +128,7 @@ private AuthCredentials extractCredentials0(final SecurityRequest request, final log.error("Api token authentication is disabled"); return null; } + log.info("API TOKEN AUTHENTICATOR IS BEING CALLED"); String jwtToken = extractJwtFromHeader(request); if (jwtToken == null) { diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java index 40c668a164..a1c7a041de 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java @@ -61,7 +61,6 @@ public class PrivilegesEvaluationContext { */ private final Map renderedPatternTemplateCache = new HashMap<>(); - @Inject public PrivilegesEvaluationContext( User user, ImmutableSet mappedRoles, diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java index 7fb61355e7..185eb68b9d 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java @@ -157,7 +157,6 @@ public class PrivilegesEvaluator { private final AtomicReference actionPrivileges = new AtomicReference<>(); private ApiTokenRepository apiTokenRepository; - @Inject public PrivilegesEvaluator( final ClusterService clusterService, Supplier clusterStateSupplier, @@ -221,6 +220,8 @@ public PrivilegesEvaluator( }); } + this.apiTokenRepository = apiTokenRepository; + } void updateConfiguration( diff --git a/src/main/java/org/opensearch/security/securityconf/DynamicConfigFactory.java b/src/main/java/org/opensearch/security/securityconf/DynamicConfigFactory.java index 10402f7b56..307b40d328 100644 --- a/src/main/java/org/opensearch/security/securityconf/DynamicConfigFactory.java +++ b/src/main/java/org/opensearch/security/securityconf/DynamicConfigFactory.java @@ -43,6 +43,7 @@ import org.opensearch.client.Client; import org.opensearch.common.settings.Settings; import org.opensearch.security.DefaultObjectMapper; +import org.opensearch.security.action.apitokens.ApiTokenRepository; import org.opensearch.security.auditlog.config.AuditConfig; import org.opensearch.security.auth.internal.InternalAuthenticationBackend; import org.opensearch.security.configuration.ClusterInfoHolder; @@ -127,6 +128,7 @@ public final static SecurityDynamicConfiguration addStatics(SecurityDynam private final Path configPath; private final InternalAuthenticationBackend iab; private final ClusterInfoHolder cih; + private final ApiTokenRepository ar; SecurityDynamicConfiguration config; @@ -137,7 +139,8 @@ public DynamicConfigFactory( Client client, ThreadPool threadPool, ClusterInfoHolder cih, - PasswordHasher passwordHasher + PasswordHasher passwordHasher, + ApiTokenRepository ar ) { super(); this.cr = cr; @@ -145,6 +148,7 @@ public DynamicConfigFactory( this.configPath = configPath; this.cih = cih; this.iab = new InternalAuthenticationBackend(passwordHasher); + this.ar = ar; if (opensearchSettings.getAsBoolean(ConfigConstants.SECURITY_UNSUPPORTED_LOAD_STATIC_RESOURCES, true)) { try { @@ -269,7 +273,7 @@ public void onChange(ConfigurationMap typeToConfig) { ); // rebuild v7 Models - dcm = new DynamicConfigModelV7(getConfigV7(config), opensearchSettings, configPath, iab, this.cih); + dcm = new DynamicConfigModelV7(getConfigV7(config), opensearchSettings, configPath, iab, this.cih, ar); ium = new InternalUsersModelV7(internalusers, roles, rolesmapping); cm = new ConfigModelV7(roles, rolesmapping, actionGroups, tenants, dcm, opensearchSettings); diff --git a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java index facfa4f075..dd818b72b3 100644 --- a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java +++ b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java @@ -88,13 +88,15 @@ public class DynamicConfigModelV7 extends DynamicConfigModel { private List> ipClientBlockRegistries; private Multimap> authBackendClientBlockRegistries; private final ClusterInfoHolder cih; + private final ApiTokenRepository ar; public DynamicConfigModelV7( ConfigV7 config, Settings opensearchSettings, Path configPath, InternalAuthenticationBackend iab, - ClusterInfoHolder cih + ClusterInfoHolder cih, + ApiTokenRepository ar ) { super(); this.config = config; @@ -102,6 +104,7 @@ public DynamicConfigModelV7( this.configPath = configPath; this.iab = iab; this.cih = cih; + this.ar = ar; buildAAA(); } @@ -390,10 +393,11 @@ private void buildAAA() { * order: -2 - prioritize the Api token authentication when it gets enabled */ Settings apiTokenSettings = getDynamicApiTokenSettings(); - if (!isKeyNull(apiTokenSettings, "signing_key") && !isKeyNull(apiTokenSettings, "encryption_key")) { + log.info("APITOKENSETTINGS" + apiTokenSettings.toString()); + if (!isKeyNull(apiTokenSettings, "signing_key")) { final AuthDomain _ad = new AuthDomain( new NoOpAuthenticationBackend(Settings.EMPTY, null), - new ApiTokenAuthenticator(getDynamicApiTokenSettings(), this.cih.getClusterName(), apiTokenRepository), + new ApiTokenAuthenticator(getDynamicApiTokenSettings(), this.cih.getClusterName(), ar), false, -2 ); 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 0818ec5530..d960a9e9bd 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 @@ -517,8 +517,8 @@ public Boolean getEnabled() { return enabled; } - public void setEnabled(Boolean oboEnabled) { - this.enabled = oboEnabled; + public void setEnabled(Boolean apiTokensEnabled) { + this.enabled = apiTokensEnabled; } public String getSigningKey() { diff --git a/src/test/java/org/opensearch/security/privileges/SystemIndexAccessEvaluatorTest.java b/src/test/java/org/opensearch/security/privileges/SystemIndexAccessEvaluatorTest.java index 878033fd5c..99944bf7c5 100644 --- a/src/test/java/org/opensearch/security/privileges/SystemIndexAccessEvaluatorTest.java +++ b/src/test/java/org/opensearch/security/privileges/SystemIndexAccessEvaluatorTest.java @@ -32,6 +32,7 @@ import org.opensearch.cluster.service.ClusterService; import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.security.action.apitokens.ApiTokenRepository; import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.resolver.IndexResolverReplacer; import org.opensearch.security.resolver.IndexResolverReplacer.Resolved; @@ -163,7 +164,8 @@ PrivilegesEvaluationContext ctx(String action) { null, null, indexNameExpressionResolver, - null + null, + mock(ApiTokenRepository.class) ); } From fb7901487b626c6eb79640ec986c1a77ca2f633c Mon Sep 17 00:00:00 2001 From: Derek Ho Date: Thu, 16 Jan 2025 09:25:27 -0500 Subject: [PATCH 22/30] Fix tests Signed-off-by: Derek Ho --- .../privileges/ActionPrivilegesTest.java | 4 +- .../security/privileges/IndexPatternTest.java | 4 +- .../RestEndpointPermissionTests.java | 14 ++++- .../dlsfls/DlsFlsLegacyHeadersTest.java | 6 +- .../dlsfls/DocumentPrivilegesTest.java | 22 +++++-- .../privileges/dlsfls/FieldMaskingTest.java | 4 +- .../dlsfls/FieldPrivilegesTest.java | 4 +- .../security/OpenSearchSecurityPlugin.java | 2 +- .../action/apitokens/ApiTokenAction.java | 7 +-- .../authtoken/jwt/ApiTokenJwtVendor.java | 14 +---- .../security/authtoken/jwt/JwtVendor.java | 44 +++++++------- .../security/authtoken/jwt/OBOJwtVendor.java | 8 --- .../security/http/ApiTokenAuthenticator.java | 1 - .../identity/SecurityTokenManager.java | 1 - .../PrivilegesEvaluationContext.java | 1 - .../privileges/PrivilegesEvaluator.java | 13 +++- .../action/apitokens/ApiTokenActionTest.java | 2 - ...OJwtVendorTest.java => JwtVendorTest.java} | 59 ++++++++++--------- .../identity/SecurityTokenManagerTest.java | 17 +++--- .../RestLayerPrivilegesEvaluatorTest.java | 4 +- .../SystemIndexAccessEvaluatorTest.java | 2 +- 21 files changed, 122 insertions(+), 111 deletions(-) rename src/test/java/org/opensearch/security/authtoken/jwt/{OBOJwtVendorTest.java => JwtVendorTest.java} (86%) diff --git a/src/integrationTest/java/org/opensearch/security/privileges/ActionPrivilegesTest.java b/src/integrationTest/java/org/opensearch/security/privileges/ActionPrivilegesTest.java index 2a60d236ba..1d517f6590 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/ActionPrivilegesTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/ActionPrivilegesTest.java @@ -53,7 +53,6 @@ import org.mockito.Mockito; import static org.hamcrest.MatcherAssert.assertThat; -import static org.mockito.Mockito.mock; import static org.opensearch.security.privileges.PrivilegeEvaluatorResponseMatcher.isAllowed; import static org.opensearch.security.privileges.PrivilegeEvaluatorResponseMatcher.isForbidden; import static org.opensearch.security.privileges.PrivilegeEvaluatorResponseMatcher.isPartiallyOk; @@ -64,6 +63,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; /** * Unit tests for ActionPrivileges. As the ActionPrivileges provides quite a few different code paths for checking @@ -1134,7 +1134,7 @@ static PrivilegesEvaluationContext ctxWithUserName(String userName, String... ro null, new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), null, - mock(ApiTokenRepository.class) + mock(ApiTokenRepository.class) ); } diff --git a/src/integrationTest/java/org/opensearch/security/privileges/IndexPatternTest.java b/src/integrationTest/java/org/opensearch/security/privileges/IndexPatternTest.java index c63a63af93..ce139934c8 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/IndexPatternTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/IndexPatternTest.java @@ -27,12 +27,12 @@ import org.opensearch.security.support.WildcardMatcher; import org.opensearch.security.user.User; -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; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; public class IndexPatternTest { final static int CURRENT_YEAR = ZonedDateTime.now().get(ChronoField.YEAR); @@ -249,7 +249,7 @@ private static PrivilegesEvaluationContext ctx() { indexResolverReplacer, indexNameExpressionResolver, () -> CLUSTER_STATE, - mock(ApiTokenRepository.class) + mock(ApiTokenRepository.class) ); } } diff --git a/src/integrationTest/java/org/opensearch/security/privileges/RestEndpointPermissionTests.java b/src/integrationTest/java/org/opensearch/security/privileges/RestEndpointPermissionTests.java index 6755347f59..f5b1529f46 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/RestEndpointPermissionTests.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/RestEndpointPermissionTests.java @@ -53,11 +53,11 @@ import org.opensearch.security.securityconf.impl.v7.RoleV7; import org.opensearch.security.user.User; -import static org.mockito.Mockito.mock; import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.CERTS_INFO_ACTION; import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.ENDPOINTS_WITH_PERMISSIONS; import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.RELOAD_CERTS_ACTION; import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.SECURITY_CONFIG_UPDATE; +import static org.mockito.Mockito.mock; /** * Moved from https://github.com/opensearch-project/security/blob/54361468f5c4b3a57f3ecffaf1bbe8dccee562be/src/test/java/org/opensearch/security/securityconf/SecurityRolesPermissionsTest.java @@ -253,7 +253,17 @@ static SecurityDynamicConfiguration createRolesConfig() throws IOExcepti } static PrivilegesEvaluationContext ctx(String... roles) { - return new PrivilegesEvaluationContext(new User("test_user"), ImmutableSet.copyOf(roles), null, null, null, null, null, null, mock(ApiTokenRepository.class)); + return new PrivilegesEvaluationContext( + new User("test_user"), + ImmutableSet.copyOf(roles), + null, + null, + null, + null, + null, + null, + mock(ApiTokenRepository.class) + ); } } 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 1224c1bc23..398b08dcb9 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/DlsFlsLegacyHeadersTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/DlsFlsLegacyHeadersTest.java @@ -48,7 +48,6 @@ 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; @@ -57,6 +56,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; public class DlsFlsLegacyHeadersTest { static NamedXContentRegistry xContentRegistry = new NamedXContentRegistry( @@ -348,7 +348,7 @@ public void prepare_ccs() throws Exception { null, new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), () -> clusterState, - mock(ApiTokenRepository.class) + mock(ApiTokenRepository.class) ); DlsFlsLegacyHeaders.prepare(threadContext, ctx, dlsFlsProcessedConfig(exampleRolesConfig(), metadata), metadata, false); @@ -368,7 +368,7 @@ static PrivilegesEvaluationContext ctx(Metadata metadata, String... roles) { null, new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), () -> clusterState, - mock(ApiTokenRepository.class) + 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 262498ff9e..3d7c67c922 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/DocumentPrivilegesTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/DocumentPrivilegesTest.java @@ -62,7 +62,6 @@ 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; @@ -70,6 +69,7 @@ import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; /** * Unit tests for the DocumentPrivileges class and the underlying AbstractRuleBasedPrivileges class. As these classes @@ -529,7 +529,7 @@ public IndicesAndAliases_getRestriction( null, null, () -> CLUSTER_STATE, - mock(ApiTokenRepository.class) + mock(ApiTokenRepository.class) ); this.statefulness = statefulness; this.dfmEmptyOverridesAll = dfmEmptyOverridesAll == DfmEmptyOverridesAll.DFM_EMPTY_OVERRIDES_ALL_TRUE; @@ -845,7 +845,7 @@ public IndicesRequest indices(String... strings) { RESOLVER_REPLACER, INDEX_NAME_EXPRESSION_RESOLVER, () -> CLUSTER_STATE, - mock(ApiTokenRepository.class) + mock(ApiTokenRepository.class) ); this.statefulness = statefulness; this.dfmEmptyOverridesAll = dfmEmptyOverridesAll == DfmEmptyOverridesAll.DFM_EMPTY_OVERRIDES_ALL_TRUE; @@ -1131,7 +1131,7 @@ public DataStreams_getRestriction( null, null, () -> CLUSTER_STATE, - mock(ApiTokenRepository.class) + mock(ApiTokenRepository.class) ); this.statefulness = statefulness; this.dfmEmptyOverridesAll = dfmEmptyOverridesAll == DfmEmptyOverridesAll.DFM_EMPTY_OVERRIDES_ALL_TRUE; @@ -1151,7 +1151,19 @@ 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, mock(ApiTokenRepository.class))); + .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/FieldMaskingTest.java b/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/FieldMaskingTest.java index 9ee43263e1..ad40329679 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/FieldMaskingTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/FieldMaskingTest.java @@ -31,12 +31,12 @@ 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; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; /** * Unit tests on the FieldMasking class - top-level functionality is tested in FieldMaskingTest.Basic. The inner classes FieldMasking.Field @@ -126,7 +126,7 @@ static PrivilegesEvaluationContext ctx(String... roles) { null, null, () -> CLUSTER_STATE, - mock(ApiTokenRepository.class) + mock(ApiTokenRepository.class) ); } } 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 394296b7d3..a1386de521 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/FieldPrivilegesTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/FieldPrivilegesTest.java @@ -30,11 +30,11 @@ 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; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; /** * Unit tests on the FieldMasking class - top-level functionality is tested in FieldMaskingTest.Basic. The inner classes FieldMasking.Field @@ -161,7 +161,7 @@ static PrivilegesEvaluationContext ctx(String... roles) { null, null, () -> CLUSTER_STATE, - mock(ApiTokenRepository.class) + mock(ApiTokenRepository.class) ); } } diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 1c4b2602db..8c945de408 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -1126,7 +1126,7 @@ public Collection createComponents( cih, irr, namedXContentRegistry.get(), - ar + ar ); dlsFlsBaseContext = new DlsFlsBaseContext(evaluator, threadPool.getThreadContext(), adminDns); 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 d690083ba1..26116714ac 100644 --- a/src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java +++ b/src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java @@ -23,9 +23,7 @@ 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.common.inject.Inject; import org.opensearch.core.action.ActionListener; import org.opensearch.core.rest.RestStatus; @@ -35,7 +33,6 @@ import org.opensearch.rest.RestChannel; import org.opensearch.rest.RestHandler; import org.opensearch.rest.RestRequest; -import org.opensearch.security.identity.SecurityTokenManager; import static org.opensearch.rest.RestRequest.Method.DELETE; import static org.opensearch.rest.RestRequest.Method.GET; @@ -55,8 +52,6 @@ public class ApiTokenAction extends BaseRestHandler { private ApiTokenRepository apiTokenRepository; public Logger log = LogManager.getLogger(this.getClass()); - - private static final List ROUTES = addRoutesPrefix( ImmutableList.of( new RestHandler.Route(POST, "/apitokens"), @@ -68,7 +63,7 @@ public class ApiTokenAction extends BaseRestHandler { @Inject public ApiTokenAction(ApiTokenRepository apiTokenRepository) { this.apiTokenRepository = apiTokenRepository; -// this.apiTokenRepository = new ApiTokenRepository(client, clusterService, securityTokenManager); + // this.apiTokenRepository = new ApiTokenRepository(client, clusterService, securityTokenManager); } @Override diff --git a/src/main/java/org/opensearch/security/authtoken/jwt/ApiTokenJwtVendor.java b/src/main/java/org/opensearch/security/authtoken/jwt/ApiTokenJwtVendor.java index da07d6e087..87de477fe4 100644 --- a/src/main/java/org/opensearch/security/authtoken/jwt/ApiTokenJwtVendor.java +++ b/src/main/java/org/opensearch/security/authtoken/jwt/ApiTokenJwtVendor.java @@ -14,16 +14,13 @@ 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; @@ -31,16 +28,11 @@ 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); @@ -61,7 +53,7 @@ public ApiTokenJwtVendor(final Settings settings, final Optional t @Override @SuppressWarnings("removal") public ExpiringBearerAuthToken createJwt(final String issuer, final String subject, final String audience, final long expiration) - throws JOSEException, ParseException { + throws JOSEException, ParseException { final long currentTimeMs = timeProvider.getAsLong(); final Date now = new Date(currentTimeMs); @@ -77,13 +69,13 @@ public ExpiringBearerAuthToken createJwt(final String issuer, final String subje final JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.parse(signingKey.getAlgorithm().getName())).build(); final SignedJWT signedJwt = AccessController.doPrivileged( - (PrivilegedAction) () -> new SignedJWT(header, claimsBuilder.build()) + (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() + "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 c66fdd2254..32da3eadb7 100644 --- a/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java +++ b/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java @@ -11,6 +11,14 @@ package org.opensearch.security.authtoken.jwt; +import java.text.ParseException; +import java.util.Base64; +import java.util.List; + +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.JWSSigner; @@ -19,32 +27,24 @@ import com.nimbusds.jose.jwk.JWK; import com.nimbusds.jose.jwk.KeyUse; import com.nimbusds.jose.jwk.OctetSequenceKey; -import org.opensearch.OpenSearchException; -import org.opensearch.common.collect.Tuple; -import org.opensearch.common.settings.Settings; - -import java.text.ParseException; -import java.util.Base64; -import java.util.List; import static org.opensearch.security.util.AuthTokenUtils.isKeyNull; 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 + 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 { + 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."); }; @@ -59,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 { diff --git a/src/main/java/org/opensearch/security/authtoken/jwt/OBOJwtVendor.java b/src/main/java/org/opensearch/security/authtoken/jwt/OBOJwtVendor.java index bde3d1980c..e5f7e65541 100644 --- a/src/main/java/org/opensearch/security/authtoken/jwt/OBOJwtVendor.java +++ b/src/main/java/org/opensearch/security/authtoken/jwt/OBOJwtVendor.java @@ -11,10 +11,7 @@ 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; @@ -23,7 +20,6 @@ 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; @@ -31,11 +27,7 @@ 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; diff --git a/src/main/java/org/opensearch/security/http/ApiTokenAuthenticator.java b/src/main/java/org/opensearch/security/http/ApiTokenAuthenticator.java index 9b6a5e0e34..88bd0c9ae6 100644 --- a/src/main/java/org/opensearch/security/http/ApiTokenAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/ApiTokenAuthenticator.java @@ -25,7 +25,6 @@ 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.ApiTokenRepository; diff --git a/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java b/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java index 3ff3c4e2d8..d8840df14b 100644 --- a/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java +++ b/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java @@ -100,7 +100,6 @@ ApiTokenJwtVendor createApiTokenJwtVendor(final Settings settings) { } } - public boolean issueOnBehalfOfTokenAllowed() { return oboJwtVendor != null && configModel != null; } diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java index a1c7a041de..3314f3f99d 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java @@ -22,7 +22,6 @@ import org.opensearch.cluster.ClusterState; import org.opensearch.cluster.metadata.IndexAbstraction; import org.opensearch.cluster.metadata.IndexNameExpressionResolver; -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; diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java index 185eb68b9d..1d9eb4f3c1 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java @@ -79,7 +79,6 @@ 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; @@ -304,7 +303,17 @@ 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, apiTokenRepository); + 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 ef40d3c1bc..e7193710e1 100644 --- a/src/test/java/org/opensearch/security/action/apitokens/ApiTokenActionTest.java +++ b/src/test/java/org/opensearch/security/action/apitokens/ApiTokenActionTest.java @@ -19,8 +19,6 @@ 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; diff --git a/src/test/java/org/opensearch/security/authtoken/jwt/OBOJwtVendorTest.java b/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java similarity index 86% rename from src/test/java/org/opensearch/security/authtoken/jwt/OBOJwtVendorTest.java rename to src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java index a723c356a2..0c1258f831 100644 --- a/src/test/java/org/opensearch/security/authtoken/jwt/OBOJwtVendorTest.java +++ b/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java @@ -30,9 +30,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.security.action.apitokens.ApiToken; import org.opensearch.security.support.ConfigConstants; import com.nimbusds.jose.JWSSigner; @@ -54,7 +51,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -public class OBOJwtVendorTest { +public class JwtVendorTest { private Appender mockAppender; private ArgumentCaptor logEventCaptor; @@ -104,7 +101,15 @@ public void testCreateJwtWithRoles() throws Exception { Settings settings = Settings.builder().put("signing_key", signingKeyB64Encoded).put("encryption_key", claimsEncryptionKey).build(); OBOJwtVendor OBOJwtVendor = new OBOJwtVendor(settings, Optional.of(currentTime)); - final ExpiringBearerAuthToken authToken = OBOJwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles, backendRoles, false); + final ExpiringBearerAuthToken authToken = OBOJwtVendor.createJwt( + issuer, + subject, + audience, + expirySeconds, + roles, + backendRoles, + false + ); SignedJWT signedJWT = SignedJWT.parse(authToken.getCompleteToken()); @@ -141,7 +146,15 @@ public void testCreateJwtWithBackendRolesIncluded() throws Exception { // CS-ENFORCE-SINGLE .build(); final OBOJwtVendor OBOJwtVendor = new OBOJwtVendor(settings, Optional.of(currentTime)); - final ExpiringBearerAuthToken authToken = OBOJwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles, backendRoles, true); + final ExpiringBearerAuthToken authToken = OBOJwtVendor.createJwt( + issuer, + subject, + audience, + expirySeconds, + roles, + backendRoles, + true + ); SignedJWT signedJWT = SignedJWT.parse(authToken.getCompleteToken()); @@ -191,7 +204,15 @@ public void testCreateJwtWithExceededExpiry() throws Exception { Settings settings = Settings.builder().put("signing_key", signingKeyB64Encoded).put("encryption_key", claimsEncryptionKey).build(); OBOJwtVendor OBOJwtVendor = new OBOJwtVendor(settings, Optional.of(currentTime)); - final ExpiringBearerAuthToken authToken = OBOJwtVendor.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)); @@ -279,19 +300,12 @@ public void testCreateJwtForApiTokenSuccess() throws Exception { final String issuer = "cluster_0"; final String subject = "test-token"; final String audience = "test-token"; - final List clusterPermissions = List.of("cluster:admin/*"); - 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() - + "]"; LongSupplier currentTime = () -> (long) 100; String claimsEncryptionKey = "1234567890123456"; Settings settings = Settings.builder().put("signing_key", signingKeyB64Encoded).put("encryption_key", claimsEncryptionKey).build(); - final OBOJwtVendor OBOJwtVendor = new OBOJwtVendor(settings, Optional.of(currentTime)); - final ExpiringBearerAuthToken authToken = OBOJwtVendor.createJwt(issuer, subject, audience, Long.MAX_VALUE); + final ApiTokenJwtVendor apiTokenJwtVendor = new ApiTokenJwtVendor(settings, Optional.of(currentTime)); + final ExpiringBearerAuthToken authToken = apiTokenJwtVendor.createJwt(issuer, subject, audience, Long.MAX_VALUE); SignedJWT signedJWT = SignedJWT.parse(authToken.getCompleteToken()); @@ -303,19 +317,6 @@ public void testCreateJwtForApiTokenSuccess() throws Exception { assertThat(((Date) signedJWT.getJWTClaimsSet().getClaims().get("exp")).getTime() / 1000, equalTo(Long.MAX_VALUE / 1000)); } - @Test - public void testEncryptJwtCorrectly() { - String claimsEncryptionKey = BaseEncoding.base64().encode("1234567890123456".getBytes(StandardCharsets.UTF_8)); - String token = - "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJkZXJlayI6ImlzIGF3ZXNvbWUifQ.aPp9mSaBRBUzMJ8V_MYWUs8UoGYnJDNVriu3B9MRJpPNZtOhnIfATE0Ghmms2bGRNw9rmyRn1VIDQRmxSOTu3w"; - String expectedEncryptedToken = - "k3JQNRXR57Y4V4W1LNkpEP7FTJZos7fySJDJDGuBQXe7pi9aiEIGJ7JqjezssGRZ1AZGD/QTPQ0jjaV+rEICxBO9oyfTYWIoDdnAg5LijqPAzaULp48hi+/dqXXAAhi1zIlCSjqTDoZMTyjFxq4aRlPLjjQFuVxR3gIDMNnAUnvmFu5xh5AiVeKa1dwGy5X34Ou2i9pnQzmEDJDnf6mh7w2ODkDThJGh8JUlsUlfZEq6NwVN1XNyOr2IhPd3IZYUMgN3vWHyfjs6uwQNyHKHHcxIj4P8bJXLIGxJy3+LV5Y="; - Settings settings = Settings.builder().put("signing_key", signingKeyB64Encoded).put("encryption_key", claimsEncryptionKey).build(); - LongSupplier currentTime = () -> (long) 100; - OBOJwtVendor OBOJwtVendor = new OBOJwtVendor(settings, Optional.of(currentTime)); - assertThat(OBOJwtVendor.encryptString(token), equalTo(expectedEncryptedToken)); - } - @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 4c3efc4aab..fb553f8a11 100644 --- a/src/test/java/org/opensearch/security/identity/SecurityTokenManagerTest.java +++ b/src/test/java/org/opensearch/security/identity/SecurityTokenManagerTest.java @@ -28,6 +28,7 @@ import org.opensearch.identity.Subject; import org.opensearch.identity.tokens.AuthToken; import org.opensearch.identity.tokens.OnBehalfOfClaims; +import org.opensearch.security.authtoken.jwt.ApiTokenJwtVendor; import org.opensearch.security.authtoken.jwt.ExpiringBearerAuthToken; import org.opensearch.security.authtoken.jwt.OBOJwtVendor; import org.opensearch.security.securityconf.ConfigModel; @@ -61,7 +62,9 @@ public class SecurityTokenManagerTest { private SecurityTokenManager tokenManager; @Mock - private OBOJwtVendor OBOJwtVendor; + private OBOJwtVendor oboJwtVendor; + @Mock + private ApiTokenJwtVendor apiTokenJwtVendor; @Mock private ClusterService cs; @Mock @@ -113,7 +116,7 @@ public void onDynamicConfigModelChanged_JwtVendorDisabled() { assertThat(tokenManager.issueOnBehalfOfTokenAllowed(), equalTo(false)); verify(dcm).getDynamicOnBehalfOfSettings(); - verify(tokenManager, never()).createJwtVendor(any()); + verify(tokenManager, never()).createOboJwtVendor(any()); } /** Creates the jwt vendor and returns a mock for validation if needed */ @@ -122,7 +125,7 @@ private DynamicConfigModel createMockJwtVendorInTokenManager() { final DynamicConfigModel dcm = mock(DynamicConfigModel.class); when(dcm.getDynamicOnBehalfOfSettings()).thenReturn(settings); when(dcm.getDynamicApiTokenSettings()).thenReturn(settings); - doAnswer((invocation) -> OBOJwtVendor).when(tokenManager).createJwtVendor(settings); + doAnswer((invocation) -> oboJwtVendor).when(tokenManager).createOboJwtVendor(settings); tokenManager.onDynamicConfigModelChanged(dcm); return dcm; } @@ -213,7 +216,7 @@ public void issueOnBehalfOfToken_jwtGenerationFailure() throws Exception { createMockJwtVendorInTokenManager(); - when(OBOJwtVendor.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 +243,7 @@ public void issueOnBehalfOfToken_success() throws Exception { createMockJwtVendorInTokenManager(); final ExpiringBearerAuthToken authToken = mock(ExpiringBearerAuthToken.class); - when(OBOJwtVendor.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 +264,7 @@ public void issueApiToken_success() throws Exception { createMockJwtVendorInTokenManager(); final ExpiringBearerAuthToken authToken = mock(ExpiringBearerAuthToken.class); - when(OBOJwtVendor.createJwt(anyString(), anyString(), anyString(), anyLong())).thenReturn(authToken); + when(apiTokenJwtVendor.createJwt(anyString(), anyString(), anyString(), anyLong())).thenReturn(authToken); final AuthToken returnedToken = tokenManager.issueApiToken("elmo", Long.MAX_VALUE); assertThat(returnedToken, equalTo(authToken)); @@ -282,7 +285,7 @@ public void encryptCallsJwtEncrypt() throws Exception { createMockJwtVendorInTokenManager(); final ExpiringBearerAuthToken authToken = mock(ExpiringBearerAuthToken.class); - when(OBOJwtVendor.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)); diff --git a/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java b/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java index da35226d62..9fdba2b407 100644 --- a/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java +++ b/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java @@ -31,6 +31,7 @@ import org.opensearch.cluster.service.ClusterService; import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.security.action.apitokens.ApiTokenRepository; import org.opensearch.security.auditlog.NullAuditLog; import org.opensearch.security.securityconf.ConfigModel; import org.opensearch.security.securityconf.DynamicConfigModel; @@ -160,7 +161,8 @@ PrivilegesEvaluator createPrivilegesEvaluator(SecurityDynamicConfiguration Date: Thu, 16 Jan 2025 11:54:50 -0500 Subject: [PATCH 23/30] Final cleanup Signed-off-by: Derek Ho --- .../action/apitokens/ApiTokenRepository.java | 58 +------ .../action/apitokens/Permissions.java | 15 +- .../security/http/ApiTokenAuthenticator.java | 10 +- .../identity/SecurityTokenManager.java | 8 +- .../security/privileges/ActionPrivileges.java | 152 ++++++++---------- .../PrivilegesEvaluationContext.java | 8 +- .../apitokens/ApiTokenAuthenticatorTest.java | 1 - .../apitokens/ApiTokenRepositoryTest.java | 71 +------- .../security/authtoken/jwt/JwtVendorTest.java | 4 +- .../identity/SecurityTokenManagerTest.java | 22 +-- 10 files changed, 94 insertions(+), 255 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 acbd48f2ad..5c2b666126 100644 --- a/src/main/java/org/opensearch/security/action/apitokens/ApiTokenRepository.java +++ b/src/main/java/org/opensearch/security/action/apitokens/ApiTokenRepository.java @@ -20,54 +20,24 @@ 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.common.inject.Inject; 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; -import static org.opensearch.security.action.apitokens.ApiToken.NAME_FIELD; - -public class ApiTokenRepository implements ClusterStateListener { +public class ApiTokenRepository { 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() { - log.info("Reloading api tokens from index. Currnet entries: " + jtis.entrySet()); - 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) + jtis.clear(); + Map tokensFromIndex = apiTokenIndexHandler.getTokenMetadatas(); + tokensFromIndex.forEach( + (key, apiToken) -> jtis.put(key, new Permissions(apiToken.getClusterPermissions(), apiToken.getIndexPermissions())) ); } @@ -88,28 +58,16 @@ public Map getJtis() { public ApiTokenRepository(Client client, ClusterService clusterService, SecurityTokenManager tokenManager) { apiTokenIndexHandler = new ApiTokenIndexHandler(client, clusterService); securityTokenManager = tokenManager; - this.client = client; - clusterService.addListener(this); } - private ApiTokenRepository(ApiTokenIndexHandler apiTokenIndexHandler, SecurityTokenManager securityTokenManager, Client client) { + private ApiTokenRepository(ApiTokenIndexHandler apiTokenIndexHandler, SecurityTokenManager securityTokenManager) { this.apiTokenIndexHandler = apiTokenIndexHandler; this.securityTokenManager = securityTokenManager; - this.client = client; } @VisibleForTesting - 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(); - } + static ApiTokenRepository forTest(ApiTokenIndexHandler apiTokenIndexHandler, SecurityTokenManager securityTokenManager) { + return new ApiTokenRepository(apiTokenIndexHandler, securityTokenManager); } public String createApiToken( diff --git a/src/main/java/org/opensearch/security/action/apitokens/Permissions.java b/src/main/java/org/opensearch/security/action/apitokens/Permissions.java index cb1478b9ae..91e8cedf80 100644 --- a/src/main/java/org/opensearch/security/action/apitokens/Permissions.java +++ b/src/main/java/org/opensearch/security/action/apitokens/Permissions.java @@ -11,30 +11,19 @@ import java.util.List; public class Permissions { - private List clusterPerm; - private List indexPermission; + private final List clusterPerm; + private final 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/http/ApiTokenAuthenticator.java b/src/main/java/org/opensearch/security/http/ApiTokenAuthenticator.java index 88bd0c9ae6..0a8e3466d7 100644 --- a/src/main/java/org/opensearch/security/http/ApiTokenAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/ApiTokenAuthenticator.java @@ -63,7 +63,6 @@ public class ApiTokenAuthenticator implements HTTPAuthenticator { @SuppressWarnings("removal") public ApiTokenAuthenticator(Settings settings, String clusterName, ApiTokenRepository apiTokenRepository) { - log.info("We instantiating it"); String apiTokenEnabledSetting = settings.get("enabled", "true"); apiTokenEnabled = Boolean.parseBoolean(apiTokenEnabledSetting); @@ -127,7 +126,6 @@ private AuthCredentials extractCredentials0(final SecurityRequest request, final log.error("Api token authentication is disabled"); return null; } - log.info("API TOKEN AUTHENTICATOR IS BEING CALLED"); String jwtToken = extractJwtFromHeader(request); if (jwtToken == null) { @@ -143,13 +141,13 @@ private AuthCredentials extractCredentials0(final SecurityRequest request, final final String subject = claims.getSubject(); if (subject == null) { - log.error("Valid jwt api token with no subject"); + log.error("Api token does not have a subject"); return null; } // TODO: handle revocation different from deletion? if (!apiTokenRepository.isValidToken(subject)) { - log.error("Token is not allowlisted"); + log.error("Api token is not allowlisted"); return null; } @@ -162,11 +160,11 @@ private AuthCredentials extractCredentials0(final SecurityRequest request, final return new AuthCredentials(API_TOKEN_USER_PREFIX + subject, List.of(), "").markComplete(); } catch (WeakKeyException e) { - log.error("Cannot authenticate user with JWT because of ", e); + log.error("Cannot authenticate api token because of ", e); return null; } catch (Exception e) { if (log.isDebugEnabled()) { - log.debug("Invalid or expired JWT token.", e); + log.debug("Invalid or expired api token.", e); } } diff --git a/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java b/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java index d8840df14b..0418879cf4 100644 --- a/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java +++ b/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java @@ -52,7 +52,7 @@ public class SecurityTokenManager implements TokenManager { private final UserService userService; private OBOJwtVendor oboJwtVendor = null; - private ApiTokenJwtVendor apiTokenOBOJwtVendor = null; + private ApiTokenJwtVendor apiTokenJwtVendor = null; private ConfigModel configModel = null; public SecurityTokenManager(final ClusterService cs, final ThreadPool threadPool, final UserService userService) { @@ -76,7 +76,7 @@ public void onDynamicConfigModelChanged(final DynamicConfigModel dcm) { final Settings apiTokenSettings = dcm.getDynamicApiTokenSettings(); final Boolean apiTokenEnabled = apiTokenSettings.getAsBoolean("enabled", false); if (apiTokenEnabled) { - apiTokenOBOJwtVendor = createApiTokenJwtVendor(apiTokenSettings); + apiTokenJwtVendor = createApiTokenJwtVendor(apiTokenSettings); } } @@ -105,7 +105,7 @@ public boolean issueOnBehalfOfTokenAllowed() { } public boolean issueApiTokenAllowed() { - return apiTokenOBOJwtVendor != null && configModel != null; + return apiTokenJwtVendor != null && configModel != null; } @Override @@ -157,7 +157,7 @@ public ExpiringBearerAuthToken issueApiToken(final String name, final Long expir final User user = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); try { - return apiTokenOBOJwtVendor.createJwt(cs.getClusterName().value(), name, name, expiration); + 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/main/java/org/opensearch/security/privileges/ActionPrivileges.java b/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java index 6d2b3316e9..2f5f6943ac 100644 --- a/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java +++ b/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java @@ -50,8 +50,6 @@ 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. *

@@ -429,37 +427,34 @@ PrivilegesEvaluatorResponse apiTokenProvidesClusterPrivilege( Set actions, Boolean explicit ) { - String userName = context.getUser().getName(); - if (userName.startsWith(API_TOKEN_USER_PREFIX)) { - Permissions permissions = context.getPermissionsForApiToken(); - Set resolvedClusterPermissions = actionGroups.resolve(permissions.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(); } } } @@ -915,74 +910,63 @@ PrivilegesEvaluatorResponse apiTokenProvidesIndexPrivilege( Set actions, Boolean explicit ) { - String userName = context.getUser().getName(); - if (userName.startsWith(API_TOKEN_USER_PREFIX)) { - 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; - } + 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(); - } - 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) { + 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(); } } diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java index 3314f3f99d..a2e3a5c924 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java @@ -51,8 +51,7 @@ public class PrivilegesEvaluationContext { private final IndexResolverReplacer indexResolverReplacer; private final IndexNameExpressionResolver indexNameExpressionResolver; private final Supplier clusterStateSupplier; - private ApiTokenRepository apiTokenRepository; - private Permissions permissionsForApiToken; + private final 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, @@ -79,8 +78,7 @@ public PrivilegesEvaluationContext( this.indexResolverReplacer = indexResolverReplacer; this.indexNameExpressionResolver = indexNameExpressionResolver; this.task = task; - this.apiTokenRepository = apiTokenRepository; - this.permissionsForApiToken = extractApiTokenPermissionsForUser(); + this.permissionsForApiToken = extractApiTokenPermissionsForUser(apiTokenRepository); } public User getUser() { @@ -210,7 +208,7 @@ public Permissions getPermissionsForApiToken() { this.permissionsForApiToken = permissions; } - private Permissions extractApiTokenPermissionsForUser() { + private Permissions extractApiTokenPermissionsForUser(ApiTokenRepository apiTokenRepository) { if (user.getName().startsWith(API_TOKEN_USER_PREFIX)) { String jti = user.getName().split(API_TOKEN_USER_PREFIX)[1]; if (apiTokenRepository.isValidToken(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 75278f2dbf..035a47d118 100644 --- a/src/test/java/org/opensearch/security/action/apitokens/ApiTokenAuthenticatorTest.java +++ b/src/test/java/org/opensearch/security/action/apitokens/ApiTokenAuthenticatorTest.java @@ -183,7 +183,6 @@ public void testAuthenticatorNotEnabled() { Settings settings = Settings.builder() .put("enabled", "false") .put("signing_key", "U3VwZXJTZWNyZXRLZXlUaGF0SXNFeGFjdGx5NjRCeXRlc0xvbmdBbmRXaWxsV29ya1dpdGhIUzUxMkFsZ29yaXRobSEhCgo=") - .put("encryption_key", "MTIzNDU2Nzg5MDEyMzQ1Ng==") .build(); ThreadContext threadContext = new ThreadContext(settings); 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 43dd2f2542..293672d96a 100644 --- a/src/test/java/org/opensearch/security/action/apitokens/ApiTokenRepositoryTest.java +++ b/src/test/java/org/opensearch/security/action/apitokens/ApiTokenRepositoryTest.java @@ -17,28 +17,13 @@ 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; @@ -53,7 +38,6 @@ 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; @@ -61,31 +45,15 @@ 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; @Before public void setUp() { apiTokenIndexHandler = mock(ApiTokenIndexHandler.class); securityTokenManager = mock(SecurityTokenManager.class); - repository = ApiTokenRepository.forTest(apiTokenIndexHandler, securityTokenManager, client); + repository = ApiTokenRepository.forTest(apiTokenIndexHandler, securityTokenManager); } @Test @@ -119,7 +87,6 @@ public void testCreateApiToken() { Long expiration = 3600L; String completeToken = "complete-token"; - String encryptedToken = "encrypted-token"; ExpiringBearerAuthToken bearerToken = mock(ExpiringBearerAuthToken.class); when(bearerToken.getCompleteToken()).thenReturn(completeToken); when(securityTokenManager.issueApiToken(any(), any())).thenReturn(bearerToken); @@ -175,32 +142,9 @@ public void testClearJtis() { 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); + when(apiTokenIndexHandler.getTokenMetadatas()).thenReturn(Map.of("test", new ApiToken("test", List.of("cluster:monitor"), List.of(), Long.MAX_VALUE))); // Execute the reload repository.reloadApiTokensFromIndex(); @@ -213,15 +157,4 @@ public void testReloadApiTokensFromIndexAndParse() throws IOException { 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; - } } 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 0c1258f831..32f64b7bb3 100644 --- a/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java +++ b/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java @@ -318,11 +318,11 @@ public void testCreateJwtForApiTokenSuccess() throws Exception { } @Test - public void testKeyTooShortThrowsException() { + public void testKeyTooShortForApiTokenThrowsException() { 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 OBOJwtVendor(settings, Optional.empty()); }); + final Throwable exception = assertThrows(OpenSearchException.class, () -> { new ApiTokenJwtVendor(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 fb553f8a11..d24111a16c 100644 --- a/src/test/java/org/opensearch/security/identity/SecurityTokenManagerTest.java +++ b/src/test/java/org/opensearch/security/identity/SecurityTokenManagerTest.java @@ -126,6 +126,7 @@ private DynamicConfigModel createMockJwtVendorInTokenManager() { when(dcm.getDynamicOnBehalfOfSettings()).thenReturn(settings); when(dcm.getDynamicApiTokenSettings()).thenReturn(settings); doAnswer((invocation) -> oboJwtVendor).when(tokenManager).createOboJwtVendor(settings); + doAnswer((invocation) -> apiTokenJwtVendor).when(tokenManager).createApiTokenJwtVendor(settings); tokenManager.onDynamicConfigModelChanged(dcm); return dcm; } @@ -272,25 +273,4 @@ public void issueApiToken_success() throws Exception { verify(cs).getClusterName(); verify(threadPool).getThreadContext(); } - - @Test - public void encryptCallsJwtEncrypt() throws Exception { - doAnswer(invockation -> new ClusterName("cluster17")).when(cs).getClusterName(); - final ThreadContext threadContext = new ThreadContext(Settings.EMPTY); - threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, new User("Jon", List.of(), null)); - when(threadPool.getThreadContext()).thenReturn(threadContext); - final ConfigModel configModel = mock(ConfigModel.class); - tokenManager.onConfigModelChanged(configModel); - - createMockJwtVendorInTokenManager(); - - final ExpiringBearerAuthToken authToken = mock(ExpiringBearerAuthToken.class); - when(oboJwtVendor.createJwt(anyString(), anyString(), anyString(), anyLong())).thenReturn(authToken); - final AuthToken returnedToken = tokenManager.issueApiToken("elmo", Long.MAX_VALUE); - - assertThat(returnedToken, equalTo(authToken)); - - verify(cs).getClusterName(); - verify(threadPool).getThreadContext(); - } } From 83769b1cfe242d8aa9b51b4ddd2c7d35943fedcd Mon Sep 17 00:00:00 2001 From: Derek Ho Date: Fri, 17 Jan 2025 17:44:55 -0500 Subject: [PATCH 24/30] Fix tests Signed-off-by: Derek Ho --- .../privileges/ActionPrivilegesTest.java | 3 ++- .../action/apitokens/ApiTokenRepository.java | 14 +++++++++++ .../apitokens/ApiTokenRepositoryTest.java | 24 +++++++++++++++++++ 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/integrationTest/java/org/opensearch/security/privileges/ActionPrivilegesTest.java b/src/integrationTest/java/org/opensearch/security/privileges/ActionPrivilegesTest.java index 47f7dda4db..6b6f87caad 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/ActionPrivilegesTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/ActionPrivilegesTest.java @@ -1189,7 +1189,8 @@ static PrivilegesEvaluationContext ctxByUsername(String username) { null, null, new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), - null + null, + mock(ApiTokenRepository.class) ); } } 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 5c2b666126..5ed0154479 100644 --- a/src/main/java/org/opensearch/security/action/apitokens/ApiTokenRepository.java +++ b/src/main/java/org/opensearch/security/action/apitokens/ApiTokenRepository.java @@ -25,6 +25,9 @@ import org.opensearch.index.IndexNotFoundException; import org.opensearch.security.authtoken.jwt.ExpiringBearerAuthToken; import org.opensearch.security.identity.SecurityTokenManager; +import org.opensearch.security.user.User; + +import static org.opensearch.security.http.ApiTokenAuthenticator.API_TOKEN_USER_PREFIX; public class ApiTokenRepository { private final ApiTokenIndexHandler apiTokenIndexHandler; @@ -41,6 +44,17 @@ void reloadApiTokensFromIndex() { ); } + public Permissions getApiTokenPermissionsForUser(User user) { + String name = user.getName(); + if (name.startsWith(API_TOKEN_USER_PREFIX)) { + String jti = user.getName().split(API_TOKEN_USER_PREFIX)[1]; + if (isValidToken(jti)) { + return getPermissionsForJti(jti); + } + } + return new Permissions(List.of(), List.of()); + } + public Permissions getPermissionsForJti(String jti) { return jtis.get(jti); } 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 293672d96a..89f8b950cd 100644 --- a/src/test/java/org/opensearch/security/action/apitokens/ApiTokenRepositoryTest.java +++ b/src/test/java/org/opensearch/security/action/apitokens/ApiTokenRepositoryTest.java @@ -24,6 +24,7 @@ import org.opensearch.index.IndexNotFoundException; import org.opensearch.security.authtoken.jwt.ExpiringBearerAuthToken; import org.opensearch.security.identity.SecurityTokenManager; +import org.opensearch.security.user.User; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; @@ -65,6 +66,29 @@ public void testDeleteApiToken() throws ApiTokenException { verify(apiTokenIndexHandler).deleteToken(tokenName); } + @Test + public void testGetApiTokenPermissionsForUser() throws ApiTokenException { + User derek = new User("derek"); + User apiTokenNotExists = new User("apitoken:notexists"); + User apiTokenExists = new User("apitoken:exists"); + repository.getJtis() + .put("exists", new Permissions(List.of("cluster_all"), List.of(new ApiToken.IndexPermission(List.of("*"), List.of("*"))))); + + Permissions permissionsForDerek = repository.getApiTokenPermissionsForUser(derek); + assertEquals(List.of(), permissionsForDerek.getClusterPerm()); + assertEquals(List.of(), permissionsForDerek.getIndexPermission()); + + Permissions permissionsForApiTokenNotExists = repository.getApiTokenPermissionsForUser(apiTokenNotExists); + assertEquals(List.of(), permissionsForApiTokenNotExists.getClusterPerm()); + assertEquals(List.of(), permissionsForApiTokenNotExists.getIndexPermission()); + + Permissions permissionsForApiTokenExists = repository.getApiTokenPermissionsForUser(apiTokenExists); + assertEquals(List.of("cluster_all"), permissionsForApiTokenExists.getClusterPerm()); + assertEquals(List.of("*"), permissionsForApiTokenExists.getIndexPermission().getFirst().getAllowedActions()); + assertEquals(List.of("*"), permissionsForApiTokenExists.getIndexPermission().getFirst().getIndexPatterns()); + + } + @Test public void testGetApiTokens() throws IndexNotFoundException { Map expectedTokens = new HashMap<>(); From 685dcfc8b7752fc4b118b1b85e3b9e6f827e7c0d Mon Sep 17 00:00:00 2001 From: Derek Ho Date: Tue, 21 Jan 2025 14:52:48 -0500 Subject: [PATCH 25/30] PR feedback Signed-off-by: Derek Ho --- .../privileges/ActionPrivilegesTest.java | 6 +- .../security/privileges/IndexPatternTest.java | 5 +- .../RestEndpointPermissionTests.java | 5 +- .../dlsfls/DlsFlsLegacyHeadersTest.java | 6 +- .../dlsfls/DocumentPrivilegesTest.java | 11 +- .../privileges/dlsfls/FieldMaskingTest.java | 5 +- .../dlsfls/FieldPrivilegesTest.java | 5 +- .../action/apitokens/ApiTokenRepository.java | 2 +- .../action/apitokens/Permissions.java | 6 + .../authtoken/jwt/ApiTokenJwtVendor.java | 83 ----------- .../security/authtoken/jwt/JwtVendor.java | 132 +++++++++++++++--- .../security/authtoken/jwt/OBOJwtVendor.java | 112 --------------- .../jwt/claims/ApiJwtClaimsBuilder.java | 19 +++ .../jwt/claims/JwtClaimsBuilder.java | 85 +++++++++++ .../jwt/claims/OBOJwtClaimsBuilder.java | 36 +++++ .../identity/SecurityTokenManager.java | 29 ++-- .../security/privileges/ActionPrivileges.java | 2 +- .../PrivilegesEvaluationContext.java | 42 +----- .../privileges/PrivilegesEvaluator.java | 2 +- .../apitokens/ApiTokenAuthenticatorTest.java | 2 +- .../security/authtoken/jwt/JwtVendorTest.java | 56 ++++---- .../identity/SecurityTokenManagerTest.java | 30 ++-- .../SystemIndexAccessEvaluatorTest.java | 4 +- 23 files changed, 344 insertions(+), 341 deletions(-) delete mode 100644 src/main/java/org/opensearch/security/authtoken/jwt/ApiTokenJwtVendor.java delete mode 100644 src/main/java/org/opensearch/security/authtoken/jwt/OBOJwtVendor.java create mode 100644 src/main/java/org/opensearch/security/authtoken/jwt/claims/ApiJwtClaimsBuilder.java create mode 100644 src/main/java/org/opensearch/security/authtoken/jwt/claims/JwtClaimsBuilder.java create mode 100644 src/main/java/org/opensearch/security/authtoken/jwt/claims/OBOJwtClaimsBuilder.java diff --git a/src/integrationTest/java/org/opensearch/security/privileges/ActionPrivilegesTest.java b/src/integrationTest/java/org/opensearch/security/privileges/ActionPrivilegesTest.java index 6b6f87caad..0c3100fa88 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/ActionPrivilegesTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/ActionPrivilegesTest.java @@ -63,7 +63,6 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; /** * Unit tests for ActionPrivileges. As the ActionPrivileges provides quite a few different code paths for checking @@ -1157,14 +1156,13 @@ static PrivilegesEvaluationContext ctxWithUserName(String userName, String... ro null, new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), null, - mock(ApiTokenRepository.class) + new Permissions() ); } 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(), @@ -1190,7 +1188,7 @@ static PrivilegesEvaluationContext ctxByUsername(String username) { null, new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), null, - mock(ApiTokenRepository.class) + new Permissions() ); } } diff --git a/src/integrationTest/java/org/opensearch/security/privileges/IndexPatternTest.java b/src/integrationTest/java/org/opensearch/security/privileges/IndexPatternTest.java index ce139934c8..ddf0c86562 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/IndexPatternTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/IndexPatternTest.java @@ -22,7 +22,7 @@ import org.opensearch.cluster.metadata.Metadata; import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; -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; @@ -32,7 +32,6 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; public class IndexPatternTest { final static int CURRENT_YEAR = ZonedDateTime.now().get(ChronoField.YEAR); @@ -249,7 +248,7 @@ private static PrivilegesEvaluationContext ctx() { indexResolverReplacer, indexNameExpressionResolver, () -> CLUSTER_STATE, - mock(ApiTokenRepository.class) + new Permissions() ); } } diff --git a/src/integrationTest/java/org/opensearch/security/privileges/RestEndpointPermissionTests.java b/src/integrationTest/java/org/opensearch/security/privileges/RestEndpointPermissionTests.java index f5b1529f46..fa4614e992 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/RestEndpointPermissionTests.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/RestEndpointPermissionTests.java @@ -44,7 +44,7 @@ import org.opensearch.common.settings.Settings; import org.opensearch.security.DefaultObjectMapper; -import org.opensearch.security.action.apitokens.ApiTokenRepository; +import org.opensearch.security.action.apitokens.Permissions; import org.opensearch.security.dlic.rest.api.Endpoint; import org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.PermissionBuilder; import org.opensearch.security.securityconf.FlattenedActionGroups; @@ -57,7 +57,6 @@ import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.ENDPOINTS_WITH_PERMISSIONS; import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.RELOAD_CERTS_ACTION; import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.SECURITY_CONFIG_UPDATE; -import static org.mockito.Mockito.mock; /** * Moved from https://github.com/opensearch-project/security/blob/54361468f5c4b3a57f3ecffaf1bbe8dccee562be/src/test/java/org/opensearch/security/securityconf/SecurityRolesPermissionsTest.java @@ -262,7 +261,7 @@ static PrivilegesEvaluationContext ctx(String... roles) { null, null, null, - mock(ApiTokenRepository.class) + new Permissions() ); } 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 398b08dcb9..66d3c99622 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/DlsFlsLegacyHeadersTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/DlsFlsLegacyHeadersTest.java @@ -35,7 +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.action.apitokens.Permissions; import org.opensearch.security.privileges.PrivilegesEvaluationContext; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; import org.opensearch.security.securityconf.impl.v7.RoleV7; @@ -348,7 +348,7 @@ public void prepare_ccs() throws Exception { null, new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), () -> clusterState, - mock(ApiTokenRepository.class) + new Permissions() ); DlsFlsLegacyHeaders.prepare(threadContext, ctx, dlsFlsProcessedConfig(exampleRolesConfig(), metadata), metadata, false); @@ -368,7 +368,7 @@ static PrivilegesEvaluationContext ctx(Metadata metadata, String... roles) { null, new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), () -> clusterState, - mock(ApiTokenRepository.class) + new Permissions() ); } 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 3d7c67c922..aff090e390 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/DocumentPrivilegesTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/DocumentPrivilegesTest.java @@ -51,7 +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.action.apitokens.Permissions; import org.opensearch.security.privileges.PrivilegesConfigurationValidationException; import org.opensearch.security.privileges.PrivilegesEvaluationContext; import org.opensearch.security.privileges.PrivilegesEvaluationException; @@ -69,7 +69,6 @@ import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import static org.mockito.Mockito.mock; /** * Unit tests for the DocumentPrivileges class and the underlying AbstractRuleBasedPrivileges class. As these classes @@ -529,7 +528,7 @@ public IndicesAndAliases_getRestriction( null, null, () -> CLUSTER_STATE, - mock(ApiTokenRepository.class) + new Permissions() ); this.statefulness = statefulness; this.dfmEmptyOverridesAll = dfmEmptyOverridesAll == DfmEmptyOverridesAll.DFM_EMPTY_OVERRIDES_ALL_TRUE; @@ -845,7 +844,7 @@ public IndicesRequest indices(String... strings) { RESOLVER_REPLACER, INDEX_NAME_EXPRESSION_RESOLVER, () -> CLUSTER_STATE, - mock(ApiTokenRepository.class) + new Permissions() ); this.statefulness = statefulness; this.dfmEmptyOverridesAll = dfmEmptyOverridesAll == DfmEmptyOverridesAll.DFM_EMPTY_OVERRIDES_ALL_TRUE; @@ -1131,7 +1130,7 @@ public DataStreams_getRestriction( null, null, () -> CLUSTER_STATE, - mock(ApiTokenRepository.class) + new Permissions() ); this.statefulness = statefulness; this.dfmEmptyOverridesAll = dfmEmptyOverridesAll == DfmEmptyOverridesAll.DFM_EMPTY_OVERRIDES_ALL_TRUE; @@ -1161,7 +1160,7 @@ public void invalidTemplatedQuery() throws Exception { null, null, null, - mock(ApiTokenRepository.class) + new Permissions() ) ); } diff --git a/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/FieldMaskingTest.java b/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/FieldMaskingTest.java index ad40329679..d7eea419e4 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/FieldMaskingTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/FieldMaskingTest.java @@ -22,7 +22,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.action.apitokens.Permissions; import org.opensearch.security.privileges.PrivilegesConfigurationValidationException; import org.opensearch.security.privileges.PrivilegesEvaluationContext; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; @@ -36,7 +36,6 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; /** * Unit tests on the FieldMasking class - top-level functionality is tested in FieldMaskingTest.Basic. The inner classes FieldMasking.Field @@ -126,7 +125,7 @@ static PrivilegesEvaluationContext ctx(String... roles) { null, null, () -> CLUSTER_STATE, - mock(ApiTokenRepository.class) + new Permissions() ); } } 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 a1386de521..66bf7db073 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/FieldPrivilegesTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/FieldPrivilegesTest.java @@ -21,7 +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.action.apitokens.Permissions; import org.opensearch.security.privileges.PrivilegesConfigurationValidationException; import org.opensearch.security.privileges.PrivilegesEvaluationContext; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; @@ -34,7 +34,6 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; /** * Unit tests on the FieldMasking class - top-level functionality is tested in FieldMaskingTest.Basic. The inner classes FieldMasking.Field @@ -161,7 +160,7 @@ static PrivilegesEvaluationContext ctx(String... roles) { null, null, () -> CLUSTER_STATE, - mock(ApiTokenRepository.class) + new Permissions() ); } } 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 5ed0154479..ad131a59a9 100644 --- a/src/main/java/org/opensearch/security/action/apitokens/ApiTokenRepository.java +++ b/src/main/java/org/opensearch/security/action/apitokens/ApiTokenRepository.java @@ -37,8 +37,8 @@ public class ApiTokenRepository { private final Map jtis = new ConcurrentHashMap<>(); void reloadApiTokensFromIndex() { - jtis.clear(); Map tokensFromIndex = apiTokenIndexHandler.getTokenMetadatas(); + jtis.keySet().removeIf(key -> !tokensFromIndex.containsKey(key)); tokensFromIndex.forEach( (key, apiToken) -> jtis.put(key, new Permissions(apiToken.getClusterPermissions(), apiToken.getIndexPermissions())) ); diff --git a/src/main/java/org/opensearch/security/action/apitokens/Permissions.java b/src/main/java/org/opensearch/security/action/apitokens/Permissions.java index 91e8cedf80..9b684cebde 100644 --- a/src/main/java/org/opensearch/security/action/apitokens/Permissions.java +++ b/src/main/java/org/opensearch/security/action/apitokens/Permissions.java @@ -8,6 +8,7 @@ package org.opensearch.security.action.apitokens; +import java.util.Collections; import java.util.List; public class Permissions { @@ -19,6 +20,11 @@ public Permissions(List clusterPerm, List inde this.indexPermission = indexPermission; } + public Permissions() { + this.clusterPerm = Collections.emptyList(); + this.indexPermission = Collections.emptyList(); + } + public List getClusterPerm() { return clusterPerm; } diff --git a/src/main/java/org/opensearch/security/authtoken/jwt/ApiTokenJwtVendor.java b/src/main/java/org/opensearch/security/authtoken/jwt/ApiTokenJwtVendor.java deleted file mode 100644 index 87de477fe4..0000000000 --- a/src/main/java/org/opensearch/security/authtoken/jwt/ApiTokenJwtVendor.java +++ /dev/null @@ -1,83 +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. - * - * 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.Date; -import java.util.Optional; -import java.util.function.LongSupplier; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -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.jwk.JWK; -import com.nimbusds.jwt.JWTClaimsSet; -import com.nimbusds.jwt.SignedJWT; - -import static org.opensearch.security.authtoken.jwt.JwtVendor.createJwkFromSettings; - -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 32da3eadb7..c1ede9d9f3 100644 --- a/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java +++ b/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java @@ -11,42 +11,53 @@ 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 org.opensearch.security.authtoken.jwt.claims.ApiJwtClaimsBuilder; +import org.opensearch.security.authtoken.jwt.claims.OBOJwtClaimsBuilder; 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.SignedJWT; import static org.opensearch.security.util.AuthTokenUtils.isKeyNull; -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 class JwtVendor { + private static final Logger logger = LogManager.getLogger(JwtVendor.class); + + private final JWK signingKey; + private final JWSSigner signer; + private final LongSupplier timeProvider; + private static final Integer MAX_EXPIRY_SECONDS = 600; + private final Settings settings; - 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."); - }; + public JwtVendor(final Settings settings, final Optional timeProvider) { + final Tuple tuple = createJwkFromSettings(settings); + signingKey = tuple.v1(); + signer = tuple.v2(); + this.settings = settings; + this.timeProvider = timeProvider.orElse(System::currentTimeMillis); + } /* * The default configuration of this web key should be: @@ -82,4 +93,93 @@ static Tuple createJwkFromSettings(final Settings settings) { throw new OpenSearchException(kle); } } + + public ExpiringBearerAuthToken createOBOJwt( + 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 long expirySeconds = Math.min(requestedExpirySeconds, MAX_EXPIRY_SECONDS); + if (expirySeconds <= 0) { + throw new IllegalArgumentException("The expiration time should be a positive integer"); + } + if (roles == null) { + throw new IllegalArgumentException("Roles cannot be null"); + } + if (isKeyNull(settings, "encryption_key")) { + throw new IllegalArgumentException("encryption_key cannot be null"); + } + + final OBOJwtClaimsBuilder claimsBuilder = new OBOJwtClaimsBuilder(settings.get("encryption_key")); + // Add obo claims + claimsBuilder.issuer(issuer); + claimsBuilder.issueTime(now); + claimsBuilder.subject(subject); + claimsBuilder.audience(audience); + claimsBuilder.notBeforeTime(now); + claimsBuilder.addBackendRoles(includeBackendRoles, backendRoles); + claimsBuilder.addRoles(roles); + + final Date expiryTime = new Date(currentTimeMs + expirySeconds * 1000); + claimsBuilder.expirationTime(expiryTime); + + 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 createApiTokenJwt( + 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 ApiJwtClaimsBuilder claimsBuilder = new ApiJwtClaimsBuilder(); + 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/OBOJwtVendor.java b/src/main/java/org/opensearch/security/authtoken/jwt/OBOJwtVendor.java deleted file mode 100644 index e5f7e65541..0000000000 --- a/src/main/java/org/opensearch/security/authtoken/jwt/OBOJwtVendor.java +++ /dev/null @@ -1,112 +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. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.security.authtoken.jwt; - -import java.text.ParseException; -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.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.jwk.JWK; -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/authtoken/jwt/claims/ApiJwtClaimsBuilder.java b/src/main/java/org/opensearch/security/authtoken/jwt/claims/ApiJwtClaimsBuilder.java new file mode 100644 index 0000000000..ebb5552045 --- /dev/null +++ b/src/main/java/org/opensearch/security/authtoken/jwt/claims/ApiJwtClaimsBuilder.java @@ -0,0 +1,19 @@ +/* + * 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.claims; + +public class ApiJwtClaimsBuilder extends JwtClaimsBuilder { + + public ApiJwtClaimsBuilder() { + super(); + } +} diff --git a/src/main/java/org/opensearch/security/authtoken/jwt/claims/JwtClaimsBuilder.java b/src/main/java/org/opensearch/security/authtoken/jwt/claims/JwtClaimsBuilder.java new file mode 100644 index 0000000000..32920296b7 --- /dev/null +++ b/src/main/java/org/opensearch/security/authtoken/jwt/claims/JwtClaimsBuilder.java @@ -0,0 +1,85 @@ +/* + * 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.claims; + +import java.util.Date; + +import org.opensearch.security.authtoken.jwt.EncryptionDecryptionUtil; + +import com.nimbusds.jwt.JWTClaimsSet; + +public class JwtClaimsBuilder { + private final JWTClaimsSet.Builder builder; + private EncryptionDecryptionUtil encryptionDecryptionUtil; + + public JwtClaimsBuilder() { + this.builder = new JWTClaimsSet.Builder(); + } + + public JwtClaimsBuilder encryptionKey(String encryptionKey) { + this.encryptionDecryptionUtil = new EncryptionDecryptionUtil(encryptionKey); + return this; + } + + public JwtClaimsBuilder issueTime(Date issueTime) { + builder.issueTime(issueTime); + return this; + } + + public JwtClaimsBuilder notBeforeTime(Date notBeforeTime) { + builder.notBeforeTime(notBeforeTime); + return this; + } + + public JwtClaimsBuilder subject(String subject) { + builder.subject(subject); + return this; + } + + public JwtClaimsBuilder issuer(String issuer) { + builder.issuer(issuer); + return this; + } + + public JwtClaimsBuilder audience(String audience) { + builder.audience(audience); + return this; + } + + public JwtClaimsBuilder issuedAt(Date issuedAt) { + builder.issueTime(issuedAt); + return this; + } + + public JwtClaimsBuilder expirationTime(Date expirationTime) { + builder.expirationTime(expirationTime); + return this; + } + + public JwtClaimsBuilder addCustomClaimWithEncryption(String claimName, String value) { + if (this.encryptionDecryptionUtil == null) { + throw new IllegalArgumentException("Cannot add encrypted field without encryption key"); + } + builder.claim(claimName, encryptionDecryptionUtil.encrypt(value)); + return this; + } + + public JwtClaimsBuilder addCustomClaim(String claimName, String value) { + builder.claim(claimName, value); + return this; + } + + public JWTClaimsSet build() { + return builder.build(); + } + +} diff --git a/src/main/java/org/opensearch/security/authtoken/jwt/claims/OBOJwtClaimsBuilder.java b/src/main/java/org/opensearch/security/authtoken/jwt/claims/OBOJwtClaimsBuilder.java new file mode 100644 index 0000000000..90d0bcc82e --- /dev/null +++ b/src/main/java/org/opensearch/security/authtoken/jwt/claims/OBOJwtClaimsBuilder.java @@ -0,0 +1,36 @@ +/* + * 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.claims; + +import java.util.List; + +public class OBOJwtClaimsBuilder extends JwtClaimsBuilder { + + public OBOJwtClaimsBuilder(String encryptionKey) { + super(); + this.encryptionKey(encryptionKey); + } + + public OBOJwtClaimsBuilder addRoles(List roles) { + final String listOfRoles = String.join(",", roles); + this.addCustomClaimWithEncryption("er", listOfRoles); + return this; + } + + public OBOJwtClaimsBuilder addBackendRoles(Boolean includeBackendRoles, List backendRoles) { + if (includeBackendRoles && backendRoles != null) { + final String listOfBackendRoles = String.join(",", backendRoles); + this.addCustomClaim("br", listOfBackendRoles); + } + return this; + } +} diff --git a/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java b/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java index 0418879cf4..3489780a1b 100644 --- a/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java +++ b/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java @@ -27,9 +27,8 @@ 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.OBOJwtVendor; +import org.opensearch.security.authtoken.jwt.JwtVendor; import org.opensearch.security.securityconf.ConfigModel; import org.opensearch.security.securityconf.DynamicConfigModel; import org.opensearch.security.support.ConfigConstants; @@ -51,8 +50,8 @@ public class SecurityTokenManager implements TokenManager { private final ThreadPool threadPool; private final UserService userService; - private OBOJwtVendor oboJwtVendor = null; - private ApiTokenJwtVendor apiTokenJwtVendor = null; + private JwtVendor oboJwtVendor = null; + private JwtVendor apiTokenJwtVendor = null; private ConfigModel configModel = null; public SecurityTokenManager(final ClusterService cs, final ThreadPool threadPool, final UserService userService) { @@ -71,29 +70,19 @@ public void onDynamicConfigModelChanged(final DynamicConfigModel dcm) { final Settings oboSettings = dcm.getDynamicOnBehalfOfSettings(); final Boolean oboEnabled = oboSettings.getAsBoolean("enabled", false); if (oboEnabled) { - oboJwtVendor = createOboJwtVendor(oboSettings); + oboJwtVendor = createJwtVendor(oboSettings); } final Settings apiTokenSettings = dcm.getDynamicApiTokenSettings(); final Boolean apiTokenEnabled = apiTokenSettings.getAsBoolean("enabled", false); if (apiTokenEnabled) { - apiTokenJwtVendor = createApiTokenJwtVendor(apiTokenSettings); + apiTokenJwtVendor = createJwtVendor(apiTokenSettings); } } /** For testing */ - OBOJwtVendor createOboJwtVendor(final Settings settings) { + JwtVendor createJwtVendor(final Settings settings) { try { - 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()); + return new JwtVendor(settings, Optional.empty()); } catch (final Exception ex) { logger.error("Unable to create the JwtVendor instance", ex); return null; @@ -135,7 +124,7 @@ public ExpiringBearerAuthToken issueOnBehalfOfToken(final Subject subject, final final Set mappedRoles = configModel.mapSecurityRoles(user, callerAddress); try { - return oboJwtVendor.createJwt( + return oboJwtVendor.createOBOJwt( cs.getClusterName().value(), user.getName(), claims.getAudience(), @@ -157,7 +146,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 apiTokenJwtVendor.createApiTokenJwt(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/ActionPrivileges.java b/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java index 9703cedb1a..e34917b1f1 100644 --- a/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java +++ b/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java @@ -487,7 +487,7 @@ PrivilegesEvaluatorResponse apiTokenProvidesClusterPrivilege( // Check for pattern matches (like "cluster:*") for (String permission : resolvedClusterPermissions) { // skip pure *, which was evaluated above - if (permission != "*") { + if (!"*".equals(permission)) { // Skip exact matches as we already checked those if (!permission.contains("*")) { continue; diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java index a2e3a5c924..c91e36d714 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java @@ -11,26 +11,21 @@ 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.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. *

@@ -68,7 +63,7 @@ public PrivilegesEvaluationContext( IndexResolverReplacer indexResolverReplacer, IndexNameExpressionResolver indexNameExpressionResolver, Supplier clusterStateSupplier, - ApiTokenRepository apiTokenRepository + Permissions permissions ) { this.user = user; this.mappedRoles = mappedRoles; @@ -78,7 +73,7 @@ public PrivilegesEvaluationContext( this.indexResolverReplacer = indexResolverReplacer; this.indexNameExpressionResolver = indexNameExpressionResolver; this.task = task; - this.permissionsForApiToken = extractApiTokenPermissionsForUser(apiTokenRepository); + this.permissionsForApiToken = permissions; } public User getUser() { @@ -184,37 +179,4 @@ public String toString() { 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(ApiTokenRepository apiTokenRepository) { - 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/privileges/PrivilegesEvaluator.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java index 265b439b66..c7ac84c0a2 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java @@ -318,7 +318,7 @@ public PrivilegesEvaluationContext createContext( irr, resolver, clusterStateSupplier, - apiTokenRepository + apiTokenRepository.getApiTokenPermissionsForUser(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 035a47d118..c24c632fa0 100644 --- a/src/test/java/org/opensearch/security/action/apitokens/ApiTokenAuthenticatorTest.java +++ b/src/test/java/org/opensearch/security/action/apitokens/ApiTokenAuthenticatorTest.java @@ -119,7 +119,7 @@ public void testExtractCredentialsFailWhenTokenIsExpired() { 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)); + verify(log).debug(eq("Invalid or expired api token."), any(ExpiredJwtException.class)); } 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 32f64b7bb3..88363b25fd 100644 --- a/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java +++ b/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java @@ -63,7 +63,7 @@ public class JwtVendorTest { public void testCreateJwkFromSettings() { final Settings settings = Settings.builder().put("signing_key", signingKeyB64Encoded).build(); - final Tuple jwk = OBOJwtVendor.createJwkFromSettings(settings); + final Tuple jwk = JwtVendor.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)); @@ -72,14 +72,14 @@ public void testCreateJwkFromSettings() { @Test public void testCreateJwkFromSettingsWithWeakKey() { Settings settings = Settings.builder().put("signing_key", "abcd1234").build(); - Throwable exception = Assert.assertThrows(OpenSearchException.class, () -> OBOJwtVendor.createJwkFromSettings(settings)); + Throwable exception = Assert.assertThrows(OpenSearchException.class, () -> JwtVendor.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, () -> OBOJwtVendor.createJwkFromSettings(settings)); + Throwable exception = Assert.assertThrows(RuntimeException.class, () -> JwtVendor.createJwkFromSettings(settings)); assertThat( exception.getMessage(), equalTo("Settings for signing key is missing. Please specify at least the option signing_key with a shared secret.") @@ -87,7 +87,7 @@ public void testCreateJwkFromSettingsWithoutSigningKey() { } @Test - public void testCreateJwtWithRoles() throws Exception { + public void testCreateOBOJwtWithRoles() throws Exception { String issuer = "cluster_0"; String subject = "admin"; String audience = "audience_0"; @@ -100,8 +100,8 @@ public void testCreateJwtWithRoles() throws Exception { String claimsEncryptionKey = "1234567890123456"; Settings settings = Settings.builder().put("signing_key", signingKeyB64Encoded).put("encryption_key", claimsEncryptionKey).build(); - OBOJwtVendor OBOJwtVendor = new OBOJwtVendor(settings, Optional.of(currentTime)); - final ExpiringBearerAuthToken authToken = OBOJwtVendor.createJwt( + JwtVendor OBOJwtVendor = new JwtVendor(settings, Optional.of(currentTime)); + final ExpiringBearerAuthToken authToken = OBOJwtVendor.createOBOJwt( issuer, subject, audience, @@ -126,7 +126,7 @@ public void testCreateJwtWithRoles() throws Exception { } @Test - public void testCreateJwtWithBackendRolesIncluded() throws Exception { + public void testCreateOBOJwtWithBackendRolesIncluded() throws Exception { final String issuer = "cluster_0"; final String subject = "admin"; final String audience = "audience_0"; @@ -145,8 +145,8 @@ public void testCreateJwtWithBackendRolesIncluded() throws Exception { .put(ConfigConstants.EXTENSIONS_BWC_PLUGIN_MODE, true) // CS-ENFORCE-SINGLE .build(); - final OBOJwtVendor OBOJwtVendor = new OBOJwtVendor(settings, Optional.of(currentTime)); - final ExpiringBearerAuthToken authToken = OBOJwtVendor.createJwt( + final JwtVendor OBOJwtVendor = new JwtVendor(settings, Optional.of(currentTime)); + final ExpiringBearerAuthToken authToken = OBOJwtVendor.createOBOJwt( issuer, subject, audience, @@ -171,7 +171,7 @@ public void testCreateJwtWithBackendRolesIncluded() throws Exception { } @Test - public void testCreateJwtWithNegativeExpiry() { + public void testCreateOBOJwtWithNegativeExpiry() { String issuer = "cluster_0"; String subject = "admin"; String audience = "audience_0"; @@ -179,11 +179,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(); - OBOJwtVendor OBOJwtVendor = new OBOJwtVendor(settings, Optional.empty()); + JwtVendor OBOJwtVendor = new JwtVendor(settings, Optional.empty()); final Throwable exception = assertThrows(RuntimeException.class, () -> { try { - OBOJwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles, List.of(), true); + OBOJwtVendor.createOBOJwt(issuer, subject, audience, expirySeconds, roles, List.of(), true); } catch (final Exception e) { throw new RuntimeException(e); } @@ -192,7 +192,7 @@ public void testCreateJwtWithNegativeExpiry() { } @Test - public void testCreateJwtWithExceededExpiry() throws Exception { + public void testCreateOBOJwtWithExceededExpiry() throws Exception { String issuer = "cluster_0"; String subject = "admin"; String audience = "audience_0"; @@ -202,9 +202,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(); - OBOJwtVendor OBOJwtVendor = new OBOJwtVendor(settings, Optional.of(currentTime)); + JwtVendor OBOJwtVendor = new JwtVendor(settings, Optional.of(currentTime)); - final ExpiringBearerAuthToken authToken = OBOJwtVendor.createJwt( + final ExpiringBearerAuthToken authToken = OBOJwtVendor.createOBOJwt( issuer, subject, audience, @@ -219,7 +219,7 @@ public void testCreateJwtWithExceededExpiry() throws Exception { } @Test - public void testCreateJwtWithBadEncryptionKey() { + public void testCreateOBOJwtWithBadEncryptionKey() { final String issuer = "cluster_0"; final String subject = "admin"; final String audience = "audience_0"; @@ -230,7 +230,7 @@ public void testCreateJwtWithBadEncryptionKey() { final Throwable exception = assertThrows(RuntimeException.class, () -> { try { - new OBOJwtVendor(settings, Optional.empty()).createJwt(issuer, subject, audience, expirySeconds, roles, List.of(), true); + new JwtVendor(settings, Optional.empty()).createOBOJwt(issuer, subject, audience, expirySeconds, roles, List.of(), true); } catch (final Exception e) { throw new RuntimeException(e); } @@ -239,7 +239,7 @@ public void testCreateJwtWithBadEncryptionKey() { } @Test - public void testCreateJwtWithBadRoles() { + public void testCreateOBOJwtWithBadRoles() { String issuer = "cluster_0"; String subject = "admin"; String audience = "audience_0"; @@ -247,11 +247,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(); - OBOJwtVendor OBOJwtVendor = new OBOJwtVendor(settings, Optional.empty()); + JwtVendor OBOJwtVendor = new JwtVendor(settings, Optional.empty()); final Throwable exception = assertThrows(RuntimeException.class, () -> { try { - OBOJwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles, List.of(), true); + OBOJwtVendor.createOBOJwt(issuer, subject, audience, expirySeconds, roles, List.of(), true); } catch (final Exception e) { throw new RuntimeException(e); } @@ -260,12 +260,12 @@ public void testCreateJwtWithBadRoles() { } @Test - public void testCreateJwtLogsCorrectly() throws Exception { + public void testCreateOBOJwtLogsCorrectly() throws Exception { mockAppender = mock(Appender.class); logEventCaptor = ArgumentCaptor.forClass(LogEvent.class); when(mockAppender.getName()).thenReturn("MockAppender"); when(mockAppender.isStarted()).thenReturn(true); - final Logger logger = (Logger) LogManager.getLogger(OBOJwtVendor.class); + final Logger logger = (Logger) LogManager.getLogger(JwtVendor.class); logger.addAppender(mockAppender); logger.setLevel(Level.DEBUG); @@ -281,9 +281,9 @@ public void testCreateJwtLogsCorrectly() throws Exception { final List backendRoles = List.of("Sales", "Support"); final int expirySeconds = 300; - final OBOJwtVendor OBOJwtVendor = new OBOJwtVendor(settings, Optional.of(currentTime)); + final JwtVendor OBOJwtVendor = new JwtVendor(settings, Optional.of(currentTime)); - OBOJwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles, backendRoles, false); + OBOJwtVendor.createOBOJwt(issuer, subject, audience, expirySeconds, roles, backendRoles, false); verify(mockAppender, times(1)).append(logEventCaptor.capture()); @@ -296,7 +296,7 @@ public void testCreateJwtLogsCorrectly() throws Exception { } @Test - public void testCreateJwtForApiTokenSuccess() throws Exception { + public void testCreateOBOJwtForApiTokenSuccess() throws Exception { final String issuer = "cluster_0"; final String subject = "test-token"; final String audience = "test-token"; @@ -304,8 +304,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 ApiTokenJwtVendor apiTokenJwtVendor = new ApiTokenJwtVendor(settings, Optional.of(currentTime)); - final ExpiringBearerAuthToken authToken = apiTokenJwtVendor.createJwt(issuer, subject, audience, Long.MAX_VALUE); + final JwtVendor apiTokenJwtVendor = new JwtVendor(settings, Optional.of(currentTime)); + final ExpiringBearerAuthToken authToken = apiTokenJwtVendor.createApiTokenJwt(issuer, subject, audience, Long.MAX_VALUE); SignedJWT signedJWT = SignedJWT.parse(authToken.getCompleteToken()); @@ -322,7 +322,7 @@ public void testKeyTooShortForApiTokenThrowsException() { 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 ApiTokenJwtVendor(settings, Optional.empty()); }); + final Throwable exception = assertThrows(OpenSearchException.class, () -> { new JwtVendor(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 d24111a16c..8b3adb0d70 100644 --- a/src/test/java/org/opensearch/security/identity/SecurityTokenManagerTest.java +++ b/src/test/java/org/opensearch/security/identity/SecurityTokenManagerTest.java @@ -28,9 +28,8 @@ import org.opensearch.identity.Subject; import org.opensearch.identity.tokens.AuthToken; import org.opensearch.identity.tokens.OnBehalfOfClaims; -import org.opensearch.security.authtoken.jwt.ApiTokenJwtVendor; import org.opensearch.security.authtoken.jwt.ExpiringBearerAuthToken; -import org.opensearch.security.authtoken.jwt.OBOJwtVendor; +import org.opensearch.security.authtoken.jwt.JwtVendor; import org.opensearch.security.securityconf.ConfigModel; import org.opensearch.security.securityconf.DynamicConfigModel; import org.opensearch.security.support.ConfigConstants; @@ -62,9 +61,9 @@ public class SecurityTokenManagerTest { private SecurityTokenManager tokenManager; @Mock - private OBOJwtVendor oboJwtVendor; + private JwtVendor oboJwtVendor; @Mock - private ApiTokenJwtVendor apiTokenJwtVendor; + private JwtVendor apiTokenJwtVendor; @Mock private ClusterService cs; @Mock @@ -116,7 +115,7 @@ public void onDynamicConfigModelChanged_JwtVendorDisabled() { assertThat(tokenManager.issueOnBehalfOfTokenAllowed(), equalTo(false)); verify(dcm).getDynamicOnBehalfOfSettings(); - verify(tokenManager, never()).createOboJwtVendor(any()); + verify(tokenManager, never()).createJwtVendor(any()); } /** Creates the jwt vendor and returns a mock for validation if needed */ @@ -125,8 +124,17 @@ private DynamicConfigModel createMockJwtVendorInTokenManager() { final DynamicConfigModel dcm = mock(DynamicConfigModel.class); when(dcm.getDynamicOnBehalfOfSettings()).thenReturn(settings); when(dcm.getDynamicApiTokenSettings()).thenReturn(settings); - doAnswer((invocation) -> oboJwtVendor).when(tokenManager).createOboJwtVendor(settings); - doAnswer((invocation) -> apiTokenJwtVendor).when(tokenManager).createApiTokenJwtVendor(settings); + doAnswer((invocation) -> oboJwtVendor).when(tokenManager).createJwtVendor(settings); + tokenManager.onDynamicConfigModelChanged(dcm); + return dcm; + } + + private DynamicConfigModel createMockApiTokenJwtVendorInTokenManager() { + final Settings settings = Settings.builder().put("enabled", true).build(); + final DynamicConfigModel dcm = mock(DynamicConfigModel.class); + when(dcm.getDynamicOnBehalfOfSettings()).thenReturn(settings); + when(dcm.getDynamicApiTokenSettings()).thenReturn(settings); + doAnswer((invocation) -> apiTokenJwtVendor).when(tokenManager).createJwtVendor(settings); tokenManager.onDynamicConfigModelChanged(dcm); return dcm; } @@ -217,7 +225,7 @@ public void issueOnBehalfOfToken_jwtGenerationFailure() throws Exception { createMockJwtVendorInTokenManager(); - when(oboJwtVendor.createJwt(any(), anyString(), anyString(), anyLong(), any(), any(), anyBoolean())).thenThrow( + when(oboJwtVendor.createOBOJwt(any(), anyString(), anyString(), anyLong(), any(), any(), anyBoolean())).thenThrow( new RuntimeException("foobar") ); final OpenSearchSecurityException exception = assertThrows( @@ -244,7 +252,7 @@ public void issueOnBehalfOfToken_success() throws Exception { createMockJwtVendorInTokenManager(); final ExpiringBearerAuthToken authToken = mock(ExpiringBearerAuthToken.class); - when(oboJwtVendor.createJwt(any(), anyString(), anyString(), anyLong(), any(), any(), anyBoolean())).thenReturn(authToken); + when(oboJwtVendor.createOBOJwt(any(), anyString(), anyString(), anyLong(), any(), any(), anyBoolean())).thenReturn(authToken); final AuthToken returnedToken = tokenManager.issueOnBehalfOfToken(null, new OnBehalfOfClaims("elmo", 450L)); assertThat(returnedToken, equalTo(authToken)); @@ -262,10 +270,10 @@ public void issueApiToken_success() throws Exception { final ConfigModel configModel = mock(ConfigModel.class); tokenManager.onConfigModelChanged(configModel); - createMockJwtVendorInTokenManager(); + createMockApiTokenJwtVendorInTokenManager(); final ExpiringBearerAuthToken authToken = mock(ExpiringBearerAuthToken.class); - when(apiTokenJwtVendor.createJwt(anyString(), anyString(), anyString(), anyLong())).thenReturn(authToken); + when(apiTokenJwtVendor.createApiTokenJwt(anyString(), anyString(), anyString(), anyLong())).thenReturn(authToken); final AuthToken returnedToken = tokenManager.issueApiToken("elmo", Long.MAX_VALUE); assertThat(returnedToken, equalTo(authToken)); diff --git a/src/test/java/org/opensearch/security/privileges/SystemIndexAccessEvaluatorTest.java b/src/test/java/org/opensearch/security/privileges/SystemIndexAccessEvaluatorTest.java index ad29af1273..c1f8ff79c7 100644 --- a/src/test/java/org/opensearch/security/privileges/SystemIndexAccessEvaluatorTest.java +++ b/src/test/java/org/opensearch/security/privileges/SystemIndexAccessEvaluatorTest.java @@ -32,7 +32,7 @@ import org.opensearch.cluster.service.ClusterService; import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; -import org.opensearch.security.action.apitokens.ApiTokenRepository; +import org.opensearch.security.action.apitokens.Permissions; import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.resolver.IndexResolverReplacer; import org.opensearch.security.resolver.IndexResolverReplacer.Resolved; @@ -165,7 +165,7 @@ PrivilegesEvaluationContext ctx(String action) { null, indexNameExpressionResolver, null, - mock(ApiTokenRepository.class) + new Permissions() ); } From 6ffc060e8176ed666b749983304ab20e76de735d Mon Sep 17 00:00:00 2001 From: Derek Ho Date: Tue, 21 Jan 2025 15:08:07 -0500 Subject: [PATCH 26/30] Mock api tokenrepository behavior Signed-off-by: Derek Ho --- .../privileges/RestLayerPrivilegesEvaluatorTest.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java b/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java index 9fdba2b407..c713d8a286 100644 --- a/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java +++ b/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java @@ -32,6 +32,7 @@ import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.security.action.apitokens.ApiTokenRepository; +import org.opensearch.security.action.apitokens.Permissions; import org.opensearch.security.auditlog.NullAuditLog; import org.opensearch.security.securityconf.ConfigModel; import org.opensearch.security.securityconf.DynamicConfigModel; @@ -40,6 +41,7 @@ import org.opensearch.security.securityconf.impl.v7.RoleV7; import org.opensearch.security.user.User; +import org.mockito.ArgumentMatchers; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.mockito.quality.Strictness; @@ -149,6 +151,8 @@ public void testEvaluate_Unsuccessful() throws Exception { } PrivilegesEvaluator createPrivilegesEvaluator(SecurityDynamicConfiguration roles) { + ApiTokenRepository mockApiTokenRepository = mock(ApiTokenRepository.class); + when(mockApiTokenRepository.getApiTokenPermissionsForUser(ArgumentMatchers.any())).thenReturn(new Permissions()); PrivilegesEvaluator privilegesEvaluator = new PrivilegesEvaluator( clusterService, () -> clusterService.state(), @@ -162,7 +166,7 @@ PrivilegesEvaluator createPrivilegesEvaluator(SecurityDynamicConfiguration Date: Wed, 22 Jan 2025 16:56:49 -0500 Subject: [PATCH 27/30] Refactor to abstract class Signed-off-by: Derek Ho --- .../privileges/ActionPrivilegesTest.java | 61 +-- .../security/privileges/IndexPatternTest.java | 8 +- .../RestEndpointPermissionTests.java | 19 +- .../dlsfls/DlsFlsLegacyHeadersTest.java | 15 +- .../dlsfls/DocumentPrivilegesTest.java | 36 +- .../privileges/dlsfls/FieldMaskingTest.java | 10 +- .../dlsfls/FieldPrivilegesTest.java | 10 +- .../action/apitokens/ApiTokenAction.java | 3 - .../action/apitokens/ApiTokenRepository.java | 2 - .../security/authtoken/jwt/JwtVendor.java | 90 +---- .../jwt/claims/JwtClaimsBuilder.java | 16 - .../jwt/claims/OBOJwtClaimsBuilder.java | 7 +- .../configuration/DlsFlsValveImpl.java | 26 +- .../SecurityFlsDlsIndexSearcherWrapper.java | 12 +- .../identity/SecurityTokenManager.java | 91 +++-- .../security/privileges/ActionPrivileges.java | 149 ++++---- ...ssionBasedPrivilegesEvaluationContext.java | 71 ++++ .../PrivilegesEvaluationContext.java | 65 +--- .../privileges/PrivilegesEvaluator.java | 24 +- .../RoleBasedPrivilegesEvaluationContext.java | 84 +++++ .../securityconf/DynamicConfigModelV7.java | 5 - .../security/authtoken/jwt/JwtVendorTest.java | 351 +++++++++--------- .../identity/SecurityTokenManagerTest.java | 65 ++-- .../SystemIndexAccessEvaluatorTest.java | 8 +- 24 files changed, 650 insertions(+), 578 deletions(-) create mode 100644 src/main/java/org/opensearch/security/privileges/PermissionBasedPrivilegesEvaluationContext.java create mode 100644 src/main/java/org/opensearch/security/privileges/RoleBasedPrivilegesEvaluationContext.java diff --git a/src/integrationTest/java/org/opensearch/security/privileges/ActionPrivilegesTest.java b/src/integrationTest/java/org/opensearch/security/privileges/ActionPrivilegesTest.java index 0c3100fa88..9e3655d613 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/ActionPrivilegesTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/ActionPrivilegesTest.java @@ -292,7 +292,10 @@ 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 = ctxForApiToken("apitoken:" + token, new Permissions(List.of("*"), List.of())); + PermissionBasedPrivilegesEvaluationContext context = ctxForApiToken( + "apitoken:" + token, + new Permissions(List.of("*"), List.of()) + ); // Explicit fails assertThat( subject.hasExplicitClusterPrivilege(context, "cluster:whatever"), @@ -309,7 +312,7 @@ 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 = ctxForApiToken( + PermissionBasedPrivilegesEvaluationContext context = ctxForApiToken( "apitoken:" + token, new Permissions(List.of("cluster:whatever"), List.of()) ); @@ -335,7 +338,10 @@ 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 = ctxForApiToken("apitoken:" + token, new Permissions(List.of("CLUSTER_ALL"), List.of())); + PermissionBasedPrivilegesEvaluationContext context = ctxForApiToken( + "apitoken:" + token, + new Permissions(List.of("CLUSTER_ALL"), List.of()) + ); // Explicit succeeds assertThat(subject.hasExplicitClusterPrivilege(context, "cluster:whatever"), isAllowed()); // Not explicit succeeds @@ -380,7 +386,7 @@ public void positive_full() throws Exception { @Test public void apiTokens_positive_full() throws Exception { String token = "blah"; - PrivilegesEvaluationContext context = ctxForApiToken( + PermissionBasedPrivilegesEvaluationContext context = ctxForApiToken( "apitoken:" + token, new Permissions(List.of("index_a11"), List.of(new ApiToken.IndexPermission(List.of("index_a11"), List.of("*")))) ); @@ -390,7 +396,7 @@ public void apiTokens_positive_full() throws Exception { @Test public void positive_partial() throws Exception { - PrivilegesEvaluationContext ctx = ctx("test_role"); + RoleBasedPrivilegesEvaluationContext ctx = ctx("test_role"); PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege(ctx, requiredActions, resolved("index_a11", "index_a12")); if (covers(ctx, "index_a11", "index_a12")) { @@ -404,7 +410,7 @@ public void positive_partial() throws Exception { @Test public void positive_partial2() throws Exception { - PrivilegesEvaluationContext ctx = ctx("test_role"); + RoleBasedPrivilegesEvaluationContext ctx = ctx("test_role"); PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege( ctx, requiredActions, @@ -437,7 +443,7 @@ public void positive_noLocal() throws Exception { @Test public void negative_wrongRole() throws Exception { - PrivilegesEvaluationContext ctx = ctx("other_role"); + RoleBasedPrivilegesEvaluationContext ctx = ctx("other_role"); PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege(ctx, requiredActions, resolved("index_a11")); assertThat(result, isForbidden(missingPrivileges(requiredActions))); } @@ -445,7 +451,7 @@ public void negative_wrongRole() throws Exception { @Test public void apiToken_negative_noPermissions() throws Exception { String token = "blah"; - PrivilegesEvaluationContext context = ctxForApiToken( + PermissionBasedPrivilegesEvaluationContext context = ctxForApiToken( "apitoken:" + token, new Permissions(List.of(), List.of(new ApiToken.IndexPermission(List.of(), List.of()))) ); @@ -456,7 +462,7 @@ public void apiToken_negative_noPermissions() throws Exception { @Test public void negative_wrongAction() throws Exception { - PrivilegesEvaluationContext ctx = ctx("test_role"); + RoleBasedPrivilegesEvaluationContext ctx = ctx("test_role"); PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege(ctx, otherActions, resolved("index_a11")); if (actionSpec.givenPrivs.contains("*")) { @@ -468,7 +474,7 @@ public void negative_wrongAction() throws Exception { @Test public void positive_hasExplicit_full() { - PrivilegesEvaluationContext ctx = ctx("test_role"); + RoleBasedPrivilegesEvaluationContext ctx = ctx("test_role"); PrivilegesEvaluatorResponse result = subject.hasExplicitIndexPrivilege(ctx, requiredActions, resolved("index_a11")); if (actionSpec.givenPrivs.contains("*")) { @@ -486,7 +492,7 @@ public void positive_hasExplicit_full() { @Test public void apiTokens_positive_hasExplicit_full() { String token = "blah"; - PrivilegesEvaluationContext context = ctxForApiToken( + PermissionBasedPrivilegesEvaluationContext context = ctxForApiToken( "apitoken:" + token, new Permissions(List.of("index_a11"), List.of(new ApiToken.IndexPermission(List.of("index_a11"), List.of("*")))) ); @@ -497,7 +503,7 @@ public void apiTokens_positive_hasExplicit_full() { } - private boolean covers(PrivilegesEvaluationContext ctx, String... indices) { + private boolean covers(RoleBasedPrivilegesEvaluationContext ctx, String... indices) { for (String index : indices) { if (!indexSpec.covers(ctx.getUser(), index)) { return false; @@ -622,7 +628,7 @@ public static class DataStreams { @Test public void positive_full() throws Exception { - PrivilegesEvaluationContext ctx = ctx("test_role"); + RoleBasedPrivilegesEvaluationContext ctx = ctx("test_role"); PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege(ctx, requiredActions, resolved("data_stream_a11")); if (covers(ctx, "data_stream_a11")) { assertThat(result, isAllowed()); @@ -638,7 +644,7 @@ public void positive_full() throws Exception { @Test public void positive_partial() throws Exception { - PrivilegesEvaluationContext ctx = ctx("test_role"); + RoleBasedPrivilegesEvaluationContext ctx = ctx("test_role"); PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege( ctx, requiredActions, @@ -669,19 +675,19 @@ public void positive_partial() throws Exception { @Test public void negative_wrongRole() throws Exception { - PrivilegesEvaluationContext ctx = ctx("other_role"); + RoleBasedPrivilegesEvaluationContext ctx = ctx("other_role"); PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege(ctx, requiredActions, resolved("data_stream_a11")); assertThat(result, isForbidden(missingPrivileges(requiredActions))); } @Test public void negative_wrongAction() throws Exception { - PrivilegesEvaluationContext ctx = ctx("test_role"); + RoleBasedPrivilegesEvaluationContext ctx = ctx("test_role"); PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege(ctx, otherActions, resolved("data_stream_a11")); assertThat(result, isForbidden(missingPrivileges(otherActions))); } - private boolean covers(PrivilegesEvaluationContext ctx, String... indices) { + private boolean covers(RoleBasedPrivilegesEvaluationContext ctx, String... indices) { for (String index : indices) { if (!indexSpec.covers(ctx.getUser(), index)) { return false; @@ -1139,15 +1145,15 @@ static SecurityDynamicConfiguration createRoles(int numberOfRoles, int n } } - static PrivilegesEvaluationContext ctx(String... roles) { + static RoleBasedPrivilegesEvaluationContext ctx(String... roles) { return ctxWithUserName("test-user", roles); } - static PrivilegesEvaluationContext ctxWithUserName(String userName, String... roles) { + static RoleBasedPrivilegesEvaluationContext 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( + return new RoleBasedPrivilegesEvaluationContext( user, ImmutableSet.copyOf(roles), null, @@ -1155,17 +1161,15 @@ static PrivilegesEvaluationContext ctxWithUserName(String userName, String... ro null, null, new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), - null, - new Permissions() + null ); } - static PrivilegesEvaluationContext ctxForApiToken(String userName, Permissions permissions) { + static PermissionBasedPrivilegesEvaluationContext ctxForApiToken(String userName, Permissions permissions) { User user = new User(userName); user.addAttributes(ImmutableMap.of("attrs.dept_no", "a11")); - return new PrivilegesEvaluationContext( + return new PermissionBasedPrivilegesEvaluationContext( user, - ImmutableSet.of(), null, null, null, @@ -1176,10 +1180,10 @@ static PrivilegesEvaluationContext ctxForApiToken(String userName, Permissions p ); } - static PrivilegesEvaluationContext ctxByUsername(String username) { + static RoleBasedPrivilegesEvaluationContext ctxByUsername(String username) { User user = new User(username); user.addAttributes(ImmutableMap.of("attrs.dept_no", "a11")); - return new PrivilegesEvaluationContext( + return new RoleBasedPrivilegesEvaluationContext( user, ImmutableSet.of(), null, @@ -1187,8 +1191,7 @@ static PrivilegesEvaluationContext ctxByUsername(String username) { null, null, new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), - null, - new Permissions() + null ); } } diff --git a/src/integrationTest/java/org/opensearch/security/privileges/IndexPatternTest.java b/src/integrationTest/java/org/opensearch/security/privileges/IndexPatternTest.java index ddf0c86562..246d28d542 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/IndexPatternTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/IndexPatternTest.java @@ -22,7 +22,6 @@ import org.opensearch.cluster.metadata.Metadata; import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; -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; @@ -232,14 +231,14 @@ public void equals() { assertFalse(a1.equals(a1.toString())); } - private static PrivilegesEvaluationContext ctx() { + private static RoleBasedPrivilegesEvaluationContext ctx() { IndexNameExpressionResolver indexNameExpressionResolver = new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)); IndexResolverReplacer indexResolverReplacer = new IndexResolverReplacer(indexNameExpressionResolver, () -> CLUSTER_STATE, null); User user = new User("test_user"); user.addAttributes(ImmutableMap.of("attrs.a11", "a11")); user.addAttributes(ImmutableMap.of("attrs.year", "year")); - return new PrivilegesEvaluationContext( + return new RoleBasedPrivilegesEvaluationContext( user, ImmutableSet.of(), "indices:action/test", @@ -247,8 +246,7 @@ private static PrivilegesEvaluationContext ctx() { null, indexResolverReplacer, indexNameExpressionResolver, - () -> CLUSTER_STATE, - new Permissions() + () -> CLUSTER_STATE ); } } diff --git a/src/integrationTest/java/org/opensearch/security/privileges/RestEndpointPermissionTests.java b/src/integrationTest/java/org/opensearch/security/privileges/RestEndpointPermissionTests.java index fa4614e992..6576a84fb4 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/RestEndpointPermissionTests.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/RestEndpointPermissionTests.java @@ -44,7 +44,6 @@ import org.opensearch.common.settings.Settings; import org.opensearch.security.DefaultObjectMapper; -import org.opensearch.security.action.apitokens.Permissions; import org.opensearch.security.dlic.rest.api.Endpoint; import org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.PermissionBuilder; import org.opensearch.security.securityconf.FlattenedActionGroups; @@ -189,13 +188,13 @@ public void hasExplicitClusterPermissionPermissionForRestAdmin() { .collect(Collectors.toList()); for (final Endpoint endpoint : noSslEndpoints) { final String permission = ENDPOINTS_WITH_PERMISSIONS.get(endpoint).build(); - final PrivilegesEvaluationContext ctx = ctx(restAdminApiRoleName(endpoint.name().toLowerCase(Locale.ROOT))); + final RoleBasedPrivilegesEvaluationContext ctx = ctx(restAdminApiRoleName(endpoint.name().toLowerCase(Locale.ROOT))); Assert.assertTrue(endpoint.name(), actionPrivileges.hasExplicitClusterPrivilege(ctx, permission).isAllowed()); assertHasNoPermissionsForRestApiAdminOnePermissionRole(endpoint, ctx); } // verify SSL endpoint with 2 actions for (final String sslAction : ImmutableSet.of(CERTS_INFO_ACTION, RELOAD_CERTS_ACTION)) { - final PrivilegesEvaluationContext ctx = ctx(restAdminApiRoleName(sslAction)); + final RoleBasedPrivilegesEvaluationContext ctx = ctx(restAdminApiRoleName(sslAction)); final PermissionBuilder permissionBuilder = ENDPOINTS_WITH_PERMISSIONS.get(Endpoint.SSL); Assert.assertTrue( Endpoint.SSL + "/" + sslAction, @@ -204,7 +203,7 @@ public void hasExplicitClusterPermissionPermissionForRestAdmin() { assertHasNoPermissionsForRestApiAdminOnePermissionRole(Endpoint.SSL, ctx); } // verify CONFIG endpoint with 1 action - final PrivilegesEvaluationContext ctx = ctx(restAdminApiRoleName(SECURITY_CONFIG_UPDATE)); + final RoleBasedPrivilegesEvaluationContext ctx = ctx(restAdminApiRoleName(SECURITY_CONFIG_UPDATE)); final PermissionBuilder permissionBuilder = ENDPOINTS_WITH_PERMISSIONS.get(Endpoint.CONFIG); Assert.assertTrue( Endpoint.SSL + "/" + SECURITY_CONFIG_UPDATE, @@ -213,7 +212,10 @@ public void hasExplicitClusterPermissionPermissionForRestAdmin() { assertHasNoPermissionsForRestApiAdminOnePermissionRole(Endpoint.CONFIG, ctx); } - void assertHasNoPermissionsForRestApiAdminOnePermissionRole(final Endpoint allowEndpoint, final PrivilegesEvaluationContext ctx) { + void assertHasNoPermissionsForRestApiAdminOnePermissionRole( + final Endpoint allowEndpoint, + final RoleBasedPrivilegesEvaluationContext ctx + ) { final Collection noPermissionEndpoints = ENDPOINTS_WITH_PERMISSIONS.keySet() .stream() .filter(e -> e != allowEndpoint) @@ -251,8 +253,8 @@ static SecurityDynamicConfiguration createRolesConfig() throws IOExcepti return SecurityDynamicConfiguration.fromNode(rolesNode, CType.ROLES, 2, 0, 0); } - static PrivilegesEvaluationContext ctx(String... roles) { - return new PrivilegesEvaluationContext( + static RoleBasedPrivilegesEvaluationContext ctx(String... roles) { + return new RoleBasedPrivilegesEvaluationContext( new User("test_user"), ImmutableSet.copyOf(roles), null, @@ -260,8 +262,7 @@ static PrivilegesEvaluationContext ctx(String... roles) { null, null, null, - null, - new Permissions() + null ); } 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 66d3c99622..65b2f30b3a 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/DlsFlsLegacyHeadersTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/DlsFlsLegacyHeadersTest.java @@ -35,8 +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.Permissions; -import org.opensearch.security.privileges.PrivilegesEvaluationContext; +import org.opensearch.security.privileges.RoleBasedPrivilegesEvaluationContext; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; import org.opensearch.security.securityconf.impl.v7.RoleV7; import org.opensearch.security.support.Base64Helper; @@ -339,7 +338,7 @@ public void prepare_ccs() throws Exception { User user = new User("test_user"); ClusterState clusterState = ClusterState.builder(ClusterState.EMPTY_STATE).metadata(metadata).build(); - PrivilegesEvaluationContext ctx = new PrivilegesEvaluationContext( + RoleBasedPrivilegesEvaluationContext ctx = new RoleBasedPrivilegesEvaluationContext( user, ImmutableSet.of("test_role"), null, @@ -347,19 +346,18 @@ public void prepare_ccs() throws Exception { null, null, new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), - () -> clusterState, - new Permissions() + () -> clusterState ); DlsFlsLegacyHeaders.prepare(threadContext, ctx, dlsFlsProcessedConfig(exampleRolesConfig(), metadata), metadata, false); assertTrue(threadContext.getResponseHeaders().containsKey(ConfigConstants.OPENDISTRO_SECURITY_DLS_QUERY_HEADER)); } - static PrivilegesEvaluationContext ctx(Metadata metadata, String... roles) { + static RoleBasedPrivilegesEvaluationContext ctx(Metadata metadata, String... roles) { User user = new User("test_user"); ClusterState clusterState = ClusterState.builder(ClusterState.EMPTY_STATE).metadata(metadata).build(); - return new PrivilegesEvaluationContext( + return new RoleBasedPrivilegesEvaluationContext( user, ImmutableSet.copyOf(roles), null, @@ -367,8 +365,7 @@ static PrivilegesEvaluationContext ctx(Metadata metadata, String... roles) { null, null, new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), - () -> clusterState, - new Permissions() + () -> clusterState ); } 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 aff090e390..f81fe39d5d 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/DocumentPrivilegesTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/DocumentPrivilegesTest.java @@ -51,10 +51,9 @@ import org.opensearch.index.query.QueryBuilder; import org.opensearch.index.query.QueryBuilders; import org.opensearch.index.query.TermQueryBuilder; -import org.opensearch.security.action.apitokens.Permissions; import org.opensearch.security.privileges.PrivilegesConfigurationValidationException; -import org.opensearch.security.privileges.PrivilegesEvaluationContext; import org.opensearch.security.privileges.PrivilegesEvaluationException; +import org.opensearch.security.privileges.RoleBasedPrivilegesEvaluationContext; import org.opensearch.security.resolver.IndexResolverReplacer; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; import org.opensearch.security.securityconf.impl.v7.RoleV7; @@ -122,7 +121,7 @@ public static class IndicesAndAliases_getRestriction { final User user; final IndexSpec indexSpec; final IndexAbstraction.Index index; - final PrivilegesEvaluationContext context; + final RoleBasedPrivilegesEvaluationContext context; final boolean dfmEmptyOverridesAll; @Test @@ -519,7 +518,7 @@ public IndicesAndAliases_getRestriction( this.indexSpec = indexSpec; this.user = userSpec.buildUser(); this.index = (IndexAbstraction.Index) INDEX_METADATA.getIndicesLookup().get(indexSpec.index); - this.context = new PrivilegesEvaluationContext( + this.context = new RoleBasedPrivilegesEvaluationContext( this.user, ImmutableSet.copyOf(userSpec.roles), null, @@ -527,8 +526,7 @@ public IndicesAndAliases_getRestriction( null, null, null, - () -> CLUSTER_STATE, - new Permissions() + () -> CLUSTER_STATE ); this.statefulness = statefulness; this.dfmEmptyOverridesAll = dfmEmptyOverridesAll == DfmEmptyOverridesAll.DFM_EMPTY_OVERRIDES_ALL_TRUE; @@ -568,7 +566,7 @@ public static class IndicesAndAliases_isUnrestricted { final User user; final IndicesSpec indicesSpec; final IndexResolverReplacer.Resolved resolvedIndices; - final PrivilegesEvaluationContext context; + final RoleBasedPrivilegesEvaluationContext context; final boolean dfmEmptyOverridesAll; @Test @@ -835,7 +833,7 @@ public IndicesRequest indices(String... strings) { return this; } }); - this.context = new PrivilegesEvaluationContext( + this.context = new RoleBasedPrivilegesEvaluationContext( this.user, ImmutableSet.copyOf(userSpec.roles), null, @@ -843,8 +841,7 @@ public IndicesRequest indices(String... strings) { null, RESOLVER_REPLACER, INDEX_NAME_EXPRESSION_RESOLVER, - () -> CLUSTER_STATE, - new Permissions() + () -> CLUSTER_STATE ); this.statefulness = statefulness; this.dfmEmptyOverridesAll = dfmEmptyOverridesAll == DfmEmptyOverridesAll.DFM_EMPTY_OVERRIDES_ALL_TRUE; @@ -877,7 +874,7 @@ public static class DataStreams_getRestriction { final User user; final IndexSpec indexSpec; final IndexAbstraction.Index index; - final PrivilegesEvaluationContext context; + final RoleBasedPrivilegesEvaluationContext context; final boolean dfmEmptyOverridesAll; @Test @@ -1121,7 +1118,7 @@ public DataStreams_getRestriction( this.indexSpec = indexSpec; this.user = userSpec.buildUser(); this.index = (IndexAbstraction.Index) INDEX_METADATA.getIndicesLookup().get(indexSpec.index); - this.context = new PrivilegesEvaluationContext( + this.context = new RoleBasedPrivilegesEvaluationContext( this.user, ImmutableSet.copyOf(userSpec.roles), null, @@ -1129,8 +1126,7 @@ public DataStreams_getRestriction( null, null, null, - () -> CLUSTER_STATE, - new Permissions() + () -> CLUSTER_STATE ); this.statefulness = statefulness; this.dfmEmptyOverridesAll = dfmEmptyOverridesAll == DfmEmptyOverridesAll.DFM_EMPTY_OVERRIDES_ALL_TRUE; @@ -1151,17 +1147,7 @@ public void invalidQuery() throws Exception { 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, - new Permissions() - ) + new RoleBasedPrivilegesEvaluationContext(new User("test_user"), ImmutableSet.of(), null, null, null, null, null, null) ); } diff --git a/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/FieldMaskingTest.java b/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/FieldMaskingTest.java index d7eea419e4..97768a6faa 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/FieldMaskingTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/FieldMaskingTest.java @@ -22,9 +22,8 @@ import org.opensearch.cluster.ClusterState; import org.opensearch.cluster.metadata.Metadata; import org.opensearch.common.settings.Settings; -import org.opensearch.security.action.apitokens.Permissions; import org.opensearch.security.privileges.PrivilegesConfigurationValidationException; -import org.opensearch.security.privileges.PrivilegesEvaluationContext; +import org.opensearch.security.privileges.RoleBasedPrivilegesEvaluationContext; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; import org.opensearch.security.securityconf.impl.v7.RoleV7; import org.opensearch.security.support.WildcardMatcher; @@ -115,8 +114,8 @@ static FieldMasking createSubject(SecurityDynamicConfiguration roleConfi ); } - static PrivilegesEvaluationContext ctx(String... roles) { - return new PrivilegesEvaluationContext( + static RoleBasedPrivilegesEvaluationContext ctx(String... roles) { + return new RoleBasedPrivilegesEvaluationContext( new User("test_user"), ImmutableSet.copyOf(roles), null, @@ -124,8 +123,7 @@ static PrivilegesEvaluationContext ctx(String... roles) { null, null, null, - () -> CLUSTER_STATE, - new Permissions() + () -> CLUSTER_STATE ); } } 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 66bf7db073..731c910fc8 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/FieldPrivilegesTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/FieldPrivilegesTest.java @@ -21,9 +21,8 @@ import org.opensearch.cluster.ClusterState; import org.opensearch.cluster.metadata.Metadata; import org.opensearch.common.settings.Settings; -import org.opensearch.security.action.apitokens.Permissions; import org.opensearch.security.privileges.PrivilegesConfigurationValidationException; -import org.opensearch.security.privileges.PrivilegesEvaluationContext; +import org.opensearch.security.privileges.RoleBasedPrivilegesEvaluationContext; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; import org.opensearch.security.securityconf.impl.v7.RoleV7; import org.opensearch.security.support.WildcardMatcher; @@ -150,8 +149,8 @@ static FieldPrivileges createSubject(SecurityDynamicConfiguration roleCo ); } - static PrivilegesEvaluationContext ctx(String... roles) { - return new PrivilegesEvaluationContext( + static RoleBasedPrivilegesEvaluationContext ctx(String... roles) { + return new RoleBasedPrivilegesEvaluationContext( new User("test_user"), ImmutableSet.copyOf(roles), null, @@ -159,8 +158,7 @@ static PrivilegesEvaluationContext ctx(String... roles) { null, null, null, - () -> CLUSTER_STATE, - new Permissions() + () -> CLUSTER_STATE ); } } 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 26116714ac..0b6a7b320a 100644 --- a/src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java +++ b/src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java @@ -24,7 +24,6 @@ import org.apache.logging.log4j.Logger; import org.opensearch.client.node.NodeClient; -import org.opensearch.common.inject.Inject; import org.opensearch.core.action.ActionListener; import org.opensearch.core.rest.RestStatus; import org.opensearch.core.xcontent.XContentBuilder; @@ -60,10 +59,8 @@ public class ApiTokenAction extends BaseRestHandler { ) ); - @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 ad131a59a9..b1f99bdbd6 100644 --- a/src/main/java/org/opensearch/security/action/apitokens/ApiTokenRepository.java +++ b/src/main/java/org/opensearch/security/action/apitokens/ApiTokenRepository.java @@ -21,7 +21,6 @@ import org.opensearch.client.Client; import org.opensearch.cluster.service.ClusterService; -import org.opensearch.common.inject.Inject; import org.opensearch.index.IndexNotFoundException; import org.opensearch.security.authtoken.jwt.ExpiringBearerAuthToken; import org.opensearch.security.identity.SecurityTokenManager; @@ -68,7 +67,6 @@ public Map getJtis() { return jtis; } - @Inject public ApiTokenRepository(Client client, ClusterService clusterService, SecurityTokenManager tokenManager) { apiTokenIndexHandler = new ApiTokenIndexHandler(client, clusterService); securityTokenManager = tokenManager; 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 c1ede9d9f3..a522056812 100644 --- a/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java +++ b/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java @@ -11,14 +11,9 @@ 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; @@ -26,8 +21,7 @@ import org.opensearch.OpenSearchException; import org.opensearch.common.collect.Tuple; import org.opensearch.common.settings.Settings; -import org.opensearch.security.authtoken.jwt.claims.ApiJwtClaimsBuilder; -import org.opensearch.security.authtoken.jwt.claims.OBOJwtClaimsBuilder; +import org.opensearch.security.authtoken.jwt.claims.JwtClaimsBuilder; import com.nimbusds.jose.JOSEException; import com.nimbusds.jose.JWSAlgorithm; @@ -47,16 +41,11 @@ public class JwtVendor { private final JWK signingKey; private final JWSSigner signer; - private final LongSupplier timeProvider; - private static final Integer MAX_EXPIRY_SECONDS = 600; - private final Settings settings; - public JwtVendor(final Settings settings, final Optional timeProvider) { + public JwtVendor(Settings settings) { final Tuple tuple = createJwkFromSettings(settings); signingKey = tuple.v1(); signer = tuple.v2(); - this.settings = settings; - this.timeProvider = timeProvider.orElse(System::currentTimeMillis); } /* @@ -94,41 +83,8 @@ static Tuple createJwkFromSettings(final Settings settings) { } } - public ExpiringBearerAuthToken createOBOJwt( - 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 long expirySeconds = Math.min(requestedExpirySeconds, MAX_EXPIRY_SECONDS); - if (expirySeconds <= 0) { - throw new IllegalArgumentException("The expiration time should be a positive integer"); - } - if (roles == null) { - throw new IllegalArgumentException("Roles cannot be null"); - } - if (isKeyNull(settings, "encryption_key")) { - throw new IllegalArgumentException("encryption_key cannot be null"); - } - - final OBOJwtClaimsBuilder claimsBuilder = new OBOJwtClaimsBuilder(settings.get("encryption_key")); - // Add obo claims - claimsBuilder.issuer(issuer); - claimsBuilder.issueTime(now); - claimsBuilder.subject(subject); - claimsBuilder.audience(audience); - claimsBuilder.notBeforeTime(now); - claimsBuilder.addBackendRoles(includeBackendRoles, backendRoles); - claimsBuilder.addRoles(roles); - - final Date expiryTime = new Date(currentTimeMs + expirySeconds * 1000); - claimsBuilder.expirationTime(expiryTime); + public ExpiringBearerAuthToken createJwt(JwtClaimsBuilder claimsBuilder, String subject, Date expiryTime, Long expirySeconds) + throws JOSEException, ParseException { final JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.parse(signingKey.getAlgorithm().getName())).build(); final SignedJWT signedJwt = new SignedJWT(header, claimsBuilder.build()); @@ -144,42 +100,4 @@ public ExpiringBearerAuthToken createOBOJwt( return new ExpiringBearerAuthToken(signedJwt.serialize(), subject, expiryTime, expirySeconds); } - - @SuppressWarnings("removal") - public ExpiringBearerAuthToken createApiTokenJwt( - 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 ApiJwtClaimsBuilder claimsBuilder = new ApiJwtClaimsBuilder(); - 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/claims/JwtClaimsBuilder.java b/src/main/java/org/opensearch/security/authtoken/jwt/claims/JwtClaimsBuilder.java index 32920296b7..2112606b54 100644 --- a/src/main/java/org/opensearch/security/authtoken/jwt/claims/JwtClaimsBuilder.java +++ b/src/main/java/org/opensearch/security/authtoken/jwt/claims/JwtClaimsBuilder.java @@ -13,23 +13,15 @@ import java.util.Date; -import org.opensearch.security.authtoken.jwt.EncryptionDecryptionUtil; - import com.nimbusds.jwt.JWTClaimsSet; public class JwtClaimsBuilder { private final JWTClaimsSet.Builder builder; - private EncryptionDecryptionUtil encryptionDecryptionUtil; public JwtClaimsBuilder() { this.builder = new JWTClaimsSet.Builder(); } - public JwtClaimsBuilder encryptionKey(String encryptionKey) { - this.encryptionDecryptionUtil = new EncryptionDecryptionUtil(encryptionKey); - return this; - } - public JwtClaimsBuilder issueTime(Date issueTime) { builder.issueTime(issueTime); return this; @@ -65,14 +57,6 @@ public JwtClaimsBuilder expirationTime(Date expirationTime) { return this; } - public JwtClaimsBuilder addCustomClaimWithEncryption(String claimName, String value) { - if (this.encryptionDecryptionUtil == null) { - throw new IllegalArgumentException("Cannot add encrypted field without encryption key"); - } - builder.claim(claimName, encryptionDecryptionUtil.encrypt(value)); - return this; - } - public JwtClaimsBuilder addCustomClaim(String claimName, String value) { builder.claim(claimName, value); return this; diff --git a/src/main/java/org/opensearch/security/authtoken/jwt/claims/OBOJwtClaimsBuilder.java b/src/main/java/org/opensearch/security/authtoken/jwt/claims/OBOJwtClaimsBuilder.java index 90d0bcc82e..22044a165d 100644 --- a/src/main/java/org/opensearch/security/authtoken/jwt/claims/OBOJwtClaimsBuilder.java +++ b/src/main/java/org/opensearch/security/authtoken/jwt/claims/OBOJwtClaimsBuilder.java @@ -13,16 +13,19 @@ import java.util.List; +import org.opensearch.security.authtoken.jwt.EncryptionDecryptionUtil; + public class OBOJwtClaimsBuilder extends JwtClaimsBuilder { + private final EncryptionDecryptionUtil encryptionDecryptionUtil; public OBOJwtClaimsBuilder(String encryptionKey) { super(); - this.encryptionKey(encryptionKey); + this.encryptionDecryptionUtil = new EncryptionDecryptionUtil(encryptionKey); } public OBOJwtClaimsBuilder addRoles(List roles) { final String listOfRoles = String.join(",", roles); - this.addCustomClaimWithEncryption("er", listOfRoles); + this.addCustomClaim("er", encryptionDecryptionUtil.encrypt(listOfRoles)); return this; } diff --git a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java index 498b908e5d..880a828f26 100644 --- a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java +++ b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java @@ -373,14 +373,14 @@ public void handleSearchContext(SearchContext searchContext, ThreadPool threadPo return; } - PrivilegesEvaluationContext privilegesEvaluationContext = this.dlsFlsBaseContext.getPrivilegesEvaluationContext(); - if (privilegesEvaluationContext == null) { + PrivilegesEvaluationContext PrivilegesEvaluationContext = this.dlsFlsBaseContext.getPrivilegesEvaluationContext(); + if (PrivilegesEvaluationContext == null) { return; } DlsFlsProcessedConfig config = this.dlsFlsProcessedConfig.get(); - DlsRestriction dlsRestriction = config.getDocumentPrivileges().getRestriction(privilegesEvaluationContext, index); + DlsRestriction dlsRestriction = config.getDocumentPrivileges().getRestriction(PrivilegesEvaluationContext, index); if (log.isTraceEnabled()) { log.trace("handleSearchContext(); index: {}; dlsRestriction: {}", index, dlsRestriction); @@ -449,36 +449,36 @@ public DlsFlsProcessedConfig getCurrentConfig() { @Override public boolean hasFlsOrFieldMasking(String index) throws PrivilegesEvaluationException { - PrivilegesEvaluationContext privilegesEvaluationContext = this.dlsFlsBaseContext.getPrivilegesEvaluationContext(); - if (privilegesEvaluationContext == null) { + PrivilegesEvaluationContext PrivilegesEvaluationContext = this.dlsFlsBaseContext.getPrivilegesEvaluationContext(); + if (PrivilegesEvaluationContext == null) { return false; } DlsFlsProcessedConfig config = this.dlsFlsProcessedConfig.get(); - return !config.getFieldPrivileges().isUnrestricted(privilegesEvaluationContext, index) - || !config.getFieldMasking().isUnrestricted(privilegesEvaluationContext, index); + return !config.getFieldPrivileges().isUnrestricted(PrivilegesEvaluationContext, index) + || !config.getFieldMasking().isUnrestricted(PrivilegesEvaluationContext, index); } @Override public boolean hasFieldMasking(String index) throws PrivilegesEvaluationException { - PrivilegesEvaluationContext privilegesEvaluationContext = this.dlsFlsBaseContext.getPrivilegesEvaluationContext(); - if (privilegesEvaluationContext == null) { + PrivilegesEvaluationContext PrivilegesEvaluationContext = this.dlsFlsBaseContext.getPrivilegesEvaluationContext(); + if (PrivilegesEvaluationContext == null) { return false; } DlsFlsProcessedConfig config = this.dlsFlsProcessedConfig.get(); - return !config.getFieldMasking().isUnrestricted(privilegesEvaluationContext, index); + return !config.getFieldMasking().isUnrestricted(PrivilegesEvaluationContext, index); } @Override public boolean isFieldAllowed(String index, String field) throws PrivilegesEvaluationException { - PrivilegesEvaluationContext privilegesEvaluationContext = this.dlsFlsBaseContext.getPrivilegesEvaluationContext(); - if (privilegesEvaluationContext == null) { + PrivilegesEvaluationContext PrivilegesEvaluationContext = this.dlsFlsBaseContext.getPrivilegesEvaluationContext(); + if (PrivilegesEvaluationContext == null) { return true; } DlsFlsProcessedConfig config = this.dlsFlsProcessedConfig.get(); - return config.getFieldPrivileges().getRestriction(privilegesEvaluationContext, index).isAllowed(field); + return config.getFieldPrivileges().getRestriction(PrivilegesEvaluationContext, index).isAllowed(field); } private static InternalAggregation aggregateBuckets(InternalAggregation aggregation) { diff --git a/src/main/java/org/opensearch/security/configuration/SecurityFlsDlsIndexSearcherWrapper.java b/src/main/java/org/opensearch/security/configuration/SecurityFlsDlsIndexSearcherWrapper.java index 4f7a412097..8a258a1307 100644 --- a/src/main/java/org/opensearch/security/configuration/SecurityFlsDlsIndexSearcherWrapper.java +++ b/src/main/java/org/opensearch/security/configuration/SecurityFlsDlsIndexSearcherWrapper.java @@ -110,13 +110,13 @@ public SecurityFlsDlsIndexSearcherWrapper( protected DirectoryReader dlsFlsWrap(final DirectoryReader reader, boolean isAdmin) throws IOException { final ShardId shardId = ShardUtils.extractShardId(reader); - PrivilegesEvaluationContext privilegesEvaluationContext = this.dlsFlsBaseContext.getPrivilegesEvaluationContext(); + PrivilegesEvaluationContext PrivilegesEvaluationContext = this.dlsFlsBaseContext.getPrivilegesEvaluationContext(); if (log.isTraceEnabled()) { - log.trace("dlsFlsWrap(); index: {}; privilegeEvaluationContext: {}", index.getName(), privilegesEvaluationContext); + log.trace("dlsFlsWrap(); index: {}; privilegeEvaluationContext: {}", index.getName(), PrivilegesEvaluationContext); } - if (isAdmin || privilegesEvaluationContext == null) { + if (isAdmin || PrivilegesEvaluationContext == null) { return new DlsFlsFilterLeafReader.DlsFlsDirectoryReader( reader, FieldPrivileges.FlsRule.ALLOW_ALL, @@ -137,13 +137,13 @@ protected DirectoryReader dlsFlsWrap(final DirectoryReader reader, boolean isAdm DlsRestriction dlsRestriction; if (!this.dlsFlsBaseContext.isDlsDoneOnFilterLevel()) { - dlsRestriction = config.getDocumentPrivileges().getRestriction(privilegesEvaluationContext, index.getName()); + dlsRestriction = config.getDocumentPrivileges().getRestriction(PrivilegesEvaluationContext, index.getName()); } else { dlsRestriction = DlsRestriction.NONE; } - FieldPrivileges.FlsRule flsRule = config.getFieldPrivileges().getRestriction(privilegesEvaluationContext, index.getName()); - FieldMasking.FieldMaskingRule fmRule = config.getFieldMasking().getRestriction(privilegesEvaluationContext, index.getName()); + FieldPrivileges.FlsRule flsRule = config.getFieldPrivileges().getRestriction(PrivilegesEvaluationContext, index.getName()); + FieldMasking.FieldMaskingRule fmRule = config.getFieldMasking().getRestriction(PrivilegesEvaluationContext, index.getName()); Query dlsQuery; diff --git a/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java b/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java index 3489780a1b..5a43365aee 100644 --- a/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java +++ b/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java @@ -11,9 +11,12 @@ package org.opensearch.security.identity; +import java.time.Duration; +import java.time.Instant; import java.util.ArrayList; -import java.util.Optional; +import java.util.Date; import java.util.Set; +import java.util.function.LongSupplier; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -29,6 +32,8 @@ import org.opensearch.identity.tokens.TokenManager; import org.opensearch.security.authtoken.jwt.ExpiringBearerAuthToken; import org.opensearch.security.authtoken.jwt.JwtVendor; +import org.opensearch.security.authtoken.jwt.claims.ApiJwtClaimsBuilder; +import org.opensearch.security.authtoken.jwt.claims.OBOJwtClaimsBuilder; import org.opensearch.security.securityconf.ConfigModel; import org.opensearch.security.securityconf.DynamicConfigModel; import org.opensearch.security.support.ConfigConstants; @@ -39,6 +44,8 @@ import joptsimple.internal.Strings; import org.greenrobot.eventbus.Subscribe; +import static org.opensearch.security.util.AuthTokenUtils.isKeyNull; + /** * This class is the Security Plugin's implementation of the TokenManager used by all Identity Plugins. * It handles the issuance of both Service Account Tokens and On Behalf Of tokens. @@ -50,9 +57,11 @@ public class SecurityTokenManager implements TokenManager { private final ThreadPool threadPool; private final UserService userService; - private JwtVendor oboJwtVendor = null; - private JwtVendor apiTokenJwtVendor = null; + private Settings oboSettings = null; + private Settings apiTokenSettings = null; private ConfigModel configModel = null; + private final LongSupplier timeProvider = System::currentTimeMillis; + private static final Integer OBO_MAX_EXPIRY_SECONDS = 600; public SecurityTokenManager(final ClusterService cs, final ThreadPool threadPool, final UserService userService) { this.cs = cs; @@ -67,22 +76,22 @@ public void onConfigModelChanged(final ConfigModel configModel) { @Subscribe public void onDynamicConfigModelChanged(final DynamicConfigModel dcm) { - final Settings oboSettings = dcm.getDynamicOnBehalfOfSettings(); - final Boolean oboEnabled = oboSettings.getAsBoolean("enabled", false); + final Settings oboSettingsFromDcm = dcm.getDynamicOnBehalfOfSettings(); + final Boolean oboEnabled = oboSettingsFromDcm.getAsBoolean("enabled", false); if (oboEnabled) { - oboJwtVendor = createJwtVendor(oboSettings); + oboSettings = oboSettingsFromDcm; } - final Settings apiTokenSettings = dcm.getDynamicApiTokenSettings(); - final Boolean apiTokenEnabled = apiTokenSettings.getAsBoolean("enabled", false); + final Settings apiTokenSettingsFromDcm = dcm.getDynamicApiTokenSettings(); + final Boolean apiTokenEnabled = apiTokenSettingsFromDcm.getAsBoolean("enabled", false); if (apiTokenEnabled) { - apiTokenJwtVendor = createJwtVendor(apiTokenSettings); + apiTokenSettings = apiTokenSettingsFromDcm; } } /** For testing */ JwtVendor createJwtVendor(final Settings settings) { try { - return new JwtVendor(settings, Optional.empty()); + return new JwtVendor(settings); } catch (final Exception ex) { logger.error("Unable to create the JwtVendor instance", ex); return null; @@ -90,11 +99,11 @@ JwtVendor createJwtVendor(final Settings settings) { } public boolean issueOnBehalfOfTokenAllowed() { - return oboJwtVendor != null && configModel != null; + return oboSettings != null && configModel != null; } public boolean issueApiTokenAllowed() { - return apiTokenJwtVendor != null && configModel != null; + return apiTokenSettings != null && configModel != null; } @Override @@ -123,16 +132,36 @@ public ExpiringBearerAuthToken issueOnBehalfOfToken(final Subject subject, final final TransportAddress callerAddress = null; /* OBO tokens must not roles based on location from network address */ final Set mappedRoles = configModel.mapSecurityRoles(user, callerAddress); + final long currentTimeMs = timeProvider.getAsLong(); + final Date now = new Date(currentTimeMs); + + final long expirySeconds = Math.min(claims.getExpiration(), OBO_MAX_EXPIRY_SECONDS); + if (expirySeconds <= 0) { + throw new IllegalArgumentException("The expiration time should be a positive integer"); + } + if (mappedRoles == null) { + throw new IllegalArgumentException("Roles cannot be null"); + } + if (isKeyNull(oboSettings, "encryption_key")) { + throw new IllegalArgumentException("encryption_key cannot be null"); + } + + final OBOJwtClaimsBuilder claimsBuilder = new OBOJwtClaimsBuilder(oboSettings.get("encryption_key")); + + // Add obo claims + claimsBuilder.issuer(cs.getClusterName().toString()); + claimsBuilder.issueTime(now); + claimsBuilder.subject(user.getName()); + claimsBuilder.audience(claims.getAudience()); + claimsBuilder.notBeforeTime(now); + claimsBuilder.addBackendRoles(false, new ArrayList<>(user.getRoles())); + claimsBuilder.addRoles(new ArrayList<>(mappedRoles)); + + final Date expiryTime = new Date(currentTimeMs + expirySeconds * 1000); + claimsBuilder.expirationTime(expiryTime); + try { - return oboJwtVendor.createOBOJwt( - cs.getClusterName().value(), - user.getName(), - claims.getAudience(), - claims.getExpiration(), - new ArrayList<>(mappedRoles), - new ArrayList<>(user.getRoles()), - false - ); + return createJwtVendor(oboSettings).createJwt(claimsBuilder, user.getName(), expiryTime, expirySeconds); } catch (final Exception ex) { logger.error("Error creating OnBehalfOfToken for " + user.getName(), ex); throw new OpenSearchSecurityException("Unable to generate OnBehalfOfToken"); @@ -145,8 +174,26 @@ public ExpiringBearerAuthToken issueApiToken(final String name, final Long expir } final User user = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + final long currentTimeMs = timeProvider.getAsLong(); + final Date now = new Date(currentTimeMs); + + final ApiJwtClaimsBuilder claimsBuilder = new ApiJwtClaimsBuilder(); + claimsBuilder.issuer(cs.getClusterName().toString()); + claimsBuilder.issueTime(now); + claimsBuilder.subject(name); + claimsBuilder.audience(name); + claimsBuilder.notBeforeTime(now); + + final Date expiryTime = new Date(expiration); + claimsBuilder.expirationTime(expiryTime); + try { - return apiTokenJwtVendor.createApiTokenJwt(cs.getClusterName().value(), name, name, expiration); + return createJwtVendor(apiTokenSettings).createJwt( + claimsBuilder, + name, + expiryTime, + Duration.between(Instant.now(), expiryTime.toInstant()).getSeconds() + ); } 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/ActionPrivileges.java b/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java index e34917b1f1..840e2dd411 100644 --- a/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java +++ b/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java @@ -469,34 +469,36 @@ PrivilegesEvaluatorResponse apiTokenProvidesClusterPrivilege( Set actions, Boolean explicit ) { - Permissions permissions = context.getPermissionsForApiToken(); - Set resolvedClusterPermissions = actionGroups.resolve(permissions.getClusterPerm()); + if (context instanceof PermissionBasedPrivilegesEvaluationContext) { + Permissions permissions = ((PermissionBasedPrivilegesEvaluationContext) context).getPermissions(); + Set resolvedClusterPermissions = actionGroups.resolve(permissions.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 exact match + if (!Collections.disjoint(resolvedClusterPermissions, actions)) { + return PrivilegesEvaluatorResponse.ok(); + } - // Check for pattern matches (like "cluster:*") - for (String permission : resolvedClusterPermissions) { - // skip pure *, which was evaluated above - if (!"*".equals(permission)) { - // Skip exact matches as we already checked those - if (!permission.contains("*")) { - continue; - } + // Check for pattern matches (like "cluster:*") + for (String permission : resolvedClusterPermissions) { + // skip pure *, which was evaluated above + if (!"*".equals(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(); + } } } } @@ -950,7 +952,6 @@ PrivilegesEvaluatorResponse providesExplicitPrivilege( } } } - return apiTokenProvidesIndexPrivilege(checkTable, context, exceptions, resolvedIndices, actions, true); } @@ -962,63 +963,73 @@ PrivilegesEvaluatorResponse apiTokenProvidesIndexPrivilege( Set actions, Boolean explicit ) { - 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 (context instanceof PermissionBasedPrivilegesEvaluationContext) { + + Permissions permissions = ((PermissionBasedPrivilegesEvaluationContext) context).getPermissions(); + 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("*")) { + 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()); + + for (String permission : resolvedIndexPermissions) { + // Skip global wildcard if explicit is true + if (explicit && permission.equals("*")) { + continue; + } - if (remainingActions.isEmpty()) { - indexHasAllPermissions = true; - break; + 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) { + break; // Found a permission that covers all actions for this index + } } - } - 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) { + 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(); } - // If we get here, all indices had sufficient permissions - return PrivilegesEvaluatorResponse.ok(); + 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); } } diff --git a/src/main/java/org/opensearch/security/privileges/PermissionBasedPrivilegesEvaluationContext.java b/src/main/java/org/opensearch/security/privileges/PermissionBasedPrivilegesEvaluationContext.java new file mode 100644 index 0000000000..9b3333cc47 --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/PermissionBasedPrivilegesEvaluationContext.java @@ -0,0 +1,71 @@ +/* + * 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.privileges; + +import java.util.function.Supplier; + +import com.google.common.collect.ImmutableSet; + +import org.opensearch.action.ActionRequest; +import org.opensearch.cluster.ClusterState; +import org.opensearch.cluster.metadata.IndexNameExpressionResolver; +import org.opensearch.security.action.apitokens.Permissions; +import org.opensearch.security.resolver.IndexResolverReplacer; +import org.opensearch.security.user.User; +import org.opensearch.tasks.Task; + +public class PermissionBasedPrivilegesEvaluationContext extends PrivilegesEvaluationContext { + private final Permissions permissions; + + public PermissionBasedPrivilegesEvaluationContext( + User user, + String action, + ActionRequest request, + Task task, + IndexResolverReplacer indexResolverReplacer, + IndexNameExpressionResolver indexNameExpressionResolver, + Supplier clusterStateSupplier, + Permissions permissions + ) { + super(user, action, request, task, indexResolverReplacer, indexNameExpressionResolver, clusterStateSupplier); + this.permissions = permissions; + } + + @Override + public String toString() { + return "PermissionBasedPrivilegesEvaluationContext{" + + "user=" + + getUser() + + ", action='" + + getAction() + + '\'' + + ", request=" + + getRequest() + + ", resolvedRequest=" + + getResolvedRequest() + + ", permissions=" + + permissions + + '}'; + } + + public Permissions getPermissions() { + return permissions; + } + + @Override + public ImmutableSet getMappedRoles() { + return ImmutableSet.of(); + } + + @Override + void setMappedRoles(ImmutableSet roles) {} +} diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java index c91e36d714..cc6a006ffc 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java @@ -20,33 +20,23 @@ import org.opensearch.cluster.ClusterState; import org.opensearch.cluster.metadata.IndexAbstraction; import org.opensearch.cluster.metadata.IndexNameExpressionResolver; -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; -/** - * Request-scoped context information for privilege evaluation. - *

- * This class carries metadata about the request and provides caching facilities for data which might need to be - * evaluated several times per request. - *

- * As this class is request-scoped, it is only used by a single thread. Thus, no thread synchronization mechanisms - * are necessary. - */ -public class PrivilegesEvaluationContext { +public abstract class PrivilegesEvaluationContext { + private final User user; private final String action; private final ActionRequest request; private IndexResolverReplacer.Resolved resolvedRequest; private Map indicesLookup; private final Task task; - private ImmutableSet mappedRoles; private final IndexResolverReplacer indexResolverReplacer; private final IndexNameExpressionResolver indexNameExpressionResolver; private final Supplier clusterStateSupplier; - private final 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, @@ -54,26 +44,22 @@ public class PrivilegesEvaluationContext { */ private final Map renderedPatternTemplateCache = new HashMap<>(); - public PrivilegesEvaluationContext( + PrivilegesEvaluationContext( User user, - ImmutableSet mappedRoles, String action, ActionRequest request, Task task, IndexResolverReplacer indexResolverReplacer, IndexNameExpressionResolver indexNameExpressionResolver, - Supplier clusterStateSupplier, - Permissions permissions + Supplier clusterStateSupplier ) { this.user = user; - this.mappedRoles = mappedRoles; this.action = action; this.request = request; - this.clusterStateSupplier = clusterStateSupplier; + this.task = task; this.indexResolverReplacer = indexResolverReplacer; this.indexNameExpressionResolver = indexNameExpressionResolver; - this.task = task; - this.permissionsForApiToken = permissions; + this.clusterStateSupplier = clusterStateSupplier; } public User getUser() { @@ -128,22 +114,6 @@ public Task getTask() { return task; } - public ImmutableSet getMappedRoles() { - return mappedRoles; - } - - /** - * Note: Ideally, mappedRoles would be an unmodifiable attribute. PrivilegesEvaluator however contains logic - * related to OPENDISTRO_SECURITY_INJECTED_ROLES_VALIDATION which first validates roles and afterwards modifies - * them again. Thus, we need to be able to set this attribute. - * - * However, this method should be only used for this one particular phase. Normally, all roles should be determined - * upfront and stay constant during the whole privilege evaluation process. - */ - void setMappedRoles(ImmutableSet mappedRoles) { - this.mappedRoles = mappedRoles; - } - public Supplier getClusterStateSupplier() { return clusterStateSupplier; } @@ -159,24 +129,7 @@ public IndexNameExpressionResolver getIndexNameExpressionResolver() { return indexNameExpressionResolver; } - @Override - public String toString() { - return "PrivilegesEvaluationContext{" - + "user=" - + user - + ", action='" - + action - + '\'' - + ", request=" - + request - + ", resolvedRequest=" - + resolvedRequest - + ", mappedRoles=" - + mappedRoles - + '}'; - } + public abstract ImmutableSet getMappedRoles(); - public Permissions getPermissionsForApiToken() { - return permissionsForApiToken; - } + abstract void setMappedRoles(ImmutableSet roles); } diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java index c7ac84c0a2..1368e1cecc 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java @@ -308,18 +308,20 @@ public PrivilegesEvaluationContext createContext( TransportAddress caller = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS); ImmutableSet mappedRoles = ImmutableSet.copyOf((injectedRoles == null) ? mapRoles(user, caller) : injectedRoles); + if (user.getName().startsWith("apitoken:")) { + return new PermissionBasedPrivilegesEvaluationContext( + user, + action0, + request, + task, + irr, + resolver, + clusterStateSupplier, + apiTokenRepository.getApiTokenPermissionsForUser(user) + ); + } - return new PrivilegesEvaluationContext( - user, - mappedRoles, - action0, - request, - task, - irr, - resolver, - clusterStateSupplier, - apiTokenRepository.getApiTokenPermissionsForUser(user) - ); + return new RoleBasedPrivilegesEvaluationContext(user, mappedRoles, action0, request, task, irr, resolver, clusterStateSupplier); } public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) { diff --git a/src/main/java/org/opensearch/security/privileges/RoleBasedPrivilegesEvaluationContext.java b/src/main/java/org/opensearch/security/privileges/RoleBasedPrivilegesEvaluationContext.java new file mode 100644 index 0000000000..ff39053d0c --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/RoleBasedPrivilegesEvaluationContext.java @@ -0,0 +1,84 @@ +/* + * 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.privileges; + +import java.util.function.Supplier; + +import com.google.common.collect.ImmutableSet; + +import org.opensearch.action.ActionRequest; +import org.opensearch.cluster.ClusterState; +import org.opensearch.cluster.metadata.IndexNameExpressionResolver; +import org.opensearch.security.resolver.IndexResolverReplacer; +import org.opensearch.security.user.User; +import org.opensearch.tasks.Task; + +/** + * Request-scoped context information for privilege evaluation. + *

+ * This class carries metadata about the request and provides caching facilities for data which might need to be + * evaluated several times per request. + *

+ * As this class is request-scoped, it is only used by a single thread. Thus, no thread synchronization mechanisms + * are necessary. + */ +public class RoleBasedPrivilegesEvaluationContext extends PrivilegesEvaluationContext { + private ImmutableSet mappedRoles; + + public RoleBasedPrivilegesEvaluationContext( + User user, + ImmutableSet mappedRoles, + String action, + ActionRequest request, + Task task, + IndexResolverReplacer indexResolverReplacer, + IndexNameExpressionResolver indexNameExpressionResolver, + Supplier clusterStateSupplier + ) { + super(user, action, request, task, indexResolverReplacer, indexNameExpressionResolver, clusterStateSupplier); + this.mappedRoles = mappedRoles; + } + + @Override + public ImmutableSet getMappedRoles() { + return mappedRoles; + } + + /** + * Note: Ideally, mappedRoles would be an unmodifiable attribute. PrivilegesEvaluator however contains logic + * related to OPENDISTRO_SECURITY_INJECTED_ROLES_VALIDATION which first validates roles and afterwards modifies + * them again. Thus, we need to be able to set this attribute. + * + * However, this method should be only used for this one particular phase. Normally, all roles should be determined + * upfront and stay constant during the whole privilege evaluation process. + */ + @Override + void setMappedRoles(ImmutableSet mappedRoles) { + this.mappedRoles = mappedRoles; + } + + @Override + public String toString() { + return "RoleBasedPrivilegesEvaluationContext{" + + "user=" + + getUser() + + ", action='" + + getAction() + + '\'' + + ", request=" + + getRequest() + + ", resolvedRequest=" + + getResolvedRequest() + + ", mappedRoles=" + + mappedRoles + + '}'; + } +} diff --git a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java index dd818b72b3..44cd041d72 100644 --- a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java +++ b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java @@ -47,7 +47,6 @@ 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; @@ -247,9 +246,6 @@ public Settings getDynamicApiTokenSettings() { .build(); } - @Inject - private ApiTokenRepository apiTokenRepository; - private void buildAAA() { final SortedSet restAuthDomains0 = new TreeSet<>(); @@ -393,7 +389,6 @@ private void buildAAA() { * order: -2 - prioritize the Api token authentication when it gets enabled */ Settings apiTokenSettings = getDynamicApiTokenSettings(); - log.info("APITOKENSETTINGS" + apiTokenSettings.toString()); if (!isKeyNull(apiTokenSettings, "signing_key")) { final AuthDomain _ad = new AuthDomain( new NoOpAuthenticationBackend(Settings.EMPTY, null), 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 88363b25fd..7db9931ace 100644 --- a/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java +++ b/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java @@ -14,22 +14,19 @@ import java.nio.charset.StandardCharsets; import java.util.Date; import java.util.List; -import java.util.Optional; import java.util.function.LongSupplier; import com.google.common.io.BaseEncoding; import org.apache.commons.lang3.RandomStringUtils; -import org.apache.logging.log4j.Level; -import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.core.Appender; import org.apache.logging.log4j.core.LogEvent; -import org.apache.logging.log4j.core.Logger; import org.junit.Assert; import org.junit.Test; import org.opensearch.OpenSearchException; import org.opensearch.common.collect.Tuple; import org.opensearch.common.settings.Settings; +import org.opensearch.security.authtoken.jwt.claims.OBOJwtClaimsBuilder; import org.opensearch.security.support.ConfigConstants; import com.nimbusds.jose.JWSSigner; @@ -41,15 +38,9 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.core.IsNull.notNullValue; import static org.junit.Assert.assertThrows; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; public class JwtVendorTest { private Appender mockAppender; @@ -87,7 +78,7 @@ public void testCreateJwkFromSettingsWithoutSigningKey() { } @Test - public void testCreateOBOJwtWithRoles() throws Exception { + public void testCreateJwtWithRoles() throws Exception { String issuer = "cluster_0"; String subject = "admin"; String audience = "audience_0"; @@ -100,15 +91,20 @@ public void testCreateOBOJwtWithRoles() throws Exception { String claimsEncryptionKey = "1234567890123456"; Settings settings = Settings.builder().put("signing_key", signingKeyB64Encoded).put("encryption_key", claimsEncryptionKey).build(); - JwtVendor OBOJwtVendor = new JwtVendor(settings, Optional.of(currentTime)); - final ExpiringBearerAuthToken authToken = OBOJwtVendor.createOBOJwt( - issuer, - subject, - audience, - expirySeconds, - roles, - backendRoles, - false + Date expiryTime = new Date(currentTime.getAsLong() + expirySeconds * 1000); + + JwtVendor OBOJwtVendor = new JwtVendor(settings); + final ExpiringBearerAuthToken authToken = OBOJwtVendor.createJwt( + new OBOJwtClaimsBuilder(claimsEncryptionKey).addRoles(roles) + .addBackendRoles(false, backendRoles) + .issuer(issuer) + .subject(subject) + .audience(audience) + .expirationTime(expiryTime) + .issueTime(new Date(currentTime.getAsLong())), + subject.toString(), + expiryTime, + (long) expirySeconds ); SignedJWT signedJWT = SignedJWT.parse(authToken.getCompleteToken()); @@ -126,7 +122,7 @@ public void testCreateOBOJwtWithRoles() throws Exception { } @Test - public void testCreateOBOJwtWithBackendRolesIncluded() throws Exception { + public void testCreateJwtWithBackendRolesIncluded() throws Exception { final String issuer = "cluster_0"; final String subject = "admin"; final String audience = "audience_0"; @@ -145,15 +141,20 @@ public void testCreateOBOJwtWithBackendRolesIncluded() throws Exception { .put(ConfigConstants.EXTENSIONS_BWC_PLUGIN_MODE, true) // CS-ENFORCE-SINGLE .build(); - final JwtVendor OBOJwtVendor = new JwtVendor(settings, Optional.of(currentTime)); - final ExpiringBearerAuthToken authToken = OBOJwtVendor.createOBOJwt( - issuer, - subject, - audience, - expirySeconds, - roles, - backendRoles, - true + final JwtVendor OBOJwtVendor = new JwtVendor(settings); + Date expiryTime = new Date(currentTime.getAsLong() + expirySeconds * 1000); + + final ExpiringBearerAuthToken authToken = OBOJwtVendor.createJwt( + new OBOJwtClaimsBuilder(claimsEncryptionKey).addRoles(roles) + .addBackendRoles(true, backendRoles) + .issuer(issuer) + .subject(subject) + .audience(audience) + .expirationTime(expiryTime) + .issueTime(new Date(currentTime.getAsLong())), + subject.toString(), + expiryTime, + (long) expirySeconds ); SignedJWT signedJWT = SignedJWT.parse(authToken.getCompleteToken()); @@ -171,7 +172,7 @@ public void testCreateOBOJwtWithBackendRolesIncluded() throws Exception { } @Test - public void testCreateOBOJwtWithNegativeExpiry() { + public void testCreateJwtWithNegativeExpiry() { String issuer = "cluster_0"; String subject = "admin"; String audience = "audience_0"; @@ -179,152 +180,166 @@ public void testCreateOBOJwtWithNegativeExpiry() { Integer expirySeconds = -300; String claimsEncryptionKey = RandomStringUtils.randomAlphanumeric(16); Settings settings = Settings.builder().put("signing_key", signingKeyB64Encoded).put("encryption_key", claimsEncryptionKey).build(); - JwtVendor OBOJwtVendor = new JwtVendor(settings, Optional.empty()); - - final Throwable exception = assertThrows(RuntimeException.class, () -> { - try { - OBOJwtVendor.createOBOJwt(issuer, subject, audience, expirySeconds, roles, List.of(), true); - } catch (final Exception e) { - throw new RuntimeException(e); - } - }); - assertThat(exception.getMessage(), is("java.lang.IllegalArgumentException: The expiration time should be a positive integer")); - } - - @Test - public void testCreateOBOJwtWithExceededExpiry() throws Exception { - String issuer = "cluster_0"; - String subject = "admin"; - String audience = "audience_0"; - List roles = List.of("IT", "HR"); - List backendRoles = List.of("Sales", "Support"); - int expirySeconds = 900_000; + JwtVendor OBOJwtVendor = new JwtVendor(settings); LongSupplier currentTime = () -> (long) 100; - String claimsEncryptionKey = RandomStringUtils.randomAlphanumeric(16); - Settings settings = Settings.builder().put("signing_key", signingKeyB64Encoded).put("encryption_key", claimsEncryptionKey).build(); - JwtVendor OBOJwtVendor = new JwtVendor(settings, Optional.of(currentTime)); - final ExpiringBearerAuthToken authToken = OBOJwtVendor.createOBOJwt( - 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)); - } - - @Test - public void testCreateOBOJwtWithBadEncryptionKey() { - final String issuer = "cluster_0"; - final String subject = "admin"; - final String audience = "audience_0"; - final List roles = List.of("admin"); - final Integer expirySeconds = 300; - - Settings settings = Settings.builder().put("signing_key", signingKeyB64Encoded).build(); - - final Throwable exception = assertThrows(RuntimeException.class, () -> { - try { - new JwtVendor(settings, Optional.empty()).createOBOJwt(issuer, subject, audience, expirySeconds, roles, List.of(), true); - } catch (final Exception e) { - throw new RuntimeException(e); - } - }); - assertThat(exception.getMessage(), is("java.lang.IllegalArgumentException: encryption_key cannot be null")); - } - - @Test - public void testCreateOBOJwtWithBadRoles() { - String issuer = "cluster_0"; - String subject = "admin"; - String audience = "audience_0"; - List roles = null; - Integer expirySeconds = 300; - String claimsEncryptionKey = RandomStringUtils.randomAlphanumeric(16); - Settings settings = Settings.builder().put("signing_key", signingKeyB64Encoded).put("encryption_key", claimsEncryptionKey).build(); - JwtVendor OBOJwtVendor = new JwtVendor(settings, Optional.empty()); + Date expiryTime = new Date(expirySeconds); final Throwable exception = assertThrows(RuntimeException.class, () -> { try { - OBOJwtVendor.createOBOJwt(issuer, subject, audience, expirySeconds, roles, List.of(), true); + OBOJwtVendor.createJwt( + new OBOJwtClaimsBuilder(claimsEncryptionKey).addRoles(roles) + .addBackendRoles(true, List.of()) + .issuer(issuer) + .subject(subject) + .audience(audience) + .expirationTime(expiryTime) + .issueTime(new Date(currentTime.getAsLong())), + subject.toString(), + expiryTime, + (long) expirySeconds + ); } catch (final Exception e) { throw new RuntimeException(e); } }); - assertThat(exception.getMessage(), is("java.lang.IllegalArgumentException: Roles cannot be null")); - } - - @Test - public void testCreateOBOJwtLogsCorrectly() throws Exception { - mockAppender = mock(Appender.class); - logEventCaptor = ArgumentCaptor.forClass(LogEvent.class); - when(mockAppender.getName()).thenReturn("MockAppender"); - when(mockAppender.isStarted()).thenReturn(true); - final Logger logger = (Logger) LogManager.getLogger(JwtVendor.class); - logger.addAppender(mockAppender); - logger.setLevel(Level.DEBUG); - - // Mock settings and other required dependencies - LongSupplier currentTime = () -> (long) 100; - String claimsEncryptionKey = RandomStringUtils.randomAlphanumeric(16); - Settings settings = Settings.builder().put("signing_key", signingKeyB64Encoded).put("encryption_key", claimsEncryptionKey).build(); - - final String issuer = "cluster_0"; - final String subject = "admin"; - final String audience = "audience_0"; - final List roles = List.of("IT", "HR"); - final List backendRoles = List.of("Sales", "Support"); - final int expirySeconds = 300; - - final JwtVendor OBOJwtVendor = new JwtVendor(settings, Optional.of(currentTime)); - - OBOJwtVendor.createOBOJwt(issuer, subject, audience, expirySeconds, roles, backendRoles, false); - - verify(mockAppender, times(1)).append(logEventCaptor.capture()); - - final LogEvent logEvent = logEventCaptor.getValue(); - final String logMessage = logEvent.getMessage().getFormattedMessage(); - assertTrue(logMessage.startsWith("Created JWT:")); - - final String[] parts = logMessage.split("\\."); - assertTrue(parts.length >= 3); - } - - @Test - public void testCreateOBOJwtForApiTokenSuccess() throws Exception { - final String issuer = "cluster_0"; - final String subject = "test-token"; - final String audience = "test-token"; - - LongSupplier currentTime = () -> (long) 100; - String claimsEncryptionKey = "1234567890123456"; - Settings settings = Settings.builder().put("signing_key", signingKeyB64Encoded).put("encryption_key", claimsEncryptionKey).build(); - final JwtVendor apiTokenJwtVendor = new JwtVendor(settings, Optional.of(currentTime)); - final ExpiringBearerAuthToken authToken = apiTokenJwtVendor.createApiTokenJwt(issuer, subject, audience, Long.MAX_VALUE); - - SignedJWT signedJWT = SignedJWT.parse(authToken.getCompleteToken()); - - assertThat(signedJWT.getJWTClaimsSet().getClaims().get("iss"), equalTo(issuer)); - assertThat(signedJWT.getJWTClaimsSet().getClaims().get("sub"), equalTo(subject)); - assertThat(signedJWT.getJWTClaimsSet().getClaims().get("aud").toString(), equalTo("[" + audience + "]")); - 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)); - } - - @Test - public void testKeyTooShortForApiTokenThrowsException() { - 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()); }); - - assertThat(exception.getMessage(), containsString("The secret length must be at least 256 bits")); + assertThat(exception.getMessage(), is("java.lang.IllegalArgumentException: The expiration time should be a positive integer")); } + // + // @Test + // public void testCreateJwtWithExceededExpiry() throws Exception { + // String issuer = "cluster_0"; + // String subject = "admin"; + // String audience = "audience_0"; + // List roles = List.of("IT", "HR"); + // List backendRoles = List.of("Sales", "Support"); + // int expirySeconds = 900_000; + // LongSupplier currentTime = () -> (long) 100; + // String claimsEncryptionKey = RandomStringUtils.randomAlphanumeric(16); + // Settings settings = Settings.builder().put("signing_key", signingKeyB64Encoded).put("encryption_key", claimsEncryptionKey).build(); + // JwtVendor OBOJwtVendor = new JwtVendor(settings, Optional.of(currentTime)); + // + // 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)); + // } + // + // @Test + // public void testCreateJwtWithBadEncryptionKey() { + // final String issuer = "cluster_0"; + // final String subject = "admin"; + // final String audience = "audience_0"; + // final List roles = List.of("admin"); + // final Integer expirySeconds = 300; + // + // Settings settings = Settings.builder().put("signing_key", signingKeyB64Encoded).build(); + // + // final Throwable exception = assertThrows(RuntimeException.class, () -> { + // try { + // new JwtVendor(settings, Optional.empty()).createJwt(issuer, subject, audience, expirySeconds, roles, List.of(), true); + // } catch (final Exception e) { + // throw new RuntimeException(e); + // } + // }); + // assertThat(exception.getMessage(), is("java.lang.IllegalArgumentException: encryption_key cannot be null")); + // } + // + // @Test + // public void testCreateJwtWithBadRoles() { + // String issuer = "cluster_0"; + // String subject = "admin"; + // String audience = "audience_0"; + // List roles = null; + // Integer expirySeconds = 300; + // String claimsEncryptionKey = RandomStringUtils.randomAlphanumeric(16); + // Settings settings = Settings.builder().put("signing_key", signingKeyB64Encoded).put("encryption_key", claimsEncryptionKey).build(); + // JwtVendor OBOJwtVendor = new JwtVendor(settings, Optional.empty()); + // + // final Throwable exception = assertThrows(RuntimeException.class, () -> { + // try { + // OBOJwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles, List.of(), true); + // } catch (final Exception e) { + // throw new RuntimeException(e); + // } + // }); + // assertThat(exception.getMessage(), is("java.lang.IllegalArgumentException: Roles cannot be null")); + // } + // + // @Test + // public void testCreateJwtLogsCorrectly() throws Exception { + // mockAppender = mock(Appender.class); + // logEventCaptor = ArgumentCaptor.forClass(LogEvent.class); + // when(mockAppender.getName()).thenReturn("MockAppender"); + // when(mockAppender.isStarted()).thenReturn(true); + // final Logger logger = (Logger) LogManager.getLogger(JwtVendor.class); + // logger.addAppender(mockAppender); + // logger.setLevel(Level.DEBUG); + // + // // Mock settings and other required dependencies + // LongSupplier currentTime = () -> (long) 100; + // String claimsEncryptionKey = RandomStringUtils.randomAlphanumeric(16); + // Settings settings = Settings.builder().put("signing_key", signingKeyB64Encoded).put("encryption_key", claimsEncryptionKey).build(); + // + // final String issuer = "cluster_0"; + // final String subject = "admin"; + // final String audience = "audience_0"; + // final List roles = List.of("IT", "HR"); + // final List backendRoles = List.of("Sales", "Support"); + // final int expirySeconds = 300; + // + // final JwtVendor OBOJwtVendor = new JwtVendor(settings, Optional.of(currentTime)); + // + // OBOJwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles, backendRoles, false); + // + // verify(mockAppender, times(1)).append(logEventCaptor.capture()); + // + // final LogEvent logEvent = logEventCaptor.getValue(); + // final String logMessage = logEvent.getMessage().getFormattedMessage(); + // assertTrue(logMessage.startsWith("Created JWT:")); + // + // final String[] parts = logMessage.split("\\."); + // assertTrue(parts.length >= 3); + // } + // + // @Test + // public void testCreateJwtForApiTokenSuccess() throws Exception { + // final String issuer = "cluster_0"; + // final String subject = "test-token"; + // final String audience = "test-token"; + // + // LongSupplier currentTime = () -> (long) 100; + // String claimsEncryptionKey = "1234567890123456"; + // Settings settings = Settings.builder().put("signing_key", signingKeyB64Encoded).put("encryption_key", claimsEncryptionKey).build(); + // final JwtVendor apiTokenJwtVendor = new JwtVendor(settings, Optional.of(currentTime)); + // final ExpiringBearerAuthToken authToken = apiTokenJwtVendor.createApiTokenJwt(issuer, subject, audience, Long.MAX_VALUE); + // + // SignedJWT signedJWT = SignedJWT.parse(authToken.getCompleteToken()); + // + // assertThat(signedJWT.getJWTClaimsSet().getClaims().get("iss"), equalTo(issuer)); + // assertThat(signedJWT.getJWTClaimsSet().getClaims().get("sub"), equalTo(subject)); + // assertThat(signedJWT.getJWTClaimsSet().getClaims().get("aud").toString(), equalTo("[" + audience + "]")); + // 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)); + // } + // + // @Test + // public void testKeyTooShortForApiTokenThrowsException() { + // 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()); }); + // + // 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 8b3adb0d70..1bf03c8dc6 100644 --- a/src/test/java/org/opensearch/security/identity/SecurityTokenManagerTest.java +++ b/src/test/java/org/opensearch/security/identity/SecurityTokenManagerTest.java @@ -12,9 +12,11 @@ package org.opensearch.security.identity; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Set; +import com.google.common.io.BaseEncoding; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -42,11 +44,9 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -61,9 +61,7 @@ public class SecurityTokenManagerTest { private SecurityTokenManager tokenManager; @Mock - private JwtVendor oboJwtVendor; - @Mock - private JwtVendor apiTokenJwtVendor; + private JwtVendor jwtVendor; @Mock private ClusterService cs; @Mock @@ -79,10 +77,13 @@ public void setup() { @After public void after() { verifyNoMoreInteractions(cs); - verifyNoMoreInteractions(threadPool); verifyNoMoreInteractions(userService); } + final static String signingKey = + "This is my super safe signing key that no one will ever be able to guess. It's would take billions of years and the world's most powerful quantum computer to crack"; + final static String signingKeyB64Encoded = BaseEncoding.base64().encode(signingKey.getBytes(StandardCharsets.UTF_8)); + @Test public void onConfigModelChanged_oboNotSupported() { final ConfigModel configModel = mock(ConfigModel.class); @@ -120,21 +121,15 @@ public void onDynamicConfigModelChanged_JwtVendorDisabled() { /** Creates the jwt vendor and returns a mock for validation if needed */ private DynamicConfigModel createMockJwtVendorInTokenManager() { - final Settings settings = Settings.builder().put("enabled", true).build(); - final DynamicConfigModel dcm = mock(DynamicConfigModel.class); - when(dcm.getDynamicOnBehalfOfSettings()).thenReturn(settings); - when(dcm.getDynamicApiTokenSettings()).thenReturn(settings); - doAnswer((invocation) -> oboJwtVendor).when(tokenManager).createJwtVendor(settings); - tokenManager.onDynamicConfigModelChanged(dcm); - return dcm; - } - - private DynamicConfigModel createMockApiTokenJwtVendorInTokenManager() { - final Settings settings = Settings.builder().put("enabled", true).build(); + final Settings settings = Settings.builder() + .put("enabled", true) + .put("encryption_key", "1234567890") + .put("signing_key", signingKeyB64Encoded) + .build(); final DynamicConfigModel dcm = mock(DynamicConfigModel.class); when(dcm.getDynamicOnBehalfOfSettings()).thenReturn(settings); when(dcm.getDynamicApiTokenSettings()).thenReturn(settings); - doAnswer((invocation) -> apiTokenJwtVendor).when(tokenManager).createJwtVendor(settings); + doAnswer((invocation) -> jwtVendor).when(tokenManager).createJwtVendor(settings); tokenManager.onDynamicConfigModelChanged(dcm); return dcm; } @@ -225,9 +220,7 @@ public void issueOnBehalfOfToken_jwtGenerationFailure() throws Exception { createMockJwtVendorInTokenManager(); - when(oboJwtVendor.createOBOJwt(any(), anyString(), anyString(), anyLong(), any(), any(), anyBoolean())).thenThrow( - new RuntimeException("foobar") - ); + when(jwtVendor.createJwt(any(), any(), any(), any())).thenThrow(new RuntimeException("foobar")); final OpenSearchSecurityException exception = assertThrows( OpenSearchSecurityException.class, () -> tokenManager.issueOnBehalfOfToken(null, new OnBehalfOfClaims("elmo", 450L)) @@ -252,7 +245,7 @@ public void issueOnBehalfOfToken_success() throws Exception { createMockJwtVendorInTokenManager(); final ExpiringBearerAuthToken authToken = mock(ExpiringBearerAuthToken.class); - when(oboJwtVendor.createOBOJwt(any(), anyString(), anyString(), anyLong(), any(), any(), anyBoolean())).thenReturn(authToken); + when(jwtVendor.createJwt(any(), any(), any(), any())).thenReturn(authToken); final AuthToken returnedToken = tokenManager.issueOnBehalfOfToken(null, new OnBehalfOfClaims("elmo", 450L)); assertThat(returnedToken, equalTo(authToken)); @@ -261,6 +254,28 @@ public void issueOnBehalfOfToken_success() throws Exception { verify(threadPool).getThreadContext(); } + @Test + public void testCreateJwtWithNegativeExpiry() { + doAnswer(invocation -> true).when(tokenManager).issueOnBehalfOfTokenAllowed(); + final ThreadContext threadContext = new ThreadContext(Settings.EMPTY); + threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, new User("Jon", List.of(), null)); + when(threadPool.getThreadContext()).thenReturn(threadContext); + final ConfigModel configModel = mock(ConfigModel.class); + tokenManager.onConfigModelChanged(configModel); + when(configModel.mapSecurityRoles(any(), any())).thenReturn(Set.of()); + + createMockJwtVendorInTokenManager(); + + final Throwable exception = assertThrows(RuntimeException.class, () -> { + try { + final AuthToken returnedToken = tokenManager.issueOnBehalfOfToken(null, new OnBehalfOfClaims("elmo", -300L)); + } catch (final Exception e) { + throw new RuntimeException(e); + } + }); + assertThat(exception.getMessage(), is("java.lang.IllegalArgumentException: The expiration time should be a positive integer")); + } + @Test public void issueApiToken_success() throws Exception { doAnswer(invockation -> new ClusterName("cluster17")).when(cs).getClusterName(); @@ -270,10 +285,10 @@ public void issueApiToken_success() throws Exception { final ConfigModel configModel = mock(ConfigModel.class); tokenManager.onConfigModelChanged(configModel); - createMockApiTokenJwtVendorInTokenManager(); + createMockJwtVendorInTokenManager(); final ExpiringBearerAuthToken authToken = mock(ExpiringBearerAuthToken.class); - when(apiTokenJwtVendor.createApiTokenJwt(anyString(), anyString(), anyString(), anyLong())).thenReturn(authToken); + when(jwtVendor.createJwt(any(), any(), any(), any())).thenReturn(authToken); final AuthToken returnedToken = tokenManager.issueApiToken("elmo", Long.MAX_VALUE); assertThat(returnedToken, equalTo(authToken)); diff --git a/src/test/java/org/opensearch/security/privileges/SystemIndexAccessEvaluatorTest.java b/src/test/java/org/opensearch/security/privileges/SystemIndexAccessEvaluatorTest.java index c1f8ff79c7..697c650f48 100644 --- a/src/test/java/org/opensearch/security/privileges/SystemIndexAccessEvaluatorTest.java +++ b/src/test/java/org/opensearch/security/privileges/SystemIndexAccessEvaluatorTest.java @@ -32,7 +32,6 @@ import org.opensearch.cluster.service.ClusterService; import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; -import org.opensearch.security.action.apitokens.Permissions; import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.resolver.IndexResolverReplacer; import org.opensearch.security.resolver.IndexResolverReplacer.Resolved; @@ -155,8 +154,8 @@ public void setup( when(log.isInfoEnabled()).thenReturn(true); } - PrivilegesEvaluationContext ctx(String action) { - return new PrivilegesEvaluationContext( + RoleBasedPrivilegesEvaluationContext ctx(String action) { + return new RoleBasedPrivilegesEvaluationContext( user, ImmutableSet.of("role_a"), action, @@ -164,8 +163,7 @@ PrivilegesEvaluationContext ctx(String action) { null, null, indexNameExpressionResolver, - null, - new Permissions() + null ); } From a6859519806bd9f22d6e653ff02aa3a5fc86e3de Mon Sep 17 00:00:00 2001 From: Derek Ho Date: Thu, 23 Jan 2025 12:48:47 -0500 Subject: [PATCH 28/30] Fix test and cleanup Signed-off-by: Derek Ho --- .../identity/SecurityTokenManager.java | 4 +- .../security/authtoken/jwt/JwtVendorTest.java | 259 +++++++----------- .../identity/SecurityTokenManagerTest.java | 81 +++++- .../RestLayerPrivilegesEvaluatorTest.java | 6 +- 4 files changed, 170 insertions(+), 180 deletions(-) diff --git a/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java b/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java index 5a43365aee..f726a8134b 100644 --- a/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java +++ b/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java @@ -149,7 +149,7 @@ public ExpiringBearerAuthToken issueOnBehalfOfToken(final Subject subject, final final OBOJwtClaimsBuilder claimsBuilder = new OBOJwtClaimsBuilder(oboSettings.get("encryption_key")); // Add obo claims - claimsBuilder.issuer(cs.getClusterName().toString()); + claimsBuilder.issuer(cs.getClusterName().value()); claimsBuilder.issueTime(now); claimsBuilder.subject(user.getName()); claimsBuilder.audience(claims.getAudience()); @@ -178,7 +178,7 @@ public ExpiringBearerAuthToken issueApiToken(final String name, final Long expir final Date now = new Date(currentTimeMs); final ApiJwtClaimsBuilder claimsBuilder = new ApiJwtClaimsBuilder(); - claimsBuilder.issuer(cs.getClusterName().toString()); + claimsBuilder.issuer(cs.getClusterName().value()); claimsBuilder.issueTime(now); claimsBuilder.subject(name); claimsBuilder.audience(name); 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 7db9931ace..6112f2794f 100644 --- a/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java +++ b/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java @@ -18,14 +18,18 @@ import com.google.common.io.BaseEncoding; import org.apache.commons.lang3.RandomStringUtils; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.core.Appender; import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.Logger; import org.junit.Assert; import org.junit.Test; import org.opensearch.OpenSearchException; import org.opensearch.common.collect.Tuple; import org.opensearch.common.settings.Settings; +import org.opensearch.security.authtoken.jwt.claims.ApiJwtClaimsBuilder; import org.opensearch.security.authtoken.jwt.claims.OBOJwtClaimsBuilder; import org.opensearch.security.support.ConfigConstants; @@ -41,6 +45,11 @@ import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.core.IsNull.notNullValue; import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; public class JwtVendorTest { private Appender mockAppender; @@ -57,7 +66,7 @@ public void testCreateJwkFromSettings() { final Tuple jwk = JwtVendor.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)); + assertTrue(jwk.v1().toOctetSequenceKey().getKeyValue().decodeToString().startsWith(signingKey)); } @Test @@ -172,174 +181,94 @@ public void testCreateJwtWithBackendRolesIncluded() throws Exception { } @Test - public void testCreateJwtWithNegativeExpiry() { + public void testCreateJwtLogsCorrectly() throws Exception { + mockAppender = mock(Appender.class); + logEventCaptor = ArgumentCaptor.forClass(LogEvent.class); + when(mockAppender.getName()).thenReturn("MockAppender"); + when(mockAppender.isStarted()).thenReturn(true); + final Logger logger = (Logger) LogManager.getLogger(JwtVendor.class); + logger.addAppender(mockAppender); + logger.setLevel(Level.DEBUG); + + // Mock settings and other required dependencies + LongSupplier currentTime = () -> (long) 100; + String claimsEncryptionKey = RandomStringUtils.randomAlphanumeric(16); + Settings settings = Settings.builder().put("signing_key", signingKeyB64Encoded).put("encryption_key", claimsEncryptionKey).build(); + + final String issuer = "cluster_0"; + final String subject = "admin"; + final String audience = "audience_0"; + final List roles = List.of("IT", "HR"); + final List backendRoles = List.of("Sales", "Support"); + int expirySeconds = 300; + + final JwtVendor OBOJwtVendor = new JwtVendor(settings); + Date expiryTime = new Date(currentTime.getAsLong() + expirySeconds * 1000); + OBOJwtVendor.createJwt( + new OBOJwtClaimsBuilder(claimsEncryptionKey).addRoles(roles) + .addBackendRoles(true, backendRoles) + .issuer(issuer) + .subject(subject) + .audience(audience) + .expirationTime(expiryTime) + .issueTime(new Date(currentTime.getAsLong())), + subject.toString(), + expiryTime, + (long) expirySeconds + ); + + verify(mockAppender, times(1)).append(logEventCaptor.capture()); + + final LogEvent logEvent = logEventCaptor.getValue(); + final String logMessage = logEvent.getMessage().getFormattedMessage(); + assertTrue(logMessage.startsWith("Created JWT:")); + + final String[] parts = logMessage.split("\\."); + assertTrue(parts.length >= 3); + } + + @Test + public void testCreateApiTokenJwtSuccess() throws Exception { String issuer = "cluster_0"; String subject = "admin"; String audience = "audience_0"; - List roles = List.of("admin"); - Integer expirySeconds = -300; - String claimsEncryptionKey = RandomStringUtils.randomAlphanumeric(16); - Settings settings = Settings.builder().put("signing_key", signingKeyB64Encoded).put("encryption_key", claimsEncryptionKey).build(); - JwtVendor OBOJwtVendor = new JwtVendor(settings); - LongSupplier currentTime = () -> (long) 100; + int expirySeconds = 300; + // 2023 oct 4, 10:00:00 AM GMT + LongSupplier currentTime = () -> 1696413600000L; + Settings settings = Settings.builder().put("signing_key", signingKeyB64Encoded).build(); + + Date expiryTime = new Date(currentTime.getAsLong() + expirySeconds * 1000); - Date expiryTime = new Date(expirySeconds); - - final Throwable exception = assertThrows(RuntimeException.class, () -> { - try { - OBOJwtVendor.createJwt( - new OBOJwtClaimsBuilder(claimsEncryptionKey).addRoles(roles) - .addBackendRoles(true, List.of()) - .issuer(issuer) - .subject(subject) - .audience(audience) - .expirationTime(expiryTime) - .issueTime(new Date(currentTime.getAsLong())), - subject.toString(), - expiryTime, - (long) expirySeconds - ); - } catch (final Exception e) { - throw new RuntimeException(e); - } - }); - assertThat(exception.getMessage(), is("java.lang.IllegalArgumentException: The expiration time should be a positive integer")); + JwtVendor apiTokenJwtVendor = new JwtVendor(settings); + final ExpiringBearerAuthToken authToken = apiTokenJwtVendor.createJwt( + new ApiJwtClaimsBuilder().issuer(issuer) + .subject(subject) + .audience(audience) + .expirationTime(expiryTime) + .issueTime(new Date(currentTime.getAsLong())), + subject.toString(), + expiryTime, + (long) expirySeconds + ); + + SignedJWT signedJWT = SignedJWT.parse(authToken.getCompleteToken()); + + assertThat(signedJWT.getJWTClaimsSet().getClaims().get("iss"), equalTo("cluster_0")); + assertThat(signedJWT.getJWTClaimsSet().getClaims().get("sub"), equalTo("admin")); + assertThat(signedJWT.getJWTClaimsSet().getClaims().get("aud").toString(), equalTo("[audience_0]")); + // 2023 oct 4, 10:00:00 AM GMT + assertThat(((Date) signedJWT.getJWTClaimsSet().getClaims().get("iat")).getTime(), is(1696413600000L)); + // 2023 oct 4, 10:05:00 AM GMT + assertThat(((Date) signedJWT.getJWTClaimsSet().getClaims().get("exp")).getTime(), is(1696413900000L)); + } + + @Test + public void testKeyTooShortForApiTokenThrowsException() { + String tooShortKey = BaseEncoding.base64().encode("short_key".getBytes()); + Settings settings = Settings.builder().put("signing_key", tooShortKey).build(); + final Throwable exception = assertThrows(OpenSearchException.class, () -> { new JwtVendor(settings); }); + + assertThat(exception.getMessage(), containsString("The secret length must be at least 256 bits")); } - // - // @Test - // public void testCreateJwtWithExceededExpiry() throws Exception { - // String issuer = "cluster_0"; - // String subject = "admin"; - // String audience = "audience_0"; - // List roles = List.of("IT", "HR"); - // List backendRoles = List.of("Sales", "Support"); - // int expirySeconds = 900_000; - // LongSupplier currentTime = () -> (long) 100; - // String claimsEncryptionKey = RandomStringUtils.randomAlphanumeric(16); - // Settings settings = Settings.builder().put("signing_key", signingKeyB64Encoded).put("encryption_key", claimsEncryptionKey).build(); - // JwtVendor OBOJwtVendor = new JwtVendor(settings, Optional.of(currentTime)); - // - // 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)); - // } - // - // @Test - // public void testCreateJwtWithBadEncryptionKey() { - // final String issuer = "cluster_0"; - // final String subject = "admin"; - // final String audience = "audience_0"; - // final List roles = List.of("admin"); - // final Integer expirySeconds = 300; - // - // Settings settings = Settings.builder().put("signing_key", signingKeyB64Encoded).build(); - // - // final Throwable exception = assertThrows(RuntimeException.class, () -> { - // try { - // new JwtVendor(settings, Optional.empty()).createJwt(issuer, subject, audience, expirySeconds, roles, List.of(), true); - // } catch (final Exception e) { - // throw new RuntimeException(e); - // } - // }); - // assertThat(exception.getMessage(), is("java.lang.IllegalArgumentException: encryption_key cannot be null")); - // } - // - // @Test - // public void testCreateJwtWithBadRoles() { - // String issuer = "cluster_0"; - // String subject = "admin"; - // String audience = "audience_0"; - // List roles = null; - // Integer expirySeconds = 300; - // String claimsEncryptionKey = RandomStringUtils.randomAlphanumeric(16); - // Settings settings = Settings.builder().put("signing_key", signingKeyB64Encoded).put("encryption_key", claimsEncryptionKey).build(); - // JwtVendor OBOJwtVendor = new JwtVendor(settings, Optional.empty()); - // - // final Throwable exception = assertThrows(RuntimeException.class, () -> { - // try { - // OBOJwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles, List.of(), true); - // } catch (final Exception e) { - // throw new RuntimeException(e); - // } - // }); - // assertThat(exception.getMessage(), is("java.lang.IllegalArgumentException: Roles cannot be null")); - // } - // - // @Test - // public void testCreateJwtLogsCorrectly() throws Exception { - // mockAppender = mock(Appender.class); - // logEventCaptor = ArgumentCaptor.forClass(LogEvent.class); - // when(mockAppender.getName()).thenReturn("MockAppender"); - // when(mockAppender.isStarted()).thenReturn(true); - // final Logger logger = (Logger) LogManager.getLogger(JwtVendor.class); - // logger.addAppender(mockAppender); - // logger.setLevel(Level.DEBUG); - // - // // Mock settings and other required dependencies - // LongSupplier currentTime = () -> (long) 100; - // String claimsEncryptionKey = RandomStringUtils.randomAlphanumeric(16); - // Settings settings = Settings.builder().put("signing_key", signingKeyB64Encoded).put("encryption_key", claimsEncryptionKey).build(); - // - // final String issuer = "cluster_0"; - // final String subject = "admin"; - // final String audience = "audience_0"; - // final List roles = List.of("IT", "HR"); - // final List backendRoles = List.of("Sales", "Support"); - // final int expirySeconds = 300; - // - // final JwtVendor OBOJwtVendor = new JwtVendor(settings, Optional.of(currentTime)); - // - // OBOJwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles, backendRoles, false); - // - // verify(mockAppender, times(1)).append(logEventCaptor.capture()); - // - // final LogEvent logEvent = logEventCaptor.getValue(); - // final String logMessage = logEvent.getMessage().getFormattedMessage(); - // assertTrue(logMessage.startsWith("Created JWT:")); - // - // final String[] parts = logMessage.split("\\."); - // assertTrue(parts.length >= 3); - // } - // - // @Test - // public void testCreateJwtForApiTokenSuccess() throws Exception { - // final String issuer = "cluster_0"; - // final String subject = "test-token"; - // final String audience = "test-token"; - // - // LongSupplier currentTime = () -> (long) 100; - // String claimsEncryptionKey = "1234567890123456"; - // Settings settings = Settings.builder().put("signing_key", signingKeyB64Encoded).put("encryption_key", claimsEncryptionKey).build(); - // final JwtVendor apiTokenJwtVendor = new JwtVendor(settings, Optional.of(currentTime)); - // final ExpiringBearerAuthToken authToken = apiTokenJwtVendor.createApiTokenJwt(issuer, subject, audience, Long.MAX_VALUE); - // - // SignedJWT signedJWT = SignedJWT.parse(authToken.getCompleteToken()); - // - // assertThat(signedJWT.getJWTClaimsSet().getClaims().get("iss"), equalTo(issuer)); - // assertThat(signedJWT.getJWTClaimsSet().getClaims().get("sub"), equalTo(subject)); - // assertThat(signedJWT.getJWTClaimsSet().getClaims().get("aud").toString(), equalTo("[" + audience + "]")); - // 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)); - // } - // - // @Test - // public void testKeyTooShortForApiTokenThrowsException() { - // 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()); }); - // - // 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 1bf03c8dc6..90531dfdb8 100644 --- a/src/test/java/org/opensearch/security/identity/SecurityTokenManagerTest.java +++ b/src/test/java/org/opensearch/security/identity/SecurityTokenManagerTest.java @@ -39,6 +39,7 @@ import org.opensearch.security.user.UserService; import org.opensearch.threadpool.ThreadPool; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; @@ -76,7 +77,6 @@ public void setup() { @After public void after() { - verifyNoMoreInteractions(cs); verifyNoMoreInteractions(userService); } @@ -97,7 +97,7 @@ public void onConfigModelChanged_oboNotSupported() { @Test public void onDynamicConfigModelChanged_JwtVendorEnabled() { final ConfigModel configModel = mock(ConfigModel.class); - final DynamicConfigModel mockConfigModel = createMockJwtVendorInTokenManager(); + final DynamicConfigModel mockConfigModel = createMockJwtVendorInTokenManager(true); tokenManager.onConfigModelChanged(configModel); @@ -120,11 +120,11 @@ public void onDynamicConfigModelChanged_JwtVendorDisabled() { } /** Creates the jwt vendor and returns a mock for validation if needed */ - private DynamicConfigModel createMockJwtVendorInTokenManager() { + private DynamicConfigModel createMockJwtVendorInTokenManager(boolean includeEncryptionKey) { final Settings settings = Settings.builder() .put("enabled", true) - .put("encryption_key", "1234567890") .put("signing_key", signingKeyB64Encoded) + .put("encryption_key", (includeEncryptionKey ? "1234567890" : null)) .build(); final DynamicConfigModel dcm = mock(DynamicConfigModel.class); when(dcm.getDynamicOnBehalfOfSettings()).thenReturn(settings); @@ -218,7 +218,7 @@ public void issueOnBehalfOfToken_jwtGenerationFailure() throws Exception { tokenManager.onConfigModelChanged(configModel); when(configModel.mapSecurityRoles(any(), any())).thenReturn(Set.of()); - createMockJwtVendorInTokenManager(); + createMockJwtVendorInTokenManager(true); when(jwtVendor.createJwt(any(), any(), any(), any())).thenThrow(new RuntimeException("foobar")); final OpenSearchSecurityException exception = assertThrows( @@ -242,7 +242,7 @@ public void issueOnBehalfOfToken_success() throws Exception { tokenManager.onConfigModelChanged(configModel); when(configModel.mapSecurityRoles(any(), any())).thenReturn(Set.of()); - createMockJwtVendorInTokenManager(); + createMockJwtVendorInTokenManager(true); final ExpiringBearerAuthToken authToken = mock(ExpiringBearerAuthToken.class); when(jwtVendor.createJwt(any(), any(), any(), any())).thenReturn(authToken); @@ -264,7 +264,7 @@ public void testCreateJwtWithNegativeExpiry() { tokenManager.onConfigModelChanged(configModel); when(configModel.mapSecurityRoles(any(), any())).thenReturn(Set.of()); - createMockJwtVendorInTokenManager(); + createMockJwtVendorInTokenManager(true); final Throwable exception = assertThrows(RuntimeException.class, () -> { try { @@ -276,6 +276,71 @@ public void testCreateJwtWithNegativeExpiry() { assertThat(exception.getMessage(), is("java.lang.IllegalArgumentException: The expiration time should be a positive integer")); } + @Test + public void testCreateJwtWithExceededExpiry() throws Exception { + doAnswer(invockation -> new ClusterName("cluster17")).when(cs).getClusterName(); + doAnswer(invocation -> true).when(tokenManager).issueOnBehalfOfTokenAllowed(); + final ThreadContext threadContext = new ThreadContext(Settings.EMPTY); + threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, new User("Jon", List.of(), null)); + when(threadPool.getThreadContext()).thenReturn(threadContext); + final ConfigModel configModel = mock(ConfigModel.class); + tokenManager.onConfigModelChanged(configModel); + when(configModel.mapSecurityRoles(any(), any())).thenReturn(Set.of()); + + createMockJwtVendorInTokenManager(true); + + tokenManager.issueOnBehalfOfToken(null, new OnBehalfOfClaims("elmo", 90000000L)); + // Expiry is a hint, the max value is controlled by the JwtVendor and reduced as is seen fit. + ArgumentCaptor longCaptor = ArgumentCaptor.forClass(Long.class); + verify(jwtVendor).createJwt(any(), any(), any(), longCaptor.capture()); + + assertThat(600L, equalTo(longCaptor.getValue())); + } + + @Test + public void testCreateJwtWithBadEncryptionKey() { + doAnswer(invocation -> true).when(tokenManager).issueOnBehalfOfTokenAllowed(); + final ThreadContext threadContext = new ThreadContext(Settings.EMPTY); + threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, new User("Jon", List.of(), null)); + when(threadPool.getThreadContext()).thenReturn(threadContext); + final ConfigModel configModel = mock(ConfigModel.class); + tokenManager.onConfigModelChanged(configModel); + when(configModel.mapSecurityRoles(any(), any())).thenReturn(Set.of()); + + createMockJwtVendorInTokenManager(false); + + final Throwable exception = assertThrows(RuntimeException.class, () -> { + try { + tokenManager.issueOnBehalfOfToken(null, new OnBehalfOfClaims("elmo", 90000000L)); + } catch (final Exception e) { + throw new RuntimeException(e); + } + }); + assertThat(exception.getMessage(), is("java.lang.IllegalArgumentException: encryption_key cannot be null")); + } + + @Test + public void testCreateJwtWithBadRoles() { + doAnswer(invocation -> true).when(tokenManager).issueOnBehalfOfTokenAllowed(); + final ThreadContext threadContext = new ThreadContext(Settings.EMPTY); + threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, new User("Jon", List.of(), null)); + when(threadPool.getThreadContext()).thenReturn(threadContext); + final ConfigModel configModel = mock(ConfigModel.class); + tokenManager.onConfigModelChanged(configModel); + when(configModel.mapSecurityRoles(any(), any())).thenReturn(null); + + createMockJwtVendorInTokenManager(true); + + final Throwable exception = assertThrows(RuntimeException.class, () -> { + try { + tokenManager.issueOnBehalfOfToken(null, new OnBehalfOfClaims("elmo", 90000000L)); + } catch (final Exception e) { + throw new RuntimeException(e); + } + }); + assertThat(exception.getMessage(), is("java.lang.IllegalArgumentException: Roles cannot be null")); + } + @Test public void issueApiToken_success() throws Exception { doAnswer(invockation -> new ClusterName("cluster17")).when(cs).getClusterName(); @@ -285,7 +350,7 @@ public void issueApiToken_success() throws Exception { final ConfigModel configModel = mock(ConfigModel.class); tokenManager.onConfigModelChanged(configModel); - createMockJwtVendorInTokenManager(); + createMockJwtVendorInTokenManager(false); final ExpiringBearerAuthToken authToken = mock(ExpiringBearerAuthToken.class); when(jwtVendor.createJwt(any(), any(), any(), any())).thenReturn(authToken); diff --git a/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java b/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java index c713d8a286..9fdba2b407 100644 --- a/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java +++ b/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java @@ -32,7 +32,6 @@ import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.security.action.apitokens.ApiTokenRepository; -import org.opensearch.security.action.apitokens.Permissions; import org.opensearch.security.auditlog.NullAuditLog; import org.opensearch.security.securityconf.ConfigModel; import org.opensearch.security.securityconf.DynamicConfigModel; @@ -41,7 +40,6 @@ import org.opensearch.security.securityconf.impl.v7.RoleV7; import org.opensearch.security.user.User; -import org.mockito.ArgumentMatchers; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.mockito.quality.Strictness; @@ -151,8 +149,6 @@ public void testEvaluate_Unsuccessful() throws Exception { } PrivilegesEvaluator createPrivilegesEvaluator(SecurityDynamicConfiguration roles) { - ApiTokenRepository mockApiTokenRepository = mock(ApiTokenRepository.class); - when(mockApiTokenRepository.getApiTokenPermissionsForUser(ArgumentMatchers.any())).thenReturn(new Permissions()); PrivilegesEvaluator privilegesEvaluator = new PrivilegesEvaluator( clusterService, () -> clusterService.state(), @@ -166,7 +162,7 @@ PrivilegesEvaluator createPrivilegesEvaluator(SecurityDynamicConfiguration Date: Thu, 23 Jan 2025 16:04:36 -0500 Subject: [PATCH 29/30] Separate the calls out based on type Signed-off-by: Derek Ho --- .../privileges/ActionPrivilegesTest.java | 4 +- .../security/privileges/ActionPrivileges.java | 310 ++++++++++-------- 2 files changed, 184 insertions(+), 130 deletions(-) diff --git a/src/integrationTest/java/org/opensearch/security/privileges/ActionPrivilegesTest.java b/src/integrationTest/java/org/opensearch/security/privileges/ActionPrivilegesTest.java index 9e3655d613..bfe6e600c9 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/ActionPrivilegesTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/ActionPrivilegesTest.java @@ -457,7 +457,7 @@ public void apiToken_negative_noPermissions() throws Exception { ); PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege(context, requiredActions, resolved("index_a11")); - assertThat(result, isForbidden(missingPrivileges(requiredActions))); + assertThat(result, isForbidden()); } @Test @@ -499,7 +499,7 @@ public void apiTokens_positive_hasExplicit_full() { PrivilegesEvaluatorResponse result = subject.hasExplicitIndexPrivilege(context, requiredActions, resolved("index_a11")); - assertThat(result, isForbidden(missingPrivileges(requiredActions))); + assertThat(result, isForbidden()); } diff --git a/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java b/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java index 840e2dd411..3daa38ad98 100644 --- a/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java +++ b/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java @@ -145,11 +145,29 @@ public ActionPrivileges( } public PrivilegesEvaluatorResponse hasClusterPrivilege(PrivilegesEvaluationContext context, String action) { - return cluster.providesPrivilege(context, action, context.getMappedRoles()); + if (context instanceof RoleBasedPrivilegesEvaluationContext) { + return cluster.providesPrivilege((RoleBasedPrivilegesEvaluationContext) context, action, context.getMappedRoles()); + } else if (context instanceof PermissionBasedPrivilegesEvaluationContext) { + return cluster.apiTokenProvidesClusterPrivilege((PermissionBasedPrivilegesEvaluationContext) context, Set.of(action), false); + } else { + // Not supported + return PrivilegesEvaluatorResponse.insufficient(action); + } } public PrivilegesEvaluatorResponse hasAnyClusterPrivilege(PrivilegesEvaluationContext context, Set actions) { - return cluster.providesAnyPrivilege(context, actions, context.getMappedRoles()); + if (context instanceof RoleBasedPrivilegesEvaluationContext) { + return cluster.providesAnyPrivilege((RoleBasedPrivilegesEvaluationContext) context, actions, context.getMappedRoles()); + } else if (context instanceof PermissionBasedPrivilegesEvaluationContext) { + return cluster.apiTokenProvidesClusterPrivilege((PermissionBasedPrivilegesEvaluationContext) context, actions, false); + } else { + // Not supported + if (actions.size() == 1) { + return PrivilegesEvaluatorResponse.insufficient(actions.iterator().next()); + } else { + return PrivilegesEvaluatorResponse.insufficient("any of " + actions); + } + } } /** @@ -163,7 +181,14 @@ public PrivilegesEvaluatorResponse hasAnyClusterPrivilege(PrivilegesEvaluationCo * Otherwise, allowed will be false and missingPrivileges will contain the name of the given action. */ public PrivilegesEvaluatorResponse hasExplicitClusterPrivilege(PrivilegesEvaluationContext context, String action) { - return cluster.providesExplicitPrivilege(context, action, context.getMappedRoles()); + if (context instanceof RoleBasedPrivilegesEvaluationContext) { + return cluster.providesExplicitPrivilege((RoleBasedPrivilegesEvaluationContext) context, action, context.getMappedRoles()); + } else if (context instanceof PermissionBasedPrivilegesEvaluationContext) { + return cluster.apiTokenProvidesClusterPrivilege((PermissionBasedPrivilegesEvaluationContext) context, Set.of(action), true); + } else { + // Not supported + return PrivilegesEvaluatorResponse.insufficient(action); + } } /** @@ -181,44 +206,62 @@ public PrivilegesEvaluatorResponse hasIndexPrivilege( Set actions, IndexResolverReplacer.Resolved resolvedIndices ) { - PrivilegesEvaluatorResponse response = this.index.providesWildcardPrivilege(context, actions); - if (response != null) { - return response; - } + if (context instanceof RoleBasedPrivilegesEvaluationContext) { + PrivilegesEvaluatorResponse response = this.index.providesWildcardPrivilege(context, actions); + if (response != null) { + return response; + } - 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. - log.debug("No local indices; grant the request"); - 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. + log.debug("No local indices; grant the request"); + return PrivilegesEvaluatorResponse.ok(); + } - // TODO one might want to consider to create a semantic wrapper for action in order to be better tell apart - // what's the action and what's the index in the generic parameters of CheckTable. - CheckTable checkTable = CheckTable.create( - resolvedIndices.getAllIndicesResolved(context.getClusterStateSupplier(), context.getIndexNameExpressionResolver()), - actions - ); + // TODO one might want to consider to create a semantic wrapper for action in order to be better tell apart + // what's the action and what's the index in the generic parameters of CheckTable. + CheckTable checkTable = CheckTable.create( + resolvedIndices.getAllIndicesResolved(context.getClusterStateSupplier(), context.getIndexNameExpressionResolver()), + actions + ); - StatefulIndexPrivileges statefulIndex = this.statefulIndex.get(); - PrivilegesEvaluatorResponse resultFromStatefulIndex = null; + StatefulIndexPrivileges statefulIndex = this.statefulIndex.get(); + PrivilegesEvaluatorResponse resultFromStatefulIndex = null; - Map indexMetadata = this.indexMetadataSupplier.get(); + Map indexMetadata = this.indexMetadataSupplier.get(); - if (statefulIndex != null) { - resultFromStatefulIndex = statefulIndex.providesPrivilege(actions, resolvedIndices, context, checkTable, indexMetadata); + if (statefulIndex != null) { + resultFromStatefulIndex = statefulIndex.providesPrivilege(actions, resolvedIndices, context, checkTable, indexMetadata); - if (resultFromStatefulIndex != null) { - // If we get a result from statefulIndex, we are done. - return resultFromStatefulIndex; + if (resultFromStatefulIndex != null) { + // If we get a result from statefulIndex, we are done. + return resultFromStatefulIndex; + } + + // Otherwise, we need to carry on checking privileges using the non-stateful object. + // Note: statefulIndex.hasPermission() modifies as a side effect the checkTable. + // We can carry on using this as an intermediate result and further complete checkTable below. } + return this.index.providesPrivilege( + (RoleBasedPrivilegesEvaluationContext) context, + actions, + resolvedIndices, + checkTable, + indexMetadata + ); + } else if (context instanceof PermissionBasedPrivilegesEvaluationContext) { + return this.index.apiTokenProvidesIndexPrivilege( + (PermissionBasedPrivilegesEvaluationContext) context, + resolvedIndices, + actions, + false + ); + } else { + // Not supported + return PrivilegesEvaluatorResponse.insufficient("No explicit privileges have been provided for the referenced indices."); - // Otherwise, we need to carry on checking privileges using the non-stateful object. - // Note: statefulIndex.hasPermission() modifies as a side effect the checkTable. - // We can carry on using this as an intermediate result and further complete checkTable below. } - - return this.index.providesPrivilege(context, actions, resolvedIndices, checkTable, indexMetadata); } /** @@ -233,8 +276,20 @@ public PrivilegesEvaluatorResponse hasExplicitIndexPrivilege( Set actions, IndexResolverReplacer.Resolved resolvedIndices ) { - CheckTable checkTable = CheckTable.create(resolvedIndices.getAllIndices(), actions); - return this.index.providesExplicitPrivilege(context, actions, resolvedIndices, checkTable, this.indexMetadataSupplier.get()); + if (context instanceof RoleBasedPrivilegesEvaluationContext) { + CheckTable checkTable = CheckTable.create(resolvedIndices.getAllIndices(), actions); + return this.index.providesExplicitPrivilege(context, actions, resolvedIndices, checkTable, this.indexMetadataSupplier.get()); + } else if (context instanceof PermissionBasedPrivilegesEvaluationContext) { + return this.index.apiTokenProvidesIndexPrivilege( + (PermissionBasedPrivilegesEvaluationContext) context, + resolvedIndices, + actions, + true + ); + } else { + // Not supported + return PrivilegesEvaluatorResponse.insufficient("any of " + actions); + } } /** @@ -423,7 +478,7 @@ static class ClusterPrivileges { * provided roles. Returns a PrivilegesEvaluatorResponse with allowed=true if privileges are available. * Otherwise, allowed will be false and missingPrivileges will contain the name of the given action. */ - PrivilegesEvaluatorResponse providesPrivilege(PrivilegesEvaluationContext context, String action, Set roles) { + PrivilegesEvaluatorResponse providesPrivilege(RoleBasedPrivilegesEvaluationContext context, String action, Set roles) { // 1: Check roles with wildcards if (CollectionUtils.containsAny(roles, this.rolesWithWildcardPermissions)) { @@ -456,8 +511,7 @@ PrivilegesEvaluatorResponse providesPrivilege(PrivilegesEvaluationContext contex } } - // 4: Evaluate api tokens - return apiTokenProvidesClusterPrivilege(context, Set.of(action), false); + return PrivilegesEvaluatorResponse.insufficient(action); } /** @@ -465,40 +519,38 @@ PrivilegesEvaluatorResponse providesPrivilege(PrivilegesEvaluationContext contex * 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 apiTokenProvidesClusterPrivilege( - PrivilegesEvaluationContext context, + PermissionBasedPrivilegesEvaluationContext context, Set actions, Boolean explicit ) { - if (context instanceof PermissionBasedPrivilegesEvaluationContext) { - Permissions permissions = ((PermissionBasedPrivilegesEvaluationContext) context).getPermissions(); - Set resolvedClusterPermissions = actionGroups.resolve(permissions.getClusterPerm()); - - // Check for wildcard permission - if (!explicit) { - if (resolvedClusterPermissions.contains("*")) { - return PrivilegesEvaluatorResponse.ok(); - } - } + Permissions permissions = context.getPermissions(); + 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 (!"*".equals(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 (!"*".equals(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(); } } } @@ -520,7 +572,11 @@ PrivilegesEvaluatorResponse apiTokenProvidesClusterPrivilege( * Returns a PrivilegesEvaluatorResponse with allowed=true if privileges are available. * Otherwise, allowed will be false and missingPrivileges will contain the name of the given action. */ - PrivilegesEvaluatorResponse providesExplicitPrivilege(PrivilegesEvaluationContext context, String action, Set roles) { + PrivilegesEvaluatorResponse providesExplicitPrivilege( + RoleBasedPrivilegesEvaluationContext context, + String action, + Set roles + ) { // 1: Check well-known actions - this should cover most cases ImmutableCompactSubSet rolesWithPrivileges = this.actionToRoles.get(action); @@ -540,7 +596,7 @@ PrivilegesEvaluatorResponse providesExplicitPrivilege(PrivilegesEvaluationContex } } - return apiTokenProvidesClusterPrivilege(context, Set.of(action), true); + return PrivilegesEvaluatorResponse.insufficient(action); } /** @@ -548,7 +604,11 @@ PrivilegesEvaluatorResponse providesExplicitPrivilege(PrivilegesEvaluationContex * provided roles. Returns a PrivilegesEvaluatorResponse with allowed=true if privileges are available. * Otherwise, allowed will be false and missingPrivileges will contain the name of the given action. */ - PrivilegesEvaluatorResponse providesAnyPrivilege(PrivilegesEvaluationContext context, Set actions, Set roles) { + PrivilegesEvaluatorResponse providesAnyPrivilege( + RoleBasedPrivilegesEvaluationContext context, + Set actions, + Set roles + ) { // 1: Check roles with wildcards if (CollectionUtils.containsAny(roles, this.rolesWithWildcardPermissions)) { return PrivilegesEvaluatorResponse.ok(); @@ -586,7 +646,11 @@ PrivilegesEvaluatorResponse providesAnyPrivilege(PrivilegesEvaluationContext con } } - return apiTokenProvidesClusterPrivilege(context, actions, false); + if (actions.size() == 1) { + return PrivilegesEvaluatorResponse.insufficient(actions.iterator().next()); + } else { + return PrivilegesEvaluatorResponse.insufficient("any of " + actions); + } } } @@ -804,7 +868,7 @@ static class IndexPrivileges { * checkTable instance as checked. */ PrivilegesEvaluatorResponse providesPrivilege( - PrivilegesEvaluationContext context, + RoleBasedPrivilegesEvaluationContext context, Set actions, IndexResolverReplacer.Resolved resolvedIndices, CheckTable checkTable, @@ -887,7 +951,13 @@ PrivilegesEvaluatorResponse providesPrivilege( return PrivilegesEvaluatorResponse.partiallyOk(availableIndices, checkTable).evaluationExceptions(exceptions); } - return apiTokenProvidesIndexPrivilege(checkTable, context, exceptions, resolvedIndices, actions, false); + 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); } /** @@ -952,84 +1022,68 @@ PrivilegesEvaluatorResponse providesExplicitPrivilege( } } } - return apiTokenProvidesIndexPrivilege(checkTable, context, exceptions, resolvedIndices, actions, true); + return PrivilegesEvaluatorResponse.insufficient(checkTable) + .reason("No explicit privileges have been provided for the referenced indices.") + .evaluationExceptions(exceptions); } PrivilegesEvaluatorResponse apiTokenProvidesIndexPrivilege( - CheckTable checkTable, - PrivilegesEvaluationContext context, - List exceptions, + PermissionBasedPrivilegesEvaluationContext context, IndexResolverReplacer.Resolved resolvedIndices, Set actions, Boolean explicit ) { - if (context instanceof PermissionBasedPrivilegesEvaluationContext) { - - Permissions permissions = ((PermissionBasedPrivilegesEvaluationContext) context).getPermissions(); - 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; - } + Permissions permissions = context.getPermissions(); + 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("Insufficient permissions for the index" + concreteIndex); + } } - 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(); } } From 7a20de31c8b1a2db6f6dda51a1302afaebc8dc72 Mon Sep 17 00:00:00 2001 From: Derek Ho Date: Tue, 4 Feb 2025 14:56:16 -0500 Subject: [PATCH 30/30] PR comments Signed-off-by: Derek Ho --- .../security/authtoken/jwt/JwtVendor.java | 7 ++++- .../configuration/DlsFlsValveImpl.java | 26 +++++++++---------- .../SecurityFlsDlsIndexSearcherWrapper.java | 12 ++++----- .../security/privileges/ActionPrivileges.java | 16 +++++++----- .../securityconf/DynamicConfigFactory.java | 8 +++--- .../securityconf/DynamicConfigModelV7.java | 8 +++--- 6 files changed, 43 insertions(+), 34 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 a522056812..8fd3589ebe 100644 --- a/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java +++ b/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java @@ -11,6 +11,8 @@ 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; @@ -83,11 +85,14 @@ static Tuple createJwkFromSettings(final Settings settings) { } } + @SuppressWarnings("removal") public ExpiringBearerAuthToken createJwt(JwtClaimsBuilder claimsBuilder, String subject, Date expiryTime, Long expirySeconds) throws JOSEException, ParseException { final JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.parse(signingKey.getAlgorithm().getName())).build(); - final SignedJWT signedJwt = new SignedJWT(header, claimsBuilder.build()); + final SignedJWT signedJwt = AccessController.doPrivileged( + (PrivilegedAction) () -> new SignedJWT(header, claimsBuilder.build()) + ); // Sign the JWT so it can be serialized signedJwt.sign(signer); diff --git a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java index 880a828f26..498b908e5d 100644 --- a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java +++ b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java @@ -373,14 +373,14 @@ public void handleSearchContext(SearchContext searchContext, ThreadPool threadPo return; } - PrivilegesEvaluationContext PrivilegesEvaluationContext = this.dlsFlsBaseContext.getPrivilegesEvaluationContext(); - if (PrivilegesEvaluationContext == null) { + PrivilegesEvaluationContext privilegesEvaluationContext = this.dlsFlsBaseContext.getPrivilegesEvaluationContext(); + if (privilegesEvaluationContext == null) { return; } DlsFlsProcessedConfig config = this.dlsFlsProcessedConfig.get(); - DlsRestriction dlsRestriction = config.getDocumentPrivileges().getRestriction(PrivilegesEvaluationContext, index); + DlsRestriction dlsRestriction = config.getDocumentPrivileges().getRestriction(privilegesEvaluationContext, index); if (log.isTraceEnabled()) { log.trace("handleSearchContext(); index: {}; dlsRestriction: {}", index, dlsRestriction); @@ -449,36 +449,36 @@ public DlsFlsProcessedConfig getCurrentConfig() { @Override public boolean hasFlsOrFieldMasking(String index) throws PrivilegesEvaluationException { - PrivilegesEvaluationContext PrivilegesEvaluationContext = this.dlsFlsBaseContext.getPrivilegesEvaluationContext(); - if (PrivilegesEvaluationContext == null) { + PrivilegesEvaluationContext privilegesEvaluationContext = this.dlsFlsBaseContext.getPrivilegesEvaluationContext(); + if (privilegesEvaluationContext == null) { return false; } DlsFlsProcessedConfig config = this.dlsFlsProcessedConfig.get(); - return !config.getFieldPrivileges().isUnrestricted(PrivilegesEvaluationContext, index) - || !config.getFieldMasking().isUnrestricted(PrivilegesEvaluationContext, index); + return !config.getFieldPrivileges().isUnrestricted(privilegesEvaluationContext, index) + || !config.getFieldMasking().isUnrestricted(privilegesEvaluationContext, index); } @Override public boolean hasFieldMasking(String index) throws PrivilegesEvaluationException { - PrivilegesEvaluationContext PrivilegesEvaluationContext = this.dlsFlsBaseContext.getPrivilegesEvaluationContext(); - if (PrivilegesEvaluationContext == null) { + PrivilegesEvaluationContext privilegesEvaluationContext = this.dlsFlsBaseContext.getPrivilegesEvaluationContext(); + if (privilegesEvaluationContext == null) { return false; } DlsFlsProcessedConfig config = this.dlsFlsProcessedConfig.get(); - return !config.getFieldMasking().isUnrestricted(PrivilegesEvaluationContext, index); + return !config.getFieldMasking().isUnrestricted(privilegesEvaluationContext, index); } @Override public boolean isFieldAllowed(String index, String field) throws PrivilegesEvaluationException { - PrivilegesEvaluationContext PrivilegesEvaluationContext = this.dlsFlsBaseContext.getPrivilegesEvaluationContext(); - if (PrivilegesEvaluationContext == null) { + PrivilegesEvaluationContext privilegesEvaluationContext = this.dlsFlsBaseContext.getPrivilegesEvaluationContext(); + if (privilegesEvaluationContext == null) { return true; } DlsFlsProcessedConfig config = this.dlsFlsProcessedConfig.get(); - return config.getFieldPrivileges().getRestriction(PrivilegesEvaluationContext, index).isAllowed(field); + return config.getFieldPrivileges().getRestriction(privilegesEvaluationContext, index).isAllowed(field); } private static InternalAggregation aggregateBuckets(InternalAggregation aggregation) { diff --git a/src/main/java/org/opensearch/security/configuration/SecurityFlsDlsIndexSearcherWrapper.java b/src/main/java/org/opensearch/security/configuration/SecurityFlsDlsIndexSearcherWrapper.java index 8a258a1307..4f7a412097 100644 --- a/src/main/java/org/opensearch/security/configuration/SecurityFlsDlsIndexSearcherWrapper.java +++ b/src/main/java/org/opensearch/security/configuration/SecurityFlsDlsIndexSearcherWrapper.java @@ -110,13 +110,13 @@ public SecurityFlsDlsIndexSearcherWrapper( protected DirectoryReader dlsFlsWrap(final DirectoryReader reader, boolean isAdmin) throws IOException { final ShardId shardId = ShardUtils.extractShardId(reader); - PrivilegesEvaluationContext PrivilegesEvaluationContext = this.dlsFlsBaseContext.getPrivilegesEvaluationContext(); + PrivilegesEvaluationContext privilegesEvaluationContext = this.dlsFlsBaseContext.getPrivilegesEvaluationContext(); if (log.isTraceEnabled()) { - log.trace("dlsFlsWrap(); index: {}; privilegeEvaluationContext: {}", index.getName(), PrivilegesEvaluationContext); + log.trace("dlsFlsWrap(); index: {}; privilegeEvaluationContext: {}", index.getName(), privilegesEvaluationContext); } - if (isAdmin || PrivilegesEvaluationContext == null) { + if (isAdmin || privilegesEvaluationContext == null) { return new DlsFlsFilterLeafReader.DlsFlsDirectoryReader( reader, FieldPrivileges.FlsRule.ALLOW_ALL, @@ -137,13 +137,13 @@ protected DirectoryReader dlsFlsWrap(final DirectoryReader reader, boolean isAdm DlsRestriction dlsRestriction; if (!this.dlsFlsBaseContext.isDlsDoneOnFilterLevel()) { - dlsRestriction = config.getDocumentPrivileges().getRestriction(PrivilegesEvaluationContext, index.getName()); + dlsRestriction = config.getDocumentPrivileges().getRestriction(privilegesEvaluationContext, index.getName()); } else { dlsRestriction = DlsRestriction.NONE; } - FieldPrivileges.FlsRule flsRule = config.getFieldPrivileges().getRestriction(PrivilegesEvaluationContext, index.getName()); - FieldMasking.FieldMaskingRule fmRule = config.getFieldMasking().getRestriction(PrivilegesEvaluationContext, index.getName()); + FieldPrivileges.FlsRule flsRule = config.getFieldPrivileges().getRestriction(privilegesEvaluationContext, index.getName()); + FieldMasking.FieldMaskingRule fmRule = config.getFieldMasking().getRestriction(privilegesEvaluationContext, index.getName()); Query dlsQuery; diff --git a/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java b/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java index 3daa38ad98..dcb6cded2d 100644 --- a/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java +++ b/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java @@ -251,10 +251,12 @@ public PrivilegesEvaluatorResponse hasIndexPrivilege( indexMetadata ); } else if (context instanceof PermissionBasedPrivilegesEvaluationContext) { + Map indexMetadata = this.indexMetadataSupplier.get(); return this.index.apiTokenProvidesIndexPrivilege( (PermissionBasedPrivilegesEvaluationContext) context, resolvedIndices, actions, + indexMetadata, false ); } else { @@ -284,6 +286,7 @@ public PrivilegesEvaluatorResponse hasExplicitIndexPrivilege( (PermissionBasedPrivilegesEvaluationContext) context, resolvedIndices, actions, + this.indexMetadataSupplier.get(), true ); } else { @@ -1031,6 +1034,7 @@ PrivilegesEvaluatorResponse apiTokenProvidesIndexPrivilege( PermissionBasedPrivilegesEvaluationContext context, IndexResolverReplacer.Resolved resolvedIndices, Set actions, + Map indexMetadata, Boolean explicit ) { Permissions permissions = context.getPermissions(); @@ -1042,14 +1046,14 @@ PrivilegesEvaluatorResponse apiTokenProvidesIndexPrivilege( // Check each index permission for (ApiToken.IndexPermission indexPermission : indexPermissions) { // First check if this permission applies to this index + IndexPattern indexPattern = IndexPattern.from(indexPermission.getIndexPatterns()); boolean indexMatched = false; - for (String pattern : indexPermission.getIndexPatterns()) { - if (WildcardMatcher.from(pattern).test(concreteIndex)) { - indexMatched = true; - break; - } + try { + indexMatched = indexPattern.matches(concreteIndex, context, indexMetadata); + } catch (PrivilegesEvaluationException e) { + // We can ignore these errors, as this max leads to fewer privileges than available + log.error("Error while evaluating index pattern. Ignoring entry"); } - if (!indexMatched) { continue; } diff --git a/src/main/java/org/opensearch/security/securityconf/DynamicConfigFactory.java b/src/main/java/org/opensearch/security/securityconf/DynamicConfigFactory.java index 307b40d328..1f86f459c8 100644 --- a/src/main/java/org/opensearch/security/securityconf/DynamicConfigFactory.java +++ b/src/main/java/org/opensearch/security/securityconf/DynamicConfigFactory.java @@ -128,7 +128,7 @@ public final static SecurityDynamicConfiguration addStatics(SecurityDynam private final Path configPath; private final InternalAuthenticationBackend iab; private final ClusterInfoHolder cih; - private final ApiTokenRepository ar; + private final ApiTokenRepository apiTokenRepository; SecurityDynamicConfiguration config; @@ -140,7 +140,7 @@ public DynamicConfigFactory( ThreadPool threadPool, ClusterInfoHolder cih, PasswordHasher passwordHasher, - ApiTokenRepository ar + ApiTokenRepository apiTokenRepository ) { super(); this.cr = cr; @@ -148,7 +148,7 @@ public DynamicConfigFactory( this.configPath = configPath; this.cih = cih; this.iab = new InternalAuthenticationBackend(passwordHasher); - this.ar = ar; + this.apiTokenRepository = apiTokenRepository; if (opensearchSettings.getAsBoolean(ConfigConstants.SECURITY_UNSUPPORTED_LOAD_STATIC_RESOURCES, true)) { try { @@ -273,7 +273,7 @@ public void onChange(ConfigurationMap typeToConfig) { ); // rebuild v7 Models - dcm = new DynamicConfigModelV7(getConfigV7(config), opensearchSettings, configPath, iab, this.cih, ar); + dcm = new DynamicConfigModelV7(getConfigV7(config), opensearchSettings, configPath, iab, this.cih, apiTokenRepository); ium = new InternalUsersModelV7(internalusers, roles, rolesmapping); cm = new ConfigModelV7(roles, rolesmapping, actionGroups, tenants, dcm, opensearchSettings); diff --git a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java index 44cd041d72..55271e960b 100644 --- a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java +++ b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java @@ -87,7 +87,7 @@ public class DynamicConfigModelV7 extends DynamicConfigModel { private List> ipClientBlockRegistries; private Multimap> authBackendClientBlockRegistries; private final ClusterInfoHolder cih; - private final ApiTokenRepository ar; + private final ApiTokenRepository apiTokenRepository; public DynamicConfigModelV7( ConfigV7 config, @@ -95,7 +95,7 @@ public DynamicConfigModelV7( Path configPath, InternalAuthenticationBackend iab, ClusterInfoHolder cih, - ApiTokenRepository ar + ApiTokenRepository apiTokenRepository ) { super(); this.config = config; @@ -103,7 +103,7 @@ public DynamicConfigModelV7( this.configPath = configPath; this.iab = iab; this.cih = cih; - this.ar = ar; + this.apiTokenRepository = apiTokenRepository; buildAAA(); } @@ -392,7 +392,7 @@ private void buildAAA() { if (!isKeyNull(apiTokenSettings, "signing_key")) { final AuthDomain _ad = new AuthDomain( new NoOpAuthenticationBackend(Settings.EMPTY, null), - new ApiTokenAuthenticator(getDynamicApiTokenSettings(), this.cih.getClusterName(), ar), + new ApiTokenAuthenticator(getDynamicApiTokenSettings(), this.cih.getClusterName(), apiTokenRepository), false, -2 );