From cacb0bba7e37f18e74ed056b1fa0cb387a2514f1 Mon Sep 17 00:00:00 2001 From: Stephen Crawford Date: Wed, 5 Jul 2023 10:38:08 -0400 Subject: [PATCH 1/2] Rebase Signed-off-by: Stephen Crawford --- src/integrationTest/resources/config.yml | 3 + .../security/OpenSearchSecurityPlugin.java | 9 +- .../security/auth/BackendRegistry.java | 206 ++++++------ .../jwt/EncryptionDecryptionUtil.java | 55 ++++ .../security/authtoken/jwt/JwtVendor.java | 191 +++++++++++ .../security/dlic/rest/support/Utils.java | 21 +- .../http/HTTPOnBehalfOfJwtAuthenticator.java | 277 ++++++++++++++++ .../identity/SecurityTokenManager.java | 143 +++++++++ .../securityconf/DynamicConfigModel.java | 3 + .../securityconf/DynamicConfigModelV6.java | 5 + .../securityconf/DynamicConfigModelV7.java | 7 + .../security/securityconf/impl/CType.java | 1 + .../securityconf/impl/v6/ConfigV6.java | 146 +++++---- .../securityconf/impl/v7/ConfigV7.java | 162 ++++++---- .../user/InternalUserTokenHandler.java | 99 ++++++ .../opensearch/security/user/UserService.java | 162 +++++----- .../security/user/UserTokenHandler.java | 161 ++++++++++ .../auth/InternalUserTokenHandlerTests.java | 108 +++++++ .../auth/SecurityTokenManagerTests.java | 201 ++++++++++++ .../security/auth/UserTokenHandlerTests.java | 226 +++++++++++++ .../authtoken/jwt/JwtVendorTests.java | 117 +++++++ .../HTTPOnBehalfOfJwtAuthenticatorTests.java | 302 ++++++++++++++++++ src/test/resources/config.yml | 3 + .../resources/restapi/securityconfig.json | 4 +- .../restapi/securityconfig_nondefault.json | 6 +- 25 files changed, 2316 insertions(+), 302 deletions(-) create mode 100644 src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java create mode 100644 src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java create mode 100644 src/main/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticator.java create mode 100644 src/main/java/org/opensearch/security/identity/SecurityTokenManager.java create mode 100644 src/main/java/org/opensearch/security/user/InternalUserTokenHandler.java create mode 100644 src/main/java/org/opensearch/security/user/UserTokenHandler.java create mode 100644 src/test/java/org/opensearch/security/auth/InternalUserTokenHandlerTests.java create mode 100644 src/test/java/org/opensearch/security/auth/SecurityTokenManagerTests.java create mode 100644 src/test/java/org/opensearch/security/auth/UserTokenHandlerTests.java create mode 100644 src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTests.java create mode 100644 src/test/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticatorTests.java diff --git a/src/integrationTest/resources/config.yml b/src/integrationTest/resources/config.yml index 5e929c0e2a..e5a040e1ee 100644 --- a/src/integrationTest/resources/config.yml +++ b/src/integrationTest/resources/config.yml @@ -15,3 +15,6 @@ config: authentication_backend: type: "internal" config: {} + on_behalf_of: + signing_key: "signing key" + encryption_key: "encryption key" diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index e4ca1050d6..d4cbd61010 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -105,6 +105,7 @@ import org.opensearch.indices.SystemIndexDescriptor; import org.opensearch.indices.breaker.CircuitBreakerService; import org.opensearch.plugins.ClusterPlugin; +import org.opensearch.plugins.IdentityPlugin; import org.opensearch.plugins.MapperPlugin; import org.opensearch.repositories.RepositoriesService; import org.opensearch.rest.RestController; @@ -191,7 +192,7 @@ import org.opensearch.watcher.ResourceWatcherService; // CS-ENFORCE-SINGLE -public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin implements ClusterPlugin, MapperPlugin { +public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin implements ClusterPlugin, MapperPlugin, IdentityPlugin { private static final String KEYWORD = ".keyword"; private static final Logger actionTrace = LogManager.getLogger("opendistro_security_action_trace"); @@ -205,6 +206,7 @@ public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin private volatile SecurityInterceptor si; private volatile PrivilegesEvaluator evaluator; private volatile UserService userService; + private volatile SecurityTokenManager securityTokenManager; private volatile ThreadPool threadPool; private volatile ConfigurationRepository cr; private volatile AdminDNs adminDns; @@ -988,6 +990,8 @@ public Collection createComponents( userService = new UserService(cs, cr, settings, localClient); + securityTokenManager = new SecurityTokenManager(threadPool, clusterService, cr, localClient, settings, userService); + final XFFResolver xffResolver = new XFFResolver(threadPool); backendRegistry = new BackendRegistry(settings, adminDns, xffResolver, auditLog, threadPool); @@ -1029,6 +1033,8 @@ public Collection createComponents( compatConfig ); + HTTPOnBehalfOfJwtAuthenticator acInstance = new HTTPOnBehalfOfJwtAuthenticator(); + final DynamicConfigFactory dcf = new DynamicConfigFactory(cr, settings, configPath, localClient, threadPool, cih); dcf.registerDCFListener(backendRegistry); dcf.registerDCFListener(compatConfig); @@ -1036,6 +1042,7 @@ public Collection createComponents( dcf.registerDCFListener(xffResolver); dcf.registerDCFListener(evaluator); dcf.registerDCFListener(securityRestHandler); + dcf.registerDCFListener(acInstance); if (!(auditLog instanceof NullAuditLog)) { // Don't register if advanced modules is disabled in which case auditlog is instance of NullAuditLog dcf.registerDCFListener(auditLog); diff --git a/src/main/java/org/opensearch/security/auth/BackendRegistry.java b/src/main/java/org/opensearch/security/auth/BackendRegistry.java index 0a287d19f5..4c44075871 100644 --- a/src/main/java/org/opensearch/security/auth/BackendRegistry.java +++ b/src/main/java/org/opensearch/security/auth/BackendRegistry.java @@ -95,43 +95,43 @@ public class BackendRegistry { private void createCaches() { userCache = CacheBuilder.newBuilder() - .expireAfterWrite(ttlInMin, TimeUnit.MINUTES) - .removalListener(new RemovalListener() { - @Override - public void onRemoval(RemovalNotification notification) { - log.debug("Clear user cache for {} due to {}", notification.getKey().getUsername(), notification.getCause()); - } - }) - .build(); + .expireAfterWrite(ttlInMin, TimeUnit.MINUTES) + .removalListener(new RemovalListener() { + @Override + public void onRemoval(RemovalNotification notification) { + log.debug("Clear user cache for {} due to {}", notification.getKey().getUsername(), notification.getCause()); + } + }) + .build(); restImpersonationCache = CacheBuilder.newBuilder() - .expireAfterWrite(ttlInMin, TimeUnit.MINUTES) - .removalListener(new RemovalListener() { - @Override - public void onRemoval(RemovalNotification notification) { - log.debug("Clear user cache for {} due to {}", notification.getKey(), notification.getCause()); - } - }) - .build(); + .expireAfterWrite(ttlInMin, TimeUnit.MINUTES) + .removalListener(new RemovalListener() { + @Override + public void onRemoval(RemovalNotification notification) { + log.debug("Clear user cache for {} due to {}", notification.getKey(), notification.getCause()); + } + }) + .build(); restRoleCache = CacheBuilder.newBuilder() - .expireAfterWrite(ttlInMin, TimeUnit.MINUTES) - .removalListener(new RemovalListener>() { - @Override - public void onRemoval(RemovalNotification> notification) { - log.debug("Clear user cache for {} due to {}", notification.getKey(), notification.getCause()); - } - }) - .build(); + .expireAfterWrite(ttlInMin, TimeUnit.MINUTES) + .removalListener(new RemovalListener>() { + @Override + public void onRemoval(RemovalNotification> notification) { + log.debug("Clear user cache for {} due to {}", notification.getKey(), notification.getCause()); + } + }) + .build(); } public BackendRegistry( - final Settings settings, - final AdminDNs adminDns, - final XFFResolver xffResolver, - final AuditLog auditLog, - final ThreadPool threadPool + final Settings settings, + final AdminDNs adminDns, + final XFFResolver xffResolver, + final AuditLog auditLog, + final ThreadPool threadPool ) { this.adminDns = adminDns; this.opensearchSettings = settings; @@ -163,7 +163,7 @@ public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { invalidateCache(); anonymousAuthEnabled = dcm.isAnonymousAuthenticationEnabled()// config.dynamic.http.anonymous_auth_enabled - && !opensearchSettings.getAsBoolean(ConfigConstants.SECURITY_COMPLIANCE_DISABLE_ANONYMOUS_AUTHENTICATION, false); + && !opensearchSettings.getAsBoolean(ConfigConstants.SECURITY_COMPLIANCE_DISABLE_ANONYMOUS_AUTHENTICATION, false); restAuthDomains = Collections.unmodifiableSortedSet(dcm.getRestAuthDomains()); restAuthorizers = Collections.unmodifiableSet(dcm.getRestAuthorizers()); @@ -187,7 +187,7 @@ public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { public boolean authenticate(final RestRequest request, final RestChannel channel, final ThreadContext threadContext) { final boolean isDebugEnabled = log.isDebugEnabled(); if (request.getHttpChannel().getRemoteAddress() instanceof InetSocketAddress - && isBlocked(((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).getAddress())) { + && isBlocked(((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).getAddress())) { if (isDebugEnabled) { log.debug("Rejecting REST request because of blocked address: {}", request.getHttpChannel().getRemoteAddress()); } @@ -237,10 +237,10 @@ && isBlocked(((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).g for (final AuthDomain authDomain : restAuthDomains) { if (isDebugEnabled) { log.debug( - "Check authdomain for rest {}/{} or {} in total", - authDomain.getBackend().getType(), - authDomain.getOrder(), - restAuthDomains.size() + "Check authdomain for rest {}/{} or {} in total", + authDomain.getBackend().getType(), + authDomain.getOrder(), + restAuthDomains.size() ); } @@ -311,22 +311,22 @@ && isBlocked(((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).g if (authenticatedUser == null) { if (isDebugEnabled) { log.debug( - "Cannot authenticate rest user {} (or add roles) with authdomain {}/{} of {}, try next", - ac.getUsername(), - authDomain.getBackend().getType(), - authDomain.getOrder(), - restAuthDomains + "Cannot authenticate rest user {} (or add roles) with authdomain {}/{} of {}, try next", + ac.getUsername(), + authDomain.getBackend().getType(), + authDomain.getOrder(), + restAuthDomains ); } for (AuthFailureListener authFailureListener : this.authBackendFailureListeners.get( - authDomain.getBackend().getClass().getName() + authDomain.getBackend().getClass().getName() )) { authFailureListener.onAuthFailure( - (request.getHttpChannel().getRemoteAddress() instanceof InetSocketAddress) - ? ((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).getAddress() - : null, - ac, - request + (request.getHttpChannel().getRemoteAddress() instanceof InetSocketAddress) + ? ((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).getAddress() + : null, + ac, + request ); } continue; @@ -336,10 +336,10 @@ && isBlocked(((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).g log.error("Cannot authenticate rest user because admin user is not permitted to login via HTTP"); auditLog.logFailedLogin(authenticatedUser.getName(), true, null, request); channel.sendResponse( - new BytesRestResponse( - RestStatus.FORBIDDEN, - "Cannot authenticate user because admin user is not permitted to login via HTTP" - ) + new BytesRestResponse( + RestStatus.FORBIDDEN, + "Cannot authenticate user because admin user is not permitted to login via HTTP" + ) ); return false; } @@ -359,14 +359,14 @@ && isBlocked(((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).g if (authenticated) { final User impersonatedUser = impersonate(request, authenticatedUser); threadContext.putTransient( - ConfigConstants.OPENDISTRO_SECURITY_USER, - impersonatedUser == null ? authenticatedUser : impersonatedUser + ConfigConstants.OPENDISTRO_SECURITY_USER, + impersonatedUser == null ? authenticatedUser : impersonatedUser ); auditLog.logSucceededLogin( - (impersonatedUser == null ? authenticatedUser : impersonatedUser).getName(), - false, - authenticatedUser.getName(), - request + (impersonatedUser == null ? authenticatedUser : impersonatedUser).getName(), + false, + authenticatedUser.getName(), + request ); } else { if (isDebugEnabled) { @@ -398,9 +398,9 @@ && isBlocked(((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).g } log.warn( - "Authentication finally failed for {} from {}", - authCredenetials == null ? null : authCredenetials.getUsername(), - remoteAddress + "Authentication finally failed for {} from {}", + authCredenetials == null ? null : authCredenetials.getUsername(), + remoteAddress ); auditLog.logFailedLogin(authCredenetials == null ? null : authCredenetials.getUsername(), false, null, request); return false; @@ -408,9 +408,9 @@ && isBlocked(((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).g } log.warn( - "Authentication finally failed for {} from {}", - authCredenetials == null ? null : authCredenetials.getUsername(), - remoteAddress + "Authentication finally failed for {} from {}", + authCredenetials == null ? null : authCredenetials.getUsername(), + remoteAddress ); auditLog.logFailedLogin(authCredenetials == null ? null : authCredenetials.getUsername(), false, null, request); @@ -425,11 +425,11 @@ && isBlocked(((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).g private void notifyIpAuthFailureListeners(RestRequest request, AuthCredentials authCredentials) { notifyIpAuthFailureListeners( - (request.getHttpChannel().getRemoteAddress() instanceof InetSocketAddress) - ? ((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).getAddress() - : null, - authCredentials, - request + (request.getHttpChannel().getRemoteAddress() instanceof InetSocketAddress) + ? ((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).getAddress() + : null, + authCredentials, + request ); } @@ -445,10 +445,10 @@ private void notifyIpAuthFailureListeners(InetAddress remoteAddress, AuthCredent * @return null if user cannot b authenticated */ private User checkExistsAndAuthz( - final Cache cache, - final User user, - final AuthenticationBackend authenticationBackend, - final Set authorizers + final Cache cache, + final User user, + final AuthenticationBackend authenticationBackend, + final Set authorizers ) { if (user == null) { return null; @@ -463,9 +463,9 @@ private User checkExistsAndAuthz( public User call() throws Exception { if (isTraceEnabled) { log.trace( - "Credentials for user {} not cached, return from {} backend directly", - user.getName(), - authenticationBackend.getType() + "Credentials for user {} not cached, return from {} backend directly", + user.getName(), + authenticationBackend.getType() ); } if (authenticationBackend.exists(user)) { @@ -512,9 +512,9 @@ private void authz(User authenticatedUser, Cache> roleCache, f try { if (isTraceEnabled) { log.trace( - "Backend roles for {} not cached, return from {} backend directly", - authenticatedUser.getName(), - ab.getType() + "Backend roles for {} not cached, return from {} backend directly", + authenticatedUser.getName(), + ab.getType() ); } ab.fillRoles(authenticatedUser, new AuthCredentials(authenticatedUser.getName())); @@ -534,11 +534,11 @@ private void authz(User authenticatedUser, Cache> roleCache, f * @return null if user cannot b authenticated */ private User authcz( - final Cache cache, - Cache> roleCache, - final AuthCredentials ac, - final AuthenticationBackend authBackend, - final Set authorizers + final Cache cache, + Cache> roleCache, + final AuthCredentials ac, + final AuthenticationBackend authBackend, + final Set authorizers ) { if (ac == null) { return null; @@ -557,9 +557,9 @@ private User authcz( public User call() throws Exception { if (log.isTraceEnabled()) { log.trace( - "Credentials for user {} not cached, return from {} backend directly", - ac.getUsername(), - authBackend.getType() + "Credentials for user {} not cached, return from {} backend directly", + ac.getUsername(), + authBackend.getType() ); } final User authenticatedUser = authBackend.authenticate(ac); @@ -591,15 +591,15 @@ private User impersonate(final RestRequest request, final User originalUser) thr if (adminDns.isAdminDN(impersonatedUserHeader)) { throw new OpenSearchSecurityException( - "It is not allowed to impersonate as an adminuser '" + impersonatedUserHeader + "'", - RestStatus.FORBIDDEN + "It is not allowed to impersonate as an adminuser '" + impersonatedUserHeader + "'", + RestStatus.FORBIDDEN ); } if (!adminDns.isRestImpersonationAllowed(originalUser.getName(), impersonatedUserHeader)) { throw new OpenSearchSecurityException( - "'" + originalUser.getName() + "' is not allowed to impersonate as '" + impersonatedUserHeader + "'", - RestStatus.FORBIDDEN + "'" + originalUser.getName() + "' is not allowed to impersonate as '" + impersonatedUserHeader + "'", + RestStatus.FORBIDDEN ); } else { final boolean isDebugEnabled = log.isDebugEnabled(); @@ -607,27 +607,27 @@ private User impersonate(final RestRequest request, final User originalUser) thr for (final AuthDomain authDomain : restAuthDomains) { final AuthenticationBackend authenticationBackend = authDomain.getBackend(); final User impersonatedUser = checkExistsAndAuthz( - restImpersonationCache, - new User(impersonatedUserHeader), - authenticationBackend, - restAuthorizers + restImpersonationCache, + new User(impersonatedUserHeader), + authenticationBackend, + restAuthorizers ); if (impersonatedUser == null) { log.debug( - "Unable to impersonate rest user from '{}' to '{}' because the impersonated user does not exists in {}, try next ...", - originalUser.getName(), - impersonatedUserHeader, - authenticationBackend.getType() + "Unable to impersonate rest user from '{}' to '{}' because the impersonated user does not exists in {}, try next ...", + originalUser.getName(), + impersonatedUserHeader, + authenticationBackend.getType() ); continue; } if (isDebugEnabled) { log.debug( - "Impersonate rest user from '{}' to '{}'", - originalUser.toStringWithAttributes(), - impersonatedUser.toStringWithAttributes() + "Impersonate rest user from '{}' to '{}'", + originalUser.toStringWithAttributes(), + impersonatedUser.toStringWithAttributes() ); } @@ -636,9 +636,9 @@ private User impersonate(final RestRequest request, final User originalUser) thr } log.debug( - "Unable to impersonate rest user from '{}' to '{}' because the impersonated user does not exists", - originalUser.getName(), - impersonatedUserHeader + "Unable to impersonate rest user from '{}' to '{}' because the impersonated user does not exists", + originalUser.getName(), + impersonatedUserHeader ); throw new OpenSearchSecurityException("No such user:" + impersonatedUserHeader, RestStatus.FORBIDDEN); } diff --git a/src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java b/src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java new file mode 100644 index 0000000000..53f1e822b3 --- /dev/null +++ b/src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java @@ -0,0 +1,55 @@ +/* + * 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.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Base64; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +public class EncryptionDecryptionUtil { + + public static String encrypt(final String secret, final String data) { + + byte[] decodedKey = Base64.getDecoder().decode(secret); + + try { + Cipher cipher = Cipher.getInstance("AES"); + // rebuild key using SecretKeySpec + SecretKey originalKey = new SecretKeySpec(Arrays.copyOf(decodedKey, 16), "AES"); + cipher.init(Cipher.ENCRYPT_MODE, originalKey); + byte[] cipherText = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8)); + return Base64.getEncoder().encodeToString(cipherText); + } catch (Exception e) { + throw new RuntimeException("Error occurred while encrypting data", e); + } + } + + public static String decrypt(final String secret, final String encryptedString) { + + byte[] decodedKey = Base64.getDecoder().decode(secret); + + try { + Cipher cipher = Cipher.getInstance("AES"); + // rebuild key using SecretKeySpec + SecretKey originalKey = new SecretKeySpec(Arrays.copyOf(decodedKey, 16), "AES"); + cipher.init(Cipher.DECRYPT_MODE, originalKey); + byte[] cipherText = cipher.doFinal(Base64.getDecoder().decode(encryptedString)); + return new String(cipherText, StandardCharsets.UTF_8); + } catch (Exception e) { + throw new RuntimeException("Error occured while decrypting data", e); + } + } +} diff --git a/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java b/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java new file mode 100644 index 0000000000..54c11f40cf --- /dev/null +++ b/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java @@ -0,0 +1,191 @@ +/* + * 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.time.Instant; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.LongSupplier; + +import com.google.common.base.Strings; +import org.apache.cxf.jaxrs.json.basic.JsonMapObjectReaderWriter; +import org.apache.cxf.rs.security.jose.jwk.JsonWebKey; +import org.apache.cxf.rs.security.jose.jwk.KeyType; +import org.apache.cxf.rs.security.jose.jwk.PublicKeyUse; +import org.apache.cxf.rs.security.jose.jws.JwsUtils; +import org.apache.cxf.rs.security.jose.jwt.JoseJwtProducer; +import org.apache.cxf.rs.security.jose.jwt.JwtClaims; +import org.apache.cxf.rs.security.jose.jwt.JwtToken; +import org.apache.cxf.rs.security.jose.jwt.JwtUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.common.settings.Settings; +import org.opensearch.common.transport.TransportAddress; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.security.securityconf.ConfigModel; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.user.User; +import org.opensearch.threadpool.ThreadPool; + +public class JwtVendor { + private static final Logger logger = LogManager.getLogger(JwtVendor.class); + + private static JsonMapObjectReaderWriter jsonMapReaderWriter = new JsonMapObjectReaderWriter(); + + private String claimsEncryptionKey; + private JsonWebKey signingKey; + private JoseJwtProducer jwtProducer; + private final LongSupplier timeProvider; + + // TODO: Relocate/Remove them at once we make the descisions about the `roles` + private ConfigModel configModel; + private ThreadContext threadContext; + + public JwtVendor(Settings settings) { + JoseJwtProducer jwtProducer = new JoseJwtProducer(); + try { + this.signingKey = createJwkFromSettings(settings); + } catch (Exception e) { + throw new RuntimeException(e); + } + this.jwtProducer = jwtProducer; + if (settings.get("encryption_key") == null) { + throw new RuntimeException("encryption_key cannot be null"); + } else { + this.claimsEncryptionKey = settings.get("encryption_key"); + } + timeProvider = System::currentTimeMillis; + } + + // For testing the expiration in the future + public JwtVendor(Settings settings, final LongSupplier timeProvider) { + JoseJwtProducer jwtProducer = new JoseJwtProducer(); + try { + this.signingKey = createJwkFromSettings(settings); + } catch (Exception e) { + throw new RuntimeException(e); + } + this.jwtProducer = jwtProducer; + if (settings.get("encryption_key") == null) { + throw new RuntimeException("encryption_key cannot be null"); + } else { + this.claimsEncryptionKey = settings.get("encryption_key"); + } + this.timeProvider = timeProvider; + } + + /* + * The default configuration of this web key should be: + * KeyType: OCTET + * PublicKeyUse: SIGN + * Encryption Algorithm: HS512 + * */ + static JsonWebKey createJwkFromSettings(Settings settings) throws Exception { + String signingKey = settings.get("signing_key"); + + if (!Strings.isNullOrEmpty(signingKey)) { + + JsonWebKey jwk = new JsonWebKey(); + + jwk.setKeyType(KeyType.OCTET); + jwk.setAlgorithm("HS512"); + jwk.setPublicKeyUse(PublicKeyUse.SIGN); + jwk.setProperty("k", signingKey); + + return jwk; + } else { + Settings jwkSettings = settings.getAsSettings("jwt").getAsSettings("key"); + + if (jwkSettings.isEmpty()) { + throw new Exception("Settings for key is missing. Please specify at least the option signing_key with a shared secret."); + } + + JsonWebKey jwk = new JsonWebKey(); + + for (String key : jwkSettings.keySet()) { + jwk.setProperty(key, jwkSettings.get(key)); + } + + return jwk; + } + } + + // TODO:Getting roles from User + public Map prepareClaimsForUser(User user, ThreadPool threadPool) { + Map claims = new HashMap<>(); + this.threadContext = threadPool.getThreadContext(); + final TransportAddress caller = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS); + Set mappedRoles = mapRoles(user, caller); + claims.put("sub", user.getName()); + claims.put("roles", String.join(",", mappedRoles)); + return claims; + } + + public Set mapRoles(final User user, final TransportAddress caller) { + return this.configModel.mapSecurityRoles(user, caller); + } + + public String createJwt(String issuer, String subject, String audience, Integer expirySeconds, List roles) throws Exception { + long timeMillis = timeProvider.getAsLong(); + Instant now = Instant.ofEpochMilli(timeProvider.getAsLong()); + + jwtProducer.setSignatureProvider(JwsUtils.getSignatureProvider(signingKey)); + JwtClaims jwtClaims = new JwtClaims(); + JwtToken jwt = new JwtToken(jwtClaims); + + jwtClaims.setIssuer(issuer); + + jwtClaims.setIssuedAt(timeMillis); + + jwtClaims.setSubject(subject); + + jwtClaims.setAudience(audience); + + jwtClaims.setNotBefore(timeMillis); + + if (expirySeconds == null) { + long expiryTime = timeProvider.getAsLong() + (300 * 1000); + jwtClaims.setExpiryTime(expiryTime); + } else if (expirySeconds > 0) { + long expiryTime = timeProvider.getAsLong() + (expirySeconds * 1000); + jwtClaims.setExpiryTime(expiryTime); + } else { + throw new Exception("The expiration time should be a positive integer"); + } + + // TODO: IF USER ENABLES THE BWC MODE, WE ARE EXPECTING TO SET PLAIN TEXT ROLE AS `dr` + if (roles != null) { + String listOfRoles = String.join(",", roles); + jwtClaims.setProperty("er", EncryptionDecryptionUtil.encrypt(claimsEncryptionKey, listOfRoles)); + } else { + throw new Exception("Roles cannot be null"); + } + + String encodedJwt = jwtProducer.processJwt(jwt); + + if (logger.isDebugEnabled()) { + logger.debug( + "Created JWT: " + + encodedJwt + + "\n" + + jsonMapReaderWriter.toJson(jwt.getJwsHeaders()) + + "\n" + + JwtUtils.claimsToJson(jwt.getClaims()) + ); + } + + return encodedJwt; + } +} diff --git a/src/main/java/org/opensearch/security/dlic/rest/support/Utils.java b/src/main/java/org/opensearch/security/dlic/rest/support/Utils.java index 74908dbf60..dc0a755bc5 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/support/Utils.java +++ b/src/main/java/org/opensearch/security/dlic/rest/support/Utils.java @@ -12,10 +12,8 @@ package org.opensearch.security.dlic.rest.support; import java.io.IOException; -import java.security.AccessController; -import java.security.PrivilegedActionException; -import java.security.PrivilegedExceptionAction; -import java.security.SecureRandom; +import java.nio.charset.StandardCharsets; +import java.security.*; import java.util.Arrays; import java.util.HashMap; import java.util.List; @@ -32,6 +30,7 @@ import org.apache.commons.lang3.tuple.Pair; import org.bouncycastle.crypto.generators.OpenBSDBCrypt; +import org.bouncycastle.util.encoders.Hex; import org.opensearch.ExceptionsHelper; import org.opensearch.OpenSearchParseException; import org.opensearch.SpecialPermission; @@ -212,6 +211,20 @@ public static String hash(final char[] clearTextPassword) { return hash; } + /** + * This generates a SHA-256 hash for a given password. + * It is used for validating internal user tokens since we don't want to store the salt in the plugin. + * @param password The password to be hashed + * @return hash of the password + */ + public static String universalHash(String password) throws NoSuchAlgorithmException { + + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest( + password.getBytes(StandardCharsets.UTF_8)); + return new String(Hex.encode(hash)); + } + /** * Generate field resource paths * @param fields fields diff --git a/src/main/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticator.java b/src/main/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticator.java new file mode 100644 index 0000000000..5a4d8e567f --- /dev/null +++ b/src/main/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticator.java @@ -0,0 +1,277 @@ +/* + * 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.Key; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PrivilegedAction; +import java.security.PublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; +import java.util.Arrays; +import java.util.Map.Entry; +import java.util.regex.Pattern; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtParser; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.WeakKeyException; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.hc.core5.http.HttpHeaders; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.greenrobot.eventbus.Subscribe; + +import org.opensearch.OpenSearchSecurityException; +import org.opensearch.SpecialPermission; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.rest.RestChannel; +import org.opensearch.rest.RestRequest; +import org.opensearch.security.auth.HTTPAuthenticator; +import org.opensearch.security.authtoken.jwt.EncryptionDecryptionUtil; +import org.opensearch.security.securityconf.DynamicConfigModel; +import org.opensearch.security.user.AuthCredentials; + +public class HTTPOnBehalfOfJwtAuthenticator implements HTTPAuthenticator { + + 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 "; + + // TODO: TO SEE IF WE NEED THE FINAL FOR FOLLOWING + private JwtParser jwtParser; + private String subjectKey; + + private String signingKey; + private String encryptionKey; + + public HTTPOnBehalfOfJwtAuthenticator() { + super(); + init(); + } + + // FOR TESTING + public HTTPOnBehalfOfJwtAuthenticator(String signingKey, String encryptionKey) { + this.signingKey = signingKey; + this.encryptionKey = encryptionKey; + init(); + } + + private void init() { + + try { + if (signingKey == null || signingKey.length() == 0) { + log.error("signingKey must not be null or empty. JWT authentication will not work"); + } else { + + signingKey = signingKey.replace("-----BEGIN PUBLIC KEY-----\n", ""); + signingKey = signingKey.replace("-----END PUBLIC KEY-----", ""); + + byte[] decoded = Decoders.BASE64.decode(signingKey); + Key key = null; + + try { + key = getPublicKey(decoded, "RSA"); + } catch (Exception e) { + log.debug("No public RSA key, try other algos ({})", e.toString()); + } + + try { + key = getPublicKey(decoded, "EC"); + } catch (Exception e) { + log.debug("No public ECDSA key, try other algos ({})", e.toString()); + } + + if (key != null) { + jwtParser = Jwts.parser().setSigningKey(key); + } else { + jwtParser = Jwts.parser().setSigningKey(decoded); + } + + } + } catch (Throwable e) { + log.error("Error while creating JWT authenticator", e); + throw new RuntimeException(e); + } + + subjectKey = "sub"; + } + + @Override + @SuppressWarnings("removal") + public AuthCredentials extractCredentials(RestRequest request, 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); + } + }); + + return creds; + } + + private AuthCredentials extractCredentials0(final RestRequest request) { + if (jwtParser == null) { + log.error("Missing Signing Key. JWT authentication will not work"); + return null; + } + + String jwtToken = request.header(HttpHeaders.AUTHORIZATION); + + if (jwtToken == null || jwtToken.length() == 0) { + if (log.isDebugEnabled()) { + log.debug("No JWT token found in '{}' header", HttpHeaders.AUTHORIZATION); + } + return null; + } + + if (!BEARER.matcher(jwtToken).matches()) { + jwtToken = null; + } + + final int index; + if ((index = jwtToken.toLowerCase().indexOf(BEARER_PREFIX)) > -1) { // detect Bearer + jwtToken = jwtToken.substring(index + BEARER_PREFIX.length()); + } else { + if (log.isDebugEnabled()) { + log.debug("No Bearer scheme found in header"); + } + } + + try { + final Claims claims = jwtParser.parseClaimsJws(jwtToken).getBody(); + + final String subject = extractSubject(claims, request); + + final String audience = claims.getAudience(); + + // TODO: GET ROLESCLAIM DEPENDING ON THE STATUS OF BWC MODE. ON: er / OFF: dr + Object rolesObject = null; + String[] roles; + + try { + rolesObject = claims.get("er"); + } catch (Throwable e) { + log.debug("No encrypted role founded in the claim, continue searching for decrypted roles."); + } + + try { + rolesObject = claims.get("dr"); + } catch (Throwable e) { + log.debug("No decrypted role founded in the claim."); + } + + if (rolesObject == null) { + log.warn("Failed to get roles from JWT claims. Check if this key is correct and available in the JWT payload."); + roles = new String[0]; + } else { + final String rolesClaim = rolesObject.toString(); + + // Extracting roles based on the compatbility mode + String decryptedRoles = rolesClaim; + if (rolesObject == claims.get("er")) { + // TODO: WHERE TO GET THE ENCRYTION KEY + decryptedRoles = EncryptionDecryptionUtil.decrypt(encryptionKey, rolesClaim); + } + roles = Arrays.stream(decryptedRoles.split(",")).map(String::trim).toArray(String[]::new); + } + + if (subject == null) { + log.error("No subject found in JWT token"); + return null; + } + + if (audience == null) { + log.error("No audience found in JWT token"); + } + + final AuthCredentials ac = new AuthCredentials(subject, roles).markComplete(); + + for (Entry claim : claims.entrySet()) { + ac.addAttribute("attr.jwt." + claim.getKey(), String.valueOf(claim.getValue())); + } + + 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; + } + } + + @Override + public boolean reRequestAuthentication(final RestChannel channel, AuthCredentials creds) { + return false; + } + + @Override + public String getType() { + return "onbehalfof_jwt"; + } + + // TODO: Extract the audience (ext_id) and inject it into thread context + + protected String extractSubject(final Claims claims, final RestRequest request) { + String subject = claims.getSubject(); + if (subjectKey != null) { + // try to get roles from claims, first as Object to avoid having to catch the ExpectedTypeException + Object subjectObject = claims.get(subjectKey, Object.class); + if (subjectObject == null) { + log.warn("Failed to get subject from JWT claims, check if subject_key '{}' is correct.", subjectKey); + return null; + } + // We expect a String. If we find something else, convert to String but issue a warning + if (!(subjectObject instanceof String)) { + log.warn( + "Expected type String in the JWT for subject_key {}, but value was '{}' ({}). Will convert this value to String.", + subjectKey, + subjectObject, + subjectObject.getClass() + ); + } + subject = String.valueOf(subjectObject); + } + return subject; + } + + private static PublicKey getPublicKey(final byte[] keyBytes, final String algo) throws NoSuchAlgorithmException, + InvalidKeySpecException { + X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes); + KeyFactory kf = KeyFactory.getInstance(algo); + return kf.generatePublic(spec); + } + + @Subscribe + public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { + + // TODO: #2615 FOR CONFIGURATION + // For Testing + signingKey = "abcd1234"; + encryptionKey = RandomStringUtils.randomAlphanumeric(16); + } + +} diff --git a/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java b/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java new file mode 100644 index 0000000000..00fb63cd6d --- /dev/null +++ b/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java @@ -0,0 +1,143 @@ +/* + * 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.identity; + +import org.opensearch.client.Client; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.inject.Inject; +import org.opensearch.common.settings.Settings; +import org.opensearch.identity.tokens.AuthToken; +import org.opensearch.identity.tokens.BasicAuthToken; +import org.opensearch.identity.tokens.BearerAuthToken; +import org.opensearch.identity.tokens.TokenManager; +import org.opensearch.security.configuration.ConfigurationRepository; +import org.opensearch.security.user.InternalUserTokenHandler; +import org.opensearch.security.user.UserService; +import org.opensearch.security.user.UserServiceException; +import org.opensearch.security.user.UserTokenHandler; +import org.opensearch.threadpool.ThreadPool; + +/** + * This class serves as a funneling implementation of the TokenManager interface. + * The class allows the Security Plugin to implement two separate types of token managers without requiring specific interfaces + * in the IdentityPlugin. + */ +public class SecurityTokenManager implements TokenManager { + + Settings settings; + + ThreadPool threadPool; + + ClusterService clusterService; + Client client; + ConfigurationRepository configurationRepository; + UserService userService; + UserTokenHandler userTokenHandler; + InternalUserTokenHandler internalUserTokenHandler; + + public final String TOKEN_NOT_SUPPORTED_MESSAGE = "The provided token type is not supported by the Security Plugin."; + + @Inject + public SecurityTokenManager(ThreadPool threadPool, ClusterService clusterService, ConfigurationRepository configurationRepository, Client client, Settings settings, UserService userService) { + this.threadPool = threadPool; + this.clusterService = clusterService; + this.client = client; + this.configurationRepository = configurationRepository; + this.settings = settings; + this.userService = userService; + userTokenHandler = new UserTokenHandler(threadPool, clusterService, configurationRepository, client); + internalUserTokenHandler = new InternalUserTokenHandler(settings, userService); + + } + + @Override + public AuthToken issueToken() { + throw new UserServiceException("The Security Plugin does not support generic token creation. Please specify a token type and argument."); + } + + public AuthToken issueToken(String type, String account) throws Exception { + + AuthToken token; + switch (type) { + case "onBehalfOfToken": + token = userTokenHandler.issueToken(account); + break; + case "internalAuthToken": + token = internalUserTokenHandler.issueToken(account); + break; + default: throw new UserServiceException("The provided type " + type + " is not a valid token. Please specify either \"onBehalfOf\" or \"internalAuthToken\"."); + } + return token; + } + + @Override + public boolean validateToken(AuthToken authToken) { + + if (authToken instanceof BearerAuthToken) { + return userTokenHandler.validateToken(authToken); + } + if (authToken instanceof BasicAuthToken) { + return internalUserTokenHandler.validateToken(authToken); + } + throw new UserServiceException(TOKEN_NOT_SUPPORTED_MESSAGE); + } + + @Override + public String getTokenInfo(AuthToken authToken) { + + if (authToken instanceof BearerAuthToken) { + return userTokenHandler.getTokenInfo(authToken); + } + if (authToken instanceof BasicAuthToken) { + return internalUserTokenHandler.getTokenInfo(authToken); + } + throw new UserServiceException(TOKEN_NOT_SUPPORTED_MESSAGE); + } + + @Override + public void revokeToken(AuthToken authToken) { + if (authToken instanceof BearerAuthToken) { + userTokenHandler.revokeToken(authToken); + return; + } + if (authToken instanceof BasicAuthToken) { + internalUserTokenHandler.revokeToken(authToken); + return; + } + throw new UserServiceException(TOKEN_NOT_SUPPORTED_MESSAGE); + } + + @Override + public void resetToken(AuthToken authToken) { + if (authToken instanceof BearerAuthToken) { + userTokenHandler.resetToken(authToken); + } + if (authToken instanceof BasicAuthToken) { + internalUserTokenHandler.resetToken(authToken); + } + throw new UserServiceException(TOKEN_NOT_SUPPORTED_MESSAGE); + } + + /** + * Only for testing + */ + public void setInternalUserTokenHandler(InternalUserTokenHandler handler) { + this.internalUserTokenHandler = handler; + } + + /** + * Only for testing + */ + public void setUserTokenHandler(UserTokenHandler handler) { + this.userTokenHandler = handler; + } +} diff --git a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModel.java b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModel.java index 08976f2013..efdec1d47d 100644 --- a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModel.java +++ b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModel.java @@ -38,6 +38,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.opensearch.common.settings.Settings; import org.opensearch.security.auth.AuthDomain; import org.opensearch.security.auth.AuthFailureListener; import org.opensearch.security.auth.AuthorizationBackend; @@ -92,6 +93,8 @@ public abstract class DynamicConfigModel { public abstract String getFilteredAliasMode(); + public abstract Settings getDynamicOnBehalfOfSettings(); + public abstract String getHostsResolverMode(); public abstract boolean isDnfofForEmptyResultsEnabled(); diff --git a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV6.java b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV6.java index 994989416b..9eb9c9bb21 100644 --- a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV6.java +++ b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV6.java @@ -187,6 +187,11 @@ public String getHostsResolverMode() { return config.dynamic.hosts_resolver_mode; } + @Override + public Settings getDynamicOnBehalfOfSettings() { + return Settings.EMPTY; + } + @Override public List getIpAuthFailureListeners() { return Collections.unmodifiableList(ipAuthFailureListeners); diff --git a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java index f6bbcc2161..8259f12dea 100644 --- a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java +++ b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java @@ -191,6 +191,13 @@ public boolean isDnfofForEmptyResultsEnabled() { public List getIpAuthFailureListeners() { return Collections.unmodifiableList(ipAuthFailureListeners); } + + @Override + public Settings getDynamicOnBehalfOfSettings() { + return Settings.builder() + .put(Settings.builder().loadFromSource(config.dynamic.on_behalf_of.configAsJson(), XContentType.JSON).build()) + .build(); + } @Override public Multimap getAuthBackendFailureListeners() { diff --git a/src/main/java/org/opensearch/security/securityconf/impl/CType.java b/src/main/java/org/opensearch/security/securityconf/impl/CType.java index 4e5e2de496..89507a9c81 100644 --- a/src/main/java/org/opensearch/security/securityconf/impl/CType.java +++ b/src/main/java/org/opensearch/security/securityconf/impl/CType.java @@ -60,6 +60,7 @@ public enum CType { WHITELIST(toMap(1, WhitelistingSettings.class, 2, WhitelistingSettings.class)), ALLOWLIST(toMap(1, AllowlistingSettings.class, 2, AllowlistingSettings.class)), AUDIT(toMap(1, AuditConfig.class, 2, AuditConfig.class)); + REVOKEDTOKENS(toMap(1, BearerAuthToken.class)); private Map> implementations; diff --git a/src/main/java/org/opensearch/security/securityconf/impl/v6/ConfigV6.java b/src/main/java/org/opensearch/security/securityconf/impl/v6/ConfigV6.java index c85e69fb0d..2e2e1af15d 100644 --- a/src/main/java/org/opensearch/security/securityconf/impl/v6/ConfigV6.java +++ b/src/main/java/org/opensearch/security/securityconf/impl/v6/ConfigV6.java @@ -36,6 +36,7 @@ import com.fasterxml.jackson.annotation.JsonAnySetter; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonProcessingException; import org.opensearch.security.DefaultObjectMapper; @@ -67,20 +68,23 @@ public static class Dynamic { public String hosts_resolver_mode = "ip-only"; public String transport_userrname_attribute; public boolean do_not_fail_on_forbidden_empty; + public OnBehalfOf on_behalf_of = new OnBehalfOf() { + + }; @Override public String toString() { return "Dynamic [filtered_alias_mode=" - + filtered_alias_mode - + ", kibana=" - + kibana - + ", http=" - + http - + ", authc=" - + authc - + ", authz=" - + authz - + "]"; + + filtered_alias_mode + + ", kibana=" + + kibana + + ", http=" + + http + + ", authc=" + + authc + + ", authz=" + + authz + + "]"; } } @@ -100,16 +104,16 @@ public static class Kibana { @Override public String toString() { return "Kibana [multitenancy_enabled=" - + multitenancy_enabled - + ", server_username=" - + server_username - + ", opendistro_role=" - + opendistro_role - + ", index=" - + index - + ", do_not_fail_on_forbidden=" - + do_not_fail_on_forbidden - + "]"; + + multitenancy_enabled + + ", server_username=" + + server_username + + ", opendistro_role=" + + opendistro_role + + ", index=" + + index + + ", do_not_fail_on_forbidden=" + + do_not_fail_on_forbidden + + "]"; } } @@ -168,13 +172,13 @@ public static class Xff { @JsonInclude(JsonInclude.Include.NON_NULL) public boolean enabled = true; public String internalProxies = Pattern.compile( - "10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|" - + "192\\.168\\.\\d{1,3}\\.\\d{1,3}|" - + "169\\.254\\.\\d{1,3}\\.\\d{1,3}|" - + "127\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|" - + "172\\.1[6-9]{1}\\.\\d{1,3}\\.\\d{1,3}|" - + "172\\.2[0-9]{1}\\.\\d{1,3}\\.\\d{1,3}|" - + "172\\.3[0-1]{1}\\.\\d{1,3}\\.\\d{1,3}" + "10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|" + + "192\\.168\\.\\d{1,3}\\.\\d{1,3}|" + + "169\\.254\\.\\d{1,3}\\.\\d{1,3}|" + + "127\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|" + + "172\\.1[6-9]{1}\\.\\d{1,3}\\.\\d{1,3}|" + + "172\\.2[0-9]{1}\\.\\d{1,3}\\.\\d{1,3}|" + + "172\\.3[0-1]{1}\\.\\d{1,3}\\.\\d{1,3}" ).toString(); public String remoteIpHeader = "X-Forwarded-For"; public String proxiesHeader = "X-Forwarded-By"; @@ -183,16 +187,16 @@ public static class Xff { @Override public String toString() { return "Xff [enabled=" - + enabled - + ", internalProxies=" - + internalProxies - + ", remoteIpHeader=" - + remoteIpHeader - + ", proxiesHeader=" - + proxiesHeader - + ", trustedProxies=" - + trustedProxies - + "]"; + + enabled + + ", internalProxies=" + + internalProxies + + ", remoteIpHeader=" + + remoteIpHeader + + ", proxiesHeader=" + + proxiesHeader + + ", trustedProxies=" + + trustedProxies + + "]"; } } @@ -233,18 +237,18 @@ public static class AuthcDomain { @Override public String toString() { return "AuthcDomain [http_enabled=" - + http_enabled - + ", transport_enabled=" - + transport_enabled - + ", enabled=" - + enabled - + ", order=" - + order - + ", http_authenticator=" - + http_authenticator - + ", authentication_backend=" - + authentication_backend - + "]"; + + http_enabled + + ", transport_enabled=" + + transport_enabled + + ", enabled=" + + enabled + + ", order=" + + order + + ", http_authenticator=" + + http_authenticator + + ", authentication_backend=" + + authentication_backend + + "]"; } } @@ -344,16 +348,44 @@ public static class AuthzDomain { @Override public String toString() { return "AuthzDomain [http_enabled=" - + http_enabled - + ", transport_enabled=" - + transport_enabled - + ", enabled=" - + enabled - + ", authorization_backend=" - + authorization_backend - + "]"; + + http_enabled + + ", transport_enabled=" + + transport_enabled + + ", enabled=" + + enabled + + ", authorization_backend=" + + authorization_backend + + "]"; + } + + } + + public static class OnBehalfOf { + @JsonProperty("signing_key") + private String signingKey; + @JsonProperty("encryption_key") + private String encryptionKey; + + public String getSigningKey() { + return signingKey; + } + + 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 "OnBehalfOf [signing_key=" + signingKey + ", encryption_key=" + encryptionKey + "]"; + } } } 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 87de6a31b0..cd8251066d 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 @@ -37,6 +37,7 @@ import com.fasterxml.jackson.annotation.JsonAnySetter; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonProcessingException; import org.opensearch.security.DefaultObjectMapper; @@ -86,27 +87,27 @@ public ConfigV7(ConfigV6 c6) { dynamic.authc = new Authc(); dynamic.authc.domains.putAll( - c6.dynamic.authc.getDomains() - .entrySet() - .stream() - .collect(Collectors.toMap(entry -> entry.getKey(), entry -> new AuthcDomain(entry.getValue()))) + c6.dynamic.authc.getDomains() + .entrySet() + .stream() + .collect(Collectors.toMap(entry -> entry.getKey(), entry -> new AuthcDomain(entry.getValue()))) ); dynamic.authz = new Authz(); dynamic.authz.domains.putAll( - c6.dynamic.authz.getDomains() - .entrySet() - .stream() - .collect(Collectors.toMap(entry -> entry.getKey(), entry -> new AuthzDomain(entry.getValue()))) + c6.dynamic.authz.getDomains() + .entrySet() + .stream() + .collect(Collectors.toMap(entry -> entry.getKey(), entry -> new AuthzDomain(entry.getValue()))) ); dynamic.auth_failure_listeners = new AuthFailureListeners(); dynamic.auth_failure_listeners.listeners.putAll( - c6.dynamic.auth_failure_listeners.getListeners() - .entrySet() - .stream() - .collect(Collectors.toMap(entry -> entry.getKey(), entry -> new AuthFailureListener(entry.getValue()))) + c6.dynamic.auth_failure_listeners.getListeners() + .entrySet() + .stream() + .collect(Collectors.toMap(entry -> entry.getKey(), entry -> new AuthFailureListener(entry.getValue()))) ); } @@ -133,20 +134,21 @@ public static class Dynamic { public String hosts_resolver_mode = "ip-only"; public String transport_userrname_attribute; public boolean do_not_fail_on_forbidden_empty; + public OnBehalfOf on_behalf_of = new OnBehalfOf(); @Override public String toString() { return "Dynamic [filtered_alias_mode=" - + filtered_alias_mode - + ", kibana=" - + kibana - + ", http=" - + http - + ", authc=" - + authc - + ", authz=" - + authz - + "]"; + + filtered_alias_mode + + ", kibana=" + + kibana + + ", http=" + + http + + ", authc=" + + authc + + ", authz=" + + authz + + "]"; } } @@ -165,18 +167,18 @@ public static class Kibana { @Override public String toString() { return "Kibana [multitenancy_enabled=" - + multitenancy_enabled - + ", private_tenant_enabled=" - + private_tenant_enabled - + ", default_tenant=" - + default_tenant - + ", server_username=" - + server_username - + ", opendistro_role=" - + opendistro_role - + ", index=" - + index - + "]"; + + multitenancy_enabled + + ", private_tenant_enabled=" + + private_tenant_enabled + + ", default_tenant=" + + default_tenant + + ", server_username=" + + server_username + + ", opendistro_role=" + + opendistro_role + + ", index=" + + index + + "]"; } } @@ -245,13 +247,13 @@ public String asJson() { public static class Xff { public boolean enabled = false; public String internalProxies = Pattern.compile( - "10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|" - + "192\\.168\\.\\d{1,3}\\.\\d{1,3}|" - + "169\\.254\\.\\d{1,3}\\.\\d{1,3}|" - + "127\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|" - + "172\\.1[6-9]{1}\\.\\d{1,3}\\.\\d{1,3}|" - + "172\\.2[0-9]{1}\\.\\d{1,3}\\.\\d{1,3}|" - + "172\\.3[0-1]{1}\\.\\d{1,3}\\.\\d{1,3}" + "10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|" + + "192\\.168\\.\\d{1,3}\\.\\d{1,3}|" + + "169\\.254\\.\\d{1,3}\\.\\d{1,3}|" + + "127\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|" + + "172\\.1[6-9]{1}\\.\\d{1,3}\\.\\d{1,3}|" + + "172\\.2[0-9]{1}\\.\\d{1,3}\\.\\d{1,3}|" + + "172\\.3[0-1]{1}\\.\\d{1,3}\\.\\d{1,3}" ).toString(); public String remoteIpHeader = "X-Forwarded-For"; @@ -317,18 +319,18 @@ public AuthcDomain(ConfigV6.AuthcDomain v6) { @Override public String toString() { return "AuthcDomain [http_enabled=" - + http_enabled - + ", transport_enabled=" - + transport_enabled - + ", order=" - + order - + ", http_authenticator=" - + http_authenticator - + ", authentication_backend=" - + authentication_backend - + ", description=" - + description - + "]"; + + http_enabled + + ", transport_enabled=" + + transport_enabled + + ", order=" + + order + + ", http_authenticator=" + + http_authenticator + + ", authentication_backend=" + + authentication_backend + + ", description=" + + description + + "]"; } } @@ -466,16 +468,54 @@ public AuthzDomain(ConfigV6.AuthzDomain v6) { @Override public String toString() { return "AuthzDomain [http_enabled=" - + http_enabled - + ", transport_enabled=" - + transport_enabled - + ", authorization_backend=" - + authorization_backend - + ", description=" - + description - + "]"; + + http_enabled + + ", transport_enabled=" + + transport_enabled + + ", authorization_backend=" + + authorization_backend + + ", description=" + + description + + "]"; } } + public static class OnBehalfOf { + @JsonProperty("signing_key") + private String signingKey; + @JsonProperty("encryption_key") + private String encryptionKey; + + @JsonIgnore + public String configAsJson() { + try { + return DefaultObjectMapper.writeValueAsString(this, false); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + public String getSigningKey() { + return signingKey; + } + + 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 "OnBehalfOf [signing_key=" + signingKey + ", encryption_key=" + encryptionKey + "]"; + } + } + } + diff --git a/src/main/java/org/opensearch/security/user/InternalUserTokenHandler.java b/src/main/java/org/opensearch/security/user/InternalUserTokenHandler.java new file mode 100644 index 0000000000..2979ae3494 --- /dev/null +++ b/src/main/java/org/opensearch/security/user/InternalUserTokenHandler.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.user; + +import org.opensearch.common.inject.Inject; +import org.opensearch.common.settings.Settings; +import org.opensearch.identity.tokens.AuthToken; +import org.opensearch.identity.tokens.BasicAuthToken; +import org.opensearch.identity.tokens.TokenManager; +import org.opensearch.security.securityconf.Hashed; +import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; + +import java.io.IOException; +import java.security.NoSuchAlgorithmException; + +import static org.opensearch.security.dlic.rest.support.Utils.universalHash; + +public class InternalUserTokenHandler implements TokenManager { + + Settings settings; + + UserService userService; + + public SecurityDynamicConfiguration internalUsersConfiguration; + + + @Inject + public InternalUserTokenHandler(final Settings settings, UserService userService) { + this.settings = settings; + this.userService = userService; + this.internalUsersConfiguration = userService.geInternalUsersConfigurationRepository(); + } + + public AuthToken issueToken() { + throw new UserServiceException("The InternalUserTokenHandler is unable to issue generic auth tokens. Please specify a valid internal user."); + } + + public AuthToken issueToken(String internalUser) { + String tokenAsString; + try { + tokenAsString = this.userService.generateAuthToken(internalUser); + } catch (IOException | UserServiceException ex){ + throw new UserServiceException("Failed to generate an auth token for " + internalUser); + } + return new BasicAuthToken(tokenAsString); + } + + public boolean validateToken(AuthToken token) { + if (!(token instanceof BasicAuthToken)) { + throw new UserServiceException("The provided auth token is of an incorrect type. Please provide a BasicAuthToken object."); + } + BasicAuthToken basicToken = (BasicAuthToken) token; + String accountName = basicToken.getUser(); + String password = basicToken.getPassword(); + String hash; + try { + hash = universalHash(password); + } catch (NoSuchAlgorithmException e) { + throw new UserServiceException("The provided token could not be validated."); + } + return (internalUsersConfiguration.exists(accountName) && hash.equals(((Hashed) internalUsersConfiguration.getCEntry(accountName)).getHash())); + } + + public String getTokenInfo(AuthToken token) { + if (!(token instanceof BasicAuthToken)) { + throw new UserServiceException("The provided token is not a BasicAuthToken."); + } + BasicAuthToken basicAuthToken = (BasicAuthToken) token; + return "The provided token is a BasicAuthToken with content: " + basicAuthToken; + } + + public void revokeToken(AuthToken token) { + if (validateToken(token)) { + BasicAuthToken basicToken = (BasicAuthToken) token; + String accountName = basicToken.getUser(); + try { + userService.clearHash(accountName); + return; + } catch (IOException e) { + throw new UserServiceException(e.getMessage()); + } + } + throw new UserServiceException("The token could not be revoked."); + } + + public void resetToken(AuthToken token) { + throw new UserServiceException("The InternalUserTokenHandler is unable to reset auth tokens."); + } +} + diff --git a/src/main/java/org/opensearch/security/user/UserService.java b/src/main/java/org/opensearch/security/user/UserService.java index 0653948a38..7f55f21efd 100644 --- a/src/main/java/org/opensearch/security/user/UserService.java +++ b/src/main/java/org/opensearch/security/user/UserService.java @@ -11,21 +11,12 @@ package org.opensearch.security.user; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.Base64; -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; - import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.collect.ImmutableList; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; - import org.opensearch.ExceptionsHelper; import org.opensearch.action.index.IndexRequest; import org.opensearch.action.support.WriteRequest; @@ -44,7 +35,17 @@ import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.support.SecurityJsonNode; -import static org.opensearch.security.dlic.rest.support.Utils.hash; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +import static org.opensearch.security.dlic.rest.support.Utils.universalHash; /** * This class handles user registration and operations on behalf of the Security Plugin. @@ -53,41 +54,43 @@ public class UserService { protected final Logger log = LogManager.getLogger(this.getClass()); ClusterService clusterService; - static ConfigurationRepository configurationRepository; + ConfigurationRepository configurationRepository; String securityIndex; Client client; - User tokenUser; final static String NO_PASSWORD_OR_HASH_MESSAGE = "Please specify either 'hash' or 'password' when creating a new internal user."; final static String RESTRICTED_CHARACTER_USE_MESSAGE = "A restricted character(s) was detected in the account name. Please remove: "; - final static String SERVICE_ACCOUNT_PASSWORD_MESSAGE = - "A password cannot be provided for a service account. Failed to register service account: "; + final static String SERVICE_ACCOUNT_PASSWORD_MESSAGE = "A password cannot be provided for a service account. Failed to register service account: "; - final static String SERVICE_ACCOUNT_HASH_MESSAGE = - "A password hash cannot be provided for service account. Failed to register service account: "; + final static String SERVICE_ACCOUNT_HASH_MESSAGE = "A password hash cannot be provided for service account. Failed to register service account: "; final static String NO_ACCOUNT_NAME_MESSAGE = "No account name was specified in the request."; final static String FAILED_ACCOUNT_RETRIEVAL_MESSAGE = "The account specified could not be accessed at this time."; final static String AUTH_TOKEN_GENERATION_MESSAGE = "An auth token could not be generated for the specified account."; + final static String FAILED_CLEAR_HASH_MESSAGE = "The hash could not be cleared from the specified account."; + private static CType getUserConfigName() { return CType.INTERNALUSERS; } static final List RESTRICTED_FROM_USERNAME = ImmutableList.of( - ":" // Not allowed in basic auth, see https://stackoverflow.com/a/33391003/533057 + ":" // Not allowed in basic auth, see https://stackoverflow.com/a/33391003/533057 ); @Inject - public UserService(ClusterService clusterService, ConfigurationRepository configurationRepository, Settings settings, Client client) { + public UserService( + ClusterService clusterService, + ConfigurationRepository configurationRepository, + Settings settings, + Client client + ) { this.clusterService = clusterService; this.configurationRepository = configurationRepository; - this.securityIndex = settings.get( - ConfigConstants.SECURITY_CONFIG_INDEX_NAME, - ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX - ); + this.securityIndex = settings.get(ConfigConstants.SECURITY_CONFIG_INDEX_NAME, + ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX); this.client = client; } @@ -96,11 +99,8 @@ public UserService(ClusterService clusterService, ConfigurationRepository config * @param config CType whose data is to be loaded in-memory * @return configuration loaded with given CType data */ - protected static final SecurityDynamicConfiguration load(final CType config, boolean logComplianceEvent) { - SecurityDynamicConfiguration loaded = configurationRepository.getConfigurationsFromIndex( - Collections.singleton(config), - logComplianceEvent - ).get(config).deepClone(); + protected final SecurityDynamicConfiguration load(final CType config, boolean logComplianceEvent) { + SecurityDynamicConfiguration loaded = configurationRepository.getConfigurationsFromIndex(Collections.singleton(config), logComplianceEvent).get(config).deepClone(); return DynamicConfigFactory.addStatics(loaded); } @@ -109,10 +109,8 @@ protected static final SecurityDynamicConfiguration load(final CType config, * * @param contentAsNode An object node of different account configurations. * @return InternalUserConfiguration with the new/updated user - * @throws UserServiceException - * @throws IOException */ - public SecurityDynamicConfiguration createOrUpdateAccount(ObjectNode contentAsNode) throws IOException { + public SecurityDynamicConfiguration createOrUpdateAccount(ObjectNode contentAsNode) throws IOException, NoSuchAlgorithmException { SecurityJsonNode securityJsonNode = new SecurityJsonNode(contentAsNode); @@ -125,20 +123,18 @@ public SecurityDynamicConfiguration createOrUpdateAccount(ObjectNode contentA SecurityJsonNode attributeNode = securityJsonNode.get("attributes"); - if (!attributeNode.get("service").isNull() && attributeNode.get("service").asString().equalsIgnoreCase("true")) { // If this is a - // service account + if (!attributeNode.get("service").isNull() && Objects.requireNonNull(attributeNode.get("service").asString()).equalsIgnoreCase("true")) + { // If this is a service account verifyServiceAccount(securityJsonNode, accountName); String password = generatePassword(); - contentAsNode.put("hash", hash(password.toCharArray())); + contentAsNode.put("hash", universalHash(password)); contentAsNode.put("service", "true"); - } else { + } else{ contentAsNode.put("service", "false"); } securityJsonNode = new SecurityJsonNode(contentAsNode); - final List foundRestrictedContents = RESTRICTED_FROM_USERNAME.stream() - .filter(accountName::contains) - .collect(Collectors.toList()); + final List foundRestrictedContents = RESTRICTED_FROM_USERNAME.stream().filter(accountName::contains).collect(Collectors.toList()); if (!foundRestrictedContents.isEmpty()) { final String restrictedContents = foundRestrictedContents.stream().map(s -> "'" + s + "'").collect(Collectors.joining(",")); throw new UserServiceException(RESTRICTED_CHARACTER_USE_MESSAGE + restrictedContents); @@ -149,10 +145,10 @@ public SecurityDynamicConfiguration createOrUpdateAccount(ObjectNode contentA final String origHash = securityJsonNode.get("hash").asString(); if (plainTextPassword != null && plainTextPassword.length() > 0) { contentAsNode.remove("password"); - contentAsNode.put("hash", hash(plainTextPassword.toCharArray())); + contentAsNode.put("hash", universalHash(plainTextPassword)); } else if (origHash != null && origHash.length() > 0) { contentAsNode.remove("password"); - } else if (plainTextPassword != null && plainTextPassword.isEmpty() && origHash == null) { + } else if (plainTextPassword != null && origHash == null) { contentAsNode.remove("password"); } @@ -172,9 +168,7 @@ public SecurityDynamicConfiguration createOrUpdateAccount(ObjectNode contentA // sanity check, this should usually not happen final String hash = ((Hashed) internalUsersConfiguration.getCEntry(accountName)).getHash(); if (hash == null || hash.length() == 0) { - throw new UserServiceException( - "Existing user " + accountName + " has no password, and no new password or hash was specified." - ); + throw new UserServiceException("Existing user " + accountName + " has no password, and no new password or hash was specified."); } contentAsNode.put("hash", hash); } @@ -182,15 +176,13 @@ public SecurityDynamicConfiguration createOrUpdateAccount(ObjectNode contentA internalUsersConfiguration.remove(accountName); contentAsNode.remove("name"); - internalUsersConfiguration.putCObject( - accountName, - DefaultObjectMapper.readTree(contentAsNode, internalUsersConfiguration.getImplementingClass()) - ); + internalUsersConfiguration.putCObject(accountName, DefaultObjectMapper.readTree(contentAsNode, internalUsersConfiguration.getImplementingClass())); return internalUsersConfiguration; } private void verifyServiceAccount(SecurityJsonNode securityJsonNode, String accountName) { + final String plainTextPassword = securityJsonNode.get("password").asString(); final String origHash = securityJsonNode.get("hash").asString(); @@ -209,8 +201,7 @@ private void verifyServiceAccount(SecurityJsonNode securityJsonNode, String acco * @return A password for a service account. */ private String generatePassword() { - String generatedPassword = "superSecurePassword"; - return generatedPassword; + return "superSecurePassword"; } /** @@ -228,38 +219,36 @@ public String generateAuthToken(String accountName) throws IOException { throw new UserServiceException(FAILED_ACCOUNT_RETRIEVAL_MESSAGE); } - String authToken = null; + String authToken; try { - DefaultObjectMapper mapper = new DefaultObjectMapper(); - JsonNode accountDetails = mapper.readTree(internalUsersConfiguration.getCEntry(accountName).toString()); + JsonNode accountDetails = DefaultObjectMapper.readTree(internalUsersConfiguration.getCEntry(accountName).toString()); final ObjectNode contentAsNode = (ObjectNode) accountDetails; SecurityJsonNode securityJsonNode = new SecurityJsonNode(contentAsNode); - Optional.ofNullable(securityJsonNode.get("service")) - .map(SecurityJsonNode::asString) - .filter("true"::equalsIgnoreCase) - .orElseThrow(() -> new UserServiceException(AUTH_TOKEN_GENERATION_MESSAGE)); + Optional.of(securityJsonNode.get("service")) + .map(SecurityJsonNode::asString) + .filter("true"::equalsIgnoreCase) + .orElseThrow(() -> new UserServiceException(AUTH_TOKEN_GENERATION_MESSAGE)); - Optional.ofNullable(securityJsonNode.get("enabled")) - .map(SecurityJsonNode::asString) - .filter("true"::equalsIgnoreCase) - .orElseThrow(() -> new UserServiceException(AUTH_TOKEN_GENERATION_MESSAGE)); + + Optional.of(securityJsonNode.get("enabled")) + .map(SecurityJsonNode::asString) + .filter("true"::equalsIgnoreCase) + .orElseThrow(() -> new UserServiceException(AUTH_TOKEN_GENERATION_MESSAGE)); // Generate a new password for the account and store the hash of it String plainTextPassword = generatePassword(); - contentAsNode.put("hash", hash(plainTextPassword.toCharArray())); + contentAsNode.put("hash", universalHash(plainTextPassword)); contentAsNode.put("enabled", "true"); contentAsNode.put("service", "true"); // Update the internal user associated with the auth token internalUsersConfiguration.remove(accountName); contentAsNode.remove("name"); - internalUsersConfiguration.putCObject( - accountName, - DefaultObjectMapper.readTree(contentAsNode, internalUsersConfiguration.getImplementingClass()) - ); + internalUsersConfiguration.putCObject(accountName, DefaultObjectMapper.readTree(contentAsNode, internalUsersConfiguration.getImplementingClass())); saveAndUpdateConfigs(getUserConfigName().toString(), client, CType.INTERNALUSERS, internalUsersConfiguration); + authToken = Base64.getUrlEncoder().encodeToString((accountName + ":" + plainTextPassword).getBytes(StandardCharsets.UTF_8)); return authToken; @@ -270,27 +259,52 @@ public String generateAuthToken(String accountName) throws IOException { } } - public static void saveAndUpdateConfigs( - final String indexName, - final Client client, - final CType cType, - final SecurityDynamicConfiguration configuration - ) { + public void clearHash(String accountName) throws IOException { + final SecurityDynamicConfiguration internalUsersConfiguration = load(getUserConfigName(), false); + + if (!internalUsersConfiguration.exists(accountName)) { + throw new UserServiceException(FAILED_ACCOUNT_RETRIEVAL_MESSAGE); + } + + JsonNode accountDetails = DefaultObjectMapper.readTree(internalUsersConfiguration.getCEntry(accountName).toString()); + final ObjectNode contentAsNode = (ObjectNode) accountDetails; + SecurityJsonNode securityJsonNode = new SecurityJsonNode(contentAsNode); + + Optional.of(securityJsonNode.get("service")) + .map(SecurityJsonNode::asString) + .filter("true"::equalsIgnoreCase) + .orElseThrow(() -> new UserServiceException(FAILED_CLEAR_HASH_MESSAGE)); + + + Optional.of(securityJsonNode.get("enabled")) + .map(SecurityJsonNode::asString) + .filter("true"::equalsIgnoreCase) + .orElseThrow(() -> new UserServiceException(FAILED_CLEAR_HASH_MESSAGE)); + + contentAsNode.remove("hash"); + contentAsNode.remove("name"); + internalUsersConfiguration.putCObject(accountName, DefaultObjectMapper.readTree(contentAsNode, internalUsersConfiguration.getImplementingClass())); + saveAndUpdateConfigs(getUserConfigName().toString(), client, CType.INTERNALUSERS, internalUsersConfiguration); + } + + public void saveAndUpdateConfigs(final String indexName, final Client client, final CType cType, final SecurityDynamicConfiguration configuration) { final IndexRequest ir = new IndexRequest(indexName); final String id = cType.toLCString(); configuration.removeStatic(); try { - client.index( - ir.id(id) + client.index(ir.id(id) .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) .setIfSeqNo(configuration.getSeqNo()) .setIfPrimaryTerm(configuration.getPrimaryTerm()) - .source(id, XContentHelper.toXContent(configuration, XContentType.JSON, false)) - ); + .source(id, XContentHelper.toXContent(configuration, XContentType.JSON, false))); } catch (IOException e) { throw ExceptionsHelper.convertToOpenSearchException(e); } } + + public SecurityDynamicConfiguration geInternalUsersConfigurationRepository() { + return load(getUserConfigName(), false); + } } diff --git a/src/main/java/org/opensearch/security/user/UserTokenHandler.java b/src/main/java/org/opensearch/security/user/UserTokenHandler.java new file mode 100644 index 0000000000..8febf95d62 --- /dev/null +++ b/src/main/java/org/opensearch/security/user/UserTokenHandler.java @@ -0,0 +1,161 @@ +/* + * 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.user; + +import com.amazon.dlic.auth.http.jwt.keybyoidc.JwtVerifier; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.cxf.rs.security.jose.jws.JwsJwtCompactConsumer; +import org.apache.cxf.rs.security.jose.jwt.JwtToken; +import org.opensearch.ExceptionsHelper; +import org.opensearch.action.index.IndexRequest; +import org.opensearch.action.support.WriteRequest; +import org.opensearch.client.Client; +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.common.xcontent.XContentHelper; +import org.opensearch.common.xcontent.XContentType; +import org.opensearch.identity.tokens.AuthToken; +import org.opensearch.identity.tokens.BearerAuthToken; +import org.opensearch.identity.tokens.TokenManager; +import org.opensearch.security.authtoken.jwt.JwtVendor; +import org.opensearch.security.configuration.ConfigurationRepository; +import org.opensearch.security.securityconf.DynamicConfigFactory; +import org.opensearch.security.securityconf.impl.CType; +import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.threadpool.ThreadPool; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; + +public class UserTokenHandler implements TokenManager { + + private final int DEFAULT_EXPIRATION_TIME_SECONDS = 300; + + JwtVendor jwtVendor; + Settings settings; + + ClusterService clusterService; + + ConfigurationRepository configurationRepository; + + ThreadPool threadPool; + + JwtVerifier jwtVerifier; + + String claimsEncryptionKey = RandomStringUtils.randomAlphanumeric(16); + + String signingKey = RandomStringUtils.randomAlphanumeric(16); + + Client client; + + public static CType getRevokedTokensConfigName() { + return CType.REVOKEDTOKENS; + } + + @Inject + public UserTokenHandler(ThreadPool threadPool, ClusterService clusterService, ConfigurationRepository configurationRepository, Client client) { + this.settings = Settings.builder().put("signing_key", signingKey).put("encryption_key", claimsEncryptionKey).build(); + this.jwtVendor = new JwtVendor(settings, () -> System.nanoTime() / 1000 + (DEFAULT_EXPIRATION_TIME_SECONDS * 1000)); + this.threadPool = threadPool; + this.clusterService = clusterService; + this.client = client; + this.configurationRepository = configurationRepository; + } + + @Override + public AuthToken issueToken() { + throw new UserServiceException("The UserTokenHandler is unable to issue generic auth tokens. Please specify a valid subject."); + } + + public AuthToken issueToken(String audience) throws Exception { + ThreadContext threadContext = threadPool.getThreadContext(); + final User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + String jwt = jwtVendor.createJwt(clusterService.getClusterName().toString(), user.getName(), audience, DEFAULT_EXPIRATION_TIME_SECONDS, new ArrayList(user.getRoles())); + return new BearerAuthToken(jwt); + } + + @Override + public boolean validateToken(AuthToken authToken) { + if (!(authToken instanceof BearerAuthToken)) { + throw new UserServiceException("The provided token is not a BearerAuthToken."); + } + BearerAuthToken bearerAuthToken = (BearerAuthToken) authToken; + JwsJwtCompactConsumer jwtConsumer = new JwsJwtCompactConsumer(bearerAuthToken.getCompleteToken()); + JwtToken jwt = jwtConsumer.getJwtToken(); + + Long iat = (Long) jwt.getClaim("iat"); + Long exp = (Long) jwt.getClaim("exp"); + SecurityDynamicConfiguration revokedTokens = load(getRevokedTokensConfigName(), false); + System.out.println("IAT is : " + iat); + System.out.println("EXP is : " + exp); + System.out.println("Current time is : " + System.currentTimeMillis()); + System.out.println("Exists in revoked is: " + revokedTokens.exists(bearerAuthToken.getCompleteToken())); + Long currentTime = System.currentTimeMillis(); + return (exp > currentTime && !revokedTokens.exists(bearerAuthToken.getCompleteToken())); + } + + @Override + public String getTokenInfo(AuthToken authToken) { + if (!(authToken instanceof BearerAuthToken)) { + throw new UserServiceException("The provided token is not a BearerAuthToken."); + } + BearerAuthToken bearerAuthToken = (BearerAuthToken) authToken; + return "The provided token is a BearerAuthToken with content: " + bearerAuthToken; + } + + @Override + public void revokeToken(AuthToken authToken) { + if (!(authToken instanceof BearerAuthToken)) { + throw new UserServiceException("The provided token is not a BearerAuthToken."); + } + BearerAuthToken bearerAuthToken = (BearerAuthToken) authToken; + SecurityDynamicConfiguration revokedTokens = load(getRevokedTokensConfigName(), false); + revokedTokens.putCObject(bearerAuthToken.getCompleteToken(), bearerAuthToken); + saveAndUpdateConfigs(getRevokedTokensConfigName().toString(), client, CType.REVOKEDTOKENS, revokedTokens); + } + + @Override + public void resetToken(AuthToken authToken) { + throw new UserServiceException("The UserTokenHandler does not support the reset operation. Please issue a new token instead."); + } + + /** + * Load data for a given CType + * @param config CType whose data is to be loaded in-memory + * @return configuration loaded with given CType data + */ + public SecurityDynamicConfiguration load(final CType config, boolean logComplianceEvent) { + SecurityDynamicConfiguration loaded = configurationRepository.getConfigurationsFromIndex(Collections.singleton(config), logComplianceEvent).get(config).deepClone(); + return DynamicConfigFactory.addStatics(loaded); + } + + public void saveAndUpdateConfigs(final String indexName, final Client client, final CType cType, final SecurityDynamicConfiguration configuration) { + final IndexRequest ir = new IndexRequest(indexName); + final String id = cType.toLCString(); + + configuration.removeStatic(); + + try { + client.index(ir.id(id) + .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) + .setIfSeqNo(configuration.getSeqNo()) + .setIfPrimaryTerm(configuration.getPrimaryTerm()) + .source(id, XContentHelper.toXContent(configuration, XContentType.JSON, false))); + } catch (IOException e) { + throw ExceptionsHelper.convertToOpenSearchException(e); + } + } +} diff --git a/src/test/java/org/opensearch/security/auth/InternalUserTokenHandlerTests.java b/src/test/java/org/opensearch/security/auth/InternalUserTokenHandlerTests.java new file mode 100644 index 0000000000..347d3ab3d3 --- /dev/null +++ b/src/test/java/org/opensearch/security/auth/InternalUserTokenHandlerTests.java @@ -0,0 +1,108 @@ +/* + * 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.auth; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockitoAnnotations; +import org.opensearch.common.settings.Settings; +import org.opensearch.identity.tokens.AuthToken; +import org.opensearch.identity.tokens.BasicAuthToken; +import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; +import org.opensearch.security.securityconf.impl.v7.InternalUserV7; +import org.opensearch.security.user.InternalUserTokenHandler; +import org.opensearch.security.user.UserService; +import org.opensearch.security.user.UserServiceException; + +import java.io.IOException; +import java.security.NoSuchAlgorithmException; + +import static org.junit.Assert.assertFalse; +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; +import static org.opensearch.security.dlic.rest.support.Utils.universalHash; +import static org.opensearch.test.OpenSearchTestCase.assertEquals; + + +public class InternalUserTokenHandlerTests { + private InternalUserTokenHandler internalUserTokenHandler; + private UserService userService; + private SecurityDynamicConfiguration internalUsersConfiguration; + + @Before + public void setup() { + MockitoAnnotations.openMocks(this); + userService = mock(UserService.class); + internalUsersConfiguration = mock(SecurityDynamicConfiguration.class); + when(userService.geInternalUsersConfigurationRepository()).thenReturn(internalUsersConfiguration); + internalUserTokenHandler = new InternalUserTokenHandler(Settings.EMPTY, userService); + internalUserTokenHandler.internalUsersConfiguration = internalUsersConfiguration; + } + + @Test + public void testIssueTokenWithValidAccount() throws IOException { + when(userService.generateAuthToken("test")).thenReturn("Basic dGVzdDp0ZTpzdA=="); // test:te:st + AuthToken token = internalUserTokenHandler.issueToken("test"); + BasicAuthToken basicAuthToken = (BasicAuthToken) token; + String accountName = basicAuthToken.getUser(); + String password = basicAuthToken.getPassword(); + assertEquals(accountName, "test"); + assertEquals(password, "te:st"); + } + + @Test + public void testIssueTokenWithInvalidAccount() throws IOException { + when(userService.generateAuthToken("test")).thenThrow(new UserServiceException("No account found")); // test:te:st + UserServiceException ex = assertThrows(UserServiceException.class, () -> internalUserTokenHandler.issueToken("test")); + assertTrue(ex.getMessage(), ex.getMessage().contains("Failed to generate an auth token for test")); + } + + @Test + public void testValidateGoodToken() throws IOException, NoSuchAlgorithmException { + when(userService.generateAuthToken("test")).thenReturn("Basic dGVzdDp0ZTpzdA=="); // test:te:st + when(internalUsersConfiguration.exists("test")).thenReturn(true); + when(internalUsersConfiguration.getCEntry("test")).thenReturn(new InternalUserV7(universalHash("te:st"), false, false, null, null, true, true)); + AuthToken token = internalUserTokenHandler.issueToken("test"); + assertTrue(internalUserTokenHandler.validateToken(token)); + } + + @Test + public void testValidateBadToken() throws IOException, NoSuchAlgorithmException { + when(userService.generateAuthToken("test")).thenReturn("Basic dGVzdDpnaWJiZXJpc2g="); // test:gibberish + when(internalUsersConfiguration.exists("test")).thenReturn(true); + when(internalUsersConfiguration.getCEntry("test")).thenReturn(new InternalUserV7(universalHash("te:st"), false, false, null, null, true, true)); + AuthToken token = internalUserTokenHandler.issueToken("test"); + assertFalse(internalUserTokenHandler.validateToken(token)); + } + + @Test + public void testGetTokenInfo() throws IOException { + when(userService.generateAuthToken("test")).thenReturn("Basic dGVzdDp0ZTpzdA=="); // test:te:st + AuthToken token = internalUserTokenHandler.issueToken("test"); + BasicAuthToken basicAuthToken = (BasicAuthToken) token; + assertTrue(internalUserTokenHandler.getTokenInfo(basicAuthToken).contains("The provided token is a BasicAuthToken with content: " )); + } + + @Test + public void testRevokeValidToken() throws IOException, NoSuchAlgorithmException { + when(userService.generateAuthToken("test")).thenReturn("Basic dGVzdDp0ZTpzdA=="); // test:te:st + when(internalUsersConfiguration.exists("test")).thenReturn(true); + when(internalUsersConfiguration.getCEntry("test")).thenReturn(new InternalUserV7(universalHash("te:st"), false, false, null, null, true, true)); + AuthToken token = internalUserTokenHandler.issueToken("test"); + internalUserTokenHandler.revokeToken(token); + verify(userService, times(1)).clearHash("test"); + } +} diff --git a/src/test/java/org/opensearch/security/auth/SecurityTokenManagerTests.java b/src/test/java/org/opensearch/security/auth/SecurityTokenManagerTests.java new file mode 100644 index 0000000000..83b98a1bbf --- /dev/null +++ b/src/test/java/org/opensearch/security/auth/SecurityTokenManagerTests.java @@ -0,0 +1,201 @@ +/* + * 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.auth; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockitoAnnotations; + +import org.opensearch.client.Client; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.settings.Settings; +import org.opensearch.identity.tokens.AuthToken; +import org.opensearch.identity.tokens.BasicAuthToken; +import org.opensearch.identity.tokens.BearerAuthToken; +import org.opensearch.identity.tokens.NoopToken; +import org.opensearch.security.configuration.ConfigurationRepository; +import org.opensearch.security.identity.SecurityTokenManager; +import org.opensearch.security.user.InternalUserTokenHandler; +import org.opensearch.security.user.UserService; +import org.opensearch.security.user.UserServiceException; +import org.opensearch.security.user.UserTokenHandler; +import org.opensearch.threadpool.ThreadPool; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doCallRealMethod; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.internal.verification.VerificationModeFactory.times; + +public class SecurityTokenManagerTests { + + + SecurityTokenManager securityTokenManager; + private UserTokenHandler userTokenHandler; + private InternalUserTokenHandler internalUserTokenHandler; + + private ClusterService clusterService; + UserService userService; + + + @Before + public void setup() { + MockitoAnnotations.openMocks(this); + Settings settings = Settings.EMPTY; + Client client = mock(Client.class); + ThreadPool threadPool = mock(ThreadPool.class); + ConfigurationRepository configurationRepository = mock(ConfigurationRepository.class); + clusterService = mock(ClusterService.class); + userService = mock(UserService.class); + securityTokenManager = spy(new SecurityTokenManager(threadPool, clusterService, configurationRepository, client, settings, userService)); + userTokenHandler = mock(UserTokenHandler.class); + internalUserTokenHandler = mock(InternalUserTokenHandler.class); + securityTokenManager.setInternalUserTokenHandler(internalUserTokenHandler); + securityTokenManager.setUserTokenHandler(userTokenHandler); + } + + @Test + public void testIssueTokenShouldPass() throws Exception { + doReturn(new BearerAuthToken("header.payload.signature")).when(userTokenHandler).issueToken("test"); + AuthToken createdBearerToken = securityTokenManager.issueToken("onBehalfOfToken", "test"); + assert(createdBearerToken instanceof BearerAuthToken); + BearerAuthToken bearerAuthToken = (BearerAuthToken) createdBearerToken; + String header = bearerAuthToken.getHeader(); + String payload = bearerAuthToken.getPayload(); + String signature = bearerAuthToken.getSignature(); + assertEquals(header, "header"); + assertEquals(payload, "payload"); + assertEquals(signature, "signature"); + + doReturn(new BasicAuthToken("Basic dGVzdDp0ZTpzdA==")).when(internalUserTokenHandler).issueToken("test"); + AuthToken createdBasicToken = securityTokenManager.issueToken("internalAuthToken", "test"); + assert(createdBasicToken instanceof BasicAuthToken); + BasicAuthToken basicAuthToken = (BasicAuthToken) createdBasicToken; + String accountName = basicAuthToken.getUser(); + String password = basicAuthToken.getPassword(); + assertEquals(accountName, "test"); + assertEquals(password, "te:st"); + } + + @Test + public void testIssueTokenShouldThrow() throws Exception { + Exception exception1 = assertThrows(UserServiceException.class, () -> securityTokenManager.issueToken()); + assert(exception1.getMessage().contains("The Security Plugin does not support generic token creation. Please specify a token type and argument.")); + + Exception exception2 = assertThrows(UserServiceException.class, () -> securityTokenManager.issueToken("notAToken", "test")); + assert(exception2.getMessage().contains("The provided type notAToken is not a valid token. Please specify either \"onBehalfOf\" or \"internalAuthToken\".")); + } + + @Test + public void testValidateTokenShouldPass() { + BearerAuthToken bearerAuthToken = new BearerAuthToken("header.payload.signature"); + doReturn(true).when(userTokenHandler).validateToken(bearerAuthToken); + + BasicAuthToken basicAuthToken = new BasicAuthToken("Basic dGVzdDp0ZTpzdA=="); + doReturn(true).when(internalUserTokenHandler).validateToken(basicAuthToken); + + assertTrue(securityTokenManager.validateToken(bearerAuthToken)); + assertTrue(securityTokenManager.validateToken(basicAuthToken)); + } + + @Test + public void testValidateTokenShouldFail() { + BearerAuthToken bearerAuthToken = new BearerAuthToken("header.payload.signature"); + doReturn(false).when(userTokenHandler).validateToken(bearerAuthToken); + + BasicAuthToken basicAuthToken = new BasicAuthToken("Basic dGVzdDp0ZTpzdA=="); + doReturn(false).when(internalUserTokenHandler).validateToken(basicAuthToken); + + assertFalse(securityTokenManager.validateToken(bearerAuthToken)); + assertFalse(securityTokenManager.validateToken(basicAuthToken)); + } + + @Test + public void testValidateTokenShouldThrow() { + Exception exception = assertThrows(UserServiceException.class, () -> securityTokenManager.validateToken(new NoopToken())); + assert(exception.getMessage().contains(securityTokenManager.TOKEN_NOT_SUPPORTED_MESSAGE)); + } + + @Test + public void testGetTokenInfoShouldPass() { + BearerAuthToken bearerAuthToken = new BearerAuthToken("header.payload.signature"); + doCallRealMethod().when(userTokenHandler).getTokenInfo(bearerAuthToken); + + BasicAuthToken basicAuthToken = new BasicAuthToken("Basic dGVzdDp0ZTpzdA=="); + doCallRealMethod().when(internalUserTokenHandler).getTokenInfo(basicAuthToken); + + assertTrue(securityTokenManager.getTokenInfo(bearerAuthToken).contains("The provided token is a BearerAuthToken with content: ")); + assertTrue(securityTokenManager.getTokenInfo(basicAuthToken).contains("The provided token is a BasicAuthToken with content: ")); + } + + @Test + public void testGetTokenInfoShouldThrow() { + NoopToken noopToken = new NoopToken(); + Exception exception = assertThrows(UserServiceException.class, () -> securityTokenManager.getTokenInfo(noopToken)); + assert(exception.getMessage().contains(securityTokenManager.TOKEN_NOT_SUPPORTED_MESSAGE)); + } + + @Test + public void testRevokeTokenShouldPass() throws Exception { + + doReturn(new BearerAuthToken("header.payload.signature")).when(userTokenHandler).issueToken("test"); + AuthToken createdBearerToken = securityTokenManager.issueToken("onBehalfOfToken", "test"); + assert(createdBearerToken instanceof BearerAuthToken); + doReturn(true).when(userTokenHandler).validateToken(createdBearerToken); + securityTokenManager.revokeToken(createdBearerToken); + verify(userTokenHandler, times(1)).revokeToken(createdBearerToken); + + doReturn(new BasicAuthToken("Basic dGVzdDp0ZTpzdA==")).when(internalUserTokenHandler).issueToken("test"); + AuthToken createdBasicToken = securityTokenManager.issueToken("internalAuthToken", "test"); + assert(createdBasicToken instanceof BasicAuthToken); + doReturn(true).when(internalUserTokenHandler).validateToken(createdBasicToken); + securityTokenManager.revokeToken(createdBasicToken); + verify(internalUserTokenHandler, times(1)).revokeToken(any()); + } + + @Test + public void testRevokeTokenShouldThrow() { + NoopToken noopToken = new NoopToken(); + Exception exception = assertThrows(UserServiceException.class, () -> securityTokenManager.revokeToken(noopToken)); + assert(exception.getMessage().contains(securityTokenManager.TOKEN_NOT_SUPPORTED_MESSAGE)); + } + + @Test + public void testResetTokenShouldPass() throws Exception { + doReturn(new BearerAuthToken("header.payload.signature")).when(userTokenHandler).issueToken("test"); + AuthToken createdBearerToken = securityTokenManager.issueToken("onBehalfOfToken", "test"); + assert(createdBearerToken instanceof BearerAuthToken); + doReturn(true).when(userTokenHandler).validateToken(createdBearerToken); + securityTokenManager.revokeToken(createdBearerToken); + verify(userTokenHandler, times(1)).revokeToken(createdBearerToken); + + doReturn(new BasicAuthToken("Basic dGVzdDp0ZTpzdA==")).when(internalUserTokenHandler).issueToken("test"); + AuthToken createdBasicToken = securityTokenManager.issueToken("internalAuthToken", "test"); + assert(createdBasicToken instanceof BasicAuthToken); + doReturn(true).when(internalUserTokenHandler).validateToken(createdBasicToken); + securityTokenManager.revokeToken(createdBasicToken); + verify(internalUserTokenHandler, times(1)).revokeToken(any()); + } + + @Test + public void testResetTokenShouldThrow() { + NoopToken noopToken = new NoopToken(); + Exception exception = assertThrows(UserServiceException.class, () -> securityTokenManager.resetToken(noopToken)); + assert(exception.getMessage().contains(securityTokenManager.TOKEN_NOT_SUPPORTED_MESSAGE)); + } +} diff --git a/src/test/java/org/opensearch/security/auth/UserTokenHandlerTests.java b/src/test/java/org/opensearch/security/auth/UserTokenHandlerTests.java new file mode 100644 index 0000000000..bfac4d9803 --- /dev/null +++ b/src/test/java/org/opensearch/security/auth/UserTokenHandlerTests.java @@ -0,0 +1,226 @@ +/* + * 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.auth; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockitoAnnotations; +import org.opensearch.client.Client; +import org.opensearch.cluster.ClusterName; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.settings.Settings; +import org.opensearch.identity.tokens.AuthToken; +import org.opensearch.identity.tokens.BearerAuthToken; +import org.opensearch.identity.tokens.NoopToken; +import org.opensearch.security.authtoken.jwt.JwtVendor; +import org.opensearch.security.configuration.ConfigurationRepository; +import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; +import org.opensearch.security.user.User; +import org.opensearch.security.user.UserServiceException; +import org.opensearch.security.user.UserTokenHandler; +import org.opensearch.threadpool.ThreadPool; + +import java.util.ArrayList; +import java.util.Arrays; + +import static java.lang.Thread.sleep; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.internal.verification.VerificationModeFactory.times; +import static org.opensearch.security.user.UserTokenHandler.getRevokedTokensConfigName; + +public class UserTokenHandlerTests { + + private UserTokenHandler userTokenHandler; + private SecurityDynamicConfiguration revokedTokensConfiguration; + + private JwtVendor jwtVendor; + private Client client; + + private ClusterService clusterService; + + private User user; + + int DEFAULT_EXPIRATION_TIME_SECONDS = 300; + + @Before + public void setup() { + MockitoAnnotations.openMocks(this); + Settings settings = Settings.builder() + .put("signing_key", "abc123") + .put("encryption_key", "def456").build(); + jwtVendor = spy(new JwtVendor(settings)); + client = mock(Client.class); + user = new User("test_user"); + ThreadPool threadPool = mock(ThreadPool.class); + ConfigurationRepository configurationRepository = mock(ConfigurationRepository.class); + clusterService = mock(ClusterService.class); + revokedTokensConfiguration = spy(SecurityDynamicConfiguration.empty()); + userTokenHandler = spy(new UserTokenHandler(threadPool, clusterService, configurationRepository, client)); + } + + @Test + public void testIssueTokenConstruction() throws Exception { + when(clusterService.getClusterName()).thenReturn(new ClusterName("test_cluster")); + when(jwtVendor.createJwt(clusterService.getClusterName().toString(), user.getName(), "test", DEFAULT_EXPIRATION_TIME_SECONDS, new ArrayList(user.getRoles()))).thenReturn("header.payload.signature"); + + // This is required because the ThreadContext cannot be mocked -- basically skips over the step of pulling the User from the threadContext + doReturn((new BearerAuthToken(jwtVendor.createJwt(clusterService.getClusterName().toString(), user.getName(), "test", DEFAULT_EXPIRATION_TIME_SECONDS, new ArrayList(user.getRoles()))))).when(userTokenHandler).issueToken("test"); + + AuthToken token = userTokenHandler.issueToken("test"); + BearerAuthToken bearerAuthToken = (BearerAuthToken) token; + String header = bearerAuthToken.getHeader(); + String payload = bearerAuthToken.getPayload(); + String signature = bearerAuthToken.getSignature(); + assertEquals(header, "header"); + assertEquals(payload, "payload"); + assertEquals(signature, "signature"); + } + + @Test + public void testIssueTokenShouldValidate() throws Exception { + when(clusterService.getClusterName()).thenReturn(new ClusterName("test_cluster")); + doReturn(revokedTokensConfiguration).when(userTokenHandler).load(getRevokedTokensConfigName(), false); + when(jwtVendor.createJwt(clusterService.getClusterName().toString(), user.getName(), "test", DEFAULT_EXPIRATION_TIME_SECONDS, Arrays.asList("test_role1", + "test_role2", + "test_role3"))).thenCallRealMethod(); + + String tokenString = jwtVendor.createJwt(clusterService.getClusterName().toString(), user.getName(), "test", DEFAULT_EXPIRATION_TIME_SECONDS, Arrays.asList("test_role1", + "test_role2", + "test_role3")); + + // This is required because the ThreadContext cannot be mocked -- basically skips over the step of pulling the User from the threadContext + doReturn(new BearerAuthToken(tokenString)).when(userTokenHandler).issueToken("test"); + + AuthToken token = userTokenHandler.issueToken("test"); + boolean isValid = userTokenHandler.validateToken(token); + assertTrue(isValid); + } + + @Test + public void testIssueTokenShouldThrowValidate() throws Exception { + when(clusterService.getClusterName()).thenReturn(new ClusterName("test_cluster")); + doReturn(revokedTokensConfiguration).when(userTokenHandler).load(getRevokedTokensConfigName(), false); + when(jwtVendor.createJwt(clusterService.getClusterName().toString(), user.getName(), "test", DEFAULT_EXPIRATION_TIME_SECONDS, Arrays.asList("test_role1", + "test_role2", + "test_role3"))).thenCallRealMethod(); + + String tokenString = jwtVendor.createJwt(clusterService.getClusterName().toString(), user.getName(), "test", DEFAULT_EXPIRATION_TIME_SECONDS, Arrays.asList("test_role1", + "test_role2", + "test_role3")); + + // This is required because the ThreadContext cannot be mocked -- basically skips over the step of pulling the User from the threadContext + doReturn(new NoopToken()).when(userTokenHandler).issueToken("test"); + + AuthToken token = userTokenHandler.issueToken("test"); + Exception ex = assertThrows(UserServiceException.class, () -> userTokenHandler.validateToken(token)); + assert(ex.getMessage().contains("The provided token is not a BearerAuthToken.")); + } + + @Test + public void testIssueTokenShouldFailValidate() throws Exception { + when(clusterService.getClusterName()).thenReturn(new ClusterName("test_cluster")); + doReturn(revokedTokensConfiguration).when(userTokenHandler).load(getRevokedTokensConfigName(), false); + when(jwtVendor.createJwt(clusterService.getClusterName().toString(), user.getName(), "test", 1, Arrays.asList("test_role1", + "test_role2", + "test_role3"))).thenCallRealMethod(); + + String tokenString = jwtVendor.createJwt(clusterService.getClusterName().toString(), user.getName(), "test", 1, Arrays.asList("test_role1", + "test_role2", + "test_role3")); + + // This is required because the ThreadContext cannot be mocked -- basically skips over the step of pulling the User from the threadContext + doReturn(new BearerAuthToken(tokenString)).when(userTokenHandler).issueToken("test"); + + AuthToken token = userTokenHandler.issueToken("test"); + sleep(1000); // Wait for token to expire + boolean isValid = userTokenHandler.validateToken(token); + assertFalse(isValid); + } + + @Test + public void testIssueTokenThenRevoke() throws Exception { + when(clusterService.getClusterName()).thenReturn(new ClusterName("test_cluster")); + doReturn(revokedTokensConfiguration).when(userTokenHandler).load(getRevokedTokensConfigName(), false); + doNothing().when(userTokenHandler).saveAndUpdateConfigs(any(), any(), any(), any()); + when(jwtVendor.createJwt(clusterService.getClusterName().toString(), user.getName(), "test", 1, Arrays.asList("test_role1", + "test_role2", + "test_role3"))).thenCallRealMethod(); + + String tokenString = jwtVendor.createJwt(clusterService.getClusterName().toString(), user.getName(), "test", 1, Arrays.asList("test_role1", + "test_role2", + "test_role3")); + + // This is required because the ThreadContext cannot be mocked -- basically skips over the step of pulling the User from the threadContext + doReturn(new BearerAuthToken(tokenString)).when(userTokenHandler).issueToken("test"); + + AuthToken token = userTokenHandler.issueToken("test"); + + userTokenHandler.revokeToken(token); + verify(userTokenHandler, times(1)).saveAndUpdateConfigs(any(), any(), any(), any()); + } + + @Test + public void testFailValidationAfterRevoke() throws Exception { + + when(clusterService.getClusterName()).thenReturn(new ClusterName("test_cluster")); + doReturn(revokedTokensConfiguration).when(userTokenHandler).load(getRevokedTokensConfigName(), false); + when(jwtVendor.createJwt(clusterService.getClusterName().toString(), user.getName(), "test", 20, Arrays.asList("test_role1", + "test_role2", + "test_role3"))).thenCallRealMethod(); + + String tokenString = jwtVendor.createJwt(clusterService.getClusterName().toString(), user.getName(), "test", 20, Arrays.asList("test_role1", + "test_role2", + "test_role3")); + + // This is required because the ThreadContext cannot be mocked -- basically skips over the step of pulling the User from the threadContext + doReturn(new BearerAuthToken(tokenString)).when(userTokenHandler).issueToken("test"); + + AuthToken token = userTokenHandler.issueToken("test"); + BearerAuthToken bearerAuthToken = (BearerAuthToken) token; + revokedTokensConfiguration.putCEntry(bearerAuthToken.getCompleteToken(), bearerAuthToken); + + boolean isValid = userTokenHandler.validateToken(token); + assertFalse(isValid); + + } + + @Test + public void testResetShouldThrowException() throws Exception { + when(clusterService.getClusterName()).thenReturn(new ClusterName("test_cluster")); + doReturn(revokedTokensConfiguration).when(userTokenHandler).load(getRevokedTokensConfigName(), false); + when(jwtVendor.createJwt(clusterService.getClusterName().toString(), user.getName(), "test", DEFAULT_EXPIRATION_TIME_SECONDS, Arrays.asList("test_role1", + "test_role2", + "test_role3"))).thenCallRealMethod(); + + String tokenString = jwtVendor.createJwt(clusterService.getClusterName().toString(), user.getName(), "test", DEFAULT_EXPIRATION_TIME_SECONDS, Arrays.asList("test_role1", + "test_role2", + "test_role3")); + + // This is required because the ThreadContext cannot be mocked -- basically skips over the step of pulling the User from the threadContext + doReturn(new BearerAuthToken(tokenString)).when(userTokenHandler).issueToken("test"); + + AuthToken token = userTokenHandler.issueToken("test"); + Exception exception = assertThrows(UserServiceException.class, () -> userTokenHandler.resetToken(token)); + assert(exception.getMessage().contains("The UserTokenHandler does not support the reset operation. Please issue a new token instead.")); + } +} + diff --git a/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTests.java b/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTests.java new file mode 100644 index 0000000000..b406dc2cfd --- /dev/null +++ b/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTests.java @@ -0,0 +1,117 @@ +/* + * 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.util.List; +import java.util.function.LongSupplier; + +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.cxf.rs.security.jose.jwk.JsonWebKey; +import org.apache.cxf.rs.security.jose.jws.JwsJwtCompactConsumer; +import org.apache.cxf.rs.security.jose.jwt.JwtToken; +import org.junit.Assert; +import org.junit.Test; + +import org.opensearch.common.settings.Settings; + +public class JwtVendorTests { + + @Test + public void testCreateJwkFromSettings() throws Exception { + Settings settings = Settings.builder().put("signing_key", "abc123").build(); + + JsonWebKey jwk = JwtVendor.createJwkFromSettings(settings); + Assert.assertEquals("HS512", jwk.getAlgorithm()); + Assert.assertEquals("sig", jwk.getPublicKeyUse().toString()); + Assert.assertEquals("abc123", jwk.getProperty("k")); + } + + @Test(expected = Exception.class) + public void testCreateJwkFromSettingsWithoutSigningKey() throws Exception { + Settings settings = Settings.builder().put("jwt", "").build(); + JwtVendor.createJwkFromSettings(settings); + } + + @Test + public void testCreateJwtWithRoles() throws Exception { + String issuer = "cluster_0"; + String subject = "admin"; + String audience = "audience_0"; + List roles = List.of("IT", "HR"); + String expectedRoles = "IT,HR"; + Integer expirySeconds = 300; + LongSupplier currentTime = () -> (int) 100; + String claimsEncryptionKey = RandomStringUtils.randomAlphanumeric(16); + Settings settings = Settings.builder().put("signing_key", "abc123").put("encryption_key", claimsEncryptionKey).build(); + Long expectedExp = currentTime.getAsLong() + (expirySeconds * 1000); + + JwtVendor jwtVendor = new JwtVendor(settings, currentTime); + String encodedJwt = jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles); + + JwsJwtCompactConsumer jwtConsumer = new JwsJwtCompactConsumer(encodedJwt); + JwtToken jwt = jwtConsumer.getJwtToken(); + + Assert.assertEquals("cluster_0", jwt.getClaim("iss")); + Assert.assertEquals("admin", jwt.getClaim("sub")); + Assert.assertEquals("audience_0", jwt.getClaim("aud")); + Assert.assertNotNull(jwt.getClaim("iat")); + Assert.assertNotNull(jwt.getClaim("exp")); + Assert.assertEquals(expectedExp, jwt.getClaim("exp")); + Assert.assertNotEquals(expectedRoles, jwt.getClaim("er")); + Assert.assertEquals(expectedRoles, EncryptionDecryptionUtil.decrypt(claimsEncryptionKey, jwt.getClaim("er").toString())); + } + + @Test(expected = Exception.class) + public void testCreateJwtWithBadExpiry() 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", "abc123").put("encryption_key", claimsEncryptionKey).build(); + JwtVendor jwtVendor = new JwtVendor(settings); + + jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles); + } + + @Test(expected = Exception.class) + public void testCreateJwtWithBadEncryptionKey() throws Exception { + String issuer = "cluster_0"; + String subject = "admin"; + String audience = "audience_0"; + List roles = List.of("admin"); + Integer expirySeconds = 300; + + Settings settings = Settings.builder().put("signing_key", "abc123").build(); + JwtVendor jwtVendor = new JwtVendor(settings); + + jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles); + } + + @Test(expected = Exception.class) + public void testCreateJwtWithBadRoles() throws Exception { + String issuer = "cluster_0"; + String subject = "admin"; + String audience = "audience_0"; + List roles = null; + Integer expirySecond = 300; + String claimsEncryptionKey = RandomStringUtils.randomAlphanumeric(16); + + Settings settings = Settings.builder().put("signing_key", "abc123").put("encryption_key", claimsEncryptionKey).build(); + + JwtVendor jwtVendor = new JwtVendor(settings); + + jwtVendor.createJwt(issuer, subject, audience, expirySecond, roles); + } +} diff --git a/src/test/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticatorTests.java b/src/test/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticatorTests.java new file mode 100644 index 0000000000..6e56ea833e --- /dev/null +++ b/src/test/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticatorTests.java @@ -0,0 +1,302 @@ +/* + * 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.nio.charset.StandardCharsets; +import java.security.SecureRandom; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.crypto.SecretKey; + +import com.google.common.io.BaseEncoding; +import io.jsonwebtoken.JwtBuilder; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.hc.core5.http.HttpHeaders; +import org.junit.Assert; +import org.junit.Test; + +import com.amazon.dlic.auth.http.jwt.HTTPJwtAuthenticator; + +import org.opensearch.common.settings.Settings; +import org.opensearch.security.user.AuthCredentials; +import org.opensearch.security.util.FakeRestRequest; + +public class HTTPOnBehalfOfJwtAuthenticatorTests { + final static byte[] secretKeyBytes = new byte[1024]; + final static String claimsEncryptionKey = RandomStringUtils.randomAlphanumeric(16); + final static SecretKey secretKey; + + static { + new SecureRandom().nextBytes(secretKeyBytes); + secretKey = Keys.hmacShaKeyFor(secretKeyBytes); + } + + final static String signingKey = BaseEncoding.base64().encode(secretKeyBytes); + + @Test + public void testNoKey() throws Exception { + + final AuthCredentials credentials = extractCredentialsFromJwtHeader( + null, + claimsEncryptionKey, + Jwts.builder().setSubject("Leonard McCoy"), + false + ); + + Assert.assertNull(credentials); + } + + @Test + public void testEmptyKey() throws Exception { + + final AuthCredentials credentials = extractCredentialsFromJwtHeader( + "", + claimsEncryptionKey, + Jwts.builder().setSubject("Leonard McCoy"), + false + ); + + Assert.assertNull(credentials); + } + + @Test + public void testBadKey() throws Exception { + + final AuthCredentials credentials = extractCredentialsFromJwtHeader( + BaseEncoding.base64().encode(new byte[] { 1, 3, 3, 4, 3, 6, 7, 8, 3, 10 }), + claimsEncryptionKey, + Jwts.builder().setSubject("Leonard McCoy"), + false + ); + + Assert.assertNull(credentials); + } + + @Test + public void testTokenMissing() throws Exception { + + HTTPOnBehalfOfJwtAuthenticator jwtAuth = new HTTPOnBehalfOfJwtAuthenticator( + BaseEncoding.base64().encode(secretKeyBytes), + claimsEncryptionKey + ); + Map headers = new HashMap(); + + AuthCredentials credentials = jwtAuth.extractCredentials(new FakeRestRequest(headers, new HashMap()), null); + + Assert.assertNull(credentials); + } + + @Test + public void testInvalid() throws Exception { + + String jwsToken = "123invalidtoken.."; + + HTTPOnBehalfOfJwtAuthenticator jwtAuth = new HTTPOnBehalfOfJwtAuthenticator( + BaseEncoding.base64().encode(secretKeyBytes), + claimsEncryptionKey + ); + Map headers = new HashMap(); + headers.put("Authorization", "Bearer " + jwsToken); + + AuthCredentials credentials = jwtAuth.extractCredentials(new FakeRestRequest(headers, new HashMap()), null); + Assert.assertNull(credentials); + } + + @Test + public void testBearer() throws Exception { + + String jwsToken = Jwts.builder() + .setSubject("Leonard McCoy") + .setAudience("ext_0") + .signWith(secretKey, SignatureAlgorithm.HS512) + .compact(); + + HTTPOnBehalfOfJwtAuthenticator jwtAuth = new HTTPOnBehalfOfJwtAuthenticator( + BaseEncoding.base64().encode(secretKeyBytes), + claimsEncryptionKey + ); + Map headers = new HashMap(); + headers.put("Authorization", "Bearer " + jwsToken); + + AuthCredentials credentials = jwtAuth.extractCredentials(new FakeRestRequest(headers, new HashMap()), null); + + Assert.assertNotNull(credentials); + Assert.assertEquals("Leonard McCoy", credentials.getUsername()); + Assert.assertEquals(0, credentials.getBackendRoles().size()); + Assert.assertEquals(2, credentials.getAttributes().size()); + } + + @Test + public void testBearerWrongPosition() throws Exception { + + Settings settings = Settings.builder().put("signing_key", BaseEncoding.base64().encode(secretKeyBytes)).build(); + + String jwsToken = Jwts.builder().setSubject("Leonard McCoy").signWith(secretKey, SignatureAlgorithm.HS512).compact(); + + HTTPJwtAuthenticator jwtAuth = new HTTPJwtAuthenticator(settings, null); + Map headers = new HashMap(); + headers.put("Authorization", jwsToken + "Bearer " + " 123"); + + AuthCredentials credentials = jwtAuth.extractCredentials(new FakeRestRequest(headers, new HashMap()), null); + + Assert.assertNull(credentials); + } + + @Test + public void testBasicAuthHeader() throws Exception { + Settings settings = Settings.builder().put("signing_key", BaseEncoding.base64().encode(secretKeyBytes)).build(); + HTTPJwtAuthenticator jwtAuth = new HTTPJwtAuthenticator(settings, null); + + String basicAuth = BaseEncoding.base64().encode("user:password".getBytes(StandardCharsets.UTF_8)); + Map headers = Collections.singletonMap(HttpHeaders.AUTHORIZATION, "Basic " + basicAuth); + + AuthCredentials credentials = jwtAuth.extractCredentials(new FakeRestRequest(headers, Collections.emptyMap()), null); + Assert.assertNull(credentials); + } + + @Test + public void testRoles() throws Exception { + + List roles = List.of("IT", "HR"); + final AuthCredentials credentials = extractCredentialsFromJwtHeader( + signingKey, + claimsEncryptionKey, + Jwts.builder().setSubject("Leonard McCoy").claim("dr", "role1,role2"), + true + ); + + Assert.assertNotNull(credentials); + Assert.assertEquals("Leonard McCoy", credentials.getUsername()); + Assert.assertEquals(2, credentials.getBackendRoles().size()); + } + + @Test + public void testNullClaim() throws Exception { + + final AuthCredentials credentials = extractCredentialsFromJwtHeader( + signingKey, + claimsEncryptionKey, + Jwts.builder().setSubject("Leonard McCoy").claim("dr", null), + false + ); + + Assert.assertNotNull(credentials); + Assert.assertEquals("Leonard McCoy", credentials.getUsername()); + Assert.assertEquals(0, credentials.getBackendRoles().size()); + } + + @Test + public void testNonStringClaim() throws Exception { + + final AuthCredentials credentials = extractCredentialsFromJwtHeader( + signingKey, + claimsEncryptionKey, + Jwts.builder().setSubject("Leonard McCoy").claim("dr", 123L), + true + ); + + Assert.assertNotNull(credentials); + Assert.assertEquals("Leonard McCoy", credentials.getUsername()); + Assert.assertEquals(1, credentials.getBackendRoles().size()); + Assert.assertTrue(credentials.getBackendRoles().contains("123")); + } + + @Test + public void testRolesMissing() throws Exception { + + final AuthCredentials credentials = extractCredentialsFromJwtHeader( + signingKey, + claimsEncryptionKey, + Jwts.builder().setSubject("Leonard McCoy"), + false + ); + + Assert.assertNotNull(credentials); + Assert.assertEquals("Leonard McCoy", credentials.getUsername()); + Assert.assertEquals(0, credentials.getBackendRoles().size()); + } + + @Test + public void testWrongSubjectKey() throws Exception { + + final AuthCredentials credentials = extractCredentialsFromJwtHeader( + signingKey, + claimsEncryptionKey, + Jwts.builder().claim("roles", "role1,role2").claim("asub", "Dr. Who"), + false + ); + + Assert.assertNull(credentials); + } + + @Test + public void testExp() throws Exception { + + final AuthCredentials credentials = extractCredentialsFromJwtHeader( + signingKey, + claimsEncryptionKey, + Jwts.builder().setSubject("Expired").setExpiration(new Date(100)), + false + ); + + Assert.assertNull(credentials); + } + + @Test + public void testNbf() throws Exception { + + final AuthCredentials credentials = extractCredentialsFromJwtHeader( + signingKey, + claimsEncryptionKey, + Jwts.builder().setSubject("Expired").setNotBefore(new Date(System.currentTimeMillis() + (1000 * 36000))), + false + ); + + Assert.assertNull(credentials); + } + + @Test + public void testRolesArray() throws Exception { + + JwtBuilder builder = Jwts.builder() + .setPayload("{" + "\"sub\": \"Cluster_0\"," + "\"aud\": \"ext_0\"," + "\"dr\": \"a,b,3rd\"" + "}"); + + final AuthCredentials credentials = extractCredentialsFromJwtHeader(signingKey, claimsEncryptionKey, builder, true); + + Assert.assertNotNull(credentials); + Assert.assertEquals("Cluster_0", credentials.getUsername()); + Assert.assertEquals(3, credentials.getBackendRoles().size()); + Assert.assertTrue(credentials.getBackendRoles().contains("a")); + Assert.assertTrue(credentials.getBackendRoles().contains("b")); + Assert.assertTrue(credentials.getBackendRoles().contains("3rd")); + } + + /** extracts a default user credential from a request header */ + private AuthCredentials extractCredentialsFromJwtHeader( + final String signingKey, + final String encryptionKey, + final JwtBuilder jwtBuilder, + final Boolean bwcPluginCompatibilityMode + ) { + final String jwsToken = jwtBuilder.signWith(secretKey, SignatureAlgorithm.HS512).compact(); + final HTTPOnBehalfOfJwtAuthenticator jwtAuth = new HTTPOnBehalfOfJwtAuthenticator(signingKey, encryptionKey); + final Map headers = Map.of("Authorization", "Bearer " + jwsToken); + return jwtAuth.extractCredentials(new FakeRestRequest(headers, new HashMap<>()), null); + } +} diff --git a/src/test/resources/config.yml b/src/test/resources/config.yml index 3663b3c706..c4bb432125 100644 --- a/src/test/resources/config.yml +++ b/src/test/resources/config.yml @@ -96,3 +96,6 @@ config: multi_rolespan_enabled: false hosts_resolver_mode: "ip-only" transport_userrname_attribute: null + on_behalf_of: + signing_key: "signing key" + encryption_key: "encryption key" diff --git a/src/test/resources/restapi/securityconfig.json b/src/test/resources/restapi/securityconfig.json index 4e4b1bba63..13bc7c23a6 100644 --- a/src/test/resources/restapi/securityconfig.json +++ b/src/test/resources/restapi/securityconfig.json @@ -153,7 +153,9 @@ "do_not_fail_on_forbidden":false, "multi_rolespan_enabled":false, "hosts_resolver_mode":"ip-only", - "do_not_fail_on_forbidden_empty":false + "do_not_fail_on_forbidden_empty":false, + "on_behalf_of": { + } } } diff --git a/src/test/resources/restapi/securityconfig_nondefault.json b/src/test/resources/restapi/securityconfig_nondefault.json index 6fb297be37..e30ca9148b 100644 --- a/src/test/resources/restapi/securityconfig_nondefault.json +++ b/src/test/resources/restapi/securityconfig_nondefault.json @@ -170,6 +170,10 @@ "do_not_fail_on_forbidden" : false, "multi_rolespan_enabled" : true, "hosts_resolver_mode" : "ip-only", - "do_not_fail_on_forbidden_empty" : false + "do_not_fail_on_forbidden_empty" : false, + "on_behalf_of": { + "signing_key": "signing key", + "encryption_key": "encryption key" + } } } From 72d2bc1669caf9a3ee87859b7948ae9c684ad756 Mon Sep 17 00:00:00 2001 From: Stephen Crawford Date: Wed, 5 Jul 2023 13:15:39 -0400 Subject: [PATCH 2/2] Update Manager Signed-off-by: Stephen Crawford --- .../security/OpenSearchSecurityPlugin.java | 14 ++ .../security/auth/BackendRegistry.java | 206 +++++++++--------- .../security/authtoken/jwt/JwtVendor.java | 12 +- .../dlic/rest/api/InternalUsersApiAction.java | 3 + .../security/dlic/rest/support/Utils.java | 3 +- .../http/HTTPOnBehalfOfJwtAuthenticator.java | 10 +- .../identity/SecurityTokenManager.java | 62 +++--- .../securityconf/DynamicConfigModelV7.java | 6 +- .../security/securityconf/impl/CType.java | 3 +- .../securityconf/impl/v6/ConfigV6.java | 114 +++++----- .../securityconf/impl/v7/ConfigV7.java | 123 ++++++----- .../securityconf/impl/v7/InternalUserV7.java | 2 +- .../user/InternalUserTokenHandler.java | 15 +- .../opensearch/security/user/UserService.java | 98 +++++---- .../security/user/UserTokenHandler.java | 58 +++-- .../auth/InternalUserTokenHandlerTests.java | 3 +- .../auth/SecurityTokenManagerTests.java | 81 ++++--- .../security/auth/UserTokenHandlerTests.java | 33 +-- .../HTTPOnBehalfOfJwtAuthenticatorTests.java | 110 +++++----- 19 files changed, 492 insertions(+), 464 deletions(-) diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index d4cbd61010..d27350f19a 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -98,6 +98,8 @@ import org.opensearch.extensions.ExtensionsManager; import org.opensearch.http.HttpServerTransport; import org.opensearch.http.HttpServerTransport.Dispatcher; +import org.opensearch.identity.Subject; +import org.opensearch.identity.tokens.TokenManager; import org.opensearch.index.Index; import org.opensearch.index.IndexModule; import org.opensearch.index.cache.query.QueryCache; @@ -142,9 +144,11 @@ import org.opensearch.security.dlic.rest.validation.PasswordValidator; import org.opensearch.security.filter.SecurityFilter; import org.opensearch.security.filter.SecurityRestFilter; +import org.opensearch.security.http.HTTPOnBehalfOfJwtAuthenticator; import org.opensearch.security.http.SecurityHttpServerTransport; import org.opensearch.security.http.SecurityNonSslHttpServerTransport; import org.opensearch.security.http.XFFResolver; +import org.opensearch.security.identity.SecurityTokenManager; import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.privileges.PrivilegesInterceptor; import org.opensearch.security.resolver.IndexResolverReplacer; @@ -1883,6 +1887,16 @@ private static String handleKeyword(final String field) { return field; } + @Override + public Subject getSubject() { + return null; + } + + @Override + public TokenManager getTokenManager() { + return securityTokenManager; + } + public static class GuiceHolder implements LifecycleComponent { private static RepositoriesService repositoriesService; diff --git a/src/main/java/org/opensearch/security/auth/BackendRegistry.java b/src/main/java/org/opensearch/security/auth/BackendRegistry.java index 4c44075871..0a287d19f5 100644 --- a/src/main/java/org/opensearch/security/auth/BackendRegistry.java +++ b/src/main/java/org/opensearch/security/auth/BackendRegistry.java @@ -95,43 +95,43 @@ public class BackendRegistry { private void createCaches() { userCache = CacheBuilder.newBuilder() - .expireAfterWrite(ttlInMin, TimeUnit.MINUTES) - .removalListener(new RemovalListener() { - @Override - public void onRemoval(RemovalNotification notification) { - log.debug("Clear user cache for {} due to {}", notification.getKey().getUsername(), notification.getCause()); - } - }) - .build(); + .expireAfterWrite(ttlInMin, TimeUnit.MINUTES) + .removalListener(new RemovalListener() { + @Override + public void onRemoval(RemovalNotification notification) { + log.debug("Clear user cache for {} due to {}", notification.getKey().getUsername(), notification.getCause()); + } + }) + .build(); restImpersonationCache = CacheBuilder.newBuilder() - .expireAfterWrite(ttlInMin, TimeUnit.MINUTES) - .removalListener(new RemovalListener() { - @Override - public void onRemoval(RemovalNotification notification) { - log.debug("Clear user cache for {} due to {}", notification.getKey(), notification.getCause()); - } - }) - .build(); + .expireAfterWrite(ttlInMin, TimeUnit.MINUTES) + .removalListener(new RemovalListener() { + @Override + public void onRemoval(RemovalNotification notification) { + log.debug("Clear user cache for {} due to {}", notification.getKey(), notification.getCause()); + } + }) + .build(); restRoleCache = CacheBuilder.newBuilder() - .expireAfterWrite(ttlInMin, TimeUnit.MINUTES) - .removalListener(new RemovalListener>() { - @Override - public void onRemoval(RemovalNotification> notification) { - log.debug("Clear user cache for {} due to {}", notification.getKey(), notification.getCause()); - } - }) - .build(); + .expireAfterWrite(ttlInMin, TimeUnit.MINUTES) + .removalListener(new RemovalListener>() { + @Override + public void onRemoval(RemovalNotification> notification) { + log.debug("Clear user cache for {} due to {}", notification.getKey(), notification.getCause()); + } + }) + .build(); } public BackendRegistry( - final Settings settings, - final AdminDNs adminDns, - final XFFResolver xffResolver, - final AuditLog auditLog, - final ThreadPool threadPool + final Settings settings, + final AdminDNs adminDns, + final XFFResolver xffResolver, + final AuditLog auditLog, + final ThreadPool threadPool ) { this.adminDns = adminDns; this.opensearchSettings = settings; @@ -163,7 +163,7 @@ public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { invalidateCache(); anonymousAuthEnabled = dcm.isAnonymousAuthenticationEnabled()// config.dynamic.http.anonymous_auth_enabled - && !opensearchSettings.getAsBoolean(ConfigConstants.SECURITY_COMPLIANCE_DISABLE_ANONYMOUS_AUTHENTICATION, false); + && !opensearchSettings.getAsBoolean(ConfigConstants.SECURITY_COMPLIANCE_DISABLE_ANONYMOUS_AUTHENTICATION, false); restAuthDomains = Collections.unmodifiableSortedSet(dcm.getRestAuthDomains()); restAuthorizers = Collections.unmodifiableSet(dcm.getRestAuthorizers()); @@ -187,7 +187,7 @@ public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { public boolean authenticate(final RestRequest request, final RestChannel channel, final ThreadContext threadContext) { final boolean isDebugEnabled = log.isDebugEnabled(); if (request.getHttpChannel().getRemoteAddress() instanceof InetSocketAddress - && isBlocked(((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).getAddress())) { + && isBlocked(((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).getAddress())) { if (isDebugEnabled) { log.debug("Rejecting REST request because of blocked address: {}", request.getHttpChannel().getRemoteAddress()); } @@ -237,10 +237,10 @@ && isBlocked(((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).g for (final AuthDomain authDomain : restAuthDomains) { if (isDebugEnabled) { log.debug( - "Check authdomain for rest {}/{} or {} in total", - authDomain.getBackend().getType(), - authDomain.getOrder(), - restAuthDomains.size() + "Check authdomain for rest {}/{} or {} in total", + authDomain.getBackend().getType(), + authDomain.getOrder(), + restAuthDomains.size() ); } @@ -311,22 +311,22 @@ && isBlocked(((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).g if (authenticatedUser == null) { if (isDebugEnabled) { log.debug( - "Cannot authenticate rest user {} (or add roles) with authdomain {}/{} of {}, try next", - ac.getUsername(), - authDomain.getBackend().getType(), - authDomain.getOrder(), - restAuthDomains + "Cannot authenticate rest user {} (or add roles) with authdomain {}/{} of {}, try next", + ac.getUsername(), + authDomain.getBackend().getType(), + authDomain.getOrder(), + restAuthDomains ); } for (AuthFailureListener authFailureListener : this.authBackendFailureListeners.get( - authDomain.getBackend().getClass().getName() + authDomain.getBackend().getClass().getName() )) { authFailureListener.onAuthFailure( - (request.getHttpChannel().getRemoteAddress() instanceof InetSocketAddress) - ? ((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).getAddress() - : null, - ac, - request + (request.getHttpChannel().getRemoteAddress() instanceof InetSocketAddress) + ? ((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).getAddress() + : null, + ac, + request ); } continue; @@ -336,10 +336,10 @@ && isBlocked(((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).g log.error("Cannot authenticate rest user because admin user is not permitted to login via HTTP"); auditLog.logFailedLogin(authenticatedUser.getName(), true, null, request); channel.sendResponse( - new BytesRestResponse( - RestStatus.FORBIDDEN, - "Cannot authenticate user because admin user is not permitted to login via HTTP" - ) + new BytesRestResponse( + RestStatus.FORBIDDEN, + "Cannot authenticate user because admin user is not permitted to login via HTTP" + ) ); return false; } @@ -359,14 +359,14 @@ && isBlocked(((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).g if (authenticated) { final User impersonatedUser = impersonate(request, authenticatedUser); threadContext.putTransient( - ConfigConstants.OPENDISTRO_SECURITY_USER, - impersonatedUser == null ? authenticatedUser : impersonatedUser + ConfigConstants.OPENDISTRO_SECURITY_USER, + impersonatedUser == null ? authenticatedUser : impersonatedUser ); auditLog.logSucceededLogin( - (impersonatedUser == null ? authenticatedUser : impersonatedUser).getName(), - false, - authenticatedUser.getName(), - request + (impersonatedUser == null ? authenticatedUser : impersonatedUser).getName(), + false, + authenticatedUser.getName(), + request ); } else { if (isDebugEnabled) { @@ -398,9 +398,9 @@ && isBlocked(((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).g } log.warn( - "Authentication finally failed for {} from {}", - authCredenetials == null ? null : authCredenetials.getUsername(), - remoteAddress + "Authentication finally failed for {} from {}", + authCredenetials == null ? null : authCredenetials.getUsername(), + remoteAddress ); auditLog.logFailedLogin(authCredenetials == null ? null : authCredenetials.getUsername(), false, null, request); return false; @@ -408,9 +408,9 @@ && isBlocked(((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).g } log.warn( - "Authentication finally failed for {} from {}", - authCredenetials == null ? null : authCredenetials.getUsername(), - remoteAddress + "Authentication finally failed for {} from {}", + authCredenetials == null ? null : authCredenetials.getUsername(), + remoteAddress ); auditLog.logFailedLogin(authCredenetials == null ? null : authCredenetials.getUsername(), false, null, request); @@ -425,11 +425,11 @@ && isBlocked(((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).g private void notifyIpAuthFailureListeners(RestRequest request, AuthCredentials authCredentials) { notifyIpAuthFailureListeners( - (request.getHttpChannel().getRemoteAddress() instanceof InetSocketAddress) - ? ((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).getAddress() - : null, - authCredentials, - request + (request.getHttpChannel().getRemoteAddress() instanceof InetSocketAddress) + ? ((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).getAddress() + : null, + authCredentials, + request ); } @@ -445,10 +445,10 @@ private void notifyIpAuthFailureListeners(InetAddress remoteAddress, AuthCredent * @return null if user cannot b authenticated */ private User checkExistsAndAuthz( - final Cache cache, - final User user, - final AuthenticationBackend authenticationBackend, - final Set authorizers + final Cache cache, + final User user, + final AuthenticationBackend authenticationBackend, + final Set authorizers ) { if (user == null) { return null; @@ -463,9 +463,9 @@ private User checkExistsAndAuthz( public User call() throws Exception { if (isTraceEnabled) { log.trace( - "Credentials for user {} not cached, return from {} backend directly", - user.getName(), - authenticationBackend.getType() + "Credentials for user {} not cached, return from {} backend directly", + user.getName(), + authenticationBackend.getType() ); } if (authenticationBackend.exists(user)) { @@ -512,9 +512,9 @@ private void authz(User authenticatedUser, Cache> roleCache, f try { if (isTraceEnabled) { log.trace( - "Backend roles for {} not cached, return from {} backend directly", - authenticatedUser.getName(), - ab.getType() + "Backend roles for {} not cached, return from {} backend directly", + authenticatedUser.getName(), + ab.getType() ); } ab.fillRoles(authenticatedUser, new AuthCredentials(authenticatedUser.getName())); @@ -534,11 +534,11 @@ private void authz(User authenticatedUser, Cache> roleCache, f * @return null if user cannot b authenticated */ private User authcz( - final Cache cache, - Cache> roleCache, - final AuthCredentials ac, - final AuthenticationBackend authBackend, - final Set authorizers + final Cache cache, + Cache> roleCache, + final AuthCredentials ac, + final AuthenticationBackend authBackend, + final Set authorizers ) { if (ac == null) { return null; @@ -557,9 +557,9 @@ private User authcz( public User call() throws Exception { if (log.isTraceEnabled()) { log.trace( - "Credentials for user {} not cached, return from {} backend directly", - ac.getUsername(), - authBackend.getType() + "Credentials for user {} not cached, return from {} backend directly", + ac.getUsername(), + authBackend.getType() ); } final User authenticatedUser = authBackend.authenticate(ac); @@ -591,15 +591,15 @@ private User impersonate(final RestRequest request, final User originalUser) thr if (adminDns.isAdminDN(impersonatedUserHeader)) { throw new OpenSearchSecurityException( - "It is not allowed to impersonate as an adminuser '" + impersonatedUserHeader + "'", - RestStatus.FORBIDDEN + "It is not allowed to impersonate as an adminuser '" + impersonatedUserHeader + "'", + RestStatus.FORBIDDEN ); } if (!adminDns.isRestImpersonationAllowed(originalUser.getName(), impersonatedUserHeader)) { throw new OpenSearchSecurityException( - "'" + originalUser.getName() + "' is not allowed to impersonate as '" + impersonatedUserHeader + "'", - RestStatus.FORBIDDEN + "'" + originalUser.getName() + "' is not allowed to impersonate as '" + impersonatedUserHeader + "'", + RestStatus.FORBIDDEN ); } else { final boolean isDebugEnabled = log.isDebugEnabled(); @@ -607,27 +607,27 @@ private User impersonate(final RestRequest request, final User originalUser) thr for (final AuthDomain authDomain : restAuthDomains) { final AuthenticationBackend authenticationBackend = authDomain.getBackend(); final User impersonatedUser = checkExistsAndAuthz( - restImpersonationCache, - new User(impersonatedUserHeader), - authenticationBackend, - restAuthorizers + restImpersonationCache, + new User(impersonatedUserHeader), + authenticationBackend, + restAuthorizers ); if (impersonatedUser == null) { log.debug( - "Unable to impersonate rest user from '{}' to '{}' because the impersonated user does not exists in {}, try next ...", - originalUser.getName(), - impersonatedUserHeader, - authenticationBackend.getType() + "Unable to impersonate rest user from '{}' to '{}' because the impersonated user does not exists in {}, try next ...", + originalUser.getName(), + impersonatedUserHeader, + authenticationBackend.getType() ); continue; } if (isDebugEnabled) { log.debug( - "Impersonate rest user from '{}' to '{}'", - originalUser.toStringWithAttributes(), - impersonatedUser.toStringWithAttributes() + "Impersonate rest user from '{}' to '{}'", + originalUser.toStringWithAttributes(), + impersonatedUser.toStringWithAttributes() ); } @@ -636,9 +636,9 @@ private User impersonate(final RestRequest request, final User originalUser) thr } log.debug( - "Unable to impersonate rest user from '{}' to '{}' because the impersonated user does not exists", - originalUser.getName(), - impersonatedUserHeader + "Unable to impersonate rest user from '{}' to '{}' because the impersonated user does not exists", + originalUser.getName(), + impersonatedUserHeader ); throw new OpenSearchSecurityException("No such user:" + impersonatedUserHeader, RestStatus.FORBIDDEN); } 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 54c11f40cf..5886590149 100644 --- a/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java +++ b/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java @@ -177,12 +177,12 @@ public String createJwt(String issuer, String subject, String audience, Integer if (logger.isDebugEnabled()) { logger.debug( - "Created JWT: " - + encodedJwt - + "\n" - + jsonMapReaderWriter.toJson(jwt.getJwsHeaders()) - + "\n" - + JwtUtils.claimsToJson(jwt.getClaims()) + "Created JWT: " + + encodedJwt + + "\n" + + jsonMapReaderWriter.toJson(jwt.getJwsHeaders()) + + "\n" + + JwtUtils.claimsToJson(jwt.getClaims()) ); } diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/InternalUsersApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/InternalUsersApiAction.java index 3e041961b0..a068d192a6 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/InternalUsersApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/InternalUsersApiAction.java @@ -13,6 +13,7 @@ import java.io.IOException; import java.nio.file.Path; +import java.security.NoSuchAlgorithmException; import java.util.List; import com.fasterxml.jackson.databind.JsonNode; @@ -158,6 +159,8 @@ protected void handlePut(RestChannel channel, final RestRequest request, final C return; } catch (IOException ex) { throw new IOException(ex); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); } // for existing users, hash is optional diff --git a/src/main/java/org/opensearch/security/dlic/rest/support/Utils.java b/src/main/java/org/opensearch/security/dlic/rest/support/Utils.java index dc0a755bc5..9ba6694740 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/support/Utils.java +++ b/src/main/java/org/opensearch/security/dlic/rest/support/Utils.java @@ -220,8 +220,7 @@ public static String hash(final char[] clearTextPassword) { public static String universalHash(String password) throws NoSuchAlgorithmException { MessageDigest digest = MessageDigest.getInstance("SHA-256"); - byte[] hash = digest.digest( - password.getBytes(StandardCharsets.UTF_8)); + byte[] hash = digest.digest(password.getBytes(StandardCharsets.UTF_8)); return new String(Hex.encode(hash)); } diff --git a/src/main/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticator.java b/src/main/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticator.java index 5a4d8e567f..c980956fb8 100644 --- a/src/main/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticator.java @@ -247,10 +247,10 @@ protected String extractSubject(final Claims claims, final RestRequest request) // We expect a String. If we find something else, convert to String but issue a warning if (!(subjectObject instanceof String)) { log.warn( - "Expected type String in the JWT for subject_key {}, but value was '{}' ({}). Will convert this value to String.", - subjectKey, - subjectObject, - subjectObject.getClass() + "Expected type String in the JWT for subject_key {}, but value was '{}' ({}). Will convert this value to String.", + subjectKey, + subjectObject, + subjectObject.getClass() ); } subject = String.valueOf(subjectObject); @@ -259,7 +259,7 @@ protected String extractSubject(final Claims claims, final RestRequest request) } private static PublicKey getPublicKey(final byte[] keyBytes, final String algo) throws NoSuchAlgorithmException, - InvalidKeySpecException { + InvalidKeySpecException { X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes); KeyFactory kf = KeyFactory.getInstance(algo); return kf.generatePublic(spec); diff --git a/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java b/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java index 00fb63cd6d..6d83859872 100644 --- a/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java +++ b/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java @@ -11,6 +11,7 @@ package org.opensearch.security.identity; +import java.util.Collections; import org.opensearch.client.Client; import org.opensearch.cluster.service.ClusterService; import org.opensearch.common.inject.Inject; @@ -20,6 +21,9 @@ import org.opensearch.identity.tokens.BearerAuthToken; import org.opensearch.identity.tokens.TokenManager; import org.opensearch.security.configuration.ConfigurationRepository; +import org.opensearch.security.securityconf.DynamicConfigFactory; +import org.opensearch.security.securityconf.impl.CType; +import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; import org.opensearch.security.user.InternalUserTokenHandler; import org.opensearch.security.user.UserService; import org.opensearch.security.user.UserServiceException; @@ -47,39 +51,39 @@ public class SecurityTokenManager implements TokenManager { public final String TOKEN_NOT_SUPPORTED_MESSAGE = "The provided token type is not supported by the Security Plugin."; @Inject - public SecurityTokenManager(ThreadPool threadPool, ClusterService clusterService, ConfigurationRepository configurationRepository, Client client, Settings settings, UserService userService) { + public SecurityTokenManager( + ThreadPool threadPool, + ClusterService clusterService, + ConfigurationRepository configurationRepository, + Client client, + Settings settings, + UserService userService + ) { this.threadPool = threadPool; this.clusterService = clusterService; this.client = client; this.configurationRepository = configurationRepository; this.settings = settings; this.userService = userService; - userTokenHandler = new UserTokenHandler(threadPool, clusterService, configurationRepository, client); + userTokenHandler = new UserTokenHandler(threadPool, clusterService, configurationRepository, client); internalUserTokenHandler = new InternalUserTokenHandler(settings, userService); } @Override - public AuthToken issueToken() { - throw new UserServiceException("The Security Plugin does not support generic token creation. Please specify a token type and argument."); - } - - public AuthToken issueToken(String type, String account) throws Exception { + public AuthToken issueToken(String account) { AuthToken token; - switch (type) { - case "onBehalfOfToken": - token = userTokenHandler.issueToken(account); - break; - case "internalAuthToken": - token = internalUserTokenHandler.issueToken(account); - break; - default: throw new UserServiceException("The provided type " + type + " is not a valid token. Please specify either \"onBehalfOf\" or \"internalAuthToken\"."); + final SecurityDynamicConfiguration internalUsersConfiguration = load(UserService.getUserConfigName(), false); + if (internalUsersConfiguration.exists(account)) { + token = internalUserTokenHandler.issueToken(account); + } + else { + token = userTokenHandler.issueToken(account); } return token; } - @Override public boolean validateToken(AuthToken authToken) { if (authToken instanceof BearerAuthToken) { @@ -91,7 +95,6 @@ public boolean validateToken(AuthToken authToken) { throw new UserServiceException(TOKEN_NOT_SUPPORTED_MESSAGE); } - @Override public String getTokenInfo(AuthToken authToken) { if (authToken instanceof BearerAuthToken) { @@ -103,7 +106,6 @@ public String getTokenInfo(AuthToken authToken) { throw new UserServiceException(TOKEN_NOT_SUPPORTED_MESSAGE); } - @Override public void revokeToken(AuthToken authToken) { if (authToken instanceof BearerAuthToken) { userTokenHandler.revokeToken(authToken); @@ -116,17 +118,6 @@ public void revokeToken(AuthToken authToken) { throw new UserServiceException(TOKEN_NOT_SUPPORTED_MESSAGE); } - @Override - public void resetToken(AuthToken authToken) { - if (authToken instanceof BearerAuthToken) { - userTokenHandler.resetToken(authToken); - } - if (authToken instanceof BasicAuthToken) { - internalUserTokenHandler.resetToken(authToken); - } - throw new UserServiceException(TOKEN_NOT_SUPPORTED_MESSAGE); - } - /** * Only for testing */ @@ -140,4 +131,17 @@ public void setInternalUserTokenHandler(InternalUserTokenHandler handler) { public void setUserTokenHandler(UserTokenHandler handler) { this.userTokenHandler = handler; } + + /** + * Load data for a given CType + * @param config CType whose data is to be loaded in-memory + * @return configuration loaded with given CType data + */ + protected final SecurityDynamicConfiguration load(final CType config, boolean logComplianceEvent) { + SecurityDynamicConfiguration loaded = configurationRepository.getConfigurationsFromIndex( + Collections.singleton(config), + logComplianceEvent + ).get(config).deepClone(); + return DynamicConfigFactory.addStatics(loaded); + } } diff --git a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java index 8259f12dea..38a49501b4 100644 --- a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java +++ b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java @@ -191,12 +191,12 @@ public boolean isDnfofForEmptyResultsEnabled() { public List getIpAuthFailureListeners() { return Collections.unmodifiableList(ipAuthFailureListeners); } - + @Override public Settings getDynamicOnBehalfOfSettings() { return Settings.builder() - .put(Settings.builder().loadFromSource(config.dynamic.on_behalf_of.configAsJson(), XContentType.JSON).build()) - .build(); + .put(Settings.builder().loadFromSource(config.dynamic.on_behalf_of.configAsJson(), XContentType.JSON).build()) + .build(); } @Override diff --git a/src/main/java/org/opensearch/security/securityconf/impl/CType.java b/src/main/java/org/opensearch/security/securityconf/impl/CType.java index 89507a9c81..cc348012e5 100644 --- a/src/main/java/org/opensearch/security/securityconf/impl/CType.java +++ b/src/main/java/org/opensearch/security/securityconf/impl/CType.java @@ -47,6 +47,7 @@ import org.opensearch.security.securityconf.impl.v7.RoleMappingsV7; import org.opensearch.security.securityconf.impl.v7.RoleV7; import org.opensearch.security.securityconf.impl.v7.TenantV7; +import org.opensearch.identity.tokens.BearerAuthToken; public enum CType { @@ -59,7 +60,7 @@ public enum CType { NODESDN(toMap(1, NodesDn.class, 2, NodesDn.class)), WHITELIST(toMap(1, WhitelistingSettings.class, 2, WhitelistingSettings.class)), ALLOWLIST(toMap(1, AllowlistingSettings.class, 2, AllowlistingSettings.class)), - AUDIT(toMap(1, AuditConfig.class, 2, AuditConfig.class)); + AUDIT(toMap(1, AuditConfig.class, 2, AuditConfig.class)), REVOKEDTOKENS(toMap(1, BearerAuthToken.class)); private Map> implementations; diff --git a/src/main/java/org/opensearch/security/securityconf/impl/v6/ConfigV6.java b/src/main/java/org/opensearch/security/securityconf/impl/v6/ConfigV6.java index 2e2e1af15d..01375b1f97 100644 --- a/src/main/java/org/opensearch/security/securityconf/impl/v6/ConfigV6.java +++ b/src/main/java/org/opensearch/security/securityconf/impl/v6/ConfigV6.java @@ -75,16 +75,16 @@ public static class Dynamic { @Override public String toString() { return "Dynamic [filtered_alias_mode=" - + filtered_alias_mode - + ", kibana=" - + kibana - + ", http=" - + http - + ", authc=" - + authc - + ", authz=" - + authz - + "]"; + + filtered_alias_mode + + ", kibana=" + + kibana + + ", http=" + + http + + ", authc=" + + authc + + ", authz=" + + authz + + "]"; } } @@ -104,16 +104,16 @@ public static class Kibana { @Override public String toString() { return "Kibana [multitenancy_enabled=" - + multitenancy_enabled - + ", server_username=" - + server_username - + ", opendistro_role=" - + opendistro_role - + ", index=" - + index - + ", do_not_fail_on_forbidden=" - + do_not_fail_on_forbidden - + "]"; + + multitenancy_enabled + + ", server_username=" + + server_username + + ", opendistro_role=" + + opendistro_role + + ", index=" + + index + + ", do_not_fail_on_forbidden=" + + do_not_fail_on_forbidden + + "]"; } } @@ -172,13 +172,13 @@ public static class Xff { @JsonInclude(JsonInclude.Include.NON_NULL) public boolean enabled = true; public String internalProxies = Pattern.compile( - "10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|" - + "192\\.168\\.\\d{1,3}\\.\\d{1,3}|" - + "169\\.254\\.\\d{1,3}\\.\\d{1,3}|" - + "127\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|" - + "172\\.1[6-9]{1}\\.\\d{1,3}\\.\\d{1,3}|" - + "172\\.2[0-9]{1}\\.\\d{1,3}\\.\\d{1,3}|" - + "172\\.3[0-1]{1}\\.\\d{1,3}\\.\\d{1,3}" + "10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|" + + "192\\.168\\.\\d{1,3}\\.\\d{1,3}|" + + "169\\.254\\.\\d{1,3}\\.\\d{1,3}|" + + "127\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|" + + "172\\.1[6-9]{1}\\.\\d{1,3}\\.\\d{1,3}|" + + "172\\.2[0-9]{1}\\.\\d{1,3}\\.\\d{1,3}|" + + "172\\.3[0-1]{1}\\.\\d{1,3}\\.\\d{1,3}" ).toString(); public String remoteIpHeader = "X-Forwarded-For"; public String proxiesHeader = "X-Forwarded-By"; @@ -187,16 +187,16 @@ public static class Xff { @Override public String toString() { return "Xff [enabled=" - + enabled - + ", internalProxies=" - + internalProxies - + ", remoteIpHeader=" - + remoteIpHeader - + ", proxiesHeader=" - + proxiesHeader - + ", trustedProxies=" - + trustedProxies - + "]"; + + enabled + + ", internalProxies=" + + internalProxies + + ", remoteIpHeader=" + + remoteIpHeader + + ", proxiesHeader=" + + proxiesHeader + + ", trustedProxies=" + + trustedProxies + + "]"; } } @@ -237,18 +237,18 @@ public static class AuthcDomain { @Override public String toString() { return "AuthcDomain [http_enabled=" - + http_enabled - + ", transport_enabled=" - + transport_enabled - + ", enabled=" - + enabled - + ", order=" - + order - + ", http_authenticator=" - + http_authenticator - + ", authentication_backend=" - + authentication_backend - + "]"; + + http_enabled + + ", transport_enabled=" + + transport_enabled + + ", enabled=" + + enabled + + ", order=" + + order + + ", http_authenticator=" + + http_authenticator + + ", authentication_backend=" + + authentication_backend + + "]"; } } @@ -348,14 +348,14 @@ public static class AuthzDomain { @Override public String toString() { return "AuthzDomain [http_enabled=" - + http_enabled - + ", transport_enabled=" - + transport_enabled - + ", enabled=" - + enabled - + ", authorization_backend=" - + authorization_backend - + "]"; + + http_enabled + + ", transport_enabled=" + + transport_enabled + + ", enabled=" + + enabled + + ", authorization_backend=" + + authorization_backend + + "]"; } } 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 cd8251066d..9052c40cda 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 @@ -87,27 +87,27 @@ public ConfigV7(ConfigV6 c6) { dynamic.authc = new Authc(); dynamic.authc.domains.putAll( - c6.dynamic.authc.getDomains() - .entrySet() - .stream() - .collect(Collectors.toMap(entry -> entry.getKey(), entry -> new AuthcDomain(entry.getValue()))) + c6.dynamic.authc.getDomains() + .entrySet() + .stream() + .collect(Collectors.toMap(entry -> entry.getKey(), entry -> new AuthcDomain(entry.getValue()))) ); dynamic.authz = new Authz(); dynamic.authz.domains.putAll( - c6.dynamic.authz.getDomains() - .entrySet() - .stream() - .collect(Collectors.toMap(entry -> entry.getKey(), entry -> new AuthzDomain(entry.getValue()))) + c6.dynamic.authz.getDomains() + .entrySet() + .stream() + .collect(Collectors.toMap(entry -> entry.getKey(), entry -> new AuthzDomain(entry.getValue()))) ); dynamic.auth_failure_listeners = new AuthFailureListeners(); dynamic.auth_failure_listeners.listeners.putAll( - c6.dynamic.auth_failure_listeners.getListeners() - .entrySet() - .stream() - .collect(Collectors.toMap(entry -> entry.getKey(), entry -> new AuthFailureListener(entry.getValue()))) + c6.dynamic.auth_failure_listeners.getListeners() + .entrySet() + .stream() + .collect(Collectors.toMap(entry -> entry.getKey(), entry -> new AuthFailureListener(entry.getValue()))) ); } @@ -139,16 +139,16 @@ public static class Dynamic { @Override public String toString() { return "Dynamic [filtered_alias_mode=" - + filtered_alias_mode - + ", kibana=" - + kibana - + ", http=" - + http - + ", authc=" - + authc - + ", authz=" - + authz - + "]"; + + filtered_alias_mode + + ", kibana=" + + kibana + + ", http=" + + http + + ", authc=" + + authc + + ", authz=" + + authz + + "]"; } } @@ -167,18 +167,18 @@ public static class Kibana { @Override public String toString() { return "Kibana [multitenancy_enabled=" - + multitenancy_enabled - + ", private_tenant_enabled=" - + private_tenant_enabled - + ", default_tenant=" - + default_tenant - + ", server_username=" - + server_username - + ", opendistro_role=" - + opendistro_role - + ", index=" - + index - + "]"; + + multitenancy_enabled + + ", private_tenant_enabled=" + + private_tenant_enabled + + ", default_tenant=" + + default_tenant + + ", server_username=" + + server_username + + ", opendistro_role=" + + opendistro_role + + ", index=" + + index + + "]"; } } @@ -247,13 +247,13 @@ public String asJson() { public static class Xff { public boolean enabled = false; public String internalProxies = Pattern.compile( - "10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|" - + "192\\.168\\.\\d{1,3}\\.\\d{1,3}|" - + "169\\.254\\.\\d{1,3}\\.\\d{1,3}|" - + "127\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|" - + "172\\.1[6-9]{1}\\.\\d{1,3}\\.\\d{1,3}|" - + "172\\.2[0-9]{1}\\.\\d{1,3}\\.\\d{1,3}|" - + "172\\.3[0-1]{1}\\.\\d{1,3}\\.\\d{1,3}" + "10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|" + + "192\\.168\\.\\d{1,3}\\.\\d{1,3}|" + + "169\\.254\\.\\d{1,3}\\.\\d{1,3}|" + + "127\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|" + + "172\\.1[6-9]{1}\\.\\d{1,3}\\.\\d{1,3}|" + + "172\\.2[0-9]{1}\\.\\d{1,3}\\.\\d{1,3}|" + + "172\\.3[0-1]{1}\\.\\d{1,3}\\.\\d{1,3}" ).toString(); public String remoteIpHeader = "X-Forwarded-For"; @@ -319,18 +319,18 @@ public AuthcDomain(ConfigV6.AuthcDomain v6) { @Override public String toString() { return "AuthcDomain [http_enabled=" - + http_enabled - + ", transport_enabled=" - + transport_enabled - + ", order=" - + order - + ", http_authenticator=" - + http_authenticator - + ", authentication_backend=" - + authentication_backend - + ", description=" - + description - + "]"; + + http_enabled + + ", transport_enabled=" + + transport_enabled + + ", order=" + + order + + ", http_authenticator=" + + http_authenticator + + ", authentication_backend=" + + authentication_backend + + ", description=" + + description + + "]"; } } @@ -468,14 +468,14 @@ public AuthzDomain(ConfigV6.AuthzDomain v6) { @Override public String toString() { return "AuthzDomain [http_enabled=" - + http_enabled - + ", transport_enabled=" - + transport_enabled - + ", authorization_backend=" - + authorization_backend - + ", description=" - + description - + "]"; + + http_enabled + + ", transport_enabled=" + + transport_enabled + + ", authorization_backend=" + + authorization_backend + + ", description=" + + description + + "]"; } } @@ -518,4 +518,3 @@ public String toString() { } } - diff --git a/src/main/java/org/opensearch/security/securityconf/impl/v7/InternalUserV7.java b/src/main/java/org/opensearch/security/securityconf/impl/v7/InternalUserV7.java index 8f10df04a4..f8f05ea0e4 100644 --- a/src/main/java/org/opensearch/security/securityconf/impl/v7/InternalUserV7.java +++ b/src/main/java/org/opensearch/security/securityconf/impl/v7/InternalUserV7.java @@ -64,7 +64,7 @@ private InternalUserV7(String hash, boolean reserved, boolean hidden, List internalUsersConfiguration; - @Inject public InternalUserTokenHandler(final Settings settings, UserService userService) { this.settings = settings; @@ -41,14 +40,16 @@ public InternalUserTokenHandler(final Settings settings, UserService userService } public AuthToken issueToken() { - throw new UserServiceException("The InternalUserTokenHandler is unable to issue generic auth tokens. Please specify a valid internal user."); + throw new UserServiceException( + "The InternalUserTokenHandler is unable to issue generic auth tokens. Please specify a valid internal user." + ); } public AuthToken issueToken(String internalUser) { String tokenAsString; try { tokenAsString = this.userService.generateAuthToken(internalUser); - } catch (IOException | UserServiceException ex){ + } catch (IOException | UserServiceException ex) { throw new UserServiceException("Failed to generate an auth token for " + internalUser); } return new BasicAuthToken(tokenAsString); @@ -58,7 +59,7 @@ public boolean validateToken(AuthToken token) { if (!(token instanceof BasicAuthToken)) { throw new UserServiceException("The provided auth token is of an incorrect type. Please provide a BasicAuthToken object."); } - BasicAuthToken basicToken = (BasicAuthToken) token; + BasicAuthToken basicToken = (BasicAuthToken) token; String accountName = basicToken.getUser(); String password = basicToken.getPassword(); String hash; @@ -67,7 +68,8 @@ public boolean validateToken(AuthToken token) { } catch (NoSuchAlgorithmException e) { throw new UserServiceException("The provided token could not be validated."); } - return (internalUsersConfiguration.exists(accountName) && hash.equals(((Hashed) internalUsersConfiguration.getCEntry(accountName)).getHash())); + return (internalUsersConfiguration.exists(accountName) + && hash.equals(((Hashed) internalUsersConfiguration.getCEntry(accountName)).getHash())); } public String getTokenInfo(AuthToken token) { @@ -80,7 +82,7 @@ public String getTokenInfo(AuthToken token) { public void revokeToken(AuthToken token) { if (validateToken(token)) { - BasicAuthToken basicToken = (BasicAuthToken) token; + BasicAuthToken basicToken = (BasicAuthToken) token; String accountName = basicToken.getUser(); try { userService.clearHash(accountName); @@ -96,4 +98,3 @@ public void resetToken(AuthToken token) { throw new UserServiceException("The InternalUserTokenHandler is unable to reset auth tokens."); } } - diff --git a/src/main/java/org/opensearch/security/user/UserService.java b/src/main/java/org/opensearch/security/user/UserService.java index 7f55f21efd..6aceaca5cd 100644 --- a/src/main/java/org/opensearch/security/user/UserService.java +++ b/src/main/java/org/opensearch/security/user/UserService.java @@ -61,9 +61,11 @@ public class UserService { final static String NO_PASSWORD_OR_HASH_MESSAGE = "Please specify either 'hash' or 'password' when creating a new internal user."; final static String RESTRICTED_CHARACTER_USE_MESSAGE = "A restricted character(s) was detected in the account name. Please remove: "; - final static String SERVICE_ACCOUNT_PASSWORD_MESSAGE = "A password cannot be provided for a service account. Failed to register service account: "; + final static String SERVICE_ACCOUNT_PASSWORD_MESSAGE = + "A password cannot be provided for a service account. Failed to register service account: "; - final static String SERVICE_ACCOUNT_HASH_MESSAGE = "A password hash cannot be provided for service account. Failed to register service account: "; + final static String SERVICE_ACCOUNT_HASH_MESSAGE = + "A password hash cannot be provided for service account. Failed to register service account: "; final static String NO_ACCOUNT_NAME_MESSAGE = "No account name was specified in the request."; @@ -72,25 +74,22 @@ public class UserService { final static String FAILED_CLEAR_HASH_MESSAGE = "The hash could not be cleared from the specified account."; - private static CType getUserConfigName() { + public static CType getUserConfigName() { return CType.INTERNALUSERS; } static final List RESTRICTED_FROM_USERNAME = ImmutableList.of( - ":" // Not allowed in basic auth, see https://stackoverflow.com/a/33391003/533057 + ":" // Not allowed in basic auth, see https://stackoverflow.com/a/33391003/533057 ); @Inject - public UserService( - ClusterService clusterService, - ConfigurationRepository configurationRepository, - Settings settings, - Client client - ) { + public UserService(ClusterService clusterService, ConfigurationRepository configurationRepository, Settings settings, Client client) { this.clusterService = clusterService; this.configurationRepository = configurationRepository; - this.securityIndex = settings.get(ConfigConstants.SECURITY_CONFIG_INDEX_NAME, - ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX); + this.securityIndex = settings.get( + ConfigConstants.SECURITY_CONFIG_INDEX_NAME, + ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX + ); this.client = client; } @@ -100,7 +99,10 @@ public UserService( * @return configuration loaded with given CType data */ protected final SecurityDynamicConfiguration load(final CType config, boolean logComplianceEvent) { - SecurityDynamicConfiguration loaded = configurationRepository.getConfigurationsFromIndex(Collections.singleton(config), logComplianceEvent).get(config).deepClone(); + SecurityDynamicConfiguration loaded = configurationRepository.getConfigurationsFromIndex( + Collections.singleton(config), + logComplianceEvent + ).get(config).deepClone(); return DynamicConfigFactory.addStatics(loaded); } @@ -123,18 +125,20 @@ public SecurityDynamicConfiguration createOrUpdateAccount(ObjectNode contentA SecurityJsonNode attributeNode = securityJsonNode.get("attributes"); - if (!attributeNode.get("service").isNull() && Objects.requireNonNull(attributeNode.get("service").asString()).equalsIgnoreCase("true")) - { // If this is a service account + if (!attributeNode.get("service").isNull() + && Objects.requireNonNull(attributeNode.get("service").asString()).equalsIgnoreCase("true")) { // If this is a service account verifyServiceAccount(securityJsonNode, accountName); String password = generatePassword(); contentAsNode.put("hash", universalHash(password)); contentAsNode.put("service", "true"); - } else{ + } else { contentAsNode.put("service", "false"); } securityJsonNode = new SecurityJsonNode(contentAsNode); - final List foundRestrictedContents = RESTRICTED_FROM_USERNAME.stream().filter(accountName::contains).collect(Collectors.toList()); + final List foundRestrictedContents = RESTRICTED_FROM_USERNAME.stream() + .filter(accountName::contains) + .collect(Collectors.toList()); if (!foundRestrictedContents.isEmpty()) { final String restrictedContents = foundRestrictedContents.stream().map(s -> "'" + s + "'").collect(Collectors.joining(",")); throw new UserServiceException(RESTRICTED_CHARACTER_USE_MESSAGE + restrictedContents); @@ -168,7 +172,9 @@ public SecurityDynamicConfiguration createOrUpdateAccount(ObjectNode contentA // sanity check, this should usually not happen final String hash = ((Hashed) internalUsersConfiguration.getCEntry(accountName)).getHash(); if (hash == null || hash.length() == 0) { - throw new UserServiceException("Existing user " + accountName + " has no password, and no new password or hash was specified."); + throw new UserServiceException( + "Existing user " + accountName + " has no password, and no new password or hash was specified." + ); } contentAsNode.put("hash", hash); } @@ -176,13 +182,15 @@ public SecurityDynamicConfiguration createOrUpdateAccount(ObjectNode contentA internalUsersConfiguration.remove(accountName); contentAsNode.remove("name"); - internalUsersConfiguration.putCObject(accountName, DefaultObjectMapper.readTree(contentAsNode, internalUsersConfiguration.getImplementingClass())); + internalUsersConfiguration.putCObject( + accountName, + DefaultObjectMapper.readTree(contentAsNode, internalUsersConfiguration.getImplementingClass()) + ); return internalUsersConfiguration; } private void verifyServiceAccount(SecurityJsonNode securityJsonNode, String accountName) { - final String plainTextPassword = securityJsonNode.get("password").asString(); final String origHash = securityJsonNode.get("hash").asString(); @@ -226,15 +234,14 @@ public String generateAuthToken(String accountName) throws IOException { SecurityJsonNode securityJsonNode = new SecurityJsonNode(contentAsNode); Optional.of(securityJsonNode.get("service")) - .map(SecurityJsonNode::asString) - .filter("true"::equalsIgnoreCase) - .orElseThrow(() -> new UserServiceException(AUTH_TOKEN_GENERATION_MESSAGE)); - + .map(SecurityJsonNode::asString) + .filter("true"::equalsIgnoreCase) + .orElseThrow(() -> new UserServiceException(AUTH_TOKEN_GENERATION_MESSAGE)); Optional.of(securityJsonNode.get("enabled")) - .map(SecurityJsonNode::asString) - .filter("true"::equalsIgnoreCase) - .orElseThrow(() -> new UserServiceException(AUTH_TOKEN_GENERATION_MESSAGE)); + .map(SecurityJsonNode::asString) + .filter("true"::equalsIgnoreCase) + .orElseThrow(() -> new UserServiceException(AUTH_TOKEN_GENERATION_MESSAGE)); // Generate a new password for the account and store the hash of it String plainTextPassword = generatePassword(); @@ -245,10 +252,12 @@ public String generateAuthToken(String accountName) throws IOException { // Update the internal user associated with the auth token internalUsersConfiguration.remove(accountName); contentAsNode.remove("name"); - internalUsersConfiguration.putCObject(accountName, DefaultObjectMapper.readTree(contentAsNode, internalUsersConfiguration.getImplementingClass())); + internalUsersConfiguration.putCObject( + accountName, + DefaultObjectMapper.readTree(contentAsNode, internalUsersConfiguration.getImplementingClass()) + ); saveAndUpdateConfigs(getUserConfigName().toString(), client, CType.INTERNALUSERS, internalUsersConfiguration); - authToken = Base64.getUrlEncoder().encodeToString((accountName + ":" + plainTextPassword).getBytes(StandardCharsets.UTF_8)); return authToken; @@ -271,34 +280,43 @@ public void clearHash(String accountName) throws IOException { SecurityJsonNode securityJsonNode = new SecurityJsonNode(contentAsNode); Optional.of(securityJsonNode.get("service")) - .map(SecurityJsonNode::asString) - .filter("true"::equalsIgnoreCase) - .orElseThrow(() -> new UserServiceException(FAILED_CLEAR_HASH_MESSAGE)); - + .map(SecurityJsonNode::asString) + .filter("true"::equalsIgnoreCase) + .orElseThrow(() -> new UserServiceException(FAILED_CLEAR_HASH_MESSAGE)); Optional.of(securityJsonNode.get("enabled")) - .map(SecurityJsonNode::asString) - .filter("true"::equalsIgnoreCase) - .orElseThrow(() -> new UserServiceException(FAILED_CLEAR_HASH_MESSAGE)); + .map(SecurityJsonNode::asString) + .filter("true"::equalsIgnoreCase) + .orElseThrow(() -> new UserServiceException(FAILED_CLEAR_HASH_MESSAGE)); contentAsNode.remove("hash"); contentAsNode.remove("name"); - internalUsersConfiguration.putCObject(accountName, DefaultObjectMapper.readTree(contentAsNode, internalUsersConfiguration.getImplementingClass())); + internalUsersConfiguration.putCObject( + accountName, + DefaultObjectMapper.readTree(contentAsNode, internalUsersConfiguration.getImplementingClass()) + ); saveAndUpdateConfigs(getUserConfigName().toString(), client, CType.INTERNALUSERS, internalUsersConfiguration); } - public void saveAndUpdateConfigs(final String indexName, final Client client, final CType cType, final SecurityDynamicConfiguration configuration) { + public void saveAndUpdateConfigs( + final String indexName, + final Client client, + final CType cType, + final SecurityDynamicConfiguration configuration + ) { final IndexRequest ir = new IndexRequest(indexName); final String id = cType.toLCString(); configuration.removeStatic(); try { - client.index(ir.id(id) + client.index( + ir.id(id) .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) .setIfSeqNo(configuration.getSeqNo()) .setIfPrimaryTerm(configuration.getPrimaryTerm()) - .source(id, XContentHelper.toXContent(configuration, XContentType.JSON, false))); + .source(id, XContentHelper.toXContent(configuration, XContentType.JSON, false)) + ); } catch (IOException e) { throw ExceptionsHelper.convertToOpenSearchException(e); } diff --git a/src/main/java/org/opensearch/security/user/UserTokenHandler.java b/src/main/java/org/opensearch/security/user/UserTokenHandler.java index 8febf95d62..5fff7998b4 100644 --- a/src/main/java/org/opensearch/security/user/UserTokenHandler.java +++ b/src/main/java/org/opensearch/security/user/UserTokenHandler.java @@ -66,28 +66,38 @@ public static CType getRevokedTokensConfigName() { } @Inject - public UserTokenHandler(ThreadPool threadPool, ClusterService clusterService, ConfigurationRepository configurationRepository, Client client) { + public UserTokenHandler( + ThreadPool threadPool, + ClusterService clusterService, + ConfigurationRepository configurationRepository, + Client client + ) { this.settings = Settings.builder().put("signing_key", signingKey).put("encryption_key", claimsEncryptionKey).build(); - this.jwtVendor = new JwtVendor(settings, () -> System.nanoTime() / 1000 + (DEFAULT_EXPIRATION_TIME_SECONDS * 1000)); + this.jwtVendor = new JwtVendor(settings, () -> System.nanoTime() / 1000 + (DEFAULT_EXPIRATION_TIME_SECONDS * 1000)); this.threadPool = threadPool; this.clusterService = clusterService; this.client = client; this.configurationRepository = configurationRepository; } - @Override - public AuthToken issueToken() { - throw new UserServiceException("The UserTokenHandler is unable to issue generic auth tokens. Please specify a valid subject."); - } - - public AuthToken issueToken(String audience) throws Exception { + public AuthToken issueToken(String audience) { ThreadContext threadContext = threadPool.getThreadContext(); final User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); - String jwt = jwtVendor.createJwt(clusterService.getClusterName().toString(), user.getName(), audience, DEFAULT_EXPIRATION_TIME_SECONDS, new ArrayList(user.getRoles())); + String jwt = null; + try { + jwt = jwtVendor.createJwt( + clusterService.getClusterName().toString(), + user.getName(), + audience, + DEFAULT_EXPIRATION_TIME_SECONDS, + new ArrayList(user.getRoles()) + ); + } catch (Exception e) { + throw new RuntimeException(e); + } return new BearerAuthToken(jwt); } - @Override public boolean validateToken(AuthToken authToken) { if (!(authToken instanceof BearerAuthToken)) { throw new UserServiceException("The provided token is not a BearerAuthToken."); @@ -99,15 +109,10 @@ public boolean validateToken(AuthToken authToken) { Long iat = (Long) jwt.getClaim("iat"); Long exp = (Long) jwt.getClaim("exp"); SecurityDynamicConfiguration revokedTokens = load(getRevokedTokensConfigName(), false); - System.out.println("IAT is : " + iat); - System.out.println("EXP is : " + exp); - System.out.println("Current time is : " + System.currentTimeMillis()); - System.out.println("Exists in revoked is: " + revokedTokens.exists(bearerAuthToken.getCompleteToken())); Long currentTime = System.currentTimeMillis(); return (exp > currentTime && !revokedTokens.exists(bearerAuthToken.getCompleteToken())); } - @Override public String getTokenInfo(AuthToken authToken) { if (!(authToken instanceof BearerAuthToken)) { throw new UserServiceException("The provided token is not a BearerAuthToken."); @@ -116,7 +121,6 @@ public String getTokenInfo(AuthToken authToken) { return "The provided token is a BearerAuthToken with content: " + bearerAuthToken; } - @Override public void revokeToken(AuthToken authToken) { if (!(authToken instanceof BearerAuthToken)) { throw new UserServiceException("The provided token is not a BearerAuthToken."); @@ -127,10 +131,6 @@ public void revokeToken(AuthToken authToken) { saveAndUpdateConfigs(getRevokedTokensConfigName().toString(), client, CType.REVOKEDTOKENS, revokedTokens); } - @Override - public void resetToken(AuthToken authToken) { - throw new UserServiceException("The UserTokenHandler does not support the reset operation. Please issue a new token instead."); - } /** * Load data for a given CType @@ -138,22 +138,32 @@ public void resetToken(AuthToken authToken) { * @return configuration loaded with given CType data */ public SecurityDynamicConfiguration load(final CType config, boolean logComplianceEvent) { - SecurityDynamicConfiguration loaded = configurationRepository.getConfigurationsFromIndex(Collections.singleton(config), logComplianceEvent).get(config).deepClone(); + SecurityDynamicConfiguration loaded = configurationRepository.getConfigurationsFromIndex( + Collections.singleton(config), + logComplianceEvent + ).get(config).deepClone(); return DynamicConfigFactory.addStatics(loaded); } - public void saveAndUpdateConfigs(final String indexName, final Client client, final CType cType, final SecurityDynamicConfiguration configuration) { + public void saveAndUpdateConfigs( + final String indexName, + final Client client, + final CType cType, + final SecurityDynamicConfiguration configuration + ) { final IndexRequest ir = new IndexRequest(indexName); final String id = cType.toLCString(); configuration.removeStatic(); try { - client.index(ir.id(id) + client.index( + ir.id(id) .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) .setIfSeqNo(configuration.getSeqNo()) .setIfPrimaryTerm(configuration.getPrimaryTerm()) - .source(id, XContentHelper.toXContent(configuration, XContentType.JSON, false))); + .source(id, XContentHelper.toXContent(configuration, XContentType.JSON, false)) + ); } catch (IOException e) { throw ExceptionsHelper.convertToOpenSearchException(e); } diff --git a/src/test/java/org/opensearch/security/auth/InternalUserTokenHandlerTests.java b/src/test/java/org/opensearch/security/auth/InternalUserTokenHandlerTests.java index 347d3ab3d3..c1990dc89e 100644 --- a/src/test/java/org/opensearch/security/auth/InternalUserTokenHandlerTests.java +++ b/src/test/java/org/opensearch/security/auth/InternalUserTokenHandlerTests.java @@ -36,8 +36,7 @@ import static org.opensearch.security.dlic.rest.support.Utils.universalHash; import static org.opensearch.test.OpenSearchTestCase.assertEquals; - -public class InternalUserTokenHandlerTests { +public class InternalUserTokenHandlerTests { private InternalUserTokenHandler internalUserTokenHandler; private UserService userService; private SecurityDynamicConfiguration internalUsersConfiguration; diff --git a/src/test/java/org/opensearch/security/auth/SecurityTokenManagerTests.java b/src/test/java/org/opensearch/security/auth/SecurityTokenManagerTests.java index 83b98a1bbf..740ca04bbb 100644 --- a/src/test/java/org/opensearch/security/auth/SecurityTokenManagerTests.java +++ b/src/test/java/org/opensearch/security/auth/SecurityTokenManagerTests.java @@ -21,9 +21,10 @@ import org.opensearch.identity.tokens.AuthToken; import org.opensearch.identity.tokens.BasicAuthToken; import org.opensearch.identity.tokens.BearerAuthToken; -import org.opensearch.identity.tokens.NoopToken; import org.opensearch.security.configuration.ConfigurationRepository; import org.opensearch.security.identity.SecurityTokenManager; +import org.opensearch.security.securityconf.impl.CType; +import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; import org.opensearch.security.user.InternalUserTokenHandler; import org.opensearch.security.user.UserService; import org.opensearch.security.user.UserServiceException; @@ -44,14 +45,13 @@ public class SecurityTokenManagerTests { - SecurityTokenManager securityTokenManager; private UserTokenHandler userTokenHandler; private InternalUserTokenHandler internalUserTokenHandler; private ClusterService clusterService; UserService userService; - + Map internalUserMap; @Before public void setup() { @@ -62,7 +62,9 @@ public void setup() { ConfigurationRepository configurationRepository = mock(ConfigurationRepository.class); clusterService = mock(ClusterService.class); userService = mock(UserService.class); - securityTokenManager = spy(new SecurityTokenManager(threadPool, clusterService, configurationRepository, client, settings, userService)); + securityTokenManager = spy( + new SecurityTokenManager(threadPool, clusterService, configurationRepository, client, settings, userService) + ); userTokenHandler = mock(UserTokenHandler.class); internalUserTokenHandler = mock(InternalUserTokenHandler.class); securityTokenManager.setInternalUserTokenHandler(internalUserTokenHandler); @@ -70,10 +72,10 @@ public void setup() { } @Test - public void testIssueTokenShouldPass() throws Exception { + public void testIssueTokenShouldPass() { doReturn(new BearerAuthToken("header.payload.signature")).when(userTokenHandler).issueToken("test"); - AuthToken createdBearerToken = securityTokenManager.issueToken("onBehalfOfToken", "test"); - assert(createdBearerToken instanceof BearerAuthToken); + AuthToken createdBearerToken = securityTokenManager.issueToken("test"); + assert (createdBearerToken instanceof BearerAuthToken); BearerAuthToken bearerAuthToken = (BearerAuthToken) createdBearerToken; String header = bearerAuthToken.getHeader(); String payload = bearerAuthToken.getPayload(); @@ -83,8 +85,8 @@ public void testIssueTokenShouldPass() throws Exception { assertEquals(signature, "signature"); doReturn(new BasicAuthToken("Basic dGVzdDp0ZTpzdA==")).when(internalUserTokenHandler).issueToken("test"); - AuthToken createdBasicToken = securityTokenManager.issueToken("internalAuthToken", "test"); - assert(createdBasicToken instanceof BasicAuthToken); + AuthToken createdBasicToken = securityTokenManager.issueToken("test"); + assert (createdBasicToken instanceof BasicAuthToken); BasicAuthToken basicAuthToken = (BasicAuthToken) createdBasicToken; String accountName = basicAuthToken.getUser(); String password = basicAuthToken.getPassword(); @@ -92,15 +94,6 @@ public void testIssueTokenShouldPass() throws Exception { assertEquals(password, "te:st"); } - @Test - public void testIssueTokenShouldThrow() throws Exception { - Exception exception1 = assertThrows(UserServiceException.class, () -> securityTokenManager.issueToken()); - assert(exception1.getMessage().contains("The Security Plugin does not support generic token creation. Please specify a token type and argument.")); - - Exception exception2 = assertThrows(UserServiceException.class, () -> securityTokenManager.issueToken("notAToken", "test")); - assert(exception2.getMessage().contains("The provided type notAToken is not a valid token. Please specify either \"onBehalfOf\" or \"internalAuthToken\".")); - } - @Test public void testValidateTokenShouldPass() { BearerAuthToken bearerAuthToken = new BearerAuthToken("header.payload.signature"); @@ -127,8 +120,13 @@ public void testValidateTokenShouldFail() { @Test public void testValidateTokenShouldThrow() { - Exception exception = assertThrows(UserServiceException.class, () -> securityTokenManager.validateToken(new NoopToken())); - assert(exception.getMessage().contains(securityTokenManager.TOKEN_NOT_SUPPORTED_MESSAGE)); + Exception exception = assertThrows(UserServiceException.class, () -> securityTokenManager.validateToken(new AuthToken() { + @Override + public int hashCode() { + return super.hashCode(); + } + })); + assert (exception.getMessage().contains(securityTokenManager.TOKEN_NOT_SUPPORTED_MESSAGE)); } @Test @@ -145,24 +143,28 @@ public void testGetTokenInfoShouldPass() { @Test public void testGetTokenInfoShouldThrow() { - NoopToken noopToken = new NoopToken(); - Exception exception = assertThrows(UserServiceException.class, () -> securityTokenManager.getTokenInfo(noopToken)); - assert(exception.getMessage().contains(securityTokenManager.TOKEN_NOT_SUPPORTED_MESSAGE)); + Exception exception = assertThrows(UserServiceException.class, () -> securityTokenManager.getTokenInfo(new AuthToken() { + @Override + public int hashCode() { + return super.hashCode(); + } + })); + assert (exception.getMessage().contains(securityTokenManager.TOKEN_NOT_SUPPORTED_MESSAGE)); } @Test public void testRevokeTokenShouldPass() throws Exception { doReturn(new BearerAuthToken("header.payload.signature")).when(userTokenHandler).issueToken("test"); - AuthToken createdBearerToken = securityTokenManager.issueToken("onBehalfOfToken", "test"); - assert(createdBearerToken instanceof BearerAuthToken); + AuthToken createdBearerToken = securityTokenManager.issueToken("test"); + assert (createdBearerToken instanceof BearerAuthToken); doReturn(true).when(userTokenHandler).validateToken(createdBearerToken); securityTokenManager.revokeToken(createdBearerToken); verify(userTokenHandler, times(1)).revokeToken(createdBearerToken); doReturn(new BasicAuthToken("Basic dGVzdDp0ZTpzdA==")).when(internalUserTokenHandler).issueToken("test"); - AuthToken createdBasicToken = securityTokenManager.issueToken("internalAuthToken", "test"); - assert(createdBasicToken instanceof BasicAuthToken); + AuthToken createdBasicToken = securityTokenManager.issueToken("test"); + assert (createdBasicToken instanceof BasicAuthToken); doReturn(true).when(internalUserTokenHandler).validateToken(createdBasicToken); securityTokenManager.revokeToken(createdBasicToken); verify(internalUserTokenHandler, times(1)).revokeToken(any()); @@ -170,32 +172,29 @@ public void testRevokeTokenShouldPass() throws Exception { @Test public void testRevokeTokenShouldThrow() { - NoopToken noopToken = new NoopToken(); - Exception exception = assertThrows(UserServiceException.class, () -> securityTokenManager.revokeToken(noopToken)); - assert(exception.getMessage().contains(securityTokenManager.TOKEN_NOT_SUPPORTED_MESSAGE)); + Exception exception = assertThrows(UserServiceException.class, () -> securityTokenManager.revokeToken(new AuthToken() { + @Override + public int hashCode() { + return super.hashCode(); + } + })); + assert (exception.getMessage().contains(securityTokenManager.TOKEN_NOT_SUPPORTED_MESSAGE)); } @Test public void testResetTokenShouldPass() throws Exception { doReturn(new BearerAuthToken("header.payload.signature")).when(userTokenHandler).issueToken("test"); - AuthToken createdBearerToken = securityTokenManager.issueToken("onBehalfOfToken", "test"); - assert(createdBearerToken instanceof BearerAuthToken); + AuthToken createdBearerToken = securityTokenManager.issueToken("test"); + assert (createdBearerToken instanceof BearerAuthToken); doReturn(true).when(userTokenHandler).validateToken(createdBearerToken); securityTokenManager.revokeToken(createdBearerToken); verify(userTokenHandler, times(1)).revokeToken(createdBearerToken); doReturn(new BasicAuthToken("Basic dGVzdDp0ZTpzdA==")).when(internalUserTokenHandler).issueToken("test"); - AuthToken createdBasicToken = securityTokenManager.issueToken("internalAuthToken", "test"); - assert(createdBasicToken instanceof BasicAuthToken); + AuthToken createdBasicToken = securityTokenManager.issueToken("test"); + assert (createdBasicToken instanceof BasicAuthToken); doReturn(true).when(internalUserTokenHandler).validateToken(createdBasicToken); securityTokenManager.revokeToken(createdBasicToken); verify(internalUserTokenHandler, times(1)).revokeToken(any()); } - - @Test - public void testResetTokenShouldThrow() { - NoopToken noopToken = new NoopToken(); - Exception exception = assertThrows(UserServiceException.class, () -> securityTokenManager.resetToken(noopToken)); - assert(exception.getMessage().contains(securityTokenManager.TOKEN_NOT_SUPPORTED_MESSAGE)); - } } diff --git a/src/test/java/org/opensearch/security/auth/UserTokenHandlerTests.java b/src/test/java/org/opensearch/security/auth/UserTokenHandlerTests.java index bfac4d9803..15c2e60dbc 100644 --- a/src/test/java/org/opensearch/security/auth/UserTokenHandlerTests.java +++ b/src/test/java/org/opensearch/security/auth/UserTokenHandlerTests.java @@ -20,7 +20,6 @@ import org.opensearch.common.settings.Settings; import org.opensearch.identity.tokens.AuthToken; import org.opensearch.identity.tokens.BearerAuthToken; -import org.opensearch.identity.tokens.NoopToken; import org.opensearch.security.authtoken.jwt.JwtVendor; import org.opensearch.security.configuration.ConfigurationRepository; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; @@ -64,9 +63,7 @@ public class UserTokenHandlerTests { @Before public void setup() { MockitoAnnotations.openMocks(this); - Settings settings = Settings.builder() - .put("signing_key", "abc123") - .put("encryption_key", "def456").build(); + Settings settings = Settings.builder().put("signing_key", "abc123").put("encryption_key", "def456").build(); jwtVendor = spy(new JwtVendor(settings)); client = mock(Client.class); user = new User("test_user"); @@ -128,7 +125,12 @@ public void testIssueTokenShouldThrowValidate() throws Exception { "test_role3")); // This is required because the ThreadContext cannot be mocked -- basically skips over the step of pulling the User from the threadContext - doReturn(new NoopToken()).when(userTokenHandler).issueToken("test"); + doReturn(new AuthToken() { + @Override + public int hashCode() { + return super.hashCode(); + } + }).when(userTokenHandler).issueToken("test"); AuthToken token = userTokenHandler.issueToken("test"); Exception ex = assertThrows(UserServiceException.class, () -> userTokenHandler.validateToken(token)); @@ -202,25 +204,4 @@ public void testFailValidationAfterRevoke() throws Exception { assertFalse(isValid); } - - @Test - public void testResetShouldThrowException() throws Exception { - when(clusterService.getClusterName()).thenReturn(new ClusterName("test_cluster")); - doReturn(revokedTokensConfiguration).when(userTokenHandler).load(getRevokedTokensConfigName(), false); - when(jwtVendor.createJwt(clusterService.getClusterName().toString(), user.getName(), "test", DEFAULT_EXPIRATION_TIME_SECONDS, Arrays.asList("test_role1", - "test_role2", - "test_role3"))).thenCallRealMethod(); - - String tokenString = jwtVendor.createJwt(clusterService.getClusterName().toString(), user.getName(), "test", DEFAULT_EXPIRATION_TIME_SECONDS, Arrays.asList("test_role1", - "test_role2", - "test_role3")); - - // This is required because the ThreadContext cannot be mocked -- basically skips over the step of pulling the User from the threadContext - doReturn(new BearerAuthToken(tokenString)).when(userTokenHandler).issueToken("test"); - - AuthToken token = userTokenHandler.issueToken("test"); - Exception exception = assertThrows(UserServiceException.class, () -> userTokenHandler.resetToken(token)); - assert(exception.getMessage().contains("The UserTokenHandler does not support the reset operation. Please issue a new token instead.")); - } } - diff --git a/src/test/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticatorTests.java b/src/test/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticatorTests.java index 6e56ea833e..c2467aafdf 100644 --- a/src/test/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticatorTests.java +++ b/src/test/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticatorTests.java @@ -53,10 +53,10 @@ public class HTTPOnBehalfOfJwtAuthenticatorTests { public void testNoKey() throws Exception { final AuthCredentials credentials = extractCredentialsFromJwtHeader( - null, - claimsEncryptionKey, - Jwts.builder().setSubject("Leonard McCoy"), - false + null, + claimsEncryptionKey, + Jwts.builder().setSubject("Leonard McCoy"), + false ); Assert.assertNull(credentials); @@ -66,10 +66,10 @@ public void testNoKey() throws Exception { public void testEmptyKey() throws Exception { final AuthCredentials credentials = extractCredentialsFromJwtHeader( - "", - claimsEncryptionKey, - Jwts.builder().setSubject("Leonard McCoy"), - false + "", + claimsEncryptionKey, + Jwts.builder().setSubject("Leonard McCoy"), + false ); Assert.assertNull(credentials); @@ -79,10 +79,10 @@ public void testEmptyKey() throws Exception { public void testBadKey() throws Exception { final AuthCredentials credentials = extractCredentialsFromJwtHeader( - BaseEncoding.base64().encode(new byte[] { 1, 3, 3, 4, 3, 6, 7, 8, 3, 10 }), - claimsEncryptionKey, - Jwts.builder().setSubject("Leonard McCoy"), - false + BaseEncoding.base64().encode(new byte[] { 1, 3, 3, 4, 3, 6, 7, 8, 3, 10 }), + claimsEncryptionKey, + Jwts.builder().setSubject("Leonard McCoy"), + false ); Assert.assertNull(credentials); @@ -92,8 +92,8 @@ public void testBadKey() throws Exception { public void testTokenMissing() throws Exception { HTTPOnBehalfOfJwtAuthenticator jwtAuth = new HTTPOnBehalfOfJwtAuthenticator( - BaseEncoding.base64().encode(secretKeyBytes), - claimsEncryptionKey + BaseEncoding.base64().encode(secretKeyBytes), + claimsEncryptionKey ); Map headers = new HashMap(); @@ -108,8 +108,8 @@ public void testInvalid() throws Exception { String jwsToken = "123invalidtoken.."; HTTPOnBehalfOfJwtAuthenticator jwtAuth = new HTTPOnBehalfOfJwtAuthenticator( - BaseEncoding.base64().encode(secretKeyBytes), - claimsEncryptionKey + BaseEncoding.base64().encode(secretKeyBytes), + claimsEncryptionKey ); Map headers = new HashMap(); headers.put("Authorization", "Bearer " + jwsToken); @@ -122,14 +122,14 @@ public void testInvalid() throws Exception { public void testBearer() throws Exception { String jwsToken = Jwts.builder() - .setSubject("Leonard McCoy") - .setAudience("ext_0") - .signWith(secretKey, SignatureAlgorithm.HS512) - .compact(); + .setSubject("Leonard McCoy") + .setAudience("ext_0") + .signWith(secretKey, SignatureAlgorithm.HS512) + .compact(); HTTPOnBehalfOfJwtAuthenticator jwtAuth = new HTTPOnBehalfOfJwtAuthenticator( - BaseEncoding.base64().encode(secretKeyBytes), - claimsEncryptionKey + BaseEncoding.base64().encode(secretKeyBytes), + claimsEncryptionKey ); Map headers = new HashMap(); headers.put("Authorization", "Bearer " + jwsToken); @@ -175,10 +175,10 @@ public void testRoles() throws Exception { List roles = List.of("IT", "HR"); final AuthCredentials credentials = extractCredentialsFromJwtHeader( - signingKey, - claimsEncryptionKey, - Jwts.builder().setSubject("Leonard McCoy").claim("dr", "role1,role2"), - true + signingKey, + claimsEncryptionKey, + Jwts.builder().setSubject("Leonard McCoy").claim("dr", "role1,role2"), + true ); Assert.assertNotNull(credentials); @@ -190,10 +190,10 @@ public void testRoles() throws Exception { public void testNullClaim() throws Exception { final AuthCredentials credentials = extractCredentialsFromJwtHeader( - signingKey, - claimsEncryptionKey, - Jwts.builder().setSubject("Leonard McCoy").claim("dr", null), - false + signingKey, + claimsEncryptionKey, + Jwts.builder().setSubject("Leonard McCoy").claim("dr", null), + false ); Assert.assertNotNull(credentials); @@ -205,10 +205,10 @@ public void testNullClaim() throws Exception { public void testNonStringClaim() throws Exception { final AuthCredentials credentials = extractCredentialsFromJwtHeader( - signingKey, - claimsEncryptionKey, - Jwts.builder().setSubject("Leonard McCoy").claim("dr", 123L), - true + signingKey, + claimsEncryptionKey, + Jwts.builder().setSubject("Leonard McCoy").claim("dr", 123L), + true ); Assert.assertNotNull(credentials); @@ -221,10 +221,10 @@ public void testNonStringClaim() throws Exception { public void testRolesMissing() throws Exception { final AuthCredentials credentials = extractCredentialsFromJwtHeader( - signingKey, - claimsEncryptionKey, - Jwts.builder().setSubject("Leonard McCoy"), - false + signingKey, + claimsEncryptionKey, + Jwts.builder().setSubject("Leonard McCoy"), + false ); Assert.assertNotNull(credentials); @@ -236,10 +236,10 @@ public void testRolesMissing() throws Exception { public void testWrongSubjectKey() throws Exception { final AuthCredentials credentials = extractCredentialsFromJwtHeader( - signingKey, - claimsEncryptionKey, - Jwts.builder().claim("roles", "role1,role2").claim("asub", "Dr. Who"), - false + signingKey, + claimsEncryptionKey, + Jwts.builder().claim("roles", "role1,role2").claim("asub", "Dr. Who"), + false ); Assert.assertNull(credentials); @@ -249,10 +249,10 @@ public void testWrongSubjectKey() throws Exception { public void testExp() throws Exception { final AuthCredentials credentials = extractCredentialsFromJwtHeader( - signingKey, - claimsEncryptionKey, - Jwts.builder().setSubject("Expired").setExpiration(new Date(100)), - false + signingKey, + claimsEncryptionKey, + Jwts.builder().setSubject("Expired").setExpiration(new Date(100)), + false ); Assert.assertNull(credentials); @@ -262,10 +262,10 @@ public void testExp() throws Exception { public void testNbf() throws Exception { final AuthCredentials credentials = extractCredentialsFromJwtHeader( - signingKey, - claimsEncryptionKey, - Jwts.builder().setSubject("Expired").setNotBefore(new Date(System.currentTimeMillis() + (1000 * 36000))), - false + signingKey, + claimsEncryptionKey, + Jwts.builder().setSubject("Expired").setNotBefore(new Date(System.currentTimeMillis() + (1000 * 36000))), + false ); Assert.assertNull(credentials); @@ -275,7 +275,7 @@ public void testNbf() throws Exception { public void testRolesArray() throws Exception { JwtBuilder builder = Jwts.builder() - .setPayload("{" + "\"sub\": \"Cluster_0\"," + "\"aud\": \"ext_0\"," + "\"dr\": \"a,b,3rd\"" + "}"); + .setPayload("{" + "\"sub\": \"Cluster_0\"," + "\"aud\": \"ext_0\"," + "\"dr\": \"a,b,3rd\"" + "}"); final AuthCredentials credentials = extractCredentialsFromJwtHeader(signingKey, claimsEncryptionKey, builder, true); @@ -289,10 +289,10 @@ public void testRolesArray() throws Exception { /** extracts a default user credential from a request header */ private AuthCredentials extractCredentialsFromJwtHeader( - final String signingKey, - final String encryptionKey, - final JwtBuilder jwtBuilder, - final Boolean bwcPluginCompatibilityMode + final String signingKey, + final String encryptionKey, + final JwtBuilder jwtBuilder, + final Boolean bwcPluginCompatibilityMode ) { final String jwsToken = jwtBuilder.signWith(secretKey, SignatureAlgorithm.HS512).compact(); final HTTPOnBehalfOfJwtAuthenticator jwtAuth = new HTTPOnBehalfOfJwtAuthenticator(signingKey, encryptionKey);