/*
* Copyright (c) 2013 EMC Corporation
* All Rights Reserved
*/
package com.emc.storageos.auth.impl;
import java.net.URI;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import com.emc.storageos.auth.TokenManager;
import com.emc.storageos.db.client.URIUtil;
import com.emc.storageos.db.client.constraint.DecommissionedConstraint;
import com.emc.storageos.db.client.constraint.URIQueryResultList;
import com.emc.storageos.db.client.model.ProxyToken;
import com.emc.storageos.db.client.model.RequestedTokenMap;
import com.emc.storageos.db.client.model.StorageOSUserDAO;
import com.emc.storageos.db.client.model.Token;
import com.emc.storageos.db.exceptions.DatabaseException;
import com.emc.storageos.geomodel.TokenResponse;
import com.emc.storageos.security.authentication.CassandraTokenValidator;
import com.emc.storageos.security.geo.RequestedTokenHelper;
import com.emc.storageos.security.authentication.TokenKeyGenerator.TokenKeysBundle;
import com.emc.storageos.security.authentication.TokenOnWire;
import com.emc.storageos.security.exceptions.SecurityException;
import com.emc.storageos.security.geo.TokenResponseBuilder;
import com.emc.storageos.security.geo.TokenResponseBuilder.TokenResponseArtifacts;
import com.emc.storageos.svcs.errorhandling.resources.APIException;
import com.netflix.astyanax.util.TimeUUIDUtils;
import org.apache.curator.framework.recipes.locks.InterProcessLock;
/**
* Cassandra based implementation of the token manager interface.
*
*/
public class CassandraTokenManager extends CassandraTokenValidator implements TokenManager {
// warning: setting these two values to higher ones will increase
// the memory footprint on dbsvc and authsvc
private static final int DEFAULT_MAX_TOKENS_PER_USERID = 100;
private static final int DEFAULT_MAX_TOKENS_FOR_PROXYUSER = 1000;
private int _maxTokensPerUserId = DEFAULT_MAX_TOKENS_PER_USERID;
private int _maxTokensForProxyUser = DEFAULT_MAX_TOKENS_FOR_PROXYUSER;
static final String PROXY_USER = "proxyuser";
private static final Logger _log = LoggerFactory.getLogger(CassandraTokenManager.class);
private static final double TOKEN_WARNING_EIGHTY_PERCENT = 0.8;
@Autowired
private RequestedTokenHelper tokenMapHelper;
public void setTokenMapHelper(RequestedTokenHelper tokenMapHelper) {
this.tokenMapHelper = tokenMapHelper;
}
/**
* Sets the maximum number of auth tokens for any user (except proxyuser)
* which can exist in the system at any given time
*
* @param max The maxTokensPerUserid to set.
* warning: increasing this value from the default will increase the memory
* footprint on dbsvc and authsvc
*/
public void setMaxTokensPerUserId(int max) {
_maxTokensPerUserId = max;
}
/**
* Sets the maximum number of auth tokens for the proxyuser
* which can exist in the system at any given time
*
* @param max The maxTokensForProxyuser to set.
* warning: increasing this value from the default will increase the memory
* footprint on dbsvc and authsvc
*/
public void setMaxTokensForProxyUser(int max) {
_maxTokensForProxyUser = max;
}
/**
* Executor for our house keeping tasks (deleting old tokens, and updating token signature keys
*/
private final ScheduledExecutorService _tokenMgmtExecutor = Executors.newScheduledThreadPool(2);
private class TokenKeysUpdater implements Runnable {
@Override
public void run() {
// get all cached VDCid-TokenKeysBundle pairs (there is one entry for each from which a
// token has been borrowed at least once
HashMap<String, TokenKeysBundle> bundles = interVDCTokenCacheHelper.getAllCachedBundles();
for (Entry<String, TokenKeysBundle> tokenKeyBndlEntry : bundles.entrySet()) {
_log.info("Cached token key bundle for VDC found: {}. Calling VDC to update...", tokenKeyBndlEntry.getKey());
try {
// call geo's getToken with no token, and either one or two key ids, depending on what we
// have in the cached bundle (if keys have never been rotated, there would be only one key id
TokenResponse response = geoClientCacheMgt.getGeoClient(tokenKeyBndlEntry.getKey())
.getToken(null,
tokenKeyBndlEntry.getValue().getKeyEntries().get(0),
tokenKeyBndlEntry.getValue().getKeyEntries().size() == 2 ?
tokenKeyBndlEntry.getValue().getKeyEntries().get(1) : null);
if (response != null) {
TokenResponseArtifacts artifacts = TokenResponseBuilder.parseTokenResponse(response);
if (artifacts.getTokenKeysBundle() != null) {
_log.info("Remote VDC sent new keys for {}. Caching them...", tokenKeyBndlEntry.getKey());
interVDCTokenCacheHelper.cacheForeignTokenAndKeys(artifacts, tokenKeyBndlEntry.getKey());
} else {
_log.info("No updated key bundles from remote VDC {}. Keys are up to date", tokenKeyBndlEntry.getKey());
}
} else {
_log.error("Null response when trying to get updated keys. It's possible remote vdc is not reachable.");
}
} catch (Exception ex) {
_log.error("Could not update token keys", ex);
}
}
_log.info("Done running the key refresh thread. There were {} cached VDC keys bundles", bundles.size());
}
}
private class ExpiredTokenCleaner implements Runnable {
final static String TOKEN_CLEANER_LOCK = "token_cleaner";
final static long MIN_TO_MICROSECS = 60 * 1000 * 1000;
// get tokens older than idle time from index
private URIQueryResultList getOldTokens() {
URIQueryResultList list = new URIQueryResultList();
long timeStartMarker = TimeUUIDUtils.getMicrosTimeFromUUID(TimeUUIDUtils.getUniqueTimeUUIDinMicros())
- (_maxLifeValuesHolder.getMaxTokenIdleTimeInMins() * MIN_TO_MICROSECS);
_dbClient.queryByConstraint(
DecommissionedConstraint.Factory.getDecommissionedObjectsConstraint(
Token.class, "indexed", timeStartMarker), list);
return list;
}
@Override
public void run() {
InterProcessLock lock = null;
try {
_log.info("Starting token cleanup executor ...");
lock = _coordinator.getLock(TOKEN_CLEANER_LOCK);
lock.acquire();
_log.info("Got token cleaner lock ...");
int deletedCount = 0;
try {
List<URI> tokens = getOldTokens();
Iterator<Token> tokenIterator = _dbClient.queryIterativeObjects(Token.class, tokens);
while (tokenIterator.hasNext()) {
Token tokenObj = tokenIterator.next();
if (!checkExpiration(tokenObj, false)) {
deletedCount++;
cleanUpRequestedTokenMap(tokenObj);
// TODO:
// Streamline the following code paths: Right now:
// 1 - AuthenticationResource.logout() calls deleteToken() which calls deleteTokenInternal(), then
// notifyExternalVDCs which
// still needs the map.
// 2 - checkExpiration calls deleteTokenInternal(). So if we put cleanUpRequestedTokenMap() in
// deleteTokenInternal(),
// it would make notifyExternalVDCs miss notifications in the #1 case above.
// * For this reason, cleanUpRequestedTokenMap is being called separately in the various paths.
// * look to see if this can be done more cleanly (moving cleanupRTkMap to somewhere common between the various
// places
// where token(s) are getting deleted)
}
}
} catch (DatabaseException ex) {
_log.error("DatabaseException in token cleanup executor: ", ex);
}
_log.info("Done token cleanup executor, deleted {} tokens", deletedCount);
} catch (Exception e) {
_log.warn("Unexpected exception during db maintenance", e);
} finally {
if (lock != null) {
try {
lock.release();
} catch (Exception e) {
_log.warn("Unexpected exception unlocking repair lock", e);
}
}
}
}
}
/**
* Removes the RequestedTokenMap associated with the passed in token if it exists.
*
* @param tokenObj
*/
private void cleanUpRequestedTokenMap(Token tokenObj) {
RequestedTokenMap map = tokenMapHelper.getTokenMap(tokenObj.getId().toString());
if (map != null) {
_dbClient.removeObject(map);
_log.info("A token had a stale RequestedTokenMap. Deleting.");
} else {
_log.info("No RequestedTokenMap for token to be deleted.");
}
}
/**
* initializer, startup the background expired token deletion thread
* and key updater thread (no op unless multi vdc)
*/
public void init() {
// Token cleaner thread
_tokenMgmtExecutor.scheduleWithFixedDelay(
new ExpiredTokenCleaner(), 1, _maxLifeValuesHolder.getMaxTokenIdleTimeInMins(), TimeUnit.MINUTES);
// Token keys updater thread
_tokenMgmtExecutor.scheduleWithFixedDelay( // update every 20 minutes
new TokenKeysUpdater(), 1, FOREIGN_TOKEN_KEYS_BUNDLE_REFRESH_RATE_IN_MINS, TimeUnit.MINUTES);
}
/**
* run the cleanup now
*/
public void runCleanupNow() {
new ExpiredTokenCleaner().run();
}
/**
* create a new token with the user info
*
* @param user
* @return
*/
private Token createNewToken(StorageOSUserDAO user) {
Token token = new Token();
token.setId(URIUtil.createId(Token.class));
token.setUserId(user.getId()); // relative index, Id of the userDAO record
long timeNow = getCurrentTimeInMins();
token.setIssuedTime(timeNow);
token.setLastAccessTime(timeNow);
token.setExpirationTime(timeNow + (_maxLifeValuesHolder.getMaxTokenLifeTimeInMins()));
token.setIndexed(true);
_dbClient.persistObject(token);
return token;
}
/**
* Persist/Update the StorageOSUserDAO record
* generates a new token or reuses an existing token.
*
* @return token as a String
*/
@Override
public String getToken(StorageOSUserDAO userDAO) {
try {
// always use lower case username for comparing/saving to db
userDAO.setUserName(userDAO.getUserName().toLowerCase());
// find an active user record, if there is one with an active token
List<StorageOSUserDAO> userRecords = getUserRecords(userDAO.getUserName());
StorageOSUserDAO user = updateDBWithUser(userDAO, userRecords);
// do we have a user account to use?
if (user == null) {
// No, create one
userDAO.setId(URIUtil.createId(StorageOSUserDAO.class));
_dbClient.persistObject(userDAO);
user = userDAO;
} else {
// check count
List<Token> tokensForUserId = getTokensForUserId(user.getId());
int maxTokens = user.getUserName().equalsIgnoreCase(PROXY_USER) ?
_maxTokensForProxyUser : _maxTokensPerUserId;
double alertTokensSize = (maxTokens * TOKEN_WARNING_EIGHTY_PERCENT);
if (tokensForUserId.size() >= maxTokens) {
throw APIException.unauthorized.maxNumberOfTokenExceededForUser();
} else if (tokensForUserId.size() == (int) alertTokensSize) {
_log.warn("Prior to creating new token, user {} had {} tokens.",
user.getUserName(), tokensForUserId.size());
}
}
return _tokenEncoder.encode(TokenOnWire.createTokenOnWire(createNewToken(user)));
} catch (DatabaseException ex) {
_log.error("Exception while persisting user information {}", userDAO.getUserName(), ex);
} catch (SecurityException e) {
_log.error("Token encoding exception. ", e);
}
return null;
}
/**
* Gets a proxy token for the given user
* If a proxy token for the given user already exists, it will be reused
*
* @return proxy-token
*/
@Override
public String getProxyToken(StorageOSUserDAO userDAO) {
InterProcessLock userLock = null;
try {
userLock = _coordinator.getLock(userDAO.getUserName());
if (userLock == null) {
_log.error("Could not acquire lock for user: {}", userDAO.getUserName());
throw SecurityException.fatals.couldNotAcquireLockForUser(userDAO.getUserName());
}
userLock.acquire();
// Look for proxy tokens based on that username.
// If any is found, use that. Else, create a new one.
ProxyToken proxyToken = getProxyTokenForUserName(userDAO.getUserName());
if (proxyToken != null) {
_log.debug("Found proxy token {} for user {}. Reusing...", proxyToken.getId(), userDAO.getUserName());
return _tokenEncoder.encode(TokenOnWire.createTokenOnWire(proxyToken));
}
// No proxy token found for this user. Create a new one.
// Create the actual proxy token
ProxyToken pToken = new ProxyToken();
pToken.setId(URIUtil.createId(ProxyToken.class));
pToken.addKnownId(userDAO.getId());
pToken.setUserName(userDAO.getUserName());
pToken.setZoneId("zone1"); // for now
pToken.setIssuedTime(getCurrentTimeInMins());
pToken.setLastValidatedTime(getCurrentTimeInMins());
_dbClient.persistObject(pToken);
return _tokenEncoder.encode(TokenOnWire.createTokenOnWire(pToken));
} catch (DatabaseException ex) {
_log.error("DatabaseException while persisting proxy token", ex);
} catch (SecurityException ex) {
_log.error("Proxy Token encoding exception. ", ex);
} catch (Exception ex) {
_log.error("Could not acquire lock while trying to get a proxy token.", ex);
} finally {
try {
if (userLock != null) {
userLock.release();
}
} catch (Exception ex) {
_log.error("Unable to release proxytoken creation lock", ex);
}
}
return null;
}
/**
* Remove token from database if valid.
*/
@Override
public void deleteToken(String tokenIn) {
try {
if (tokenIn == null) {
_log.error("Null token passed for deletion");
return;
}
URI tkId = _tokenEncoder.decode(tokenIn).getTokenId();
Token verificationToken = _dbClient.queryObject(Token.class, tkId);
if (verificationToken == null) {
_log.error("Could not fetch token from the database: {}", tkId);
return;
}
deleteTokenInternal(verificationToken);
} catch (DatabaseException ex) {
throw SecurityException.fatals.databseExceptionDuringTokenDeletion(tokenIn,
ex);
} catch (SecurityException e) {
_log.error("Token decoding exception during deleteToken.", e);
}
}
/**
* Delete all tokens belonging to the user and mark all the user records for this user for deletion.
*/
@Override
public void deleteAllTokensForUser(String userName, boolean includeProxyTokens) {
try {
List<StorageOSUserDAO> userRecords = getUserRecords(userName.toLowerCase());
for (StorageOSUserDAO userRecord : userRecords) {
List<Token> tokensToDelete = getTokensForUserId(userRecord.getId());
for (Token token : tokensToDelete) {
_log.info("Removing token {} using userDAO {} for username {}",
new String[] { token.getId().toString(), userRecord.getId().toString(), userName });
_dbClient.removeObject(token);
cleanUpRequestedTokenMap(token);
}
// making proxy token deletion optional
List<ProxyToken> pTokensToDelete = getProxyTokensForUserId(userRecord.getId());
if (includeProxyTokens) {
for (ProxyToken token : pTokensToDelete) {
_log.info("Removing proxy token {} using userDAO {} for username {}",
new String[] { token.getId().toString(), userRecord.getId().toString(), userName });
_dbClient.removeObject(token);
}
_log.info("Marking for deletion: user record {} for username {}",
userRecord.getId().toString(), userName);
_dbClient.markForDeletion(userRecord);
} else if (pTokensToDelete.isEmpty()) {
_log.info("No proxy tokens found. Marking for deletion: user record {} for username {}",
userRecord.getId().toString(), userName);
_dbClient.markForDeletion(userRecord);
}
}
} catch (DatabaseException ex) {
throw SecurityException.fatals.exceptionDuringTokenDeletionForUser(userName,
ex);
}
}
@Override
public StorageOSUserDAO updateDBWithUser(final StorageOSUserDAO userDAO,
final List<StorageOSUserDAO> userRecords) {
StorageOSUserDAO user = null;
for (StorageOSUserDAO record : userRecords) {
if (!record.getInactive()) {
// update the record, most of the cases this is a NO-OP
// because user info does not change much
record.updateFrom(userDAO);
user = record;
_dbClient.persistObject(record);
}
}
return user;
}
@Override
public int getMaxTokenLifeTimeInSecs() {
return _maxLifeValuesHolder.getMaxTokenLifeTimeInMins() * 60;
}
}