/*
* Copyright (c) 2014 EMC Corporation
* All Rights Reserved
*/
package com.emc.storageos.security.geo;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import javax.crypto.SecretKey;
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.coordinator.common.Configuration;
import com.emc.storageos.coordinator.common.impl.ConfigurationImpl;
import com.emc.storageos.db.client.DbClient;
import com.emc.storageos.db.client.model.StorageOSUserDAO;
import com.emc.storageos.db.client.model.Token;
import com.emc.storageos.security.SerializerUtils;
import com.emc.storageos.security.authentication.CassandraTokenValidator;
import com.emc.storageos.security.authentication.TokenKeyGenerator.TokenKeysBundle;
import com.emc.storageos.security.authentication.TokenMaxLifeValuesHolder;
import com.emc.storageos.security.exceptions.SecurityException;
import com.emc.storageos.security.geo.TokenResponseBuilder.TokenResponseArtifacts;
import org.apache.curator.framework.recipes.locks.InterProcessLock;
/**
* This class provides utility methods to cache tokens, user records and token signature keys
*/
public class InterVDCTokenCacheHelper {
private static final Logger log = LoggerFactory.getLogger(InterVDCTokenCacheHelper.class);
private static final String FOREIGN_TOKEN_KEYS_BUNDLE_CONFIG = "ForeignTokenKeysBundleConfig";
private static final String FOREIGN_TOKEN_KEYS_BUNDLE_KEYID = "ForeignTokenKeysBundleKeyId";
private static final long MIN_TO_MSECS = 60 * 1000;
private static final String FOREIGN_TOKEN_BUNDLE_CONFIG_LOCK = "ForeignTokenConfigLock";
@Autowired
private CoordinatorClient coordinator;
@Autowired
private DbClient dbClient;
@Autowired
protected TokenMaxLifeValuesHolder maxLifeValuesHolder;
public void setCoordinator(CoordinatorClient c) {
this.coordinator = c;
}
public void setDbClient(DbClient client) {
this.dbClient = client;
}
public void setMaxLifeValuesHolder(TokenMaxLifeValuesHolder valueHolder) {
this.maxLifeValuesHolder = valueHolder;
}
/**
* saves token artifacts to the cache. The artifacts can be the token & user record, and token key ids.
* Token key ids (TokenKeyBundle) goes to zk. Token and user record goes to cassandra.
*
* @param artifacts
* @param vdcID
*/
public void cacheForeignTokenAndKeys(TokenResponseArtifacts artifacts, String vdcID) {
Token token = artifacts.getToken();
StorageOSUserDAO user = artifacts.getUser();
TokenKeysBundle bundle = artifacts.getTokenKeysBundle();
if (token != null && user != null) {
cacheForeignTokenArtifacts(token, user);
}
if (bundle != null) {
saveTokenKeysBundle(vdcID, bundle);
}
}
/**
* Saves the token and user dao records to the db. Set the cache expiration time
* to 10 minutes or time left on the token, whichever is sooner.
* Note: this method assumes validity of the token (expiration) has been checked
*
* @param t
* @param user
* @param now current time in minutes
*/
private synchronized void cacheForeignTokenArtifacts(final Token token, final StorageOSUserDAO user) {
long now = System.currentTimeMillis() / (MIN_TO_MSECS);
InterProcessLock tokenLock = null;
try {
tokenLock = coordinator.getLock(token.getId().toString());
if (tokenLock == null) {
log.error("Could not acquire lock for token caching");
throw SecurityException.fatals.couldNotAcquireLockTokenCaching();
}
tokenLock.acquire();
StorageOSUserDAO userToPersist = dbClient.queryObject(StorageOSUserDAO.class, user.getId());
userToPersist = (userToPersist == null) ? new StorageOSUserDAO() : userToPersist;
userToPersist.setAttributes(user.getAttributes());
userToPersist.setCreationTime(user.getCreationTime());
userToPersist.setDistinguishedName(user.getDistinguishedName());
userToPersist.setGroups(user.getGroups());
userToPersist.setId(user.getId());
userToPersist.setIsLocal(user.getIsLocal());
userToPersist.setTenantId(user.getTenantId());
userToPersist.setUserName(user.getUserName());
dbClient.persistObject(userToPersist);
Token tokenToPersist = dbClient.queryObject(Token.class, token.getId());
tokenToPersist = (tokenToPersist == null) ? new Token() : tokenToPersist;
if ((token.getExpirationTime() - now) > maxLifeValuesHolder.getForeignTokenCacheExpirationInMins()) {
tokenToPersist.setCacheExpirationTime(now + maxLifeValuesHolder.getForeignTokenCacheExpirationInMins());
} else {
tokenToPersist.setCacheExpirationTime(token.getExpirationTime());
}
tokenToPersist.setId(token.getId());
tokenToPersist.setUserId(user.getId()); // relative index, Id of the userDAO record
tokenToPersist.setIssuedTime(token.getIssuedTime());
tokenToPersist.setLastAccessTime(now);
tokenToPersist.setExpirationTime(token.getExpirationTime());
tokenToPersist.setIndexed(true);
tokenToPersist.setZoneId(token.getZoneId());
dbClient.persistObject(tokenToPersist);
log.info("Cached user {} and token", user.getUserName());
} catch (Exception ex) {
log.error("Could not acquire lock while trying to get a proxy token.", ex);
} finally {
try {
if (tokenLock != null) {
tokenLock.release();
}
} catch (Exception ex) {
log.error("Unable to release token caching lock", ex);
}
}
}
private static ConcurrentHashMap<String, TokenKeysBundle> foreignTokenKeysMap =
new ConcurrentHashMap<String, TokenKeysBundle>();
/**
* Reads local cache to find the key for the passed in vdcid. If not found in cache,
* causes a re-read from ZK and updates the cache.
*
* @param vdcID
* @param keyId
* @return
*/
public SecretKey getForeignSecretKey(String vdcID, String keyId) {
SecretKey toReturn = null;
// try to get the bundle from cached map
TokenKeysBundle bundle = foreignTokenKeysMap.get(vdcID);
if (bundle != null) {
toReturn = bundle.getKey(keyId);
}
// still nothing, try to reread from zk.
if (toReturn == null) {
try {
bundle = readTokenKeysBundle(vdcID);
} catch (Exception e) {
log.error("Exception while reading foreign key bundle for vdcid {}", vdcID);
}
if (bundle != null) {
toReturn = bundle.getKey(keyId);
}
}
return toReturn;
}
/**
* Retrieves all the cached TokenKeysBundles for VDCs from which tokens were borrowed
*
* @return VDCid->bundle map
*/
public HashMap<String, TokenKeysBundle> getAllCachedBundles() {
HashMap<String, TokenKeysBundle> bundleToReturn = new HashMap<String, TokenKeysBundle>();
Configuration config = coordinator.queryConfiguration(FOREIGN_TOKEN_KEYS_BUNDLE_CONFIG,
FOREIGN_TOKEN_KEYS_BUNDLE_KEYID);
if (config == null) {
log.info("No cached foreign token keys");
return bundleToReturn;
}
Map<String, String> bundles = config.getAllConfigs(true);
log.info("Key bundles retrieved: {}", bundles.size());
for (Entry<String, String> e : bundles.entrySet()) {
TokenKeysBundle bundle;
try {
bundle = (TokenKeysBundle) SerializerUtils.deserialize(e.getValue());
} catch (Exception ex) {
log.error("Could not deserialize token keys bundle", ex);
return null;
}
bundleToReturn.put(e.getKey(), bundle);
}
return bundleToReturn;
}
/**
* Checks if the requested id falls within reasonnable range of the currently
* known cached ids.
* Reasonnable means: not newer than the current key by more than 1 key rotation
* Not older than the previous key
*
* @param bundle the cached bundle to check against
* @param reqId the key id that came from the incoming token
* @return
*/
public boolean sanitizeRequestedKeyIds(TokenKeysBundle bundle, String reqId) {
int sz = bundle.getKeyEntries().size();
if (sz == 0) {
log.info("There is no cached bundle to compare against.");
return true;
}
String upperBound = sz == 1 ? null : bundle.getKeyEntries().get(1);
String lowerBound = bundle.getKeyEntries().get(0);
long lowKey = Long.parseLong(lowerBound);
long highKey = upperBound == null ? 0 : Long.parseLong(upperBound);
long requestedKeyId = Long.parseLong(reqId);
long rotationInterval = maxLifeValuesHolder.computeRotationTimeInMSecs();
// because there is a 20 minutes delay between key updates, we add a 20 minutes
// grace period
rotationInterval += CassandraTokenValidator.
FOREIGN_TOKEN_KEYS_BUNDLE_REFRESH_RATE_IN_MINS * 60 * 1000;
// case where there's no upper bound, we only have one bound to go by.
// We have to check that the requested key is not in the past and not
// more than one rotation in the future
if (upperBound == null) {
if (requestedKeyId < lowKey || requestedKeyId > lowKey + rotationInterval) {
log.error("One bound. Key id {} is not legitimate for query", requestedKeyId);
return false;
}
log.info("One bound. Key id {} is legitimate for query", requestedKeyId);
return true;
}
// case where there is a lower bound and upper bound. Here, we need
// to check that the requested key is not in the past, not between the bounds
// (this would be wrong because it should have matched one of the bounds)
// and no more than 1 rotation + 20 minutes in the future.
if (requestedKeyId < lowKey ||
(requestedKeyId > lowKey && requestedKeyId < highKey) ||
requestedKeyId > highKey + rotationInterval) {
log.error("Two bounds. Key id {} is not legitimate for query", requestedKeyId);
return false;
}
log.info("Two bounds. Key id {} is legitimate for query", requestedKeyId);
return true;
}
/**
* Gets the TokenKeysBundle for the given vdcid
*
* @param VDCid
* @return
*/
public TokenKeysBundle getTokenKeysBundle(String VDCid) {
return foreignTokenKeysMap.get(VDCid);
}
/**
* Reads from zk to get a TokenKeyBundle for the passed in vdc id.
* Updates the local cache map if found.
*
* @param vdcID
* @return
* @throws Exception
*/
private TokenKeysBundle readTokenKeysBundle(String vdcID) throws Exception {
Configuration config = coordinator.queryConfiguration(FOREIGN_TOKEN_KEYS_BUNDLE_CONFIG,
FOREIGN_TOKEN_KEYS_BUNDLE_KEYID);
if (config == null || config.getConfig(vdcID) == null) {
log.info("Foreign token keys bundle not found for vdcid {}", vdcID);
return null;
}
String serializedBundle = config.getConfig(vdcID);
log.debug("Got foreign token keys bundle from coordinator: {}", vdcID);
TokenKeysBundle bundle = (TokenKeysBundle) SerializerUtils.deserialize(serializedBundle);
foreignTokenKeysMap.put(vdcID, bundle);
return bundle;
}
/**
* Stores the token key bundle in cache (zookeeper path based on vdcid)
* Locks on vdcid for the write.
*
* @param vdcID
* @param bundle
*/
private synchronized void saveTokenKeysBundle(String vdcID, TokenKeysBundle bundle) {
InterProcessLock tokenBundleLock = null;
try {
tokenBundleLock = coordinator.getLock(FOREIGN_TOKEN_BUNDLE_CONFIG_LOCK);
if (tokenBundleLock == null) {
log.error("Could not acquire lock for tokenkeys bundle caching");
throw SecurityException.fatals.couldNotAcquireLockTokenCaching();
}
tokenBundleLock.acquire();
Configuration config = coordinator.queryConfiguration(FOREIGN_TOKEN_KEYS_BUNDLE_CONFIG,
FOREIGN_TOKEN_KEYS_BUNDLE_KEYID);
ConfigurationImpl configImpl = null;
if (config == null) {
configImpl = new ConfigurationImpl();
configImpl.setId(FOREIGN_TOKEN_KEYS_BUNDLE_KEYID);
configImpl.setKind(FOREIGN_TOKEN_KEYS_BUNDLE_CONFIG);
log.debug("Creating new foreign tokens config");
} else {
configImpl = (ConfigurationImpl) config;
log.debug("Updating existing foreign token config");
}
configImpl.setConfig(vdcID, SerializerUtils.serializeAsBase64EncodedString(bundle));
coordinator.persistServiceConfiguration(configImpl);
foreignTokenKeysMap.put(vdcID, bundle);
} catch (Exception ex) {
log.error("Could not acquire lock while trying to cache tokenkeys bundle.", ex);
} finally {
try {
if (tokenBundleLock != null) {
tokenBundleLock.release();
}
} catch (Exception ex) {
log.error("Unable to release token keys bundle caching lock", ex);
}
}
}
}