inferno_core 0.4.39 → 0.4.40
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 +4 -4
- data/lib/inferno/apps/web/router.rb +3 -0
- data/lib/inferno/config/application.rb +2 -0
- data/lib/inferno/dsl/auth_info.rb +142 -6
- data/lib/inferno/dsl/fhir_client.rb +2 -2
- data/lib/inferno/dsl/fhir_client_builder.rb +29 -0
- data/lib/inferno/dsl/fhir_resource_validation.rb +18 -14
- data/lib/inferno/dsl/fhir_validation.rb +14 -9
- data/lib/inferno/dsl/fhirpath_evaluation.rb +121 -0
- data/lib/inferno/dsl/jwks.rb +79 -0
- data/lib/inferno/dsl/messages.rb +71 -0
- data/lib/inferno/dsl/runnable.rb +12 -0
- data/lib/inferno/dsl.rb +5 -1
- data/lib/inferno/entities/test.rb +0 -76
- data/lib/inferno/entities/test_group.rb +13 -1
- data/lib/inferno/entities/test_suite.rb +15 -0
- data/lib/inferno/exceptions.rb +19 -0
- data/lib/inferno/ext/fhir_client.rb +3 -3
- data/lib/inferno/jobs/invoke_validator_session.rb +9 -10
- data/lib/inferno/public/bundle.js +18 -18
- data/lib/inferno/result_collection.rb +72 -0
- data/lib/inferno/result_summarizer.rb +9 -10
- data/lib/inferno/test_runner.rb +55 -27
- data/lib/inferno/version.rb +1 -1
- data/spec/fixtures/auth_info_constants.rb +71 -0
- metadata +7 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 62cf688066c4ae8b8855f9ba04cd376c78ec6ae3238d74c4e34f4fdf3a926aef
|
4
|
+
data.tar.gz: 301885b06d03b494d74f7b2f053b741d0fc5d546371fc2eaf602f5b73e00462c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6081f531d0f06172ed695265ac0c772182fc1481a97290441758d40e1013b08f1d684683e38e496a4d8fae4ac05210ec11c07ee7d2ca5e4513eda0cc5eae3f43
|
7
|
+
data.tar.gz: 5fe4104c0e129a061f81ebaffe2bb6f993a8424aabe1b0118aeae49f1eea29e72f1c1ac1120c8d480e54514547386e47faa04b750988af1aaa7b8499965ef275
|
@@ -53,6 +53,9 @@ module Inferno
|
|
53
53
|
# Should not need Content-Type header but GitHub Codespaces will not work without them.
|
54
54
|
# This could be investigated and likely removed if addressed properly elsewhere.
|
55
55
|
get '/', to: ->(_env) { [200, { 'Content-Type' => 'text/html' }, [client_page]] }
|
56
|
+
get '/jwks.json', to: lambda { |_env|
|
57
|
+
[200, { 'Content-Type' => 'application/json' }, [Inferno::JWKS.jwks_json]]
|
58
|
+
}, as: :jwks
|
56
59
|
|
57
60
|
Inferno.routes.each do |route|
|
58
61
|
cleaned_id = route[:suite].id.gsub(/[^a-zA-Z\d\-._~]/, '_')
|
@@ -13,6 +13,7 @@ module Inferno
|
|
13
13
|
raw_js_host = ENV.fetch('JS_HOST', '')
|
14
14
|
base_path = ENV.fetch('BASE_PATH', '')
|
15
15
|
public_path = base_path.blank? ? '/public' : "/#{base_path}/public"
|
16
|
+
jwks_path = base_path.blank? ? '/jwks.json' : "/#{base_path}/jwks.json"
|
16
17
|
js_host = raw_js_host.present? ? "#{raw_js_host}/public" : public_path
|
17
18
|
|
18
19
|
Application.register('js_host', js_host)
|
@@ -21,6 +22,7 @@ module Inferno
|
|
21
22
|
Application.register('async_jobs', ENV['ASYNC_JOBS'] != 'false')
|
22
23
|
Application.register('inferno_host', ENV.fetch('INFERNO_HOST', 'http://localhost:4567'))
|
23
24
|
Application.register('base_url', URI.join(Application['inferno_host'], base_path).to_s)
|
25
|
+
Application.register('jwks_url', URI.join(Application['inferno_host'], jwks_path).to_s)
|
24
26
|
Application.register('cache_bust_token', SecureRandom.uuid)
|
25
27
|
|
26
28
|
configure do |config|
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require_relative '../entities/attributes'
|
2
|
+
require_relative 'jwks'
|
2
3
|
|
3
4
|
module Inferno
|
4
5
|
module DSL
|
@@ -19,7 +20,7 @@ module Inferno
|
|
19
20
|
# to normal inputs.
|
20
21
|
#
|
21
22
|
# The AuthInfo input type supports two different modes in the UI. Different
|
22
|
-
# fields will be presented to the user
|
23
|
+
# fields will be presented to the user depending on which mode is selected.
|
23
24
|
# - `auth` - This presents the inputs needed to perform authorization, and
|
24
25
|
# is appropriate to use as an input to test groups which perform
|
25
26
|
# authorization
|
@@ -161,13 +162,148 @@ module Inferno
|
|
161
162
|
|
162
163
|
# @private
|
163
164
|
def add_to_client(client)
|
164
|
-
|
165
|
-
|
166
|
-
#
|
165
|
+
client.auth_info = self
|
166
|
+
self.client = client
|
167
|
+
# TODO: do we want to perform authorization if no access_token or rely on SMART/ other auth tests?
|
168
|
+
return unless access_token.present?
|
167
169
|
|
168
|
-
|
170
|
+
client.set_bearer_token(access_token)
|
171
|
+
end
|
172
|
+
|
173
|
+
# @private
|
174
|
+
def need_to_refresh?
|
175
|
+
return false if access_token.blank? || (!backend_services? && refresh_token.blank?)
|
176
|
+
|
177
|
+
return true if expires_in.blank?
|
178
|
+
|
179
|
+
issue_time.to_i + expires_in.to_i - DateTime.now.to_i < 60
|
180
|
+
end
|
181
|
+
|
182
|
+
# @private
|
183
|
+
def able_to_refresh?
|
184
|
+
token_url.present? && (backend_services? || refresh_token.present?)
|
185
|
+
end
|
186
|
+
|
187
|
+
# @private
|
188
|
+
def backend_services?
|
189
|
+
auth_type == 'backend_services'
|
190
|
+
end
|
191
|
+
|
192
|
+
# @private
|
193
|
+
def oauth2_refresh_params
|
194
|
+
case auth_type
|
195
|
+
when 'public'
|
196
|
+
public_auth_refresh_params
|
197
|
+
when 'symmetric'
|
198
|
+
symmetric_auth_refresh_params
|
199
|
+
when 'asymmetric'
|
200
|
+
asymmetric_auth_refresh_params
|
201
|
+
when 'backend_services'
|
202
|
+
backend_services_auth_refresh_params
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
# @private
|
207
|
+
def symmetric_auth_refresh_params
|
208
|
+
{
|
209
|
+
'grant_type' => 'refresh_token',
|
210
|
+
'refresh_token' => refresh_token
|
211
|
+
}
|
212
|
+
end
|
213
|
+
|
214
|
+
# @private
|
215
|
+
def public_auth_refresh_params
|
216
|
+
symmetric_auth_refresh_params.merge('client_id' => client_id)
|
217
|
+
end
|
218
|
+
|
219
|
+
# @private
|
220
|
+
def asymmetric_auth_refresh_params
|
221
|
+
symmetric_auth_refresh_params.merge(
|
222
|
+
'client_assertion_type' => 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
|
223
|
+
'client_assertion' => client_assertion
|
224
|
+
)
|
225
|
+
end
|
226
|
+
|
227
|
+
# @private
|
228
|
+
def backend_services_auth_refresh_params
|
229
|
+
{
|
230
|
+
'grant_type' => 'client_credentials',
|
231
|
+
'scope' => requested_scopes,
|
232
|
+
'client_assertion_type' => 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
|
233
|
+
'client_assertion' => client_assertion
|
234
|
+
}
|
235
|
+
end
|
236
|
+
|
237
|
+
# @private
|
238
|
+
def oauth2_refresh_headers
|
239
|
+
base_headers = { 'Content-Type' => 'application/x-www-form-urlencoded' }
|
240
|
+
|
241
|
+
return base_headers unless auth_type == 'symmetric'
|
242
|
+
|
243
|
+
credentials = "#{client_id}:#{client_secret}"
|
244
|
+
|
245
|
+
base_headers.merge(
|
246
|
+
'Authorization' => "Basic #{Base64.strict_encode64(credentials)}"
|
247
|
+
)
|
248
|
+
end
|
249
|
+
|
250
|
+
# @private
|
251
|
+
def private_key
|
252
|
+
@private_key ||= JWKS.jwks(user_jwks: jwks)
|
253
|
+
.select { |key| key[:key_ops]&.include?('sign') }
|
254
|
+
.select { |key| key[:alg] == encryption_algorithm }
|
255
|
+
.find { |key| !kid || key[:kid] == kid }
|
256
|
+
end
|
257
|
+
|
258
|
+
# @private
|
259
|
+
def signing_key
|
260
|
+
if private_key.nil?
|
261
|
+
raise Inferno::Exceptions::AssertionException,
|
262
|
+
"No signing key found for inputs: encryption method = '#{encryption_algorithm}' and kid = '#{kid}'"
|
263
|
+
end
|
264
|
+
|
265
|
+
@private_key.signing_key
|
266
|
+
end
|
267
|
+
|
268
|
+
# @private
|
269
|
+
def auth_jwt_header
|
270
|
+
{
|
271
|
+
'alg' => encryption_algorithm,
|
272
|
+
'kid' => private_key['kid'],
|
273
|
+
'typ' => 'JWT',
|
274
|
+
'jku' => Inferno::Application['jwks_url']
|
275
|
+
}
|
276
|
+
end
|
277
|
+
|
278
|
+
# @private
|
279
|
+
def auth_jwt_claims
|
280
|
+
{
|
281
|
+
'iss' => client_id,
|
282
|
+
'sub' => client_id,
|
283
|
+
'aud' => token_url,
|
284
|
+
'exp' => 5.minutes.from_now.to_i,
|
285
|
+
'jti' => SecureRandom.hex(32)
|
286
|
+
}
|
287
|
+
end
|
288
|
+
|
289
|
+
# @private
|
290
|
+
def client_assertion
|
291
|
+
JWT.encode auth_jwt_claims, signing_key, encryption_algorithm, auth_jwt_header
|
292
|
+
end
|
293
|
+
|
294
|
+
# @private
|
295
|
+
def update_from_response_body(request)
|
296
|
+
token_response_body = JSON.parse(request.response_body)
|
297
|
+
|
298
|
+
expires_in = token_response_body['expires_in'].is_a?(Numeric) ? token_response_body['expires_in'] : nil
|
299
|
+
|
300
|
+
self.access_token = token_response_body['access_token']
|
301
|
+
self.refresh_token = token_response_body['refresh_token'] if token_response_body['refresh_token'].present?
|
302
|
+
self.expires_in = expires_in
|
303
|
+
self.issue_time = DateTime.now
|
169
304
|
|
170
|
-
|
305
|
+
add_to_client(client)
|
306
|
+
self
|
171
307
|
end
|
172
308
|
end
|
173
309
|
end
|
@@ -347,7 +347,7 @@ module Inferno
|
|
347
347
|
|
348
348
|
# @private
|
349
349
|
def perform_refresh(client)
|
350
|
-
credentials = client.oauth_credentials
|
350
|
+
credentials = client.auth_info || client.oauth_credentials
|
351
351
|
|
352
352
|
post(
|
353
353
|
credentials.token_url,
|
@@ -363,7 +363,7 @@ module Inferno
|
|
363
363
|
Inferno::Repositories::SessionData.new.save(
|
364
364
|
name: credentials.name,
|
365
365
|
value: credentials,
|
366
|
-
type: 'oauth_credentials',
|
366
|
+
type: credentials.is_a?(Inferno::DSL::AuthInfo) ? 'auth_info' : 'oauth_credentials',
|
367
367
|
test_session_id:
|
368
368
|
)
|
369
369
|
end
|
@@ -20,6 +20,20 @@ module Inferno
|
|
20
20
|
# url :url
|
21
21
|
# bearer_token :access_token
|
22
22
|
# end
|
23
|
+
#
|
24
|
+
# @example
|
25
|
+
# input :url
|
26
|
+
# input :fhir_auth,
|
27
|
+
# type: :auth_info,
|
28
|
+
# options: {
|
29
|
+
# mode: 'access'
|
30
|
+
# }
|
31
|
+
#
|
32
|
+
# fhir_client do
|
33
|
+
# url :url
|
34
|
+
# headers 'My-Custom_header' => 'CUSTOM_HEADER_VALUE'
|
35
|
+
# auth_info :fhir_auth
|
36
|
+
# end
|
23
37
|
class FHIRClientBuilder
|
24
38
|
attr_accessor :runnable
|
25
39
|
|
@@ -33,6 +47,7 @@ module Inferno
|
|
33
47
|
client.default_json
|
34
48
|
client.set_bearer_token bearer_token if bearer_token
|
35
49
|
oauth_credentials&.add_to_client(client)
|
50
|
+
auth_info&.add_to_client(client)
|
36
51
|
end
|
37
52
|
end
|
38
53
|
|
@@ -80,6 +95,20 @@ module Inferno
|
|
80
95
|
end
|
81
96
|
end
|
82
97
|
|
98
|
+
# Define auth info for a client. Auth info contains info needed for client
|
99
|
+
# to perform authorization and refresh access token when necessary
|
100
|
+
#
|
101
|
+
# @param auth_info [Inferno::DSL::AuthInfo, Symbol]
|
102
|
+
# @return [void]
|
103
|
+
def auth_info(auth_info = nil)
|
104
|
+
@auth_info ||=
|
105
|
+
if auth_info.is_a? Symbol
|
106
|
+
runnable.send(auth_info)
|
107
|
+
else
|
108
|
+
auth_info
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
83
112
|
# Define custom headers for a client
|
84
113
|
#
|
85
114
|
# @param headers [Hash]
|
@@ -277,32 +277,36 @@ module Inferno
|
|
277
277
|
end
|
278
278
|
|
279
279
|
# @private
|
280
|
-
def operation_outcome_from_hl7_wrapped_response(
|
281
|
-
|
282
|
-
|
283
|
-
validator_session_repo.save(test_suite_id:, validator_session_id: res['sessionId'],
|
280
|
+
def operation_outcome_from_hl7_wrapped_response(response_hash)
|
281
|
+
if response_hash['sessionId'] && response_hash['sessionId'] != @session_id
|
282
|
+
validator_session_repo.save(test_suite_id:, validator_session_id: response_hash['sessionId'],
|
284
283
|
validator_name: name.to_s, suite_options: requirements)
|
285
|
-
@session_id =
|
284
|
+
@session_id = response_hash['sessionId']
|
286
285
|
end
|
287
286
|
|
288
287
|
# assume for now that one resource -> one request
|
289
|
-
issues =
|
288
|
+
issues = response_hash['outcomes'][0]['issues']&.map do |i|
|
290
289
|
{ severity: i['level'].downcase, expression: i['location'], details: { text: i['message'] } }
|
291
290
|
end
|
292
291
|
# this is circuitous, ideally we would map this response directly to message_hashes
|
293
292
|
FHIR::OperationOutcome.new(issue: issues)
|
294
293
|
end
|
295
294
|
|
295
|
+
# @private
|
296
|
+
def remove_invalid_characters(string)
|
297
|
+
string.gsub(/[^[:print:]\r\n]+/, '')
|
298
|
+
end
|
299
|
+
|
296
300
|
# @private
|
297
301
|
def operation_outcome_from_validator_response(response, runnable)
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
302
|
+
sanitized_body = remove_invalid_characters(response.body)
|
303
|
+
|
304
|
+
operation_outcome_from_hl7_wrapped_response(JSON.parse(sanitized_body))
|
305
|
+
rescue JSON::ParserError
|
306
|
+
runnable.add_message('error', "Validator Response: HTTP #{response.status}\n#{sanitized_body}")
|
307
|
+
raise Inferno::Exceptions::ErrorInValidatorException,
|
308
|
+
'Validator response was an unexpected format. ' \
|
309
|
+
'Review Messages tab or validator service logs for more information.'
|
306
310
|
end
|
307
311
|
end
|
308
312
|
|
@@ -124,7 +124,7 @@ module Inferno
|
|
124
124
|
runnable.add_message('error', e.message)
|
125
125
|
raise Inferno::Exceptions::ErrorInValidatorException, "Unable to connect to validator at #{url}."
|
126
126
|
end
|
127
|
-
outcome = operation_outcome_from_validator_response(response
|
127
|
+
outcome = operation_outcome_from_validator_response(response, runnable)
|
128
128
|
|
129
129
|
message_hashes = message_hashes_from_outcome(outcome, resource, profile_url)
|
130
130
|
|
@@ -212,16 +212,21 @@ module Inferno
|
|
212
212
|
).post('validate', resource.source_contents)
|
213
213
|
end
|
214
214
|
|
215
|
+
# @private
|
216
|
+
def remove_invalid_characters(string)
|
217
|
+
string.gsub(/[^[:print:]\r\n]+/, '')
|
218
|
+
end
|
219
|
+
|
215
220
|
# @private
|
216
221
|
def operation_outcome_from_validator_response(response, runnable)
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
222
|
+
sanitized_body = remove_invalid_characters(response.body)
|
223
|
+
|
224
|
+
FHIR::OperationOutcome.new(JSON.parse(sanitized_body))
|
225
|
+
rescue JSON::ParserError
|
226
|
+
runnable.add_message('error', "Validator Response: HTTP #{response.status}\n#{sanitized_body}")
|
227
|
+
raise Inferno::Exceptions::ErrorInValidatorException,
|
228
|
+
'Validator response was an unexpected format. ' \
|
229
|
+
'Review Messages tab or validator service logs for more information.'
|
225
230
|
end
|
226
231
|
end
|
227
232
|
|
@@ -0,0 +1,121 @@
|
|
1
|
+
module Inferno
|
2
|
+
module DSL
|
3
|
+
# This module contains the methods needed to perform FHIRPath evaluations
|
4
|
+
# on FHIR resources/elements. The actual evaluation is typically performed by an external
|
5
|
+
# FHIRPath evaluation service.
|
6
|
+
#
|
7
|
+
# Tests can leverage the evaluation functionality by calling `evaluate_fhirpath` to retrieve
|
8
|
+
# results of FHIRPath expressions.
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
#
|
12
|
+
# results = evaluate_fhirpath(resource: patient_resource, path: 'Patient.name.given')
|
13
|
+
#
|
14
|
+
# results will be an array representing the result of evaluating the given
|
15
|
+
# expression against the given root element. Each "result" in the returned
|
16
|
+
# array will be in the form
|
17
|
+
# `{ "type": "[FHIR datatype of the result]", "element": "[result value of the FHIRPath expression]" }`.
|
18
|
+
# @note the `element` field can either be a primitive value (string, boolean, etc.) or a FHIR::Model.
|
19
|
+
module FhirpathEvaluation
|
20
|
+
def self.included(klass)
|
21
|
+
klass.extend ClassMethods
|
22
|
+
end
|
23
|
+
|
24
|
+
# Evaluates a fhirpath expression for a given FHIR resource
|
25
|
+
#
|
26
|
+
# @param resource [FHIR::Model] the root FHIR resource to use when evaluating the fhirpath expression.
|
27
|
+
# @param path [String] The FHIRPath expression to evaluate.
|
28
|
+
# @param url [String] the url of the fhirpath service to use.
|
29
|
+
# @return [Array<Hash>] An array of hashes representing the result of evaluating the given expression against
|
30
|
+
# the given root resource.
|
31
|
+
def evaluate_fhirpath(resource:, path:, url: nil)
|
32
|
+
self.class.evaluator(url).evaluate_fhirpath(resource, path, self)
|
33
|
+
end
|
34
|
+
|
35
|
+
class Evaluator
|
36
|
+
# @private
|
37
|
+
def initialize(url = nil)
|
38
|
+
url(url)
|
39
|
+
end
|
40
|
+
|
41
|
+
# @private
|
42
|
+
def default_fhirpath_url
|
43
|
+
ENV.fetch('FHIRPATH_URL')
|
44
|
+
end
|
45
|
+
|
46
|
+
# Set/Get the url of the fhirpath service
|
47
|
+
#
|
48
|
+
# @param fhirpath_url [String]
|
49
|
+
# @return [String]
|
50
|
+
def url(fhirpath_url = nil)
|
51
|
+
@url ||= fhirpath_url || default_fhirpath_url
|
52
|
+
end
|
53
|
+
|
54
|
+
# Evaluates a fhirpath expression for a given FHIR resource
|
55
|
+
#
|
56
|
+
# @param fhir_resource [FHIR::Model] the root FHIR resource to use when evaluating the fhirpath expression.
|
57
|
+
# @param fhirpath_expression [String] The FHIRPath expression to evaluate.
|
58
|
+
# @param runnable [Inferno::Test] to add any error message that occurs.
|
59
|
+
# @return [Array<Hash>] An array hashes representing the result of evaluating the given expression against
|
60
|
+
# the given root resource. Each "result" in the returned array will be in the form
|
61
|
+
# `{ "type": "[FHIR datatype of the result]", "element": "[result value of the FHIRPath expression]" }`.
|
62
|
+
# @note the `element` field can either be a primitive value (string, boolean, etc.) or a FHIR::Model.
|
63
|
+
def evaluate_fhirpath(fhir_resource, fhirpath_expression, runnable)
|
64
|
+
begin
|
65
|
+
response = call_fhirpath_service(fhir_resource, fhirpath_expression)
|
66
|
+
rescue StandardError => e
|
67
|
+
# This could be a complete failure to connect (fhirpath service isn't running)
|
68
|
+
# or a timeout (fhirpath service took too long to respond).
|
69
|
+
runnable.add_message('error', e.message)
|
70
|
+
raise Inferno::Exceptions::ErrorInFhirpathException, "Unable to connect to FHIRPath service at #{url}."
|
71
|
+
end
|
72
|
+
|
73
|
+
sanitized_body = remove_invalid_characters(response.body)
|
74
|
+
return transform_fhirpath_results(JSON.parse(sanitized_body)) if response.status.to_s.start_with? '2'
|
75
|
+
|
76
|
+
runnable.add_message('error', "FHIRPath service Response: HTTP #{response.status}\n#{sanitized_body}")
|
77
|
+
raise Inferno::Exceptions::ErrorInFhirpathException,
|
78
|
+
'FHIRPath service call failed. Review Messages tab for more information.'
|
79
|
+
rescue JSON::ParserError
|
80
|
+
runnable.add_message('error', "Invalid FHIRPath service response format:\n#{sanitized_body}")
|
81
|
+
raise Inferno::Exceptions::ErrorInFhirpathException,
|
82
|
+
'Error occurred in the FHIRPath service. Review Messages tab for more information.'
|
83
|
+
end
|
84
|
+
|
85
|
+
# @private
|
86
|
+
def transform_fhirpath_results(fhirpath_results)
|
87
|
+
fhirpath_results.each do |result|
|
88
|
+
klass = FHIR.const_get(result['type'])
|
89
|
+
result['element'] = klass.new(result['element'])
|
90
|
+
rescue NameError
|
91
|
+
next
|
92
|
+
end
|
93
|
+
fhirpath_results
|
94
|
+
end
|
95
|
+
|
96
|
+
def call_fhirpath_service(fhir_resource, fhirpath_expression)
|
97
|
+
Faraday.new(
|
98
|
+
url,
|
99
|
+
request: { timeout: 600 }
|
100
|
+
).post(
|
101
|
+
"evaluate?path=#{fhirpath_expression}",
|
102
|
+
fhir_resource.to_json,
|
103
|
+
content_type: 'application/json'
|
104
|
+
)
|
105
|
+
end
|
106
|
+
|
107
|
+
# @private
|
108
|
+
def remove_invalid_characters(string)
|
109
|
+
string.gsub(/[^[:print:]\r\n]+/, '')
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
module ClassMethods
|
114
|
+
# @private
|
115
|
+
def evaluator(url = nil)
|
116
|
+
@evaluator ||= Inferno::DSL::FhirpathEvaluation::Evaluator.new(url)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module Inferno
|
2
|
+
module DSL
|
3
|
+
# The JWKS class provides methods to handle JSON Web Key Sets (JWKS)
|
4
|
+
# within Inferno.
|
5
|
+
#
|
6
|
+
# This class allows users to fetch, parse, and manage JWKS, ensuring
|
7
|
+
# that the necessary keys for verifying tokens are available.
|
8
|
+
class JWKS
|
9
|
+
class << self
|
10
|
+
# Returns a formatted JSON string of the JWKS public keys that are used for verification.
|
11
|
+
# This method filters out keys that do not have the 'verify' operation.
|
12
|
+
#
|
13
|
+
# @return [String] The formatted JSON string of the JWKS public keys.
|
14
|
+
#
|
15
|
+
# @example
|
16
|
+
# jwks_json = Inferno::JWKS.jwks_json
|
17
|
+
# puts jwks_json
|
18
|
+
def jwks_json
|
19
|
+
@jwks_json ||=
|
20
|
+
JSON.pretty_generate(
|
21
|
+
{ keys: jwks.export[:keys].select { |key| key[:key_ops]&.include?('verify') } }
|
22
|
+
)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Provides the default file path to the JWKS file.
|
26
|
+
# This method is primarily used internally to locate the default JWKS file.
|
27
|
+
#
|
28
|
+
# @return [String] The default JWKS file path.
|
29
|
+
#
|
30
|
+
# @private
|
31
|
+
def default_jwks_path
|
32
|
+
@default_jwks_path ||= File.join(__dir__, 'jwks.json')
|
33
|
+
end
|
34
|
+
|
35
|
+
# Fetches the JWKS file path from the environment variable `INFERNO_JWKS_PATH`.
|
36
|
+
# If the environment variable is not set, it falls back to the default path
|
37
|
+
# provided by `.default_jwks_path`.
|
38
|
+
#
|
39
|
+
# @return [String] The JWKS file path.
|
40
|
+
#
|
41
|
+
# @private
|
42
|
+
def jwks_path
|
43
|
+
@jwks_path ||=
|
44
|
+
ENV.fetch('INFERNO_JWKS_PATH', default_jwks_path)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Reads the JWKS content from the file located at the JWKS path.
|
48
|
+
#
|
49
|
+
# @return [String] The json content of the JWKS file.
|
50
|
+
#
|
51
|
+
# @private
|
52
|
+
def default_jwks_json
|
53
|
+
@default_jwks_json ||= File.read(jwks_path)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Parses and returns a `JWT::JWK::Set` object from the provided JWKS string
|
57
|
+
# or from the file located at the JWKS path. If a user-provided JWKS string
|
58
|
+
# is not available, it reads the JWKS from the file.
|
59
|
+
#
|
60
|
+
# @param user_jwks [String, nil] An optional json containing the JWKS.
|
61
|
+
# If not provided, the method reads from the file.
|
62
|
+
# @return [JWT::JWK::Set] The parsed JWKS set.
|
63
|
+
#
|
64
|
+
# @example
|
65
|
+
# # Using a user-provided JWKS string
|
66
|
+
# user_jwks = '{"keys":[...]}'
|
67
|
+
# jwks_set = Inferno::JWKS.jwks(user_jwks: user_jwks)
|
68
|
+
#
|
69
|
+
# # Using the default JWKS file
|
70
|
+
# jwks_set = Inferno::JWKS.jwks
|
71
|
+
def jwks(user_jwks: nil)
|
72
|
+
JWT::JWK::Set.new(JSON.parse(user_jwks.presence || default_jwks_json))
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
JWKS = DSL::JWKS
|
79
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require_relative '../utils/markdown_formatter'
|
2
|
+
|
3
|
+
module Inferno
|
4
|
+
module DSL
|
5
|
+
# This module contains methods to add meessages to runnable results
|
6
|
+
module Messages
|
7
|
+
include Inferno::Utils::MarkdownFormatter
|
8
|
+
# @private
|
9
|
+
def messages
|
10
|
+
@messages ||= []
|
11
|
+
end
|
12
|
+
|
13
|
+
# Add a message to the result.
|
14
|
+
#
|
15
|
+
# @param type [String] error, warning, or info
|
16
|
+
# @param message [String]
|
17
|
+
# @return [void]
|
18
|
+
def add_message(type, message)
|
19
|
+
messages << { type: type.to_s, message: format_markdown(message) }
|
20
|
+
end
|
21
|
+
|
22
|
+
# Add an informational message to the results of a test. If passed a
|
23
|
+
# block, a failed assertion will become an info message and test execution
|
24
|
+
# will continue.
|
25
|
+
#
|
26
|
+
# @param message [String]
|
27
|
+
# @return [void]
|
28
|
+
# @example
|
29
|
+
# # Add an info message
|
30
|
+
# info 'This message will be added to the test results'
|
31
|
+
#
|
32
|
+
# # The message for the failed assertion will be treated as an info
|
33
|
+
# # message. Test exection will continue.
|
34
|
+
# info { assert false == true }
|
35
|
+
def info(message = nil)
|
36
|
+
unless block_given?
|
37
|
+
add_message('info', message) unless message.nil?
|
38
|
+
return
|
39
|
+
end
|
40
|
+
|
41
|
+
yield
|
42
|
+
rescue Exceptions::AssertionException => e
|
43
|
+
add_message('info', e.message)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Add a warning message to the results of a test. If passed a block, a
|
47
|
+
# failed assertion will become a warning message and test execution will
|
48
|
+
# continue.
|
49
|
+
#
|
50
|
+
# @param message [String]
|
51
|
+
# @return [void]
|
52
|
+
# @example
|
53
|
+
# # Add a warning message
|
54
|
+
# warning 'This message will be added to the test results'
|
55
|
+
#
|
56
|
+
# # The message for the failed assertion will be treated as a warning
|
57
|
+
# # message. Test exection will continue.
|
58
|
+
# warning { assert false == true }
|
59
|
+
def warning(message = nil)
|
60
|
+
unless block_given?
|
61
|
+
add_message('warning', message) unless message.nil?
|
62
|
+
return
|
63
|
+
end
|
64
|
+
|
65
|
+
yield
|
66
|
+
rescue Exceptions::AssertionException => e
|
67
|
+
add_message('warning', e.message)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
data/lib/inferno/dsl/runnable.rb
CHANGED
@@ -291,6 +291,18 @@ module Inferno
|
|
291
291
|
to_s
|
292
292
|
end
|
293
293
|
|
294
|
+
# Set/Get the block that is executed when a runnable is run
|
295
|
+
#
|
296
|
+
# @param block [Proc]
|
297
|
+
# @return [Proc] the block that is executed when a runnable is run
|
298
|
+
def block(&block)
|
299
|
+
return @block unless block_given?
|
300
|
+
|
301
|
+
@block = block
|
302
|
+
end
|
303
|
+
|
304
|
+
alias run block
|
305
|
+
|
294
306
|
# @private
|
295
307
|
def all_children
|
296
308
|
@all_children ||= []
|