Skip to content

Commit

Permalink
Issue #1547 - enable validation of profile assertions (#1721)
Browse files Browse the repository at this point in the history
* Issue #1547 - enable validation of profile assertions

Signed-off-by: Mike Schroeder <mschroed@us.ibm.com>

* Issue #1547 - reset config and tenant after test

Signed-off-by: Mike Schroeder <mschroed@us.ibm.com>
  • Loading branch information
michaelwschroeder authored Nov 17, 2020
1 parent f8b2ce5 commit 8f9f41f
Show file tree
Hide file tree
Showing 6 changed files with 1,065 additions and 15 deletions.
23 changes: 18 additions & 5 deletions docs/src/pages/guides/FHIRServerUsersGuide.md
Original file line number Diff line number Diff line change
Expand Up @@ -955,9 +955,16 @@ For example, you can configure a set of FHIRPath Constraints to run for resource
}
```
It is also possible to configure a set of profiles, one or more of which a resource must claim conformance to and be successfully validated against in order to be persisted to the FHIR server. The FHIR server supports this optional behavior via the `fhirServer/resources/<resourceType>/profiles/atLeastOne` configuration parameter. If this configuration parameter is set to a non-empty list of profiles, then the FHIR server will perform the following validation, returning FAILURE if not successful:
* Validate that at least one profile in the list is specified in the resource's `meta.profile` element
* Validate that all profiles specified in the resource's `meta.profile` element are supported by the FHIR server
* Validate that the resource's data conforms to all profiles specified in the resource's `meta.profile` element
If this configuration parameter is not set or is set to an empty list, then the FHIR server will perform its standard validation.
The IBM FHIR Server pre-packages all conformance resources from the core specification.
See [Validation Guide - Optional profile support](https://ibm.github.io/FHIR/guides/FHIRValidationGuide#optional-profile-support) for a list of pre-built Implmentation Guide resources and how to load them into the IBM FHIR server.
See [Validation Guide - Optional profile support](https://ibm.github.io/FHIR/guides/FHIRValidationGuide#optional-profile-support) for a list of pre-built Implementation Guide resources and how to load them into the IBM FHIR server.
See [Validation Guide - Making profiles available to the fhir registry](https://ibm.github.io/FHIR/guides/FHIRValidationGuide#making-profiles-available-to-the-fhir-registry-component-fhirregistry) for information about how to extend the server with additional Implementation Guide artifacts.
Expand Down Expand Up @@ -1794,12 +1801,14 @@ This section contains reference information about each of the configuration prop
|`fhirServer/resources/Resource/searchIncludes`|string list|A comma-separated list of \_include values supported for all resource types. Individual resource types may override this value via `fhirServer/resources/<resourceType>/searchIncludes`. Omitting this property is equivalent to supporting all \_include values for the supported resources. An empty list, `[]`, can be used to indicate that no \_include values are supported.|
|`fhirServer/resources/Resource/searchRevIncludes`|string list|A comma-separated list of \_revinclude values supported for all resource types. Individual resource types may override this value via `fhirServer/resources/<resourceType>/searchRevIncludes`. Omitting this property is equivalent to supporting all \_revinclude values for the supported resources. An empty list, `[]`, can be used to indicate that no \_revinclude values are supported.|
|`fhirServer/resources/Resource/searchParameterCombinations`|string list|A comma-separated list of search parameter combinations supported for all resource types. Each search parameter combination is a string, where a plus sign, `+`, separates the search parameters that can be used in combination. To indicate that searching without any search parameters is allowed, an empty string must be included in the list. Including an asterisk, `*`, in the list indicates support of any search parameter combination. Individual resource types may override this value via `fhirServer/resources/<resourceType>/searchParameterCombinations`. Omitting this property is equivalent to supporting any search parameter combination.|
|`fhirServer/resources/Resource/profiles/atLeastOne`|string list|A comma-separated list of profiles, at least one of which must be specified in a resource's `meta.profile` element and successfully validated against in order for a resource to be persisted to the FHIR server. Individual resource types may override this value via `fhirServer/resources/<resourceType>/profiles/atLeastOne`. Omitting this property or specifying an empty list is equivalent to not requiring any profile assertions for a resource.|
|`fhirServer/resources/<resourceType>/interactions`|string list|A list of strings that represent the RESTful interactions (create, read, vread, update, patch, delete, history, and/or search) to support for this resource type. For resources without the property, the value of `fhirServer/resources/Resource/interactions` is used.|
|`fhirServer/resources/<resourceType>/searchParameters`|object|The set of search parameters to support for this resource type. Global search parameters defined on the `Resource` resource can be overridden on a per-resourceType basis.|
|`fhirServer/resources/<resourceType>/searchParameters/<code>`|string|The URL of the search parameter definition to use for the search parameter `<code>` on resources of type `<resourceType>`.|
|`fhirServer/resources/<resourceType>/searchIncludes`|string list|A comma-separated list of \_include values supported for this resource type. An empty list, `[]`, can be used to indicate that no \_include values are supported. For resources without the property, the value of `fhirServer/resources/Resource/searchIncludes` is used.|
|`fhirServer/resources/<resourceType>/searchRevIncludes`|string list|A comma-separated list of \_revinclude values supported for this resource type. An empty list, `[]`, can be used to indicate that no \_revinclude values are supported. For resources without the property, the value of `fhirServer/resources/Resource/searchRevIncludes` is used.|
|`fhirServer/resources/<resourceType>/searchParameterCombinations`|string list|A comma-separated list of search parameter combinations supported for this resource type. Each search parameter combination is a string, where a plus sign, `+`, separates the search parameters that can be used in combination. To indicate that searching without any search parameters is allowed, an empty string must be included in the list. Including an asterisk, `*`, in the list indicates support of any search parameter combination. For resources without the property, the value of `fhirServer/resources/Resource/searchParameterCombinations` is used.|
|`fhirServer/resources/<resourceType>/profiles/atLeastOne`|string list|A comma-separated list of profiles, at least one of which must be specified in a resource's `meta.profile` element and be successfully validated against in order for a resource of this type to be persisted to the FHIR server. If this property is not specified, or if an empty list is specified, the value of `fhirServer/resources/Resource/profiles/atLeastOne` will be used.|
|`fhirServer/notifications/common/includeResourceTypes`|string list|A comma-separated list of resource types for which notification event messages should be published.|
|`fhirServer/notifications/websocket/enabled`|boolean|A boolean flag which indicates whether or not websocket notifications are enabled.|
|`fhirServer/notifications/kafka/enabled`|boolean|A boolean flag which indicates whether or not kafka notifications are enabled.|
Expand Down Expand Up @@ -1884,12 +1893,14 @@ This section contains reference information about each of the configuration prop
|`fhirServer/resources/Resource/searchIncludes`|null (all \_include values supported)|
|`fhirServer/resources/Resource/searchRevIncludes`|null (all \_revinclude values supported)|
|`fhirServer/resources/Resource/searchParameterCombinations`|null (all search parameter combinations supported)|
|`fhirServer/resources/<resourceType>/interactions`|null (inherets from `fhirServer/resources/Resource/interactions`)|
|`fhirServer/resources/Resource/profiles/atLeastOne`|null (no resource profile assertions required)|
|`fhirServer/resources/<resourceType>/interactions`|null (inherits from `fhirServer/resources/Resource/interactions`)|
|`fhirServer/resources/<resourceType>/searchParameters`|null (all type-specific search parameters supported)|
|`fhirServer/resources/<resourceType>/searchParameters/<code>`|null|
|`fhirServer/resources/<resourceType>/searchIncludes`|null (inherets from `fhirServer/resources/Resource/searchIncludes`)|
|`fhirServer/resources/<resourceType>/searchRevIncludes`|null (inherets from `fhirServer/resources/Resource/searchRevIncludes`)|
|`fhirServer/resources/<resourceType>/searchParameterCombinations`|null (inherets from `fhirServer/resources/Resource/searchParameterCombinations`)|
|`fhirServer/resources/<resourceType>/searchIncludes`|null (inherits from `fhirServer/resources/Resource/searchIncludes`)|
|`fhirServer/resources/<resourceType>/searchRevIncludes`|null (inherits from `fhirServer/resources/Resource/searchRevIncludes`)|
|`fhirServer/resources/<resourceType>/searchParameterCombinations`|null (inherits from `fhirServer/resources/Resource/searchParameterCombinations`)|
|`fhirServer/resources/<resourceType>/profiles/atLeastOne`|null (inherits from `fhirServer/resources/Resource/profiles/atLeastOne`)|
|`fhirServer/notifications/common/includeResourceTypes`|`["*"]`|
|`fhirServer/notifications/websocket/enabled`|false|
|`fhirServer/notifications/kafka/enabled`|false|
Expand Down Expand Up @@ -1966,12 +1977,14 @@ must restart the server for that change to take effect.
|`fhirServer/resources/Resource/searchIncludes`|Y|Y|
|`fhirServer/resources/Resource/searchRevIncludes`|Y|Y|
|`fhirServer/resources/Resource/searchParameterCombinations`|Y|Y|
|`fhirServer/resources/Resource/profiles/atLeastOne`|Y|Y|
|`fhirServer/resources/<resourceType>/interactions`|Y|Y|
|`fhirServer/resources/<resourceType>/searchParameters`|Y|Y|
|`fhirServer/resources/<resourceType>/searchParameters/<code>`|Y|Y|
|`fhirServer/resources/<resourceType>/searchIncludes`|Y|Y|
|`fhirServer/resources/<resourceType>/searchRevIncludes`|Y|Y|
|`fhirServer/resources/<resourceType>/searchParameterCombinations`|Y|Y|
|`fhirServer/resources/<resourceType>/profiles/atLeastOne`|Y|Y|
|`fhirServer/notifications/common/includeResourceTypes`|N|N|
|`fhirServer/notifications/websocket/enabled`|N|N|
|`fhirServer/notifications/kafka/enabled`|N|N|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ public class FHIRConfiguration {
public static final String PROPERTY_FIELD_RESOURCES_SEARCH_INCLUDES = "searchIncludes";
public static final String PROPERTY_FIELD_RESOURCES_SEARCH_REV_INCLUDES = "searchRevIncludes";
public static final String PROPERTY_FIELD_RESOURCES_SEARCH_PARAMETER_COMBINATIONS = "searchParameterCombinations";
public static final String PROPERTY_FIELD_RESOURCES_PROFILES = "profiles";
public static final String PROPERTY_FIELD_RESOURCES_PROFILES_AT_LEAST_ONE = "atLeastOne";

// Auth and security properties
public static final String PROPERTY_SECURITY_CORS = "fhirServer/security/cors";
Expand Down
104 changes: 94 additions & 10 deletions fhir-server/src/main/java/com/ibm/fhir/server/util/FHIRRestHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
import com.ibm.fhir.model.resource.OperationOutcome.Issue;
import com.ibm.fhir.model.resource.Parameters;
import com.ibm.fhir.model.resource.Resource;
import com.ibm.fhir.model.resource.StructureDefinition;
import com.ibm.fhir.model.type.Code;
import com.ibm.fhir.model.type.CodeableConcept;
import com.ibm.fhir.model.type.Extension;
Expand Down Expand Up @@ -79,6 +80,7 @@
import com.ibm.fhir.persistence.interceptor.impl.FHIRPersistenceInterceptorMgr;
import com.ibm.fhir.persistence.jdbc.exception.FHIRPersistenceDataAccessException;
import com.ibm.fhir.persistence.util.FHIRPersistenceUtil;
import com.ibm.fhir.profile.ProfileSupport;
import com.ibm.fhir.provider.util.FHIRUrlParser;
import com.ibm.fhir.search.SearchConstants;
import com.ibm.fhir.search.SummaryValueSet;
Expand Down Expand Up @@ -219,7 +221,6 @@ public FHIRRestOperationResponse doCreate(String type, Resource resource, String

// If there were no validation errors, then create the resource and return the location header.


// First, invoke the 'beforeCreate' interceptor methods.
FHIRPersistenceEvent event =
new FHIRPersistenceEvent(resource, buildPersistenceEventProperties(type, null, null, requestProperties));
Expand Down Expand Up @@ -430,7 +431,6 @@ private FHIRRestOperationResponse doPatchOrUpdate(String type, String id, FHIRPa
}
}


FHIRPersistenceContext persistenceContext =
FHIRPersistenceContextFactory.createPersistenceContext(event);
SingleResourceResult<Resource> result = persistence.update(persistenceContext, id, newResource);
Expand Down Expand Up @@ -709,7 +709,6 @@ public Resource doRead(String type, String id, boolean throwExcOnNull, boolean i
HTTPHandlingPreference.LENIENT.equals(requestContext.getHandlingPreference()));
}


// First, invoke the 'beforeRead' interceptor methods.
FHIRPersistenceEvent event =
new FHIRPersistenceEvent(contextResource, buildPersistenceEventProperties(type, id, null, requestProperties));
Expand Down Expand Up @@ -782,7 +781,6 @@ public Resource doVRead(String type, String id, String versionId,
Class<? extends Resource> resourceType =
getResourceType(resourceTypeName);


// First, invoke the 'beforeVread' interceptor methods.
FHIRPersistenceEvent event =
new FHIRPersistenceEvent(null, buildPersistenceEventProperties(type, id, versionId, requestProperties));
Expand Down Expand Up @@ -859,7 +857,6 @@ public Bundle doHistory(String type, String id, MultivaluedMap<String, String> q
FHIRHistoryContext historyContext =
FHIRPersistenceUtil.parseHistoryParameters(queryParameters, HTTPHandlingPreference.LENIENT.equals(requestContext.getHandlingPreference()));


// First, invoke the 'beforeHistory' interceptor methods.
FHIRPersistenceEvent event =
new FHIRPersistenceEvent(null, buildPersistenceEventProperties(type, id, null, requestProperties));
Expand Down Expand Up @@ -934,7 +931,6 @@ public Bundle doSearch(String type, String compartment, String compartmentId,
Class<? extends Resource> resourceType =
getResourceType(resourceTypeName);


// First, invoke the 'beforeSearch' interceptor methods.
FHIRPersistenceEvent event =
new FHIRPersistenceEvent(contextResource, buildPersistenceEventProperties(type, null, null, requestProperties));
Expand Down Expand Up @@ -1105,7 +1101,7 @@ public FHIRPersistenceTransaction getTransaction() throws Exception {
*/
private List<OperationOutcome.Issue> validateInput(Resource resource)
throws FHIRValidationException, FHIROperationException {
List<OperationOutcome.Issue> issues = FHIRValidator.validator().validate(resource);
List<OperationOutcome.Issue> issues = validateResource(resource);
if (!issues.isEmpty()) {
for (OperationOutcome.Issue issue : issues) {
if (FHIRUtil.isFailure(issue.getSeverity())) {
Expand Down Expand Up @@ -1242,8 +1238,7 @@ private Bundle validateBundle(Bundle bundle) throws Exception {

// If the request entry contains a resource, then validate it now.
if (resource != null) {
List<OperationOutcome.Issue> issues =
FHIRValidator.validator().validate(resource);
List<Issue> issues = validateResource(resource);
if (!issues.isEmpty()) {
if (anyFailureInIssues(issues)) {
if (requestType == BundleType.ValueSet.TRANSACTION) {
Expand Down Expand Up @@ -2695,7 +2690,96 @@ public int doReindex(FHIROperationContext operationContext, OperationOutcome.Bui
txn.end();
}
} while (attempt++ < TX_ATTEMPTS);

return result;
}

/**
* Validate a resource. First validate profile assertions for the resource if configured to do so,
* then validate the resource itself.
*
* @param resource
* the resource to be validated
* @return A list of validation errors and warnings
* @throws FHIRValidationException
*/
private List<Issue> validateResource(Resource resource) throws FHIRValidationException {
List<String> profiles = null;

// Retrieve the profile configuration
try {
StringBuilder defaultProfileConfigPath = new StringBuilder(FHIRConfiguration.PROPERTY_RESOURCES).append("/Resource/")
.append(FHIRConfiguration.PROPERTY_FIELD_RESOURCES_PROFILES).append("/")
.append(FHIRConfiguration.PROPERTY_FIELD_RESOURCES_PROFILES_AT_LEAST_ONE);
StringBuilder resourceSpecificProfileConfigPath = new StringBuilder(FHIRConfiguration.PROPERTY_RESOURCES).append("/")
.append(resource.getClass().getSimpleName()).append("/").append(FHIRConfiguration.PROPERTY_FIELD_RESOURCES_PROFILES)
.append("/").append(FHIRConfiguration.PROPERTY_FIELD_RESOURCES_PROFILES_AT_LEAST_ONE);

// Get the 'atLeastOne' property
List<String> resourceSpecificProfiles = FHIRConfigHelper.getStringListProperty(resourceSpecificProfileConfigPath.toString());
if (resourceSpecificProfiles != null) {
profiles = resourceSpecificProfiles;
} else {
List<String> defaultProfiles = FHIRConfigHelper.getStringListProperty(defaultProfileConfigPath.toString());
if (defaultProfiles != null) {
profiles = defaultProfiles;
}
}

if (log.isLoggable(Level.FINE)) {
log.fine("Required profile list: " + profiles);
}
} catch (Exception e) {
return Collections.singletonList(buildOperationOutcomeIssue(IssueSeverity.ERROR, IssueType.UNKNOWN,
"Error retrieving profile configuration."));
}

// If required profiles were configured, perform validation of asserted profiles against required profiles
if (profiles != null && !profiles.isEmpty()) {

// Get the profiles asserted for this resource
List<String> resourceAssertedProfiles = ProfileSupport.getResourceAssertedProfiles(resource);
if (log.isLoggable(Level.FINE)) {
log.fine("Asserted profiles: " + resourceAssertedProfiles);
}

// Check if a profile is required but none specified
if (resourceAssertedProfiles.isEmpty()) {
return Collections.singletonList(buildOperationOutcomeIssue(IssueSeverity.ERROR, IssueType.REQUIRED,
"A profile is required but no profile was specified."));
}

// Check if at least one asserted profile is in list of required profiles
boolean validProfileFound = false;
for (String resourceAssertedProfile : resourceAssertedProfiles) {
if (profiles.contains(resourceAssertedProfile)) {
if (log.isLoggable(Level.FINE)) {
log.fine("Valid asserted profile found: '" + resourceAssertedProfile + "'");
}
validProfileFound = true;
break;
}
}
if (!validProfileFound) {
return Collections.singletonList(buildOperationOutcomeIssue(IssueSeverity.ERROR, IssueType.INVALID,
"A required profile was not specified."));
}

// Check if asserted profiles are supported
List<Issue> issues = new ArrayList<>();
for (String resourceAssertedProfile : resourceAssertedProfiles) {
StructureDefinition profile = ProfileSupport.getProfile(resourceAssertedProfile);
if (profile == null) {
issues.add(buildOperationOutcomeIssue(IssueSeverity.ERROR, IssueType.NOT_SUPPORTED,
"Profile '" + resourceAssertedProfile + "' is not supported"));
}
}
if (!issues.isEmpty()) {
return issues;
}
}

return FHIRValidator.validator().validate(resource);
}

}
Loading

0 comments on commit 8f9f41f

Please sign in to comment.