diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ab70543fa..803afd64c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,6 +44,25 @@ jobs: - name: Test run: bundle exec rake smithy:spec + smithy-rbs: + needs: [smithy] + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ env.latest_ruby_version }} + bundler-cache: true + + - name: Install rbs collection + run: rbs collection install + + - name: RBS + run: bundle exec rake smithy:rbs + smithy-schema: uses: ./.github/workflows/test.yml with: diff --git a/gems/smithy-client/lib/smithy-client.rb b/gems/smithy-client/lib/smithy-client.rb index 00156cec6..42149983f 100644 --- a/gems/smithy-client/lib/smithy-client.rb +++ b/gems/smithy-client/lib/smithy-client.rb @@ -51,6 +51,7 @@ # identity and auth require_relative 'smithy-client/auth' +require_relative 'smithy-client/auth_option' require_relative 'smithy-client/auth_scheme' require_relative 'smithy-client/identity_provider' require_relative 'smithy-client/null_signer' diff --git a/gems/smithy-client/lib/smithy-client/api_key_signer.rb b/gems/smithy-client/lib/smithy-client/api_key_signer.rb index 7da952c26..0526c3d7f 100644 --- a/gems/smithy-client/lib/smithy-client/api_key_signer.rb +++ b/gems/smithy-client/lib/smithy-client/api_key_signer.rb @@ -4,6 +4,13 @@ module Smithy module Client # Signs requests with the ApiKey identity. class ApiKeySigner + # @param [Hash] options + # @option options [String] :name + # The name of the header or query parameter to which the API key value will be assigned. + # @option options [String] :in + # Where to place the API key value. Valid values are "header" and "query". + # @option options [String] :scheme + # An optional scheme to be prepended to the API key value when signing in a header. def initialize(options = {}) @name = options[:name] @in = options[:in] diff --git a/gems/smithy-client/lib/smithy-client/auth.rb b/gems/smithy-client/lib/smithy-client/auth.rb index 87d56a20e..df6547b86 100644 --- a/gems/smithy-client/lib/smithy-client/auth.rb +++ b/gems/smithy-client/lib/smithy-client/auth.rb @@ -32,7 +32,10 @@ def resolve_with_endpoint_auth(config, endpoint_auth_schemes) properties[key] = value end - normalized_endpoint_schemes << { scheme_id: normalized_scheme_id, signer_properties: properties } + normalized_endpoint_schemes << AuthOption.new( + scheme_id: normalized_scheme_id, + signer_properties: properties + ) end resolved_auth_options = prioritize_auth_options(normalized_endpoint_schemes, config.auth_scheme_preference) resolve_auth_scheme(config.auth_schemes, resolved_auth_options) @@ -49,7 +52,7 @@ def prioritize_auth_options(auth_options, auth_scheme_preference) auth_options_by_id = {} auth_options.each do |option| - auth_options_by_id[option[:scheme_id]] = option + auth_options_by_id[option.scheme_id] = option end preferred_options = [] @@ -63,25 +66,22 @@ def prioritize_auth_options(auth_options, auth_scheme_preference) preferred_options.empty? ? auth_options : preferred_options end - def resolve_auth_scheme(auth_schemes, auth_options) # rubocop:disable Metrics/MethodLength + def resolve_auth_scheme(auth_schemes, auth_options) raise 'No auth options were resolved' if auth_options.empty? failures = [] auth_options.each do |auth_option| - scheme_id = auth_option[:scheme_id] - if scheme_id == 'smithy.api#noAuth' - return ResolvedAuth.new( - scheme_id: 'smithy.api#noAuth', signer: NullSigner.new, signer_properties: {}, - identity_provider: nil - ) - end + scheme_id = auth_option.scheme_id + return ResolvedAuth.new(scheme_id: scheme_id, signer: NullSigner.new) if scheme_id == 'smithy.api#noAuth' + auth_scheme = auth_schemes[scheme_id] error = validate_auth_scheme(auth_scheme, scheme_id) + unless error return ResolvedAuth.new( scheme_id: scheme_id, signer: auth_scheme.signer, - signer_properties: auth_option[:signer_properties] || {}, + signer_properties: auth_option.signer_properties, identity_provider: auth_scheme.identity_provider ) end diff --git a/gems/smithy-client/lib/smithy-client/auth_option.rb b/gems/smithy-client/lib/smithy-client/auth_option.rb new file mode 100644 index 000000000..87206fc43 --- /dev/null +++ b/gems/smithy-client/lib/smithy-client/auth_option.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Smithy + module Client + # Contains information about candidate authentication schemes. + class AuthOption + # @param [Hash] options + # @option options [String] :scheme_id The auth scheme ID. + # @option options [Hash] :signer_properties + # Additional properties to pass to the signer. + # This is a hash of String keys to String values and is signer-specific. + def initialize(options = {}) + @scheme_id = options[:scheme_id] + @signer_properties = options[:signer_properties] || {} + end + + # @return [String] + attr_reader :scheme_id + + # @return [Hash] + attr_reader :signer_properties + end + end +end diff --git a/gems/smithy-client/lib/smithy-client/login_signer.rb b/gems/smithy-client/lib/smithy-client/login_signer.rb index 4f6e5754a..441d4e84b 100644 --- a/gems/smithy-client/lib/smithy-client/login_signer.rb +++ b/gems/smithy-client/lib/smithy-client/login_signer.rb @@ -4,12 +4,15 @@ module Smithy module Client # Signs requests with the Login identity. class LoginSigner + # @param [Hash] options + # @option options [Symbol] :type + # The type of login authentication. Valid values are: :http_basic. def initialize(options = {}) - @scheme_id = options[:scheme_id] + @type = options[:type] end def sign_request(context) - raise NotImplementedError unless @scheme_id == 'smithy.api#httpBasicAuth' + raise NotImplementedError unless @type == :http_basic sign_with_basic(context.http_request, context.config.login_provider) end diff --git a/gems/smithy-client/lib/smithy-client/plugins/http_api_key_auth.rb b/gems/smithy-client/lib/smithy-client/plugins/http_api_key_auth.rb index 5da78610b..365539dbc 100644 --- a/gems/smithy-client/lib/smithy-client/plugins/http_api_key_auth.rb +++ b/gems/smithy-client/lib/smithy-client/plugins/http_api_key_auth.rb @@ -28,7 +28,11 @@ class that responds to #identity and returns a {Smithy::Client::ApiKey}. provider if provider.set? end - option(:api_key_signer) do |config| + option( + :api_key_signer, + doc_type: Smithy::Client::ApiKeySigner, + docstring: 'A signer class that signs requests with an API key.' + ) do |config| trait = config.service.traits['smithy.api#httpApiKeyAuth'] ApiKeySigner.new(name: trait['name'], in: trait['in'], scheme: trait['scheme']) end diff --git a/gems/smithy-client/lib/smithy-client/plugins/http_basic_auth.rb b/gems/smithy-client/lib/smithy-client/plugins/http_basic_auth.rb index c16df797f..34b61e652 100644 --- a/gems/smithy-client/lib/smithy-client/plugins/http_basic_auth.rb +++ b/gems/smithy-client/lib/smithy-client/plugins/http_basic_auth.rb @@ -36,8 +36,12 @@ class that responds to #identity and returns a {Smithy::Client::Login}. provider if provider.set? end - option(:login_signer) do |_config| - LoginSigner.new(scheme_id: 'smithy.api#httpBasicAuth') + option( + :login_signer, + doc_type: Smithy::Client::LoginSigner, + docstring: 'A signer class that signs requests with login credentials.' + ) do |_config| + LoginSigner.new(type: :http_basic) end def after_initialize(client) diff --git a/gems/smithy-client/lib/smithy-client/plugins/http_bearer_auth.rb b/gems/smithy-client/lib/smithy-client/plugins/http_bearer_auth.rb index 0c48363df..21710cbd1 100644 --- a/gems/smithy-client/lib/smithy-client/plugins/http_bearer_auth.rb +++ b/gems/smithy-client/lib/smithy-client/plugins/http_bearer_auth.rb @@ -28,7 +28,11 @@ class that responds to #identity and returns a {Smithy::Client::BearerToken}. provider if provider.set? end - option(:bearer_token_signer) do |_config| + option( + :bearer_token_signer, + doc_type: Smithy::Client::BearerTokenSigner, + docstring: 'A signer class that signs requests with a bearer token.' + ) do |_config| BearerTokenSigner.new end diff --git a/gems/smithy-client/lib/smithy-client/plugins/http_digest_auth.rb b/gems/smithy-client/lib/smithy-client/plugins/http_digest_auth.rb index a6743a4fb..314af415b 100644 --- a/gems/smithy-client/lib/smithy-client/plugins/http_digest_auth.rb +++ b/gems/smithy-client/lib/smithy-client/plugins/http_digest_auth.rb @@ -36,8 +36,12 @@ class that responds to #identity and returns a {Smithy::Client::Login}. provider if provider.set? end - option(:login_signer) do |_config| - LoginSigner.new(scheme_id: 'smithy.api#httpDigestAuth') + option( + :login_signer, + doc_type: Smithy::Client::LoginSigner, + docstring: 'A signer class that signs requests with login credentials.' + ) do |_config| + LoginSigner.new(type: :http_digest) end def after_initialize(client) diff --git a/gems/smithy-client/sig/smithy-client/auth_option.rbs b/gems/smithy-client/sig/smithy-client/auth_option.rbs new file mode 100644 index 000000000..235c1f87f --- /dev/null +++ b/gems/smithy-client/sig/smithy-client/auth_option.rbs @@ -0,0 +1,9 @@ +module Smithy + module Client + module AuthOption + def initialize: (?Hash[Symbol, untyped] options) -> void + attr_reader scheme_id: String + attr_reader signer_properties: Hash[String, String] + end + end +end diff --git a/gems/smithy-client/sig/smithy-client/service_error.rbs b/gems/smithy-client/sig/smithy-client/service_error.rbs index 68873e9e7..3a67288e8 100644 --- a/gems/smithy-client/sig/smithy-client/service_error.rbs +++ b/gems/smithy-client/sig/smithy-client/service_error.rbs @@ -1,11 +1,11 @@ module Smithy module Client class ServiceError < RuntimeError - def initialize: (HandlerContext, Schema::Structure) -> void + def initialize: (HandlerContext?, Schema::Structure?) -> void attr_reader code: String? - attr_reader context: HandlerContext - attr_accessor data: untyped? + attr_reader context: HandlerContext? + attr_accessor data: Schema::Structure? def self.code: () -> String? def self.code=: (String) -> void diff --git a/gems/smithy-client/spec/smithy-client/plugins/resolve_auth_spec.rb b/gems/smithy-client/spec/smithy-client/plugins/resolve_auth_spec.rb index 6bb86ee8d..828fb0917 100644 --- a/gems/smithy-client/spec/smithy-client/plugins/resolve_auth_spec.rb +++ b/gems/smithy-client/spec/smithy-client/plugins/resolve_auth_spec.rb @@ -107,10 +107,10 @@ module Plugins auth_resolver = Class.new do def resolve(_) [ - { + AuthOption.new( scheme_id: 'smithy.api#httpApiKeyAuth', signer_properties: { 'location' => 'query', 'name' => 'api_key' } - } + ) ] end end diff --git a/gems/smithy/lib/smithy/templates/client/auth_resolver.erb b/gems/smithy/lib/smithy/templates/client/auth_resolver.erb index a106f57d0..3ac833c77 100644 --- a/gems/smithy/lib/smithy/templates/client/auth_resolver.erb +++ b/gems/smithy/lib/smithy/templates/client/auth_resolver.erb @@ -6,7 +6,7 @@ module <%= module_name %> # Resolves the auth scheme from {AuthParameters}. class AuthResolver # @param [AuthParameters] parameters - # @return [Array] + # @return [Array] def resolve(parameters) <% auth_rules_code.each do |line| -%> <%= line %> diff --git a/gems/smithy/lib/smithy/templates/client/auth_resolver_rbs.erb b/gems/smithy/lib/smithy/templates/client/auth_resolver_rbs.erb index 317925dd6..b615ea66f 100644 --- a/gems/smithy/lib/smithy/templates/client/auth_resolver_rbs.erb +++ b/gems/smithy/lib/smithy/templates/client/auth_resolver_rbs.erb @@ -1,5 +1,5 @@ module <%= module_name %> class AuthResolver - def resolve: (AuthParameters) -> Array[String] + def resolve: (AuthParameters) -> Array[Smithy::Client::AuthOption] end end diff --git a/gems/smithy/lib/smithy/views/client/auth_resolver.rb b/gems/smithy/lib/smithy/views/client/auth_resolver.rb index bad2a78a0..c95f83b11 100644 --- a/gems/smithy/lib/smithy/views/client/auth_resolver.rb +++ b/gems/smithy/lib/smithy/views/client/auth_resolver.rb @@ -35,7 +35,7 @@ def auth_rules_code def add_service_auth_schemes_to_code(lines) service_auth_schemes.each do |auth_scheme| - lines << "options << { scheme_id: '#{auth_scheme}' }" + lines << "options << Smithy::Client::AuthOption.new(scheme_id: '#{auth_scheme}')" end end @@ -53,7 +53,7 @@ def add_operation_case_to_code(lines, auth_operations) def add_operation_auth_options_to_code(lines, operation) operation_auth_schemes(operation).each do |auth_scheme| - lines << " options << { scheme_id: '#{auth_scheme}' }" + lines << " options << Smithy::Client::AuthOption.new(scheme_id: '#{auth_scheme}')" end end diff --git a/gems/smithy/spec/interfaces/client/auth_resolver_spec.rb b/gems/smithy/spec/interfaces/client/auth_resolver_spec.rb index 0c4177751..ef778da7f 100644 --- a/gems/smithy/spec/interfaces/client/auth_resolver_spec.rb +++ b/gems/smithy/spec/interfaces/client/auth_resolver_spec.rb @@ -16,30 +16,30 @@ params = NoAuthTrait::AuthParameters.new(operation_name: :operation_a) auth_options = subject.resolve(params) expected = [ - { scheme_id: 'smithy.api#httpBasicAuth' }, - { scheme_id: 'smithy.api#httpBearerAuth' }, - { scheme_id: 'smithy.api#httpDigestAuth' } + 'smithy.api#httpBasicAuth', + 'smithy.api#httpBearerAuth', + 'smithy.api#httpDigestAuth' ] - expect(auth_options).to eq(expected) + expect(auth_options.map(&:scheme_id)).to eq(expected) end it 'returns the auth options for the operation with the auth trait' do params = NoAuthTrait::AuthParameters.new(operation_name: :operation_b) auth_options = subject.resolve(params) - expect(auth_options).to eq([{ scheme_id: 'smithy.api#httpDigestAuth' }]) + expected = ['smithy.api#httpDigestAuth'] + expect(auth_options.map(&:scheme_id)).to eq(expected) end it 'returns the auth options for the operation with the optionalAuth trait' do params = NoAuthTrait::AuthParameters.new(operation_name: :operation_g) auth_options = subject.resolve(params) - expect(auth_options).to eq( - [ - { scheme_id: 'smithy.api#httpBasicAuth' }, - { scheme_id: 'smithy.api#httpBearerAuth' }, - { scheme_id: 'smithy.api#httpDigestAuth' }, - { scheme_id: 'smithy.api#noAuth' } - ] - ) + expected = [ + 'smithy.api#httpBasicAuth', + 'smithy.api#httpBearerAuth', + 'smithy.api#httpDigestAuth', + 'smithy.api#noAuth' + ] + expect(auth_options.map(&:scheme_id)).to eq(expected) end end end @@ -53,32 +53,33 @@ it 'returns the auth options with the service auth trait' do params = AuthTrait::AuthParameters.new(operation_name: :operation_c) auth_options = subject.resolve(params) - expected = [{ scheme_id: 'smithy.api#httpBasicAuth' }, { scheme_id: 'smithy.api#httpDigestAuth' }] - expect(auth_options).to eq(expected) + expected = ['smithy.api#httpBasicAuth', 'smithy.api#httpDigestAuth'] + expect(auth_options.map(&:scheme_id)).to eq(expected) end it 'returns the auth options for the operation overriding the service auth trait' do params = AuthTrait::AuthParameters.new(operation_name: :operation_d) auth_options = subject.resolve(params) - expect(auth_options).to eq([{ scheme_id: 'smithy.api#httpBearerAuth' }]) + expected = ['smithy.api#httpBearerAuth'] + expect(auth_options.map(&:scheme_id)).to eq(expected) end it 'returns a noAuth option when the auth trait is empty' do params = AuthTrait::AuthParameters.new(operation_name: :operation_e) auth_options = subject.resolve(params) - expect(auth_options).to eq([{ scheme_id: 'smithy.api#noAuth' }]) + expected = ['smithy.api#noAuth'] + expect(auth_options.map(&:scheme_id)).to eq(expected) end it 'returns the auth options for the operation with the optionalAuth trait' do params = AuthTrait::AuthParameters.new(operation_name: :operation_f) auth_options = subject.resolve(params) - expect(auth_options).to eq( - [ - { scheme_id: 'smithy.api#httpBasicAuth' }, - { scheme_id: 'smithy.api#httpDigestAuth' }, - { scheme_id: 'smithy.api#noAuth' } - ] - ) + expected = [ + 'smithy.api#httpBasicAuth', + 'smithy.api#httpDigestAuth', + 'smithy.api#noAuth' + ] + expect(auth_options.map(&:scheme_id)).to eq(expected) end end end @@ -92,13 +93,15 @@ it 'returns the auth options for the operation with no auth traits' do params = NoAuth::AuthParameters.new(operation_name: :operation_h) auth_options = subject.resolve(params) - expect(auth_options).to eq([{ scheme_id: 'smithy.api#noAuth' }]) + expected = ['smithy.api#noAuth'] + expect(auth_options.map(&:scheme_id)).to eq(expected) end it 'returns the auth options for the operation with the optionalAuth trait' do params = NoAuth::AuthParameters.new(operation_name: :operation_i) auth_options = subject.resolve(params) - expect(auth_options).to eq([{ scheme_id: 'smithy.api#noAuth' }]) + expected = ['smithy.api#noAuth'] + expect(auth_options.map(&:scheme_id)).to eq(expected) end end end diff --git a/gems/smithy/spec/interfaces/welds/auth_schemes_spec.rb b/gems/smithy/spec/interfaces/welds/auth_schemes_spec.rb index 46a343832..aa2be542f 100644 --- a/gems/smithy/spec/interfaces/welds/auth_schemes_spec.rb +++ b/gems/smithy/spec/interfaces/welds/auth_schemes_spec.rb @@ -36,14 +36,14 @@ def remove_auth_schemes auth_resolver = ServiceWithAuthTrait::AuthResolver.new auth_parameters = ServiceWithAuthTrait::AuthParameters.new(operation_name: :operation_c) resolved_auths = auth_resolver.resolve(auth_parameters) - expect(resolved_auths).to include({ scheme_id: 'smithy.api#httpBasicAuth' }) + expect(resolved_auths.map(&:scheme_id)).to include('smithy.api#httpBasicAuth') end it 'removes auth schemes from the client' do auth_resolver = ServiceWithAuthTrait::AuthResolver.new auth_parameters = ServiceWithAuthTrait::AuthParameters.new(operation_name: :operation_d) resolved_auths = auth_resolver.resolve(auth_parameters) - expect(resolved_auths).to_not include({ scheme_id: 'smithy.api#httpBearerAuth' }) + expect(resolved_auths.map(&:scheme_id)).to_not include('smithy.api#httpBearerAuth') end end end