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.
- checksums.yaml +7 -0
- data/LICENSE +201 -0
- data/lib/inferno/exceptions.rb +31 -0
- data/lib/inferno/ext/bloomer.rb +24 -0
- data/lib/inferno/repositiories/validators.rb +17 -0
- data/lib/inferno/repositiories/value_sets.rb +26 -0
- data/lib/inferno/terminology/bcp47.rb +95 -0
- data/lib/inferno/terminology/bcp_13.rb +26 -0
- data/lib/inferno/terminology/codesystem.rb +49 -0
- data/lib/inferno/terminology/expected_manifest.yml +1123 -0
- data/lib/inferno/terminology/fhir_package_manager.rb +69 -0
- data/lib/inferno/terminology/loader.rb +298 -0
- data/lib/inferno/terminology/tasks/check_built_terminology.rb +77 -0
- data/lib/inferno/terminology/tasks/cleanup.rb +13 -0
- data/lib/inferno/terminology/tasks/cleanup_precursors.rb +23 -0
- data/lib/inferno/terminology/tasks/count_codes_in_value_set.rb +20 -0
- data/lib/inferno/terminology/tasks/create_value_set_validators.rb +34 -0
- data/lib/inferno/terminology/tasks/download_fhir_terminology.rb +27 -0
- data/lib/inferno/terminology/tasks/download_umls.rb +109 -0
- data/lib/inferno/terminology/tasks/download_umls_notice.rb +20 -0
- data/lib/inferno/terminology/tasks/expand_value_set_to_file.rb +36 -0
- data/lib/inferno/terminology/tasks/process_umls.rb +91 -0
- data/lib/inferno/terminology/tasks/process_umls_translations.rb +85 -0
- data/lib/inferno/terminology/tasks/run_umls_jar.rb +75 -0
- data/lib/inferno/terminology/tasks/temp_dir.rb +27 -0
- data/lib/inferno/terminology/tasks/unzip_umls.rb +42 -0
- data/lib/inferno/terminology/tasks/validate_code.rb +36 -0
- data/lib/inferno/terminology/tasks.rb +11 -0
- data/lib/inferno/terminology/terminology_configuration.rb +52 -0
- data/lib/inferno/terminology/terminology_validation.rb +42 -0
- data/lib/inferno/terminology/validator.rb +64 -0
- data/lib/inferno/terminology/value_set.rb +462 -0
- data/lib/inferno/terminology.rb +16 -0
- data/lib/onc_certification_g10_test_kit/authorization_request_builder.rb +87 -0
- data/lib/onc_certification_g10_test_kit/base_token_refresh_group.rb +48 -0
- data/lib/onc_certification_g10_test_kit/bulk_data_authorization.rb +235 -0
- data/lib/onc_certification_g10_test_kit/bulk_data_group_export.rb +255 -0
- data/lib/onc_certification_g10_test_kit/bulk_data_group_export_validation.rb +474 -0
- data/lib/onc_certification_g10_test_kit/bulk_data_jwks.json +58 -0
- data/lib/onc_certification_g10_test_kit/bulk_export_validation_tester.rb +171 -0
- data/lib/onc_certification_g10_test_kit/configuration_checker.rb +104 -0
- data/lib/onc_certification_g10_test_kit/export_kick_off_performer.rb +12 -0
- data/lib/onc_certification_g10_test_kit/igs/StructureDefinition-bodyheight.json +3772 -0
- data/lib/onc_certification_g10_test_kit/igs/StructureDefinition-bodytemp.json +3772 -0
- data/lib/onc_certification_g10_test_kit/igs/StructureDefinition-bodyweight.json +3772 -0
- data/lib/onc_certification_g10_test_kit/igs/StructureDefinition-bp.json +6034 -0
- data/lib/onc_certification_g10_test_kit/igs/StructureDefinition-heartrate.json +3756 -0
- data/lib/onc_certification_g10_test_kit/igs/StructureDefinition-resprate.json +3756 -0
- data/lib/onc_certification_g10_test_kit/limited_scope_grant_test.rb +66 -0
- data/lib/onc_certification_g10_test_kit/multi_patient_api.rb +43 -0
- data/lib/onc_certification_g10_test_kit/patient_context_test.rb +30 -0
- data/lib/onc_certification_g10_test_kit/profile_guesser.rb +69 -0
- data/lib/onc_certification_g10_test_kit/resource_access_test.rb +96 -0
- data/lib/onc_certification_g10_test_kit/restricted_access_test.rb +12 -0
- data/lib/onc_certification_g10_test_kit/restricted_resource_type_access_group.rb +303 -0
- data/lib/onc_certification_g10_test_kit/smart_app_launch_invalid_aud_group.rb +136 -0
- data/lib/onc_certification_g10_test_kit/smart_ehr_practitioner_app_group.rb +209 -0
- data/lib/onc_certification_g10_test_kit/smart_invalid_token_group.rb +197 -0
- data/lib/onc_certification_g10_test_kit/smart_limited_app_group.rb +123 -0
- data/lib/onc_certification_g10_test_kit/smart_public_standalone_launch_group.rb +113 -0
- data/lib/onc_certification_g10_test_kit/smart_scopes_test.rb +153 -0
- data/lib/onc_certification_g10_test_kit/smart_standalone_patient_app_group.rb +177 -0
- data/lib/onc_certification_g10_test_kit/terminology_binding_validator.rb +140 -0
- data/lib/onc_certification_g10_test_kit/token_revocation_group.rb +133 -0
- data/lib/onc_certification_g10_test_kit/unauthorized_access_test.rb +25 -0
- data/lib/onc_certification_g10_test_kit/unrestricted_resource_type_access_group.rb +375 -0
- data/lib/onc_certification_g10_test_kit/version.rb +3 -0
- data/lib/onc_certification_g10_test_kit/visual_inspection_and_attestations_group.rb +470 -0
- data/lib/onc_certification_g10_test_kit/well_known_capabilities_test.rb +37 -0
- data/lib/onc_certification_g10_test_kit.rb +223 -0
- 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
|