/**
* 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 org.codice.ddf.security.claims.attributequery.common;
import java.io.IOException;
import java.io.StringReader;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.stream.StreamSource;
import javax.xml.ws.Dispatch;
import org.apache.commons.lang.StringUtils;
import org.codice.ddf.platform.util.TransformerProperties;
import org.codice.ddf.platform.util.XMLUtils;
import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
import org.opensaml.core.xml.io.MarshallingException;
import org.opensaml.core.xml.io.Unmarshaller;
import org.opensaml.core.xml.io.UnmarshallingException;
import org.opensaml.saml.saml2.core.Assertion;
import org.opensaml.saml.saml2.core.AttributeQuery;
import org.opensaml.saml.saml2.core.Response;
import org.opensaml.saml.saml2.core.Status;
import org.opensaml.soap.soap11.Envelope;
import org.opensaml.soap.soap11.impl.EnvelopeMarshaller;
import org.opensaml.xmlsec.signature.support.SignatureException;
import org.opensaml.xmlsec.signature.support.Signer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import ddf.security.samlp.SamlProtocol;
import ddf.security.samlp.SimpleSign;
public class AttributeQueryClient {
private static final Logger LOGGER = LoggerFactory.getLogger(AttributeQueryClient.class);
private static final String SAML2_PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol";
private static final String SAML2_SUCCESS = "urn:oasis:names:tc:SAML:2.0:status:Success";
private static final String SAML2_UNKNOWN_ATTR_PROFILE =
"urn:oasis:names:tc:SAML:2.0:status:UnknownAttrProfile";
private static final String SAML2_INVALID_ATTR_NAME_VALUE =
"urn:oasis:names:tc:SAML:2.0:status:InvalidAttrNameOrValue";
private static final String SAML2_UNKNOWN_PRINCIPAL =
"urn:oasis:names:tc:SAML:2.0:status:UnknownPrincipal";
private Dispatch<StreamSource> dispatch;
private SimpleSign simpleSign;
private String externalAttributeStoreUrl;
private String issuer;
private String destination;
public AttributeQueryClient(Dispatch<StreamSource> dispatch, SimpleSign simpleSign,
String externalAttributeStoreUrl, String issuer, String destination) {
LOGGER.debug("Initializing AttributeQueryClient.");
this.dispatch = dispatch;
this.simpleSign = simpleSign;
this.externalAttributeStoreUrl = externalAttributeStoreUrl;
this.issuer = issuer;
this.destination = destination;
}
/**
* Query the external attribute store using an AttributeQuery request.
*
* @return Assertion of the response.
*/
public Assertion query(String username) {
return retrieveResponse(signRequest(createRequest(username)));
}
private AttributeQuery createRequest(String username) {
LOGGER.debug("Creating SAML Protocol AttributeQuery for user: {}.", username);
AttributeQuery attributeQuery = SamlProtocol.createAttributeQuery(SamlProtocol.createIssuer(
issuer),
SamlProtocol.createSubject(SamlProtocol.createNameID(username)),
destination);
LOGGER.debug("SAML Protocol AttributeQuery created for user: {}.", username);
return attributeQuery;
}
/**
* Signs AttributeQuery request.
*
* @param attributeQuery request to be signed.
* @return Document of the AttributeQuery.
*/
private Document signRequest(AttributeQuery attributeQuery) throws AttributeQueryException {
Element soapElement;
try {
// Create and set signature for request.
simpleSign.signSamlObject(attributeQuery);
// Create soap message for request.
soapElement = createSoapMessage(attributeQuery);
// Sign soap message.
Signer.signObject(attributeQuery.getSignature());
} catch (SignatureException | SimpleSign.SignatureException e) {
throw new AttributeQueryException("Error occurred during signing of the request.", e);
}
// Print AttributeQuery Request.
if (LOGGER.isTraceEnabled()) {
printXML("SAML Protocol AttributeQuery Request:\n{}", soapElement);
}
return soapElement.getOwnerDocument();
}
/**
* Creates a SOAP message of the AttributeQuery request.
*
* @param attributeQuery is added to the SOAP message
* @return soapElement is the Element of the SOAP message
*/
private Element createSoapMessage(AttributeQuery attributeQuery)
throws AttributeQueryException {
LOGGER.debug("Creating SOAP message from the SAML AttributeQuery.");
Envelope envelope = SamlProtocol.createSoapMessage(attributeQuery);
LOGGER.debug("SOAP message from the SAML AttributeQuery created.");
try {
return new EnvelopeMarshaller().marshall(envelope);
} catch (MarshallingException e) {
throw new AttributeQueryException("Cannot marshall SOAP object to an Element.", e);
}
}
/**
* Retrieves the response and returns its SAML Assertion.
*
* @param requestDocument of the request.
* @return Assertion of the response or null if the response is empty.
* @throws AttributeQueryException
*/
private Assertion retrieveResponse(Document requestDocument) throws AttributeQueryException {
Assertion assertion = null;
try {
Document responseDocument = sendRequest(requestDocument);
if (responseDocument == null) {
return null;
}
// Print Response
if (LOGGER.isTraceEnabled()) {
printXML("SAML Response:\n {}", responseDocument);
}
// Extract Response from Soap message.
NodeList elementsByTagNameNS = responseDocument.getElementsByTagNameNS(SAML2_PROTOCOL,
"Response");
if (elementsByTagNameNS == null) {
throw new AttributeQueryException("Unable to find SAML Response.");
}
Node responseNode = elementsByTagNameNS.item(0);
Element responseElement = (Element) responseNode;
Unmarshaller unmarshaller = XMLObjectProviderRegistrySupport.getUnmarshallerFactory()
.getUnmarshaller(responseElement);
Response response = (Response) unmarshaller.unmarshall(responseElement);
LOGGER.debug("Successfully marshalled Element to SAML Response.");
if (response.getStatus()
.getStatusCode()
.getValue()
.equals(SAML2_SUCCESS)) {
LOGGER.debug("Successful response, retrieved attributes.");
// Should only have one assertion.
assertion = response.getAssertions()
.get(0);
} else {
reportError(response.getStatus());
}
return assertion;
} catch (UnmarshallingException e) {
throw new AttributeQueryException("Unable to marshall Element to SAML Response.", e);
}
}
/**
* Sends the request to the external attribute store via a Dispatch client.
*
* @param requestDocument of the request.
* @return Document of the response or null if the response is empty.
* @throws AttributeQueryException
*/
protected Document sendRequest(Document requestDocument) {
TransformerProperties transformerProperties = new TransformerProperties();
transformerProperties.addOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
String request = XMLUtils.format(requestDocument, transformerProperties);
StreamSource streamSource;
try {
streamSource = dispatch.invoke(new StreamSource(new StringReader(request)));
} catch (Exception e) {
throw new AttributeQueryException(String.format("Could not connect to: %s",
this.externalAttributeStoreUrl), e);
}
String response = XMLUtils.format(streamSource, transformerProperties);
if (StringUtils.isBlank(response)) {
LOGGER.debug("Response is empty.");
return null;
}
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
documentBuilderFactory.setNamespaceAware(true);
try {
documentBuilderFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-dtd-grammar", false);
documentBuilderFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
} catch (ParserConfigurationException e) {
LOGGER.debug("Unable to configure features on document builder.", e);
}
DocumentBuilder documentBuilder;
Document responseDoc;
try {
documentBuilder = documentBuilderFactory.newDocumentBuilder();
responseDoc = documentBuilder.parse(new InputSource(new StringReader(response)));
} catch (ParserConfigurationException | SAXException | IOException e) {
throw new AttributeQueryException(
"Unable to parse response string into an XML document.",
e);
}
return responseDoc;
}
/**
* If the response is bad, report the cause of the error.
*
* @param status Status of response object.
*/
private void reportError(Status status) {
String statusCodeValue = status.getStatusCode()
.getValue();
switch (statusCodeValue) {
case SAML2_UNKNOWN_ATTR_PROFILE:
LOGGER.debug(
"Error in the response: {}. Incorrect version number or incorrect parsing error.",
statusCodeValue);
break;
case SAML2_INVALID_ATTR_NAME_VALUE:
LOGGER.debug("Error in the response: {}. Request attribute name is unknown.",
statusCodeValue);
break;
case SAML2_UNKNOWN_PRINCIPAL:
LOGGER.debug(
"Error in the response: {}. Unknown principal name, user is not recognized.",
statusCodeValue);
break;
default:
LOGGER.debug("Error in the response: {}", statusCodeValue);
break;
}
if (status.getStatusMessage() != null && status.getStatusMessage()
.getMessage() != null) {
LOGGER.debug(status.getStatusMessage()
.getMessage());
}
// Allow bad response to go through.
}
/**
* Prints the given XML.
*
* @param xmlNode Node to transform.
* @param message Message to display.
*/
private void printXML(String message, Node xmlNode) {
TransformerProperties transformerProperties = new TransformerProperties();
transformerProperties.addOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
LOGGER.trace(message, XMLUtils.format(xmlNode, transformerProperties));
}
public void setDispatch(Dispatch<StreamSource> dispatch) {
this.dispatch = dispatch;
}
public void setSimpleSign(SimpleSign simpleSign) {
this.simpleSign = simpleSign;
}
public void setExternalAttributeStoreUrl(String externalAttributeStoreUrl) {
this.externalAttributeStoreUrl = externalAttributeStoreUrl;
}
public void setIssuer(String issuer) {
this.issuer = issuer;
}
public void setDestination(String destination) {
this.destination = destination;
}
}