View Javadoc
1   /*
2    * Copyright 2009 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.ws.cxf.audit;
17  
18  import lombok.Getter;
19  import lombok.Setter;
20  import org.apache.cxf.binding.soap.SoapMessage;
21  import org.apache.cxf.headers.Header;
22  import org.apache.cxf.message.Message;
23  import org.apache.cxf.security.transport.TLSSessionInfo;
24  import org.apache.cxf.transport.http.AbstractHTTPDestination;
25  import org.apache.cxf.ws.addressing.AddressingProperties;
26  import org.apache.cxf.ws.addressing.AttributedURIType;
27  import org.apache.cxf.ws.addressing.EndpointReferenceType;
28  import org.apache.cxf.ws.addressing.JAXWSAConstants;
29  import org.openehealth.ipf.commons.audit.AuditContext;
30  import org.openehealth.ipf.commons.core.config.Lookup;
31  import org.openehealth.ipf.commons.ihe.core.atna.AuditStrategy;
32  import org.openehealth.ipf.commons.ihe.ws.InterceptorUtils;
33  import org.openehealth.ipf.commons.ihe.ws.cxf.AbstractSafeInterceptor;
34  import org.slf4j.Logger;
35  import org.slf4j.LoggerFactory;
36  
37  import javax.naming.ldap.LdapName;
38  import javax.naming.ldap.Rdn;
39  import javax.servlet.http.HttpServletRequest;
40  import java.security.Principal;
41  import java.security.cert.Certificate;
42  import java.security.cert.X509Certificate;
43  import java.util.List;
44  
45  import static java.util.Objects.requireNonNull;
46  
47  /**
48   * Base class for all ATNA audit-related CXF interceptors.
49   *
50   * @author Dmytro Rud
51   */
52  abstract public class AbstractAuditInterceptor<T extends WsAuditDataset> extends AbstractSafeInterceptor {
53  
54      private static final transient Logger LOG = LoggerFactory.getLogger(AbstractAuditInterceptor.class);
55  
56  
57      /**
58       * Key used to store audit datasets in Web Service contexts.
59       */
60      public static final String DATASET_CONTEXT_KEY = AbstractAuditInterceptor.class.getName() + ".DATASET";
61  
62      /**
63       * Processor for extracting SAML tokens when XUA is used
64       */
65      @Getter
66      @Setter
67      private static XuaProcessor xuaProcessor = Lookup.lookup(XuaProcessor.class).orElse(XuaProcessor.NOOP);
68  
69      /**
70       * Audit strategy associated with this interceptor.
71       */
72      private final AuditStrategy<T> auditStrategy;
73  
74      @Getter
75      private final AuditContext auditContext;
76  
77      /**
78       * Constructor which sets a strategy.
79       *
80       * @param phase         the phase in which to use this interceptor.
81       * @param auditStrategy an audit strategy instance. <p><code>null</code> values are
82       *                      explicitly prohibited.
83       */
84      protected AbstractAuditInterceptor(String phase, AuditStrategy<T> auditStrategy, AuditContext auditContext) {
85          super(phase);
86          this.auditStrategy = requireNonNull(auditStrategy);
87          this.auditContext = requireNonNull(auditContext);
88      }
89  
90  
91      /**
92       * Returns an audit dataset instance which corresponds to the given message.
93       * <p>
94       * When no such instance is currently associated with the message, a new one
95       * will be created by means of the corresponding {@link AuditStrategy}
96       * and registered in the message's exchange.
97       *
98       * @param message CXF message currently handled by this interceptor.
99       * @return an audit dataset instance, or <code>null</code> when this instance
100      * could be neither obtained nor created from scratch.
101      */
102     protected T getAuditDataset(SoapMessage message) {
103         T auditDataset = InterceptorUtils.findContextualProperty(message, DATASET_CONTEXT_KEY);
104         if (auditDataset == null) {
105             auditDataset = getAuditStrategy().createAuditDataset();
106             if (auditDataset == null) {
107                 LOG.warn("Cannot obtain audit dataset instance, NPE is pending");
108                 return null;
109             }
110             message.getExchange().put(DATASET_CONTEXT_KEY, auditDataset);
111         }
112         return auditDataset;
113     }
114 
115 
116     /**
117      * Returns the audit strategy associated with this interceptor.
118      *
119      * @return an audit strategy instance or <code>null</code> when none configured.
120      */
121     protected AuditStrategy<T> getAuditStrategy() {
122         return auditStrategy;
123     }
124 
125 
126     /**
127      * Extracts user ID from an WS-Addressing SOAP header and stores it in the given
128      * audit dataset.
129      *
130      * @param message             CXF message.
131      * @param isInbound           <code>true</code> when the CXF message is an inbound one,
132      *                            <code>false</code> otherwise.
133      * @param inverseWsaDirection <code>true</code> when direction is actually inversed, i.e. when the
134      *                            user ID should be taken not from the "ReplyTo:" WS-Addressing header,
135      *                            but from "To:" --- useful for asynchronous responses, where the endpoint
136      *                            which receives the response is not the endpoint which sent the request.
137      * @param auditDataset        target audit dataset.
138      */
139     protected static void extractUserIdFromWSAddressing(
140             SoapMessage message,
141             boolean isInbound,
142             boolean inverseWsaDirection,
143             WsAuditDataset auditDataset) {
144         AddressingProperties wsaProperties = (AddressingProperties) message.get(isInbound ?
145                 JAXWSAConstants.ADDRESSING_PROPERTIES_INBOUND :
146                 JAXWSAConstants.ADDRESSING_PROPERTIES_OUTBOUND);
147 
148         if (wsaProperties != null) {
149             AttributedURIType address = null;
150             if (inverseWsaDirection) {
151                 address = wsaProperties.getTo();
152             } else {
153                 EndpointReferenceType replyTo = wsaProperties.getReplyTo();
154                 if (replyTo != null) {
155                     address = replyTo.getAddress();
156                 }
157             }
158 
159             if (address != null) {
160                 auditDataset.setSourceUserId(address.getValue());
161             }
162         }
163         if (auditDataset.getSourceUserId() == null) {
164             LOG.info("Missing WS-Addressing headers");
165             auditDataset.setSourceUserId("unknown");
166         }
167     }
168 
169 
170     /**
171      * Extracts ITI-40 XUA user name from the SAML2 assertion contained
172      * in the given CXF message, and stores it in the ATNA audit dataset.
173      *
174      * @param message         source CXF message.
175      * @param headerDirection direction of the header containing the SAML2 assertion.
176      * @param auditDataset    target ATNA audit dataset.
177      */
178     protected static void extractXuaUserNameFromSaml2Assertion(
179             SoapMessage message,
180             Header.Direction headerDirection,
181             WsAuditDataset auditDataset) {
182         xuaProcessor.extractXuaUserNameFromSaml2Assertion(message, headerDirection, auditDataset);
183     }
184 
185 
186     /**
187      * Extracts service URI and client IP address from the servlet request.
188      */
189     protected static void extractAddressesFromServletRequest(
190             SoapMessage message,
191             WsAuditDataset auditDataset) {
192         HttpServletRequest request =
193                 (HttpServletRequest) message.get(AbstractHTTPDestination.HTTP_REQUEST);
194         auditDataset.setRemoteAddress(request.getRemoteAddr());
195         auditDataset.setLocalAddress(request.getRequestURL().toString());
196         auditDataset.setDestinationUserId(request.getRequestURL().toString());
197     }
198 
199     /**
200      * Extract TLS information from servlet request, if available
201      */
202     protected static void extractClientCertificateCommonName(
203             SoapMessage message,
204             WsAuditDataset auditDataset) {
205         TLSSessionInfo request = message.get(TLSSessionInfo.class);
206         if (request != null) {
207             Certificate[] certificates = request.getPeerCertificates();
208             if (certificates != null && certificates.length > 0) {
209                 try {
210                     X509Certificate certificate = (X509Certificate) certificates[0];
211                     Principal principal = certificate.getSubjectDN();
212                     String dn = principal.getName();
213                     LdapName ldapDN = new LdapName(dn);
214                     for (Rdn rdn : ldapDN.getRdns()) {
215                         if (rdn.getType().equalsIgnoreCase("CN")) {
216                             auditDataset.setSourceUserName((String) rdn.getValue());
217                             break;
218                         }
219                     }
220                 } catch (Exception e) {
221                     LOG.info("Could not extract CN from client certificate", e);
222                 }
223             }
224         }
225     }
226 
227     /**
228      * Extracts POJO from the given CXF message.
229      *
230      * @return POJO or <code>null</code> when none found.
231      */
232     protected static Object extractPojo(Message message) {
233         List<?> list = message.getContent(List.class);
234         return ((list == null) || list.isEmpty()) ? null : list.get(0);
235     }
236 }