/*
* Copyright (c) Members of the EGEE Collaboration. 2006-2010.
* See http://www.eu-egee.org/partners/ for details on the copyright holders.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.glite.authz.pep.server;
import java.io.IOException;
import java.util.Iterator;
import java.util.List;
import net.jcip.annotations.ThreadSafe;
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.store.MemoryStoreEvictionPolicy;
import org.glite.authz.common.context.DecisionRequestContext;
import org.glite.authz.common.context.DecisionRequestContextHelper;
import org.glite.authz.common.logging.LoggingConstants;
import org.glite.authz.common.model.Request;
import org.glite.authz.common.model.Response;
import org.glite.authz.common.model.Result;
import org.glite.authz.common.model.Status;
import org.glite.authz.common.model.StatusCode;
import org.glite.authz.common.model.util.XACMLConverter;
import org.glite.authz.pep.obligation.ObligationProcessingException;
import org.glite.authz.pep.pip.PIPProcessingException;
import org.glite.authz.pep.pip.PolicyInformationPoint;
import org.glite.authz.pep.server.config.PEPDaemonConfiguration;
import org.opensaml.Configuration;
import org.opensaml.saml2.core.Assertion;
import org.opensaml.saml2.core.Statement;
import org.opensaml.ws.soap.client.SOAPFaultException;
import org.opensaml.ws.soap.common.SOAPException;
import org.opensaml.ws.soap.soap11.Envelope;
import org.opensaml.xacml.ctx.RequestType;
import org.opensaml.xacml.ctx.StatusCodeType;
import org.opensaml.xacml.profile.saml.XACMLAuthzDecisionStatementType;
import org.opensaml.xml.XMLObject;
import org.opensaml.xml.io.MarshallingException;
import org.opensaml.xml.security.SecurityException;
import org.opensaml.xml.util.XMLHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Element;
/** Handles an incoming daemon {@link Request}. */
@ThreadSafe
public class PEPDaemonRequestHandler {
/** Name of the cache used to cache PDP responses. */
public static final String RESPONSE_CACHE_NAME= "org.glite.authz.pep.server.responseCache";
/** Class logger. */
private final Logger log= LoggerFactory.getLogger(PEPDaemonRequestHandler.class);
/** Audit log. */
private final Logger auditLog= LoggerFactory.getLogger(LoggingConstants.AUDIT_CATEGORY);
/** Protocol message log. */
private final Logger protocolLog= LoggerFactory.getLogger(LoggingConstants.PROTOCOL_MESSAGE_CATEGORY);
/** The daemon's configuration. */
private PEPDaemonConfiguration daemonConfig;
/** Cache used to store response to a request. */
private Cache responseCache;
/**
* Constructor.
*
* @param config
* the constructor for the daemon
*/
public PEPDaemonRequestHandler(final PEPDaemonConfiguration config) {
if (config == null) {
throw new IllegalArgumentException("Daemon configuration may not be null");
}
daemonConfig= config;
if (daemonConfig.getMaxCachedResponses() > 0) {
CacheManager cacheMgr= CacheManager.create();
responseCache= new Cache(RESPONSE_CACHE_NAME,
daemonConfig.getMaxCachedResponses(),
MemoryStoreEvictionPolicy.LFU,
false,
null,
false,
daemonConfig.getCachedResponseTTL(),
daemonConfig.getCachedResponseTTL(),
false,
Long.MAX_VALUE,
null,
null);
cacheMgr.addCache(responseCache);
}
else {
responseCache= null;
}
}
/**
* Handles a PEP think client request. The request is deserialized from the
* input stream and then converted into a {@link RequestType}. The request
* is sent to a PDP with each registered PDP being tried in turn until one
* accepts the incoming connection. The
* {@link org.opensaml.xacml.ctx.ResponseType} from the PDP is then turned
* in to a {@link Response}, serialized and then written out.
*
* @param request
* the request to be evaluated
*
* @return the response to the given request
*
* @throws IOException
* thrown if there is an error writing a response to the output
* stream
*/
public Response handle(Request request) throws IOException {
daemonConfig.getServiceMetrics().incrementTotalServiceRequests();
DecisionRequestContext messageContext= DecisionRequestContextHelper.buildMessageContext(daemonConfig.getEntityId());
Response response= null;
try {
// run the policy information points over the request
for (PolicyInformationPoint pip : daemonConfig.getPolicyInformationPoints()) {
if (pip.populateRequest(request)) {
log.debug("Applied PIP {} to Hessian request", pip.getId());
}
else {
log.debug("PIP {} did not apply to this request",
pip.getId());
}
}
protocolLog.info("Hessian request after PIPs have been run\n{}",
request.toString());
// check to see if we have a cached response, if not, make the
// request to the PDP
if (responseCache != null) {
log.debug("Checking if a response has already been cached for this request");
net.sf.ehcache.Element cacheElement= responseCache.get(request);
if (cacheElement != null) {
response= (Response) cacheElement.getValue();
if (response != null) {
log.debug("Cached response found, using it");
return response;
}
}
}
log.debug("Response not found in cache");
// no cached response so make request to PDP and cache the result
response= sendRequestToPDP(messageContext, request);
if (response == null) {
log.debug("No response received from registered PDPs");
daemonConfig.getServiceMetrics().incrementTotalServiceRequestErrors();
response= buildErrorResponse(request,
StatusCodeType.SC_PROCESSING_ERROR,
null);
return response;
}
Result result= response.getResults().get(0);
// cache Deny/Permit decisions
if (responseCache != null
&& (result.getDecision() == Result.DECISION_DENY || result.getDecision() == Result.DECISION_PERMIT)) {
log.debug("Caching response {} for request {}",
messageContext.getInboundMessageId(),
messageContext.getOutboundMessageId());
responseCache.put(new net.sf.ehcache.Element(request, response));
}
// run obligations handlers over the response
if (daemonConfig.getObligationService() != null) {
log.debug("Processing obligations");
daemonConfig.getObligationService().processObligations(request,
result);
}
} catch (PIPProcessingException e) {
daemonConfig.getServiceMetrics().incrementTotalServiceRequestErrors();
log.error("Error processing policy information points: "
+ e.getMessage());
log.debug("", e);
response= buildErrorResponse(request,
StatusCodeType.SC_PROCESSING_ERROR,
e.getMessage());
} catch (ObligationProcessingException e) {
daemonConfig.getServiceMetrics().incrementTotalServiceRequestErrors();
log.error("Error processing obligation handlers: " + e.getMessage());
log.debug("", e);
response= buildErrorResponse(request,
StatusCodeType.SC_PROCESSING_ERROR,
e.getMessage());
} catch (Exception e) {
daemonConfig.getServiceMetrics().incrementTotalServiceRequestErrors();
log.error("Error processing authorization request: "
+ e.getMessage());
log.debug("", e);
response= buildErrorResponse(request,
StatusCodeType.SC_PROCESSING_ERROR,
null);
} finally {
protocolLog.info("Complete hessian response\n{}",
response.toString());
}
writeAuditLogEntry(messageContext);
return response;
}
/**
* Attempts to send the SOAP request. This method attempts to send the
* request to each registered PDP endpoint until one endpoint responses with
* an HTTP 200 status code. If PDP returns a 200 then null is returned,
* indicating that the response could not be sent to any PDP.
*
* @param messageContext
* current request context
* @param authzRequest
* the authorization request to be sent
*
* @return the returned response
*/
private Response sendRequestToPDP(DecisionRequestContext messageContext,
Request authzRequest) {
RequestType xacmlRequest= XACMLConverter.requestToXACML(authzRequest);
Envelope soapRequest= DecisionRequestContextHelper.buildSOAPMessage(daemonConfig.getEntityId(),
messageContext,
xacmlRequest);
logSOAPProtocolMessage(soapRequest, true);
Iterator<String> pdpItr= daemonConfig.getPDPEndpoints().iterator();
String pdpEndpoint= null;
Response authzResponse= null;
while (pdpItr.hasNext()) {
try {
pdpEndpoint= pdpItr.next();
log.debug("Sending request {} to {}",
messageContext.getOutboundMessageId(),
pdpEndpoint);
daemonConfig.getSOAPClient().send(pdpEndpoint, messageContext);
authzResponse= extractResponse(messageContext,
pdpEndpoint,
(Envelope) messageContext.getInboundMessage());
if (authzResponse != null) {
logSOAPProtocolMessage(messageContext.getInboundMessage(),
false);
messageContext.setRespondingPDP(pdpEndpoint);
messageContext.setAuthorizationDecision(authzResponse.getResults().get(0).getDecisionString());
break;
}
} catch (SOAPFaultException e) {
log.warn("Recieved SOAP Fault " + e.getFault().getCode()
+ " from PDP endpoint: " + pdpEndpoint, e);
} catch (SOAPException e) {
log.error("Error sending request to PDP endpoint "
+ pdpEndpoint, e);
} catch (SecurityException e) {
log.error("Response from PDP endpoint " + pdpEndpoint
+ " did not meet message security requirements", e);
}
}
if (authzResponse != null) {
log.debug("A decision of {} was reached by {} in response to request {}",
new Object[] {
authzResponse.getResults().get(0).getDecisionString(),
messageContext.getRespondingPDP(),
messageContext.getOutboundMessageId(), });
return authzResponse;
}
else {
log.error("No PDP endpoint was able to answer the authorization request");
return null;
}
}
/**
* Extracts the response from a PDP response. If more than one assertion is
* present
*
* @param messageContext
* current request context
* @param pdpEndpoint
* the endpoint to which the message should be sent
* @param soapResponse
* the SOAP response containing the XACML-SAML authorization
* response
*
* @return the extract response
*/
private Response extractResponse(DecisionRequestContext messageContext,
String pdpEndpoint, Envelope soapResponse) {
org.opensaml.saml2.core.Response samlResponse= (org.opensaml.saml2.core.Response) soapResponse.getBody().getOrderedChildren().get(0);
if (samlResponse.getAssertions() == null
|| samlResponse.getAssertions().isEmpty()) {
log.warn("Response from PDP {} was an invalid message. It did not contain an assertion",
pdpEndpoint);
return null;
}
if (samlResponse.getAssertions().size() > 1) {
log.warn("Response from PDP {} was an invalid message. It contained more than 1 assertion",
pdpEndpoint);
return null;
}
Assertion samlAssertion= samlResponse.getAssertions().get(0);
List<Statement> authzStatements= samlAssertion.getStatements(XACMLAuthzDecisionStatementType.TYPE_NAME_XACML20);
if (authzStatements == null || authzStatements.isEmpty()) {
log.warn("Response from PDP {} was an invalid message. It did not contain an authorization statement",
pdpEndpoint);
return null;
}
if (authzStatements.size() > 1) {
log.warn("Response from PDP {} was an invalid message. It contained more than 1 authorization statement",
pdpEndpoint);
return null;
}
messageContext.setInboundMessageId(samlResponse.getID());
XACMLAuthzDecisionStatementType authzStatement= (XACMLAuthzDecisionStatementType) authzStatements.get(0);
return XACMLConverter.responseFromXACML(authzStatement.getResponse(),
authzStatement.getRequest());
}
/**
* Builds a Response containing an error. The Decision on error in
* <b>Indeterminate</b>
*
* @param request
* the request that caused the error
* @param statusCode
* status code of the error
* @param errorMessage
* associated error message
*
* @return the built response
*/
private Response buildErrorResponse(Request request, String statusCode,
String errorMessage) {
StatusCode errorCode= new StatusCode();
errorCode.setCode(statusCode);
Status status= new Status();
status.setCode(errorCode);
if (errorMessage != null) {
status.setMessage(errorMessage);
}
Result result= new Result();
result.setDecision(Result.DECISION_INDETERMINATE);
result.setStatus(status);
Response response= new Response();
response.setRequest(request);
response.getResults().add(result);
return response;
}
/**
* Logs an inbound/outbound SOAP message.
*
* @param message
* the message to log
* @param isRequest
* whether the message is a request
*/
private void logSOAPProtocolMessage(XMLObject message, boolean isRequest) {
if (message == null) {
return;
}
if (protocolLog.isDebugEnabled()) {
try {
Element messageDom= Configuration.getMarshallerFactory().getMarshaller(message).marshall(message);
if (isRequest) {
protocolLog.debug("Outgoing SOAP request\n{}",
XMLHelper.prettyPrintXML(messageDom));
}
else {
protocolLog.debug("Inbound SOAP response\n{}",
XMLHelper.prettyPrintXML(messageDom));
}
} catch (MarshallingException e) {
log.error("Unable to marshall SOAP message");
}
}
}
/**
* Writes a PEP daemon audit log entry.
*
* @param messageContext
* current message context
*/
private void writeAuditLogEntry(DecisionRequestContext messageContext) {
AuditLogEntry entry= new AuditLogEntry(messageContext.getOutboundMessageId(),
messageContext.getRespondingPDP(),
messageContext.getInboundMessageId(),
messageContext.getAuthorizationDecision());
auditLog.info(entry.toString());
}
}