Skip to content

Commit

Permalink
Issue #2300 - Update beforeHistory
Browse files Browse the repository at this point in the history
Signed-off-by: Troy Biesterfeld <tbieste@us.ibm.com>
  • Loading branch information
tbieste committed Mar 2, 2022
1 parent 1151f4a commit e4bae71
Show file tree
Hide file tree
Showing 8 changed files with 215 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,14 @@ public class FHIRPersistenceEvent {
*/
public static final String PROPNAME_SEARCH_CONTEXT_IMPL = "SEARCH_CONTEXT_IMPL";

/**
* This property is of type FHIRSystemHistoryContext and is the system history context
* associated with a system history request, but it may be null.
* For other operations, this property will be null.
*/
public static final String PROPNAME_SYSTEM_HISTORY_CONTEXT_IMPL = "SYSTEM_HISTORY_CONTEXT_IMPL";


private Resource fhirResource;
private Resource prevFhirResource = null;
private boolean prevFhirResourceSet = false;
Expand Down Expand Up @@ -207,4 +215,11 @@ public FHIRSearchContext getSearchContextImpl() {
return (FHIRSearchContext) getProperty(PROPNAME_SEARCH_CONTEXT_IMPL);
}

/**
* Returns the FHIRSystemHistoryContext instance currently being used by the FHIR REST API layer
* to process the current request.
*/
public FHIRSystemHistoryContext getSystemHistoryContextImpl() {
return (FHIRSystemHistoryContext) getProperty(PROPNAME_SYSTEM_HISTORY_CONTEXT_IMPL);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import com.ibm.fhir.persistence.ResourceEraseRecord;
import com.ibm.fhir.persistence.SingleResourceResult;
import com.ibm.fhir.persistence.context.FHIRPersistenceEvent;
import com.ibm.fhir.persistence.context.FHIRSystemHistoryContext;
import com.ibm.fhir.persistence.erase.EraseDTO;
import com.ibm.fhir.persistence.exception.FHIRPersistenceException;
import com.ibm.fhir.persistence.payload.PayloadPersistenceResponse;
Expand All @@ -36,10 +37,10 @@ public interface FHIRResourceHelpers {
public static final boolean DO_VALIDATION = true;
// Constant for indicating whether an update can be skipped when the requested update resource matches the existing one
public static final boolean SKIPPABLE_UPDATE = true;

// Constant for when we don't use the If-Not-Match header value
public static final Integer IF_NOT_MATCH_NULL = null;

public enum Interaction {
CREATE("create"),
DELETE("delete"),
Expand Down Expand Up @@ -186,11 +187,13 @@ public FHIRRestOperationResponse doPatchOrUpdatePersist(FHIRPersistenceEvent eve
* the resource version
* @param searchContext
* the request search context
* @param systemHistoryContext
* the request system history context
* @return a map of persistence event properties
* @throws FHIRPersistenceException
*/
Map<String, Object> buildPersistenceEventProperties(String type, String id,
String version, FHIRSearchContext searchContext) throws FHIRPersistenceException;
String version, FHIRSearchContext searchContext, FHIRSystemHistoryContext systemHistoryContext) throws FHIRPersistenceException;

/**
* Performs an update operation (a new version of the Resource will be stored). Validates the resource.
Expand All @@ -208,7 +211,7 @@ Map<String, Object> buildPersistenceEventProperties(String type, String id,
* @param skippableUpdate
* if true, and the resource content in the update matches the existing resource on the server, then skip the update;
* if false, then always attempt the update
* @param ifNoneMatch
* @param ifNoneMatch
* conditional create-on-update
* @return a FHIRRestOperationResponse that contains the results of the operation
* @throws Exception
Expand Down Expand Up @@ -236,7 +239,7 @@ default FHIRRestOperationResponse doUpdate(String type, String id, Resource newR
* if false, then always attempt the update
* @param doValidation
* if true, validate the resource; if false, assume the resource has already been validated
* @param ifNoneMatch
* @param ifNoneMatch
* conditional create-on-update
* @return a FHIRRestOperationResponse that contains the results of the operation
* @throws Exception
Expand Down Expand Up @@ -391,7 +394,7 @@ default Bundle doHistory(MultivaluedMap<String, String> queryParameters, String
* Implement the system level history operation to obtain a list of changes to resources
* with an optional resourceType which supports for example [base]/Patient/_history
* requests to return the complete history of changes filtered to a specific resource type.
* Because the resource type is included in the path, this variant allows only a single
* Because the resource type is included in the path, this variant allows only a single
* resource type to be specified. To obtain history for more than one resource type, the
* [base]/_history whole system history endpoint should be used instead with a list of
* resource types specified using the _type query parameter.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ private FHIRRestInteraction processEntryForPatch(Entry requestEntry, FHIRUrlPars

// Build the event we'll use when executing the interaction command
// - the resource gets injected later when we have it
FHIRPersistenceEvent event = new FHIRPersistenceEvent(null, helpers.buildPersistenceEventProperties(resourceType, resourceId, null, null));
FHIRPersistenceEvent event = new FHIRPersistenceEvent(null, helpers.buildPersistenceEventProperties(resourceType, resourceId, null, null, null));

// We don't perform the actual operation here, just generate the command
// we want to execute later
Expand Down Expand Up @@ -575,7 +575,7 @@ private FHIRRestInteraction processEntryForPost(Entry requestEntry, Entry valida

// Create the event
FHIRPersistenceEvent event =
new FHIRPersistenceEvent(resource, helpers.buildPersistenceEventProperties(resource.getClass().getSimpleName(), null, null, null));
new FHIRPersistenceEvent(resource, helpers.buildPersistenceEventProperties(resource.getClass().getSimpleName(), null, null, null, null));

result = new FHIRRestInteractionCreate(entryIndex, event, validationResponseEntry, requestDescription, requestURL, pathTokens[0], resource, ifNoneExist, localIdentifier);
} else {
Expand Down Expand Up @@ -667,7 +667,7 @@ private FHIRRestInteraction processEntryForPut(Entry requestEntry, Entry validat
}

// Create the event we'll use for this resource interaction
FHIRPersistenceEvent event = new FHIRPersistenceEvent(resource, helpers.buildPersistenceEventProperties(type, id, null, null));
FHIRPersistenceEvent event = new FHIRPersistenceEvent(resource, helpers.buildPersistenceEventProperties(type, id, null, null, null));
result = new FHIRRestInteractionUpdate(entryIndex, event, validationResponseEntry, requestDescription, requestURL,
type, id, resource, ifMatchBundleValue, requestURL.getQuery(), skippableUpdate, localIdentifier, ifNoneMatch);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ public FHIRRestOperationResponse doCreate(String type, Resource resource, String
try {
// Prepare the persistence event
FHIRPersistenceEvent event =
new FHIRPersistenceEvent(resource, buildPersistenceEventProperties(type, null, null, null));
new FHIRPersistenceEvent(resource, buildPersistenceEventProperties(type, null, null, null, null));

// Run the meta phase to handle ifNoneExist and update the resource meta-data
response = doCreateMeta(event, warnings, type, resource, ifNoneExist);
Expand Down Expand Up @@ -444,7 +444,7 @@ private FHIRRestOperationResponse doPatchOrUpdate(String type, String id, FHIRPa
FHIRRequestContext requestContext = FHIRRequestContext.get();
try {
// Do the first phase, which includes updating the meta in the resource
FHIRPersistenceEvent event = new FHIRPersistenceEvent(newResource, buildPersistenceEventProperties(type, id, null, null));
FHIRPersistenceEvent event = new FHIRPersistenceEvent(newResource, buildPersistenceEventProperties(type, id, null, null, null));
List<Issue> warnings = new ArrayList<>();
FHIRRestOperationResponse metaResponse = doUpdateMeta(event, type, id, patch, newResource, ifMatchValue, searchQueryString, skippableUpdate, doValidation, warnings);
if (metaResponse.isCompleted()) {
Expand Down Expand Up @@ -998,7 +998,7 @@ public FHIRRestOperationResponse doDelete(String type, String id, String searchQ
// Because we no longer store a resource payload along with the deletion marker, there's
// no fhirResource value set in the event.
FHIRPersistenceEvent event =
new FHIRPersistenceEvent(null, buildPersistenceEventProperties(type, id, null, null));
new FHIRPersistenceEvent(null, buildPersistenceEventProperties(type, id, null, null, null));
event.setPrevFhirResource(resourceToDelete);

// First, invoke the 'beforeDelete' interceptor methods.
Expand Down Expand Up @@ -1134,7 +1134,7 @@ private SingleResourceResult<? extends Resource> doRead(String type, String id,

// First, invoke the 'beforeRead' interceptor methods.
FHIRPersistenceEvent event =
new FHIRPersistenceEvent(contextResource, buildPersistenceEventProperties(type, id, null, searchContext));
new FHIRPersistenceEvent(contextResource, buildPersistenceEventProperties(type, id, null, searchContext, null));
getInterceptorMgr().fireBeforeReadEvent(event);

FHIRPersistenceContext persistenceContext =
Expand Down Expand Up @@ -1199,7 +1199,7 @@ public Resource doVRead(String type, String id, String versionId, MultivaluedMap

// First, invoke the 'beforeVread' interceptor methods.
FHIRPersistenceEvent event =
new FHIRPersistenceEvent(null, buildPersistenceEventProperties(type, id, versionId, searchContext));
new FHIRPersistenceEvent(null, buildPersistenceEventProperties(type, id, versionId, searchContext, null));
getInterceptorMgr().fireBeforeVreadEvent(event);

FHIRPersistenceContext persistenceContext =
Expand Down Expand Up @@ -1279,7 +1279,7 @@ public Bundle doHistory(String type, String id, MultivaluedMap<String, String> q

// First, invoke the 'beforeHistory' interceptor methods.
FHIRPersistenceEvent event =
new FHIRPersistenceEvent(null, buildPersistenceEventProperties(type, id, null, null));
new FHIRPersistenceEvent(null, buildPersistenceEventProperties(type, id, null, null, null));
getInterceptorMgr().fireBeforeHistoryEvent(event);

FHIRPersistenceContext persistenceContext =
Expand Down Expand Up @@ -1359,7 +1359,7 @@ public Bundle doSearch(String type, String compartment, String compartmentId,

// First, invoke the 'beforeSearch' interceptor methods.
FHIRPersistenceEvent event =
new FHIRPersistenceEvent(contextResource, buildPersistenceEventProperties(type, null, null, searchContext));
new FHIRPersistenceEvent(contextResource, buildPersistenceEventProperties(type, null, null, searchContext, null));
getInterceptorMgr().fireBeforeSearchEvent(event);

FHIRPersistenceContext persistenceContext =
Expand Down Expand Up @@ -2565,7 +2565,7 @@ private String getRequestBaseUri(String type) throws Exception {

@Override
public Map<String, Object> buildPersistenceEventProperties(String type, String id,
String version, FHIRSearchContext searchContext) throws FHIRPersistenceException {
String version, FHIRSearchContext searchContext, FHIRSystemHistoryContext systemHistoryContext) throws FHIRPersistenceException {
Map<String, Object> props = new HashMap<>();
props.put(FHIRPersistenceEvent.PROPNAME_PERSISTENCE_IMPL, persistence);
if (type != null) {
Expand All @@ -2580,6 +2580,9 @@ public Map<String, Object> buildPersistenceEventProperties(String type, String i
if (searchContext != null) {
props.put(FHIRPersistenceEvent.PROPNAME_SEARCH_CONTEXT_IMPL, searchContext);
}
if (searchContext != null) {
props.put(FHIRPersistenceEvent.PROPNAME_SYSTEM_HISTORY_CONTEXT_IMPL, systemHistoryContext);
}
return props;
}

Expand Down Expand Up @@ -3040,7 +3043,7 @@ public Bundle doHistory(MultivaluedMap<String, String> queryParameters, String r

// First, invoke the 'beforeHistory' interceptor methods.
FHIRPersistenceEvent event =
new FHIRPersistenceEvent(null, buildPersistenceEventProperties(resourceType == null ? "Resource" : resourceType, null, null, null));
new FHIRPersistenceEvent(null, buildPersistenceEventProperties(resourceType == null ? "Resource" : resourceType, null, null, null, historyContext));
getInterceptorMgr().fireBeforeHistoryEvent(event);

// Start a new txn in the persistence layer if one is not already active.
Expand Down Expand Up @@ -3291,9 +3294,8 @@ public Bundle doHistory(MultivaluedMap<String, String> queryParameters, String r
bundleBuilder.type(BundleType.HISTORY);
Bundle bundle = bundleBuilder.build();

event.setFhirResource(bundle);

// Invoke the 'afterHistory' interceptor methods.
event.setFhirResource(bundle);
getInterceptorMgr().fireAfterHistoryEvent(event);

return bundle;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,11 @@ public void beforeHistory(FHIRPersistenceEvent event) throws FHIRPersistenceInte
String resourceType = event.getFhirResourceType();
DecodedJWT jwt = JWT.decode(getAccessToken());

if (PATIENT.equals(resourceType)) {
if (event.getFhirResourceId() == null) {
// System/type-level history
enforceSystemHistoryAccess(resourceType, event.getSystemHistoryContextImpl().getResourceTypes(), jwt);
} else if (PATIENT.equals(resourceType)) {
// For a Patient resource instance, check scopes for direct patient access
enforceDirectPatientAccess(resourceType, event.getFhirResourceId(), jwt);
} else {
checkScopes(resourceType, Permission.READ, getScopesFromToken(jwt));
Expand Down Expand Up @@ -311,6 +315,33 @@ private void enforceDirectPatientAccess(String resourceType, String resourceId,
}
}

private void enforceSystemHistoryAccess(String resourceType, List<String> types, DecodedJWT jwt) throws FHIRPersistenceInterceptorException {
List<Scope> scopesFromToken = getScopesFromToken(jwt);
Map<ContextType, List<Scope>> groupedScopes = getScopesFromToken(jwt).stream()
.collect(Collectors.groupingBy(s -> s.getContextType()));

// Check for user or system access to the resource types
// If types specified, then check each of those, otherwise check the single type (which may be 'Resource')
boolean deny = false;
List<String> typesToCheck = types.isEmpty() ? Arrays.asList(resourceType) : types;
for (String type : typesToCheck) {
if (!isApprovedByScopes(type, Permission.READ, groupedScopes.get(ContextType.USER)) &&
!isApprovedByScopes(type, Permission.READ, groupedScopes.get(ContextType.SYSTEM))) {
deny = true;
}
}

if (deny) {
String msg = "Read permission for system history of '" + typesToCheck +
"' is not granted by any of the provided scopes: " + scopesFromToken;
if (log.isLoggable(Level.FINE)) {
log.fine(msg);
}
throw new FHIRPersistenceInterceptorException(msg)
.withIssue(FHIRUtil.buildOperationOutcomeIssue(msg, IssueType.FORBIDDEN));
}
}

/**
* This method ensures the search is either for a resource type that is not a member of the
* patient compartment, or is a valid patient-compartment resource search that is scoped
Expand Down
Loading

0 comments on commit e4bae71

Please sign in to comment.