onc_certification_g10_test_kit 2.3.0 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|