diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/AuditingInterceptor.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/AuditingInterceptor.java index 7753145b527..6df48a7901d 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/AuditingInterceptor.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/AuditingInterceptor.java @@ -1,7 +1,30 @@ package ca.uhn.fhir.rest.server.interceptor; +/* + * #%L + * HAPI FHIR - Core Library + * %% + * Copyright (C) 2014 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 javax.servlet.http.HttpServletRequest; @@ -14,12 +37,16 @@ import ca.uhn.fhir.model.api.BundleEntry; import ca.uhn.fhir.model.api.IResource; 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.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.ResourceTypeEnum; 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.rest.client.interceptor.UserInfoInterceptor; import ca.uhn.fhir.rest.method.RequestDetails; @@ -30,10 +57,15 @@ 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; + private IAuditDataStore myDataStore = null; private Map>> myAuditableResources = new HashMap>>(); private boolean myClientParamsOptional = false; @@ -48,47 +80,61 @@ public class AuditingInterceptor extends InterceptorAdapter { @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; + } try{ log.info("Auditing bundle: " + theResponseObject + " from request " + theRequestDetails); - SecurityEvent auditEvent = new SecurityEvent(); - + SecurityEvent auditEvent = new SecurityEvent(); + auditEvent.setEvent(getEventInfo(theRequestDetails)); //get user info from request if available - boolean hasUserInfo = addParticipantToEvent(theServletRequest, auditEvent); - if(!hasUserInfo) return true; //no user to audit - throws exception if client params are required + Participant participant = getParticipant(theServletRequest); + if(participant == null) return true; //no user to audit - throws exception if client params are required - SecurityEventObjectLifecycleEnum lifecycle = mapResourceTypeToSecurityLifecycle(theRequestDetails.getResourceOperationType()); - boolean hasAuditableEntry = false; + SecurityEventObjectLifecycleEnum lifecycle = mapResourceTypeToSecurityLifecycle(theRequestDetails.getResourceOperationType()); byte[] query = getQueryFromRequestDetails(theRequestDetails); + List auditableObjects = new ArrayList(); for(BundleEntry entry: theResponseObject.getEntries()){ - IResource resource = entry.getResource(); - boolean hasAuditableEntryInResource = addResourceObjectToEvent(auditEvent, resource, lifecycle, query); - if(hasAuditableEntryInResource) hasAuditableEntry = true; + IResource resource = entry.getResource(); + ObjectElement auditableObject = getObjectElement(resource, lifecycle , query); + if(auditableObject != null) auditableObjects.add(auditableObject); } - if(!hasAuditableEntry) return true; //no PHI to audit - + if(!auditableObjects.isEmpty()) return true; //no PHI to audit + auditEvent.setObject(auditableObjects); store(auditEvent); - return true; + return true; //success }catch(Exception e){ log.error("Unable to audit resource: " + theResponseObject + " from request: " + theRequestDetails, e); - return false; + return false; //fail request } } - - private void store(SecurityEvent auditEvent) throws Exception { - if(myDataStore == null) throw new InternalErrorException("No data store provided to persist audit events"); - myDataStore.store(auditEvent); - } - + @Override public boolean outgoingResponse(RequestDetails theRequestDetails, IResource 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; + } 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) return true; //no user to audit - throws exception if client params are required + byte[] query = getQueryFromRequestDetails(theRequestDetails); SecurityEventObjectLifecycleEnum lifecycle = mapResourceTypeToSecurityLifecycle(theRequestDetails.getResourceOperationType()); - boolean hasAuditableEntry = addResourceObjectToEvent(auditEvent, theResponseObject, lifecycle , query); - if(!hasAuditableEntry) return true; //nothing to audit + ObjectElement auditableObject = getObjectElement(theResponseObject, lifecycle , query); + if(auditableObject == null) return true; //nothing to audit + List auditableObjects = new ArrayList(1); + auditableObjects.add(auditableObject); + auditEvent.setObject(auditableObjects); store(auditEvent); return true; }catch(Exception e){ @@ -96,8 +142,23 @@ public class AuditingInterceptor extends InterceptorAdapter { return false; } } + + + protected void store(SecurityEvent auditEvent) throws Exception { + if(myDataStore == null) throw new InternalErrorException("No data store provided to persist audit events"); + myDataStore.store(auditEvent); + } + + protected Event getEventInfo(RequestDetails theRequestDetails) { + Event event = new Event(); + event.setAction(mapResourceTypeToSecurityEventAction(theRequestDetails.getResourceOperationType())); + 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; + + } - private byte[] getQueryFromRequestDetails(RequestDetails theRequestDetails) { + protected byte[] getQueryFromRequestDetails(RequestDetails theRequestDetails) { byte[] query; try { query = theRequestDetails.getCompleteUrl().getBytes("UTF-8"); @@ -109,7 +170,7 @@ public class AuditingInterceptor extends InterceptorAdapter { } /** - * If the resource is considered an auditable resource containing PHI, add it as an object to the Security Event + * If the resource is considered an auditable resource containing PHI, create an ObjectElement, otherwise return null * @param auditEvent * @param resource * @param lifecycle @@ -118,7 +179,7 @@ public class AuditingInterceptor extends InterceptorAdapter { * @throws IllegalAccessException * @throws InstantiationException */ - protected boolean addResourceObjectToEvent(SecurityEvent auditEvent, IResource resource, SecurityEventObjectLifecycleEnum lifecycle, byte[] query) throws InstantiationException, IllegalAccessException { + protected ObjectElement getObjectElement(IResource resource, SecurityEventObjectLifecycleEnum lifecycle, byte[] query) throws InstantiationException, IllegalAccessException { ResourceTypeEnum resourceType = resource.getResourceType(); if(myAuditableResources.containsKey(resourceType)){ @@ -126,7 +187,7 @@ public class AuditingInterceptor extends InterceptorAdapter { IResourceAuditor auditableResource = (IResourceAuditor) myAuditableResources.get(resourceType).newInstance(); auditableResource.setResource(resource); if(auditableResource.isAuditable()){ - ObjectElement object = auditEvent.addObject(); + ObjectElement object = new ObjectElement(); object.setReference(new ResourceReferenceDt(resource.getId())); object.setLifecycle(lifecycle); object.setQuery(query); @@ -136,38 +197,54 @@ public class AuditingInterceptor extends InterceptorAdapter { object.setDescription(auditableResource.getDescription()); object.setDetail(auditableResource.getDetail()); object.setSensitivity(auditableResource.getSensitivity()); - return true; + return object; } } - return false; //not something we care to audit + return null; //not something we care to audit } - private boolean addParticipantToEvent(HttpServletRequest theServletRequest, SecurityEvent auditEvent) throws InvalidRequestException, NotImplementedException { + protected Participant getParticipant(HttpServletRequest theServletRequest) throws InvalidRequestException, NotImplementedException { if(theServletRequest.getHeader(Constants.HEADER_AUTHORIZATION) != null && theServletRequest.getHeader(Constants.HEADER_AUTHORIZATION).startsWith("OAuth")){ //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 false; //no auditing + 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 = auditEvent.addParticipant(); + Participant participant = new Participant(); participant.setUserId(userId); participant.setName(userName); ParticipantNetwork network = participant.getNetwork(); network.setType(SecurityEventParticipantNetworkTypeEnum.IP_ADDRESS); network.setIdentifier(userIp); - return true; + return participant; } - } - private SecurityEventObjectLifecycleEnum mapResourceTypeToSecurityLifecycle(RestfulOperationTypeEnum resourceOperationType) { + protected SecurityEventActionEnum mapResourceTypeToSecurityEventAction(RestfulOperationTypeEnum resourceOperationType) { + switch (resourceOperationType) { + 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 + } + } + + //do we need both SecurityEventObjectLifecycleEnum and SecurityEventActionEnum? probably not + protected SecurityEventObjectLifecycleEnum mapResourceTypeToSecurityLifecycle(RestfulOperationTypeEnum resourceOperationType) { switch (resourceOperationType) { case READ: return SecurityEventObjectLifecycleEnum.ACCESS_OR_USE; case CREATE: return SecurityEventObjectLifecycleEnum.ORIGINATION_OR_CREATION;