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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/lib/inferno/terminology/expected_manifest.yml +242 -29
  3. data/lib/inferno/terminology/fhir_package_manager.rb +27 -0
  4. data/lib/inferno/terminology/loader.rb +22 -1
  5. data/lib/inferno/terminology/tasks/create_value_set_validators.rb +1 -1
  6. data/lib/inferno/terminology/tasks/download_fhir_terminology.rb +5 -0
  7. data/lib/inferno/terminology/value_set.rb +51 -5
  8. data/lib/onc_certification_g10_test_kit/base_token_refresh_group.rb +5 -4
  9. data/lib/onc_certification_g10_test_kit/bulk_data_group_export_stu1.rb +5 -0
  10. data/lib/onc_certification_g10_test_kit/bulk_data_group_export_stu2.rb +2 -0
  11. data/lib/onc_certification_g10_test_kit/bulk_data_group_export_validation.rb +206 -28
  12. data/lib/onc_certification_g10_test_kit/bulk_export_validation_tester.rb +25 -40
  13. data/lib/onc_certification_g10_test_kit/encounter_context_test.rb +30 -0
  14. data/lib/onc_certification_g10_test_kit/feature.rb +5 -8
  15. data/lib/onc_certification_g10_test_kit/limited_scope_grant_test.rb +18 -5
  16. data/lib/onc_certification_g10_test_kit/profile_selector.rb +175 -0
  17. data/lib/onc_certification_g10_test_kit/restricted_resource_type_access_group.rb +54 -4
  18. data/lib/onc_certification_g10_test_kit/single_patient_us_core_5_api_group.rb +93 -0
  19. data/lib/onc_certification_g10_test_kit/smart_app_launch_invalid_aud_group.rb +50 -5
  20. data/lib/onc_certification_g10_test_kit/smart_ehr_patient_launch_group.rb +94 -0
  21. data/lib/onc_certification_g10_test_kit/smart_ehr_patient_launch_group_stu2.rb +94 -0
  22. data/lib/onc_certification_g10_test_kit/smart_ehr_practitioner_app_group.rb +197 -13
  23. data/lib/onc_certification_g10_test_kit/smart_invalid_pkce_group.rb +310 -0
  24. data/lib/onc_certification_g10_test_kit/smart_invalid_token_group_stu2.rb +211 -0
  25. data/lib/onc_certification_g10_test_kit/smart_limited_app_group.rb +135 -9
  26. data/lib/onc_certification_g10_test_kit/smart_public_standalone_launch_group.rb +16 -4
  27. data/lib/onc_certification_g10_test_kit/smart_public_standalone_launch_group_stu2.rb +130 -0
  28. data/lib/onc_certification_g10_test_kit/smart_scopes_test.rb +134 -67
  29. data/lib/onc_certification_g10_test_kit/smart_standalone_patient_app_group.rb +166 -11
  30. data/lib/onc_certification_g10_test_kit/unrestricted_resource_type_access_group.rb +119 -135
  31. data/lib/onc_certification_g10_test_kit/version.rb +1 -1
  32. data/lib/onc_certification_g10_test_kit/visual_inspection_and_attestations_group.rb +19 -0
  33. data/lib/onc_certification_g10_test_kit/well_known_capabilities_test.rb +7 -1
  34. data/lib/onc_certification_g10_test_kit.rb +115 -74
  35. metadata +19 -11
  36. 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
- 'Patient',
16
- 'AllergyIntolerance',
17
- 'Binary',
18
- 'CarePlan',
19
- 'CareTeam',
20
- 'Condition',
21
- 'Device',
22
- 'DiagnosticReport',
23
- 'DocumentReference',
24
- 'Encounter',
25
- 'Goal',
26
- 'Immunization',
27
- 'Location',
28
- 'Medication',
29
- 'MedicationOrder',
30
- 'MedicationRequest',
31
- 'MedicationStatement',
32
- 'Observation',
33
- 'Organization',
34
- 'Person',
35
- 'Practitioner',
36
- 'PractitionerRole',
37
- 'Procedure',
38
- 'Provenance',
39
- 'RelatedPerson'
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
- patient_scope_found = false
121
+ correct_scope_type_found = false
45
122
 
46
- scopes.each do |scope|
47
- bad_format_message =
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
- resource_access = scope_pieces[1].split('.')
55
- bad_resource_message = "'#{resource_access[0]}' must be either a valid resource type or '*'"
129
+ scope_type, resource_scope = scope_pieces
130
+ resource_scope_parts = resource_scope.split('.')
56
131
 
57
- if scope_type == 'patient' && patient_compartment_resource_types.exclude?(resource_access[0])
58
- assert ['user', 'patient'].include?(scope_pieces[0]),
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 scope_pieces[0] == scope_type, bad_format_message
141
+ assert scope_type == required_scope_type, bad_format_message(scope)
62
142
  end
63
143
 
64
- assert resource_access.count == 2, bad_format_message
65
- assert valid_resource_types.include?(resource_access[0]), bad_resource_message
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
- patient_scope_found = true
147
+ correct_scope_type_found = true
69
148
  end
70
149
 
71
- assert patient_scope_found,
72
- "#{scope_type.capitalize}-level scope in the format: " \
73
- "`#{scope_type}/[ resource | * ].[ read | *]` was not requested."
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 |scope|
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
- resource_access = scope_pieces[1].split('.')
84
- next unless resource_access.count == 2
164
+ _scope_type, resource_scope = scope_pieces
85
165
 
86
- granted_resource_types << resource_access[0] if resource_access[1] =~ /^(\*|read)/
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 [SMART on
27
- FHIR](http://www.hl7.org/fhir/smart-app-launch/1.0.0/) confidential client
28
- with a patient context, refresh token, and [OpenID Connect
29
- (OIDC)](https://openid.net/specs/openid-connect-core-1_0.html) identity
30
- token. After launch, a simple Patient resource read is performed on the
31
- patient in context. The access token is then refreshed, and the Patient
32
- resource is read using the new access token to ensure that the refresh
33
- was successful. The authentication information provided by OpenID
34
- Connect is decoded and validated, and simple queries are performed to
35
- ensure that access is granted to all USCDI data elements.
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 scope_type
262
+ def required_scope_type
108
263
  'patient'
109
264
  end
110
265
  end