View Javadoc
1   /*
2    * Copyright 2016 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.openehealth.ipf.commons.ihe.xua;
17  
18  import lombok.extern.slf4j.Slf4j;
19  import org.apache.commons.lang3.StringUtils;
20  import org.apache.cxf.binding.soap.SoapMessage;
21  import org.apache.cxf.headers.Header;
22  import org.openehealth.ipf.commons.audit.types.ActiveParticipantRoleId;
23  import org.openehealth.ipf.commons.audit.types.PurposeOfUse;
24  import org.openehealth.ipf.commons.ihe.ws.cxf.audit.AbstractAuditInterceptor;
25  import org.openehealth.ipf.commons.ihe.ws.cxf.audit.WsAuditDataset;
26  import org.openehealth.ipf.commons.ihe.ws.cxf.audit.XuaProcessor;
27  import org.opensaml.core.config.ConfigurationService;
28  import org.opensaml.core.config.InitializationException;
29  import org.opensaml.core.config.InitializationService;
30  import org.opensaml.core.xml.XMLObject;
31  import org.opensaml.core.xml.config.XMLObjectProviderRegistry;
32  import org.opensaml.core.xml.io.Unmarshaller;
33  import org.opensaml.core.xml.io.UnmarshallerFactory;
34  import org.opensaml.core.xml.io.UnmarshallingException;
35  import org.opensaml.saml.common.xml.SAMLConstants;
36  import org.opensaml.saml.saml2.core.*;
37  import org.opensaml.soap.wssecurity.WSSecurityConstants;
38  import org.w3c.dom.Document;
39  import org.w3c.dom.Element;
40  import org.w3c.dom.Node;
41  import org.w3c.dom.NodeList;
42  
43  import javax.xml.namespace.QName;
44  import java.util.*;
45  
46  import static org.openehealth.ipf.commons.audit.types.ActiveParticipantRoleId.of;
47  import static org.openehealth.ipf.commons.ihe.ws.utils.SoapUtils.SOAP_NS_URIS;
48  import static org.openehealth.ipf.commons.ihe.ws.utils.SoapUtils.getElementNS;
49  
50  /**
51   * @author Dmytro Rud
52   * @see <a href="http://docs.oasis-open.org/xacml/xspa/v1.0/xacml-xspa-1.0-os.html">Cross-Enterprise Security
53   * and Privacy Authorization (XSPA) Profile of XACML v2.0 for Healthcare Version 1.0</a>
54   */
55  @Slf4j
56  public class BasicXuaProcessor implements XuaProcessor {
57  
58      /**
59       * If a SAML assertion is stored under this key in the Web Service context,
60       * IPF will use it instead of parsing the WS-Security header by itself.
61       * If there are no Web Service context element under this key, or if this element
62       * does not contain a SAML assertion, IPF will parse the WS-Security header
63       * and store the assertion extracted from there (if any) under this key.
64       */
65      private static final String XUA_SAML_ASSERTION = AbstractAuditInterceptor.class.getName() + ".XUA_SAML_ASSERTION";
66  
67      private static final Set<String> WSSE_NS_URIS = new HashSet<>(Arrays.asList(
68              WSSecurityConstants.WSSE_NS,
69              WSSecurityConstants.WSSE11_NS));
70  
71      private static final String PURPOSE_OF_USE_ATTRIBUTE_NAME = "urn:oasis:names:tc:xspa:1.0:subject:purposeofuse";
72      private static final String SUBJECT_NAME_ATTRIBUTE_NAME   = "urn:oasis:names:tc:xspa:1.0:subject:subject-id";
73      private static final String SUBJECT_ROLE_ATTRIBUTE_NAME   = "urn:oasis:names:tc:xacml:2.0:subject:role";
74      private static final String PATIENT_ID_ATTRIBUTE_NAME     = "urn:oasis:names:tc:xacml:2.0:resource:resource-id";
75  
76      private static final QName PURPOSE_OF_USE_ELEMENT_NAME = new QName("urn:hl7-org:v3", "PurposeOfUse");
77      private static final QName SUBJECT_ROLE_ELEMENT_NAME   = new QName("urn:hl7-org:v3", "Role");
78  
79      /** Map from principal role to assistant role */
80      public static final Map<ActiveParticipantRoleId, ActiveParticipantRoleId> PRINCIPAL_ASSISTANT_ROLE_RELATIONSHIPS;
81      static {
82          PRINCIPAL_ASSISTANT_ROLE_RELATIONSHIPS = new HashMap<>();
83          // old (obsolete) and new coding system IDs
84          for (String codingSystemId : new String[]{"2.16.756.5.30.1.127.3.10.4", "2.16.756.5.30.1.127.3.10.6"}) {
85              PRINCIPAL_ASSISTANT_ROLE_RELATIONSHIPS.put(of("PAT", codingSystemId, ""), of("REP",       codingSystemId, "Representative"));
86              PRINCIPAL_ASSISTANT_ROLE_RELATIONSHIPS.put(of("HCP", codingSystemId, ""), of("ASSISTANT", codingSystemId, "Assistant"));
87          }
88      }
89  
90      private static final UnmarshallerFactory SAML_UNMARSHALLER_FACTORY;
91      static {
92          try {
93              InitializationService.initialize();
94              XMLObjectProviderRegistry registry = ConfigurationService.get(XMLObjectProviderRegistry.class);
95              SAML_UNMARSHALLER_FACTORY = registry.getUnmarshallerFactory();
96          } catch (InitializationException e) {
97              throw new RuntimeException(e);
98          }
99      }
100 
101     private static Element extractAssertionElementFromCxfMessage(SoapMessage message, Header.Direction headerDirection) {
102         Header header = message.getHeader(new QName(WSSecurityConstants.WSSE_NS, "Security"));
103         if (!((header != null) &&
104                 headerDirection.equals(header.getDirection()) &&
105                 (header.getObject() instanceof Element))) {
106             return null;
107         }
108 
109         Element headerElem = (Element) header.getObject();
110         NodeList nodeList = headerElem.getElementsByTagNameNS(SAMLConstants.SAML20_NS, "Assertion");
111         return (Element) nodeList.item(0);
112     }
113 
114     private static Element extractAssertionElementFromDom(SoapMessage message) {
115         Document document = (Document) message.getContent(Node.class);
116         if (document == null) {
117             return null;
118         }
119         Element element = getElementNS(document.getDocumentElement(), SOAP_NS_URIS, "Header");
120         element = getElementNS(element, WSSE_NS_URIS, "Security");
121         return getElementNS(element, Collections.singleton(SAMLConstants.SAML20_NS), "Assertion");
122     }
123 
124     /**
125      * Extracts ITI-40 XUA user name from the SAML2 assertion contained
126      * in the given CXF message, and stores it in the ATNA audit dataset.
127      *
128      * @param message         source CXF message.
129      * @param headerDirection direction of the header containing the SAML2 assertion.
130      * @param auditDataset    target ATNA audit dataset.
131      */
132     public void extractXuaUserNameFromSaml2Assertion(
133             SoapMessage message,
134             Header.Direction headerDirection,
135             WsAuditDataset auditDataset)
136     {
137         Assertion assertion = null;
138 
139         // check whether someone has already parsed the SAML2 assertion
140         Object o = message.getContextualProperty(XUA_SAML_ASSERTION);
141         if (o instanceof Assertion) {
142             assertion = (Assertion) o;
143         }
144 
145         // extract SAML assertion the from WS-Security SOAP header
146         if (assertion == null) {
147             Element assertionElem = extractAssertionElementFromCxfMessage(message, headerDirection);
148             if (assertionElem == null) {
149                 assertionElem = extractAssertionElementFromDom(message);
150             }
151             if (assertionElem == null) {
152                 return;
153             }
154 
155             Unmarshaller unmarshaller = SAML_UNMARSHALLER_FACTORY.getUnmarshaller(assertionElem);
156             try {
157                 assertion = (Assertion) unmarshaller.unmarshall(assertionElem);
158             } catch (UnmarshallingException e) {
159                 log.warn("Cannot extract SAML assertion from the WS-Security SOAP header", e);
160                 return;
161             }
162 
163             message.getExchange().put(XUA_SAML_ASSERTION, assertion);
164         }
165 
166         WsAuditDataset.HumanUser mainUser = new WsAuditDataset.HumanUser();
167         WsAuditDataset.HumanUser assistantUser = new WsAuditDataset.HumanUser();
168 
169         // extract information about the main user and the optional assistant
170         if (assertion.getSubject() != null) {
171             mainUser.setId(createXuaUserId(assertion.getIssuer(), assertion.getSubject().getNameID()));
172 
173             // process information about the assistant (if any)
174             for (SubjectConfirmation subjectConfirmation : assertion.getSubject().getSubjectConfirmations()) {
175                 assistantUser.setId(createXuaUserId(assertion.getIssuer(), subjectConfirmation.getNameID()));
176                 AttributeStatement statement = new AttributeStatementExtractor().extractAttributeStatement(subjectConfirmation);
177                 statement.getAttributes().stream()
178                         .filter(attr -> SUBJECT_NAME_ATTRIBUTE_NAME.equals(attr.getName()))
179                         .findAny()
180                         .ifPresent(attr -> assistantUser.setName(extractSingleStringAttributeValue(attr)));
181             }
182         }
183 
184         // collect purposes of use, user role codes, and the patient ID
185         for (AttributeStatement statement : assertion.getAttributeStatements()) {
186             for (Attribute attribute : statement.getAttributes()) {
187                 switch (attribute.getName()) {
188                     case PURPOSE_OF_USE_ATTRIBUTE_NAME:
189                         auditDataset.setPurposesOfUse(extractPurposeOfUse(attribute, PURPOSE_OF_USE_ELEMENT_NAME));
190                         break;
191                     case SUBJECT_NAME_ATTRIBUTE_NAME:
192                         mainUser.setName(extractSingleStringAttributeValue(attribute));
193                         break;
194                     case SUBJECT_ROLE_ATTRIBUTE_NAME:
195                         extractActiveParticipantRoleId(attribute, SUBJECT_ROLE_ELEMENT_NAME).forEach(mainUserRoleId -> {
196                             mainUser.getRoles().add(mainUserRoleId);
197                             ActiveParticipantRoleId normalizedMainUserRoleId = of(mainUserRoleId.getCode(), mainUserRoleId.getCodeSystemName(), "");
198                             ActiveParticipantRoleId assistantUserRoleId = PRINCIPAL_ASSISTANT_ROLE_RELATIONSHIPS.get(normalizedMainUserRoleId);
199                             if (assistantUserRoleId != null) {
200                                 assistantUser.getRoles().add(assistantUserRoleId);
201                             }
202                         });
203                         break;
204                     case PATIENT_ID_ATTRIBUTE_NAME:
205                         auditDataset.setXuaPatientId(extractSingleStringAttributeValue(attribute));
206                         break;
207                 }
208             }
209         }
210 
211         if (!mainUser.isEmpty()) {
212             auditDataset.getHumanUsers().add(mainUser);
213         }
214         if (!assistantUser.isEmpty()) {
215             auditDataset.getHumanUsers().add(assistantUser);
216         }
217     }
218 
219     private static String createXuaUserId(Issuer issuer, NameID nameID) {
220         String userName     = (nameID != null) ? nameID.getValue() : null;
221         String issuerName   = (issuer != null) ? issuer.getValue() : null;
222         String spProvidedId = (nameID != null) ? StringUtils.stripToEmpty(nameID.getSPProvidedID()) : null;
223 
224         return StringUtils.isNoneEmpty(issuerName, userName)
225                 ? spProvidedId + '<' + userName + '@' + issuerName + '>'
226                 : null;
227     }
228 
229     private static String extractSingleStringAttributeValue(Attribute attribute) {
230         List<XMLObject> attributeValues = attribute.getAttributeValues();
231         return ((attributeValues != null) && (!attributeValues.isEmpty()) && (attributeValues.get(0) != null) && (attributeValues.get(0).getDOM() != null))
232                 ? attributeValues.get(0).getDOM().getTextContent()
233                 : null;
234     }
235 
236     private static PurposeOfUse[] extractPurposeOfUse(Attribute attribute, QName valueElementName) {
237         List<PurposeOfUse> targetCollection = new ArrayList<>();
238         for (XMLObject value : attribute.getAttributeValues()) {
239             if (value.getDOM() != null) {
240                 NodeList nodeList = value.getDOM().getElementsByTagNameNS(valueElementName.getNamespaceURI(), valueElementName.getLocalPart());
241                 for (int i = 0; i < nodeList.getLength(); ++i) {
242                     Element elem = (Element) nodeList.item(i);
243                     targetCollection.add(elementToPurposeOfUse(elem));
244                 }
245             }
246         }
247         return targetCollection.toArray(new PurposeOfUse[targetCollection.size()]);
248 
249     }
250 
251     private static PurposeOfUse elementToPurposeOfUse(Element element) {
252         return PurposeOfUse.of(
253                 element.getAttribute("code"),
254                 element.getAttribute("codeSystem"),
255                 element.getAttribute("displayName")
256         );
257     }
258 
259     private static List<ActiveParticipantRoleId> extractActiveParticipantRoleId(Attribute attribute, QName valueElementName) {
260         List<ActiveParticipantRoleId> result = new ArrayList<>();
261         for (XMLObject value : attribute.getAttributeValues()) {
262             if (value.getDOM() != null) {
263                 NodeList nodeList = value.getDOM().getElementsByTagNameNS(valueElementName.getNamespaceURI(), valueElementName.getLocalPart());
264                 for (int i = 0; i < nodeList.getLength(); ++i) {
265                     Element elem = (Element) nodeList.item(i);
266                     result.add(elementToActiveParticipantRoleId(elem));
267                 }
268             }
269         }
270         return result;
271     }
272 
273     private static ActiveParticipantRoleId elementToActiveParticipantRoleId(Element element) {
274         return ActiveParticipantRoleId.of(
275                 element.getAttribute("code"),
276                 element.getAttribute("codeSystem"),
277                 element.getAttribute("displayName")
278         );
279     }
280 }