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.
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