/* * Copyright 2015 floragunn UG (haftungsbeschränkt) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.floragunn.searchguard.auth; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; import javax.naming.InvalidNameException; import javax.naming.ldap.LdapName; import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.cluster.ClusterService; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.logging.ESLogger; import org.elasticsearch.common.logging.Loggers; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.rest.BytesRestResponse; import org.elasticsearch.rest.RestChannel; import org.elasticsearch.rest.RestController; import org.elasticsearch.rest.RestRequest; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.transport.TransportChannel; import org.elasticsearch.transport.TransportRequest; import com.floragunn.searchguard.action.configupdate.TransportConfigUpdateAction; import com.floragunn.searchguard.auditlog.AuditLog; import com.floragunn.searchguard.auth.internal.InternalAuthenticationBackend; import com.floragunn.searchguard.auth.internal.NoOpAuthenticationBackend; import com.floragunn.searchguard.auth.internal.NoOpAuthorizationBackend; import com.floragunn.searchguard.configuration.AdminDNs; import com.floragunn.searchguard.configuration.ConfigChangeListener; import com.floragunn.searchguard.configuration.ConfigurationService; import com.floragunn.searchguard.filter.SearchGuardRestFilter; import com.floragunn.searchguard.http.HTTPBasicAuthenticator; import com.floragunn.searchguard.http.HTTPClientCertAuthenticator; import com.floragunn.searchguard.http.HTTPHostAuthenticator; import com.floragunn.searchguard.http.HTTPProxyAuthenticator; import com.floragunn.searchguard.http.XFFResolver; import com.floragunn.searchguard.support.ConfigConstants; import com.floragunn.searchguard.support.HTTPHelper; import com.floragunn.searchguard.support.LogHelper; import com.floragunn.searchguard.user.AuthCredentials; import com.floragunn.searchguard.user.User; import com.google.common.base.Strings; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.cache.RemovalListener; import com.google.common.cache.RemovalNotification; public class BackendRegistry implements ConfigChangeListener { protected final ESLogger log = Loggers.getLogger(this.getClass()); private final Map<String, String> authImplMap = new HashMap<String, String>(); private final SortedSet<AuthDomain> authDomains = new TreeSet<AuthDomain>(); private final Set<AuthorizationBackend> authorizers = new HashSet<AuthorizationBackend>(); private volatile boolean initialized; private final TransportConfigUpdateAction tcua; private final AdminDNs adminDns; private final XFFResolver xffResolver; private volatile boolean anonymousAuthEnabled = false; private final Settings esSettings; private final InternalAuthenticationBackend iab; private final AuditLog auditLog; private Cache<AuthCredentials, User> userCache = CacheBuilder.newBuilder() .expireAfterWrite(1, TimeUnit.HOURS) .removalListener(new RemovalListener<AuthCredentials, User>() { @Override public void onRemoval(RemovalNotification<AuthCredentials, User> notification) { log.debug("Clear user cache for {} due to {}", notification.getKey().getUsername(), notification.getCause()); } }).build(); private Cache<String, User> userCacheTransport = CacheBuilder.newBuilder() .expireAfterWrite(1, TimeUnit.HOURS) .removalListener(new RemovalListener<String, User>() { @Override public void onRemoval(RemovalNotification<String, User> notification) { log.debug("Clear user cache for {} due to {}", notification.getKey(), notification.getCause()); } }).build(); private Cache<AuthCredentials, User> authenticatedUserCacheTransport = CacheBuilder.newBuilder() .expireAfterWrite(1, TimeUnit.HOURS) .removalListener(new RemovalListener<AuthCredentials, User>() { @Override public void onRemoval(RemovalNotification<AuthCredentials, User> notification) { log.debug("Clear user cache for {} due to {}", notification.getKey().getUsername(), notification.getCause()); } }).build(); @Inject public BackendRegistry(final Settings settings, final RestController controller, final TransportConfigUpdateAction tcua, final ClusterService cse, final AdminDNs adminDns, final XFFResolver xffResolver, InternalAuthenticationBackend iab, AuditLog auditLog) { tcua.addConfigChangeListener(ConfigurationService.CONFIGNAME_CONFIG, this); controller.registerFilter(new SearchGuardRestFilter(this, auditLog)); this.tcua = tcua; this.adminDns = adminDns; this.esSettings = settings; this.xffResolver = xffResolver; this.iab = iab; this.auditLog = auditLog; authImplMap.put("intern_c", InternalAuthenticationBackend.class.getName()); authImplMap.put("intern_z", NoOpAuthorizationBackend.class.getName()); authImplMap.put("internal_c", InternalAuthenticationBackend.class.getName()); authImplMap.put("internal_z", NoOpAuthorizationBackend.class.getName()); authImplMap.put("noop_c", NoOpAuthenticationBackend.class.getName()); authImplMap.put("noop_z", NoOpAuthorizationBackend.class.getName()); authImplMap.put("ldap_c", "com.floragunn.dlic.auth.ldap.backend.LDAPAuthenticationBackend"); authImplMap.put("ldap_z", "com.floragunn.dlic.auth.ldap.backend.LDAPAuthorizationBackend"); authImplMap.put("basic_h", HTTPBasicAuthenticator.class.getName()); authImplMap.put("proxy_h", HTTPProxyAuthenticator.class.getName()); authImplMap.put("clientcert_h", HTTPClientCertAuthenticator.class.getName()); authImplMap.put("kerberos_h", "com.floragunn.dlic.auth.http.kerberos.HTTPSpnegoAuthenticator"); authImplMap.put("jwt_h", "com.floragunn.dlic.auth.http.jwt.HTTPJwtAuthenticator"); authImplMap.put("host_h", HTTPHostAuthenticator.class.getName()); } public void invalidateCache() { userCache.invalidateAll(); userCacheTransport.invalidateAll(); authenticatedUserCacheTransport.invalidateAll(); } private <T> T newInstance(final String clazzOrShortcut, String type, final Settings settings) throws ClassNotFoundException, NoSuchMethodException, SecurityException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { String clazz = clazzOrShortcut; if(authImplMap.containsKey(clazz+"_"+type)) { clazz = authImplMap.get(clazz+"_"+type); } final Class<T> t = (Class<T>) Class.forName(clazz); try { final Constructor<T> tctor = t.getConstructor(Settings.class); return tctor.newInstance(settings); } catch (final Exception e) { log.warn("Unable to create instance of class {} with (Settings.class) constructor due to {}", e, t, e.toString()); final Constructor<T> tctor = t.getConstructor(Settings.class, TransportConfigUpdateAction.class); return tctor.newInstance(settings, tcua); } } @Override public void onChange(final String event, final Settings settings) { authDomains.clear(); authorizers.clear(); anonymousAuthEnabled = settings.getAsBoolean("searchguard.dynamic.http.anonymous_auth_enabled", false); final Map<String, Settings> authzDyn = settings.getGroups("searchguard.dynamic.authz"); for (final String ad : authzDyn.keySet()) { final Settings ads = authzDyn.get(ad); if (ads.getAsBoolean("enabled", true)) { try { final AuthorizationBackend authorizationBackend = newInstance( ads.get("authorization_backend.type", "noop"),"z", Settings.builder().put(esSettings).put(ads.getAsSettings("authorization_backend.config")).build()); authorizers.add(authorizationBackend); } catch (final Exception e) { log.error("Unable to initialize AuthorizationBackend {} due to {}", e, ad, e.toString()); } } } final Map<String, Settings> dyn = settings.getGroups("searchguard.dynamic.authc"); for (final String ad : dyn.keySet()) { final Settings ads = dyn.get(ad); if (ads.getAsBoolean("enabled", true)) { try { AuthenticationBackend authenticationBackend; String authBackendClazz = ads.get("authentication_backend.type", InternalAuthenticationBackend.class.getName()); if(authBackendClazz.equals(InternalAuthenticationBackend.class.getName()) || authBackendClazz.equals("internal") || authBackendClazz.equals("intern")) { authenticationBackend = iab; } else { authenticationBackend = newInstance( authBackendClazz,"c", Settings.builder().put(esSettings).put(ads.getAsSettings("authentication_backend.config")).build()); } String httpAuthenticatorType = ads.get("http_authenticator.type"); //no default HTTPAuthenticator httpAuthenticator = httpAuthenticatorType==null?null: (HTTPAuthenticator) newInstance(httpAuthenticatorType,"h", Settings.builder().put(esSettings).put(ads.getAsSettings("http_authenticator.config")).build()); authDomains.add(new AuthDomain(authenticationBackend, httpAuthenticator, ads.getAsBoolean("http_authenticator.challenge", true), ads.getAsInt("order", 0))); } catch (final Exception e) { log.error("Unable to initialize auth domain {} due to {}", e, ad, e.toString()); } } } if(authDomains.isEmpty()) { authDomains.add(new AuthDomain(iab, new HTTPBasicAuthenticator(Settings.EMPTY), true, 0)); } initialized = true; } @Override public void validate(final String event, final Settings settings) throws ElasticsearchSecurityException { } public boolean authenticate(final TransportRequest request, final TransportChannel channel) throws ElasticsearchSecurityException { impersonate(request, channel); final User user = request.getFromContext(ConfigConstants.SG_USER); if(user == null) { return false; } if(adminDns.isAdmin(user.getName())) { auditLog.logAuthenticatedRequest(request, channel.action()); return true; } AuthCredentials _creds = null; final String authorizationHeader = request.getHeader("Authorization"); _creds = HTTPHelper.extractCredentials(authorizationHeader, log); final AuthCredentials creds = _creds; if(log.isDebugEnabled() && creds != null) { log.debug("User {} submitted also basic credentials: {}", user.getName(), creds); } for (final Iterator<AuthDomain> iterator = new TreeSet<AuthDomain>(authDomains).iterator(); iterator.hasNext();) { final AuthDomain authDomain = (AuthDomain) iterator.next(); User authenticatedUser = null; if(creds == null) { if(log.isDebugEnabled()) { log.debug("Transport User '{}' is in cache? {} (cache size: {})", user.getName(), userCacheTransport.getIfPresent(user.getName())!=null, userCacheTransport.size()); } try { authenticatedUser = userCacheTransport.get(user.getName(), new Callable<User>() { @Override public User call() throws Exception { if (log.isDebugEnabled()) { log.debug(user.getName() + " not cached, return from backend directly"); } if (authDomain.getBackend().exists(user)) { for (final AuthorizationBackend ab : authorizers) { // TODO transform username try { ab.fillRoles(user, new AuthCredentials(user.getName())); } catch (Exception e) { log.error("Problems retrieving roles for {} from {}", user, ab.getClass()); } } return user; } throw new Exception("no such user " + user.getName()); } }); } catch (Exception e) { log.error("Unexpected exception {} ", e, e.toString()); throw new ElasticsearchSecurityException(e.toString(), e); } } else { //auth if (log.isDebugEnabled()) { log.debug("Transport User '{}' is in cache? {} (cache size: {})", creds.getUsername(), authenticatedUserCacheTransport.getIfPresent(creds) != null, authenticatedUserCacheTransport.size()); } try { authenticatedUser = authenticatedUserCacheTransport.get(creds, new Callable<User>() { @Override public User call() throws Exception { if (log.isDebugEnabled()) { log.debug(creds.getUsername() + " not cached, return from backend directly"); } // full authentication User _user = authDomain.getBackend().authenticate(creds); for (final AuthorizationBackend ab : authorizers) { // TODO transform username try { ab.fillRoles(_user, new AuthCredentials(_user.getName())); } catch (Exception e) { log.error("Problems retrieving roles for {} from {}", _user, ab.getClass()); } } return _user; } }); } catch (Exception e) { log.error("Unexpected exception {} ", e, e.toString()); throw new ElasticsearchSecurityException(e.toString(), e); } finally { creds.clearSecrets(); } } try { if(authenticatedUser == null) { log.info("Cannot authenticate user (or add roles) with ad {} due to user is null, try next", authDomain.getOrder()); continue; } if(adminDns.isAdmin(authenticatedUser.getName())) { log.error("Cannot authenticate user because admin user is not permitted to login"); auditLog.logFailedLogin(authenticatedUser.getName(), request); return false; } //authenticatedUser.addRoles(ac.getBackendRoles()); if(log.isDebugEnabled()) { log.debug("User '{}' is authenticated", authenticatedUser); } request.putInContext(ConfigConstants.SG_USER, authenticatedUser); return true; } catch (final ElasticsearchSecurityException e) { log.info("Cannot authenticate user (or add roles) with ad {} due to {}, try next", authDomain.getOrder(), e.toString()); continue; } }//end for if(creds == null) { auditLog.logFailedLogin(user.getName(), request); } else { auditLog.logFailedLogin(creds.getUsername(), request); } return false; } /** * * @param request * @param channel * @return The authenticated user, null means another roundtrip * @throws ElasticsearchSecurityException */ public boolean authenticate(final RestRequest request, final RestChannel channel) throws ElasticsearchSecurityException { if(log.isTraceEnabled()) { log.trace(LogHelper.toString(request)); } String sslPrincipal = (String) request.getFromContext(ConfigConstants.SG_SSL_PRINCIPAL); if(adminDns.isAdmin(sslPrincipal)) { //PKI authenticated REST call request.putInContext(ConfigConstants.SG_USER, new User(sslPrincipal)); //auditLog.logAuthenticatedRequest(request); return true; } if (!isInitialized()) { log.error("Not yet initialized (you may need to run sgadmin)"); channel.sendResponse(new BytesRestResponse(RestStatus.SERVICE_UNAVAILABLE, "Search Guard not initialized (SG11). See https://github.com/floragunncom/search-guard-docs/blob/master/sgadmin.md")); return false; } request.putInContext(ConfigConstants.SG_REMOTE_ADDRESS, xffResolver.resolve(request)); boolean authenticated = false; User authenticatedUser = null; AuthCredentials authCredenetials = null; HTTPAuthenticator firstChallengingHttpAuthenticator = null; for (final Iterator<AuthDomain> iterator = new TreeSet<AuthDomain>(authDomains).iterator(); iterator.hasNext();) { final AuthDomain authDomain = iterator.next(); final HTTPAuthenticator httpAuthenticator = authDomain.getHttpAuthenticator(); if(httpAuthenticator == null) { continue; //this domain is for transport protocol only } if(authDomain.isChallenge() && firstChallengingHttpAuthenticator == null) { firstChallengingHttpAuthenticator = httpAuthenticator; } if(log.isDebugEnabled()) { log.debug("Try to extract auth creds from http {} ",httpAuthenticator.getType()); } final AuthCredentials ac; try { ac = httpAuthenticator.extractCredentials(request); } catch (Exception e1) { if(log.isDebugEnabled()) { log.debug("'{}' extracting credentials from {} authenticator", e1, httpAuthenticator.getType()); } continue; } authCredenetials = ac; if (ac == null) { //no credentials found in request if(anonymousAuthEnabled) { continue; } if(authDomain.isChallenge() && httpAuthenticator.reRequestAuthentication(channel, null)) { auditLog.logFailedLogin(null, request); return false; } else { //no reRequest possible continue; //log.debug("extraction authentication credentials from http request finally failed"); //channel.sendResponse(new BytesRestResponse(RestStatus.UNAUTHORIZED)); //return false; } } else if (!ac.isComplete()) { //credentials found in request but we need another client challenge if(httpAuthenticator.reRequestAuthentication(channel, ac)) { auditLog.logFailedLogin(ac.getUsername()+" <incomplete>", request); return false; } else { //no reRequest possible continue; //log.error(httpAuthenticator.getClass()+" does not support reRequestAuthentication but return incomplete authentication credentials"); //channel.sendResponse(new BytesRestResponse(RestStatus.UNAUTHORIZED)); //return false; } } ////credentials found in request and they are complete if(log.isDebugEnabled()) { log.debug("User '{}' is in cache? {} (cache size: {})", ac.getUsername(), userCache.getIfPresent(ac)!=null, userCache.size()); } try { try { authenticatedUser = userCache.get(ac, new Callable<User>() { @Override public User call() throws Exception { if(log.isDebugEnabled()) { log.debug(ac.getUsername()+" not cached, return from "+authDomain.getBackend().getType()+" backend directly"); } User authenticatedUser = authDomain.getBackend().authenticate(ac); for (final AuthorizationBackend ab : authorizers) { //TODO transform username try { ab.fillRoles(authenticatedUser, new AuthCredentials(authenticatedUser.getName())); } catch (Exception e) { log.error("Problems retrieving roles for {} from {}", authenticatedUser, ab.getClass()); } } //authDomain.getAbackend().fillRoles(authenticatedUser, new AuthCredentials(authenticatedUser.getName(), (Object) null)); return authenticatedUser; } }); } catch (Exception e) { //no audit log here, we catch this exception later log.error("Unexpected exception {} ", e, e.toString()); throw new ElasticsearchSecurityException(e.toString(), e); } finally { ac.clearSecrets(); } if(authenticatedUser == null) { log.info("Cannot authenticate user (or add roles) with ad {} due to user is null, try next", authDomain.getOrder()); continue; } if(adminDns.isAdmin(authenticatedUser.getName())) { log.error("Cannot authenticate user because admin user is not permitted to login via HTTP"); auditLog.logFailedLogin(authenticatedUser.getName(), request); channel.sendResponse(new BytesRestResponse(RestStatus.FORBIDDEN)); return false; } final String tenant = request.header("sg_tenant"); //authenticatedUser.addRoles(ac.getBackendRoles()); if(log.isDebugEnabled()) { log.debug("User '{}' is authenticated", authenticatedUser); log.debug("sg_tenant '{}'", tenant); } authenticatedUser.setRequestedTenant(tenant); request.putInContext(ConfigConstants.SG_USER, authenticatedUser); authenticated = true; break; } catch (final ElasticsearchSecurityException e) { log.info("Cannot authenticate user (or add roles) with ad {} due to {}, try next", authDomain.getOrder(), e.toString()); continue; } }//end for if(!authenticated) { //if(httpAuthenticator.reRequestAuthentication(channel, null)) { // return false; //} //no reRequest possible if(log.isDebugEnabled()) { log.debug("User not authenticated after checking {} auth domains", authDomains.size()); } if(authCredenetials == null && anonymousAuthEnabled) { request.putInContext(ConfigConstants.SG_USER, User.ANONYMOUS); if(log.isDebugEnabled()) { log.debug("Anonymous User is authenticated"); } return true; } if(firstChallengingHttpAuthenticator != null) { if(log.isDebugEnabled()) { log.debug("Rerequest with {}", firstChallengingHttpAuthenticator.getClass()); } if(firstChallengingHttpAuthenticator.reRequestAuthentication(channel, null)) { if(log.isDebugEnabled()) { log.debug("Rerequest {} failed", firstChallengingHttpAuthenticator.getClass()); } auditLog.logFailedLogin(authCredenetials == null ? null:authCredenetials.getUsername(), request); return false; } } if(log.isDebugEnabled()) { log.debug("Authentication finally failed"); } auditLog.logFailedLogin(authCredenetials == null ? null:authCredenetials.getUsername(), request); channel.sendResponse(new BytesRestResponse(RestStatus.UNAUTHORIZED)); return false; } return authenticated; } @Override public boolean isInitialized() { return initialized; } private boolean impersonate(final TransportRequest tr, final TransportChannel channel) throws ElasticsearchSecurityException { final String impersonatedUser = tr.getHeader("sg_impersonate_as"); if(Strings.isNullOrEmpty(impersonatedUser)) { return false; //nothing to do } if (!isInitialized()) { throw new ElasticsearchSecurityException("Could not check for impersonation because Search Guard is not yet initialized"); } final User origPKIuser = tr.getFromContext(ConfigConstants.SG_USER); if (origPKIuser == null) { throw new ElasticsearchSecurityException("no original PKI user found"); } User aU = origPKIuser; if (adminDns.isAdmin(impersonatedUser)) { throw new ElasticsearchSecurityException("'"+origPKIuser.getName() + "' is not allowed to impersonate as an adminuser '" + impersonatedUser+"'"); } try { if (impersonatedUser != null && !adminDns.isImpersonationAllowed(new LdapName(origPKIuser.getName()), impersonatedUser)) { throw new ElasticsearchSecurityException("'"+origPKIuser.getName() + "' is not allowed to impersonate as '" + impersonatedUser+"'"); } else if (impersonatedUser != null) { aU = new User(impersonatedUser); if(log.isDebugEnabled()) { log.debug("Impersonate from '{}' to '{}'",origPKIuser.getName(), impersonatedUser); } auditLog.logAuthenticatedRequest(tr, channel.action()); } } catch (final InvalidNameException e1) { throw new ElasticsearchSecurityException("PKI does not have a valid name ('" + origPKIuser.getName() + "'), should never happen", e1); } tr.putInContext(ConfigConstants.SG_USER, Objects.requireNonNull((User) aU)); return true; } }