/**
* Copyright (c) Codice Foundation
* <p/>
* This is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser
* General Public License as published by the Free Software Foundation, either version 3 of the
* License, or any later version.
* <p/>
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details. A copy of the GNU Lesser General Public License
* is distributed along with this program and can be found at
* <http://www.gnu.org/licenses/lgpl.html>.
*/
package ddf.security.pep.interceptor;
import java.io.StringWriter;
import javax.xml.namespace.QName;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.ws.handler.MessageContext;
import org.apache.commons.lang.StringUtils;
import org.apache.cxf.binding.soap.model.SoapOperationInfo;
import org.apache.cxf.interceptor.Fault;
import org.apache.cxf.interceptor.security.AccessDeniedException;
import org.apache.cxf.message.Message;
import org.apache.cxf.phase.AbstractPhaseInterceptor;
import org.apache.cxf.phase.Phase;
import org.apache.cxf.service.model.BindingOperationInfo;
import org.apache.cxf.service.model.MessageInfo;
import org.apache.cxf.ws.addressing.JAXWSAConstants;
import org.apache.cxf.ws.addressing.Names;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Element;
import ddf.security.SecurityConstants;
import ddf.security.Subject;
import ddf.security.assertion.SecurityAssertion;
import ddf.security.common.audit.SecurityLogger;
import ddf.security.permission.ActionPermission;
import ddf.security.service.SecurityManager;
import ddf.security.service.SecurityServiceException;
import ddf.security.service.impl.SecurityAssertionStore;
/**
* Interceptor used to perform service authentication.
*
*/
public class PEPAuthorizingInterceptor extends AbstractPhaseInterceptor<Message> {
private Logger logger = LoggerFactory.getLogger(PEPAuthorizingInterceptor.class);
private SecurityManager securityManager;
public PEPAuthorizingInterceptor() {
super(Phase.PRE_INVOKE);
addAfter(org.apache.cxf.ws.policy.PolicyVerificationInInterceptor.class.getName());
}
/**
* Sets the security manager that will be used to create a subject.
*
* @param securityManager
*/
public void setSecurityManager(SecurityManager securityManager) {
logger.trace("Setting the security manager");
this.securityManager = securityManager;
}
/**
* Intercepts a message. Interceptors should NOT invoke handleMessage or handleFault on the next
* interceptor - the interceptor chain will take care of this.
*
* @param message
*/
@Override
public void handleMessage(Message message) throws Fault {
if (message != null) {
// grab the SAML assertion associated with this Message from the
// token store
SecurityAssertion assertion = SecurityAssertionStore.getSecurityAssertion(message);
SecurityLogger.logSecurityAssertionInfo(message);
boolean isPermitted = false;
if ((assertion != null) && (assertion.getSecurityToken() != null)) {
Subject user = null;
ActionPermission action = null;
String actionURI = getActionUri(message);
try {
user = securityManager.getSubject(assertion.getSecurityToken());
if (user == null) {
throw new AccessDeniedException("Unauthorized");
}
if (logger.isTraceEnabled()) {
logger.trace(format(assertion.getSecurityToken().getToken()));
}
logger.debug("Is user authenticated: {}", user.isAuthenticated());
logger.debug("Checking for permission");
SecurityLogger.logInfo(
"Is user [" + user.getPrincipal() + "] authenticated: " + user
.isAuthenticated());
if (StringUtils.isEmpty(actionURI)) {
logger.info("Denying access : unable to determine action for {}",
user.getPrincipal());
SecurityLogger.logWarn("Denying access to [" + user.getPrincipal()
+ "] for unknown action.");
throw new AccessDeniedException("Unauthorized");
}
action = new ActionPermission(actionURI);
logger.debug("Action Permission: {}", action);
isPermitted = user.isPermitted(action);
logger.debug("Result of permission: {}", isPermitted);
SecurityLogger.logInfo(
"Is user [" + user.getPrincipal() + "] permitted: " + isPermitted);
// store the subject so the DDF framework can use it later
message.put(SecurityConstants.SAML_ASSERTION, user);
logger.debug("Added assertion information to message at key {}",
SecurityConstants.SAML_ASSERTION);
} catch (SecurityServiceException e) {
logger.warn("Caught exception when trying to perform AuthZ.", e);
SecurityLogger.logWarn(
"Denying access : Caught exception when trying to authenticate user for service ["
+ actionURI + "]", e);
throw new AccessDeniedException("Unauthorized");
}
if (!isPermitted) {
if (action != null) {
logger.info("Denying access to {} for service {}", user.getPrincipal(),
action.getAction());
SecurityLogger.logWarn(
"Denying access to [" + user.getPrincipal() + "] for service "
+ action.getAction());
}
throw new AccessDeniedException("Unauthorized");
}
} else {
logger.warn(
"Unable to retrieve the security assertion associated with the web service call.");
throw new AccessDeniedException("Unauthorized");
}
} else {
logger.warn(
"Unable to retrieve the current message associated with the web service call.");
throw new AccessDeniedException("Unauthorized");
}
}
/**
* This method is an implementation of the WSA-M and WSA-W specs for determining the action URI.<br>
* <ul>
* <li>http://www.w3.org/TR/ws-addr-metadata/#actioninwsdl</li>
* <li>http://www.w3.org/TR/ws-addr-wsdl/#actioninwsdl</li>
* </ul>
* Adapted from {@link org.apache.cxf.ws.addressing.impl.MAPAggregatorImpl} and
* {@link org.apache.cxf.ws.addressing.impl.InternalContextUtils}
*
* @param message
* @return
*/
private String getActionUri(Message message) {
String actionURI = null;
/**
* See if the action is explicitly defined in the WSDL message service model. Retrieves one
* of the Action attribute in the wsdl:input message.
*/
MessageInfo msgInfo = (MessageInfo) message.get(MessageInfo.class.getName());
if (msgInfo != null && msgInfo.getExtensionAttributes() != null) {
// wsaw:Action
Object attr = msgInfo.getExtensionAttribute(JAXWSAConstants.WSAW_ACTION_QNAME);
// wsam:Action
if (attr == null) {
attr = msgInfo.getExtensionAttribute(JAXWSAConstants.WSAM_ACTION_QNAME);
}
// support for older usages
if (attr == null) {
attr = msgInfo.getExtensionAttributes()
.get(new QName(JAXWSAConstants.NS_WSA, Names.WSAW_ACTION_NAME));
}
if (attr == null) {
attr = msgInfo.getExtensionAttributes()
.get(new QName(Names.WSA_NAMESPACE_WSDL_NAME_OLD, Names.WSAW_ACTION_NAME));
}
if (attr instanceof QName) {
actionURI = ((QName) attr).getLocalPart();
} else {
actionURI = attr == null ? null : attr.toString();
}
}
/**
* See if the action is explicitly defined in the WSDL operation service model. Retrieves
* the operation soap:soapAction property.
*/
if (StringUtils.isEmpty(actionURI)) {
BindingOperationInfo bindingOpInfo = message.getExchange()
.get(BindingOperationInfo.class);
SoapOperationInfo soi = bindingOpInfo.getExtensor(SoapOperationInfo.class);
if (soi == null && bindingOpInfo.isUnwrapped()) {
soi = bindingOpInfo.getWrappedOperation().getExtensor(SoapOperationInfo.class);
}
actionURI = soi == null ? null : soi.getAction();
actionURI = StringUtils.isEmpty(actionURI) ? null : actionURI;
}
/**
* If the service model doesn't explicitly defines the action, we'll construct the default
* URI string.
*/
if (StringUtils.isEmpty(actionURI)) {
QName op = (QName) message.get(MessageContext.WSDL_OPERATION);
QName port = (QName) message.get(MessageContext.WSDL_PORT);
if (op != null && port != null) {
actionURI = port.getNamespaceURI();
actionURI = addPath(actionURI, port.getLocalPart());
actionURI = addPath(actionURI, op.getLocalPart() + "Request");
}
}
return actionURI;
}
private String addPath(String uri, String path) {
StringBuilder builder = new StringBuilder(uri);
String delimiter = uri.startsWith("urn") ? ":" : "/";
if (!uri.endsWith(delimiter) && !path.startsWith(delimiter)) {
builder.append(delimiter);
}
builder.append(path);
return builder.toString();
}
private String format(Element unformattedXml) {
if (unformattedXml == null) {
logger.error("Unable to transform xml: null");
return null;
}
StreamResult xmlOutput = new StreamResult(new StringWriter());
TransformerFactory transformerFactory = TransformerFactory.newInstance();
Transformer transformer = null;
String formattedXml = null;
try {
transformer = transformerFactory.newTransformer();
logger.trace("transformer class: {}", transformer.getClass());
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
transformer.transform(new DOMSource(unformattedXml), xmlOutput);
formattedXml = xmlOutput.getWriter().toString();
} catch (TransformerConfigurationException e) {
String message =
"Unable to transform xml:\n" + unformattedXml + "\nUsing unformatted xml.";
logger.error(message, e);
} catch (TransformerException e) {
String message =
"Unable to transform xml:\n" + unformattedXml + "\nUsing unformatted xml.";
logger.error(message, e);
}
return formattedXml;
}
}