Skip to content
New issue

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

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

Already on GitHub? # to your account

FI-3710: Auth info fixes #602

Merged
merged 12 commits into from
Feb 12, 2025
1 change: 1 addition & 0 deletions lib/inferno/dsl.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require_relative 'dsl/assertions'
require_relative 'dsl/auth_info'
require_relative 'dsl/fhir_client'
require_relative 'dsl/fhir_validation'
require_relative 'dsl/fhir_evaluation/evaluator'
Expand Down
88 changes: 87 additions & 1 deletion lib/inferno/dsl/auth_info.rb
Original file line number Diff line number Diff line change
Expand Up @@ -307,9 +307,95 @@ def update_from_response_body(request)
self.expires_in = expires_in
self.issue_time = DateTime.now

add_to_client(client)
add_to_client(client) if client
self
end

# Returns the default configuration for the "auth_type" component
# @return [Hash]
def self.default_auth_type_component
{
name: :auth_type,
options: {
list_options: [
{ label: 'Public', value: 'public' },
{ label: 'Confidential Symmetric', value: 'symmetric' },
{ label: 'Confidential Asymmetric', value: 'asymmetric' },
{ label: 'Backend Services', value: 'backend_services' }
]
}
}
end

# Returns the default configuration for the "auth_type" component without
# the option for backend services auth
# @return [Hash]
def self.default_auth_type_component_without_backend_services
{
name: :auth_type,
options: {
list_options: [
{ label: 'Public', value: 'public' },
{ label: 'Confidential Symmetric', value: 'symmetric' },
{ label: 'Confidential Asymmetric', value: 'asymmetric' }
]
}
}
end

# Returns true when using public auth
# @return [Boolean]
def public_auth?
auth_type&.casecmp? 'public'
end

# Returns true when using confidential symmetric auth
# @return [Boolean]
def symmetric_auth?
auth_type&.casecmp? 'symmetric'
end

# Returns true when using confidential asymmetric auth
# @return [Boolean]
def asymmetric_auth?
auth_type&.casecmp? 'asymmetric'
end

# Returns true when using backend services auth
# @return [Boolean]
def backend_services_auth?
auth_type&.casecmp? 'backend_services'
end

# Returns true when using GET as the authorization request method
# @return [Boolean]
def get_auth_request?
auth_request_method&.casecmp? 'get'
end

# Returns true when using POST as the authorization request method
# @return [Boolean]
def post_auth_request?
auth_request_method&.casecmp? 'post'
end

# Returns true when pkce is enabled
# @return [Boolean]
def pkce_enabled?
pkce_support&.casecmp? 'enabled'
end

# Returns true when using the S256 pkce code challenge method
# @return [Boolean]
def s256_code_challenge_method?
pkce_code_challenge_method&.casecmp? 'S256'
end

# Returns true when using the palin pkce code challenge method
# @return [Boolean]
def plain_code_challenge_method?
pkce_code_challenge_method&.casecmp? 'plain'
end
end
end
end
15 changes: 14 additions & 1 deletion lib/inferno/dsl/configurable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,19 @@ def inputs
configuration[:inputs] ||= {}
end

# @private
# Recursively duplicate arrays/hashes to prevent them from being shared
# across different runnables
def deep_dup(value)
if value.is_a? Array
value.map { |element| deep_dup(element) }
elsif value.is_a? Hash
value.transform_values { |element| deep_dup(element) }
else
value.dup
end
end

# @private
def add_input(identifier, new_config = {})
existing_config = input(identifier)
Expand All @@ -148,7 +161,7 @@ def add_input(identifier, new_config = {})

inputs[identifier] =
Entities::Input
.new(**existing_config.to_hash)
.new(**deep_dup(existing_config.to_hash))
.merge(Entities::Input.new(**new_config))
end

Expand Down
1 change: 1 addition & 0 deletions lib/inferno/dsl/input_output_handling.rb
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ def available_inputs(selected_suite_options = nil)
end

available_inputs = children_available_inputs(selected_suite_options).merge(available_inputs)

order_available_inputs(available_inputs)
end
end
Expand Down
14 changes: 13 additions & 1 deletion lib/inferno/dsl/runnable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,23 @@ def self.extended(extending_class)
:@children_available_inputs # Needs to be recalculated
].freeze

# Recursively duplicate arrays/hashes to prevent them from being shared
# across different runnables
def deep_dup(value)
if value.is_a? Array
value.map { |element| deep_dup(element) }
elsif value.is_a? Hash
value.transform_values { |element| deep_dup(element) }
else
value.dup
end
end

# @private
def copy_instance_variables(subclass)
instance_variables
.reject { |variable| VARIABLES_NOT_TO_COPY.include? variable }
.each { |variable| subclass.instance_variable_set(variable, instance_variable_get(variable).dup) }
.each { |variable| subclass.instance_variable_set(variable, deep_dup(instance_variable_get(variable))) }

subclass.config(config)

Expand Down
66 changes: 63 additions & 3 deletions lib/inferno/entities/input.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class Input

# These are the attributes that can be directly copied when merging a
# runnable's input with an input configuration.
MERGEABLE_ATTRIBUTES = (ATTRIBUTES - [:type]).freeze
MERGEABLE_ATTRIBUTES = (ATTRIBUTES - [:type, :options]).freeze

def initialize(**params)
bad_params = params.keys - ATTRIBUTES
Expand All @@ -69,21 +69,27 @@ def merge_with_child(child_input)

self.type = child_input.type if child_input.present? && child_input.type != 'text'

merge_options(primary_source: self, secondary_source: child_input)

self
end

# @private
# Merge this input with an input from a configuration. Fields defined in
# the configuration take precedence over those defined on this input.
def merge(other_input)
def merge(other_input, merge_all: false)
return self if other_input.nil?

MERGEABLE_ATTRIBUTES.each do |attribute|
attributes_to_merge = merge_all ? ATTRIBUTES : MERGEABLE_ATTRIBUTES

attributes_to_merge.each do |attribute|
merge_attribute(attribute, primary_source: other_input, secondary_source: self)
end

self.type = other_input.type if other_input.type.present? && other_input.type != 'text'

merge_options(primary_source: other_input, secondary_source: self)

self
end

Expand All @@ -103,6 +109,60 @@ def merge_attribute(attribute, primary_source:, secondary_source:)
send("#{attribute}=", value)
end

# @private
# Merge input options. This performs a normal merge for all options except
# for the "components" field, the members of which are individually merged
# by `merge_components`
# @param primary_source [Input]
# @param secondary_source [Input]
def merge_options(primary_source:, secondary_source:)
primary_options = primary_source.options.dup || {}
secondary_options = secondary_source.options.dup || {}

return if primary_options.blank? && secondary_options.blank?

primary_components = primary_options.delete(:components) || []
secondary_components = secondary_options.delete(:components) || []

send('options=', secondary_options.merge(primary_options))

merge_components(primary_components:, secondary_components:)
end

# @private
# Merge component hashes.
# @param primary_source [Input]
# @param secondary_source [Input]
def merge_components(primary_components:, secondary_components:) # rubocop:disable Metrics/CyclomaticComplexity
primary_components
.each { |component| component[:name] = component[:name].to_sym }
secondary_components
.each { |component| component[:name] = component[:name].to_sym }

return if primary_components.blank? && secondary_components.blank?

component_keys =
(primary_components + secondary_components)
.map { |component| component[:name] }
.uniq

merged_components = component_keys.map do |key|
primary_component = primary_components.find { |component| component[:name] == key }
secondary_component = secondary_components.find { |component| component[:name] == key }

next secondary_component if primary_component.blank?

next primary_component if secondary_component.blank?

Input.new(**secondary_component).merge(Input.new(**primary_component), merge_all: true).to_hash
end

merged_components.each { |component| component[:name] = component[:name].to_sym }

self.options ||= {}
self.options[:components] = merged_components
end

def to_hash
ATTRIBUTES.each_with_object({}) do |attribute, hash|
value = send(attribute)
Expand Down
2 changes: 2 additions & 0 deletions lib/inferno/repositories/session_data.rb
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ def serialize_auth_info(params)
auth =
if params[:value].is_a? String
DSL::AuthInfo.new(JSON.parse(params[:value]))
elsif params[:value].is_a? Hash
DSL::AuthInfo.new(params[:value])
elsif !params[:value].is_a? DSL::AuthInfo
raise Exceptions::BadSessionDataType.new(
params[:name],
Expand Down
77 changes: 77 additions & 0 deletions spec/inferno/dsl/configurable_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,83 @@ def self.all_children
expect(config.input(identifier)).to eq(Inferno::Entities::Input.new(**existing_config.merge(new_config)))
end
end

context 'with auth_info' do
it 'merges individual component options' do
identifier = :auth_info_merge

list_options = [
{
label: 'Public',
value: 'public'
},
{
label: 'Confidential Symmetric',
value: 'symmetric'
}
]
existing_component_config = [
{
name: :use_discovery,
locked: true
},
{
name: :auth_type,
options: {
list_options:
}
},
{
name: :pkce_support,
default: 'enabled'
}
]

new_component_config = [
{
name: :auth_type,
default: 'symmetric'
},
{
name: :pkce_support,
locked: true
}
]

existing_config = {
type: 'auth_info',
options: {
components: existing_component_config
}
}
new_config = {
type: 'auth_info',
options: {
components: new_component_config
}
}

config.add_input(identifier, existing_config)
config.add_input(identifier, new_config)

final_components = config.input(identifier).options[:components]

expect(final_components.length).to eq(3)

auth_type_component = final_components.find { |component| component[:name] == :auth_type }

expect(auth_type_component[:default]).to eq('symmetric')
expect(auth_type_component[:options][:list_options].length).to eq(2)

pkce_component = final_components.find { |component| component[:name] == :pkce_support }

expect(pkce_component[:default]).to eq('enabled')
expect(pkce_component[:locked]).to be(true)

discovery_component = final_components.find { |component| component[:name] == :use_discovery }
expect(discovery_component).to eq(existing_component_config.first)
end
end
end

describe '#add_output' do
Expand Down