added AuditingInterceptor and IAuditDataStore with various ResourceAuditors to provide pluggable auditing functionality to FHIR server

This commit is contained in:
lmds1 2014-09-24 15:52:48 -04:00
parent cb8dca13d2
commit e0d160ad7e
8 changed files with 630 additions and 0 deletions

View File

@ -0,0 +1,79 @@
package ca.uhn.fhir.rest.server.audit;
import java.util.ArrayList;
import java.util.List;
import ca.uhn.fhir.model.dstu.composite.IdentifierDt;
import ca.uhn.fhir.model.dstu.resource.DiagnosticReport;
import ca.uhn.fhir.model.dstu.resource.SecurityEvent.ObjectDetail;
import ca.uhn.fhir.model.dstu.valueset.SecurityEventObjectSensitivityEnum;
import ca.uhn.fhir.model.dstu.valueset.SecurityEventObjectTypeEnum;
public class DiagnosticReportAuditor implements IResourceAuditor<DiagnosticReport> {
DiagnosticReport myDiagnosticReport;
@Override
public boolean isAuditable() {
return myDiagnosticReport != null;
}
@Override
public String getName() {
if(myDiagnosticReport != null){
return myDiagnosticReport.getName().getText().getValue();
}
return null;
}
@Override
public IdentifierDt getIdentifier() {
if(myDiagnosticReport != null){
return myDiagnosticReport.getIdentifier();
}
return null;
}
@Override
public SecurityEventObjectTypeEnum getType() {
return SecurityEventObjectTypeEnum.OTHER;
}
@Override
public DiagnosticReport getResource() {
return myDiagnosticReport;
}
@Override
public void setResource(DiagnosticReport theDiagnosticReport) {
myDiagnosticReport = theDiagnosticReport;
}
@Override
public String getDescription() {
return null; //name and ID should suffice for audit purposes
}
@Override
public List<ObjectDetail> getDetail() {
List<ObjectDetail> details = new ArrayList<ObjectDetail>();
details.add(makeObjectDetail("dateIssued", myDiagnosticReport.getIssued().getValueAsString()));
details.add(makeObjectDetail("version", myDiagnosticReport.getId().getVersionIdPart()));
return details;
}
@Override
public SecurityEventObjectSensitivityEnum getSensitivity() {
return null; //no sensitivity indicated
}
private 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;
}
}

View File

@ -0,0 +1,93 @@
package ca.uhn.fhir.rest.server.audit;
import java.util.ArrayList;
import java.util.List;
import ca.uhn.fhir.model.dstu.composite.IdentifierDt;
import ca.uhn.fhir.model.dstu.resource.Encounter;
import ca.uhn.fhir.model.dstu.resource.SecurityEvent.ObjectDetail;
import ca.uhn.fhir.model.dstu.valueset.SecurityEventObjectSensitivityEnum;
import ca.uhn.fhir.model.dstu.valueset.SecurityEventObjectTypeEnum;
public class EncounterAuditor implements IResourceAuditor<Encounter> {
private Encounter myEncounter;
@Override
public Encounter getResource() {
return myEncounter;
}
@Override
public void setResource(Encounter theEncounter) {
myEncounter = theEncounter;
}
@Override
public boolean isAuditable() {
return myEncounter != null;
}
@Override
public String getName() {
if(myEncounter != null){
String id = myEncounter.getIdentifierFirstRep().getValue().getValue();
String system = myEncounter.getIdentifierFirstRep().getSystem().getValueAsString();
String service = myEncounter.getServiceProvider().getDisplay().getValue();
return id + "/" + system + ": " + service;
}
return null;
}
@Override
public IdentifierDt getIdentifier() {
if(myEncounter != null){
return myEncounter.getIdentifierFirstRep();
}
return null;
}
@Override
public SecurityEventObjectTypeEnum getType() {
return SecurityEventObjectTypeEnum.OTHER;
}
@Override
public String getDescription() {
if(myEncounter != null){
String type = myEncounter.getTypeFirstRep().getText().getValue();
String status = myEncounter.getStatus().getValueAsString();
String startDate = myEncounter.getPeriod().getStart().getValueAsString();
String endDate = myEncounter.getPeriod().getEnd().getValueAsString();
return type + ": " + status +", "+ startDate + " - " + endDate;
}
return null;
}
@Override
public List<ObjectDetail> getDetail() {
List<ObjectDetail> details = new ArrayList<ObjectDetail>();
details.add(makeObjectDetail("startDate", myEncounter.getPeriod().getStart().getValueAsString()));
details.add(makeObjectDetail("endDate", myEncounter.getPeriod().getEnd().getValueAsString()));
details.add(makeObjectDetail("service", myEncounter.getServiceProvider().getDisplay().getValue()));
details.add(makeObjectDetail("type", myEncounter.getTypeFirstRep().getText().getValue()));
details.add(makeObjectDetail("status", myEncounter.getStatus().getValueAsString()));
return details;
}
private 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;
}
@Override
public SecurityEventObjectSensitivityEnum getSensitivity() {
//override this method to provide sensitivity information about the visit
return null;
}
}

View File

@ -0,0 +1,26 @@
package ca.uhn.fhir.rest.server.audit;
import java.util.List;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.dstu.composite.IdentifierDt;
import ca.uhn.fhir.model.dstu.resource.SecurityEvent.ObjectDetail;
import ca.uhn.fhir.model.dstu.valueset.SecurityEventObjectSensitivityEnum;
import ca.uhn.fhir.model.dstu.valueset.SecurityEventObjectTypeEnum;
public interface IResourceAuditor<T extends IResource> {
public T getResource();
public void setResource(T resource);
public boolean isAuditable();
public String getName();
public IdentifierDt getIdentifier();
public SecurityEventObjectTypeEnum getType();
public String getDescription();
public List<ObjectDetail> getDetail();
public SecurityEventObjectSensitivityEnum getSensitivity();
}

View File

@ -0,0 +1,73 @@
package ca.uhn.fhir.rest.server.audit;
import java.util.List;
import ca.uhn.fhir.model.dstu.composite.IdentifierDt;
import ca.uhn.fhir.model.dstu.resource.Medication;
import ca.uhn.fhir.model.dstu.resource.MedicationPrescription;
import ca.uhn.fhir.model.dstu.resource.SecurityEvent.ObjectDetail;
import ca.uhn.fhir.model.dstu.valueset.SecurityEventObjectSensitivityEnum;
import ca.uhn.fhir.model.dstu.valueset.SecurityEventObjectTypeEnum;
public class MedicationPrescriptionAuditor implements IResourceAuditor<MedicationPrescription> {
MedicationPrescription myMedicationPrescription;
@Override
public boolean isAuditable() {
return myMedicationPrescription != null; //if we have a medication prescription, we want to audit it
}
@Override
public String getName() {
if(myMedicationPrescription != null){
if(myMedicationPrescription.getMedication() != null){
Medication m = (Medication) myMedicationPrescription.getMedication().getResource();
if(m != null){
return m.getName().getValue();
}
}
return myMedicationPrescription.getId().getValueAsString(); //if we don't have a medication name, use the id as the name
}
return ""; //no medication prescription, nothing to do here
}
@Override
public IdentifierDt getIdentifier() {
if(myMedicationPrescription != null){
return myMedicationPrescription.getIdentifierFirstRep();
}
return null;
}
@Override
public SecurityEventObjectTypeEnum getType() {
return SecurityEventObjectTypeEnum.OTHER;
}
@Override
public MedicationPrescription getResource() {
return myMedicationPrescription;
}
@Override
public void setResource(MedicationPrescription theMedicationPrescription) {
myMedicationPrescription = theMedicationPrescription;
}
@Override
public String getDescription() {
return null; //name and ID should suffice for audit purposes
}
@Override
public List<ObjectDetail> getDetail() {
return null; //no additional details required for audit?
}
@Override
public SecurityEventObjectSensitivityEnum getSensitivity() {
return null; //no sensitivity indicated in MedicationPrescription
}
}

View File

@ -0,0 +1,71 @@
package ca.uhn.fhir.rest.server.audit;
import java.util.List;
import ca.uhn.fhir.model.dstu.composite.IdentifierDt;
import ca.uhn.fhir.model.dstu.resource.Medication;
import ca.uhn.fhir.model.dstu.resource.MedicationStatement;
import ca.uhn.fhir.model.dstu.resource.SecurityEvent.ObjectDetail;
import ca.uhn.fhir.model.dstu.valueset.SecurityEventObjectSensitivityEnum;
import ca.uhn.fhir.model.dstu.valueset.SecurityEventObjectTypeEnum;
public class MedicationStatementAuditor implements IResourceAuditor<MedicationStatement> {
MedicationStatement myMedicationStatement;
@Override
public boolean isAuditable() {
return myMedicationStatement != null;
}
@Override
public String getName() {
if(myMedicationStatement != null){
Medication m = (Medication) myMedicationStatement.getMedication().getResource();
if(m != null){
return m.getName().getValue();
}
return myMedicationStatement.getId().getValueAsString(); //if we don't have a medication name, use the id as the name
}
return null;
}
@Override
public IdentifierDt getIdentifier() {
if(myMedicationStatement != null){
return myMedicationStatement.getIdentifierFirstRep();
}
return null;
}
@Override
public SecurityEventObjectTypeEnum getType() {
return SecurityEventObjectTypeEnum.OTHER;
}
@Override
public MedicationStatement getResource() {
return myMedicationStatement;
}
@Override
public void setResource(MedicationStatement theMedicationPrescription) {
myMedicationStatement = theMedicationPrescription;
}
@Override
public String getDescription() {
return null; //name and ID should suffice for audit purposes
}
@Override
public List<ObjectDetail> getDetail() {
return null; //no additional details required for audit?
}
@Override
public SecurityEventObjectSensitivityEnum getSensitivity() {
return null; //no sensitivity indicated in MedicationStatement
}
}

View File

@ -0,0 +1,86 @@
package ca.uhn.fhir.rest.server.audit;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
import ca.uhn.fhir.model.dstu.composite.IdentifierDt;
import ca.uhn.fhir.model.dstu.resource.Patient;
import ca.uhn.fhir.model.dstu.resource.SecurityEvent.ObjectDetail;
import ca.uhn.fhir.model.dstu.valueset.SecurityEventObjectSensitivityEnum;
import ca.uhn.fhir.model.dstu.valueset.SecurityEventObjectTypeEnum;
public class PatientAuditor implements IResourceAuditor<Patient> {
private Patient myPatient;
@Override
public boolean isAuditable() {
return myPatient != null; //if we have a patient, we want to audit it
}
@Override
public Patient getResource() {
return myPatient;
}
@Override
public void setResource(Patient thePatient) {
myPatient = thePatient;
}
@Override
public String getName() {
if(myPatient != null){
return myPatient.getNameFirstRep().getText().getValue();
}
return null;
}
@Override
public IdentifierDt getIdentifier() {
if(myPatient != null){
return myPatient.getIdentifierFirstRep();
}
return null;
}
@Override
public SecurityEventObjectTypeEnum getType() {
return SecurityEventObjectTypeEnum.PERSON;
}
@Override
public String getDescription() {
return null; //name + identifier ought to suffice?
}
@Override
public List<ObjectDetail> getDetail() {
if(myPatient != null){
List<IdentifierDt> ids = myPatient.getIdentifier();
if(ids != null && !ids.isEmpty()){
List<ObjectDetail> detailList = new ArrayList<ObjectDetail>(ids.size());
for(IdentifierDt id: ids){
ObjectDetail detail = new ObjectDetail();
detail.setType(id.getSystem().getValueAsString());
try {
detail.setValue(id.getValue().getValueAsString().getBytes("UTF-8"));
} catch (UnsupportedEncodingException e) {
detail.setValue(id.getValue().getValueAsString().getBytes());
}
detailList.add(detail);
}
return detailList;
}
}
return null;
}
@Override
public SecurityEventObjectSensitivityEnum getSensitivity() {
return null; //override to include things like locked patient records
}
}

View File

@ -0,0 +1,185 @@
package ca.uhn.fhir.rest.server.interceptor;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.NotImplementedException;
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.ResourceReferenceDt;
import ca.uhn.fhir.model.dstu.resource.SecurityEvent;
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.valueset.ResourceTypeEnum;
import ca.uhn.fhir.model.dstu.valueset.RestfulOperationTypeEnum;
import ca.uhn.fhir.model.dstu.valueset.SecurityEventObjectLifecycleEnum;
import ca.uhn.fhir.model.dstu.valueset.SecurityEventParticipantNetworkTypeEnum;
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.store.IAuditDataStore;
public class AuditingInterceptor extends InterceptorAdapter {
private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AuditingInterceptor.class);
private IAuditDataStore myDataStore;
private Map<ResourceTypeEnum, Class<? extends IResourceAuditor<? extends IResource>>> myAuditableResources = new HashMap<ResourceTypeEnum, Class<? extends IResourceAuditor<? extends IResource>>>();
@Override
public boolean outgoingResponse(RequestDetails theRequestDetails, Bundle theResponseObject, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse)
throws AuthenticationException {
try{
log.info("Auditing bundle: " + theResponseObject + " from request " + theRequestDetails);
SecurityEvent auditEvent = new SecurityEvent();
SecurityEventObjectLifecycleEnum lifecycle = mapResourceTypeToSecurityLifecycle(theRequestDetails.getResourceOperationType());
boolean hasAuditableEntry = false;
byte[] query = getQueryFromRequestDetails(theRequestDetails);
for(BundleEntry entry: theResponseObject.getEntries()){
IResource resource = entry.getResource();
boolean hasAuditableEntryInResource = addResourceObjectToEvent(auditEvent, resource, lifecycle, query);
if(hasAuditableEntryInResource) hasAuditableEntry = true;
}
if(!hasAuditableEntry) return true; //no PHI to audit
addParticipantToEvent(theServletRequest, auditEvent);
store(auditEvent);
return true;
}catch(Exception e){
log.error("Unable to audit resource: " + theResponseObject + " from request: " + theRequestDetails, e);
return false;
}
}
private void store(SecurityEvent auditEvent) {
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 {
try{
log.info("Auditing resource: " + theResponseObject + " from request: " + theRequestDetails);
SecurityEvent auditEvent = new SecurityEvent();
byte[] query = getQueryFromRequestDetails(theRequestDetails);
SecurityEventObjectLifecycleEnum lifecycle = mapResourceTypeToSecurityLifecycle(theRequestDetails.getResourceOperationType());
boolean hasAuditableEntry = addResourceObjectToEvent(auditEvent, theResponseObject, lifecycle , query);
if(!hasAuditableEntry) return true; //nothing to audit
store(auditEvent);
return true;
}catch(Exception e){
log.error("Unable to audit resource: " + theResponseObject + " from request: " + theRequestDetails, e);
return false;
}
}
private 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, add it as an object to the Security Event
* @param auditEvent
* @param resource
* @param lifecycle
* @param query
* @return
* @throws IllegalAccessException
* @throws InstantiationException
*/
protected boolean addResourceObjectToEvent(SecurityEvent auditEvent, IResource resource, SecurityEventObjectLifecycleEnum lifecycle, byte[] query) throws InstantiationException, IllegalAccessException {
//TODO: get resource name from IResource -- James will put this in the model
//reference ResourceTypeEnum
ResourceTypeEnum resourceType = null; //resource.getResourceType();
if(myAuditableResources.containsKey(resourceType)){
@SuppressWarnings("unchecked")
IResourceAuditor<IResource> auditableResource = (IResourceAuditor<IResource>) myAuditableResources.get(resourceType).newInstance();
auditableResource.setResource(resource);
if(auditableResource.isAuditable()){
ObjectElement object = auditEvent.addObject();
object.setReference(new ResourceReferenceDt(resource.getId()));
object.setLifecycle(lifecycle);
object.setQuery(query);
object.setName(auditableResource.getName());
object.setIdentifier(auditableResource.getIdentifier());
object.setType(auditableResource.getType());
object.setDescription(auditableResource.getDescription());
object.setDetail(auditableResource.getDetail());
object.setSensitivity(auditableResource.getSensitivity());
return true;
}
}
return false; //not something we care to audit
}
private void addParticipantToEvent(HttpServletRequest theServletRequest, SecurityEvent auditEvent) {
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) userId = "anonymous"; //TODO: throw new InvalidParameterException(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.setUserId(userId);
participant.setName(userName);
ParticipantNetwork network = participant.getNetwork();
network.setType(SecurityEventParticipantNetworkTypeEnum.IP_ADDRESS);
network.setIdentifier(userIp);
}
}
private SecurityEventObjectLifecycleEnum mapResourceTypeToSecurityLifecycle(RestfulOperationTypeEnum resourceOperationType) {
switch (resourceOperationType) {
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
}
}
public void setDataStore(IAuditDataStore theDataStore) {
myDataStore = theDataStore;
}
public Map<ResourceTypeEnum, Class<? extends IResourceAuditor<? extends IResource>>> getAuditableResources() {
return myAuditableResources;
}
public void setAuditableResources(Map<ResourceTypeEnum, Class<? extends IResourceAuditor<? extends IResource>>> theAuditableResources) {
myAuditableResources = theAuditableResources;
}
public void addAuditableResource(ResourceTypeEnum resourceType, Class<? extends IResourceAuditor<? extends IResource>> auditableResource){
if(myAuditableResources == null) myAuditableResources = new HashMap<ResourceTypeEnum, Class<? extends IResourceAuditor<? extends IResource>>>();
myAuditableResources.put(resourceType, auditableResource);
}
}

View File

@ -0,0 +1,17 @@
package ca.uhn.fhir.store;
import ca.uhn.fhir.model.dstu.resource.SecurityEvent;
/**
* This interface provides a way to persist FHIR SecurityEvents to any kind of data store
*/
public interface IAuditDataStore {
/**
* Take in a SecurityEvent object and handle storing it to a persistent data store (database, JMS, file, etc).
* @param auditEvent a FHIR SecurityEvent to be persisted
* @throws Exception if there is an error while persisting the data
*/
public void store(SecurityEvent auditEvent) throws Exception;
}