/* * Copyright (c) 2013 EMC Corporation * All Rights Reserved */ package com.emc.storageos.auth.impl; import com.emc.storageos.auth.*; import com.emc.storageos.coordinator.client.service.CoordinatorClient; import com.emc.storageos.db.client.DbClient; import com.emc.storageos.db.client.constraint.PrefixConstraint; import com.emc.storageos.db.client.model.AuthnProvider; import com.emc.storageos.db.client.model.StorageOSUserDAO; import com.emc.storageos.db.client.model.UserGroup; import com.emc.storageos.db.client.util.CustomQueryUtility; import com.emc.storageos.db.exceptions.DatabaseException; import com.emc.storageos.security.authorization.BasePermissionsHelper.UserMapping; import com.emc.storageos.security.exceptions.SecurityException; import com.emc.storageos.security.resource.UserInfoPage.UserDetails; import com.emc.storageos.svcs.errorhandling.resources.APIException; import com.emc.storageos.svcs.errorhandling.resources.BadRequestException; import org.apache.commons.httpclient.Credentials; import org.apache.commons.httpclient.UsernamePasswordCredentials; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.util.CollectionUtils; import java.net.URI; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Default Authentication manager implementation */ public class CustomAuthenticationManager implements AuthenticationManager { private final Logger _log = LoggerFactory.getLogger(CustomAuthenticationManager.class); private static final int _DEFAULT_UPDATE_CHECK_MINUTES = 10; private static final int _DEFAULT_UPDATE_RETRY_SECONDS = 2; private int _providerUpdateCheckMinutes = _DEFAULT_UPDATE_CHECK_MINUTES; private final int _providerUpdateRetrySeconds = _DEFAULT_UPDATE_RETRY_SECONDS; private ImmutableAuthenticationProviders _authNProviders; private DbClient _dbClient; private CoordinatorClient _coordinator; private AuthenticationProvider _localAuthenticationProvider; private final ProviderConfigUpdater _updaterRunnable = new ProviderConfigUpdater(); private LdapProviderMonitor _ldapProviderMonitor; @Autowired protected TokenManager _tokenManager; public CustomAuthenticationManager() { } public void setDbClient(DbClient dbClient) { _dbClient = dbClient; } public void setCoordinator(CoordinatorClient coordinator) { _coordinator = coordinator; } public void setProviderUpdateCheckMinutes(int providerUpdateCheckMinutes) { _providerUpdateCheckMinutes = providerUpdateCheckMinutes; } public void setTokenManager(TokenManager tokenManager) { _tokenManager = tokenManager; } @Override public StorageOSUserDAO authenticate(final Credentials credentials) { boolean found = false; String handlerName; for (AuthenticationProvider provider : getAuthenticationProviders()) { StorageOSAuthenticationHandler authenticationHandler = provider.getHandler(); StorageOSPersonAttributeDao attributeRepository = provider.getAttributeRepository(); if (!authenticationHandler.supports(credentials)) { continue; } found = true; handlerName = authenticationHandler.getClass().getName(); if (authenticationHandler.authenticate(credentials)) { _log.info("{} successfully authenticated {}", handlerName, logFormat(credentials)); final StorageOSUserDAO user = attributeRepository.getStorageOSUser(credentials); _log.info("Authenticated {}.", user); _log.debug("Attribute map for {}: {}", user, user.getAttributes()); return user; } _log.info("{} failed to authenticate {}", handlerName, logFormat(credentials)); } // failed authn if (found) { _log.error("Failed to authenticate {}", logFormat(credentials)); return null; } // we don't have a handler that supports the credentials given _log.error("Unsupported credentials {}", logFormat(credentials)); return null; } @Override public boolean isGroupValid(final String groupId, ValidationFailureReason[] failureReason) { boolean providerFound = false; if (isUserGroup(groupId)) { // If the domain component is empty consider this group // as a local userGroup. So, avoid the validation // of the group from the authnProvider. return isValidUserGroup(failureReason, groupId); } else { UsernamePasswordCredentials groupCreds = new UsernamePasswordCredentials(groupId, ""); for (AuthenticationProvider provider : getAuthenticationProviders()) { if (!provider.getHandler().supports(groupCreds)) { continue; } providerFound = true; _log.debug("Found auth handler: {}", provider.getHandler().getClass()); try { if (provider.getAttributeRepository().isGroupValid(groupId, failureReason)) { return true; } } catch (Exception e) { failureReason[0] = ValidationFailureReason.USER_OR_GROUP_NOT_FOUND_FOR_TENANT; _log.error("Exception validating group {}", groupId, e); } } } if (!providerFound) { _log.error( "Could not find an authentication provider that supports the group {}", groupId); } return false; } @Override public void validateUser(final String userId, final String tenantId, final String altTenantId) { UsernamePasswordCredentials creds = new UsernamePasswordCredentials(userId, ""); boolean providerFound = false; for (AuthenticationProvider provider : getAuthenticationProviders()) { if (!provider.getHandler().supports(creds)) { continue; } providerFound = true; provider.getAttributeRepository().validateUser(userId, tenantId, altTenantId); } if (!providerFound) { _log.error( "Could not find an authentication provider that supports the user {}", userId); throw APIException.badRequests.noAuthnProviderFound(userId); } } @Override public UserDetails getUserDetails(final String username) { UsernamePasswordCredentials creds = new UsernamePasswordCredentials(username, ""); for (AuthenticationProvider provider : getAuthenticationProviders()) { if (!provider.getHandler().supports(creds)) { continue; } ValidationFailureReason[] reason = new ValidationFailureReason[] { ValidationFailureReason.USER_OR_GROUP_NOT_FOUND_FOR_TENANT }; StorageOSUserDAO user = provider.getAttributeRepository().getStorageOSUser(creds, reason); if (user != null) { UserDetails userDetails = new UserDetails(); userDetails.setUsername(username); userDetails.getUserGroupList().addAll(user.getGroups()); userDetails.setTenant(user.getTenantId()); return userDetails; } else { switch (reason[0]) { case LDAP_CONNECTION_FAILED: throw SecurityException.fatals .communicationToLDAPResourceFailed(); case LDAP_MANAGER_AUTH_FAILED: throw SecurityException.fatals.ldapManagerAuthenticationFailed(); default: case USER_OR_GROUP_NOT_FOUND_FOR_TENANT: throw APIException.badRequests.principalSearchFailed(username); // LDAP_CANNOT_SEARCH_GROUP_IN_LDAP_MODE case is not needed here as // this is only user search. } } } throw APIException.badRequests.principalSearchFailed(username); } @Override public void refreshUser(String username) throws SecurityException, BadRequestException { UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(username, ""); for (AuthenticationProvider provider : getAuthenticationProviders()) { StorageOSAuthenticationHandler authenticationHandler = provider.getHandler(); if (!authenticationHandler.supports(credentials)) { continue; } List<StorageOSUserDAO> userDAOs = _tokenManager.getUserRecords(username); if (CollectionUtils.isEmpty(userDAOs)) { _log.error("user " + username + "does not exist in database"); throw APIException.badRequests.invalidParameter("username", username); } ValidationFailureReason[] failureReason = new ValidationFailureReason[] { ValidationFailureReason.USER_OR_GROUP_NOT_FOUND_FOR_TENANT }; StorageOSPersonAttributeDao attributeRepository = provider.getAttributeRepository(); final StorageOSUserDAO userDAO = attributeRepository.getStorageOSUser( credentials, failureReason); // if we had connection failures, then just throw exceptions and don't update // anything... if (userDAO == null && failureReason[0] == ValidationFailureReason.LDAP_CONNECTION_FAILED) { throw SecurityException.fatals.communicationToLDAPResourceFailed(); } else if (userDAO == null && failureReason[0] == ValidationFailureReason.LDAP_MANAGER_AUTH_FAILED) { throw SecurityException.fatals.ldapManagerAuthenticationFailed(); } else if (userDAO == null) { // we coudln't find the user, which means it's no longer valid, so we need // to logout the user _tokenManager.deleteAllTokensForUser(username, true); throw APIException.badRequests.principalSearchFailed(username); } // update the user records in the DB _tokenManager.updateDBWithUser(userDAO, userDAOs); return; } // we don't have a handler that supports the given credentials _log.error("Unsupported credentials {}", username); _tokenManager.deleteAllTokensForUser(username, true); // failed to refresh throw APIException.badRequests.principalSearchFailed(username); } protected List<AuthenticationProvider> getAuthenticationProviders() { return _authNProviders.getAuthenticationProviders(); } public void setLocalAuthenticationProvider(AuthenticationProvider localAuthenticationProvider) { _localAuthenticationProvider = localAuthenticationProvider; } /* * @see com.emc.storageos.auth.AuthenticationManager#getUserTenants(java.lang.String) */ @Override public Map<URI, UserMapping> getUserTenants(String username) { UsernamePasswordCredentials creds = new UsernamePasswordCredentials(username, ""); for (AuthenticationProvider provider : getAuthenticationProviders()) { if (!provider.getHandler().supports(creds)) { continue; } return provider.getAttributeRepository().getUserTenants(username); } return null; } @Override public Map<URI, UserMapping> peekUserTenants(String username, URI tenantUri, List<UserMapping> userMappings) { UsernamePasswordCredentials creds = new UsernamePasswordCredentials(username, ""); for (AuthenticationProvider provider : getAuthenticationProviders()) { if (!provider.getHandler().supports(creds)) { continue; } return provider.getAttributeRepository().peekUserTenants(username, tenantUri, userMappings); } return null; } /** * Return something useful to log from the credentials object * We don't want to just log credentials because it may contain * sensisitive information such as a password * * @param credentials the credentials that will be logged * @return the object to identify the credentials in log */ private Object logFormat(final Credentials credentials) { if (UsernamePasswordCredentials.class.isAssignableFrom(credentials.getClass())) { return ((UsernamePasswordCredentials) credentials).getUserName(); } else { return credentials.getClass(); } } /* * @see com.emc.storageos.auth.AuthenticationManager#init() */ @Override public void init() { // initialized the provider list to local only, called only from the bean init _authNProviders = ImmutableAuthenticationProviders.getInstance(_dbClient, _coordinator, _localAuthenticationProvider, null); // start the async reload thread Thread thread = new Thread(_updaterRunnable); thread.setName("ProviderConfigUpdater"); _updaterRunnable.wakeup(); thread.start(); _ldapProviderMonitor = new LdapProviderMonitor(_coordinator, _dbClient, _authNProviders); _ldapProviderMonitor.start(); } @Override public void shutdown() { _updaterRunnable.shutdown(); _updaterRunnable.wakeup(); _ldapProviderMonitor.stop(); } @Override public void reload() { _updaterRunnable.wakeup(); } /** * Class for reloading provider config from database when needed */ private class ProviderConfigUpdater implements Runnable { private final Logger _log = LoggerFactory.getLogger(ProviderConfigUpdater.class); private boolean _doRun = true; private Long _lastNotificationTime = 0L; private Long _lastReloadTime = 0L; private HashMap<URI, Long> _lastKnownConfiguration = null; private int _lastKnownLdapConnectionTimeout = 0; private class Waiter { private long _t = 0; public synchronized void sleep(long milliSeconds) { _t = System.currentTimeMillis() + milliSeconds; while (true) { final long dt = _t - System.currentTimeMillis(); if (dt <= 0) { return; } else { try { if (_lastNotificationTime <= _lastReloadTime) { wait(dt); } else { return; } } catch (InterruptedException e) { _log.info("ProviderConfigUpdater: waiter interrupted", e); } } } } public synchronized void wakeup() { _t = 0; notifyAll(); } } private final Waiter _waiter = new Waiter(); private void sleep(final long ms) { _waiter.sleep(ms); } public void wakeup() { _lastNotificationTime = System.currentTimeMillis(); _log.info("received notification to reload {}", _lastNotificationTime); _waiter.wakeup(); } public void shutdown() { _doRun = false; } // load provider list from db private List<AuthnProvider> getProvidersFromDB() { _log.debug("Reading authentication providers from the database"); List<URI> providerIds = _dbClient.queryByType(AuthnProvider.class, true); return _dbClient.queryObject(AuthnProvider.class, providerIds); } /** * checks to see if any of the provider configuration is updated from when we have seen it last * * @return true if it is updated, false otherwise * @throws DatabaseException */ private boolean checkForUpdates(List<AuthnProvider> providers) { if (_lastKnownConfiguration != null) { // compare to check if anything changed // check count if (_lastKnownConfiguration.keySet().size() != providers.size()) { return true; } // check they all match for (AuthnProvider provider : providers) { // we have seen this before if (!_lastKnownConfiguration.containsKey(provider.getId())) { return true; } // we have the same lastmodified timestamp if (!_lastKnownConfiguration.get(provider.getId()).equals(provider.getLastModified())) { return true; } } } else { if (!providers.isEmpty()) { return true; } } if (_lastKnownLdapConnectionTimeout != SystemPropertyUtil.getLdapConnectionTimeout(_coordinator)) { return true; } // nothing is changed return false; } /** * update out last known provider config information * * @param knownProviders */ private void updateLastKnown(List<AuthnProvider> knownProviders) { _lastKnownConfiguration = new HashMap<URI, Long>(); for (AuthnProvider provider : knownProviders) { _lastKnownConfiguration.put(provider.getId(), provider.getLastModified()); } _lastKnownLdapConnectionTimeout = SystemPropertyUtil.getLdapConnectionTimeout(_coordinator); } @Override public void run() { while (_doRun) { boolean bForceReload = (_lastNotificationTime > _lastReloadTime); _log.info("Starting authn provider config reload, lastNotificationTime = {}, lastReloadTime = {}", _lastNotificationTime, _lastReloadTime); try { long timeNow = System.currentTimeMillis(); List<AuthnProvider> providers = getProvidersFromDB(); if (!bForceReload) { // Its not a notified reload, check to see if anything changed if (checkForUpdates(providers)) { bForceReload = true; _log.debug("Provider configuration changed, reloading providers"); } else { _log.debug("Provider configuration not changed, skipping reload providers"); } } if (bForceReload) { _authNProviders = ImmutableAuthenticationProviders.getInstance(_dbClient, _coordinator, _localAuthenticationProvider, providers); _lastReloadTime = timeNow; updateLastKnown(providers); _ldapProviderMonitor.setAuthnProviders(_authNProviders); _log.info("Done authn provider config reload. lastReloadTime {}", _lastReloadTime); } // sleep and check for updates _log.debug("Next check will run in {} min", _providerUpdateCheckMinutes); sleep(_providerUpdateCheckMinutes * 60 * 1000); // An update notification came in ... run again immediately } catch (Exception e) { _log.error("Exception loading authentication provider configuration from db" + ", will a retry in {} secs", _providerUpdateRetrySeconds, e); // schedule a retry try { Thread.sleep(_providerUpdateRetrySeconds * 1000); } catch (Exception ignore) { _log.error("Got Exception in thread.sleep()", e); } } } } } /** * Check if the given groupId is a local user group or * not. This is done by checking if the groupId contains "@domain" * suffix or not. If it contains the "@domain" suffix that is considered * to be group in the authnprovider. * * @param groupId to be checked if it is local user group or not. * @return true if it is local user group otherwise false. */ private boolean isUserGroup(String groupId) { boolean isUserGroup = false; if (StringUtils.isNotBlank(groupId)) { String[] groupParts = groupId.split("@"); isUserGroup = (groupParts.length == 1); } return isUserGroup; } /** * Check the given group name matches the label of the active * user group in the db or not. * * @param failureReason to be returned. * @param group to be checked if there is a valid user group * available in the system that matches this label or not. * @return true if it is local user group found in the system * otherwise false. */ private boolean isValidUserGroup(ValidationFailureReason[] failureReason, String group) { List<UserGroup> objectList = CustomQueryUtility.queryActiveResourcesByConstraint(_dbClient, UserGroup.class, PrefixConstraint.Factory.getFullMatchConstraint(UserGroup.class, "label", group)); if (CollectionUtils.isEmpty(objectList)) { // null means Exception has been thrown and error logged already, empty means no group found in LDAP/AD _log.error("UserGroup {} is not present in DB", group); failureReason[0] = ValidationFailureReason.USER_OR_GROUP_NOT_FOUND_FOR_TENANT; return false; } else { _log.debug("UserGroup {} is valid", group); return true; } } }