onc_certification_g10_test_kit 2.3.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/inferno/terminology/expected_manifest.yml +242 -29
- data/lib/inferno/terminology/fhir_package_manager.rb +27 -0
- data/lib/inferno/terminology/loader.rb +22 -1
- data/lib/inferno/terminology/tasks/create_value_set_validators.rb +1 -1
- data/lib/inferno/terminology/tasks/download_fhir_terminology.rb +5 -0
- data/lib/inferno/terminology/value_set.rb +51 -5
- data/lib/onc_certification_g10_test_kit/base_token_refresh_group.rb +5 -4
- data/lib/onc_certification_g10_test_kit/bulk_data_group_export_stu1.rb +5 -0
- data/lib/onc_certification_g10_test_kit/bulk_data_group_export_stu2.rb +2 -0
- data/lib/onc_certification_g10_test_kit/bulk_data_group_export_validation.rb +206 -28
- data/lib/onc_certification_g10_test_kit/bulk_export_validation_tester.rb +25 -40
- data/lib/onc_certification_g10_test_kit/encounter_context_test.rb +30 -0
- data/lib/onc_certification_g10_test_kit/feature.rb +5 -8
- data/lib/onc_certification_g10_test_kit/limited_scope_grant_test.rb +18 -5
- data/lib/onc_certification_g10_test_kit/profile_selector.rb +175 -0
- data/lib/onc_certification_g10_test_kit/restricted_resource_type_access_group.rb +54 -4
- data/lib/onc_certification_g10_test_kit/single_patient_us_core_5_api_group.rb +93 -0
- data/lib/onc_certification_g10_test_kit/smart_app_launch_invalid_aud_group.rb +50 -5
- data/lib/onc_certification_g10_test_kit/smart_ehr_patient_launch_group.rb +94 -0
- data/lib/onc_certification_g10_test_kit/smart_ehr_patient_launch_group_stu2.rb +94 -0
- data/lib/onc_certification_g10_test_kit/smart_ehr_practitioner_app_group.rb +197 -13
- data/lib/onc_certification_g10_test_kit/smart_invalid_pkce_group.rb +310 -0
- data/lib/onc_certification_g10_test_kit/smart_invalid_token_group_stu2.rb +211 -0
- data/lib/onc_certification_g10_test_kit/smart_limited_app_group.rb +135 -9
- data/lib/onc_certification_g10_test_kit/smart_public_standalone_launch_group.rb +16 -4
- data/lib/onc_certification_g10_test_kit/smart_public_standalone_launch_group_stu2.rb +130 -0
- data/lib/onc_certification_g10_test_kit/smart_scopes_test.rb +134 -67
- data/lib/onc_certification_g10_test_kit/smart_standalone_patient_app_group.rb +166 -11
- data/lib/onc_certification_g10_test_kit/unrestricted_resource_type_access_group.rb +119 -135
- data/lib/onc_certification_g10_test_kit/version.rb +1 -1
- data/lib/onc_certification_g10_test_kit/visual_inspection_and_attestations_group.rb +19 -0
- data/lib/onc_certification_g10_test_kit/well_known_capabilities_test.rb +7 -1
- data/lib/onc_certification_g10_test_kit.rb +115 -74
- metadata +19 -11
- data/lib/onc_certification_g10_test_kit/profile_guesser.rb +0 -72
@@ -9,81 +9,165 @@ module ONCCertificationG10TestKit
|
|
9
9
|
input :requested_scopes, :received_scopes
|
10
10
|
uses_request :token
|
11
11
|
|
12
|
+
VALID_RESOURCE_TYPES = [
|
13
|
+
'*',
|
14
|
+
'Patient',
|
15
|
+
'AllergyIntolerance',
|
16
|
+
'Binary',
|
17
|
+
'CarePlan',
|
18
|
+
'CareTeam',
|
19
|
+
'Condition',
|
20
|
+
'Device',
|
21
|
+
'DiagnosticReport',
|
22
|
+
'DocumentReference',
|
23
|
+
'Encounter',
|
24
|
+
'Goal',
|
25
|
+
'Immunization',
|
26
|
+
'Location',
|
27
|
+
'Medication',
|
28
|
+
'MedicationOrder',
|
29
|
+
'MedicationRequest',
|
30
|
+
'MedicationStatement',
|
31
|
+
'Observation',
|
32
|
+
'Organization',
|
33
|
+
'Person',
|
34
|
+
'Practitioner',
|
35
|
+
'PractitionerRole',
|
36
|
+
'Procedure',
|
37
|
+
'Provenance',
|
38
|
+
'RelatedPerson'
|
39
|
+
].freeze
|
40
|
+
|
41
|
+
V5_VALID_RESOURCE_TYPES =
|
42
|
+
(VALID_RESOURCE_TYPES + ['ServiceRequest', 'QuestionnaireResponse']).freeze
|
43
|
+
|
44
|
+
PATIENT_COMPARTMENT_RESOURCE_TYPES = [
|
45
|
+
'*',
|
46
|
+
'Patient',
|
47
|
+
'AllergyIntolerance',
|
48
|
+
'CarePlan',
|
49
|
+
'CareTeam',
|
50
|
+
'Condition',
|
51
|
+
'DiagnosticReport',
|
52
|
+
'DocumentReference',
|
53
|
+
'Goal',
|
54
|
+
'Immunization',
|
55
|
+
'MedicationRequest',
|
56
|
+
'Observation',
|
57
|
+
'Procedure',
|
58
|
+
'Provenance'
|
59
|
+
].freeze
|
60
|
+
|
61
|
+
V5_PATIENT_COMPARTMENT_RESOURCE_TYPES =
|
62
|
+
(PATIENT_COMPARTMENT_RESOURCE_TYPES + ['ServiceRequest']).freeze
|
63
|
+
|
64
|
+
def patient_compartment_resource_types
|
65
|
+
return PATIENT_COMPARTMENT_RESOURCE_TYPES unless suite_options[:us_core_version] == 'us_core_5'
|
66
|
+
|
67
|
+
V5_PATIENT_COMPARTMENT_RESOURCE_TYPES
|
68
|
+
end
|
69
|
+
|
12
70
|
def valid_resource_types
|
13
|
-
[
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
71
|
+
return VALID_RESOURCE_TYPES unless suite_options[:us_core_version] == 'us_core_5'
|
72
|
+
|
73
|
+
V5_VALID_RESOURCE_TYPES
|
74
|
+
end
|
75
|
+
|
76
|
+
def read_format
|
77
|
+
@read_format ||=
|
78
|
+
begin
|
79
|
+
v1_read_format = 'read'
|
80
|
+
v2_read_format = 'c?ru?d?s?'
|
81
|
+
|
82
|
+
case config.options[:scope_version]
|
83
|
+
when :v1
|
84
|
+
"#{v1_read_format} | *"
|
85
|
+
when :v2
|
86
|
+
"#{v2_read_format} | *"
|
87
|
+
else
|
88
|
+
[v1_read_format, v2_read_format, '*'].join(' | ')
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def access_level_regex
|
94
|
+
@access_level_regex ||=
|
95
|
+
case config.options[:scope_version]
|
96
|
+
when :v1
|
97
|
+
/\A(\*|read)/
|
98
|
+
when :v2
|
99
|
+
/\A(\*|c?ru?d?s?\b)/
|
100
|
+
else
|
101
|
+
/\A(\*|read|c?ru?d?s?\b)/
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def bad_format_message(scope)
|
106
|
+
%(
|
107
|
+
Requested scope `#{scope}` does not follow the format:
|
108
|
+
`#{required_scope_type}/[ <ResourceType> | * ].[ #{read_format} ]`
|
109
|
+
)
|
110
|
+
end
|
111
|
+
|
112
|
+
def strip_experimental_scope_syntax(full_scope)
|
113
|
+
if config.options[:scope_version] == :v1
|
114
|
+
full_scope
|
115
|
+
else
|
116
|
+
full_scope.split('?').first
|
117
|
+
end
|
41
118
|
end
|
42
119
|
|
43
120
|
def requested_scope_test(scopes, patient_compartment_resource_types)
|
44
|
-
|
121
|
+
correct_scope_type_found = false
|
45
122
|
|
46
|
-
scopes.each do |
|
47
|
-
|
48
|
-
"Requested scope '#{scope}' does not follow the format: `#{scope_type}" \
|
49
|
-
'/[ resource | * ].[ read | * ]`'
|
123
|
+
scopes.each do |full_scope|
|
124
|
+
scope = strip_experimental_scope_syntax(full_scope)
|
50
125
|
|
51
126
|
scope_pieces = scope.split('/')
|
52
|
-
assert scope_pieces.count == 2, bad_format_message
|
127
|
+
assert scope_pieces.count == 2, bad_format_message(scope)
|
53
128
|
|
54
|
-
|
55
|
-
|
129
|
+
scope_type, resource_scope = scope_pieces
|
130
|
+
resource_scope_parts = resource_scope.split('.')
|
56
131
|
|
57
|
-
|
58
|
-
|
132
|
+
assert resource_scope_parts.length == 2, bad_format_message(scope)
|
133
|
+
|
134
|
+
resource_type, access_level = resource_scope_parts
|
135
|
+
bad_resource_message = "'#{resource_type}' must be either a valid resource type or '*'"
|
136
|
+
|
137
|
+
if required_scope_type == 'patient' && patient_compartment_resource_types.exclude?(resource_type)
|
138
|
+
assert ['user', 'patient'].include?(scope_type),
|
59
139
|
"Requested scope '#{scope}' must begin with either 'user/' or 'patient/'"
|
60
140
|
else
|
61
|
-
assert
|
141
|
+
assert scope_type == required_scope_type, bad_format_message(scope)
|
62
142
|
end
|
63
143
|
|
64
|
-
assert
|
65
|
-
assert
|
66
|
-
assert resource_access[1] =~ /^(\*|read)/, bad_format_message
|
144
|
+
assert valid_resource_types.include?(resource_type), bad_resource_message
|
145
|
+
assert access_level =~ access_level_regex, bad_format_message(scope)
|
67
146
|
|
68
|
-
|
147
|
+
correct_scope_type_found = true
|
69
148
|
end
|
70
149
|
|
71
|
-
assert
|
72
|
-
"#{
|
73
|
-
"`#{
|
150
|
+
assert correct_scope_type_found,
|
151
|
+
"#{required_scope_type.capitalize}-level scope in the format: " \
|
152
|
+
"`#{required_scope_type}/[ <ResourceType> | * ].[ #{read_format} ]` was not requested."
|
74
153
|
end
|
75
154
|
|
76
155
|
def received_scope_test(scopes, patient_compartment_resource_types)
|
77
156
|
granted_resource_types = []
|
78
157
|
|
79
|
-
scopes.each do |
|
158
|
+
scopes.each do |full_scope|
|
159
|
+
scope = strip_experimental_scope_syntax(full_scope)
|
160
|
+
|
80
161
|
scope_pieces = scope.split('/')
|
81
162
|
next unless scope_pieces.count == 2
|
82
163
|
|
83
|
-
|
84
|
-
next unless resource_access.count == 2
|
164
|
+
_scope_type, resource_scope = scope_pieces
|
85
165
|
|
86
|
-
|
166
|
+
resource_scope_parts = resource_scope.split('.')
|
167
|
+
next unless resource_scope_parts.count == 2
|
168
|
+
|
169
|
+
resource_type, access_level = resource_scope_parts
|
170
|
+
granted_resource_types << resource_type if access_level =~ access_level_regex
|
87
171
|
end
|
88
172
|
|
89
173
|
missing_resource_types =
|
@@ -100,23 +184,6 @@ module ONCCertificationG10TestKit
|
|
100
184
|
run do
|
101
185
|
skip_if request.status != 200, 'Token exchange was unsuccessful'
|
102
186
|
|
103
|
-
patient_compartment_resource_types = [
|
104
|
-
'*',
|
105
|
-
'Patient',
|
106
|
-
'AllergyIntolerance',
|
107
|
-
'CarePlan',
|
108
|
-
'CareTeam',
|
109
|
-
'Condition',
|
110
|
-
'DiagnosticReport',
|
111
|
-
'DocumentReference',
|
112
|
-
'Goal',
|
113
|
-
'Immunization',
|
114
|
-
'MedicationRequest',
|
115
|
-
'Observation',
|
116
|
-
'Procedure',
|
117
|
-
'Provenance'
|
118
|
-
].freeze
|
119
|
-
|
120
187
|
[
|
121
188
|
{
|
122
189
|
scopes: requested_scopes,
|
@@ -23,16 +23,22 @@ module ONCCertificationG10TestKit
|
|
23
23
|
|
24
24
|
description %(
|
25
25
|
This scenario demonstrates the ability of a system to perform a Patient
|
26
|
-
Standalone Launch to a
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
26
|
+
Standalone Launch to a SMART on FHIR confidential client with a patient
|
27
|
+
context, refresh token, OpenID Connect (OIDC) identity token, and use
|
28
|
+
the GET HTTP method for code exchange. After launch, a simple Patient
|
29
|
+
resource read is performed on the patient in context. The access token
|
30
|
+
is then refreshed, and the Patient resource is read using the new access
|
31
|
+
token to ensure that the refresh was successful. The authentication
|
32
|
+
information provided by OpenID Connect is decoded and validated, and
|
33
|
+
simple queries are performed to ensure that access is granted to all
|
34
|
+
USCDI data elements.
|
35
|
+
|
36
|
+
* [SMART on FHIR
|
37
|
+
(STU1)](http://www.hl7.org/fhir/smart-app-launch/1.0.0/)
|
38
|
+
* [SMART on FHIR
|
39
|
+
(STU2)](http://hl7.org/fhir/smart-app-launch/STU2)
|
40
|
+
* [OpenID Connect
|
41
|
+
(OIDC)](https://openid.net/specs/openid-connect-core-1_0.html)
|
36
42
|
)
|
37
43
|
id :g10_smart_standalone_patient_app
|
38
44
|
run_as_group
|
@@ -49,6 +55,8 @@ module ONCCertificationG10TestKit
|
|
49
55
|
input_order :url, :standalone_client_id, :standalone_client_secret
|
50
56
|
|
51
57
|
group from: :smart_discovery do
|
58
|
+
required_suite_options(smart_app_launch_version: 'smart_app_launch_1')
|
59
|
+
|
52
60
|
test from: 'g10_smart_well_known_capabilities',
|
53
61
|
config: {
|
54
62
|
options: {
|
@@ -65,7 +73,32 @@ module ONCCertificationG10TestKit
|
|
65
73
|
}
|
66
74
|
end
|
67
75
|
|
76
|
+
group from: :smart_discovery_stu2 do
|
77
|
+
required_suite_options(smart_app_launch_version: 'smart_app_launch_2')
|
78
|
+
|
79
|
+
test from: 'g10_smart_well_known_capabilities',
|
80
|
+
config: {
|
81
|
+
options: {
|
82
|
+
required_capabilities: [
|
83
|
+
'launch-standalone',
|
84
|
+
'client-public',
|
85
|
+
'client-confidential-symmetric',
|
86
|
+
'sso-openid-connect',
|
87
|
+
'context-standalone-patient',
|
88
|
+
'permission-offline',
|
89
|
+
'permission-patient',
|
90
|
+
'authorize-post',
|
91
|
+
'permission-v1',
|
92
|
+
'permission-v2'
|
93
|
+
|
94
|
+
]
|
95
|
+
}
|
96
|
+
}
|
97
|
+
end
|
98
|
+
|
68
99
|
group from: :smart_standalone_launch do
|
100
|
+
required_suite_options(smart_app_launch_version: 'smart_app_launch_1')
|
101
|
+
|
69
102
|
title 'Standalone Launch With Patient Scope'
|
70
103
|
description %(
|
71
104
|
# Background
|
@@ -92,11 +125,133 @@ module ONCCertificationG10TestKit
|
|
92
125
|
Sequence](http://hl7.org/fhir/smart-app-launch/1.0.0/index.html#standalone-launch-sequence)
|
93
126
|
)
|
94
127
|
|
128
|
+
config(
|
129
|
+
inputs: {
|
130
|
+
requested_scopes: {
|
131
|
+
default: %(
|
132
|
+
launch/patient openid fhirUser offline_access
|
133
|
+
patient/Medication.read patient/AllergyIntolerance.read
|
134
|
+
patient/CarePlan.read patient/CareTeam.read patient/Condition.read
|
135
|
+
patient/Device.read patient/DiagnosticReport.read
|
136
|
+
patient/DocumentReference.read patient/Encounter.read
|
137
|
+
patient/Goal.read patient/Immunization.read patient/Location.read
|
138
|
+
patient/MedicationRequest.read patient/Observation.read
|
139
|
+
patient/Organization.read patient/Patient.read
|
140
|
+
patient/Practitioner.read patient/Procedure.read
|
141
|
+
patient/Provenance.read patient/PractitionerRole.read
|
142
|
+
).gsub(/\s{2,}/, ' ').strip
|
143
|
+
}
|
144
|
+
}
|
145
|
+
)
|
146
|
+
|
147
|
+
test from: :g10_smart_scopes do
|
148
|
+
config(
|
149
|
+
inputs: {
|
150
|
+
requested_scopes: { name: :standalone_requested_scopes },
|
151
|
+
received_scopes: { name: :standalone_received_scopes }
|
152
|
+
},
|
153
|
+
options: {
|
154
|
+
scope_version: :v1
|
155
|
+
}
|
156
|
+
)
|
157
|
+
|
158
|
+
def required_scopes
|
159
|
+
['openid', 'fhirUser', 'launch/patient', 'offline_access']
|
160
|
+
end
|
161
|
+
|
162
|
+
def required_scope_type
|
163
|
+
'patient'
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
test from: :g10_unauthorized_access,
|
168
|
+
config: {
|
169
|
+
inputs: {
|
170
|
+
patient_id: { name: :standalone_patient_id }
|
171
|
+
}
|
172
|
+
}
|
173
|
+
|
174
|
+
test from: :g10_patient_context,
|
175
|
+
config: {
|
176
|
+
inputs: {
|
177
|
+
patient_id: { name: :standalone_patient_id },
|
178
|
+
smart_credentials: { name: :standalone_smart_credentials }
|
179
|
+
}
|
180
|
+
}
|
181
|
+
end
|
182
|
+
|
183
|
+
group from: :smart_standalone_launch_stu2,
|
184
|
+
config: {
|
185
|
+
inputs: {
|
186
|
+
use_pkce: {
|
187
|
+
default: 'true',
|
188
|
+
locked: true
|
189
|
+
},
|
190
|
+
pkce_code_challenge_method: {
|
191
|
+
locked: true
|
192
|
+
},
|
193
|
+
authorization_method: {
|
194
|
+
name: :standalone_authorization_method,
|
195
|
+
default: 'get',
|
196
|
+
locked: true
|
197
|
+
}
|
198
|
+
}
|
199
|
+
} do
|
200
|
+
required_suite_options(smart_app_launch_version: 'smart_app_launch_2')
|
201
|
+
|
202
|
+
title 'Standalone Launch With Patient Scope'
|
203
|
+
description %(
|
204
|
+
# Background
|
205
|
+
|
206
|
+
The [Standalone
|
207
|
+
Launch Sequence](http://hl7.org/fhir/smart-app-launch/STU2/app-launch.html#launch-app-standalone-launch)
|
208
|
+
allows an app, like Inferno, to be launched independent of an
|
209
|
+
existing EHR session. It is one of the two launch methods described in
|
210
|
+
the SMART App Launch Framework alongside EHR Launch. The app will
|
211
|
+
request authorization for the provided scope from the authorization
|
212
|
+
endpoint, ultimately receiving an authorization token which can be used
|
213
|
+
to gain access to resources on the FHIR server.
|
214
|
+
|
215
|
+
# Test Methodology
|
216
|
+
|
217
|
+
Inferno will redirect the user to the the authorization endpoint so that
|
218
|
+
they may provide any required credentials and authorize the application.
|
219
|
+
Upon successful authorization, Inferno will exchange the authorization
|
220
|
+
code provided for an access token.
|
221
|
+
|
222
|
+
For more information on the #{title}:
|
223
|
+
|
224
|
+
* [Standalone Launch
|
225
|
+
Sequence](http://hl7.org/fhir/smart-app-launch/STU2/app-launch.html#launch-app-standalone-launch)
|
226
|
+
)
|
227
|
+
|
228
|
+
config(
|
229
|
+
inputs: {
|
230
|
+
requested_scopes: {
|
231
|
+
default: %(
|
232
|
+
launch/patient openid fhirUser offline_access
|
233
|
+
patient/Medication.rs patient/AllergyIntolerance.rs
|
234
|
+
patient/CarePlan.rs patient/CareTeam.rs patient/Condition.rs
|
235
|
+
patient/Device.rs patient/DiagnosticReport.rs
|
236
|
+
patient/DocumentReference.rs patient/Encounter.rs
|
237
|
+
patient/Goal.rs patient/Immunization.rs patient/Location.rs
|
238
|
+
patient/MedicationRequest.rs patient/Observation.rs
|
239
|
+
patient/Organization.rs patient/Patient.rs
|
240
|
+
patient/Practitioner.rs patient/Procedure.rs
|
241
|
+
patient/Provenance.rs patient/PractitionerRole.rs
|
242
|
+
).gsub(/\s{2,}/, ' ').strip
|
243
|
+
}
|
244
|
+
}
|
245
|
+
)
|
246
|
+
|
95
247
|
test from: :g10_smart_scopes do
|
96
248
|
config(
|
97
249
|
inputs: {
|
98
250
|
requested_scopes: { name: :standalone_requested_scopes },
|
99
251
|
received_scopes: { name: :standalone_received_scopes }
|
252
|
+
},
|
253
|
+
options: {
|
254
|
+
scope_version: :v2
|
100
255
|
}
|
101
256
|
)
|
102
257
|
|
@@ -104,7 +259,7 @@ module ONCCertificationG10TestKit
|
|
104
259
|
['openid', 'fhirUser', 'launch/patient', 'offline_access']
|
105
260
|
end
|
106
261
|
|
107
|
-
def
|
262
|
+
def required_scope_type
|
108
263
|
'patient'
|
109
264
|
end
|
110
265
|
end
|