MessageSecurityAuthorizationSAMLPolicy.java
/*
* GovWay - A customizable API Gateway
* https://govway.org
*
* Copyright (c) 2005-2024 Link.it srl (https://link.it).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3, as published by
* the Free Software Foundation.
*
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.openspcoop2.security.message.authorization;
import java.io.File;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.xml.soap.SOAPElement;
import javax.xml.soap.SOAPException;
import org.apache.wss4j.common.saml.builder.SAML2Constants;
import org.herasaf.xacml.core.context.impl.DecisionType;
import org.herasaf.xacml.core.context.impl.ResultType;
import org.openspcoop2.core.id.IDServizio;
import org.openspcoop2.core.registry.AccordoServizioParteSpecifica;
import org.openspcoop2.core.registry.Documento;
import org.openspcoop2.core.registry.constants.TipiDocumentoSicurezza;
import org.openspcoop2.core.registry.driver.IDServizioFactory;
import org.openspcoop2.message.OpenSPCoop2Message;
import org.openspcoop2.message.OpenSPCoop2SoapMessage;
import org.openspcoop2.message.constants.MessageType;
import org.openspcoop2.message.soap.WSSecurityUtils;
import org.openspcoop2.message.xml.MessageDynamicNamespaceContextFactory;
import org.openspcoop2.message.xml.XPathExpressionEngine;
import org.openspcoop2.protocol.registry.RegistroServiziManager;
import org.openspcoop2.protocol.sdk.Busta;
import org.openspcoop2.security.message.constants.SecurityConstants;
import org.openspcoop2.security.message.saml.SAMLConstants;
import org.openspcoop2.utils.resources.FileSystemUtilities;
import org.openspcoop2.utils.transport.http.HttpConstants;
import org.openspcoop2.utils.transport.http.HttpRequest;
import org.openspcoop2.utils.transport.http.HttpRequestMethod;
import org.openspcoop2.utils.transport.http.HttpResponse;
import org.openspcoop2.utils.transport.http.HttpUtilities;
import org.openspcoop2.utils.xacml.CachedMapBasedSimplePolicyRepository;
import org.openspcoop2.utils.xacml.MarshallUtilities;
import org.openspcoop2.utils.xacml.PolicyDecisionPoint;
import org.openspcoop2.utils.xacml.PolicyException;
import org.openspcoop2.utils.xacml.ResultCombining;
import org.openspcoop2.utils.xacml.ResultUtilities;
import org.openspcoop2.utils.xacml.XacmlRequest;
import org.openspcoop2.utils.xml.AbstractXPathExpressionEngine;
import org.openspcoop2.utils.xml.DynamicNamespaceContext;
import org.openspcoop2.utils.xml.XPathException;
import org.openspcoop2.utils.xml.XPathNotFoundException;
import org.openspcoop2.utils.xml.XPathNotValidException;
import org.openspcoop2.utils.xml.XPathReturnType;
import org.slf4j.Logger;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
/**
* Implementazione dell'interfaccia Authorization basata su token SAML
*
* @author Bussu Giovanni (bussu@link.it)
* @author $Author$
* @version $Rev$, $Date$
*/
public class MessageSecurityAuthorizationSAMLPolicy implements IMessageSecurityAuthorization{
public static final String SAML_20_ISSUER = "urn:oasis:names:tc:SAML:2.0:assertion:Issuer";
public static final String SAML_20_SUBJECT_NAME = "urn:oasis:names:tc:SAML:2.0:assertion:Subject:NameID";
public static final String SAML_20_ATTRIBUTE_NAME = "urn:oasis:names:tc:SAML:2.0:assertion:AttributeStatement:Attribute:Name:";
public static final String SAML_11_ISSUER = "urn:oasis:names:tc:SAML:2.0:assertion:Issuer";
public static final String SAML_11_AUTHENTICATION_SUBJECT_NAME = "urn:oasis:names:tc:SAML:1.0:assertion:AuthenticationStatement:Subject:NameIdentifier";
public static final String SAML_11_AUTHORIZATION_SUBJECT_NAME = "urn:oasis:names:tc:SAML:1.0:assertion:AttributeStatement:Subject:NameIdentifier";
public static final String SAML_11_ATTRIBUTE_NAME = "urn:oasis:names:tc:SAML:1.0:assertion:AttributeStatement:Attribute:Name:";
private static PolicyDecisionPoint pdp;
private static synchronized void initPdD(Logger log) throws PolicyException{
if(MessageSecurityAuthorizationSAMLPolicy.pdp == null) {
MessageSecurityAuthorizationSAMLPolicy.pdp = new PolicyDecisionPoint(log);
}
}
public MessageSecurityAuthorizationSAMLPolicy() {
PolicyDecisionPoint.runInitializers(); //necessario per far inizializzare gli unmarshaller in caso di pdp remoto
}
@Override
public MessageSecurityAuthorizationResult authorize(MessageSecurityAuthorizationRequest request) throws SecurityException{
String principalWSS = request.getSecurityPrincipal();
Busta busta = request.getBusta();
org.openspcoop2.security.message.MessageSecurityContext messageSecurityContext = request.getMessageSecurityContext();
OpenSPCoop2Message msg = request.getMessage();
IDServizio idServizio = null;
try {
idServizio = IDServizioFactory.getInstance().getIDServizioFromValues(busta.getTipoServizio(), busta.getServizio(),
busta.getTipoDestinatario(), busta.getDestinatario(), busta.getVersioneServizio());
OpenSPCoop2SoapMessage soapMessage = msg.castAsSoap();
// ****** Proprieta' WSSecurity ********
String actor = messageSecurityContext.getActor();
if("".equals(messageSecurityContext.getActor()))
actor = null;
String pdpLocalString = (String) messageSecurityContext.getIncomingProperties().get(SecurityConstants.AUTH_PDP_LOCAL);
boolean pdpLocal = true;
if(pdpLocalString==null || "".equals(pdpLocalString.trim())){
pdpLocal = true;
}
else if("true".equalsIgnoreCase(pdpLocalString.trim())){
pdpLocal = true;
}
else if("false".equalsIgnoreCase(pdpLocalString.trim())){
pdpLocal = false;
}
else{
throw new SecurityException("Property '"+SecurityConstants.AUTH_PDP_LOCAL+"' with wrong value ["+pdpLocalString.trim()+"]");
}
String remotePdD_url = null;
Integer remotePdD_connectionTimeout = HttpUtilities.HTTP_CONNECTION_TIMEOUT;
Integer remotePdD_readConnectionTimeout = HttpUtilities.HTTP_READ_CONNECTION_TIMEOUT;
if(!pdpLocal){
remotePdD_url = (String) messageSecurityContext.getIncomingProperties().get(SecurityConstants.AUTH_PDP_REMOTE_URL);
if(pdpLocalString==null || "".equals(pdpLocalString.trim())){
throw new SecurityException("Property '"+SecurityConstants.AUTH_PDP_REMOTE_URL+"' not found (required with "+SecurityConstants.AUTH_PDP_LOCAL+"=false)");
}
pdpLocalString = pdpLocalString.trim();
try{
(new URI(remotePdD_url)).toString();
}catch(Exception e){
throw new SecurityException("Property '"+SecurityConstants.AUTH_PDP_REMOTE_URL+"' with wrong value ["+remotePdD_url+"]: "+e.getMessage(),e);
}
String tmp = (String) messageSecurityContext.getIncomingProperties().get(SecurityConstants.AUTH_PDP_REMOTE_CONNECTION_TIMEOUT);
if(tmp!=null && !"".equals(tmp)){
tmp = tmp.trim();
try{
remotePdD_connectionTimeout = Integer.parseInt(tmp);
}catch(Exception e){
throw new SecurityException("Property '"+SecurityConstants.AUTH_PDP_REMOTE_CONNECTION_TIMEOUT+"' with wrong value ["+tmp+"]: "+e.getMessage(),e);
}
}
tmp = (String) messageSecurityContext.getIncomingProperties().get(SecurityConstants.AUTH_PDP_REMOTE_READ_CONNECTION_TIMEOUT);
if(tmp!=null && !"".equals(tmp)){
tmp = tmp.trim();
try{
remotePdD_readConnectionTimeout = Integer.parseInt(tmp);
}catch(Exception e){
throw new SecurityException("Property '"+SecurityConstants.AUTH_PDP_REMOTE_READ_CONNECTION_TIMEOUT+"' with wrong value ["+tmp+"]: "+e.getMessage(),e);
}
}
}
// ****** Raccolta Dati ********
String tipoSoggettoErogatore = busta.getTipoDestinatario();
String nomeSoggettoErogatore = busta.getDestinatario();
String tipoServizio = busta.getTipoServizio();
String nomeServizio = busta.getServizio();
String azione = busta.getAzione() != null ? busta.getAzione() : "";
String servizioKey = "http://"+tipoSoggettoErogatore+nomeSoggettoErogatore+".openspcoop2.org/servizi/"+tipoServizio+nomeServizio;
String azioneKey = servizioKey+"/" + azione;
if(pdpLocal){
byte[] policy = null;
try{
AccordoServizioParteSpecifica asps = RegistroServiziManager.getInstance().getAccordoServizioParteSpecifica(idServizio, null, true, null); // non serve requestInfo tanto ho necessita degli allegati
for (int i = 0; i < asps.sizeSpecificaSicurezzaList(); i++) {
Documento d = asps.getSpecificaSicurezza(i);
if(TipiDocumentoSicurezza.XACML_POLICY.getNome().equals(d.getTipo())){
if(policy == null){
if(d.getByteContenuto()!=null){
policy = d.getByteContenuto();
}
else if(d.getFile()!=null){
if(d.getFile().startsWith("http://") || d.getFile().startsWith("file://")){
URL url = new URL(d.getFile());
policy = HttpUtilities.requestHTTPFile(url.toString());
}
else{
File f = new File(d.getFile());
policy = FileSystemUtilities.readBytesFromFile(f);
}
}
}else
throw new SecurityException("Piu di una xacml policy trovata trovata per il servizio "+idServizio.toString());
}
}
}catch(Exception e){
throw new SecurityException("Errore durante la ricerca delle policies xacml per il servizio "+idServizio.toString()+": "+e.getMessage(),e);
}
if(policy== null){
throw new SecurityException("Nessuna xacml policy trovata per il servizio "+idServizio.toString());
}
if(MessageSecurityAuthorizationSAMLPolicy.pdp == null) {
MessageSecurityAuthorizationSAMLPolicy.initPdD(messageSecurityContext.getLog());
}
// Caricamento in PdP vedendo che la policy non sia gia stata caricata ....
MessageSecurityAuthorizationSAMLPolicy.pdp.addPolicy(MarshallUtilities.unmarshallPolicy(policy), servizioKey);
}
// ****** Header WSSecurity con SAML ********
SOAPElement security = null;
try{
security = (SOAPElement) WSSecurityUtils.getSecurityHeader(soapMessage.getSOAPPart(), soapMessage.getMessageType(), actor, true);
}catch(Exception e){
throw new SecurityException("Errore durante la ricerca dell'header WSSecurity (actor:"+actor+") "+e.getMessage(),e);
}
if(security==null){
throw new SecurityException("Header WSSecurity (actor:"+actor+") contenente una SAML non trovato");
}
DynamicNamespaceContext dnc = null;
if(MessageType.SOAP_11.equals(soapMessage.getMessageType())) {
dnc = MessageDynamicNamespaceContextFactory.getInstance(soapMessage.getFactory()).getNamespaceContextFromSoapEnvelope11(soapMessage.getSOAPPart().getEnvelope());
} else {
dnc = MessageDynamicNamespaceContextFactory.getInstance(soapMessage.getFactory()).getNamespaceContextFromSoapEnvelope12(soapMessage.getSOAPPart().getEnvelope());
}
AbstractXPathExpressionEngine xpathExpressionEngine = new XPathExpressionEngine(soapMessage.getFactory());
boolean SAML_2_0 = false;
try {
Object o = xpathExpressionEngine.getMatchPattern(security, dnc, SAMLConstants.XPATH_SAML_20_ASSERTION, XPathReturnType.NODE);
SAML_2_0 = (o!=null);
} catch(XPathNotFoundException e) {
SAML_2_0 = false;
}
// ****** Produzione XACMLRequest a partire dalla SAML ********
XacmlRequest xacmlRequest = new XacmlRequest();
// ** action **
xacmlRequest.addAction(azioneKey);
// ** subject **
// Creare un Subject che contiene nel subject id:
// come urn:oasis:names:tc:xacml:1.0:subject:subject-id il valore del principal presente nella richiesta (principalWSS)
if(principalWSS != null) {
xacmlRequest.addSubjectAttribute("urn:oasis:names:tc:xacml:1.0:subject:subject-id", principalWSS);
}
// inoltre creare altri attribute del subject che contengano:
SAMLAttributes saml = new SAMLAttributes(security, dnc, SAML_2_0, xpathExpressionEngine);
// il valore del NameID all'interno dell'elemento Subject
if(SAML_2_0){
if(saml.nameIDAuthorization!=null){
xacmlRequest.addSubjectAttribute(SAML_20_SUBJECT_NAME, saml.nameIDAuthorization);
}
}
else{
if(saml.nameIDAuthorization!=null){
xacmlRequest.addSubjectAttribute(SAML_11_AUTHORIZATION_SUBJECT_NAME, saml.nameIDAuthorization);
}
if(saml.nameIDAuthentication!=null){
xacmlRequest.addSubjectAttribute(SAML_11_AUTHENTICATION_SUBJECT_NAME, saml.nameIDAuthentication);
}
}
// il valore dell'elemento Issuer
if(SAML_2_0){
xacmlRequest.addSubjectAttribute(SAML_20_ISSUER, saml.issuer);
}
else{
xacmlRequest.addSubjectAttribute(SAML_11_ISSUER, saml.issuer);
}
// - AttributeId="urn:oasis:names:tc:SAML:XX:assertion/AttributeStatement/Attribute/Name/<Nome> se il NameFormat non รจ una uri altrimenti direttamente il nome come attribute Id
for(String keyAttribute: saml.customAttributes.keySet()) {
xacmlRequest.addSubjectAttribute(keyAttribute, saml.customAttributes.get(keyAttribute));
}
// ** environment **
xacmlRequest.createEnvironment();
// ****** Valutazione XACMLRequest con PdD ********
List<ResultType> results = null;
if(pdpLocal){
try {
//solo in caso di pdp locale inizializzo la resource __resource_id___ con il nome del servizio in modo da consentire l'identificazione della policy
xacmlRequest.addResourceAttribute(CachedMapBasedSimplePolicyRepository.RESOURCE_ATTRIBUTE_ID_TO_MATCH, servizioKey);
messageSecurityContext.getLog().debug("----XACML Request locale begin ---");
messageSecurityContext.getLog().debug(new String(MarshallUtilities.marshallRequest(xacmlRequest)));
messageSecurityContext.getLog().debug("----XACML Request locale end ---");
results = MessageSecurityAuthorizationSAMLPolicy.pdp.evaluate(xacmlRequest);
messageSecurityContext.getLog().debug("----XACML Results begin ---");
for(ResultType result: results) {
messageSecurityContext.getLog().debug("Decision: "+result.getDecision().toString());
}
messageSecurityContext.getLog().debug("----XACML Results end ---");
} catch(Exception e) {
throw new SecurityException("Errore avvenuto durante l'interrogazione del PdP locale per l'autorizzazione del servizio "+idServizio.toString()+": "+e.getMessage(),e);
}
}
else{
try{
xacmlRequest.createResource();
byte[] xacmlBytes = MarshallUtilities.marshallRequest(xacmlRequest);
messageSecurityContext.getLog().debug("----XACML Request remota begin ---");
messageSecurityContext.getLog().debug(new String(xacmlBytes));
messageSecurityContext.getLog().debug("----XACML Request remota end ---");
HttpRequest httpRequest = new HttpRequest();
httpRequest.setUrl(remotePdD_url);
httpRequest.setContent(xacmlBytes);
httpRequest.setMethod(HttpRequestMethod.POST);
httpRequest.setContentType(HttpConstants.CONTENT_TYPE_TEXT_XML);
httpRequest.setReadTimeout(remotePdD_readConnectionTimeout);
httpRequest.setConnectTimeout(remotePdD_connectionTimeout);
HttpResponse httpResponse = HttpUtilities.httpInvoke(httpRequest);
if(httpResponse.getResultHTTPOperation()==200){
byte[] res = httpResponse.getContent();
results = MarshallUtilities.unmarshallResult(res);
}
else{
throw new Exception("invocazione fallita (http-return-code: "+httpResponse.getResultHTTPOperation()+")");
}
}catch(Exception e){
throw new SecurityException("Errore avvenuto durante l'interrogazione del PdP remoto ["+remotePdD_url+"] per l'autorizzazione del servizio "+idServizio.toString()+": "+e.getMessage(),e);
}
}
DecisionType decision = ResultCombining.combineDenyOverrides(results);
MessageSecurityAuthorizationResult result = new MessageSecurityAuthorizationResult();
if(DecisionType.PERMIT.equals(decision)) {
result.setAuthorized(true);
result.setErrorMessage(null);
} else {
String url = "";
String tipo = "XACML-Policy";
if(!pdpLocal){
url = " url["+remotePdD_url+"]";
tipo = "XACML-Policy-RemotePdp";
}
try{
String resultAsString = ResultUtilities.toRawString(results);
messageSecurityContext.getLog().error("Autorizzazione con XACMLPolicy fallita pddLocal["+pdpLocal+"]"+url+"; results (size:"+results.size()+"): \n"+resultAsString);
}catch(Throwable e){
messageSecurityContext.getLog().error("Autorizzazione con XACMLPolicy fallita pddLocal["+pdpLocal+"]"+url+". Serializzazione risposta non riuscita",e);
}
result.setAuthorized(false);
String resultAsString = ResultUtilities.toString(results, decision);
if(resultAsString!=null && resultAsString.length()>0){
result.setErrorMessage(tipo+" "+resultAsString);
}
else{
result.setErrorMessage(tipo);
}
}
return result;
} catch(SecurityException e) {
messageSecurityContext.getLog().error("Errore di sicurezza durante la gestione della richiesta per il servizio: " + idServizio, e);
throw e;
} catch(Exception e) {
messageSecurityContext.getLog().error("Errore generico durante la gestione della richiesta per il servizio: " + idServizio, e);
throw new SecurityException(e);
}
}
private class SAMLAttributes {
public String nameIDAuthentication;
public String nameIDAuthorization;
public String issuer;
public Map<String, List<String>> customAttributes;
private DynamicNamespaceContext dnc;
private boolean saml20;
private AbstractXPathExpressionEngine xpathExpressionEngine;
public SAMLAttributes(SOAPElement security, DynamicNamespaceContext dnc, boolean saml20, AbstractXPathExpressionEngine xpathExpressionEngine) throws Exception {
this.dnc = dnc;
this.saml20 = saml20;
this.xpathExpressionEngine = xpathExpressionEngine;
if(this.saml20){
this.nameIDAuthorization = find(security, SAMLConstants.XPATH_SAML_20_ASSERTION_SUBJECT_NAMEID,false);
this.issuer = find(security, SAMLConstants.XPATH_SAML_20_ASSERTION_ISSUER,true);
}
else{
this.nameIDAuthorization = find(security, SAMLConstants.XPATH_SAML_11_ASSERTION_ATTRIBUTESTATEMENT_SUBJECT_NAMEID,false);
this.nameIDAuthentication = find(security, SAMLConstants.XPATH_SAML_11_ASSERTION_AUTHENTICATIONSTATEMENT_SUBJECT_NAMEID,false);
this.issuer = find(security, SAMLConstants.XPATH_SAML_11_ASSERTION_ISSUER,true);
}
this.customAttributes = findNameAttributes(security);
}
private String find(SOAPElement security,String xpath, boolean required) throws SAXException, SOAPException,
XPathException, XPathNotValidException, Exception {
String match = null;
try {
match = (String) this.xpathExpressionEngine.getMatchPattern(security, this.dnc, xpath, XPathReturnType.STRING);
} catch(XPathNotFoundException e) {
if(required)
throw new Exception(e.getMessage(),e);
else
return null;
}
return match;
}
private Map<String, List<String>> findNameAttributes(SOAPElement security) throws SAXException, SOAPException,
XPathException, XPathNotValidException, Exception {
Map<String, List<String>> nameAttributes = new HashMap<>();
String xpath = null;
if(this.saml20){
xpath = SAMLConstants.XPATH_SAML_20_ASSERTION_ATTRIBUTESTATEMENT_ATTRIBUTE;
}
else{
xpath = SAMLConstants.XPATH_SAML_11_ASSERTION_ATTRIBUTESTATEMENT_ATTRIBUTE;
}
try {
NodeList attributes = (NodeList) this.xpathExpressionEngine.getMatchPattern(security, this.dnc, xpath, XPathReturnType.NODESET);
if(attributes != null) {
for(int i = 0; i < attributes.getLength(); i++) {
org.w3c.dom.Node attribute = attributes.item(i);
String name = null;
String friendlyName = null;
boolean uriNameFormat = false;
if(this.saml20){
Node nameAttribute = attribute.getAttributes().getNamedItem("Name");
name = nameAttribute.getNodeValue();
Node friendlyNameAttribute = attribute.getAttributes().getNamedItem("FriendlyName");
if(friendlyNameAttribute!=null){
friendlyName = friendlyNameAttribute.getNodeValue();
}
Node nameFormatAttribute = attribute.getAttributes().getNamedItem("NameFormat");
if(nameFormatAttribute!=null){
uriNameFormat = SAML2Constants.ATTRNAME_FORMAT_URI.equals(nameFormatAttribute.getNodeValue());
}
}
else{
Node nameAttribute = attribute.getAttributes().getNamedItem("AttributeNamespace");
name = nameAttribute.getNodeValue();
Node friendlyNameAttribute = attribute.getAttributes().getNamedItem("AttributeName");
friendlyName = friendlyNameAttribute.getNodeValue(); // obbligatorio in saml11
}
if(uriNameFormat==false){
if(friendlyName==null){
if(name.startsWith("urn:") || name.startsWith("url:") || name.startsWith("http:") || name.startsWith("htts:")){
uriNameFormat = true;
}
}
}
String key = null;
if(friendlyName != null) {
if(this.saml20){
key = SAML_20_ATTRIBUTE_NAME + friendlyName;
}
else{
key = SAML_11_ATTRIBUTE_NAME + friendlyName;
}
} else {
if(uriNameFormat) {
key = name;
} else {
if(this.saml20){
key = SAML_20_ATTRIBUTE_NAME + name;
}
else{
key = SAML_11_ATTRIBUTE_NAME + name;
}
}
}
String samlNamespace = null;
if(this.saml20){
samlNamespace = SAMLConstants.SAML_20_NAMESPACE;
}else{
samlNamespace = SAMLConstants.SAML_11_NAMESPACE;
}
List<String> values = null;
if(nameAttributes.containsKey(key)) {
values = nameAttributes.remove(key);
} else {
values = new ArrayList<>();
}
NodeList attributeValues = attribute.getChildNodes();
if(attributeValues != null) {
for(int j = 0; j < attributeValues.getLength(); j++) {
org.w3c.dom.Node attributeValue = attributeValues.item(j);
if(attributeValue != null && "AttributeValue".equals(attributeValue.getLocalName()) &&
samlNamespace.equals(attributeValue.getNamespaceURI())) {
values.add(attributeValue.getTextContent());
}
}
}
nameAttributes.put(key, values);
}
}
} catch(XPathNotFoundException e) {
throw new Exception("Impossibile trovare l'xPath ["+xpath+"]");
}
return nameAttributes;
}
}
}