diff --git a/pom.xml b/pom.xml index ad3cfafcb5..49db4fc449 100644 --- a/pom.xml +++ b/pom.xml @@ -158,6 +158,12 @@ + + commons-validator + commons-validator + 1.7 + + com.google.guava diff --git a/securityconfig/config.yml b/securityconfig/config.yml index 251b13aef1..fe9dd32062 100644 --- a/securityconfig/config.yml +++ b/securityconfig/config.yml @@ -81,7 +81,24 @@ config: ###### more information about XFF https://en.wikipedia.org/wiki/X-Forwarded-For ###### and here https://tools.ietf.org/html/rfc7239 ###### and https://tomcat.apache.org/tomcat-8.0-doc/config/valve.html#Remote_IP_Valve + #auth_token_provider: + # enabled: true + # jwt_signing_key_hs512: "test_abc" + # max_validity: "1y" + # max_tokens_per_user: 100 authc: + opendistro_issued_jwt_auth_domain: + description: "Authenticate via Json Web Tokens issued by Opendistro Security" + http_enabled: false + transport_enabled: false + # This auth domain is only available for HTTP + order: 1 + http_authenticator: + type: auth_token + challenge: false + # This auth domain automatically pulls configuration from the auth_token_provider config above + authentication_backend: + type: auth_token kerberos_auth_domain: http_enabled: false transport_enabled: false diff --git a/src/main/java/com/amazon/dlic/auth/http/jwt/AbstractHTTPJwtAuthenticator.java b/src/main/java/com/amazon/dlic/auth/http/jwt/AbstractHTTPJwtAuthenticator.java index 49f673f6dc..745df7cd47 100644 --- a/src/main/java/com/amazon/dlic/auth/http/jwt/AbstractHTTPJwtAuthenticator.java +++ b/src/main/java/com/amazon/dlic/auth/http/jwt/AbstractHTTPJwtAuthenticator.java @@ -52,10 +52,10 @@ public abstract class AbstractHTTPJwtAuthenticator implements HTTPAuthenticator private KeyProvider keyProvider; private JwtVerifier jwtVerifier; - private final String jwtHeaderName; - private final boolean isDefaultAuthHeader; - private final String jwtUrlParameter; - private final String subjectKey; + private String jwtHeaderName; + private boolean isDefaultAuthHeader; + private String jwtUrlParameter; + private String subjectKey; private final String rolesKey; public AbstractHTTPJwtAuthenticator(Settings settings, Path configPath) { @@ -232,4 +232,11 @@ public boolean reRequestAuthentication(RestChannel channel, AuthCredentials auth return true; } + public void setAuthenticatorSettings(String jwtHeaderName, String subjectKey) { + this.jwtHeaderName = jwtHeaderName; + this.isDefaultAuthHeader = HttpHeaders.AUTHORIZATION.equalsIgnoreCase(this.jwtHeaderName); + this.jwtUrlParameter = null; + this.subjectKey = subjectKey; + } + } diff --git a/src/main/java/com/amazon/dlic/auth/ldap/backend/LDAPAuthenticationBackend.java b/src/main/java/com/amazon/dlic/auth/ldap/backend/LDAPAuthenticationBackend.java index fc329728bc..33910e564f 100755 --- a/src/main/java/com/amazon/dlic/auth/ldap/backend/LDAPAuthenticationBackend.java +++ b/src/main/java/com/amazon/dlic/auth/ldap/backend/LDAPAuthenticationBackend.java @@ -26,6 +26,7 @@ import java.util.Set; import java.util.UUID; + import org.apache.commons.lang3.tuple.Pair; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -42,12 +43,12 @@ import com.amazon.dlic.auth.ldap.util.ConfigConstants; import com.amazon.dlic.auth.ldap.util.LdapHelper; import com.amazon.dlic.auth.ldap.util.Utils; -import com.amazon.opendistroforelasticsearch.security.auth.AuthenticationBackend; +import com.amazon.opendistroforelasticsearch.security.auth.SyncAuthenticationBackend; import com.amazon.opendistroforelasticsearch.security.user.AuthCredentials; import com.amazon.opendistroforelasticsearch.security.user.User; import com.amazon.opendistroforelasticsearch.security.support.WildcardMatcher; -public class LDAPAuthenticationBackend implements AuthenticationBackend { +public class LDAPAuthenticationBackend implements SyncAuthenticationBackend { static final int ZERO_PLACEHOLDER = 0; static final String DEFAULT_USERBASE = ""; diff --git a/src/main/java/com/amazon/dlic/auth/ldap2/LDAPAuthenticationBackend2.java b/src/main/java/com/amazon/dlic/auth/ldap2/LDAPAuthenticationBackend2.java index a576b773be..b6bc7fb67f 100755 --- a/src/main/java/com/amazon/dlic/auth/ldap2/LDAPAuthenticationBackend2.java +++ b/src/main/java/com/amazon/dlic/auth/ldap2/LDAPAuthenticationBackend2.java @@ -43,13 +43,13 @@ import com.amazon.dlic.auth.ldap.util.ConfigConstants; import com.amazon.dlic.auth.ldap.util.Utils; import com.amazon.dlic.util.SettingsBasedSSLConfigurator.SSLConfigException; -import com.amazon.opendistroforelasticsearch.security.auth.AuthenticationBackend; +import com.amazon.opendistroforelasticsearch.security.auth.SyncAuthenticationBackend; import com.amazon.opendistroforelasticsearch.security.auth.Destroyable; import com.amazon.opendistroforelasticsearch.security.user.AuthCredentials; import com.amazon.opendistroforelasticsearch.security.user.User; import com.amazon.opendistroforelasticsearch.security.support.WildcardMatcher; -public class LDAPAuthenticationBackend2 implements AuthenticationBackend, Destroyable { +public class LDAPAuthenticationBackend2 implements SyncAuthenticationBackend, Destroyable { protected static final Logger log = LogManager.getLogger(LDAPAuthenticationBackend2.class); diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/OpenDistroSecurityPlugin.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/OpenDistroSecurityPlugin.java index 42adff1243..570f9335db 100644 --- a/src/main/java/com/amazon/opendistroforelasticsearch/security/OpenDistroSecurityPlugin.java +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/OpenDistroSecurityPlugin.java @@ -51,6 +51,7 @@ import com.amazon.opendistroforelasticsearch.security.auditlog.impl.AuditLogImpl; import com.amazon.opendistroforelasticsearch.security.compliance.ComplianceIndexingOperationListenerImpl; import com.amazon.opendistroforelasticsearch.security.configuration.DlsFlsValveImpl; +import com.amazon.opendistroforelasticsearch.security.authtoken.AuthTokenService; import com.amazon.opendistroforelasticsearch.security.configuration.OpenDistroSecurityFlsDlsIndexSearcherWrapper; import com.amazon.opendistroforelasticsearch.security.configuration.PrivilegesInterceptorImpl; import com.amazon.opendistroforelasticsearch.security.configuration.Salt; @@ -798,7 +799,10 @@ public Collection createComponents(Client localClient, ClusterService cl securityRestHandler = new OpenDistroSecurityRestFilter(backendRegistry, auditLog, threadPool, principalExtractor, settings, configPath, compatConfig); - final DynamicConfigFactory dcf = new DynamicConfigFactory(cr, settings, configPath, localClient, threadPool, cih); + + AuthTokenService authTokenService = new AuthTokenService(); + final DynamicConfigFactory dcf = new DynamicConfigFactory(cr, settings, configPath, localClient, threadPool, cih, authTokenService); + dcf.registerDCFListener(backendRegistry); dcf.registerDCFListener(compatConfig); dcf.registerDCFListener(irr); diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/AuthenticationBackend.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/AuthenticationBackend.java index d95bbc18a8..7bf8ee9cb3 100644 --- a/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/AuthenticationBackend.java +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/AuthenticationBackend.java @@ -35,6 +35,8 @@ import com.amazon.opendistroforelasticsearch.security.user.AuthCredentials; import com.amazon.opendistroforelasticsearch.security.user.User; +import java.util.function.Consumer; + /** * Open Distro Security custom authentication backends need to implement this interface. *

@@ -69,7 +71,7 @@ public interface AuthenticationBackend { * @throws ElasticsearchSecurityException in case an authentication failure * (when credentials are incorrect, the user does not exist or the backend is not reachable) */ - User authenticate(AuthCredentials credentials) throws ElasticsearchSecurityException; + void authenticate(AuthCredentials credentials, Consumer onSuccess, Consumer onFailure); /** * @@ -81,4 +83,13 @@ public interface AuthenticationBackend { */ boolean exists(User user); + default UserCachingPolicy userCachingPolicy() { + return UserCachingPolicy.ALWAYS; + } + + enum UserCachingPolicy { + ALWAYS, + ONLY_IF_AUTHZ_SEPARATE, + NEVER + } } diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/BackendRegistry.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/BackendRegistry.java index 1a85b65323..5fafc87267 100644 --- a/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/BackendRegistry.java +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/BackendRegistry.java @@ -40,12 +40,14 @@ import java.util.Set; import java.util.SortedSet; import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import javax.naming.InvalidNameException; import javax.naming.ldap.LdapName; import javax.naming.ldap.Rdn; +import com.amazon.opendistroforelasticsearch.security.authtoken.authenticator.AuthTokenHttpJwtAuthenticator; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.elasticsearch.ElasticsearchSecurityException; @@ -296,7 +298,7 @@ public User authenticate(final TransportRequest request, final String sslPrincip } else { //auth credentials submitted //impersonation not possible, if requested it will be ignored - authenticatedUser = authcz(authenticatedUserCacheTransport, transportRoleCache, creds, authDomain.getBackend(), transportAuthorizers); + authenticatedUser = authcz(authenticatedUserCacheTransport, transportRoleCache, creds, (SyncAuthenticationBackend) authDomain.getBackend(), transportAuthorizers); } if (authenticatedUser == null) { @@ -467,7 +469,11 @@ public boolean authenticate(final RestRequest request, final RestChannel channel } //http completed - authenticatedUser = authcz(userCache, restRoleCache, ac, authDomain.getBackend(), restAuthorizers); + if (httpAuthenticator instanceof AuthTokenHttpJwtAuthenticator) { + authenticatedUser = authcz(restRoleCache, ac, authDomain.getBackend(), restAuthorizers); + } else { + authenticatedUser = authcz(userCache, restRoleCache, ac, (SyncAuthenticationBackend) authDomain.getBackend(), restAuthorizers); + } if(authenticatedUser == null) { if(log.isDebugEnabled()) { @@ -648,7 +654,7 @@ 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 SyncAuthenticationBackend authBackend, final Set authorizers) { if(ac == null) { return null; } @@ -683,6 +689,44 @@ public User call() throws Exception { } } + private User authcz(Cache> roleCache, final AuthCredentials ac, + final AuthenticationBackend authBackend, final Set authorizers) { + + AuthenticationBackend.UserCachingPolicy cachingPolicy = authBackend.userCachingPolicy(); + + CompletableFuture completableFuture = new CompletableFuture<>(); + authBackend.authenticate(ac, completableFuture::complete, completableFuture::completeExceptionally); + + User authenticatedUser; + + if (cachingPolicy == AuthenticationBackend.UserCachingPolicy.NEVER) { + try { + authenticatedUser = completableFuture.get(); + + if (!ac.isAuthzComplete() && !authenticatedUser.isAuthzComplete()) { + authz(authenticatedUser, roleCache, authorizers); + } + return authenticatedUser; + + } catch (Exception e ) { + e.printStackTrace(); + } + + } else if (cachingPolicy == AuthenticationBackend.UserCachingPolicy.ONLY_IF_AUTHZ_SEPARATE && authorizers.isEmpty()) { + // noop backend + // that means authc and authz was completely done via HTTP (like JWT or PKI) + + try{ + return completableFuture.get(); + } catch (Exception e) { + throw new RuntimeException(e); + } + + } + + return null; + } + private User impersonate(final TransportRequest tr, final User origPKIuser) throws ElasticsearchSecurityException { final String impersonatedUser = threadPool.getThreadContext().getHeader("opendistro_security_impersonate_as"); diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/SyncAuthenticationBackend.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/SyncAuthenticationBackend.java new file mode 100644 index 0000000000..0ff760a115 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/SyncAuthenticationBackend.java @@ -0,0 +1,41 @@ +package com.amazon.opendistroforelasticsearch.security.auth; + +import java.util.function.Consumer; + +import com.amazon.opendistroforelasticsearch.security.user.AuthCredentials; +import com.amazon.opendistroforelasticsearch.security.user.User; +import org.elasticsearch.ElasticsearchSecurityException; + +public interface SyncAuthenticationBackend extends AuthenticationBackend { + + /** + * Validate credentials and return an authenticated user (or throw an ElasticsearchSecurityException) + *

+ * Results of this method are normally cached so that we not need to query the backend for every authentication attempt. + *

+ * @param The credentials to be validated, never null + * @return the authenticated User, never null + * @throws ElasticsearchSecurityException in case an authentication failure + * (when credentials are incorrect, the user does not exist or the backend is not reachable) + */ + User authenticate(AuthCredentials credentials) throws ElasticsearchSecurityException; + + /** + * Validate credentials and return an authenticated user (or throw an ElasticsearchSecurityException) + *

+ * Results of this method are normally cached so that we not need to query the backend for every authentication attempt. + *

+ * @param The credentials to be validated, never null + * @return the authenticated User, never null + * @throws ElasticsearchSecurityException in case an authentication failure + * (when credentials are incorrect, the user does not exist or the backend is not reachable) + */ + default void authenticate(AuthCredentials credentials, Consumer onSuccess, Consumer onFailure) { + try { + User user = this.authenticate(credentials); + onSuccess.accept(user); + } catch (Exception e) { + onFailure.accept(e); + } + } +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/SyncAuthorizationBackend.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/SyncAuthorizationBackend.java new file mode 100644 index 0000000000..2b0a3dd498 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/SyncAuthorizationBackend.java @@ -0,0 +1,36 @@ +package com.amazon.opendistroforelasticsearch.security.auth; + +import com.amazon.opendistroforelasticsearch.security.user.AuthCredentials; +import com.amazon.opendistroforelasticsearch.security.user.User; +import org.elasticsearch.ElasticsearchSecurityException; + +import java.util.Collection; +import java.util.Collections; +import java.util.function.Consumer; + +public interface SyncAuthorizationBackend extends AuthorizationBackend { + /** + * Populate a {@link User} with backend roles. This method will not be called for cached users. + *

+ * Add them by calling either {@code user.addRole()} or {@code user.addRoles()} + *

+ * @param user The authenticated user to populate with backend roles, never null + * @param credentials Credentials to authenticate to the authorization backend, maybe null. + * This parameter is for future usage, currently always empty credentials are passed! + * @throws ElasticsearchSecurityException in case when the authorization backend cannot be reached + * or the {@code credentials} are insufficient to authenticate to the authorization backend. + */ + void fillRoles(User user, AuthCredentials credentials) throws ElasticsearchSecurityException; + + default void retrieveRoles(User user, AuthCredentials credentials, Consumer> onSuccess, Consumer onFailure) { + try { + fillRoles(user, credentials); + // TODO notsonice + + onSuccess.accept(Collections.emptyList()); + } catch (Exception e) { + onFailure.accept(e); + } + } + +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/internal/InternalAuthenticationBackend.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/internal/InternalAuthenticationBackend.java index 68aea3f5ce..808052dbcc 100644 --- a/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/internal/InternalAuthenticationBackend.java +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/internal/InternalAuthenticationBackend.java @@ -39,17 +39,18 @@ import java.util.Map; import java.util.Map.Entry; + import org.bouncycastle.crypto.generators.OpenBSDBCrypt; import org.elasticsearch.ElasticsearchSecurityException; -import com.amazon.opendistroforelasticsearch.security.auth.AuthenticationBackend; -import com.amazon.opendistroforelasticsearch.security.auth.AuthorizationBackend; +import com.amazon.opendistroforelasticsearch.security.auth.SyncAuthenticationBackend; +import com.amazon.opendistroforelasticsearch.security.auth.SyncAuthorizationBackend; import com.amazon.opendistroforelasticsearch.security.securityconf.InternalUsersModel; import com.amazon.opendistroforelasticsearch.security.user.AuthCredentials; import com.amazon.opendistroforelasticsearch.security.user.User; import org.greenrobot.eventbus.Subscribe; -public class InternalAuthenticationBackend implements AuthenticationBackend, AuthorizationBackend { +public class InternalAuthenticationBackend implements SyncAuthenticationBackend, SyncAuthorizationBackend { private InternalUsersModel internalUsersModel; diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/internal/NoOpAuthenticationBackend.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/internal/NoOpAuthenticationBackend.java index e1ef693c3b..1ed75d07a1 100644 --- a/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/internal/NoOpAuthenticationBackend.java +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/internal/NoOpAuthenticationBackend.java @@ -32,13 +32,14 @@ import java.nio.file.Path; + import org.elasticsearch.common.settings.Settings; -import com.amazon.opendistroforelasticsearch.security.auth.AuthenticationBackend; +import com.amazon.opendistroforelasticsearch.security.auth.SyncAuthenticationBackend; import com.amazon.opendistroforelasticsearch.security.user.AuthCredentials; import com.amazon.opendistroforelasticsearch.security.user.User; -public class NoOpAuthenticationBackend implements AuthenticationBackend { +public class NoOpAuthenticationBackend implements SyncAuthenticationBackend { public NoOpAuthenticationBackend(final Settings settings, final Path configPath) { super(); diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/authtoken/AuthToken.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/authtoken/AuthToken.java new file mode 100644 index 0000000000..b69bcad289 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/authtoken/AuthToken.java @@ -0,0 +1,46 @@ +package com.amazon.opendistroforelasticsearch.security.authtoken; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.time.Instant; + +public class AuthToken { + + private static final Logger log = LogManager.getLogger(AuthToken.class); + + private static final long serialVersionUID = 6038589333544878668L; + private String userName; + private String tokenName; + private String id; + private Instant creationTime; + private Instant expiryTime; + private Instant revokedAt; + + AuthToken(){ + } + + public String getUserName() { + return userName; + } + + public String getTokenName() { + return tokenName; + } + + public String getId() { + return id; + } + + public Instant getCreationTime() { + return creationTime; + } + + public Instant getExpiryTime() { + return expiryTime; + } + + public Instant getRevokedAt() { + return revokedAt; + } +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/authtoken/AuthTokenService.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/authtoken/AuthTokenService.java new file mode 100644 index 0000000000..725e8d56f3 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/authtoken/AuthTokenService.java @@ -0,0 +1,30 @@ +package com.amazon.opendistroforelasticsearch.security.authtoken; + +import com.amazon.opendistroforelasticsearch.security.authtoken.config.AuthTokenServiceConfig; +import org.apache.cxf.rs.security.jose.jwt.JwtException; +import org.apache.cxf.rs.security.jose.jwt.JwtToken; + +import java.util.Map; +public class AuthTokenService { + + public static final String USER_TYPE = "opendistro_security_auth_token"; + public static final String USER_TYPE_FULL_CURRENT_PERMISSIONS = "opendistro_security_auth_token_full_current_permissions"; + + public AuthTokenService() { + } + + public void setConfig(AuthTokenServiceConfig config) { + } + + public AuthToken getTokenByClaims(Map claims) { + return null; + } + + public JwtToken createToken() { + return null; + } + + public JwtToken getVerifiedJwtToken(String encodedJwt) throws JwtException { + return null; + } +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/authtoken/authenticator/AuthTokenAuthenticationBackend.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/authtoken/authenticator/AuthTokenAuthenticationBackend.java new file mode 100644 index 0000000000..7dff8dac48 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/authtoken/authenticator/AuthTokenAuthenticationBackend.java @@ -0,0 +1,48 @@ +package com.amazon.opendistroforelasticsearch.security.authtoken.authenticator; + +import java.nio.file.Path; +import java.util.HashSet; +import java.util.function.Consumer; + +import com.amazon.opendistroforelasticsearch.security.auth.AuthenticationBackend; +import com.amazon.opendistroforelasticsearch.security.authtoken.AuthTokenService; +import com.amazon.opendistroforelasticsearch.security.user.AuthCredentials; +import com.amazon.opendistroforelasticsearch.security.user.User; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.common.settings.Settings; + +public class AuthTokenAuthenticationBackend implements AuthenticationBackend { + + private static final Logger log = LogManager.getLogger(AuthTokenAuthenticationBackend.class); + + private AuthTokenService authTokenService; + + public AuthTokenAuthenticationBackend(final Settings settings, final Path configPath) { + } + + public void setAuthTokenService(AuthTokenService authTokenService) { + this.authTokenService = authTokenService; + } + + @Override + public String getType() { + return "opendistro_security_auth_token"; + } + + @Override + public void authenticate(AuthCredentials credentials, Consumer onSuccess, Consumer onFailure) { + } + + @Override + public boolean exists(User user) { + // This is only related to impersonation. Auth tokens don't support impersonation. + return false; + } + + @Override + public UserCachingPolicy userCachingPolicy() { + return UserCachingPolicy.NEVER; + } + +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/authtoken/authenticator/AuthTokenHttpJwtAuthenticator.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/authtoken/authenticator/AuthTokenHttpJwtAuthenticator.java new file mode 100644 index 0000000000..e44ea4e29a --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/authtoken/authenticator/AuthTokenHttpJwtAuthenticator.java @@ -0,0 +1,73 @@ +package com.amazon.opendistroforelasticsearch.security.authtoken.authenticator; + +import java.nio.file.Path; + +import com.amazon.dlic.auth.http.jwt.AbstractHTTPJwtAuthenticator; +import com.amazon.dlic.auth.http.jwt.keybyoidc.KeyProvider; +import com.amazon.opendistroforelasticsearch.security.authtoken.AuthTokenService; +import com.amazon.opendistroforelasticsearch.security.user.AuthCredentials; +import org.apache.cxf.rs.security.jose.jwt.JwtClaims; +import org.apache.cxf.rs.security.jose.jwt.JwtConstants; +import org.apache.cxf.rs.security.jose.jwt.JwtException; +import org.apache.cxf.rs.security.jose.jwt.JwtToken; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.rest.RestRequest; + +public class AuthTokenHttpJwtAuthenticator extends AbstractHTTPJwtAuthenticator { + + private final static Logger log = LogManager.getLogger(AuthTokenHttpJwtAuthenticator.class); + + private AuthTokenService authTokenService; + + public AuthTokenHttpJwtAuthenticator(final Settings settings, final Path configPath) { + super(settings, configPath); + setAuthenticatorSettings("Authorization", JwtConstants.CLAIM_SUBJECT); + } + + public void setAuthTokenService(AuthTokenService authTokenService) { + this.authTokenService = authTokenService; + } + + private AuthCredentials extractCredentials0(RestRequest request) throws ElasticsearchSecurityException { + String encodedJwt = getJwtTokenString(request); + if (Strings.isNullOrEmpty(encodedJwt)) { + return null; + } + + try { + JwtToken jwt = authTokenService.getVerifiedJwtToken(encodedJwt); + + JwtClaims claims = jwt.getClaims(); + + String subject = extractSubject(claims); + + if (subject == null) { + log.error("No subject found in JWT token: " + claims); + return null; + } + return AuthCredentials.forUser(subject).claims(claims.asMap()).complete().build(); + + } catch (JwtException e) { + log.info("JWT is invalid", e); + return null; + } catch (Exception e) { + e.printStackTrace(); + return null; + } + + } + + @Override + protected KeyProvider initKeyProvider(Settings settings, Path configPath) throws Exception { + return null; + } + + @Override + public String getType() { + return "opendistro_security_auth_token"; + } +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/authtoken/config/AuthTokenServiceConfig.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/authtoken/config/AuthTokenServiceConfig.java new file mode 100644 index 0000000000..7c10200a41 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/authtoken/config/AuthTokenServiceConfig.java @@ -0,0 +1,13 @@ +package com.amazon.opendistroforelasticsearch.security.authtoken.config; + +import com.fasterxml.jackson.databind.JsonNode; + + +public class AuthTokenServiceConfig { + + public static final String DEFAULT_AUDIENCE = "opendistro_security_authtoken"; + + public static AuthTokenServiceConfig parse(JsonNode jsonNode) { + return null; + } +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/securityconf/DynamicConfigFactory.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/securityconf/DynamicConfigFactory.java index e971ced989..36f1d5c46f 100644 --- a/src/main/java/com/amazon/opendistroforelasticsearch/security/securityconf/DynamicConfigFactory.java +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/securityconf/DynamicConfigFactory.java @@ -40,9 +40,12 @@ import java.util.concurrent.atomic.AtomicBoolean; import com.amazon.opendistroforelasticsearch.security.auditlog.config.AuditConfig; +import com.amazon.opendistroforelasticsearch.security.authtoken.AuthTokenService; +import com.amazon.opendistroforelasticsearch.security.authtoken.config.AuthTokenServiceConfig; import com.amazon.opendistroforelasticsearch.security.securityconf.impl.NodesDn; import com.amazon.opendistroforelasticsearch.security.securityconf.impl.WhitelistingSettings; import com.amazon.opendistroforelasticsearch.security.support.WildcardMatcher; +import com.fasterxml.jackson.core.JsonPointer; import com.google.common.collect.ImmutableList; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -83,6 +86,7 @@ public class DynamicConfigFactory implements Initializable, ConfigurationChangeL private static SecurityDynamicConfiguration staticActionGroups = SecurityDynamicConfiguration.empty(); private static SecurityDynamicConfiguration staticTenants = SecurityDynamicConfiguration.empty(); private static final WhitelistingSettings defaultWhitelistingSettings = new WhitelistingSettings(); + private final String auth_token_provider_path = "/dynamic/auth_token_provider"; static void resetStatics() { staticRoles = SecurityDynamicConfiguration.empty(); @@ -127,15 +131,17 @@ public final static SecurityDynamicConfiguration addStatics(SecurityDynamicCo private final Settings esSettings; private final Path configPath; private final InternalAuthenticationBackend iab = new InternalAuthenticationBackend(); + private final AuthTokenService authTokenService; SecurityDynamicConfiguration config; public DynamicConfigFactory(ConfigurationRepository cr, final Settings esSettings, - final Path configPath, Client client, ThreadPool threadPool, ClusterInfoHolder cih) { + final Path configPath, Client client, ThreadPool threadPool, ClusterInfoHolder cih, AuthTokenService authTokenService) { super(); this.cr = cr; this.esSettings = esSettings; this.configPath = configPath; + this.authTokenService = authTokenService; if(esSettings.getAsBoolean(ConfigConstants.OPENDISTRO_SECURITY_UNSUPPORTED_LOAD_STATIC_RESOURCES, true)) { try { @@ -235,12 +241,19 @@ public void onChange(Map> typeToConfig) { //rebuild v7 Models - dcm = new DynamicConfigModelV7(getConfigV7(config), esSettings, configPath, iab); + ConfigV7 configV7 = getConfigV7(config); + dcm = new DynamicConfigModelV7(configV7, esSettings, configPath, iab, authTokenService); ium = new InternalUsersModelV7((SecurityDynamicConfiguration) internalusers, (SecurityDynamicConfiguration) roles, (SecurityDynamicConfiguration) rolesmapping); cm = new ConfigModelV7((SecurityDynamicConfiguration) roles,(SecurityDynamicConfiguration)rolesmapping, (SecurityDynamicConfiguration)actionGroups, (SecurityDynamicConfiguration) tenants,dcm, esSettings); - + try{ + AuthTokenServiceConfig config = AuthTokenServiceConfig.parse(getAuthTokenConfig(configV7)); + authTokenService.setConfig(config); + } catch (Exception exception) { + log.error("Error occured while parsing the auth_token_provider configuration"); + exception.printStackTrace(); + } } else { //rebuild v6 Models @@ -263,6 +276,25 @@ public void onChange(Map> typeToConfig) { initialized.set(true); } + + private JsonNode getAuthTokenConfig(Object entry) { + + + if (entry == null) { + if (log.isDebugEnabled()) { + log.debug("No config entry 'config'" + " in " + config); + } + } + + JsonNode subNode = DefaultObjectMapper.objectMapper.valueToTree(entry).at(JsonPointer.compile(auth_token_provider_path)); + + if (subNode == null || subNode.isMissingNode()) { + if (log.isDebugEnabled()) { + log.debug("JsonPointer " + auth_token_provider_path + " not found"); + } + } + return subNode; + } private static ConfigV6 getConfigV6(SecurityDynamicConfiguration sdc) { @SuppressWarnings("unchecked") diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/securityconf/DynamicConfigModel.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/securityconf/DynamicConfigModel.java index c3d00265fa..80bac8fc9c 100644 --- a/src/main/java/com/amazon/opendistroforelasticsearch/security/securityconf/DynamicConfigModel.java +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/securityconf/DynamicConfigModel.java @@ -39,6 +39,8 @@ import java.util.Set; import java.util.SortedSet; +import com.amazon.opendistroforelasticsearch.security.authtoken.authenticator.AuthTokenAuthenticationBackend; +import com.amazon.opendistroforelasticsearch.security.authtoken.authenticator.AuthTokenHttpJwtAuthenticator; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -108,6 +110,9 @@ public DynamicConfigModel() { authImplMap.put("ldap2_c", "com.amazon.dlic.auth.ldap2.LDAPAuthenticationBackend2"); authImplMap.put("ldap2_z", "com.amazon.dlic.auth.ldap2.LDAPAuthorizationBackend2"); + authImplMap.put("auth_token_h", AuthTokenHttpJwtAuthenticator.class.getName()); + authImplMap.put("auth_token_c", AuthTokenAuthenticationBackend.class.getName()); + authImplMap.put("basic_h", HTTPBasicAuthenticator.class.getName()); authImplMap.put("proxy_h", HTTPProxyAuthenticator.class.getName()); authImplMap.put("extended-proxy_h", HTTPExtendedProxyAuthenticator.class.getName()); diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/securityconf/DynamicConfigModelV7.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/securityconf/DynamicConfigModelV7.java index 16aca8c6da..4dd71cfb8c 100644 --- a/src/main/java/com/amazon/opendistroforelasticsearch/security/securityconf/DynamicConfigModelV7.java +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/securityconf/DynamicConfigModelV7.java @@ -43,6 +43,9 @@ import java.util.SortedSet; import java.util.TreeSet; +import com.amazon.opendistroforelasticsearch.security.authtoken.AuthTokenService; +import com.amazon.opendistroforelasticsearch.security.authtoken.authenticator.AuthTokenAuthenticationBackend; +import com.amazon.opendistroforelasticsearch.security.authtoken.authenticator.AuthTokenHttpJwtAuthenticator; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.XContentType; @@ -76,18 +79,20 @@ public class DynamicConfigModelV7 extends DynamicConfigModel { private Set transportAuthorizers; private List destroyableComponents; private final InternalAuthenticationBackend iab; + private final AuthTokenService authTokenService; private List ipAuthFailureListeners; private Multimap authBackendFailureListeners; private List> ipClientBlockRegistries; private Multimap> authBackendClientBlockRegistries; - public DynamicConfigModelV7(ConfigV7 config, Settings esSettings, Path configPath, InternalAuthenticationBackend iab) { + public DynamicConfigModelV7(ConfigV7 config, Settings esSettings, Path configPath, InternalAuthenticationBackend iab, AuthTokenService authTokenService) { super(); this.config = config; this.esSettings = esSettings; this.configPath = configPath; this.iab = iab; + this.authTokenService = authTokenService; buildAAA(); } @Override @@ -282,6 +287,10 @@ private void buildAAA() { , configPath); } + if (authenticationBackend instanceof AuthTokenAuthenticationBackend) { + ((AuthTokenAuthenticationBackend) authenticationBackend).setAuthTokenService(this.authTokenService); + } + String httpAuthenticatorType = ad.getValue().http_authenticator.type; //no default HTTPAuthenticator httpAuthenticator = httpAuthenticatorType==null?null: (HTTPAuthenticator) newInstance(httpAuthenticatorType,"h", Settings.builder().put(esSettings) @@ -290,6 +299,10 @@ private void buildAAA() { , configPath); + if ( httpAuthenticator instanceof AuthTokenHttpJwtAuthenticator) { + ((AuthTokenHttpJwtAuthenticator) httpAuthenticator).setAuthTokenService(this.authTokenService); + } + final AuthDomain _ad = new AuthDomain(authenticationBackend, httpAuthenticator, ad.getValue().http_authenticator.challenge, ad.getValue().order); diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/securityconf/impl/v7/ConfigV7.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/securityconf/impl/v7/ConfigV7.java index 656d428324..fb894dac18 100644 --- a/src/main/java/com/amazon/opendistroforelasticsearch/security/securityconf/impl/v7/ConfigV7.java +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/securityconf/impl/v7/ConfigV7.java @@ -124,11 +124,12 @@ 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 HashMap auth_token_provider = null; + @Override public String toString() { return "Dynamic [filtered_alias_mode=" + filtered_alias_mode + ", kibana=" + kibana + ", http=" + http + ", authc=" + authc + ", authz=" - + authz + "]"; + + authz + ", auth_token_provider=" + auth_token_provider + "]"; } } diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/support/ModuleType.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/support/ModuleType.java index 46a1261fb3..ccde4a4c17 100644 --- a/src/main/java/com/amazon/opendistroforelasticsearch/security/support/ModuleType.java +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/support/ModuleType.java @@ -46,6 +46,8 @@ import com.amazon.opendistroforelasticsearch.security.http.proxy.HTTPExtendedProxyAuthenticator; import com.amazon.opendistroforelasticsearch.security.ssl.transport.PrincipalExtractor; import com.amazon.opendistroforelasticsearch.security.transport.InterClusterRequestEvaluator; +import com.amazon.opendistroforelasticsearch.security.authtoken.authenticator.AuthTokenAuthenticationBackend; +import com.amazon.opendistroforelasticsearch.security.authtoken.authenticator.AuthTokenHttpJwtAuthenticator; public enum ModuleType implements Serializable { @@ -60,9 +62,11 @@ public enum ModuleType implements Serializable { OPENID_AUTHENTICATION_BACKEND("OpenID authentication backend", "com.amazon.dlic.auth.http.jwt.keybyoidc.HTTPJwtKeyByOpenIdConnectAuthenticator", Boolean.TRUE), SAML_AUTHENTICATION_BACKEND("SAML authentication backend", "com.amazon.dlic.auth.http.saml.HTTPSamlAuthenticator", Boolean.TRUE), INTERNAL_USERS_AUTHENTICATION_BACKEND("Internal users authentication backend", InternalAuthenticationBackend.class.getName(), Boolean.FALSE), + AUTHTOKEN_AUTHENTICATION_BACKEND("Internal users authentication backend", AuthTokenAuthenticationBackend.class.getName(), Boolean.FALSE), NOOP_AUTHENTICATION_BACKEND("Noop authentication backend", NoOpAuthenticationBackend.class.getName(), Boolean.FALSE), NOOP_AUTHORIZATION_BACKEND("Noop authorization backend", NoOpAuthorizationBackend.class.getName(), Boolean.FALSE), HTTP_BASIC_AUTHENTICATOR("HTTP Basic Authenticator", HTTPBasicAuthenticator.class.getName(), Boolean.FALSE), + HTTP_AUTHTOKEN_AUTHENTICATOR("HTTP Auth Token Authenticator" , AuthTokenHttpJwtAuthenticator.class.getName(), Boolean.TRUE), HTTP_PROXY_AUTHENTICATOR("HTTP Proxy Authenticator", HTTPProxyAuthenticator.class.getName(), Boolean.FALSE), HTTP_EXT_PROXY_AUTHENTICATOR("HTTP Extended Proxy Authenticator", HTTPExtendedProxyAuthenticator.class.getName(), Boolean.FALSE), HTTP_CLIENTCERT_AUTHENTICATOR("HTTP Client Certificate Authenticator", HTTPClientCertAuthenticator.class.getName(), Boolean.FALSE), diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/user/AuthCredentials.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/user/AuthCredentials.java index 94fca45b5b..df9c6d7e3d 100644 --- a/src/main/java/com/amazon/opendistroforelasticsearch/security/user/AuthCredentials.java +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/user/AuthCredentials.java @@ -30,6 +30,7 @@ package com.amazon.opendistroforelasticsearch.security.user; +import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Arrays; @@ -57,6 +58,10 @@ public final class AuthCredentials { private boolean complete; private final byte[] internalPasswordHash; private final Map attributes = new HashMap<>(); + // claims obtained from AuthToken + private Map claims = new HashMap<>(); + // User roles wont be cached by BackendRegistry if set to true + private boolean authzComplete; /** * Create new credentials with a username and native credentials @@ -66,7 +71,7 @@ public final class AuthCredentials { * @throws IllegalArgumentException if username or nativeCredentials are null or empty */ public AuthCredentials(final String username, final Object nativeCredentials) { - this(username, null, nativeCredentials); + this(username, null, nativeCredentials, null, null); if (nativeCredentials == null) { throw new IllegalArgumentException("nativeCredentials must not be null or empty"); @@ -81,7 +86,7 @@ public AuthCredentials(final String username, final Object nativeCredentials) { * @throws IllegalArgumentException if username or password is null or empty */ public AuthCredentials(final String username, final byte[] password) { - this(username, password, null); + this(username, password, null, null, null); if (password == null || password.length == 0) { throw new IllegalArgumentException("password must not be null or empty"); @@ -96,10 +101,11 @@ public AuthCredentials(final String username, final byte[] password) { * @throws IllegalArgumentException if username is null or empty */ public AuthCredentials(final String username, String... backendRoles) { - this(username, null, null, backendRoles); + this(username, null, null, null, backendRoles); } - private AuthCredentials(final String username, byte[] password, Object nativeCredentials, String... backendRoles) { + private AuthCredentials(final String username, byte[] password, Object nativeCredentials, Map claims, + String... backendRoles) { super(); if (username == null || username.isEmpty()) { @@ -132,6 +138,12 @@ private AuthCredentials(final String username, byte[] password, Object nativeCre if(backendRoles != null && backendRoles.length > 0) { this.backendRoles.addAll(Arrays.asList(backendRoles)); } + + if (claims != null) { + this.claims = Collections.unmodifiableMap(claims); + } else { + this.claims = new HashMap<>(); + } } /** @@ -191,8 +203,9 @@ public boolean equals(Object obj) { @Override public String toString() { - return "AuthCredentials [username=" + username + ", password empty=" + (password == null) + ", nativeCredentials empty=" - + (nativeCredentials == null) + ",backendRoles="+backendRoles+"]"; + return "AuthCredentials [username=" + username + ", password empty=" + (password == null) + ", " + + "nativeCredentials empty=" + + (nativeCredentials == null) + ",backendRoles=" + backendRoles + "]"; } /** @@ -229,4 +242,138 @@ public void addAttribute(String name, String value) { public Map getAttributes() { return Collections.unmodifiableMap(this.attributes); } + + public Map getClaims() { + return claims; + } + + public boolean isAuthzComplete() { + return authzComplete; + } + + public static class Builder { + private static final String DIGEST_ALGORITHM = "SHA-256"; + private String username; + private byte[] password; + private Object nativeCredentials; + private Set backendRoles = new HashSet<>(); + private boolean complete; + private byte[] internalPasswordHash; + private Map attributes = new HashMap<>(); + private Map claims = new HashMap<>(); + private boolean authzComplete; + + public Builder() { + + } + + public Builder username(String username) { + this.username = username; + return this; + } + + public Builder password(byte[] password) { + if (password == null || password.length == 0) { + throw new IllegalArgumentException("password must not be null or empty"); + } + + this.password = Arrays.copyOf(password, password.length); + + try { + MessageDigest digester = MessageDigest.getInstance(DIGEST_ALGORITHM); + internalPasswordHash = digester.digest(this.password); + } catch (NoSuchAlgorithmException e) { + throw new ElasticsearchSecurityException("Unable to digest password", e); + } + + Arrays.fill(password, (byte) '\0'); + + return this; + } + + public Builder password(String password) { + return this.password(password.getBytes(StandardCharsets.UTF_8)); + } + + public Builder nativeCredentials(Object nativeCredentials) { + if (nativeCredentials == null) { + throw new IllegalArgumentException("nativeCredentials must not be null or empty"); + } + this.nativeCredentials = nativeCredentials; + return this; + } + + public Builder backendRoles(String... backendRoles) { + if (backendRoles == null) { + return this; + } + + this.backendRoles.addAll(Arrays.asList(backendRoles)); + return this; + } + + /** + * If the credentials are complete and no further roundtrips with the originator are due + * then this method must be called so that the authentication flow can proceed. + *

+ * If this credentials are already marked a complete then a call to this method does nothing. + */ + public Builder complete() { + this.complete = true; + return this; + } + + public Builder authzComplete() { + this.authzComplete = true; + return this; + } + + public Builder oldAttribute(String name, String value) { + if (name != null && !name.isEmpty()) { + this.attributes.put(name, value); + } + return this; + } + + public Builder oldAttributes(Map map) { + this.attributes.putAll(map); + return this; + } + + public Builder prefixOldAttributes(String keyPrefix, Map map) { + for (Map.Entry entry : map.entrySet()) { + this.attributes.put(keyPrefix + entry.getKey(), entry.getValue() != null ? + entry.getValue().toString() : null); + } + return this; + } + + public Builder claims(Map map) { + this.claims.putAll(map); + return this; + } + + + public String getUsername() { + return username; + } + + public AuthCredentials build() { + int n = backendRoles.size(); + String roles[] = new String[n]; + System.arraycopy(backendRoles.toArray(), 0, roles, 0, n); + + AuthCredentials result = new AuthCredentials(username, password, nativeCredentials, claims, roles); + result.complete = this.complete; + result.authzComplete = this.authzComplete; + this.password = null; + this.nativeCredentials = null; + this.internalPasswordHash = null; + return result; + } + } + + public static Builder forUser(String username) { + return new Builder().username(username); + } } diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/user/User.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/user/User.java index c04f8de536..5e968ba1b6 100644 --- a/src/main/java/com/amazon/opendistroforelasticsearch/security/user/User.java +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/user/User.java @@ -39,6 +39,7 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; +import java.util.Arrays; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; @@ -55,7 +56,7 @@ public class User implements Serializable, Writeable, CustomAttributesAware { public static final User ANONYMOUS = new User("opendistro_security_anonymous", Lists.newArrayList("opendistro_security_anonymous_backendrole"), null); - + private static final long serialVersionUID = -5500938501822658596L; private final String name; /** @@ -66,19 +67,27 @@ public class User implements Serializable, Writeable, CustomAttributesAware { private String requestedTenant; private Map attributes = new HashMap<>(); private boolean isInjected = false; + // To check the user is of type AuthToken + private String type; + // Contains the id for authToken + private final Object specialAuthzConfig; + // User roles wont be cached by BackendRegistry if set to true + private boolean authzComplete; public User(final StreamInput in) throws IOException { super(); name = in.readString(); + type = in.readOptionalString(); roles.addAll(in.readList(StreamInput::readString)); requestedTenant = in.readString(); attributes = in.readMap(StreamInput::readString, StreamInput::readString); openDistroSecurityRoles.addAll(in.readList(StreamInput::readString)); + specialAuthzConfig = null; } - + /** * Create a new authenticated user - * + * * @param name The username (must not be null or empty) * @param roles Roles of which the user is a member off (maybe null) * @param customAttributes Custom attributes associated with this (maybe null) @@ -92,20 +101,37 @@ public User(final String name, final Collection roles, final AuthCredent } this.name = name; + this.type = null; + this.specialAuthzConfig = null; + configureUser(name, roles, customAttributes); + } + + public User(final String name, String type, final Collection roles, final Set openDistroSecurityRoles, final AuthCredentials customAttributes, Object specialAuthzConfig) { + super(); + if (name == null || name.isEmpty()) { + throw new IllegalArgumentException("name must not be null or empty"); + } + this.name = name; + this.type = type; + this.openDistroSecurityRoles.addAll(openDistroSecurityRoles); + this.specialAuthzConfig = specialAuthzConfig; + configureUser(name, roles, customAttributes); + } + + private void configureUser(final String name, final Collection roles, final AuthCredentials customAttributes) { if (roles != null) { this.addRoles(roles); } - - if(customAttributes != null) { + + if (customAttributes != null) { this.attributes.putAll(customAttributes.getAttributes()); } - } /** * Create a new authenticated user without roles and attributes - * + * * @param name The username (must not be null or empty) * @throws IllegalArgumentException if name is null or empty */ @@ -118,7 +144,7 @@ public final String getName() { } /** - * + * * @return A unmodifiable set of the backend roles this user is a member of */ public final Set getRoles() { @@ -127,7 +153,7 @@ public final Set getRoles() { /** * Associate this user with a backend role - * + * * @param role The backend role */ public final void addRole(final String role) { @@ -136,7 +162,7 @@ public final void addRole(final String role) { /** * Associate this user with a set of backend roles - * + * * @param roles The backend roles */ public final void addRoles(final Collection roles) { @@ -147,7 +173,7 @@ public final void addRoles(final Collection roles) { /** * Check if this user is a member of a backend role - * + * * @param role The backend role * @return true if this user is a member of the backend role, false otherwise */ @@ -157,7 +183,7 @@ public final boolean isUserInRole(final String role) { /** * Associate this user with a set of backend roles - * + * * @param roles The backend roles */ public final void addAttributes(final Map attributes) { @@ -165,7 +191,7 @@ public final void addAttributes(final Map attributes) { this.attributes.putAll(attributes); } } - + public final String getRequestedTenant() { return requestedTenant; } @@ -173,8 +199,8 @@ public final String getRequestedTenant() { public final void setRequestedTenant(String requestedTenant) { this.requestedTenant = requestedTenant; } - - + + public boolean isInjected() { return isInjected; } @@ -224,7 +250,7 @@ public final boolean equals(final Object obj) { /** * Copy all backend roles from another user - * + * * @param user The user from which the backend roles should be copied over */ public final void copyRolesFrom(final User user) { @@ -244,7 +270,7 @@ public void writeTo(StreamOutput out) throws IOException { /** * Get the custom attributes associated with this user - * + * * @return A modifiable map with all the current custom attributes associated with this user */ public synchronized final Map getCustomAttributesMap() { @@ -253,14 +279,128 @@ public synchronized final Map getCustomAttributesMap() { } return attributes; } - + public final void addOpenDistroSecurityRoles(final Collection securityRoles) { if(securityRoles != null && this.openDistroSecurityRoles != null) { this.openDistroSecurityRoles.addAll(securityRoles); } } - + public final Set getOpenDistroSecurityRoles() { return this.openDistroSecurityRoles == null ? Collections.emptySet() : Collections.unmodifiableSet(this.openDistroSecurityRoles); } + + public String getType() { + return type; + } + + public Object getSpecialAuthzConfig() { + return specialAuthzConfig; + } + + public boolean isAuthzComplete() { + return authzComplete; + } + + public Builder copy() { + Builder builder = new Builder(); + builder.name = name; + builder.type = type; + builder.backendRoles.addAll(roles); + builder.openDistroSecurityRoles.addAll(openDistroSecurityRoles); + builder.requestedTenant = requestedTenant; + builder.attributes.putAll(attributes); + builder.isInjected = isInjected; + builder.specialAuthzConfig = specialAuthzConfig; + builder.authzComplete = authzComplete; + + return builder; + } + + + public static class Builder { + private String name; + private String type; + private final Set backendRoles = new HashSet(); + private final Set openDistroSecurityRoles = new HashSet(); + private String requestedTenant; + private Map attributes = new HashMap<>(); + private boolean isInjected; + private Object specialAuthzConfig; + private boolean authzComplete; + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder type(String type) { + this.type = type; + return this; + } + + public Builder requestedTenant(String requestedTenant) { + this.requestedTenant = requestedTenant; + return this; + } + + public Builder backendRoles(String... backendRoles) { + return this.backendRoles(Arrays.asList(backendRoles)); + } + + public Builder backendRoles(Collection backendRoles) { + if (backendRoles != null) { + this.backendRoles.addAll(backendRoles); + } + return this; + } + + public Builder openDistroSecurityRoles(String... openDistroSecurityRoles) { + return this.openDistroSecurityRoles(Arrays.asList(openDistroSecurityRoles)); + } + + public Builder openDistroSecurityRoles(Collection openDistroSecurityRoles) { + if (openDistroSecurityRoles != null) { + this.openDistroSecurityRoles.addAll(openDistroSecurityRoles); + } + return this; + } + + @Deprecated + public Builder oldAttributes(Map attributes) { + this.attributes.putAll(attributes); + return this; + } + + @Deprecated + public Builder oldAttribute(String key, String value) { + this.attributes.put(key, value); + return this; + } + + public Builder injected() { + this.isInjected = true; + return this; + } + + public Builder specialAuthzConfig(Object specialAuthzConfig) { + this.specialAuthzConfig = specialAuthzConfig; + return this; + } + + public Builder authzComplete() { + this.authzComplete = true; + return this; + } + + public User build() { + User user = new User(name, type, backendRoles, openDistroSecurityRoles, null, specialAuthzConfig); + user.authzComplete = this.authzComplete; + return user; + } + } + + public static Builder forUser(String username) { + return new Builder().name(username); + } } diff --git a/src/test/java/com/amazon/opendistroforelasticsearch/security/cache/DummyAuthenticationBackend.java b/src/test/java/com/amazon/opendistroforelasticsearch/security/cache/DummyAuthenticationBackend.java index 446b398817..f478dbea44 100644 --- a/src/test/java/com/amazon/opendistroforelasticsearch/security/cache/DummyAuthenticationBackend.java +++ b/src/test/java/com/amazon/opendistroforelasticsearch/security/cache/DummyAuthenticationBackend.java @@ -20,13 +20,12 @@ import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.common.settings.Settings; -import com.amazon.opendistroforelasticsearch.security.auth.AuthenticationBackend; -import com.amazon.opendistroforelasticsearch.security.auth.AuthorizationBackend; +import com.amazon.opendistroforelasticsearch.security.auth.SyncAuthenticationBackend; import com.amazon.opendistroforelasticsearch.security.user.AuthCredentials; import com.amazon.opendistroforelasticsearch.security.user.User; -public class DummyAuthenticationBackend implements AuthenticationBackend { +public class DummyAuthenticationBackend implements SyncAuthenticationBackend { private static volatile long authCount; private static volatile long existsCount; diff --git a/src/test/java/com/amazon/opendistroforelasticsearch/security/securityconf/impl/v7/ConfigV7Test.java b/src/test/java/com/amazon/opendistroforelasticsearch/security/securityconf/impl/v7/ConfigV7Test.java index 93df87b8f5..838f4d3598 100644 --- a/src/test/java/com/amazon/opendistroforelasticsearch/security/securityconf/impl/v7/ConfigV7Test.java +++ b/src/test/java/com/amazon/opendistroforelasticsearch/security/securityconf/impl/v7/ConfigV7Test.java @@ -1,6 +1,12 @@ package com.amazon.opendistroforelasticsearch.security.securityconf.impl.v7; +import com.amazon.opendistroforelasticsearch.security.dlic.rest.api.AbstractRestApiUnitTest; +import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants; +import com.amazon.opendistroforelasticsearch.security.test.helper.rest.RestHelper; import com.fasterxml.jackson.databind.JsonNode; +import org.apache.http.Header; +import org.apache.http.HttpStatus; +import org.elasticsearch.common.settings.Settings; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; @@ -11,7 +17,7 @@ import com.google.common.collect.ImmutableList; @RunWith(Parameterized.class) -public class ConfigV7Test { +public class ConfigV7Test extends AbstractRestApiUnitTest { private final boolean omitDefaults; @Parameterized.Parameters @@ -97,4 +103,29 @@ public void testKibana() throws Exception { assertEquals(kibana, DefaultObjectMapper.readTree(json)); assertEquals(kibana, DefaultObjectMapper.readValue(json, ConfigV7.Kibana.class)); } + + @Test + public void testConfigAuthTokenProvider() throws Exception { + Settings settings = Settings.builder().put(ConfigConstants.OPENDISTRO_SECURITY_UNSUPPORTED_RESTAPI_ALLOW_SECURITYCONFIG_MODIFICATION, true).build(); + setup(settings); + + rh.sendAdminCertificate = true; + + RestHelper.HttpResponse response = rh.executeGetRequest("/_opendistro/_security/api/securityconfig"); + Assert.assertTrue(!response.getBody().contains("auth_token_provider")); + + String authTokenProviderPayload = "{\"max_tokens_per_user\" : 100," + + " \"max_validity\" : \"1y\"," + + " \"jwt_signing_key_hs512\" : \"abc\"," + + " \"enabled\" : true}"; + + response = rh.executePatchRequest("/_opendistro/_security/api/securityconfig", + "[{\"op\": \"add\",\"path\": \"/config/dynamic/auth_token_provider\"," + + "\"value\": " + authTokenProviderPayload + "}]", new Header[0]); + + Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); + + response = rh.executeGetRequest("/_opendistro/_security/api/securityconfig"); + Assert.assertTrue(response.getBody().contains("auth_token_provider")); + } } diff --git a/src/test/java/com/amazon/opendistroforelasticsearch/security/user/AuthCredentialsTests.java b/src/test/java/com/amazon/opendistroforelasticsearch/security/user/AuthCredentialsTests.java index ad30e3051d..3ebcebcb52 100644 --- a/src/test/java/com/amazon/opendistroforelasticsearch/security/user/AuthCredentialsTests.java +++ b/src/test/java/com/amazon/opendistroforelasticsearch/security/user/AuthCredentialsTests.java @@ -3,6 +3,9 @@ import org.junit.Assert; import org.junit.Test; +import java.util.HashMap; +import java.util.Map; + public class AuthCredentialsTests { @Test public void testEquality() { @@ -29,4 +32,24 @@ public void testEquality() { new AuthCredentials("george", "secret".getBytes()), new AuthCredentials("george", "admin")); } + + @Test + public void testAuthCredentialsBuilder() { + AuthCredentials.Builder builder = AuthCredentials.forUser("test_user"); + builder.backendRoles("role1", "role2"); + + + Map claims = new HashMap<>(); + claims.put("sub", "test_user"); + claims.put("aud", "opendistro_security_authtoken"); + claims.put("jti", "some_random_identifier"); + + builder.claims(claims); + + AuthCredentials authCredentials = builder.authzComplete().build(); + + Assert.assertEquals(authCredentials.getBackendRoles().size(), 2); + Assert.assertEquals(authCredentials.getUsername(), "test_user"); + Assert.assertEquals(authCredentials.getClaims(), claims); + } } diff --git a/src/test/java/com/amazon/opendistroforelasticsearch/security/user/UserTests.java b/src/test/java/com/amazon/opendistroforelasticsearch/security/user/UserTests.java new file mode 100644 index 0000000000..4a3dd12fc4 --- /dev/null +++ b/src/test/java/com/amazon/opendistroforelasticsearch/security/user/UserTests.java @@ -0,0 +1,53 @@ +package com.amazon.opendistroforelasticsearch.security.user; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class UserTests { + + Set opendistroSecurityRoles = new HashSet(); + + @Test + public void testUsersCopy() { + opendistroSecurityRoles.add("role_1"); + + User user = new User("test_user", "auth_token", null, opendistroSecurityRoles, null, "some_random_id"); + + User copyUser = user.copy().build(); + + Assert.assertEquals(user.getName(), copyUser.getName()); + Assert.assertEquals(user.getRoles(), copyUser.getRoles()); + Assert.assertEquals(user.getOpenDistroSecurityRoles(), copyUser.getOpenDistroSecurityRoles()); + Assert.assertEquals(user.getType(), copyUser.getType()); + } + + @Test + public void testUserBuilder() { + User.Builder builder = new User.Builder(); + builder.openDistroSecurityRoles(opendistroSecurityRoles); + + opendistroSecurityRoles.add("role_1"); + builder.backendRoles(opendistroSecurityRoles); + + String userType = "auth_token"; + builder.type(userType); + + builder.name("auth_token_"); + + User user = builder.build(); + + Assert.assertEquals(user.getName(), "auth_token_"); + Assert.assertEquals(user.getType(), userType); + Assert.assertEquals(user.getOpenDistroSecurityRoles().size(), 0); + Assert.assertEquals(user.getRoles().size(), 1); + + + } + + +}