/*
* Copyright (c) 2013 EMC Corporation
* All Rights Reserved
*/
package com.emc.storageos.security.geo;
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.URIQueryResultList;
import com.emc.storageos.db.client.model.RequestedTokenMap;
import com.emc.storageos.db.client.model.VirtualDataCenter;
import com.emc.storageos.db.common.VdcUtil;
import com.emc.storageos.security.authentication.TokenEncoder;
import com.emc.storageos.security.exceptions.RetryableSecurityException;
import com.emc.storageos.security.exceptions.SecurityException;
import org.apache.curator.framework.recipes.locks.InterProcessLock;
import com.sun.jersey.api.client.ClientResponse;
/**
* Helper class to manage adding and removing VDC ids to the list of
* VDCs that borrowed a particular token.
* Also manages intervdc notifications of logged out tokens.
*/
public class RequestedTokenHelper {
private static final Logger log = LoggerFactory.getLogger(RequestedTokenHelper.class);
@Autowired
private DbClient dbClient;
@Autowired
private CoordinatorClient coordinator;
@Autowired
protected GeoClientCacheManager geoClientCacheMgt;
@Autowired
protected TokenEncoder tokenEncoder;
/**
* Sets the field called 'dbClient' to the given value.
*
* @param dbClient The dbClient to set.
*/
public void setDbClient(DbClient dbClient) {
this.dbClient = dbClient;
}
/**
* Sets the field called 'coordinator' to the given value.
*
* @param coordinator The coordinator to set.
*/
public void setCoordinator(CoordinatorClient coordinator) {
this.coordinator = coordinator;
}
private static final String TOKEN_MAP_LOCK_PREFIX = "TokenMap_%s";
public enum Operation {
ADD_VDC, REMOVE_VDC
};
/**
* Adds or removes a VDCid to the list of VDCids that requested the given token
*
* @param op ADD_VDC or REMOVE_VDC
* @param tokenId the token for which the map should be updated
* @param requestingVDC the short vdcid of the vdc to add or remove
*/
public void addOrRemoveRequestingVDC(Operation op, String tokenId, String requestingVDC) {
InterProcessLock tokenLock = null;
String lockName = String.format(TOKEN_MAP_LOCK_PREFIX, tokenId.toString());
try {
tokenLock = coordinator.getLock(lockName);
if (tokenLock == null) {
log.error("Could not acquire lock for requested token map update");
throw SecurityException.fatals.couldNotAcquireRequestedTokenMapCaching();
}
tokenLock.acquire();
if (op == Operation.ADD_VDC) {
addRequestingVDC(tokenId, requestingVDC);
} else {
removeRequestingVDC(tokenId, requestingVDC);
}
} catch (Exception ex) {
log.error("Could not acquire lock while trying to update requested token map.", ex);
} finally {
try {
if (tokenLock != null) {
tokenLock.release();
}
} catch (Exception ex) {
log.error("Unable to release requested token map lock", ex);
}
}
}
private void addRequestingVDC(String tokenId, String requestingVDC) {
RequestedTokenMap map = getTokenMap(tokenId);
if (map == null) {
map = new RequestedTokenMap();
map.setId(URIUtil.createId(RequestedTokenMap.class));
map.setTokenID(tokenId);
}
if (!map.getVDCIDs().contains(requestingVDC)) {
map.addVDCID(requestingVDC);
log.debug("Adding shortId {}", requestingVDC);
dbClient.persistObject(map);
}
}
private void removeRequestingVDC(String tokenId, String requestingVDC) {
RequestedTokenMap map = getTokenMap(tokenId);
if (map != null) {
if (map.getVDCIDs().contains(requestingVDC)) {
map.removeVDCID(requestingVDC);
if (map.getVDCIDs().isEmpty()) {
log.info("Last vdcid entry removed from requested token map. Removing map.");
dbClient.removeObject(map);
} else {
dbClient.persistObject(map);
}
}
}
}
/**
* Retrieves the list of vdcid that have requested a copy of this token
*
* @param tokenId
* @return
*/
public RequestedTokenMap getTokenMap(String tokenId) {
URIQueryResultList maps = new URIQueryResultList();
List<URI> mapsURI = new ArrayList<URI>();
dbClient.queryByConstraint(
AlternateIdConstraint.Factory.getRequestedTokenMapTokenIdConstraint(tokenId.toString()), maps);
if (maps == null) {
log.info("No requested token map found. No map.");
return null;
}
while (maps.iterator().hasNext()) {
mapsURI.add(maps.iterator().next());
}
List<RequestedTokenMap> objects = dbClient.queryObject(RequestedTokenMap.class, mapsURI);
if (objects == null || objects.size() != 1) {
log.info("No requested token map found. Empty map.");
return null;
}
return objects.get(0);
}
/**
* Notify the originatorVDC of the token or follows the map of VDCs that have a copy of this token
* depending on whether or not the passed in token is from this VDC or not.
*
* @param tokenId token URI for lookups in the requested token map
*/
public void notifyExternalVDCs(String rawToken) {
String tokenId = tokenEncoder.decode(rawToken).getTokenId().toString();
// If this is a token this VDC did not create, it needs to call back the
// originator
String originatorVDCId = URIUtil.parseVdcIdFromURI(tokenId);
if (!VdcUtil.getLocalShortVdcId().equals(originatorVDCId)) {
// Call originator. If this fails, this is a problem.
log.info("Calling token originator to propagate deletion of token");
boolean failed = false;
try {
ClientResponse resp = geoClientCacheMgt.getGeoClient(originatorVDCId).logoutToken(rawToken, null, false);
if (resp.getStatus() != ClientResponse.Status.OK.getStatusCode()) {
failed = true;
}
} catch (Exception ex) {
failed = true;
}
if (failed) {
throw RetryableSecurityException.retryables.unableToNotifyTokenOriginatorForLogout(originatorVDCId);
}
}
// Else, if this VDC created this token, go through the list of VDCs
// that may have a copy and notify them.
RequestedTokenMap map = getTokenMap(tokenId);
if (map == null || map.getVDCIDs().isEmpty()) {
return;
}
log.info("This token had potential copies still active in other VDCs. Notifying...");
for (String shortId : map.getVDCIDs()) {
try {
ClientResponse resp = geoClientCacheMgt.getGeoClient(shortId).logoutToken(rawToken, null, false);
// whether this succeeded or not, we remote the entry from the map. We log a warning,
// but the remote VDC will expire the copy of the token automatically in less than 10 minutes.
// The remove logout notification is a best effort attempt to remove the remote token quicker.
if (resp.getStatus() != ClientResponse.Status.OK.getStatusCode()) {
log.warn("Unable to successfully verify that remote copy of token was deleted. It will expire is less than 10 minutes.");
}
} catch (Exception e) {
log.error("Could not contact remote VDC to invalidate token: {}", shortId);
}
removeRequestingVDC(tokenId, shortId); // remove from the requested map whether logout success or not.
}
}
/**
* Iterates through the list of VDCs, skips the local vdc, and sends a logout?force=true request to
* each vdc found, with optionally the ?username= parameter if supplied.
*
* @param rawToken
* @param username optional
*/
public void broadcastLogoutForce(String rawToken, String username) {
List<URI> vdcIds = dbClient.queryByType(VirtualDataCenter.class, true);
Iterator<VirtualDataCenter> vdcIter = dbClient.queryIterativeObjects(VirtualDataCenter.class, vdcIds);
while (vdcIter.hasNext()) {
VirtualDataCenter vdc = vdcIter.next();
if (vdc.getShortId().equals(VdcUtil.getLocalShortVdcId())) {
log.info("Skipping local vdc. Already proceeded logout?force locally");
continue;
}
try {
ClientResponse resp = geoClientCacheMgt.getGeoClient(vdc.getShortId()).
logoutToken(rawToken, username, true);
} catch (Exception e) {
log.error("Could not contact remote VDC to invalidate token with force option: {}", vdc.getShortId());
}
}
}
}