refactored auditing interceptor, added additional securityevent info
This commit is contained in:
parent
730ff7b21a
commit
0c7cf18914
|
@ -1,7 +1,30 @@
|
||||||
package ca.uhn.fhir.rest.server.interceptor;
|
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.io.UnsupportedEncodingException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
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.api.IResource;
|
||||||
import ca.uhn.fhir.model.dstu.composite.ResourceReferenceDt;
|
import ca.uhn.fhir.model.dstu.composite.ResourceReferenceDt;
|
||||||
import ca.uhn.fhir.model.dstu.resource.SecurityEvent;
|
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.ObjectElement;
|
||||||
import ca.uhn.fhir.model.dstu.resource.SecurityEvent.Participant;
|
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.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.ResourceTypeEnum;
|
||||||
import ca.uhn.fhir.model.dstu.valueset.RestfulOperationTypeEnum;
|
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.SecurityEventObjectLifecycleEnum;
|
||||||
|
import ca.uhn.fhir.model.dstu.valueset.SecurityEventOutcomeEnum;
|
||||||
import ca.uhn.fhir.model.dstu.valueset.SecurityEventParticipantNetworkTypeEnum;
|
import ca.uhn.fhir.model.dstu.valueset.SecurityEventParticipantNetworkTypeEnum;
|
||||||
import ca.uhn.fhir.rest.client.interceptor.UserInfoInterceptor;
|
import ca.uhn.fhir.rest.client.interceptor.UserInfoInterceptor;
|
||||||
import ca.uhn.fhir.rest.method.RequestDetails;
|
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.rest.server.exceptions.InvalidRequestException;
|
||||||
import ca.uhn.fhir.store.IAuditDataStore;
|
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 {
|
public class AuditingInterceptor extends InterceptorAdapter {
|
||||||
private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AuditingInterceptor.class);
|
private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AuditingInterceptor.class);
|
||||||
|
|
||||||
private IAuditDataStore myDataStore;
|
private IAuditDataStore myDataStore = null;
|
||||||
private Map<ResourceTypeEnum, Class<? extends IResourceAuditor<? extends IResource>>> myAuditableResources = new HashMap<ResourceTypeEnum, Class<? extends IResourceAuditor<? extends IResource>>>();
|
private Map<ResourceTypeEnum, Class<? extends IResourceAuditor<? extends IResource>>> myAuditableResources = new HashMap<ResourceTypeEnum, Class<? extends IResourceAuditor<? extends IResource>>>();
|
||||||
private boolean myClientParamsOptional = false;
|
private boolean myClientParamsOptional = false;
|
||||||
|
|
||||||
|
@ -48,47 +80,61 @@ public class AuditingInterceptor extends InterceptorAdapter {
|
||||||
@Override
|
@Override
|
||||||
public boolean outgoingResponse(RequestDetails theRequestDetails, Bundle theResponseObject, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse)
|
public boolean outgoingResponse(RequestDetails theRequestDetails, Bundle theResponseObject, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse)
|
||||||
throws AuthenticationException {
|
throws AuthenticationException {
|
||||||
|
if(myClientParamsOptional && myDataStore == null){
|
||||||
|
//auditing is not required or configured, so do nothing here
|
||||||
|
log.debug("No auditing configured.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
try{
|
try{
|
||||||
log.info("Auditing bundle: " + theResponseObject + " from request " + theRequestDetails);
|
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
|
//get user info from request if available
|
||||||
boolean hasUserInfo = addParticipantToEvent(theServletRequest, auditEvent);
|
Participant participant = getParticipant(theServletRequest);
|
||||||
if(!hasUserInfo) return true; //no user to audit - throws exception if client params are required
|
if(participant == null) return true; //no user to audit - throws exception if client params are required
|
||||||
|
|
||||||
SecurityEventObjectLifecycleEnum lifecycle = mapResourceTypeToSecurityLifecycle(theRequestDetails.getResourceOperationType());
|
SecurityEventObjectLifecycleEnum lifecycle = mapResourceTypeToSecurityLifecycle(theRequestDetails.getResourceOperationType());
|
||||||
boolean hasAuditableEntry = false;
|
|
||||||
byte[] query = getQueryFromRequestDetails(theRequestDetails);
|
byte[] query = getQueryFromRequestDetails(theRequestDetails);
|
||||||
|
List<ObjectElement> auditableObjects = new ArrayList<SecurityEvent.ObjectElement>();
|
||||||
for(BundleEntry entry: theResponseObject.getEntries()){
|
for(BundleEntry entry: theResponseObject.getEntries()){
|
||||||
IResource resource = entry.getResource();
|
IResource resource = entry.getResource();
|
||||||
boolean hasAuditableEntryInResource = addResourceObjectToEvent(auditEvent, resource, lifecycle, query);
|
ObjectElement auditableObject = getObjectElement(resource, lifecycle , query);
|
||||||
if(hasAuditableEntryInResource) hasAuditableEntry = true;
|
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);
|
store(auditEvent);
|
||||||
return true;
|
return true; //success
|
||||||
}catch(Exception e){
|
}catch(Exception e){
|
||||||
log.error("Unable to audit resource: " + theResponseObject + " from request: " + theRequestDetails, 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
|
@Override
|
||||||
public boolean outgoingResponse(RequestDetails theRequestDetails, IResource theResponseObject, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse)
|
public boolean outgoingResponse(RequestDetails theRequestDetails, IResource theResponseObject, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse)
|
||||||
throws AuthenticationException {
|
throws AuthenticationException {
|
||||||
|
if(myClientParamsOptional && myDataStore == null){
|
||||||
|
//auditing is not required or configured, so do nothing here
|
||||||
|
log.debug("No auditing configured.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
try{
|
try{
|
||||||
log.info("Auditing resource: " + theResponseObject + " from request: " + theRequestDetails);
|
log.info("Auditing resource: " + theResponseObject + " from request: " + theRequestDetails);
|
||||||
SecurityEvent auditEvent = new SecurityEvent();
|
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);
|
byte[] query = getQueryFromRequestDetails(theRequestDetails);
|
||||||
SecurityEventObjectLifecycleEnum lifecycle = mapResourceTypeToSecurityLifecycle(theRequestDetails.getResourceOperationType());
|
SecurityEventObjectLifecycleEnum lifecycle = mapResourceTypeToSecurityLifecycle(theRequestDetails.getResourceOperationType());
|
||||||
boolean hasAuditableEntry = addResourceObjectToEvent(auditEvent, theResponseObject, lifecycle , query);
|
ObjectElement auditableObject = getObjectElement(theResponseObject, lifecycle , query);
|
||||||
if(!hasAuditableEntry) return true; //nothing to audit
|
if(auditableObject == null) return true; //nothing to audit
|
||||||
|
List<ObjectElement> auditableObjects = new ArrayList<SecurityEvent.ObjectElement>(1);
|
||||||
|
auditableObjects.add(auditableObject);
|
||||||
|
auditEvent.setObject(auditableObjects);
|
||||||
store(auditEvent);
|
store(auditEvent);
|
||||||
return true;
|
return true;
|
||||||
}catch(Exception e){
|
}catch(Exception e){
|
||||||
|
@ -96,8 +142,23 @@ public class AuditingInterceptor extends InterceptorAdapter {
|
||||||
return false;
|
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;
|
byte[] query;
|
||||||
try {
|
try {
|
||||||
query = theRequestDetails.getCompleteUrl().getBytes("UTF-8");
|
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 auditEvent
|
||||||
* @param resource
|
* @param resource
|
||||||
* @param lifecycle
|
* @param lifecycle
|
||||||
|
@ -118,7 +179,7 @@ public class AuditingInterceptor extends InterceptorAdapter {
|
||||||
* @throws IllegalAccessException
|
* @throws IllegalAccessException
|
||||||
* @throws InstantiationException
|
* @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();
|
ResourceTypeEnum resourceType = resource.getResourceType();
|
||||||
if(myAuditableResources.containsKey(resourceType)){
|
if(myAuditableResources.containsKey(resourceType)){
|
||||||
|
@ -126,7 +187,7 @@ public class AuditingInterceptor extends InterceptorAdapter {
|
||||||
IResourceAuditor<IResource> auditableResource = (IResourceAuditor<IResource>) myAuditableResources.get(resourceType).newInstance();
|
IResourceAuditor<IResource> auditableResource = (IResourceAuditor<IResource>) myAuditableResources.get(resourceType).newInstance();
|
||||||
auditableResource.setResource(resource);
|
auditableResource.setResource(resource);
|
||||||
if(auditableResource.isAuditable()){
|
if(auditableResource.isAuditable()){
|
||||||
ObjectElement object = auditEvent.addObject();
|
ObjectElement object = new ObjectElement();
|
||||||
object.setReference(new ResourceReferenceDt(resource.getId()));
|
object.setReference(new ResourceReferenceDt(resource.getId()));
|
||||||
object.setLifecycle(lifecycle);
|
object.setLifecycle(lifecycle);
|
||||||
object.setQuery(query);
|
object.setQuery(query);
|
||||||
|
@ -136,38 +197,54 @@ public class AuditingInterceptor extends InterceptorAdapter {
|
||||||
object.setDescription(auditableResource.getDescription());
|
object.setDescription(auditableResource.getDescription());
|
||||||
object.setDetail(auditableResource.getDetail());
|
object.setDetail(auditableResource.getDetail());
|
||||||
object.setSensitivity(auditableResource.getSensitivity());
|
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")){
|
if(theServletRequest.getHeader(Constants.HEADER_AUTHORIZATION) != null && theServletRequest.getHeader(Constants.HEADER_AUTHORIZATION).startsWith("OAuth")){
|
||||||
//TODO: get user info from token
|
//TODO: get user info from token
|
||||||
throw new NotImplementedException("OAuth user auditing not yet implemented.");
|
throw new NotImplementedException("OAuth user auditing not yet implemented.");
|
||||||
}else { //no auth or basic auth or anything else, use HTTP headers for user info
|
}else { //no auth or basic auth or anything else, use HTTP headers for user info
|
||||||
String userId = theServletRequest.getHeader(UserInfoInterceptor.HEADER_USER_ID);
|
String userId = theServletRequest.getHeader(UserInfoInterceptor.HEADER_USER_ID);
|
||||||
if(userId == null){
|
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.");
|
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);
|
String userName = theServletRequest.getHeader(UserInfoInterceptor.HEADER_USER_NAME);
|
||||||
if(userName == null) userName = "Anonymous";
|
if(userName == null) userName = "Anonymous";
|
||||||
String userIp = theServletRequest.getRemoteAddr();
|
String userIp = theServletRequest.getRemoteAddr();
|
||||||
Participant participant = auditEvent.addParticipant();
|
Participant participant = new Participant();
|
||||||
participant.setUserId(userId);
|
participant.setUserId(userId);
|
||||||
participant.setName(userName);
|
participant.setName(userName);
|
||||||
ParticipantNetwork network = participant.getNetwork();
|
ParticipantNetwork network = participant.getNetwork();
|
||||||
network.setType(SecurityEventParticipantNetworkTypeEnum.IP_ADDRESS);
|
network.setType(SecurityEventParticipantNetworkTypeEnum.IP_ADDRESS);
|
||||||
network.setIdentifier(userIp);
|
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) {
|
switch (resourceOperationType) {
|
||||||
case READ: return SecurityEventObjectLifecycleEnum.ACCESS_OR_USE;
|
case READ: return SecurityEventObjectLifecycleEnum.ACCESS_OR_USE;
|
||||||
case CREATE: return SecurityEventObjectLifecycleEnum.ORIGINATION_OR_CREATION;
|
case CREATE: return SecurityEventObjectLifecycleEnum.ORIGINATION_OR_CREATION;
|
||||||
|
|
Loading…
Reference in New Issue