/**
* Copyright (C) 2014 - present by OpenGamma Inc. and the OpenGamma group of companies
*
* Please see distribution for license.
*/
package com.opengamma.bbg.permission;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.commons.lang.StringUtils;
import org.apache.shiro.authz.UnauthenticatedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.Lifecycle;
import org.threeten.bp.Duration;
import com.bloomberglp.blpapi.CorrelationID;
import com.bloomberglp.blpapi.Element;
import com.bloomberglp.blpapi.Event;
import com.bloomberglp.blpapi.Event.EventType;
import com.bloomberglp.blpapi.EventHandler;
import com.bloomberglp.blpapi.EventQueue;
import com.bloomberglp.blpapi.Identity;
import com.bloomberglp.blpapi.Message;
import com.bloomberglp.blpapi.Name;
import com.bloomberglp.blpapi.Request;
import com.bloomberglp.blpapi.Service;
import com.bloomberglp.blpapi.Session;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.UncheckedExecutionException;
import com.opengamma.bbg.BloombergConnector;
import com.opengamma.bbg.BloombergConstants;
import com.opengamma.bbg.BloombergPermissions;
import com.opengamma.bbg.SessionProvider;
import com.opengamma.core.id.ExternalSchemes;
import com.opengamma.provider.permission.PermissionCheckProvider;
import com.opengamma.provider.permission.PermissionCheckProviderRequest;
import com.opengamma.provider.permission.PermissionCheckProviderResult;
import com.opengamma.provider.permission.impl.AbstractPermissionCheckProvider;
import com.opengamma.util.ArgumentChecker;
/**
* Bloomberg B-PIPE permission/EID check provider.
* <p>
* This provider provides the service of checking whether a user has permission
* to access a piece of Bloomberg reference data.
* <p>
* In order to check permissions, Bloomberg requires an {@code Identity} object.
* The {@code Identity} is directly connected to Bloomberg and is updated as entitlements change.
* The provider request must contain the EMRS user id and IP address of a logged on BPS terminal.
* These are checked by Bloomberg when creating an {@code Identity}.
* Failure at this stage returns a result with an authentication error.
* <p>
* Once an {@code Identity} is obtained, the requested permissions are checked against it.
* Only EID permissions in the request are checked, other permissions are returned as denied.
* The EID is created and extracted using {@link BloombergPermissions}.
* Failure at this stage returns a result with an authorization error.
*/
public final class BloombergBpipePermissionCheckProvider
extends AbstractPermissionCheckProvider
implements PermissionCheckProvider, Lifecycle {
/** Logger. */
private static final Logger s_logger = LoggerFactory.getLogger(BloombergBpipePermissionCheckProvider.class);
private static final Name AUTHORIZATION_SUCCESS = Name.getName("AuthorizationSuccess");
private static final Name AUTHORIZATION_FAILURE = Name.getName("AuthorizationFailure");
private static final Name AUTHORIZATION_REVOKED = Name.getName("AuthorizationRevoked");
private static final Name ENTITITLEMENT_CHANGED = Name.getName("EntitlementChanged");
private static final int WAIT_TIME_MS = 10 * 1000; // 10 seconds
private static final Duration DEFAULT_IDENTITY_EXPIRY = Duration.ofHours(24);
private final LoadingCache<IdentityCacheKey, Identity> _userIdentityCache;
private final BloombergConnector _bloombergConnector;
private final AtomicBoolean _isRunning = new AtomicBoolean(false);
private volatile Session _session;
private volatile Service _apiAuthSvc;
private volatile Service _apiRefDataSvc;
/** Creates and manages the Bloomberg session and service. */
private final SessionProvider _sessionProvider;
/**
* Creates a bloomberg permission check provider with default identity expiry
*
* @param bloombergConnector the Bloomberg connector, not null
*/
public BloombergBpipePermissionCheckProvider(BloombergConnector bloombergConnector) {
this(bloombergConnector, DEFAULT_IDENTITY_EXPIRY);
}
/**
* Creates a bloomberg permission check provider
*
* @param bloombergConnector the Bloomberg connector, not null
* @param identityExpiry the identity expiry in hours, not null
*/
public BloombergBpipePermissionCheckProvider(BloombergConnector bloombergConnector, Duration identityExpiry) {
ArgumentChecker.notNull(bloombergConnector, "bloombergConnector");
ArgumentChecker.notNull(bloombergConnector.getSessionOptions(), "bloombergConnector.sessionOptions");
ArgumentChecker.isTrue(identityExpiry.getSeconds() > 0, "identityExpiry must be positive");
ArgumentChecker.isTrue(bloombergConnector.requiresAuthentication(), "authentication options must be set");
_userIdentityCache = createUserIdentityCache(identityExpiry);
_bloombergConnector = bloombergConnector;
List<String> serviceNames = Lists.newArrayList(
BloombergConstants.AUTH_SVC_NAME, BloombergConstants.REF_DATA_SVC_NAME);
SessionEventHandler eventHandler = new SessionEventHandler();
_sessionProvider = new SessionProvider(_bloombergConnector, serviceNames, eventHandler);
}
//-------------------------------------------------------------------------
@Override
public PermissionCheckProviderResult isPermitted(PermissionCheckProviderRequest request) {
ArgumentChecker.notNull(request, "request");
// validate
if (isRunning() == false) {
return PermissionCheckProviderResult.ofAuthenticationError(
"Bloomberg permission check found connection not running");
}
String emrsId = StringUtils.trimToNull(request.getUserIdBundle().getValue(ExternalSchemes.BLOOMBERG_EMRSID));
if (emrsId == null) {
return PermissionCheckProviderResult.ofAuthenticationError(
"Bloomberg permission check request did not contain an EMRS user ID");
}
if (request.getNetworkAddress() == null) {
return PermissionCheckProviderResult.ofAuthenticationError(
"Bloomberg permission check request did not contain a network address");
}
// obtain user identity
Identity userIdentity;
try {
userIdentity = _userIdentityCache.get(IdentityCacheKey.of(request.getNetworkAddress(), emrsId));
} catch (ExecutionException | UncheckedExecutionException ex) {
return processAuthenticationError(request, ex);
}
// check whether identity has the permissions
return checkPermissions(request, userIdentity);
}
// checks the requested permissions against the identity object
private PermissionCheckProviderResult checkPermissions(
PermissionCheckProviderRequest request, Identity userIdentity) {
try {
// evaluate permissions one by one to meet our API
Map<String, Boolean> result = new HashMap<>();
for (String permission : request.getRequestedPermissions()) {
if (BloombergPermissions.isEid(permission)) {
int eid = BloombergPermissions.extractEid(permission);
boolean permitted = userIdentity.hasEntitlements(new int[] {eid}, _apiRefDataSvc);
result.put(permission, permitted);
} else {
// permissions other than EID permissions are returned as false without error
result.put(permission, false);
}
}
return PermissionCheckProviderResult.of(result);
} catch (RuntimeException ex) {
String msg = String.format("Bloomberg authorization failure for user: %s IpAddress: %s",
request.getUserIdBundle(), request.getNetworkAddress());
s_logger.warn(msg, ex);
return PermissionCheckProviderResult.ofAuthorizationError(
"Bloomberg authorization error: " + ex.getCause().getClass().getName() + ": " + ex.getMessage());
}
}
// handles any errors during authentication
private PermissionCheckProviderResult processAuthenticationError(
PermissionCheckProviderRequest request, Exception ex) {
String msg = String.format("Bloomberg authentication failure for user: %s IpAddress: %s",
request.getUserIdBundle(), request.getNetworkAddress());
if (ex.getCause() == null) {
s_logger.warn(msg, ex);
return PermissionCheckProviderResult.ofAuthenticationError(
"Bloomberg authentication error: Unknown cause: " + ex.getMessage());
} else if (ex.getCause() instanceof UnauthenticatedException) {
s_logger.debug(msg);
return PermissionCheckProviderResult.ofAuthenticationError(
"Bloomberg authentication failed: " + ex.getCause().getMessage());
} else {
s_logger.warn(msg, ex.getCause());
return PermissionCheckProviderResult.ofAuthenticationError(
"Bloomberg authentication error: " + ex.getCause().getClass().getName() + ": " + ex.getMessage());
}
}
//-------------------------------------------------------------------------
/**
* Creates the loading cache of user identities.
* <p>
* The user identities are loaded by the cache when an entry is found to be missing.
* See {@link #loadUserIdentity(IdentityCacheKey)}.
*
* @param identityExpiry the duration before the identity expires
* @return the cache
*/
private LoadingCache<IdentityCacheKey, Identity> createUserIdentityCache(Duration identityExpiry) {
// called from constructor - must not use instance variables in this method
return CacheBuilder.newBuilder()
.expireAfterWrite(identityExpiry.getSeconds(), TimeUnit.SECONDS)
.build(new CacheLoader<IdentityCacheKey, Identity>() {
@Override
public Identity load(IdentityCacheKey userCredential) throws Exception {
return loadUserIdentity(userCredential);
}
});
}
// called from the cache to load user identities
private Identity loadUserIdentity(IdentityCacheKey userInfo) throws IOException, InterruptedException {
Request authRequest = _apiAuthSvc.createAuthorizationRequest();
authRequest.set("emrsId", userInfo.getUserId());
authRequest.set("ipAddress", userInfo.getIpAddress());
Identity userIdentity = _session.createIdentity();
s_logger.debug("Sending {}", authRequest);
EventQueue eventQueue = new EventQueue();
_session.sendAuthorizationRequest(authRequest, userIdentity, eventQueue, new CorrelationID(userInfo));
Event event = eventQueue.nextEvent(WAIT_TIME_MS);
// handle known responses to loading an identity ignoring other events
switch (event.eventType().intValue()) {
case EventType.Constants.RESPONSE:
case EventType.Constants.REQUEST_STATUS: {
for (Message message : event) {
if (AUTHORIZATION_SUCCESS.equals(message.messageType())) {
return userIdentity;
}
if (AUTHORIZATION_FAILURE.equals(message.messageType())) {
String failureMsg = "Unknown";
Element reasonElem = message.getElement("reason");
if (reasonElem != null) {
failureMsg = reasonElem.getElementAsString("message");
String failureCode = StringUtils.stripToNull(reasonElem.getElementAsString("code"));
if (failureCode != null) {
failureMsg = failureMsg + " (code " + failureCode + ")";
}
}
throw new UnauthenticatedException(
String.format("User: %s IpAddress: %s Reason: %s",
userInfo.getUserId(), userInfo.getIpAddress(), failureMsg));
}
}
}
default:
// no action on other event types
}
throw new UnauthenticatedException(
String.format("User: %s IpAddress: %s Reason: Unexpected response to authorization request",
userInfo.getUserId(), userInfo.getIpAddress()));
}
//-------------------------------------------------------------------------
/**
* Handler for events sent about users.
*/
private class SessionEventHandler implements EventHandler {
public void processEvent(Event event, Session session) {
switch (event.eventType().intValue()) {
case EventType.Constants.AUTHORIZATION_STATUS:
processAuthorizationEvent(event);
break;
default:
// no action on other event types
}
}
}
/**
* Processes events indicating changes in authorization.
*
* @param event the event, not null
*/
private void processAuthorizationEvent(Event event) {
for (Message msg : event) {
CorrelationID correlationId = msg.correlationID();
IdentityCacheKey userCredential = (IdentityCacheKey) correlationId.object();
if (AUTHORIZATION_REVOKED.equals(msg.messageType())) {
// the current Identity object has been revoked
// documentation says that this is the only reason to destroy the current cached Identity object
Element errorinfo = msg.getElement("reason");
int code = errorinfo.getElementAsInt32("code");
String reason = errorinfo.getElementAsString("message");
s_logger.debug("Authorization revoked for emrsid: {} with code: {} and reason {}",
userCredential.getUserId(), code, reason);
_userIdentityCache.invalidate(userCredential);
} else if (ENTITITLEMENT_CHANGED.equals(msg.messageType())) {
// the current Identity object will have been updated with new entitlements
// no need to replace the identity as any caching is internal to Identity
// if there are client side caches, they should be cleared at this point
s_logger.debug("Entitlements updated for emrsid: {}", userCredential.getUserId());
}
}
}
//-------------------------------------------------------------------------
@Override
public synchronized void start() {
if (!isRunning()) {
_sessionProvider.start();
_session = _sessionProvider.getSession();
_apiAuthSvc = _sessionProvider.getService(BloombergConstants.AUTH_SVC_NAME);
_apiRefDataSvc = _sessionProvider.getService(BloombergConstants.REF_DATA_SVC_NAME);
_isRunning.getAndSet(true);
}
}
@Override
public void stop() {
if (isRunning()) {
try {
_session.stop();
} catch (InterruptedException ex) {
Thread.interrupted();
s_logger.warn("Thread interrupted while trying to shut down bloomberg session");
}
}
}
@Override
public boolean isRunning() {
return _isRunning.get();
}
}