Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fi 2189 2190 required smart capabilities and scopes #25

Merged
merged 10 commits into from
Nov 6, 2023
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# frozen_string_literal: true

module CarinForBlueButtonTestKit
module CARIN4BBV110
class C4BBSMARTLaunchGroup < Inferno::TestGroup
id :c4bb_v110_smart_launch
title 'SMART App Launch'
description %(
# Background
Applications authorize to gain access to a patient record using the
[SMART App Launch
Protocol](http://hl7.org/fhir/smart-app-launch/1.0.0/)'s standalone
launch sequence.

# Testing Methodology
These tests first access the server's SMART discovery endpoints and
verify that they are available and that the server advertises support
for the required SMART capabilities and required authorization scopes.
They then perform a standalone launch to obtain an access token which
can be used by the remaining tests to access patient data.
)

group from: :smart_discovery,
run_as_group: true

group from: :smart_standalone_launch,
run_as_group: true,
config: {
outputs: {
patient_id: { name: :patient_ids },
smart_credentials: { name: :smart_credentials }
}
},
description: %(
# Background

The [Standalone
Launch Sequence](https://www.hl7.org/fhir/smart-app-launch/1.0.0/index.html#standalone-launch-sequence)
allows an app, like Inferno, to be launched independent of an
existing EHR session. It is one of the two launch methods described in
the SMART App Launch Framework alongside EHR Launch. The app will
request authorization for the provided scope from the authorization
endpoint, ultimately receiving an authorization token which can be used
to gain access to resources on the FHIR server.

# Test Methodology

Inferno will redirect the user to the authorization endpoint so that
they can provide any required credentials and authorize the application.
Upon successful authorization, Inferno will exchange the authorization
code provided for an access token.

For more information on the #{title}:

* [Standalone Launch Sequence](https://www.hl7.org/fhir/smart-app-launch/1.0.0/index.html#standalone-launch-sequence)
)
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# frozen_string_literal: true

module CarinForBlueButtonTestKit
module CARIN4BBV200DEVNONFINANCIAL
class SmartScopesTest < Inferno::Test
id :c4bb_v200devnonfinancial_smart_scopes
title 'Server supports the required authorization scopes.'
description %(
All required scopes requested are expected to be granted.
Server SHALL support, at a minimum, the following requested authorization scopes:
* `openid`
* `fhirUser`
* `launch/patient`
* `patient/ExplanationOfBenefit.read`
* `patient/Coverage.read`
* `patient/Patient.read`
* `patient/Organization.read`
* `patient/Practitioner.read`
* `user/ExplanationOfBenefit.read`
* `user/Coverage.read`
* `user/Patient.read`
* `user/Organization.read`
* `user/Practitioner.read`
)
input :requested_scopes, :received_scopes
uses_request :token

PATIENT_COMPARTMENT_RESOURCE_TYPES = %w[
Patient
ExplanationOfBenefit
Coverage
Organization
Practitioner
].freeze

def patient_compartment_resource_types
PATIENT_COMPARTMENT_RESOURCE_TYPES
end

def required_scopes
config.options[:required_scopes]
end

def access_level_regex
/\A(\*|\b(read|c?ru?d?s?)\b)/
end

def received_scope_test(scopes)
# check if openid, fhirUser, & launch/patient was granted
scope_subset = scopes - ['openid', 'fhirUser', 'launch/patient']
assert scope_subset.length == scopes.length - 3,
'openid, fhirUser, & launch/patient scopes must be supported. Received scopes: ' \
"#{scope_subset.join(', ')}."

granted_patient_level_resource_types = []
granted_user_level_resource_types = []

scope_subset.each do |scope|
scope_pieces = scope.split('/')
next unless scope_pieces.length == 2

scope_type, resource_scope = scope_pieces
next unless %w[patient user].include?(scope_type)

resource_scope_parts = resource_scope.split('.')
next unless resource_scope_parts.length == 2

resource_type, access_level = resource_scope_parts
next unless access_level =~ access_level_regex

if scope_type == 'patient'
granted_patient_level_resource_types << resource_type
else
granted_user_level_resource_types << resource_type
end
end

# Check if the required patient and user level scopes are granted
missing_patient_level_resource_types = patient_compartment_resource_types - granted_patient_level_resource_types
missing_patient_level_resource_types = [] if granted_patient_level_resource_types.include?('*')

assert missing_patient_level_resource_types.empty?,
"Requested patient-level scopes #{missing_patient_level_resource_types.join(', ')} " \
'were not granted by authorization server.'

missing_user_level_resource_types = patient_compartment_resource_types - granted_user_level_resource_types
missing_user_level_resource_types = [] if granted_user_level_resource_types.include?('*')
assert missing_user_level_resource_types.empty?,
"Requested user-level scopes #{missing_user_level_resource_types.join(', ')} " \
'were not granted by authorization server.'
end

run do
skip_if request.status != 200, 'Token exchange was unsuccessful'
[
{
scopes: requested_scopes,
received_or_requested: 'requested'
},
{
scopes: received_scopes,
received_or_requested: 'received'
}
].each do |metadata|
scopes = metadata[:scopes].split
received_or_requested = metadata[:received_or_requested]
missing_scopes = required_scopes - scopes

if received_or_requested == 'requested'
assert missing_scopes.empty?,
"Required scopes were not #{received_or_requested}: #{missing_scopes.join(', ')}"
else
received_scope_test(scopes)
end
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# frozen_string_literal: true

module CarinForBlueButtonTestKit
module CARIN4BBV200DEVNONFINANCIAL
class WellKnownCapabilitiesTest < Inferno::Test
id :c4bb_v200devnonfinancial_smart_capabilities
title 'Server Well-known configuration declares support for the required SMART capabilities'
description %(
Servers SHALL support the following [SMART on FHIR
capabilities](https://hl7.org/fhir/us/carin-bb/Security_And_Privacy_Considerations.html#authentication-and-authorization-requirements):

* `launch-standalone`
* `client-public`
* `client-confidential-symmetric`
* `sso-openid-connect`
* `context-standalone-patient`
* `permission-offline`
* `permission-patient`
* `permission-user`
)

input :well_known_configuration

run do
skip_if well_known_configuration.blank?, 'No SMART well-known configuration received'

assert_valid_json(well_known_configuration)

advertised_capabilities = JSON.parse(well_known_configuration)['capabilities']

assert advertised_capabilities.is_a?(Array),
"Expected `capabilities` field to be an Array, but found #{advertised_capabilities.class.name}"

required_capabilities = config.options[:required_capabilities] || []

missing_capabilities = required_capabilities - advertised_capabilities

missing_capabilities_string =
missing_capabilities
.map { |capability| "\n* `#{capability}`" }
.join

assert missing_capabilities.empty?,
"
Server did not advertise support for the following required capabilities:
#{missing_capabilities_string}
"
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# frozen_string_literal: true

require_relative 'c4bb_smart_launch/well_known_capabilities_test'
require_relative 'c4bb_smart_launch/smart_scopes_test'

module CarinForBlueButtonTestKit
module CARIN4BBV200DEVNONFINANCIAL
class C4BBSMARTLaunchGroup < Inferno::TestGroup
id :c4bb_v200devnonfinancial_smart_launch
title 'SMART App Launch'
description %(
# Background
Applications authorize to gain access to a patient record using the
[SMART App Launch
Protocol](http://hl7.org/fhir/smart-app-launch/1.0.0/)'s standalone
launch sequence.

# Testing Methodology
These tests first access the server's SMART discovery endpoints and
verify that they are available and that the server advertises support
for the required SMART capabilities and required authorization scopes.
They then perform a standalone launch to obtain an access token which
can be used by the remaining tests to access patient data.
)
input_order :url,
:standalone_client_id,
:standalone_client_secret,
:standalone_requested_scopes,
:use_pkce,
:pkce_code_challenge_method,
:standalone_authorization_method,
:client_auth_type,
:client_auth_encryption_method

group from: :smart_discovery do
run_as_group

test from: :c4bb_v200devnonfinancial_smart_capabilities do
config(
options: {
required_capabilities: %w[
launch-standalone
client-public
client-confidential-symmetric
sso-openid-connect
context-standalone-patient
permission-offline
permission-patient
permission-user
]
}
)
end
end

group from: :smart_standalone_launch do
run_as_group
description %(
# Background

The [Standalone
Launch Sequence](https://www.hl7.org/fhir/smart-app-launch/1.0.0/index.html#standalone-launch-sequence)
allows an app, like Inferno, to be launched independent of an
existing EHR session. It is one of the two launch methods described in
the SMART App Launch Framework alongside EHR Launch. The app will
request authorization for the provided scope from the authorization
endpoint, ultimately receiving an authorization token which can be used
to gain access to resources on the FHIR server.

# Test Methodology

Inferno will redirect the user to the authorization endpoint so that
they can provide any required credentials and authorize the application.
Upon successful authorization, Inferno will exchange the authorization
code provided for an access token.

For more information on the #{title}:

* [Standalone Launch Sequence](https://www.hl7.org/fhir/smart-app-launch/1.0.0/index.html#standalone-launch-sequence)
)

config(
inputs: {
requested_scopes: {
default: %(
launch/patient openid fhirUser
patient/ExplanationOfBenefit.read patient/Coverage.read
patient/Patient.read patient/Organization.read
patient/Practitioner.read user/ExplanationOfBenefit.read
user/Coverage.read user/Patient.read
user/Organization.read user/Practitioner.read
).gsub(/\s{2,}/, ' ').strip
}
},
outputs: {
patient_id: { name: :patient_ids },
smart_credentials: { name: :smart_credentials }
}
)

test from: :c4bb_v200devnonfinancial_smart_scopes do
config(
inputs: {
requested_scopes: { name: :standalone_requested_scopes },
received_scopes: { name: :standalone_received_scopes }
},
options: {
required_scopes: %w[
openid
fhirUser
launch/patient
patient/ExplanationOfBenefit.read
patient/Coverage.read
patient/Patient.read
patient/Organization.read
patient/Practitioner.read
user/ExplanationOfBenefit.read
user/Coverage.read
user/Patient.read
user/Organization.read
user/Practitioner.read
]
}
)
end
end
end
end
end
Loading