/* * Copyright (c) 2013 EMC Corporation * All Rights Reserved */ package com.emc.storageos.security.authentication; import java.net.URI; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import com.emc.storageos.coordinator.client.service.CoordinatorClient; import com.emc.storageos.db.client.DbClient; import com.emc.storageos.db.client.URIUtil; import com.emc.storageos.db.client.constraint.AlternateIdConstraint; import com.emc.storageos.db.client.constraint.ContainmentConstraint; import com.emc.storageos.db.client.constraint.URIQueryResultList; import com.emc.storageos.db.client.model.BaseToken; import com.emc.storageos.db.client.model.ProxyToken; import com.emc.storageos.db.client.model.StorageOSUserDAO; import com.emc.storageos.db.client.model.Token; import com.emc.storageos.db.common.VdcUtil; import com.emc.storageos.db.exceptions.DatabaseException; import com.emc.storageos.geomodel.TokenResponse; import com.emc.storageos.security.geo.GeoClientCacheManager; import com.emc.storageos.security.geo.InterVDCTokenCacheHelper; import com.emc.storageos.security.geo.TokenResponseBuilder; import com.emc.storageos.security.geo.TokenResponseBuilder.TokenResponseArtifacts; import com.emc.storageos.security.validator.Validator; import com.emc.storageos.svcs.errorhandling.resources.APIException; import org.springframework.util.CollectionUtils; /** * Cassandra based implementation of the TokenValidator interface * Token record has: rowid || fields | alternateId ( rowid of a StorageOSUserDAO record) * StorageOSUserDAO record has: rowid || fields | alternatId (username) */ public class CassandraTokenValidator implements TokenValidator { public static final int FOREIGN_TOKEN_KEYS_BUNDLE_REFRESH_RATE_IN_MINS = 20; private static final Logger _log = LoggerFactory.getLogger(CassandraTokenValidator.class); @Autowired protected TokenMaxLifeValuesHolder _maxLifeValuesHolder; protected static final long MIN_TO_MSECS = 60 * 1000; @Autowired protected DbClient _dbClient; @Autowired protected TokenEncoder _tokenEncoder; @Autowired protected CoordinatorClient _coordinator; @Autowired protected InterVDCTokenCacheHelper interVDCTokenCacheHelper; @Autowired protected GeoClientCacheManager geoClientCacheMgt; /** * Setter for coordinator client. Needed for testing. Otherwise * gets autowired. * * @param c */ public void setCoordinator(CoordinatorClient c) { _coordinator = c; } public void setTokenEncoder(TokenEncoder e) { _tokenEncoder = e; } public void setDbClient(DbClient dbclient) { _dbClient = dbclient; } public void setTokenMaxLifeValuesHolder(TokenMaxLifeValuesHolder holder) { _maxLifeValuesHolder = holder; } public void setInterVDCTokenCacheHelper(InterVDCTokenCacheHelper helper) { interVDCTokenCacheHelper = helper; } /** * get current time in minutes * * @return */ public static long getCurrentTimeInMins() { return System.currentTimeMillis() / (MIN_TO_MSECS); } /** * Get all user DAO records from DB for a given user * * @param userName * @return List of StorageOSUserDAO */ public List<StorageOSUserDAO> getUserRecords(String userName) { URIQueryResultList users = new URIQueryResultList(); _dbClient.queryByConstraint( AlternateIdConstraint.Factory.getUserIdsByUserName(userName), users); List<URI> userURIs = new ArrayList<URI>(); for (Iterator<URI> it = users.iterator(); it.hasNext();) { userURIs.add(it.next()); } return _dbClient.queryObject(StorageOSUserDAO.class, userURIs); } /** * Get all tickets referring to a given User DAO record ID * * @param userId * @param tokenTypeProxy: true if looking for proxy tokens. False for regular tokens * @return */ private List<URI> getTokensForUserId(URI userId, boolean tokenTypeProxy) { URIQueryResultList tokens = new URIQueryResultList(); List<URI> tokenURIs = new ArrayList<URI>(); if (!tokenTypeProxy) { _dbClient.queryByConstraint( ContainmentConstraint.Factory.getUserIdTokenConstraint(userId), tokens); } else { _dbClient.queryByConstraint( ContainmentConstraint.Factory.getUserIdProxyTokenConstraint(userId), tokens); } for (Iterator<URI> it = tokens.iterator(); it.hasNext();) { tokenURIs.add(it.next()); } return tokenURIs; } /** * Get proxy tokens based on a username * * @param username * @return the proxy token for that user if it exists. */ protected ProxyToken getProxyTokenForUserName(String username) { URIQueryResultList tokens = new URIQueryResultList(); _dbClient.queryByConstraint(AlternateIdConstraint .Factory.getProxyTokenUserNameConstraint(username), tokens); List<URI> uris = new ArrayList<URI>(); for (Iterator<URI> it = tokens.iterator(); it.hasNext();) { uris.add(it.next()); } List<ProxyToken> toReturn = _dbClient.queryObject(ProxyToken.class, uris); if (CollectionUtils.isEmpty(toReturn)) { _log.info("No proxy token found for user {}", username); return null; } return toReturn.get(0); } /** * Returns all regular tokens for User id * * @param userId * @return */ protected List<Token> getTokensForUserId(URI userId) { List<URI> tokenURIs = getTokensForUserId(userId, false); return _dbClient.queryObject(Token.class, tokenURIs); } /** * Returns all proxy tokens for User id * * @param userId * @return */ protected List<ProxyToken> getProxyTokensForUserId(URI userId) { List<URI> tokenURIs = getTokensForUserId(userId, true); return _dbClient.queryObject(ProxyToken.class, tokenURIs); } /** * Delete the given token from db, if this is last token referring the userDAO, * and there are no proxy token associated, mark the userDAO for deletion * * @param token */ protected void deleteTokenInternal(Token token) { URI userId = token.getUserId(); _dbClient.removeObject(token); List<Token> tokens = getTokensForUserId(userId); List<ProxyToken> pTokens = getProxyTokensForUserId(userId); if (CollectionUtils.isEmpty(tokens) && CollectionUtils.isEmpty(pTokens)) { _log.info("There are no more tokens referring to the user id {}, marking it inactive"); StorageOSUserDAO userDAO = _dbClient.queryObject(StorageOSUserDAO.class, userId); _dbClient.markForDeletion(userDAO); } } /** * Queries the remote VDC for token and userdao objects * * @param tw TokenOnWire object * @param rawToken the rawToken to send to the remote vdc * @return */ private StorageOSUserDAO getForeignToken(TokenOnWire tw, String rawToken) { StorageOSUserDAO userFromCache = this.foreignTokenCacheLookup(tw); if (userFromCache != null) { return userFromCache; } try { String shortVDCid = URIUtil.parseVdcIdFromURI(tw.getTokenId()); TokenResponse response = geoClientCacheMgt.getGeoClient(shortVDCid). getToken(rawToken, null, null); if (response != null) { TokenResponseArtifacts artifacts = TokenResponseBuilder.parseTokenResponse(response); _log.info("Got username for foreign token: {}", artifacts.getUser().getUserName()); _log.debug("Got token object: {}", artifacts.getToken().getId().toString()); interVDCTokenCacheHelper.cacheForeignTokenAndKeys(artifacts, shortVDCid); return artifacts.getUser(); } else { _log.error("Null response from getForeignToken call. It's possible remote vdc is not reachable."); } } catch (Exception e) { _log.error("Could not validate foreign token ", e); } return null; } /** * Validate a token. If valid, return the corresponding user record. * The passed in token, is an id to the record in the db, is used to query the object from db * The userId field in token object is used to create the user information. * * If the token is a foreign token, the cache will be queried and a call to the remote VDC * will be made if the cache did not produce the desired token. */ @Override public StorageOSUserDAO validateToken(String tokenIn) { if (tokenIn == null) { _log.error("token is null"); return null; } TokenOnWire tw = _tokenEncoder.decode(tokenIn); String vdcId = URIUtil.parseVdcIdFromURI(tw.getTokenId()); // If this isn't our token, go get it from the remote vdc if (vdcId != null && !tw.isProxyToken() && !VdcUtil.getLocalShortVdcId().equals(vdcId)) { return getForeignToken(tw, tokenIn); } return resolveUser(fetchTokenLocal(tw)); } /** * Looks in the cache for token/user record. Returns null if not found or found but cache expired * * @param tw * @return user record */ private StorageOSUserDAO foreignTokenCacheLookup(TokenOnWire tw) { BaseToken bToken = fetchTokenLocal(tw); if (bToken == null || !Token.class.isInstance(bToken)) { _log.info("Token: no hit from cache"); return null; } Token token = (Token) bToken; Long expirationTime = token.getCacheExpirationTime(); if (expirationTime != null && expirationTime > getCurrentTimeInMins()) { StorageOSUserDAO user = resolveUser(token); _log.info("Got user from cached token: {}", user != null ? user.getUserName() : "no hit from cache"); return user; } _log.info("Cache expired for foreign token {}", token.getId()); return null; } /** * Check to see if the token passed in is still usable, if not, delete it from db and return false * if still usable, update the lastAccessTime * * @param tokenObj Token object * @param updateLastAccess if true, will update the last accessed timestamp if needed * @return True if the token is good, False otherwise */ protected boolean checkExpiration(Token tokenObj, boolean updateLastAccess) { if (!tokenObj.getInactive()) { long timeNow = getCurrentTimeInMins(); long timeLastAccess = tokenObj.getLastAccessTime(); long timeIdleTimeExpiry = timeLastAccess + (_maxLifeValuesHolder.getMaxTokenIdleTimeInMins()) + (_maxLifeValuesHolder.getTokenIdleTimeGraceInMins()); if (tokenObj.getExpirationTime() != null && timeIdleTimeExpiry > timeNow && tokenObj.getExpirationTime() > timeNow) { // update Last access time, if we haven't in the last TOKEN_IDLE_TIME_GRACE_IN_MINS // this will save us some extra db writes if (updateLastAccess) { long nextLastAccessUpdate = timeLastAccess + (_maxLifeValuesHolder.getTokenIdleTimeGraceInMins()); if (nextLastAccessUpdate <= timeNow) { tokenObj.setLastAccessTime(timeNow); try { _dbClient.persistObject(tokenObj); } catch (DatabaseException ex) { _log.error("failed updating last access time for token {}", tokenObj.getId()); } } } return true; } _log.debug("token expired: {}, now {}, lastAccess {}, idle expiry {}, expiry {}", new String[] { tokenObj.getId().toString(), "" + timeNow, "" + tokenObj.getLastAccessTime(), "" + timeIdleTimeExpiry, "" + tokenObj.getExpirationTime() }); } // we are here because token is either expired or inactive, // remove the token and return false try { deleteTokenInternal(tokenObj); } catch (DatabaseException ex) { _log.error("exception deleting token {}", tokenObj.getId(), ex); } return false; } /** * Fetches a token without consideration for cache expiration */ @Override public BaseToken verifyToken(String tokenIn) { if (tokenIn == null) { _log.error("token is null"); return null; } TokenOnWire tw = _tokenEncoder.decode(tokenIn); return this.fetchTokenLocal(tw); } /** * Retrieves a token and checks expiration * * @param tw * @return */ private BaseToken fetchTokenLocal(TokenOnWire tw) { BaseToken verificationToken = null; URI tkId = tw.getTokenId(); if (!tw.isProxyToken()) { verificationToken = _dbClient.queryObject(Token.class, tkId); if (null != verificationToken && !checkExpiration(((Token) verificationToken), true)) { _log.warn("Token found in database but is expired: {}", verificationToken.getId()); return null; } } else { verificationToken = _dbClient.queryObject(ProxyToken.class, tkId); if (null != verificationToken && !checkExpiration((ProxyToken) verificationToken)) { _log.warn("ProxyToken found in database but is expired: {}", verificationToken.getId()); return null; } } if (verificationToken == null) { _log.error("Could not find token with id {} for validation", tkId); } return verificationToken; } /** * Gets a userDAO record from a token or proxytoken */ @Override public StorageOSUserDAO resolveUser(BaseToken token) { if (token == null) { return null; } URI userId = null; // Skip expiration verification for proxy tokens. // verify it is still valid, if not remove it from db and send back null boolean isProxy = token instanceof ProxyToken; if (isProxy) { userId = ((ProxyToken) token).peekLastKnownId(); } else { userId = ((Token) token).getUserId(); } StorageOSUserDAO userDAO = _dbClient.queryObject(StorageOSUserDAO.class, userId); if (userDAO == null) { _log.error("No user record found or userId: {}", userId.toString()); return null; } return userDAO; } /* * Check to see if the proxy token passed in is still usable, if not, delete it from * db and return false. * * @param tokenObj * ProxyToken object * * @return True if the token is good, False otherwise */ protected boolean checkExpiration(ProxyToken tokenObj) { if (tokenObj.getInactive()) { return false; } long timeNow = getCurrentTimeInMins(); Long lastValidatedTime = tokenObj.getLastValidatedTime(); // if this is a proxy token from before v2, then this might be null. New proxy // tokens should have this value set. if (lastValidatedTime == null) { lastValidatedTime = timeNow - _maxLifeValuesHolder.getMaxTokenLifeTimeInMins(); } long lastValidationTimeExpiry = lastValidatedTime + _maxLifeValuesHolder.getMaxTokenLifeTimeInMins(); if (lastValidationTimeExpiry <= timeNow) { try { Validator.refreshUser(tokenObj.getUserName()); } catch (APIException e) { // LDAP could not find the user _log.error(e.getMessage(), e); return false; } catch (Exception e) { _log.error(e.getMessage(), e); return true; } tokenObj.setLastValidatedTime(timeNow); _dbClient.persistObject(tokenObj); } return true; } }