onc_certification_g10_test_kit 2.0.0.rc1
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.
- 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
|