onc_certification_g10_test_kit 2.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +201 -0
  3. data/lib/inferno/exceptions.rb +31 -0
  4. data/lib/inferno/ext/bloomer.rb +24 -0
  5. data/lib/inferno/repositiories/validators.rb +17 -0
  6. data/lib/inferno/repositiories/value_sets.rb +26 -0
  7. data/lib/inferno/terminology/bcp47.rb +95 -0
  8. data/lib/inferno/terminology/bcp_13.rb +26 -0
  9. data/lib/inferno/terminology/codesystem.rb +49 -0
  10. data/lib/inferno/terminology/expected_manifest.yml +1123 -0
  11. data/lib/inferno/terminology/fhir_package_manager.rb +69 -0
  12. data/lib/inferno/terminology/loader.rb +298 -0
  13. data/lib/inferno/terminology/tasks/check_built_terminology.rb +77 -0
  14. data/lib/inferno/terminology/tasks/cleanup.rb +13 -0
  15. data/lib/inferno/terminology/tasks/cleanup_precursors.rb +23 -0
  16. data/lib/inferno/terminology/tasks/count_codes_in_value_set.rb +20 -0
  17. data/lib/inferno/terminology/tasks/create_value_set_validators.rb +34 -0
  18. data/lib/inferno/terminology/tasks/download_fhir_terminology.rb +27 -0
  19. data/lib/inferno/terminology/tasks/download_umls.rb +109 -0
  20. data/lib/inferno/terminology/tasks/download_umls_notice.rb +20 -0
  21. data/lib/inferno/terminology/tasks/expand_value_set_to_file.rb +36 -0
  22. data/lib/inferno/terminology/tasks/process_umls.rb +91 -0
  23. data/lib/inferno/terminology/tasks/process_umls_translations.rb +85 -0
  24. data/lib/inferno/terminology/tasks/run_umls_jar.rb +75 -0
  25. data/lib/inferno/terminology/tasks/temp_dir.rb +27 -0
  26. data/lib/inferno/terminology/tasks/unzip_umls.rb +42 -0
  27. data/lib/inferno/terminology/tasks/validate_code.rb +36 -0
  28. data/lib/inferno/terminology/tasks.rb +11 -0
  29. data/lib/inferno/terminology/terminology_configuration.rb +52 -0
  30. data/lib/inferno/terminology/terminology_validation.rb +42 -0
  31. data/lib/inferno/terminology/validator.rb +64 -0
  32. data/lib/inferno/terminology/value_set.rb +462 -0
  33. data/lib/inferno/terminology.rb +16 -0
  34. data/lib/onc_certification_g10_test_kit/authorization_request_builder.rb +87 -0
  35. data/lib/onc_certification_g10_test_kit/base_token_refresh_group.rb +48 -0
  36. data/lib/onc_certification_g10_test_kit/bulk_data_authorization.rb +235 -0
  37. data/lib/onc_certification_g10_test_kit/bulk_data_group_export.rb +255 -0
  38. data/lib/onc_certification_g10_test_kit/bulk_data_group_export_validation.rb +474 -0
  39. data/lib/onc_certification_g10_test_kit/bulk_data_jwks.json +58 -0
  40. data/lib/onc_certification_g10_test_kit/bulk_export_validation_tester.rb +171 -0
  41. data/lib/onc_certification_g10_test_kit/configuration_checker.rb +104 -0
  42. data/lib/onc_certification_g10_test_kit/export_kick_off_performer.rb +12 -0
  43. data/lib/onc_certification_g10_test_kit/igs/StructureDefinition-bodyheight.json +3772 -0
  44. data/lib/onc_certification_g10_test_kit/igs/StructureDefinition-bodytemp.json +3772 -0
  45. data/lib/onc_certification_g10_test_kit/igs/StructureDefinition-bodyweight.json +3772 -0
  46. data/lib/onc_certification_g10_test_kit/igs/StructureDefinition-bp.json +6034 -0
  47. data/lib/onc_certification_g10_test_kit/igs/StructureDefinition-heartrate.json +3756 -0
  48. data/lib/onc_certification_g10_test_kit/igs/StructureDefinition-resprate.json +3756 -0
  49. data/lib/onc_certification_g10_test_kit/limited_scope_grant_test.rb +66 -0
  50. data/lib/onc_certification_g10_test_kit/multi_patient_api.rb +43 -0
  51. data/lib/onc_certification_g10_test_kit/patient_context_test.rb +30 -0
  52. data/lib/onc_certification_g10_test_kit/profile_guesser.rb +69 -0
  53. data/lib/onc_certification_g10_test_kit/resource_access_test.rb +96 -0
  54. data/lib/onc_certification_g10_test_kit/restricted_access_test.rb +12 -0
  55. data/lib/onc_certification_g10_test_kit/restricted_resource_type_access_group.rb +303 -0
  56. data/lib/onc_certification_g10_test_kit/smart_app_launch_invalid_aud_group.rb +136 -0
  57. data/lib/onc_certification_g10_test_kit/smart_ehr_practitioner_app_group.rb +209 -0
  58. data/lib/onc_certification_g10_test_kit/smart_invalid_token_group.rb +197 -0
  59. data/lib/onc_certification_g10_test_kit/smart_limited_app_group.rb +123 -0
  60. data/lib/onc_certification_g10_test_kit/smart_public_standalone_launch_group.rb +113 -0
  61. data/lib/onc_certification_g10_test_kit/smart_scopes_test.rb +153 -0
  62. data/lib/onc_certification_g10_test_kit/smart_standalone_patient_app_group.rb +177 -0
  63. data/lib/onc_certification_g10_test_kit/terminology_binding_validator.rb +140 -0
  64. data/lib/onc_certification_g10_test_kit/token_revocation_group.rb +133 -0
  65. data/lib/onc_certification_g10_test_kit/unauthorized_access_test.rb +25 -0
  66. data/lib/onc_certification_g10_test_kit/unrestricted_resource_type_access_group.rb +375 -0
  67. data/lib/onc_certification_g10_test_kit/version.rb +3 -0
  68. data/lib/onc_certification_g10_test_kit/visual_inspection_and_attestations_group.rb +470 -0
  69. data/lib/onc_certification_g10_test_kit/well_known_capabilities_test.rb +37 -0
  70. data/lib/onc_certification_g10_test_kit.rb +223 -0
  71. metadata +310 -0
@@ -0,0 +1,462 @@
1
+ require 'sqlite3'
2
+ require 'date'
3
+ require 'fhir_models'
4
+
5
+ require_relative '../exceptions'
6
+ require_relative 'bcp_13'
7
+ require_relative 'bcp47'
8
+ require_relative 'codesystem'
9
+ require_relative 'fhir_package_manager'
10
+
11
+ module Inferno
12
+ module Terminology
13
+ class ValueSet
14
+ # STU3 ValueSets located at: http://hl7.org/fhir/stu3/terminologies-valuesets.html
15
+ # STU3 ValueSet Resource: http://hl7.org/fhir/stu3/valueset.html
16
+ #
17
+ # snomed in umls: https://www.nlm.nih.gov/research/umls/Snomed/snomed_represented.html
18
+
19
+ # The UMLS Database
20
+ attr_accessor :db
21
+ # The FHIR::Model Representation of the ValueSet
22
+ attr_accessor :value_set_model
23
+
24
+ # Flag to say "use the provided expansion" when processing the valueset
25
+ attr_accessor :use_expansions
26
+
27
+ @value_sets_repo = Inferno::Repositories::ValueSets.new
28
+
29
+ class << self
30
+ attr_reader :value_sets_repo
31
+ end
32
+
33
+ # UMLS Vocabulary: https://www.nlm.nih.gov/research/umls/sourcereleasedocs/index.html
34
+ SAB = {
35
+ 'http://www.nlm.nih.gov/research/umls/rxnorm' => {
36
+ abbreviation: 'RXNORM',
37
+ name: 'RxNorm Vocabulary'
38
+ }.freeze,
39
+ 'http://loinc.org' => {
40
+ abbreviation: 'LNC',
41
+ name: 'Logical Observation Identifiers Names and Codes terminology (LOINC)'
42
+ }.freeze,
43
+ 'http://snomed.info/sct' => {
44
+ abbreviation: 'SNOMEDCT_US',
45
+ name: 'Systematized Nomenclature of Medicine-Clinical Terms (SNOMED CT), US Edition'
46
+ }.freeze,
47
+ 'http://www.cms.gov/Medicare/Coding/ICD10' => {
48
+ abbreviation: 'ICD10PCS',
49
+ name: 'ICD-10 Procedure Coding System (ICD-10-PCS)'
50
+ }.freeze,
51
+ 'http://hl7.org/fhir/sid/cvx' => {
52
+ abbreviation: 'CVX',
53
+ name: 'Vaccines Administered (CVX)'
54
+ }.freeze,
55
+ 'http://hl7.org/fhir/sid/icd-10-cm' => {
56
+ abbreviation: 'ICD10CM',
57
+ name: 'International Classification of Diseases, Tenth Revision, Clinical Modification (ICD-10-CM)'
58
+ }.freeze,
59
+ 'http://hl7.org/fhir/sid/icd-9-cm' => {
60
+ abbreviation: 'ICD9CM',
61
+ name: 'International Classification of Diseases, Ninth Revision, Clinical Modification (ICD-9-CM)'
62
+ }.freeze,
63
+ 'http://unitsofmeasure.org' => {
64
+ abbreviation: 'NCI_UCUM',
65
+ name: 'Unified Code for Units of Measure (UCUM)'
66
+ }.freeze,
67
+ 'http://nucc.org/provider-taxonomy' => {
68
+ abbreviation: 'NUCCHCPT',
69
+ name: 'National Uniform Claim Committee - Health Care Provider Taxonomy (NUCCHCPT)'
70
+ }.freeze,
71
+ 'http://www.ama-assn.org/go/cpt' => {
72
+ abbreviation: 'CPT',
73
+ name: 'Current Procedural Terminology (CPT)'
74
+ }.freeze,
75
+ 'urn:oid:2.16.840.1.113883.6.285' => {
76
+ abbreviation: 'HCPCS',
77
+ name: 'Healthcare Common Procedure Coding System (HCPCS)'
78
+ }.freeze,
79
+ 'urn:oid:2.16.840.1.113883.6.13' => {
80
+ abbreviation: 'CDT',
81
+ name: 'Code on Dental Procedures and Nomenclature (CDT)'
82
+ }.freeze
83
+ }.freeze
84
+
85
+ CODE_SYS = {
86
+ 'urn:ietf:bcp:13' => -> { BCP13.code_set },
87
+ 'urn:ietf:bcp:47' => ->(filter = nil) { BCP47.code_set(filter) },
88
+ 'http://ihe.net/fhir/ValueSet/IHE.FormatCode.codesystem' =>
89
+ -> { value_sets_repo.find('http://hl7.org/fhir/ValueSet/formatcodes').value_set },
90
+ 'https://www.usps.com/' =>
91
+ -> { value_sets_repo.find('http://hl7.org/fhir/us/core/ValueSet/us-core-usps-state').value_set }
92
+ }.freeze
93
+
94
+ # https://www.nlm.nih.gov/research/umls/knowledge_sources/metathesaurus/release/attribute_names.html
95
+ FILTER_PROP = {
96
+ 'CLASSTYPE' => 'LCN',
97
+ 'DOC' => 'Doc',
98
+ 'SCALE_TYP' => 'LOINC_SCALE_TYP'
99
+ }.freeze
100
+
101
+ def initialize(database, use_expansions = true) # rubocop:disable Style/OptionalBooleanParameter
102
+ @db = database
103
+ @use_expansions = use_expansions
104
+ end
105
+
106
+ def umls_abbreviation(system)
107
+ return SAB.dig(system, :abbreviation) if system != 'http://nucc.org/provider-taxonomy'
108
+
109
+ @nucc_system ||= # rubocop:disable Naming/MemoizedInstanceVariableName
110
+ if @db.execute("SELECT COUNT(*) FROM mrconso WHERE SAB = 'NUCCPT'").flatten.first.positive?
111
+ 'NUCCPT'
112
+ else
113
+ 'NUCCHCPT'
114
+ end
115
+ end
116
+
117
+ def code_system_metadata(system)
118
+ SAB[system]
119
+ end
120
+
121
+ def value_sets_repo
122
+ self.class.value_sets_repo
123
+ end
124
+
125
+ # The ValueSet [Set]
126
+ def value_set
127
+ return @value_set if @value_set
128
+
129
+ if @use_expansions
130
+ process_with_expansions
131
+ else
132
+ process_value_set
133
+ end
134
+ end
135
+
136
+ # Read the desired valueset from a JSON file
137
+ #
138
+ # @param filename [String] the name of the file
139
+ def read_value_set(filename)
140
+ @value_set_model = FHIR::Json.from_json(File.read(filename))
141
+ end
142
+
143
+ def code_system_set(code_system)
144
+ filter_code_set(code_system)
145
+ end
146
+
147
+ def expansion_as_fhir_value_set
148
+ expansion_backbone = FHIR::ValueSet::Expansion.new
149
+ expansion_backbone.timestamp = DateTime.now.strftime('%Y-%m-%dT%H:%M:%S%:z')
150
+ expansion_backbone.contains = value_set.map do |code|
151
+ FHIR::ValueSet::Expansion::Contains.new({ system: code[:system], code: code[:code] })
152
+ end
153
+ expansion_backbone.total = expansion_backbone.contains.length
154
+ expansion_value_set = @value_set_model.deep_dup # Make a copy so that the original definition is left intact
155
+ expansion_value_set.expansion = expansion_backbone
156
+ expansion_value_set
157
+ end
158
+
159
+ # Return the url of the valueset
160
+ def url
161
+ @value_set_model.url
162
+ end
163
+
164
+ # Return the number of codes in the valueset
165
+ def count
166
+ @value_set.length
167
+ end
168
+
169
+ def included_code_systems
170
+ @value_set_model.compose.include.map(&:system).compact.uniq
171
+ end
172
+
173
+ TOO_COSTLY_URL = 'http://hl7.org/fhir/StructureDefinition/valueset-toocostly'.freeze
174
+ def too_costly?
175
+ @value_set_model&.expansion&.extension&.find { |vs| vs.url == TOO_COSTLY_URL }&.value
176
+ end
177
+
178
+ UNCLOSED_URL = 'http://hl7.org/fhir/StructureDefinition/valueset-unclosed'.freeze
179
+ def unclosed?
180
+ @value_set_model&.expansion&.extension&.find { |vs| vs.url == UNCLOSED_URL }&.value
181
+ end
182
+
183
+ def expansion_present?
184
+ !!@value_set_model&.expansion&.contains
185
+ end
186
+
187
+ # Delegates to process_expanded_valueset if there's already an expansion
188
+ # Otherwise it delegates to process_valueset to do the expansion
189
+ def process_with_expansions
190
+ if expansion_present?
191
+ # This is moved into a nested clause so we can tell in the debug statements which path we're taking
192
+ if too_costly? || unclosed?
193
+ Inferno.logger.debug("ValueSet too costly or unclosed: #{url}")
194
+ process_value_set
195
+ else
196
+ Inferno.logger.debug("Processing expanded valueset: #{url}")
197
+ process_expanded_value_set
198
+ end
199
+ else
200
+ Inferno.logger.debug("Processing composed valueset: #{url}")
201
+ process_value_set
202
+ end
203
+ end
204
+
205
+ # Creates the whole valueset
206
+ #
207
+ # Creates a [Set] representing the valueset
208
+ def process_value_set
209
+ Inferno.logger.debug "Processing #{@value_set_model.url}"
210
+ include_set = Set.new
211
+ @value_set_model.compose.include.each do |include|
212
+ # Cumulative of each include
213
+ include_set.merge(get_code_sets(include))
214
+ end
215
+ @value_set_model.compose.exclude.each do |exclude|
216
+ # Remove excluded codes
217
+ include_set.subtract(get_code_sets(exclude))
218
+ end
219
+ @value_set = include_set
220
+ end
221
+
222
+ def process_expanded_value_set
223
+ include_set = Set.new
224
+ @value_set_model.expansion.contains.each do |contain|
225
+ include_set.add(system: contain.system, code: contain.code)
226
+ end
227
+ @value_set = include_set
228
+ end
229
+
230
+ # Checks if the provided code is in the valueset
231
+ #
232
+ # Codes should be provided as a [Hash] type object
233
+ #
234
+ # e.g. {system: 'http://loinc.org', code: '1234'}
235
+ #
236
+ # @param [Hash] code the code to evaluate
237
+ # @return [Boolean]
238
+ def contains_code?(code)
239
+ @value_set.include? code
240
+ end
241
+
242
+ def generate_bloom
243
+ require 'bloomer'
244
+
245
+ @bf = Bloomer::Scalable.create_with_sufficient_size(value_set.length)
246
+ value_set.each do |cc|
247
+ @bf.add_without_duplication("#{cc[:system]}|#{cc[:code]}")
248
+ end
249
+ @bf
250
+ end
251
+
252
+ # Saves the valueset bloomfilter to a msgpack file
253
+ #
254
+ # @param [String] filename the name of the file
255
+ def save_bloom_to_file(
256
+ filename = "resources/validators/bloom/#{(URI(url).host + URI(url).path).gsub(%r{[./]}, '_')}.msgpack"
257
+ )
258
+ generate_bloom unless @bf
259
+ bloom_file = File.new(filename, 'wb')
260
+ bloom_file.write(@bf.to_msgpack) unless @bf.nil?
261
+ filename
262
+ end
263
+
264
+ # Saves the valueset to a csv
265
+ # @param [String] filename the name of the file
266
+ def save_csv_to_file(filename = "resources/validators/csv/#{(URI(url).host + URI(url).path).gsub(%r{[./]},
267
+ '_')}.csv")
268
+ CSV.open(filename, 'wb') do |csv|
269
+ value_set.each do |code|
270
+ csv << [code[:system], code[:code]]
271
+ end
272
+ end
273
+ end
274
+
275
+ # Load a code system from a file
276
+ #
277
+ # @param [String] filename the file containing the code system JSON
278
+ def self.load_system(filename)
279
+ # TODO: Generalize this
280
+ cs = FHIR::Json.from_json(File.read(filename))
281
+ cs_set = Set.new
282
+ load_codes = lambda do |concept|
283
+ concept.each do |concept_code|
284
+ cs_set.add(system: cs.url, code: concept_code.code)
285
+ load_codes.call(concept_code.concept) unless concept_code.concept.empty?
286
+ end
287
+ end
288
+ load_codes.call(cs.concept)
289
+ cs_set
290
+ end
291
+
292
+ private
293
+
294
+ # Get all the code systems from within an include/exclude and return the set representing the intersection
295
+ #
296
+ # See: http://hl7.org/fhir/stu3/valueset.html#compositions
297
+ #
298
+ # @param [ValueSet::Compose::Include] vscs the FHIR ValueSet include or exclude
299
+ def get_code_sets(vscs)
300
+ intersection_set = nil
301
+
302
+ # Get Concepts
303
+ if !vscs.concept.empty?
304
+ intersection_set = Set.new
305
+ vscs.concept.each do |concept|
306
+ intersection_set.add(system: vscs.system, code: concept.code)
307
+ end
308
+ # Filter based on the filters. Note there cannot be both concepts and filters within a single include/exclude
309
+ elsif !vscs.filter.empty?
310
+ intersection_set = filter_code_set(vscs.system, vscs.filter.first)
311
+ vscs.filter.drop(1).each do |filter|
312
+ intersection_set = intersection_set.intersection(filter_code_set(vscs.system, filter))
313
+ end
314
+ # Import whole code systems if given
315
+ elsif vscs.system
316
+ intersection_set = filter_code_set(vscs.system)
317
+ end
318
+
319
+ unless vscs.valueSet.empty?
320
+ # If no concepts or filtered systems were present and already created the intersection_set
321
+ im_val_set = import_value_set(vscs.valueSet.first)
322
+ vscs.valueSet.drop(1).each do |im_val|
323
+ im_val_set = im_val_set.intersection(im_val)
324
+ end
325
+ intersection_set = intersection_set.nil? ? im_val_set : intersection_set.intersection(im_val_set)
326
+ end
327
+ intersection_set
328
+ end
329
+
330
+ # Provides a codeset based on the system and filters provided
331
+ # @param [String] system the code system url
332
+ # @param [FHIR::ValueSet::Compose::Include::Filter] filter the filter object
333
+ # @return [Set] the filtered set of codes
334
+ def filter_code_set(system, filter = nil, _version = nil)
335
+ fhir_codesystem = File.join(PACKAGE_DIR, "#{FHIRPackageManager.encode_name(system)}.json")
336
+ if CODE_SYS.include? system
337
+ Inferno.logger.debug " loading #{system} codes..."
338
+ return filter.nil? ? CODE_SYS[system].call : CODE_SYS[system].call(filter)
339
+ elsif File.exist?(fhir_codesystem)
340
+ if umls_abbreviation(system).nil?
341
+ fhir_cs = Inferno::Terminology::Codesystem
342
+ .new(FHIR::Json.from_json(File.read(fhir_codesystem)))
343
+
344
+ raise UnknownCodeSystemException, system if fhir_cs.codesystem_model.concept.empty?
345
+
346
+ return fhir_cs.filter_codes(filter)
347
+ end
348
+ end
349
+
350
+ filter_clause = lambda do |filter| # rubocop:disable Lint/ShadowingOuterLocalVariable
351
+ where = +''
352
+ case filter.op
353
+ when 'in'
354
+ col = filter.property
355
+ vals = filter.value.split(',')
356
+ where << "( #{col} = '#{vals[0]}'"
357
+ # Remove the first element after we've used it
358
+ vals.shift
359
+ vals.each do |val|
360
+ where << " OR #{col} = '#{val}' "
361
+ end
362
+ where << ')'
363
+ when '='
364
+ col = filter.property
365
+ where << "#{col} = '#{filter.value}'"
366
+ else
367
+ Inferno.logger.debug "Cannot handle filter operation: #{filter.op}"
368
+ end
369
+ where
370
+ end
371
+
372
+ filtered_set = Set.new
373
+ raise FilterOperationException, filter&.op unless ['=', 'in', 'is-a', nil].include? filter&.op
374
+ raise UnknownCodeSystemException, system if umls_abbreviation(system).nil?
375
+
376
+ # Fix for some weirdness around UMLS and provider taxonomy subsetting
377
+ if system == 'http://nucc.org/provider-taxonomy'
378
+ @db.execute(
379
+ "SELECT code FROM mrconso WHERE SAB = '#{umls_abbreviation(system)}' AND TTY IN('PT', 'OP')"
380
+ ) do |row|
381
+ filtered_set.add(system: system, code: row[0])
382
+ end
383
+ elsif filter.nil?
384
+ @db.execute("SELECT code FROM mrconso WHERE SAB = '#{umls_abbreviation(system)}'") do |row|
385
+ filtered_set.add(system: system, code: row[0])
386
+ end
387
+ elsif ['=', 'in', nil].include? filter&.op
388
+ if FILTER_PROP[filter.property]
389
+ @db.execute(
390
+ "SELECT code FROM mrsat WHERE SAB = '#{umls_abbreviation(system)}' " \
391
+ "AND ATN = '#{fp_self(filter.property)}' AND ATV = '#{fp_self(filter.value)}'"
392
+ ) do |row|
393
+ filtered_set.add(system: system, code: row[0])
394
+ end
395
+ else
396
+ @db.execute(
397
+ "SELECT code FROM mrconso WHERE SAB = '#{umls_abbreviation(system)}' AND #{filter_clause.call(filter)}"
398
+ ) do |row|
399
+ filtered_set.add(system: system, code: row[0])
400
+ end
401
+ end
402
+ elsif filter&.op == 'is-a'
403
+ filtered_set = filter_is_a(system, filter)
404
+ else
405
+ raise FilterOperationException, filter&.op
406
+ end
407
+ filtered_set
408
+ end
409
+
410
+ # Imports the ValueSet with the provided URL from the known local ValueSet Authority
411
+ #
412
+ # @param [Object] url the url of the desired valueset
413
+ # @return [Set] the imported valueset
414
+ def import_value_set(desired_url)
415
+ value_sets_repo.find(desired_url)
416
+ end
417
+
418
+ # Filters UMLS codes for "is-a" filters
419
+ #
420
+ # @param [String] system The code system url
421
+ # @param [FHIR::ValueSet::Compose::Include::Filter] filter the filter object
422
+ # @return [Set] the filtered codes
423
+ def filter_is_a(system, filter)
424
+ children = {}
425
+ find_children = lambda do |_parent, system| # rubocop:disable Lint/ShadowingOuterLocalVariable
426
+ @db.execute("SELECT c1.code, c2.code
427
+ FROM mrrel r
428
+ JOIN mrconso c1 ON c1.aui=r.aui1
429
+ JOIN mrconso c2 ON c2.aui=r.aui2
430
+ WHERE r.rel='CHD' AND r.SAB= '#{umls_abbreviation(system)}'") do |row|
431
+ children[row[0]] ||= []
432
+ children[row[0]] << row[1]
433
+ end
434
+ end
435
+ # Get all the children/parent hierarchy
436
+ find_children.call(filter.value, system)
437
+
438
+ desired_children = Set.new
439
+ subsume = lambda do |parent|
440
+ # Only execute if we haven't processed this parent yet
441
+ par = { system: system, code: parent }
442
+ unless desired_children.include? par
443
+ desired_children.add(system: system, code: parent)
444
+ children[parent]&.each do |child|
445
+ subsume.call(child)
446
+ end
447
+ end
448
+ end
449
+ subsume.call(filter.value)
450
+ desired_children
451
+ end
452
+
453
+ # fp_self is short for filter_prop_or_self
454
+ # @param [String] prop The property name
455
+ # @return [String] either the value from FILTER_PROP for that key, or prop
456
+ # if that key isn't in FILTER_PROP
457
+ def fp_self(prop)
458
+ FILTER_PROP[prop] || prop
459
+ end
460
+ end
461
+ end
462
+ end
@@ -0,0 +1,16 @@
1
+ require_relative 'terminology/loader'
2
+
3
+ module Inferno
4
+ module Terminology
5
+ PACKAGE_DIR = File.join('tmp', 'terminology', 'fhir')
6
+
7
+ def self.code_system_metadata
8
+ @code_system_metadata ||=
9
+ if File.file? File.join('resources', 'terminology', 'validators', 'bloom', 'metadata.yml')
10
+ YAML.load_file(File.join('resources', 'terminology', 'validators', 'bloom', 'metadata.yml'))
11
+ else
12
+ {}
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,87 @@
1
+ require 'json/jwt'
2
+
3
+ module ONCCertificationG10TestKit
4
+ class AuthorizationRequestBuilder
5
+ def self.build(...)
6
+ new(...).authorization_request
7
+ end
8
+
9
+ def self.bulk_data_jwks
10
+ @bulk_data_jwks ||= JSON.parse(File.read(File.join(__dir__, 'bulk_data_jwks.json')))
11
+ end
12
+
13
+ attr_reader :encryption_method, :scope, :iss, :sub, :aud, :content_type, :grant_type, :client_assertion_type, :exp,
14
+ :jti
15
+
16
+ def initialize(
17
+ encryption_method:,
18
+ scope:,
19
+ iss:,
20
+ sub:,
21
+ aud:,
22
+ content_type: 'application/x-www-form-urlencoded',
23
+ grant_type: 'client_credentials',
24
+ client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
25
+ exp: 5.minutes.from_now,
26
+ jti: SecureRandom.hex(32)
27
+ )
28
+ @encryption_method = encryption_method
29
+ @scope = scope
30
+ @iss = iss
31
+ @sub = sub
32
+ @aud = aud
33
+ @content_type = content_type
34
+ @grant_type = grant_type
35
+ @client_assertion_type = client_assertion_type
36
+ @exp = exp
37
+ @jti = jti
38
+ end
39
+
40
+ def bulk_private_key
41
+ @bulk_private_key ||=
42
+ self.class.bulk_data_jwks['keys']
43
+ .select { |key| key['key_ops']&.include?('sign') }
44
+ .find { |key| key['alg'] == encryption_method }
45
+ end
46
+
47
+ def jwt_token
48
+ @jwt_token ||= JSON::JWT.new(iss: iss, sub: sub, aud: aud, exp: exp, jti: jti).compact
49
+ end
50
+
51
+ def jwk
52
+ @jwk ||= JSON::JWK.new(bulk_private_key)
53
+ end
54
+
55
+ def authorization_request_headers
56
+ {
57
+ content_type: content_type,
58
+ accept: 'application/json'
59
+ }.compact
60
+ end
61
+
62
+ def authorization_request_query_values
63
+ {
64
+ 'scope' => scope,
65
+ 'grant_type' => grant_type,
66
+ 'client_assertion_type' => client_assertion_type,
67
+ 'client_assertion' => client_assertion.to_s
68
+ }.compact
69
+ end
70
+
71
+ def client_assertion
72
+ @client_assertion ||=
73
+ begin
74
+ jwt_token.kid = jwk['kid']
75
+ jwk_private_key = jwk.to_key
76
+ jwt_token.sign(jwk_private_key, bulk_private_key['alg'])
77
+ end
78
+ end
79
+
80
+ def authorization_request
81
+ uri = Addressable::URI.new
82
+ uri.query_values = authorization_request_query_values
83
+
84
+ { body: uri.query, headers: authorization_request_headers }
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,48 @@
1
+ module ONCCertificationG10TestKit
2
+ class BaseTokenRefreshGroup < Inferno::TestGroup
3
+ title 'Token Refresh'
4
+ description %(
5
+ # Background
6
+
7
+ The #{title} Sequence tests the ability of the system to successfuly
8
+ exchange a refresh token for an access token. Refresh tokens are typically
9
+ longer lived than access tokens and allow client applications to obtain a
10
+ new access token Refresh tokens themselves cannot provide access to
11
+ resources on the server.
12
+
13
+ Token refreshes are accomplished through a `POST` request to the token
14
+ exchange endpoint as described in the [SMART App Launch
15
+ Framework](http://www.hl7.org/fhir/smart-app-launch/#step-5-later-app-uses-a-refresh-token-to-obtain-a-new-access-token).
16
+
17
+ # Test Methodology
18
+
19
+ This test attempts to exchange the refresh token for a new access token
20
+ and verify that the information returned contains the required fields and
21
+ uses the proper headers.
22
+
23
+ For more information see:
24
+
25
+ * [The OAuth 2.0 Authorization
26
+ Framework](https://tools.ietf.org/html/rfc6749)
27
+ * [Using a refresh token to obtain a new access
28
+ token](http://hl7.org/fhir/smart-app-launch/#step-5-later-app-uses-a-refresh-token-to-obtain-a-new-access-token)
29
+ )
30
+ id :g10_token_refresh
31
+
32
+ test from: :smart_token_refresh,
33
+ id: :g10_token_refresh_without_scopes,
34
+ config: {
35
+ options: { include_scopes: false }
36
+ }
37
+ test from: :smart_token_refresh_body,
38
+ id: :g10_token_refresh_body_without_scopes
39
+ test from: :smart_token_refresh,
40
+ title: 'Server successfully refreshes the access token when optional scope parameter provided',
41
+ id: :g10_token_refresh_with_scopes,
42
+ config: {
43
+ options: { include_scopes: true }
44
+ }
45
+ test from: :smart_token_refresh_body,
46
+ id: :g10_token_refresh_body_with_scopes
47
+ end
48
+ end