package ca.uhn.fhir.rest.server.interceptor; /* * #%L * HAPI FHIR Structures - DSTU1 (FHIR v0.80) * %% * Copyright (C) 2014 - 2017 University Health Network * %% * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * #L% */ import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang3.NotImplementedException; import org.hl7.fhir.instance.model.api.IBaseResource; import ca.uhn.fhir.model.api.Bundle; import ca.uhn.fhir.model.api.BundleEntry; import ca.uhn.fhir.model.api.IResource; import ca.uhn.fhir.model.dstu.composite.CodingDt; import ca.uhn.fhir.model.dstu.composite.IdentifierDt; import ca.uhn.fhir.model.dstu.composite.ResourceReferenceDt; import ca.uhn.fhir.model.dstu.resource.SecurityEvent; import ca.uhn.fhir.model.dstu.resource.SecurityEvent.Event; import ca.uhn.fhir.model.dstu.resource.SecurityEvent.ObjectDetail; import ca.uhn.fhir.model.dstu.resource.SecurityEvent.ObjectElement; import ca.uhn.fhir.model.dstu.resource.SecurityEvent.Participant; import ca.uhn.fhir.model.dstu.resource.SecurityEvent.ParticipantNetwork; import ca.uhn.fhir.model.dstu.resource.SecurityEvent.Source; import ca.uhn.fhir.model.dstu.valueset.RestfulOperationTypeEnum; import ca.uhn.fhir.model.dstu.valueset.SecurityEventActionEnum; import ca.uhn.fhir.model.dstu.valueset.SecurityEventObjectLifecycleEnum; import ca.uhn.fhir.model.dstu.valueset.SecurityEventOutcomeEnum; import ca.uhn.fhir.model.dstu.valueset.SecurityEventParticipantNetworkTypeEnum; import ca.uhn.fhir.model.dstu.valueset.SecurityEventSourceTypeEnum; import ca.uhn.fhir.model.primitive.StringDt; import ca.uhn.fhir.rest.client.interceptor.UserInfoInterceptor; import ca.uhn.fhir.rest.method.RequestDetails; import ca.uhn.fhir.rest.server.Constants; import ca.uhn.fhir.rest.server.audit.IResourceAuditor; import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.store.IAuditDataStore; /** * Server interceptor that provides auditing capability * * To use, set a data store that implements the IAuditDataStore interface, specify */ public class AuditingInterceptor extends InterceptorAdapter { private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AuditingInterceptor.class); private IAuditDataStore myDataStore = null; private Map<String, Class<? extends IResourceAuditor<? extends IResource>>> myAuditableResources = new HashMap<String, Class<? extends IResourceAuditor<? extends IResource>>>(); private boolean myClientParamsOptional = false; protected String mySiteId; /** * Create a new AuditingInterceptor with auditing required by default and a siteid of "NONE" */ public AuditingInterceptor() { mySiteId = "NONE"; myClientParamsOptional = false; } /** * Create a new AuditingInterceptor with auditing required by default and a site ID as specified * * @param theSiteId * the name of the site producing the audit data (will be set as SecurityEvent.Source.siteid) */ public AuditingInterceptor(String theSiteId) { mySiteId = theSiteId; myClientParamsOptional = false; } /** * Create a new AuditingInterceptor with the specified site ID and indicating if auditing is optional or required * * @param theSiteId * theSiteId the name of the site producing the audit data (will be set as SecurityEvent.Source.siteid) * @param theClientParamsOptional * - true if auditing is optional, false if auditing is required (will throw exception if user params are * not specified or audit store is not set) */ public AuditingInterceptor(String theSiteId, boolean theClientParamsOptional) { mySiteId = theSiteId; myClientParamsOptional = theClientParamsOptional; } /** * Intercept the outgoing response to perform auditing of the request data if the bundle contains auditable * resources. */ @Override public boolean outgoingResponse(RequestDetails theRequestDetails, Bundle theResponseObject, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) throws AuthenticationException { if (myClientParamsOptional && myDataStore == null) { // auditing is not required or configured, so do nothing here log.debug("No auditing configured."); return true; } if (theResponseObject == null || theResponseObject.isEmpty()) { log.debug("No bundle to audit"); return true; } try { log.info("Auditing bundle: " + theResponseObject + " from request " + theRequestDetails); SecurityEvent auditEvent = new SecurityEvent(); auditEvent.setEvent(getEventInfo(theRequestDetails)); // get user info from request if available Participant participant = getParticipant(theServletRequest); if (participant == null) { log.debug("No participant to audit"); return true; // no user to audit - throws exception if client params are required } List<Participant> participants = new ArrayList<SecurityEvent.Participant>(1); participants.add(participant); auditEvent.setParticipant(participants); SecurityEventObjectLifecycleEnum lifecycle = mapResourceTypeToSecurityLifecycle(theRequestDetails.getRestOperationType()); byte[] query = getQueryFromRequestDetails(theRequestDetails); List<ObjectElement> auditableObjects = new ArrayList<SecurityEvent.ObjectElement>(); for (BundleEntry entry : theResponseObject.getEntries()) { IResource resource = entry.getResource(); ObjectElement auditableObject = getObjectElement(resource, lifecycle, query); if (auditableObject != null) auditableObjects.add(auditableObject); } if (auditableObjects.isEmpty()) { log.debug("No auditable resources to audit."); return true; // no PHI to audit } else { log.debug("Auditing " + auditableObjects.size() + " resources."); } auditEvent.setObject(auditableObjects); auditEvent.setSource(getSourceElement(theServletRequest)); store(auditEvent); return true; // success } catch (Exception e) { log.error("Unable to audit resource: " + theResponseObject + " from request: " + theRequestDetails, e); throw new InternalErrorException("Auditing failed, unable to complete request", e); } } /** * Intercept the outgoing response to perform auditing of the request data if the resource is auditable. */ @Override public boolean outgoingResponse(RequestDetails theRequestDetails, IBaseResource theResponseObject, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) throws AuthenticationException { if (myClientParamsOptional && myDataStore == null) { // auditing is not required or configured, so do nothing here log.debug("No auditing configured."); return true; } if (theResponseObject == null) { log.debug("No object to audit"); return true; } try { log.info("Auditing resource: " + theResponseObject + " from request: " + theRequestDetails); SecurityEvent auditEvent = new SecurityEvent(); auditEvent.setEvent(getEventInfo(theRequestDetails)); // get user info from request if available Participant participant = getParticipant(theServletRequest); if (participant == null) { log.debug("No participant to audit"); return true; // no user to audit - throws exception if client params are required } List<Participant> participants = new ArrayList<SecurityEvent.Participant>(1); participants.add(participant); auditEvent.setParticipant(participants); byte[] query = getQueryFromRequestDetails(theRequestDetails); SecurityEventObjectLifecycleEnum lifecycle = mapResourceTypeToSecurityLifecycle(theRequestDetails.getRestOperationType()); ObjectElement auditableObject = getObjectElement((IResource) theResponseObject, lifecycle, query); if (auditableObject == null) { log.debug("No auditable resources to audit"); return true; } List<ObjectElement> auditableObjects = new ArrayList<SecurityEvent.ObjectElement>(1); auditableObjects.add(auditableObject); auditEvent.setObject(auditableObjects); auditEvent.setSource(getSourceElement(theServletRequest)); log.debug("Auditing one resource."); store(auditEvent); return true; } catch (Exception e) { log.error("Unable to audit resource: " + theResponseObject + " from request: " + theRequestDetails, e); throw new InternalErrorException("Auditing failed, unable to complete request", e); } } /** * Store the SecurityEvent generated for this request to a persistent store (database, file, JMS, etc) * * @param auditEvent * the SecurityEvent generated for this request * @throws Exception * if the data store is not configured or if an error occurs during storage */ protected void store(SecurityEvent auditEvent) throws Exception { if (myDataStore == null) throw new InternalErrorException("No data store provided to persist audit events"); myDataStore.store(auditEvent); } /** * Generates the Event segment of the SecurityEvent based on the incoming request details * * @param theRequestDetails * the RequestDetails of the incoming request * @return an Event populated with the action, date, and outcome */ protected Event getEventInfo(RequestDetails theRequestDetails) { Event event = new Event(); event.setAction(mapResourceTypeToSecurityEventAction(theRequestDetails.getRestOperationType())); event.setDateTimeWithMillisPrecision(new Date()); event.setOutcome(SecurityEventOutcomeEnum.SUCCESS); // we audit successful return of PHI only, otherwise an // exception is thrown and no resources are returned to be // audited return event; } /** * Return the query URL encoded to bytes to be set as the Object.query on the Security Event * * @param theRequestDetails * the RequestDetails of the incoming request * @return the query URL of the request encoded to bytes */ protected byte[] getQueryFromRequestDetails(RequestDetails theRequestDetails) { byte[] query; try { query = theRequestDetails.getCompleteUrl().getBytes("UTF-8"); } catch (UnsupportedEncodingException e1) { log.warn("Unable to encode URL to bytes in UTF-8, defaulting to platform default charset.", e1); query = theRequestDetails.getCompleteUrl().getBytes(); } return query; } /** * If the resource is considered an auditable resource containing PHI, create an ObjectElement, otherwise return null * * @param resource * the resource to be audited * @param lifecycle * the SecurityEventObjectLifecycleEnum of the request * @param query * the byte encoded query string of the request * @return an ObjectElement populated with information about the resource * @throws IllegalAccessException * if the auditor for this resource cannot be instantiated * @throws InstantiationException * if the auditor for this resource cannot be instantiated */ protected ObjectElement getObjectElement(IResource resource, SecurityEventObjectLifecycleEnum lifecycle, byte[] query) throws InstantiationException, IllegalAccessException { String resourceType = resource.getResourceName(); if (myAuditableResources.containsKey(resourceType)) { log.debug("Found auditable resource of type: " + resourceType); @SuppressWarnings("unchecked") IResourceAuditor<IResource> auditableResource = (IResourceAuditor<IResource>) myAuditableResources.get(resourceType).newInstance(); auditableResource.setResource(resource); if (auditableResource.isAuditable()) { ObjectElement object = new ObjectElement(); object.setReference(new ResourceReferenceDt(resource.getId())); object.setLifecycle(lifecycle); object.setQuery(query); object.setName(auditableResource.getName()); object.setIdentifier((IdentifierDt) auditableResource.getIdentifier()); object.setType(auditableResource.getType()); object.setDescription(auditableResource.getDescription()); Map<String, String> detailMap = auditableResource.getDetail(); if (detailMap != null && !detailMap.isEmpty()) { List<ObjectDetail> details = new ArrayList<SecurityEvent.ObjectDetail>(); for (Entry<String, String> entry : detailMap.entrySet()) { ObjectDetail detail = makeObjectDetail(entry.getKey(), entry.getValue()); details.add(detail); } object.setDetail(details); } if (auditableResource.getSensitivity() != null) { CodingDt coding = object.getSensitivity().addCoding(); coding.setSystem(auditableResource.getSensitivity().getSystemElement().getValue()); coding.setCode(auditableResource.getSensitivity().getCodeElement().getValue()); coding.setDisplay(auditableResource.getSensitivity().getDisplayElement().getValue()); } return object; } else { log.debug("Resource is not auditable"); } } else { log.debug("No auditor configured for resource type " + resourceType); } return null; // not something we care to audit } /** * Helper method to create an ObjectDetail from a pair of Strings. * * @param type * the type of the ObjectDetail * @param value * the value of the ObejctDetail to be encoded * @return an ObjectDetail of the given type and value */ protected ObjectDetail makeObjectDetail(String type, String value) { ObjectDetail detail = new ObjectDetail(); if (type != null) detail.setType(type); if (value != null) detail.setValue(value.getBytes()); return detail; } /** * Create a participant object based on information from the incoming servlet request * * @param theServletRequest * the incoming request * @return a Participant object populated with the user id, user name, and IP * @throws InvalidRequestException * if auditing is required but userId is not specified as a request header * @throws NotImplementedException * if the authorization type is OAuth */ protected Participant getParticipant(HttpServletRequest theServletRequest) throws InvalidRequestException, NotImplementedException { // TODO: move the specification of auth params into another interceptor separate from auditing? if (theServletRequest.getHeader(Constants.HEADER_AUTHORIZATION) != null && theServletRequest.getHeader(Constants.HEADER_AUTHORIZATION).startsWith("OAuth")) { if (myClientParamsOptional) { log.debug("OAuth request received but no auditing required."); return null; } // TODO: get user info from token throw new NotImplementedException("OAuth user auditing not yet implemented."); } else { // no auth or basic auth or anything else, use HTTP headers for user info String userId = theServletRequest.getHeader(UserInfoInterceptor.HEADER_USER_ID); if (userId == null) { if (myClientParamsOptional) return null; // no auditing else throw new InvalidRequestException(UserInfoInterceptor.HEADER_USER_ID + " must be specified as an HTTP header to access PHI."); } String userName = theServletRequest.getHeader(UserInfoInterceptor.HEADER_USER_NAME); if (userName == null) userName = "Anonymous"; String userIp = theServletRequest.getRemoteAddr(); Participant participant = new Participant(); participant.setUserId(userId); participant.setName(userName); ParticipantNetwork network = participant.getNetwork(); network.setType(SecurityEventParticipantNetworkTypeEnum.IP_ADDRESS); network.setIdentifier(userIp); return participant; } } /** * Create a Source object based on information in the incoming request * * @param theServletRequest * the incoming request * @return a Source object with the identifier, type, and site specified * @throws NotImplementedException * if the authorization type is OAuth */ protected Source getSourceElement(HttpServletRequest theServletRequest) throws NotImplementedException { if (theServletRequest.getHeader(Constants.HEADER_AUTHORIZATION) != null && theServletRequest.getHeader(Constants.HEADER_AUTHORIZATION).startsWith("OAuth")) { if (myClientParamsOptional) return null; // no auditing required // TODO: get application info from token throw new NotImplementedException("OAuth auditing not yet implemented."); } else { // no auth or basic auth or anything else, use HTTP headers for audit info String appId = theServletRequest.getHeader(UserInfoInterceptor.HEADER_APPLICATION_NAME); Source source = new Source(); source.setIdentifier(appId); source.setType(getAccessType(theServletRequest)); source.setSite(getSiteId(theServletRequest)); return source; } } /** * Returns the site ID (for SecurityEvent.source.siteid) * * @param theServletRequest * the incoming request * @return the site ID to audit. By default, uses the value set in the constructor. Override this method to set the * value via request parameters. */ protected StringDt getSiteId(HttpServletRequest theServletRequest) { return new StringDt(mySiteId); // override this method to pull the site id from the request info } /** * Return the access type for this request * * @param theServletRequest * the incoming request * @return the SecurityEventSourceTypeEnum representing the type of request being made */ protected List<CodingDt> getAccessType(HttpServletRequest theServletRequest) { List<CodingDt> types = new ArrayList<CodingDt>(); if (theServletRequest.getHeader(Constants.HEADER_AUTHORIZATION) != null && theServletRequest.getHeader(Constants.HEADER_AUTHORIZATION).startsWith("OAuth")) { types.add(new CodingDt(SecurityEventSourceTypeEnum.USER_DEVICE.getSystem(), SecurityEventSourceTypeEnum.USER_DEVICE.getCode())); } else { String userId = theServletRequest.getHeader(UserInfoInterceptor.HEADER_USER_ID); String appId = theServletRequest.getHeader(UserInfoInterceptor.HEADER_APPLICATION_NAME); if (userId == null && appId != null) types.add(new CodingDt(SecurityEventSourceTypeEnum.APPLICATION_SERVER.getSystem(), SecurityEventSourceTypeEnum.APPLICATION_SERVER.getCode())); else types.add(new CodingDt(SecurityEventSourceTypeEnum.USER_DEVICE.getSystem(), SecurityEventSourceTypeEnum.USER_DEVICE.getCode())); } return types; } /** * Returns the SecurityEventActionEnum corresponding to the specified RestfulOperationTypeEnum * * @param theRestfulOperationTypeEnum * the type of operation (Read, Create, Delete, etc) * @return the corresponding SecurityEventActionEnum (Read/View/Print, Create, Delete, etc) */ protected SecurityEventActionEnum mapResourceTypeToSecurityEventAction(ca.uhn.fhir.rest.api.RestOperationTypeEnum theRestfulOperationTypeEnum) { if (theRestfulOperationTypeEnum == null) return null; switch (theRestfulOperationTypeEnum) { case READ: return SecurityEventActionEnum.READ_VIEW_PRINT; case CREATE: return SecurityEventActionEnum.CREATE; case DELETE: return SecurityEventActionEnum.DELETE; case HISTORY_INSTANCE: return SecurityEventActionEnum.READ_VIEW_PRINT; case HISTORY_TYPE: return SecurityEventActionEnum.READ_VIEW_PRINT; case SEARCH_TYPE: return SecurityEventActionEnum.READ_VIEW_PRINT; case UPDATE: return SecurityEventActionEnum.UPDATE; case VALIDATE: return SecurityEventActionEnum.READ_VIEW_PRINT; case VREAD: return SecurityEventActionEnum.READ_VIEW_PRINT; default: return SecurityEventActionEnum.READ_VIEW_PRINT; // read/view catch all } } /** * Returns the SecurityEventObjectLifecycleEnum corresponding to the specified RestfulOperationTypeEnum * * @param theRestfulOperationTypeEnum * the type of operation (Read, Create, Delete, etc) * @return the corresponding SecurityEventObjectLifecycleEnum (Access/Use, Origination/Creation, Logical Deletion, * etc) */ protected SecurityEventObjectLifecycleEnum mapResourceTypeToSecurityLifecycle(ca.uhn.fhir.rest.api.RestOperationTypeEnum theRestfulOperationTypeEnum) { if (theRestfulOperationTypeEnum == null) return null; switch (theRestfulOperationTypeEnum) { case READ: return SecurityEventObjectLifecycleEnum.ACCESS_OR_USE; case CREATE: return SecurityEventObjectLifecycleEnum.ORIGINATION_OR_CREATION; case DELETE: return SecurityEventObjectLifecycleEnum.LOGICAL_DELETION; case HISTORY_INSTANCE: return SecurityEventObjectLifecycleEnum.ACCESS_OR_USE; case HISTORY_TYPE: return SecurityEventObjectLifecycleEnum.ACCESS_OR_USE; case SEARCH_TYPE: return SecurityEventObjectLifecycleEnum.ACCESS_OR_USE; case UPDATE: return SecurityEventObjectLifecycleEnum.AMENDMENT; case VALIDATE: return SecurityEventObjectLifecycleEnum.VERIFICATION; case VREAD: return SecurityEventObjectLifecycleEnum.ACCESS_OR_USE; default: return SecurityEventObjectLifecycleEnum.ACCESS_OR_USE; // access/use catch all } } /** * Provide a persistent store implementing IAuditDataStore to store the audit events * * @param theDataStore * an implementation of IAuditDataStore to be used to store the audit events */ public void setDataStore(IAuditDataStore theDataStore) { myDataStore = theDataStore; } /** * Get this auditor's map of auditable resources and auditors * * @return the map of auditable resource types to auditors */ public Map<String, Class<? extends IResourceAuditor<? extends IResource>>> getAuditableResources() { return myAuditableResources; } /** * Specify which types of resources are to be audited (Patient, Encounter, etc) and provide an IResourceAuditor for * each type * * @param theAuditableResources * a map of resources to be audited and their corresponding IResouceAuditors */ public void setAuditableResources(Map<String, Class<? extends IResourceAuditor<? extends IResource>>> theAuditableResources) { myAuditableResources = theAuditableResources; } /** * Add a type of auditable resource and its auditor to this AuditingInterceptor's resource map * * @param resourceType * the type of resource to be audited (Patient, Encounter, etc) * @param auditableResource * an implementation of IResourceAuditor that can audit resources of the given type */ public void addAuditableResource(String resourceType, Class<? extends IResourceAuditor<? extends IResource>> auditableResource) { if (myAuditableResources == null) myAuditableResources = new HashMap<String, Class<? extends IResourceAuditor<? extends IResource>>>(); myAuditableResources.put(resourceType, auditableResource); } }