From c885c23e8d553a1f64a6a1c3169305d96a21a28a Mon Sep 17 00:00:00 2001 From: Matt Muller Date: Tue, 16 Sep 2025 16:05:41 -0400 Subject: [PATCH 1/7] WIP: rework of credentials --- gems/smithy-client/lib/smithy-client.rb | 2 +- .../lib/smithy-client/api_key.rb | 25 ++ .../lib/smithy-client/api_key_provider.rb | 16 ++ gems/smithy-client/lib/smithy-client/auth.rb | 94 ++++++++ .../lib/smithy-client/bearer_token.rb | 25 ++ .../smithy-client/bearer_token_provider.rb | 16 ++ .../smithy-client/http_api_key_provider.rb | 15 -- .../lib/smithy-client/http_bearer_provider.rb | 15 -- .../lib/smithy-client/http_login_provider.rb | 16 -- .../lib/smithy-client/identities/api_key.rb | 24 -- .../smithy-client/identities/http_login.rb | 22 -- .../lib/smithy-client/identities/token.rb | 24 -- .../lib/smithy-client/identity.rb | 15 -- .../lib/smithy-client/identity_provider.rb | 11 +- gems/smithy-client/lib/smithy-client/login.rb | 30 +++ .../lib/smithy-client/login_provider.rb | 17 ++ .../plugins/http_api_key_auth.rb | 25 +- .../smithy-client/plugins/http_basic_auth.rb | 27 +-- .../smithy-client/plugins/http_bearer_auth.rb | 23 +- .../smithy-client/plugins/http_digest_auth.rb | 36 +-- .../lib/smithy-client/plugins/resolve_auth.rb | 59 +---- .../smithy-client/plugins/stub_responses.rb | 7 +- .../refreshing_identity_provider.rb | 21 +- .../lib/smithy-client/stubbing.rb | 1 - .../stubbing/endpoint_provider.rb | 22 -- .../sig/smithy-client/api_key.rbs | 9 + .../sig/smithy-client/api_key_provider.rbs | 8 + .../sig/smithy-client/bearer_provider.rbs | 8 + .../sig/smithy-client/bearer_token.rbs | 10 + .../smithy-client/http_api_key_provider.rbs | 9 - .../smithy-client/http_bearer_provider.rbs | 9 - .../sig/smithy-client/identities/api_key.rbs | 10 - .../smithy-client/identities/http_login.rbs | 11 - .../sig/smithy-client/identities/token.rbs | 10 - .../sig/smithy-client/identity.rbs | 8 - .../sig/smithy-client/identity_provider.rbs | 4 +- .../smithy-client/sig/smithy-client/login.rbs | 10 + ..._login_provider.rbs => login_provider.rbs} | 1 - .../spec/smithy-client/identity_spec.rb | 19 -- .../plugins/http_api_key_auth_spec.rb | 34 +-- .../plugins/http_basic_auth_spec.rb | 54 ++--- .../plugins/http_bearer_auth_spec.rb | 36 +-- .../plugins/http_digest_auth_spec.rb | 52 ++--- .../plugins/resolve_auth_spec.rb | 218 +++++++++++++++--- .../plugins/stub_responses_spec.rb | 23 +- .../refreshing_identity_provider_spec.rb | 95 ++++---- .../smithy/templates/client/auth_resolver.erb | 2 +- .../templates/client/endpoint_plugin.erb | 26 ++- .../lib/smithy/views/client/auth_resolver.rb | 4 +- .../smithy/views/client/endpoint_plugin.rb | 17 +- gems/smithy/lib/smithy/weld.rb | 9 + .../transforms/default_endpoint_rules.json | 2 +- .../lib/smithy/welds/transforms/endpoints.rb | 11 +- .../interfaces/client/auth_resolver_spec.rb | 36 +-- .../interfaces/welds/auth_schemes_spec.rb | 4 +- .../shapes/lib/shapes/auth_resolver.rb | 4 +- projections/shapes/lib/shapes/client.rb | 8 +- .../shapes/lib/shapes/endpoint_parameters.rb | 2 +- .../shapes/lib/shapes/plugins/endpoint.rb | 26 ++- projections/shapes/shapes.gemspec | 2 +- projections/shapes/sig/shapes/client.rbs | 1 + .../weather/lib/weather/auth_resolver.rb | 4 +- projections/weather/lib/weather/client.rb | 8 +- .../lib/weather/endpoint_parameters.rb | 2 +- .../weather/lib/weather/plugins/endpoint.rb | 26 ++- projections/weather/sig/weather/client.rbs | 1 + projections/weather/weather.gemspec | 2 +- 67 files changed, 807 insertions(+), 616 deletions(-) create mode 100644 gems/smithy-client/lib/smithy-client/api_key.rb create mode 100644 gems/smithy-client/lib/smithy-client/api_key_provider.rb create mode 100644 gems/smithy-client/lib/smithy-client/auth.rb create mode 100644 gems/smithy-client/lib/smithy-client/bearer_token.rb create mode 100644 gems/smithy-client/lib/smithy-client/bearer_token_provider.rb delete mode 100644 gems/smithy-client/lib/smithy-client/http_api_key_provider.rb delete mode 100644 gems/smithy-client/lib/smithy-client/http_bearer_provider.rb delete mode 100644 gems/smithy-client/lib/smithy-client/http_login_provider.rb delete mode 100644 gems/smithy-client/lib/smithy-client/identities/api_key.rb delete mode 100644 gems/smithy-client/lib/smithy-client/identities/http_login.rb delete mode 100644 gems/smithy-client/lib/smithy-client/identities/token.rb delete mode 100644 gems/smithy-client/lib/smithy-client/identity.rb create mode 100644 gems/smithy-client/lib/smithy-client/login.rb create mode 100644 gems/smithy-client/lib/smithy-client/login_provider.rb delete mode 100644 gems/smithy-client/lib/smithy-client/stubbing/endpoint_provider.rb create mode 100644 gems/smithy-client/sig/smithy-client/api_key.rbs create mode 100644 gems/smithy-client/sig/smithy-client/api_key_provider.rbs create mode 100644 gems/smithy-client/sig/smithy-client/bearer_provider.rbs create mode 100644 gems/smithy-client/sig/smithy-client/bearer_token.rbs delete mode 100644 gems/smithy-client/sig/smithy-client/http_api_key_provider.rbs delete mode 100644 gems/smithy-client/sig/smithy-client/http_bearer_provider.rbs delete mode 100644 gems/smithy-client/sig/smithy-client/identities/api_key.rbs delete mode 100644 gems/smithy-client/sig/smithy-client/identities/http_login.rbs delete mode 100644 gems/smithy-client/sig/smithy-client/identities/token.rbs delete mode 100644 gems/smithy-client/sig/smithy-client/identity.rbs create mode 100644 gems/smithy-client/sig/smithy-client/login.rbs rename gems/smithy-client/sig/smithy-client/{http_login_provider.rbs => login_provider.rbs} (99%) delete mode 100644 gems/smithy-client/spec/smithy-client/identity_spec.rb diff --git a/gems/smithy-client/lib/smithy-client.rb b/gems/smithy-client/lib/smithy-client.rb index 2aeee5ae5..9dc2ce651 100644 --- a/gems/smithy-client/lib/smithy-client.rb +++ b/gems/smithy-client/lib/smithy-client.rb @@ -50,7 +50,7 @@ # identity and auth -require_relative 'smithy-client/identity' +require_relative 'smithy-client/auth' require_relative 'smithy-client/identity_provider' require_relative 'smithy-client/refreshing_identity_provider' diff --git a/gems/smithy-client/lib/smithy-client/api_key.rb b/gems/smithy-client/lib/smithy-client/api_key.rb new file mode 100644 index 000000000..8cf1cafbd --- /dev/null +++ b/gems/smithy-client/lib/smithy-client/api_key.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Smithy + module Client + # Identity class for API Key authentication. + class ApiKey + def initialize(options = {}) + @key = options[:key] + end + + # @return [String, nil] + attr_reader :key + + # @return [Boolean] + def set? + !!@key && !@key.empty? + end + + # @api private + def inspect + super.gsub(/@key="(\\"|[^"])*"/, '@key=[FILTERED]') + end + end + end +end diff --git a/gems/smithy-client/lib/smithy-client/api_key_provider.rb b/gems/smithy-client/lib/smithy-client/api_key_provider.rb new file mode 100644 index 000000000..4283074cf --- /dev/null +++ b/gems/smithy-client/lib/smithy-client/api_key_provider.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Smithy + module Client + # Provides an API key for authentication. + class ApiKeyProvider + include IdentityProvider + + # @param [Hash] options + # @option options [String, nil] :key + def initialize(options = {}) + @identity = ApiKey.new(key: options[:key]) + end + end + end +end diff --git a/gems/smithy-client/lib/smithy-client/auth.rb b/gems/smithy-client/lib/smithy-client/auth.rb new file mode 100644 index 000000000..ca14ff5f3 --- /dev/null +++ b/gems/smithy-client/lib/smithy-client/auth.rb @@ -0,0 +1,94 @@ +module Smithy + module Client + # @api private + module Auth + class << self + def resolve_auth(context, endpoint_properties = {}) + if endpoint_properties.key?('authSchemes') + resolve_auth_scheme_with_endpoint(context, endpoint_properties['authSchemes']) + else + resolve_auth_scheme_without_endpoint(context) + end + end + + private + + def resolve_auth_scheme_with_endpoint(context, endpoint_auth_schemes) + normalized_endpoint_schemes = [] + endpoint_auth_schemes.each do |scheme| + scheme_id = context.config.endpoint_auth_schemes[scheme['name']] + next unless scheme_id + + normalized_scheme = { scheme_id: scheme_id } + scheme.each do |key, value| + next if key == 'name' + + normalized_scheme[key] = value + end + normalized_endpoint_schemes << normalized_scheme + end + resolved_auth_options = prioritize_auth_options( + normalized_endpoint_schemes, + context.config.auth_scheme_preference + ) + resolve_auth_scheme(context.config.auth_schemes, resolved_auth_options) + end + + def resolve_auth_scheme_without_endpoint(context) + auth_parameters = context.client.class.auth_parameters.create(context) + auth_options = context.config.auth_resolver.resolve(auth_parameters) + resolved_auth_options = prioritize_auth_options(auth_options, context.config.auth_scheme_preference) + resolve_auth_scheme(context.config.auth_schemes, resolved_auth_options) + end + + def prioritize_auth_options(auth_options, auth_scheme_preference) + return auth_options if auth_scheme_preference.empty? + + auth_options_by_id = {} + auth_options.each do |option| + auth_options_by_id[option[:scheme_id]] = option + end + + preferred_options = [] + auth_scheme_preference.each do |scheme_id| + option = auth_options_by_id[scheme_id] + next unless option + + preferred_options << option + end + + preferred_options.empty? ? auth_options : preferred_options + end + + 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] + + # Anonymous auth does not have a plugin and does not sign + return auth_option if scheme_id == 'smithy.api#noAuth' + + error = validate_auth_scheme(auth_schemes, scheme_id) + return auth_option unless error + + failures << error + end + + raise failures.join("\n") + end + + def validate_auth_scheme(auth_schemes, scheme_id) + return "Auth scheme #{scheme_id} was not enabled for this request" unless auth_schemes.key?(scheme_id) + + identity_provider = auth_schemes[scheme_id] + return "Auth scheme #{scheme_id} did not have an identity provider configured" unless identity_provider + return "Auth scheme #{scheme_id} failed to resolve identity" unless identity_provider.set? + + nil + end + end + end + end +end diff --git a/gems/smithy-client/lib/smithy-client/bearer_token.rb b/gems/smithy-client/lib/smithy-client/bearer_token.rb new file mode 100644 index 000000000..39a67ad0a --- /dev/null +++ b/gems/smithy-client/lib/smithy-client/bearer_token.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Smithy + module Client + # Identity class for Bearer token authentication. + class BearerToken + def initialize(options = {}) + @token = options[:token] + end + + # @return [String, nil] + attr_reader :token + + # @return [Boolean] + def set? + !!@token && !@token.empty? + end + + # @api private + def inspect + super.gsub(/@token="(\\"|[^"])*"/, '@token=[FILTERED]') + end + end + end +end diff --git a/gems/smithy-client/lib/smithy-client/bearer_token_provider.rb b/gems/smithy-client/lib/smithy-client/bearer_token_provider.rb new file mode 100644 index 000000000..b9ff03b12 --- /dev/null +++ b/gems/smithy-client/lib/smithy-client/bearer_token_provider.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Smithy + module Client + # Provides a Bearer token for authentication. + class BearerTokenProvider + include IdentityProvider + + # @param [Hash] options + # @option options [String] :token + def initialize(options = {}) + @identity = BearerToken.new(token: options[:token]) + end + end + end +end diff --git a/gems/smithy-client/lib/smithy-client/http_api_key_provider.rb b/gems/smithy-client/lib/smithy-client/http_api_key_provider.rb deleted file mode 100644 index edaab218b..000000000 --- a/gems/smithy-client/lib/smithy-client/http_api_key_provider.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -module Smithy - module Client - # Returns an HTTP API key identity - class HttpApiKeyProvider - include IdentityProvider - - # @param [String] key - def initialize(key) - @identity = Identities::ApiKey.new(key: key) - end - end - end -end diff --git a/gems/smithy-client/lib/smithy-client/http_bearer_provider.rb b/gems/smithy-client/lib/smithy-client/http_bearer_provider.rb deleted file mode 100644 index 0a79cd621..000000000 --- a/gems/smithy-client/lib/smithy-client/http_bearer_provider.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -module Smithy - module Client - # Returns a Token identity - class HttpBearerProvider - include IdentityProvider - - # @param [String] token - def initialize(token) - @identity = Identities::Token.new(token: token) - end - end - end -end diff --git a/gems/smithy-client/lib/smithy-client/http_login_provider.rb b/gems/smithy-client/lib/smithy-client/http_login_provider.rb deleted file mode 100644 index e30f61c2e..000000000 --- a/gems/smithy-client/lib/smithy-client/http_login_provider.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -module Smithy - module Client - # Returns an HTTP login identity - class HttpLoginProvider - include IdentityProvider - - # @param [String] username - # @param [String] password - def initialize(username, password) - @identity = Identities::HttpLogin.new(username: username, password: password) - end - end - end -end diff --git a/gems/smithy-client/lib/smithy-client/identities/api_key.rb b/gems/smithy-client/lib/smithy-client/identities/api_key.rb deleted file mode 100644 index 4aa6d4bdc..000000000 --- a/gems/smithy-client/lib/smithy-client/identities/api_key.rb +++ /dev/null @@ -1,24 +0,0 @@ -# frozen_string_literal: true - -module Smithy - module Client - module Identities - # Identity class for API Key authentication. - class ApiKey < Identity - def initialize(key:, **) - @key = key - super(**) - end - - # @return [String, nil] - attr_reader :key - - # Removing the key from the default inspect string. - # @api private - def inspect - super.gsub(/@key="(\\"|[^"])*"/, '@key=[FILTERED]') - end - end - end - end -end diff --git a/gems/smithy-client/lib/smithy-client/identities/http_login.rb b/gems/smithy-client/lib/smithy-client/identities/http_login.rb deleted file mode 100644 index bf4dc93bb..000000000 --- a/gems/smithy-client/lib/smithy-client/identities/http_login.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -module Smithy - module Client - module Identities - # Identity class for HTTP login authentication. - class HttpLogin < Identity - def initialize(username:, password:, **) - @username = username - @password = password - super(**) - end - - # @return [String] - attr_reader :username - - # @return [String] - attr_reader :password - end - end - end -end diff --git a/gems/smithy-client/lib/smithy-client/identities/token.rb b/gems/smithy-client/lib/smithy-client/identities/token.rb deleted file mode 100644 index 8907ef043..000000000 --- a/gems/smithy-client/lib/smithy-client/identities/token.rb +++ /dev/null @@ -1,24 +0,0 @@ -# frozen_string_literal: true - -module Smithy - module Client - module Identities - # Identity class for token authentication. - class Token < Identity - def initialize(token:, **) - @token = token - super(**) - end - - # @return [String, nil] - attr_reader :token - - # Removing the token from the default inspect string. - # @api private - def inspect - super.gsub(/@token="(\\"|[^"])*"/, '@token=[FILTERED]') - end - end - end - end -end diff --git a/gems/smithy-client/lib/smithy-client/identity.rb b/gems/smithy-client/lib/smithy-client/identity.rb deleted file mode 100644 index 7811b1951..000000000 --- a/gems/smithy-client/lib/smithy-client/identity.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -module Smithy - module Client - # Base class for all Identity classes. - class Identity - def initialize(expiration: nil) - @expiration = expiration - end - - # @return [Time, nil] - attr_reader :expiration - end - end -end diff --git a/gems/smithy-client/lib/smithy-client/identity_provider.rb b/gems/smithy-client/lib/smithy-client/identity_provider.rb index f62243711..889672814 100644 --- a/gems/smithy-client/lib/smithy-client/identity_provider.rb +++ b/gems/smithy-client/lib/smithy-client/identity_provider.rb @@ -2,11 +2,18 @@ module Smithy module Client - # This module provides basic accessors and methods for an - # Identity Provider class which provides an Identity. + # This module provides basic accessors and methods for an Identity provider. module IdentityProvider # @return [Identity] attr_reader :identity + + # @return [Time, nil] + attr_reader :expiration + + # @return [Boolean] + def set? + !!identity && identity.set? + end end end end diff --git a/gems/smithy-client/lib/smithy-client/login.rb b/gems/smithy-client/lib/smithy-client/login.rb new file mode 100644 index 000000000..0a4b877c1 --- /dev/null +++ b/gems/smithy-client/lib/smithy-client/login.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Smithy + module Client + # Identity class for login authentication. + class Login + def initialize(options = {}) + @username = options[:username] + @password = options[:password] + end + + # @return [String] + attr_reader :username + + # @return [String] + attr_reader :password + + # @return [Boolean] + def set? + # username and password can be empty strings + !@username.nil? && !@password.nil? + end + + # @api private + def inspect + super.gsub(/@password="(\\"|[^"])*"/, '@password=[FILTERED]') + end + end + end +end diff --git a/gems/smithy-client/lib/smithy-client/login_provider.rb b/gems/smithy-client/lib/smithy-client/login_provider.rb new file mode 100644 index 000000000..74aaf0b07 --- /dev/null +++ b/gems/smithy-client/lib/smithy-client/login_provider.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Smithy + module Client + # Provides a login credentials for authentication. + class LoginProvider + include IdentityProvider + + # @param [Hash] options + # @option options [String, nil] :username + # @option options [String. nil] :password + def initialize(options = {}) + @identity = Login.new(username: options[:username], password: options[:password]) + end + end + end +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 40563e628..0fa93f3dd 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 @@ -1,7 +1,7 @@ # frozen_string_literal: true -require_relative '../http_api_key_provider' -require_relative '../identities/api_key' +require_relative '../api_key' +require_relative '../api_key_provider' module Smithy module Client @@ -9,7 +9,7 @@ module Plugins # @api private class HttpApiKeyAuth < Plugin option( - :http_api_key, + :api_key, doc_type: String, docstring: 'The API key to use for authentication.' ) do |config| @@ -17,17 +17,18 @@ class HttpApiKeyAuth < Plugin end option( - :http_api_key_provider, - doc_type: HttpApiKeyProvider, + :api_key_provider, + doc_type: ApiKeyProvider, docstring: <<~DOCS) do |config| - An API key identity provider. This can be an instance of a {Smithy::Client::HttpApiKeyProvider} or any - class that responds to #identity and returns a {Smithy::Client::Identities::HttpApiKey}. + An API key identity provider. This can be an instance of a {Smithy::Client::ApiKeyProvider} or any + class that responds to #identity and returns a {Smithy::Client::ApiKey}. DOCS - HttpApiKeyProvider.new(config.http_api_key) if config.http_api_key + provider = ApiKeyProvider.new(key: config.api_key) + provider if provider.set? end def after_initialize(client) - client.config.auth_schemes['smithy.api#httpApiKeyAuth'] = client.config.http_api_key_provider + client.config.auth_schemes['smithy.api#httpApiKeyAuth'] = client.config.api_key_provider end # @api private @@ -41,9 +42,11 @@ def call(context) def sign(context) properties = context.config.service.traits['smithy.api#httpApiKeyAuth'] + http_request = context.http_request + identity = context.config.api_key_provider.identity case properties['in'] - when 'header' then sign_in_header(properties, context.http_request, context.auth[:identity]) - when 'query' then sign_in_query_param(properties, context.http_request, context.auth[:identity]) + when 'header' then sign_in_header(properties, http_request, identity) + when 'query' then sign_in_query_param(properties, http_request, identity) end 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 b6aa5dd0e..4e0dd0476 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 @@ -1,7 +1,7 @@ # frozen_string_literal: true -require_relative '../http_login_provider' -require_relative '../identities/http_login' +require_relative '../login' +require_relative '../login_provider' module Smithy module Client @@ -9,7 +9,7 @@ module Plugins # @api private class HttpBasicAuth < Plugin option( - :http_login_username, + :login_username, doc_type: String, docstring: 'The username to use for authentication.' ) do |config| @@ -17,7 +17,7 @@ class HttpBasicAuth < Plugin end option( - :http_login_password, + :login_password, doc_type: String, docstring: 'The password to use for authentication.' ) do |config| @@ -25,19 +25,18 @@ class HttpBasicAuth < Plugin end option( - :http_login_provider, - doc_type: Smithy::Client::HttpLoginProvider, + :login_provider, + doc_type: Smithy::Client::LoginProvider, docstring: <<~DOCS) do |config| - A login identity provider. This can be an instance of a {Smithy::Client::HttpLoginProvider} or any - class that responds to #identity and returns a {Smithy::Client::Identities::HttpLogin}. + A login identity provider. This can be an instance of a {Smithy::Client::LoginProvider} or any + class that responds to #identity and returns a {Smithy::Client::Login}. DOCS - if config.http_login_username && config.http_login_password - Smithy::Client::HttpLoginProvider.new(config.http_login_username, config.http_login_password) - end + provider = LoginProvider.new(username: config.login_username, password: config.login_password) + provider if provider.set? end def after_initialize(client) - client.config.auth_schemes['smithy.api#httpBasicAuth'] = client.config.http_login_provider + client.config.auth_schemes['smithy.api#httpBasicAuth'] = client.config.login_provider end # @api private @@ -51,10 +50,8 @@ def call(context) def sign(context) http_request = context.http_request - identity = context.auth[:identity] - + identity = context.config.login_provider.identity http_request.headers.delete('Authorization') - # TODO: does not handle realm or other properties identity_string = "#{identity.username}:#{identity.password}" encoded = Base64.strict_encode64(identity_string) http_request.headers['Authorization'] = "Basic #{encoded}" 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 45b352735..de47c3884 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 @@ -1,7 +1,7 @@ # frozen_string_literal: true -require_relative '../http_bearer_provider' -require_relative '../identities/token' +require_relative '../bearer_token' +require_relative '../bearer_token_provider' module Smithy module Client @@ -9,7 +9,7 @@ module Plugins # @api private class HttpBearerAuth < Plugin option( - :http_bearer_token, + :bearer_token, doc_type: String, docstring: 'The bearer token to use for authentication.' ) do |config| @@ -17,17 +17,18 @@ class HttpBearerAuth < Plugin end option( - :http_bearer_provider, - doc_type: Smithy::Client::HttpBearerProvider, + :bearer_token_provider, + doc_type: Smithy::Client::BearerTokenProvider, docstring: <<~DOCS) do |config| - A bearer token identity provider. This can be an instance of a {Smithy::Client::HttpBearerProvider} or any - class that responds to #identity and returns a {Smithy::Client::Identities::HttpBearer}. + A bearer token identity provider. This can be an instance of a {Smithy::Client::BearerTokenProvider} or any + class that responds to #identity and returns a {Smithy::Client::BearerToken}. DOCS - Smithy::Client::HttpBearerProvider.new(config.http_bearer_token) if config.http_bearer_token + provider = Smithy::Client::BearerTokenProvider.new(token: config.bearer_token) + provider if provider.set? end def after_initialize(client) - client.config.auth_schemes['smithy.api#httpBearerAuth'] = client.config.http_bearer_provider + client.config.auth_schemes['smithy.api#httpBearerAuth'] = client.config.bearer_token_provider end # @api private @@ -41,8 +42,8 @@ def call(context) def sign(context) context.http_request.headers.delete('Authorization') - # TODO: does not handle realm or other properties - context.http_request.headers['Authorization'] = "Bearer #{context.auth[:identity].token}" + provider = context.config.bearer_token_provider + context.http_request.headers['Authorization'] = "Bearer #{provider.identity.token}" end 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 c0db89ed6..1fcd5734c 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 @@ -1,7 +1,7 @@ # frozen_string_literal: true -require_relative '../http_login_provider' -require_relative '../identities/http_login' +require_relative '../login' +require_relative '../login_provider' module Smithy module Client @@ -9,7 +9,7 @@ module Plugins # @api private class HttpDigestAuth < Plugin option( - :http_login_username, + :login_username, doc_type: String, docstring: 'The username to use for authentication.' ) do |config| @@ -17,7 +17,7 @@ class HttpDigestAuth < Plugin end option( - :http_login_password, + :login_password, doc_type: String, docstring: 'The password to use for authentication.' ) do |config| @@ -25,32 +25,18 @@ class HttpDigestAuth < Plugin end option( - :http_login_identity, - doc_type: Identities::HttpLogin, - docstring: 'The login identity to use for authentication.' - ) do |config| - if config.http_login_username && config.http_login_password - Identities::HttpLogin.new( - username: config.http_login_username, - password: config.http_login_password - ) - end - end - - option( - :http_login_provider, - doc_type: Smithy::Client::HttpLoginProvider, + :login_provider, + doc_type: Smithy::Client::LoginProvider, docstring: <<~DOCS) do |config| - A login identity provider. This can be an instance of a {Smithy::Client::HttpLoginProvider} or any - class that responds to #identity and returns a {Smithy::Client::Identities::HttpLogin}. + A login identity provider. This can be an instance of a {Smithy::Client::LoginProvider} or any + class that responds to #identity and returns a {Smithy::Client::Login}. DOCS - if config.http_login_username && config.http_login_password - Smithy::Client::HttpLoginProvider.new(config.http_login_username, config.http_login_password) - end + provider = LoginProvider.new(username: config.login_username, password: config.login_password) + provider if provider.set? end def after_initialize(client) - client.config.auth_schemes['smithy.api#httpDigestAuth'] = client.config.http_login_provider + client.config.auth_schemes['smithy.api#httpDigestAuth'] = client.config.login_provider end # @api private diff --git a/gems/smithy-client/lib/smithy-client/plugins/resolve_auth.rb b/gems/smithy-client/lib/smithy-client/plugins/resolve_auth.rb index e25c80f72..de834a394 100644 --- a/gems/smithy-client/lib/smithy-client/plugins/resolve_auth.rb +++ b/gems/smithy-client/lib/smithy-client/plugins/resolve_auth.rb @@ -10,10 +10,18 @@ class ResolveAuth < Plugin doc_default: 'AuthResolver.new', doc_type: '#resolve(context)', rbs_type: 'AuthResolver', - docstring: 'An object that resolves authentication schemes for request signing' + docstring: 'An object that resolves authentication schemes for request signing.' ) - # @api private + option( + :auth_scheme_preference, + doc_type: 'Array', + rbs_type: 'Array[String]', + docstring: 'A list of preferred authentication schemes to use when making a request.' + ) do + [] + end + option(:auth_schemes) { {} } def before_initialize(client_class, options) @@ -23,54 +31,9 @@ def before_initialize(client_class, options) # @api private class Handler < Smithy::Client::Handler def call(context) - auth_parameters = context.client.class.auth_parameters.create(context) - auth_options = context.config.auth_resolver.resolve(auth_parameters) - context.auth = resolve_auth(context, auth_options) + context.auth = Auth.resolve_auth(context, context[:resolved_endpoint].properties) @handler.call(context) end - - private - - def resolve_auth(context, auth_options) - failures = [] - raise 'No auth options were resolved' if auth_options.empty? - - auth_options.each do |auth_option| - # Anonymous auth does not have a plugin and does not sign, - # so if auth scheme is noAuth then just return scheme_id. - return { scheme_id: auth_option } if auth_option == 'smithy.api#noAuth' - - unless context.config.auth_schemes.key?(auth_option) - failures << "Auth scheme #{auth_option} was not enabled for this request" - next - end - - identity_provider = context.config.auth_schemes[auth_option] - resolved_auth = try_load_auth_scheme(auth_option, identity_provider, failures) - - return resolved_auth if resolved_auth - end - - raise failures.join("\n") - end - - def try_load_auth_scheme(scheme_id, identity_provider, failures) - unless identity_provider - failures << "Auth scheme #{scheme_id} did not have an identity resolver configured" - return - end - - identity = identity_provider.identity - unless identity - failures << "Auth scheme #{scheme_id} failed to resolve identity" - return - end - - { - scheme_id: scheme_id, - identity: identity - } - end end handler(Handler, step: :sign, priority: 70) diff --git a/gems/smithy-client/lib/smithy-client/plugins/stub_responses.rb b/gems/smithy-client/lib/smithy-client/plugins/stub_responses.rb index ca3166f79..f24d4719f 100644 --- a/gems/smithy-client/lib/smithy-client/plugins/stub_responses.rb +++ b/gems/smithy-client/lib/smithy-client/plugins/stub_responses.rb @@ -17,13 +17,9 @@ class StubResponses < Plugin DOCS option(:stubs) { {} } - # @api private option(:stubs_mutex) { Mutex.new } - # @api private option(:api_requests) { [] } - # @api private option(:api_requests_mutex) { Mutex.new } - # @api private option(:stubber) { Stubbing::NullProtocol.new } def add_handlers(handlers, config) @@ -35,8 +31,9 @@ def add_handlers(handlers, config) def before_initialize(_client_class, options) return unless options[:stub_responses] + return if options.key?(:endpoint) || options.key?(:endpoint_provider) - options[:endpoint_provider] ||= Stubbing::EndpointProvider.new + options[:endpoint] = 'http://stubbed-endpoint' end def after_initialize(client) diff --git a/gems/smithy-client/lib/smithy-client/refreshing_identity_provider.rb b/gems/smithy-client/lib/smithy-client/refreshing_identity_provider.rb index 53ae29878..9489dca54 100644 --- a/gems/smithy-client/lib/smithy-client/refreshing_identity_provider.rb +++ b/gems/smithy-client/lib/smithy-client/refreshing_identity_provider.rb @@ -10,20 +10,23 @@ module RefreshingIdentityProvider SYNC_EXPIRATION_LENGTH = 300 # 5 minutes ASYNC_EXPIRATION_LENGTH = 600 # 10 minutes - def initialize + def initialize(_options = {}) @mutex = Mutex.new + refresh end - # @return [Identities::Base] + # @return [Identity] def identity - if @identity - refresh_if_near_expiration! - else # initialization - @mutex.synchronize { refresh } - end + refresh_if_near_expiration! @identity end + # Refresh credentials. + # @return [void] + def refresh! + @mutex.synchronize { refresh } + end + private def sync_expiration_length @@ -58,7 +61,9 @@ def refresh_if_near_expiration! end def near_expiration?(expiration_length) - (Time.now.to_i + expiration_length) > @identity.expiration.to_i + return false unless @expiration + + Time.now + expiration_length > @expiration end end end diff --git a/gems/smithy-client/lib/smithy-client/stubbing.rb b/gems/smithy-client/lib/smithy-client/stubbing.rb index 7e9b7d2f4..84a3f47b2 100644 --- a/gems/smithy-client/lib/smithy-client/stubbing.rb +++ b/gems/smithy-client/lib/smithy-client/stubbing.rb @@ -2,7 +2,6 @@ require_relative 'stubbing/data_applicator' require_relative 'stubbing/empty_stub' -require_relative 'stubbing/endpoint_provider' require_relative 'stubbing/null_protocol' require_relative 'stubbing/stub_data' diff --git a/gems/smithy-client/lib/smithy-client/stubbing/endpoint_provider.rb b/gems/smithy-client/lib/smithy-client/stubbing/endpoint_provider.rb deleted file mode 100644 index 38b5bc950..000000000 --- a/gems/smithy-client/lib/smithy-client/stubbing/endpoint_provider.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -module Smithy - module Client - module Stubbing - # Default endpoint provider when stubbing is configured. - # @api private - class EndpointProvider - def resolve(parameters) - uri = - if EndpointRules.set?(parameters.endpoint) - parameters.endpoint - else - 'http://stubbed-endpoint' - end - - EndpointRules::Endpoint.new(uri: uri) - end - end - end - end -end diff --git a/gems/smithy-client/sig/smithy-client/api_key.rbs b/gems/smithy-client/sig/smithy-client/api_key.rbs new file mode 100644 index 000000000..4208c2da7 --- /dev/null +++ b/gems/smithy-client/sig/smithy-client/api_key.rbs @@ -0,0 +1,9 @@ +module Smithy + module Client + class ApiKey + def initialize: (Hash[Symbol, untyped] options) -> void + attr_reader key: String + def set?: () -> bool + end + end +end diff --git a/gems/smithy-client/sig/smithy-client/api_key_provider.rbs b/gems/smithy-client/sig/smithy-client/api_key_provider.rbs new file mode 100644 index 000000000..54f652df7 --- /dev/null +++ b/gems/smithy-client/sig/smithy-client/api_key_provider.rbs @@ -0,0 +1,8 @@ +module Smithy + module Client + class ApiKeyProvider + include IdentityProvider + def initialize: (Hash[Symbol, untyped] options) -> void + end + end +end diff --git a/gems/smithy-client/sig/smithy-client/bearer_provider.rbs b/gems/smithy-client/sig/smithy-client/bearer_provider.rbs new file mode 100644 index 000000000..709ede4cd --- /dev/null +++ b/gems/smithy-client/sig/smithy-client/bearer_provider.rbs @@ -0,0 +1,8 @@ +module Smithy + module Client + class BearerTokenProvider + include IdentityProvider + def initialize: (Hash[Symbol, untyped] options) -> void + end + end +end diff --git a/gems/smithy-client/sig/smithy-client/bearer_token.rbs b/gems/smithy-client/sig/smithy-client/bearer_token.rbs new file mode 100644 index 000000000..f229412cd --- /dev/null +++ b/gems/smithy-client/sig/smithy-client/bearer_token.rbs @@ -0,0 +1,10 @@ +module Smithy + module Client + class BearerToken + def initialize: (Hash[Symbol, untyped] options) -> void + attr_reader token: String + attr_reader password: String + def set?: () -> bool + end + end +end diff --git a/gems/smithy-client/sig/smithy-client/http_api_key_provider.rbs b/gems/smithy-client/sig/smithy-client/http_api_key_provider.rbs deleted file mode 100644 index ed665a02f..000000000 --- a/gems/smithy-client/sig/smithy-client/http_api_key_provider.rbs +++ /dev/null @@ -1,9 +0,0 @@ -module Smithy - module Client - class HttpApiKeyProvider - include IdentityProvider - - def initialize: (String) -> void - end - end -end diff --git a/gems/smithy-client/sig/smithy-client/http_bearer_provider.rbs b/gems/smithy-client/sig/smithy-client/http_bearer_provider.rbs deleted file mode 100644 index fe2f789f2..000000000 --- a/gems/smithy-client/sig/smithy-client/http_bearer_provider.rbs +++ /dev/null @@ -1,9 +0,0 @@ -module Smithy - module Client - class HttpBearerProvider - include IdentityProvider - - def initialize: (String) -> void - end - end -end diff --git a/gems/smithy-client/sig/smithy-client/identities/api_key.rbs b/gems/smithy-client/sig/smithy-client/identities/api_key.rbs deleted file mode 100644 index d93832a25..000000000 --- a/gems/smithy-client/sig/smithy-client/identities/api_key.rbs +++ /dev/null @@ -1,10 +0,0 @@ -module Smithy - module Client - module Identities - class HttpApiKey < Identity - def initialize: (key: String, **untyped options) -> void - attr_reader key: String - end - end - end -end diff --git a/gems/smithy-client/sig/smithy-client/identities/http_login.rbs b/gems/smithy-client/sig/smithy-client/identities/http_login.rbs deleted file mode 100644 index 085fad89e..000000000 --- a/gems/smithy-client/sig/smithy-client/identities/http_login.rbs +++ /dev/null @@ -1,11 +0,0 @@ -module Smithy - module Client - module Identities - class HttpLogin < Identity - def initialize: (username: String, password: String, **untyped options) -> void - attr_reader username: String - attr_reader password: String - end - end - end -end diff --git a/gems/smithy-client/sig/smithy-client/identities/token.rbs b/gems/smithy-client/sig/smithy-client/identities/token.rbs deleted file mode 100644 index 7443db992..000000000 --- a/gems/smithy-client/sig/smithy-client/identities/token.rbs +++ /dev/null @@ -1,10 +0,0 @@ -module Smithy - module Client - module Identities - class Token < Identity - def initialize: (token: String, **untyped options) -> void - attr_reader token: String - end - end - end -end diff --git a/gems/smithy-client/sig/smithy-client/identity.rbs b/gems/smithy-client/sig/smithy-client/identity.rbs deleted file mode 100644 index 0275ef1b5..000000000 --- a/gems/smithy-client/sig/smithy-client/identity.rbs +++ /dev/null @@ -1,8 +0,0 @@ -module Smithy - module Client - class Identity - def initialize: (?expiration: Time?) -> void - attr_reader expiration: Time? - end - end -end diff --git a/gems/smithy-client/sig/smithy-client/identity_provider.rbs b/gems/smithy-client/sig/smithy-client/identity_provider.rbs index 6e1e436ae..2cfedd2d8 100644 --- a/gems/smithy-client/sig/smithy-client/identity_provider.rbs +++ b/gems/smithy-client/sig/smithy-client/identity_provider.rbs @@ -1,7 +1,9 @@ module Smithy module Client module IdentityProvider - attr_reader identity: Identity + attr_reader identity: Object + attr_reader expiration: Time? + def set?: () -> bool end end end diff --git a/gems/smithy-client/sig/smithy-client/login.rbs b/gems/smithy-client/sig/smithy-client/login.rbs new file mode 100644 index 000000000..4ae021de5 --- /dev/null +++ b/gems/smithy-client/sig/smithy-client/login.rbs @@ -0,0 +1,10 @@ +module Smithy + module Client + class Login + def initialize: (Hash[Symbol, untyped] options) -> void + attr_reader username: String + attr_reader password: String + def set?: () -> bool + end + end +end diff --git a/gems/smithy-client/sig/smithy-client/http_login_provider.rbs b/gems/smithy-client/sig/smithy-client/login_provider.rbs similarity index 99% rename from gems/smithy-client/sig/smithy-client/http_login_provider.rbs rename to gems/smithy-client/sig/smithy-client/login_provider.rbs index 495c028b2..97e38b9fd 100644 --- a/gems/smithy-client/sig/smithy-client/http_login_provider.rbs +++ b/gems/smithy-client/sig/smithy-client/login_provider.rbs @@ -2,7 +2,6 @@ module Smithy module Client class HttpLoginProvider include IdentityProvider - def initialize: (String, String) -> void end end diff --git a/gems/smithy-client/spec/smithy-client/identity_spec.rb b/gems/smithy-client/spec/smithy-client/identity_spec.rb deleted file mode 100644 index 0bc4f26cb..000000000 --- a/gems/smithy-client/spec/smithy-client/identity_spec.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -require_relative '../spec_helper' - -module Smithy - module Client - describe Identity do - let(:expiration) { Time.now } - - subject { Identity.new(expiration: expiration) } - - describe '#expiration' do - it 'returns the expiration' do - expect(subject.expiration).to eq(expiration) - end - end - end - end -end diff --git a/gems/smithy-client/spec/smithy-client/plugins/http_api_key_auth_spec.rb b/gems/smithy-client/spec/smithy-client/plugins/http_api_key_auth_spec.rb index bb4cfabfc..4179c1e92 100644 --- a/gems/smithy-client/spec/smithy-client/plugins/http_api_key_auth_spec.rb +++ b/gems/smithy-client/spec/smithy-client/plugins/http_api_key_auth_spec.rb @@ -17,38 +17,38 @@ module Plugins let(:client) { client_class.new(stub_responses: true) } - it 'adds an :http_api_key option to config' do - expect(client.config).to respond_to(:http_api_key) + it 'adds an :api_key option to config' do + expect(client.config).to respond_to(:api_key) end - it 'adds an :http_api_key_provider option to config' do - expect(client.config).to respond_to(:http_api_key_provider) + it 'adds an :api_key_provider option to config' do + expect(client.config).to respond_to(:api_key_provider) end - it 'does not default a :http_api_key' do + it 'does not default a :api_key' do client = client_class.new - expect(client.config.http_api_key).to be_nil + expect(client.config.api_key).to be_nil end - it 'does not default a :http_api_key_provider' do + it 'does not default a :api_key_provider' do client = client_class.new - expect(client.config.http_api_key_provider).to be_nil + expect(client.config.api_key_provider).to be_nil end - it 'has a default :http_api_key when :stub_responses is true' do - expect(client.config.http_api_key).to eq('stubbed-api-key') + it 'has a default :api_key when :stub_responses is true' do + expect(client.config.api_key).to eq('stubbed-api-key') end - it 'has a default :http_api_key_provider when :stub_responses is true' do - provider = client.config.http_api_key_provider - expect(provider).to be_a(HttpApiKeyProvider) + it 'has a default :api_key_provider when :stub_responses is true' do + provider = client.config.api_key_provider + expect(provider).to be_a(ApiKeyProvider) expect(provider.identity.key).to eq('stubbed-api-key') end - it 'defaults a :http_api_key_provider when :http_api_key is set' do - client = client_class.new(http_api_key: 'api-key') - provider = client.config.http_api_key_provider - expect(provider).to be_a(HttpApiKeyProvider) + it 'defaults a :api_key_provider when :api_key is set' do + client = client_class.new(api_key: 'api-key') + provider = client.config.api_key_provider + expect(provider).to be_a(ApiKeyProvider) expect(provider.identity.key).to eq('api-key') end diff --git a/gems/smithy-client/spec/smithy-client/plugins/http_basic_auth_spec.rb b/gems/smithy-client/spec/smithy-client/plugins/http_basic_auth_spec.rb index dbe356783..b8092bd12 100644 --- a/gems/smithy-client/spec/smithy-client/plugins/http_basic_auth_spec.rb +++ b/gems/smithy-client/spec/smithy-client/plugins/http_basic_auth_spec.rb @@ -17,58 +17,58 @@ module Plugins let(:client) { client_class.new(stub_responses: true) } - it 'adds an :http_login_username option to config' do - expect(client.config).to respond_to(:http_login_username) + it 'adds an :login_username option to config' do + expect(client.config).to respond_to(:login_username) end - it 'adds an :http_login_password option to config' do - expect(client.config).to respond_to(:http_login_password) + it 'adds an :login_password option to config' do + expect(client.config).to respond_to(:login_password) end - it 'adds an :http_login_provider option to config' do - expect(client.config).to respond_to(:http_login_provider) + it 'adds an :login_provider option to config' do + expect(client.config).to respond_to(:login_provider) end - it 'does not default an :http_login_username or :http_login_password' do + it 'does not default an :login_username or :login_password' do client = client_class.new - expect(client.config.http_login_username).to be_nil - expect(client.config.http_login_password).to be_nil + expect(client.config.login_username).to be_nil + expect(client.config.login_password).to be_nil end - it 'does not default a :http_login_provider' do + it 'does not default a :login_provider' do client = client_class.new - expect(client.config.http_login_provider).to be_nil + expect(client.config.login_provider).to be_nil end - it 'has a default :http_login_username and :http_login_password when :stub_responses is true' do - expect(client.config.http_login_username).to eq('stubbed-username') - expect(client.config.http_login_password).to eq('stubbed-password') + it 'has a default :login_username and :login_password when :stub_responses is true' do + expect(client.config.login_username).to eq('stubbed-username') + expect(client.config.login_password).to eq('stubbed-password') end - it 'has a default :http_login_provider when :stub_responses is true' do - provider = client.config.http_login_provider - expect(provider).to be_a(HttpLoginProvider) + it 'has a default :login_provider when :stub_responses is true' do + provider = client.config.login_provider + expect(provider).to be_a(LoginProvider) identity = provider.identity expect(identity.username).to eq('stubbed-username') expect(identity.password).to eq('stubbed-password') end - it 'defaults a :http_login_provider when :http_login_username and :http_login_password are set' do - client = client_class.new(http_login_username: 'username', http_login_password: 'password') - provider = client.config.http_login_provider - expect(provider).to be_a(HttpLoginProvider) + it 'defaults a :login_provider when :login_username and :login_password are set' do + client = client_class.new(login_username: 'username', login_password: 'password') + provider = client.config.login_provider + expect(provider).to be_a(LoginProvider) identity = provider.identity expect(identity.username).to eq('username') expect(identity.password).to eq('password') end - it 'does not default a:http_login_provider when one of the parts is set' do - client = client_class.new(http_login_username: 'username') - provider = client.config.http_login_provider + it 'does not default a:login_provider when one of the parts is set' do + client = client_class.new(login_username: 'username') + provider = client.config.login_provider expect(provider).to be_nil - client = client_class.new(http_login_password: 'password') - provider = client.config.http_login_provider + client = client_class.new(login_password: 'password') + provider = client.config.login_provider expect(provider).to be_nil end @@ -77,7 +77,7 @@ module Plugins shapes['smithy.ruby.tests#SampleClient']['traits']['smithy.api#httpBasicAuth'] = {} response = client.operation - identity_string = "#{client.config.http_login_username}:#{client.config.http_login_password}" + identity_string = "#{client.config.login_username}:#{client.config.login_password}" expect(response.context.http_request.headers['Authorization']) .to eq("Basic #{Base64.strict_encode64(identity_string)}") end diff --git a/gems/smithy-client/spec/smithy-client/plugins/http_bearer_auth_spec.rb b/gems/smithy-client/spec/smithy-client/plugins/http_bearer_auth_spec.rb index f0a332e16..7bd670e03 100644 --- a/gems/smithy-client/spec/smithy-client/plugins/http_bearer_auth_spec.rb +++ b/gems/smithy-client/spec/smithy-client/plugins/http_bearer_auth_spec.rb @@ -17,38 +17,38 @@ module Plugins let(:client) { client_class.new(stub_responses: true) } - it 'adds an :http_bearer_token option to config' do - expect(client.config).to respond_to(:http_bearer_token) + it 'adds an :bearer_token option to config' do + expect(client.config).to respond_to(:bearer_token) end - it 'adds an :http_bearer_provider option to config' do - expect(client.config).to respond_to(:http_bearer_provider) + it 'adds an :bearer_token_provider option to config' do + expect(client.config).to respond_to(:bearer_token_provider) end - it 'does not default a :http_bearer_token' do + it 'does not default a :bearer_token' do client = client_class.new - expect(client.config.http_bearer_token).to be_nil + expect(client.config.bearer_token).to be_nil end - it 'does not default a :http_bearer_provider' do + it 'does not default a :bearer_token_provider' do client = client_class.new - expect(client.config.http_bearer_provider).to be_nil + expect(client.config.bearer_token_provider).to be_nil end - it 'has a default :http_bearer_token when :stub_responses is true' do - expect(client.config.http_bearer_token).to eq('stubbed-bearer-token') + it 'has a default :bearer_token when :stub_responses is true' do + expect(client.config.bearer_token).to eq('stubbed-bearer-token') end - it 'has a default :http_bearer_provider when :stub_responses is true' do - provider = client.config.http_bearer_provider - expect(provider).to be_a(HttpBearerProvider) + it 'has a default :bearer_token_provider when :stub_responses is true' do + provider = client.config.bearer_token_provider + expect(provider).to be_a(BearerTokenProvider) expect(provider.identity.token).to eq('stubbed-bearer-token') end - it 'defaults a :http_bearer_provider when :http_bearer_token is set' do - client = client_class.new(http_bearer_token: 'bearer') - provider = client.config.http_bearer_provider - expect(provider).to be_a(HttpBearerProvider) + it 'defaults a :bearer_token_provider when :bearer_token is set' do + client = client_class.new(bearer_token: 'bearer') + provider = client.config.bearer_token_provider + expect(provider).to be_a(BearerTokenProvider) expect(provider.identity.token).to eq('bearer') end @@ -58,7 +58,7 @@ module Plugins response = client.operation expect(response.context.http_request.headers['Authorization']) - .to eq("Bearer #{client.config.http_bearer_token}") + .to eq("Bearer #{client.config.bearer_token}") end end end diff --git a/gems/smithy-client/spec/smithy-client/plugins/http_digest_auth_spec.rb b/gems/smithy-client/spec/smithy-client/plugins/http_digest_auth_spec.rb index e1b3c2344..5bf866907 100644 --- a/gems/smithy-client/spec/smithy-client/plugins/http_digest_auth_spec.rb +++ b/gems/smithy-client/spec/smithy-client/plugins/http_digest_auth_spec.rb @@ -17,58 +17,58 @@ module Plugins let(:client) { client_class.new(stub_responses: true) } - it 'adds an :http_login_username option to config' do - expect(client.config).to respond_to(:http_login_username) + it 'adds an :login_username option to config' do + expect(client.config).to respond_to(:login_username) end - it 'adds an :http_login_password option to config' do - expect(client.config).to respond_to(:http_login_password) + it 'adds an :login_password option to config' do + expect(client.config).to respond_to(:login_password) end - it 'adds an :http_login_provider option to config' do - expect(client.config).to respond_to(:http_login_provider) + it 'adds an :login_provider option to config' do + expect(client.config).to respond_to(:login_provider) end - it 'does not default an :http_login_username or :http_login_password' do + it 'does not default an :login_username or :login_password' do client = client_class.new - expect(client.config.http_login_username).to be_nil - expect(client.config.http_login_password).to be_nil + expect(client.config.login_username).to be_nil + expect(client.config.login_password).to be_nil end - it 'does not default a :http_login_provider' do + it 'does not default a :login_provider' do client = client_class.new - expect(client.config.http_login_provider).to be_nil + expect(client.config.login_provider).to be_nil end - it 'has a default :http_login_username and :http_login_password when :stub_responses is true' do - expect(client.config.http_login_username).to eq('stubbed-username') - expect(client.config.http_login_password).to eq('stubbed-password') + it 'has a default :login_username and :login_password when :stub_responses is true' do + expect(client.config.login_username).to eq('stubbed-username') + expect(client.config.login_password).to eq('stubbed-password') end - it 'has a default :http_login_provider when :stub_responses is true' do - provider = client.config.http_login_provider - expect(provider).to be_a(HttpLoginProvider) + it 'has a default :login_provider when :stub_responses is true' do + provider = client.config.login_provider + expect(provider).to be_a(LoginProvider) identity = provider.identity expect(identity.username).to eq('stubbed-username') expect(identity.password).to eq('stubbed-password') end - it 'defaults a :http_login_provider when :http_login_username and :http_login_password are set' do - client = client_class.new(http_login_username: 'username', http_login_password: 'password') - provider = client.config.http_login_provider - expect(provider).to be_a(HttpLoginProvider) + it 'defaults a :login_provider when :login_username and :login_password are set' do + client = client_class.new(login_username: 'username', login_password: 'password') + provider = client.config.login_provider + expect(provider).to be_a(LoginProvider) identity = provider.identity expect(identity.username).to eq('username') expect(identity.password).to eq('password') end - it 'does not default a:http_login_provider when one of the parts is set' do - client = client_class.new(http_login_username: 'username') - provider = client.config.http_login_provider + it 'does not default a:login_provider when one of the parts is set' do + client = client_class.new(login_username: 'username') + provider = client.config.login_provider expect(provider).to be_nil - client = client_class.new(http_login_password: 'password') - provider = client.config.http_login_provider + client = client_class.new(login_password: 'password') + provider = client.config.login_provider expect(provider).to be_nil end 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 3ab0b569b..d6981c752 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 @@ -15,47 +15,48 @@ module Plugins expect(client.config).to respond_to(:auth_resolver) end + it 'adds an :auth_scheme_preference option to config' do + expect(client.config).to respond_to(:auth_scheme_preference) + end + it 'adds scheme ids to auth scheme config hash' do client_class.add_plugin(HttpApiKeyAuth) client_class.add_plugin(HttpBasicAuth) client_class.add_plugin(HttpBearerAuth) client_class.add_plugin(HttpDigestAuth) - expect(client.config.auth_schemes['smithy.api#httpApiKeyAuth']).to be_a(HttpApiKeyProvider) - expect(client.config.auth_schemes['smithy.api#httpBasicAuth']).to be_a(HttpLoginProvider) - expect(client.config.auth_schemes['smithy.api#httpBearerAuth']).to be_a(HttpBearerProvider) - expect(client.config.auth_schemes['smithy.api#httpDigestAuth']).to be_a(HttpLoginProvider) + expect(client.config.auth_schemes['smithy.api#httpApiKeyAuth']).to be_a(ApiKeyProvider) + expect(client.config.auth_schemes['smithy.api#httpBasicAuth']).to be_a(LoginProvider) + expect(client.config.auth_schemes['smithy.api#httpBearerAuth']).to be_a(BearerTokenProvider) + expect(client.config.auth_schemes['smithy.api#httpDigestAuth']).to be_a(LoginProvider) end - it 'resolves auth for anonymous auth' do + it 'supports anonymous auth' do resp = client.operation - expect(resp.context.auth[:scheme_id]).to equal('smithy.api#noAuth') + expect(resp.context.auth[:scheme_id]).to eq('smithy.api#noAuth') end - it 'resolves auth for http api key auth' do + it 'supports http api key auth' do shapes['smithy.ruby.tests#SampleClient']['traits']['smithy.api#httpApiKeyAuth'] = {} client_class.add_plugin(HttpApiKeyAuth) resp = client.operation - expect(resp.context.auth[:scheme_id]).to equal('smithy.api#httpApiKeyAuth') - expect(resp.context.auth[:identity]).to be_a(Identities::ApiKey) + expect(resp.context.auth).to eq({ scheme_id: 'smithy.api#httpApiKeyAuth' }) end - it 'resolves auth for http basic auth' do + it 'supports http basic auth' do shapes['smithy.ruby.tests#SampleClient']['traits']['smithy.api#httpBasicAuth'] = {} client_class.add_plugin(HttpBasicAuth) resp = client.operation - expect(resp.context.auth[:scheme_id]).to equal('smithy.api#httpBasicAuth') - expect(resp.context.auth[:identity]).to be_a(Identities::HttpLogin) + expect(resp.context.auth).to eq({ scheme_id: 'smithy.api#httpBasicAuth' }) end - it 'resolves auth for http bearer auth' do + it 'supports http bearer auth' do shapes['smithy.ruby.tests#SampleClient']['traits']['smithy.api#httpBearerAuth'] = {} client_class.add_plugin(HttpBearerAuth) resp = client.operation - expect(resp.context.auth[:scheme_id]).to equal('smithy.api#httpBearerAuth') - expect(resp.context.auth[:identity]).to be_a(Identities::Token) + expect(resp.context.auth).to eq({ scheme_id: 'smithy.api#httpBearerAuth' }) end - it 'resolves auth for http digest auth' do + it 'supports http digest auth' do shapes['smithy.ruby.tests#SampleClient']['traits']['smithy.api#httpDigestAuth'] = {} client_class.add_plugin(HttpDigestAuth) expect { client.operation }.to raise_error(NotImplementedError) @@ -63,14 +64,37 @@ module Plugins it 'resolves the first supported auth scheme' do shapes['smithy.ruby.tests#SampleClient']['traits']['smithy.api#auth'] = - %w[smithy.api#httpBasicAuth smithy.api#httpApiKeyAuth] + %w[smithy.api#httpBearerAuth smithy.api#httpApiKeyAuth] shapes['smithy.ruby.tests#SampleClient']['traits']['smithy.api#httpApiKeyAuth'] = {} - shapes['smithy.ruby.tests#SampleClient']['traits']['smithy.api#httpBasicAuth'] = {} + shapes['smithy.ruby.tests#SampleClient']['traits']['smithy.api#httpBearerAuth'] = {} client_class.add_plugin(HttpApiKeyAuth) - client_class.add_plugin(HttpBasicAuth) + client_class.add_plugin(HttpBearerAuth) + resp = client.operation + expect(resp.context.auth).to eq({ scheme_id: 'smithy.api#httpBearerAuth' }) + end + + it 'resolves the first supported auth scheme with an identity provider configured' do + shapes['smithy.ruby.tests#SampleClient']['traits']['smithy.api#auth'] = + %w[smithy.api#httpBearerAuth smithy.api#httpApiKeyAuth] + shapes['smithy.ruby.tests#SampleClient']['traits']['smithy.api#httpApiKeyAuth'] = {} + shapes['smithy.ruby.tests#SampleClient']['traits']['smithy.api#httpBearerAuth'] = {} + client_class.add_plugin(HttpApiKeyAuth) + client_class.add_plugin(HttpBearerAuth) + client = client_class.new(stub_responses: true, bearer_token_provider: nil) + resp = client.operation + expect(resp.context.auth).to eq({ scheme_id: 'smithy.api#httpApiKeyAuth' }) + end + + it 'resolves the first supported auth scheme with a resolved identity' do + shapes['smithy.ruby.tests#SampleClient']['traits']['smithy.api#auth'] = + %w[smithy.api#httpBearerAuth smithy.api#httpApiKeyAuth] + shapes['smithy.ruby.tests#SampleClient']['traits']['smithy.api#httpApiKeyAuth'] = {} + shapes['smithy.ruby.tests#SampleClient']['traits']['smithy.api#httpBearerAuth'] = {} + client_class.add_plugin(HttpApiKeyAuth) + client_class.add_plugin(HttpBearerAuth) + client = client_class.new(stub_responses: true, bearer_token_provider: BearerTokenProvider.new) resp = client.operation - expect(resp.context.auth[:scheme_id]).to equal('smithy.api#httpBasicAuth') - expect(resp.context.auth[:identity]).to be_a(Identities::HttpLogin) + expect(resp.context.auth).to eq({ scheme_id: 'smithy.api#httpApiKeyAuth' }) end it 'raises an error when no auth options were resolved' do @@ -87,24 +111,160 @@ def resolve(_) it 'raises an error when auth scheme is not enabled' do shapes['smithy.ruby.tests#SampleClient']['traits']['smithy.api#httpApiKeyAuth'] = {} client_class.remove_plugin(HttpApiKeyAuth) - expect { client.operation }.to raise_error(/was not enabled/) + expect { client.operation }.to raise_error(/was not enabled for this request/) end - it 'raises an error when identity resolver is not configured' do + it 'raises an error when identity provider is not configured' do shapes['smithy.ruby.tests#SampleClient']['traits']['smithy.api#httpApiKeyAuth'] = {} client_class.add_plugin(HttpApiKeyAuth) - client = client_class.new(stub_responses: true, http_api_key_provider: nil) - expect { client.operation }.to raise_error(/did not have an identity resolver configured/) + client = client_class.new(stub_responses: true, api_key_provider: nil) + expect { client.operation }.to raise_error(/did not have an identity provider configured/) end - it 'raises an error when identity resolver fails to resolve identity' do + it 'raises an error when identity provider fails to resolve identity' do shapes['smithy.ruby.tests#SampleClient']['traits']['smithy.api#httpApiKeyAuth'] = {} client_class.add_plugin(HttpApiKeyAuth) - provider = HttpApiKeyProvider.new('stubbed_key') - expect(provider).to receive(:identity).and_return(nil) - client = client_class.new(stub_responses: true, http_api_key_provider: provider) + client = client_class.new(stub_responses: true, api_key_provider: ApiKeyProvider.new) expect { client.operation }.to raise_error(/failed to resolve identity/) end + + context 'with auth scheme preference' do + before do + shapes['smithy.ruby.tests#SampleClient']['traits']['smithy.api#auth'] = + %w[smithy.api#httpApiKeyAuth smithy.api#httpBasicAuth] + shapes['smithy.ruby.tests#SampleClient']['traits']['smithy.api#httpApiKeyAuth'] = {} + shapes['smithy.ruby.tests#SampleClient']['traits']['smithy.api#httpBasicAuth'] = {} + client_class.add_plugin(HttpApiKeyAuth) + client_class.add_plugin(HttpBasicAuth) + end + + it 'uses the preferred auth scheme when multiple schemes are supported' do + client = client_class.new( + stub_responses: true, + auth_scheme_preference: ['smithy.api#httpBasicAuth'] + ) + resp = client.operation + expect(resp.context.auth).to eq({ scheme_id: 'smithy.api#httpBasicAuth' }) + end + + it 'ignores unsupported preferred auth schemes' do + client = client_class.new( + stub_responses: true, + auth_scheme_preference: ['smithy.api#httpDigestAuth', 'smithy.api#httpBasicAuth'] + ) + resp = client.operation + expect(resp.context.auth).to eq({ scheme_id: 'smithy.api#httpBasicAuth' }) + end + + it 'falls back to modeled order when no preferred auth schemes are supported' do + client = client_class.new( + stub_responses: true, + auth_scheme_preference: ['smithy.api#httpDigestAuth'] + ) + resp = client.operation + expect(resp.context.auth).to eq({ scheme_id: 'smithy.api#httpApiKeyAuth' }) + end + end + + context 'endpoint auth schemes' do + def endpoint_rules(auth_schemes = []) + { + 'version' => '1.0', + 'parameters' => { + 'endpoint' => { + 'type' => 'string', + 'builtIn' => 'SDK::Endpoint', + 'documentation' => 'Endpoint used for making requests. Should be formatted as a URI.' + } + }, + 'rules' => [ + { + 'conditions' => [{ 'fn' => 'isSet', 'argv' => [{ 'ref' => 'endpoint' }] }], + 'endpoint' => { + 'url' => { 'ref' => 'endpoint' }, + 'properties' => { + 'authSchemes' => auth_schemes + } + }, + 'type' => 'endpoint' + }, + { + 'conditions' => [], + 'error' => 'Endpoint is not set - you must configure an endpoint.', + 'type' => 'error' + } + ] + } + end + + it 'uses endpoint auth schemes instead of modeled auth' do + shapes['smithy.ruby.tests#SampleClient']['traits']['smithy.rules#endpointRuleSet'] = + endpoint_rules([{ 'name' => 'bearer' }]) + shapes['smithy.ruby.tests#SampleClient']['traits']['smithy.api#auth'] = %w[smithy.api#httpApiKeyAuth] + shapes['smithy.ruby.tests#SampleClient']['traits']['smithy.api#httpApiKeyAuth'] = {} + client_class.add_plugin(HttpApiKeyAuth) + client_class.add_plugin(HttpBearerAuth) # to register the endpoint auth scheme + client = client_class.new(stub_responses: true) + resp = client.operation + expect(resp.context.auth).to eq({ scheme_id: 'smithy.api#httpBearerAuth' }) + end + + it 'selects the first supported endpoint auth scheme' do + shapes['smithy.ruby.tests#SampleClient']['traits']['smithy.rules#endpointRuleSet'] = + endpoint_rules([{ 'name' => 'other' }, { 'name' => 'bearer' }]) + client_class.add_plugin(HttpBearerAuth) # to register the endpoint auth scheme + client = client_class.new(stub_responses: true) + resp = client.operation + expect(resp.context.auth).to eq({ scheme_id: 'smithy.api#httpBearerAuth' }) + end + + it 'forwards additional auth scheme properties from the endpoint' do + shapes['smithy.ruby.tests#SampleClient']['traits']['smithy.rules#endpointRuleSet'] = + endpoint_rules([{ 'name' => 'bearer', 'foo' => 'bar' }]) + client_class.add_plugin(HttpBearerAuth) # to register the endpoint auth scheme + client = client_class.new(stub_responses: true) + resp = client.operation + expect(resp.context.auth).to eq({ scheme_id: 'smithy.api#httpBearerAuth', 'foo' => 'bar' }) + end + + context 'with auth scheme preference' do + it 'uses the preference list to prioritize endpoint auth schemes' do + shapes['smithy.ruby.tests#SampleClient']['traits']['smithy.rules#endpointRuleSet'] = + endpoint_rules([{ 'name' => 'other' }, { 'name' => 'bearer' }]) + client_class.add_plugin(HttpBearerAuth) # to register the endpoint auth scheme + client = client_class.new( + stub_responses: true, + auth_scheme_preference: ['smithy.api#noAuth', 'smithy.api#httpBearerAuth'] + ) + resp = client.operation + expect(resp.context.auth).to eq({ scheme_id: 'smithy.api#httpBearerAuth' }) + end + + it 'ignores unsupported preferred auth schemes' do + shapes['smithy.ruby.tests#SampleClient']['traits']['smithy.rules#endpointRuleSet'] = + endpoint_rules([{ 'name' => 'other' }, { 'name' => 'bearer' }]) + client_class.add_plugin(HttpBearerAuth) # to register the endpoint auth scheme + client = client_class.new( + stub_responses: true, + auth_scheme_preference: ['smithy.api#httpDigestAuth', 'smithy.api#httpBearerAuth'] + ) + resp = client.operation + expect(resp.context.auth).to eq({ scheme_id: 'smithy.api#httpBearerAuth' }) + end + + it 'falls back to endpoint auth scheme order when no preferred auth schemes are supported' do + shapes['smithy.ruby.tests#SampleClient']['traits']['smithy.rules#endpointRuleSet'] = + endpoint_rules([{ 'name' => 'bearer' }]) + client_class.add_plugin(HttpBearerAuth) # to register the endpoint auth scheme + client = client_class.new( + stub_responses: true, + auth_scheme_preference: ['smithy.api#httpDigestAuth'] + ) + resp = client.operation + expect(resp.context.auth).to eq({ scheme_id: 'smithy.api#httpBearerAuth' }) + end + end + end end end end diff --git a/gems/smithy-client/spec/smithy-client/plugins/stub_responses_spec.rb b/gems/smithy-client/spec/smithy-client/plugins/stub_responses_spec.rb index 1726cd8ad..a40d9cad3 100644 --- a/gems/smithy-client/spec/smithy-client/plugins/stub_responses_spec.rb +++ b/gems/smithy-client/spec/smithy-client/plugins/stub_responses_spec.rb @@ -32,25 +32,20 @@ module Plugins expect(client.handlers).to include(StubResponses::APIRequestsHandler) end - it 'defaults the endpoint provider if :stub_responses is true' do - expect(client.config.endpoint_provider).to be_a(Stubbing::EndpointProvider) + it 'defaults the endpoint if :stub_responses is true' do + expect(client.config.endpoint).to eq('http://stubbed-endpoint') end - it 'allows for passed in endpoint providers' do + it 'does not default the endpoint if a custom endpoint is set' do + client = client_class.new(stub_responses: true, endpoint: 'https://example.com') + expect(client.config.endpoint).to eq('https://example.com') + end + + it 'does not default the endpoint if a custom endpoint provider is set' do endpoint_provider = double('endpoint-provider') client = client_class.new(stub_responses: true, endpoint_provider: endpoint_provider) expect(client.config.endpoint_provider).to be(endpoint_provider) - end - - it 'defaults the endpoint using the stubbing endpoint provider' do - response = client.operation - expect(response.context.http_request.endpoint.host).to eq('stubbed-endpoint') - end - - it 'allows for passed in endpoints using the stubbing endpoint provider' do - client = client_class.new(stub_responses: true, endpoint: 'https://example.com') - response = client.operation - expect(response.context.http_request.endpoint.host).to eq('example.com') + expect(client.config.endpoint).to be_nil end it 'signals error for exceptions' do diff --git a/gems/smithy-client/spec/smithy-client/refreshing_identity_provider_spec.rb b/gems/smithy-client/spec/smithy-client/refreshing_identity_provider_spec.rb index eb16a8925..359b323de 100644 --- a/gems/smithy-client/spec/smithy-client/refreshing_identity_provider_spec.rb +++ b/gems/smithy-client/spec/smithy-client/refreshing_identity_provider_spec.rb @@ -5,80 +5,67 @@ module Smithy module Client describe RefreshingIdentityProvider do - let(:identity_resolver) do + let(:identity_provider) do Class.new do + include IdentityProvider include RefreshingIdentityProvider - def initialize(proc) - @proc = proc + def initialize(options = {}) + @proc = options[:proc] @async_refresh = true - super() + super end + attr_accessor :expiration + + private + def refresh @identity = @proc.call + @expiration = Time.now + 3600 end end end - let(:refreshed_expiration) { Time.now + 3600 } - let(:refreshed_expiration_identity) do - Identity.new(expiration: refreshed_expiration) + let(:time) { Time.now } + before do + allow(Time).to receive(:now).and_return(time) end + let(:near_sync_expiration) { time + 200 } + let(:near_async_expiration) { time + 500 } + let(:refreshed_expiration) { time + 3600 } - let(:properties) { { foo: 'bar' } } - let(:proc) { -> {} } + let(:identity) { double('identity') } + let(:proc) { -> { identity } } - subject { identity_resolver.new(proc) } - - describe '#identity' do - it 'initializes the identity' do - expect(proc).to receive(:call).and_return(refreshed_expiration_identity) - expect(subject).to receive(:refresh).and_call_original + subject { identity_provider.new(proc: proc) } - expect(subject.instance_variable_get(:@identity)).to be_nil - identity = subject.identity - expect(identity).to eq(refreshed_expiration_identity) - expect(subject.instance_variable_get(:@identity)).to eq(identity) + describe '#initialize' do + it 'calls refresh' do + expect_any_instance_of(identity_provider).to receive(:refresh).and_call_original + expect(subject.identity).to eq(identity) + expect(subject.expiration).to eq(refreshed_expiration) end + end - context 'near sync expiration' do - let(:near_sync_expiration) { Time.now + 200 } - let(:near_sync_expiration_identity) do - Identity.new(expiration: near_sync_expiration) - end - - it 'refreshes synchronously' do - expect(Thread).not_to receive(:new) - expect(proc).to receive(:call) - .and_return(near_sync_expiration_identity) - expect(proc).to receive(:call) - .and_return(refreshed_expiration_identity) - - identity = subject.identity # initialize - expect(identity).to eq(near_sync_expiration_identity) - identity = subject.identity # refreshing - expect(identity).to eq(refreshed_expiration_identity) - end + describe '#identity' do + it 'refreshes synchronously' do + subject.expiration = near_sync_expiration + expect(subject).to receive(:refresh).and_call_original + subject.identity # force refresh + expect(subject.expiration).to eq(refreshed_expiration) + expect(subject).not_to receive(:refresh) + subject.identity # no refresh end - context 'near async expiration' do - let(:near_async_expiration) { Time.now + 500 } - let(:near_async_expiration_identity) do - Identity.new(expiration: near_async_expiration) - end - - it 'refreshes asynchronously' do - expect(Thread).to receive(:new).and_yield - expect(proc).to receive(:call) - .and_return(near_async_expiration_identity) - expect(proc).to receive(:call) - .and_return(refreshed_expiration_identity) - identity = subject.identity # initialize - expect(identity).to eq(near_async_expiration_identity) - identity = subject.identity # refreshing - expect(identity).to eq(refreshed_expiration_identity) - end + it 'refreshes asynchronously' do + expect(Thread).to receive(:new).and_yield + expect(subject).to receive(:refresh).and_call_original + subject.expiration = near_async_expiration + subject.identity # force refresh + expect(subject.expiration).to eq(refreshed_expiration) + expect(subject).not_to receive(:refresh) + subject.identity # no refresh end 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 4d1545d85..2c6492e33 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 [String] + # @return [Hash] def resolve(parameters) <% auth_rules_code.each do |line| -%> <%= line %> diff --git a/gems/smithy/lib/smithy/templates/client/endpoint_plugin.erb b/gems/smithy/lib/smithy/templates/client/endpoint_plugin.erb index 6723a00e7..16b5cd94e 100644 --- a/gems/smithy/lib/smithy/templates/client/endpoint_plugin.erb +++ b/gems/smithy/lib/smithy/templates/client/endpoint_plugin.erb @@ -8,10 +8,11 @@ module <%= module_name %> class Endpoint < Smithy::Client::Plugin option( :endpoint_provider, - doc_type: '<%= module_name %>::EndpointProvider', - docstring: <<~DOCS) do |_config| - The endpoint provider used to resolve endpoints. Any object that responds to `#resolve(parameters)`. - DOCS + doc_type: '#resolve(parameters)', + doc_default: '<%= module_name %>::EndpointProvider', + rbs_type: '<%= module_name %>::EndpointProvider', + docstring: 'An object that provides an endpoint to use for the request.' + ) do |_config| EndpointProvider.new end @@ -29,24 +30,27 @@ module <%= module_name %> ) <% end -%> + option(:endpoint_auth_schemes) do + <%= endpoint_auth_scheme_bindings %> + end + # @api private class Handler < Smithy::Client::Handler def call(context) params = EndpointParameters.create(context) + context[:endpoint_params] = params endpoint = context.config.endpoint_provider.resolve(params) + context[:resolved_endpoint] = endpoint - context.http_request.endpoint = endpoint.uri - apply_endpoint_headers(context, endpoint.headers) - - context[:endpoint_params] = params - context[:endpoint_properties] = endpoint.properties + apply_endpoint(context, endpoint) @handler.call(context) end private - def apply_endpoint_headers(context, headers) - headers.each do |key, value| + def apply_endpoint(context, endpoint) + context.http_request.endpoint = endpoint.uri + endpoint.headers.each do |key, value| context.http_request.headers[key] = value 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 725b71d32..bad2a78a0 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 << '#{auth_scheme}'" + lines << "options << { 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 << '#{auth_scheme}'" + lines << " options << { scheme_id: '#{auth_scheme}' }" end end diff --git a/gems/smithy/lib/smithy/views/client/endpoint_plugin.rb b/gems/smithy/lib/smithy/views/client/endpoint_plugin.rb index 6dcfead44..110e6eb3c 100644 --- a/gems/smithy/lib/smithy/views/client/endpoint_plugin.rb +++ b/gems/smithy/lib/smithy/views/client/endpoint_plugin.rb @@ -8,11 +8,8 @@ class EndpointPlugin < View def initialize(plan) @plan = plan @model = plan.model - service = @plan.service.values.first - @endpoint_rules = service['traits']['smithy.rules#endpointRuleSet'] - @parameters = @endpoint_rules['parameters'] - .map { |id, data| EndpointParameter.new(id, data, @plan) } - + _, service = @plan.service.first + @parameters = build_parameters(service['traits']['smithy.rules#endpointRuleSet']) super() end @@ -21,6 +18,16 @@ def initialize(plan) def module_name @plan.module_name end + + def endpoint_auth_scheme_bindings + @plan.welds.map(&:endpoint_auth_scheme_bindings).reduce({}, :merge) + end + + private + + def build_parameters(endpoint_rules) + endpoint_rules['parameters'].map { |id, data| EndpointParameter.new(id, data, @plan) } + end end end end diff --git a/gems/smithy/lib/smithy/weld.rb b/gems/smithy/lib/smithy/weld.rb index 055a48537..1862d4e7e 100644 --- a/gems/smithy/lib/smithy/weld.rb +++ b/gems/smithy/lib/smithy/weld.rb @@ -65,6 +65,15 @@ def endpoint_function_bindings {} end + # Called when constructing the endpoints plugin. Any bindings defined here will + # be merged with other endpoint auth scheme bindings. The key is the name of the + # binding for the endpoints auth, and the value is the absolute shape id of the + # auth scheme trait. + # @return [Hash] endpoint auth scheme bindings for use in endpoint rules + def endpoint_auth_scheme_bindings + {} + end + # Called when constructing the client. Any plugins defined here will be merged # with other plugins. The key is the fully qualified class name of the plugin, # and the value is a hash with any of the following keys: diff --git a/gems/smithy/lib/smithy/welds/transforms/default_endpoint_rules.json b/gems/smithy/lib/smithy/welds/transforms/default_endpoint_rules.json index bdea6ec1c..dd1e0cd08 100644 --- a/gems/smithy/lib/smithy/welds/transforms/default_endpoint_rules.json +++ b/gems/smithy/lib/smithy/welds/transforms/default_endpoint_rules.json @@ -4,7 +4,7 @@ "endpoint": { "type": "string", "builtIn": "SDK::Endpoint", - "documentation": "Endpoint used for making requests. Should be formatted as a URI." + "documentation": "Endpoint used for making requests. Should be formatted as a URI." } }, "rules": [ diff --git a/gems/smithy/lib/smithy/welds/transforms/endpoints.rb b/gems/smithy/lib/smithy/welds/transforms/endpoints.rb index 73a49f507..fc4eaf61e 100644 --- a/gems/smithy/lib/smithy/welds/transforms/endpoints.rb +++ b/gems/smithy/lib/smithy/welds/transforms/endpoints.rb @@ -48,6 +48,13 @@ def endpoint_function_bindings } end + def endpoint_auth_scheme_bindings + { + 'bearer' => 'smithy.api#httpBearerAuth', + 'none' => 'smithy.api#noAuth' + } + end + private def add_default_endpoints(service) @@ -57,11 +64,11 @@ def add_default_endpoints(service) end def default_endpoint_rules - ::JSON.load_file(File.join(__dir__.to_s, 'default_endpoint_rules.json')) + JSON.load_file(File.join(__dir__.to_s, 'default_endpoint_rules.json')) end def default_endpoint_tests - ::JSON.load_file(File.join(__dir__.to_s, 'default_endpoint_tests.json')) + JSON.load_file(File.join(__dir__.to_s, 'default_endpoint_tests.json')) end 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 16c442bb3..0c4177751 100644 --- a/gems/smithy/spec/interfaces/client/auth_resolver_spec.rb +++ b/gems/smithy/spec/interfaces/client/auth_resolver_spec.rb @@ -15,25 +15,29 @@ it 'returns the auth options alphabetically by default' do params = NoAuthTrait::AuthParameters.new(operation_name: :operation_a) auth_options = subject.resolve(params) - expected = %w[smithy.api#httpBasicAuth smithy.api#httpBearerAuth smithy.api#httpDigestAuth] + expected = [ + { scheme_id: 'smithy.api#httpBasicAuth' }, + { scheme_id: 'smithy.api#httpBearerAuth' }, + { scheme_id: 'smithy.api#httpDigestAuth' } + ] expect(auth_options).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(['smithy.api#httpDigestAuth']) + expect(auth_options).to eq([{ scheme_id: 'smithy.api#httpDigestAuth' }]) 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( - %w[ - smithy.api#httpBasicAuth - smithy.api#httpBearerAuth - smithy.api#httpDigestAuth - smithy.api#noAuth + [ + { scheme_id: 'smithy.api#httpBasicAuth' }, + { scheme_id: 'smithy.api#httpBearerAuth' }, + { scheme_id: 'smithy.api#httpDigestAuth' }, + { scheme_id: 'smithy.api#noAuth' } ] ) end @@ -49,26 +53,32 @@ 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 = %w[smithy.api#httpBasicAuth smithy.api#httpDigestAuth] + expected = [{ scheme_id: 'smithy.api#httpBasicAuth' }, { scheme_id: 'smithy.api#httpDigestAuth' }] expect(auth_options).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(['smithy.api#httpBearerAuth']) + expect(auth_options).to eq([{ scheme_id: 'smithy.api#httpBearerAuth' }]) 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(['smithy.api#noAuth']) + expect(auth_options).to eq([{ scheme_id: 'smithy.api#noAuth' }]) 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(%w[smithy.api#httpBasicAuth smithy.api#httpDigestAuth smithy.api#noAuth]) + expect(auth_options).to eq( + [ + { scheme_id: 'smithy.api#httpBasicAuth' }, + { scheme_id: 'smithy.api#httpDigestAuth' }, + { scheme_id: 'smithy.api#noAuth' } + ] + ) end end end @@ -82,13 +92,13 @@ 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(['smithy.api#noAuth']) + expect(auth_options).to eq([{ scheme_id: 'smithy.api#noAuth' }]) 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(['smithy.api#noAuth']) + expect(auth_options).to eq([{ scheme_id: 'smithy.api#noAuth' }]) 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 740d6b64f..46a343832 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('smithy.api#httpBasicAuth') + expect(resolved_auths).to include({ scheme_id: '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('smithy.api#httpBearerAuth') + expect(resolved_auths).to_not include({ scheme_id: 'smithy.api#httpBearerAuth' }) end end end diff --git a/projections/shapes/lib/shapes/auth_resolver.rb b/projections/shapes/lib/shapes/auth_resolver.rb index bad636518..c3f060b0e 100644 --- a/projections/shapes/lib/shapes/auth_resolver.rb +++ b/projections/shapes/lib/shapes/auth_resolver.rb @@ -6,10 +6,10 @@ module ShapeService # Resolves the auth scheme from {AuthParameters}. class AuthResolver # @param [AuthParameters] parameters - # @return [String] + # @return [Hash] def resolve(parameters) options = [] - options << 'smithy.api#noAuth' + options << { scheme_id: 'smithy.api#noAuth' } options end end diff --git a/projections/shapes/lib/shapes/client.rb b/projections/shapes/lib/shapes/client.rb index 6d4a334c0..108dcf22f 100644 --- a/projections/shapes/lib/shapes/client.rb +++ b/projections/shapes/lib/shapes/client.rb @@ -54,7 +54,9 @@ class Client < Smithy::Client::Base # the request. When false, the request will raise a `CapacityNotAvailableError` and will # not retry instead of sleeping. # @option options [#resolve(context)] :auth_resolver (AuthResolver.new) - # An object that resolves authentication schemes for request signing + # An object that resolves authentication schemes for request signing. + # @option options [Array] :auth_scheme_preference + # A list of preferred authentication schemes to use when making a request. # @option options [Boolean] :convert_params (true) # When `true`, request parameters are coerced into the required types. # @option options [Boolean] :disable_host_prefix_injection @@ -63,8 +65,8 @@ class Client < Smithy::Client::Base # When `true`, the request body will not be compressed for supported operations. # @option options [String] :endpoint # Custom Endpoint - # @option options [ShapeService::EndpointProvider] :endpoint_provider - # The endpoint provider used to resolve endpoints. Any object that responds to `#resolve(parameters)`. + # @option options [#resolve(parameters)] :endpoint_provider (ShapeService::EndpointProvider) + # An object that provides an endpoint to use for the request. # @option options [String] :http_ca_file # The path to a CA certification file in PEM format. Defaults to `nil` which uses # the Net::HTTP default value. diff --git a/projections/shapes/lib/shapes/endpoint_parameters.rb b/projections/shapes/lib/shapes/endpoint_parameters.rb index e5cc0b6c2..a55ba8dc0 100644 --- a/projections/shapes/lib/shapes/endpoint_parameters.rb +++ b/projections/shapes/lib/shapes/endpoint_parameters.rb @@ -5,7 +5,7 @@ module ShapeService # Endpoint parameters used to resolve endpoints per request. # @!attribute endpoint - # Endpoint used for making requests. Should be formatted as a URI. + # Endpoint used for making requests. Should be formatted as a URI. # # @return [String] # diff --git a/projections/shapes/lib/shapes/plugins/endpoint.rb b/projections/shapes/lib/shapes/plugins/endpoint.rb index aea388f0a..1792eb6c2 100644 --- a/projections/shapes/lib/shapes/plugins/endpoint.rb +++ b/projections/shapes/lib/shapes/plugins/endpoint.rb @@ -8,10 +8,11 @@ module Plugins class Endpoint < Smithy::Client::Plugin option( :endpoint_provider, - doc_type: 'ShapeService::EndpointProvider', - docstring: <<~DOCS) do |_config| - The endpoint provider used to resolve endpoints. Any object that responds to `#resolve(parameters)`. - DOCS + doc_type: '#resolve(parameters)', + doc_default: 'ShapeService::EndpointProvider', + rbs_type: 'ShapeService::EndpointProvider', + docstring: 'An object that provides an endpoint to use for the request.' + ) do |_config| EndpointProvider.new end @@ -21,24 +22,27 @@ class Endpoint < Smithy::Client::Plugin docstring: 'Custom Endpoint' ) + option(:endpoint_auth_schemes) do + {"bearer" => "smithy.api#httpBearerAuth", "none" => "smithy.api#noAuth"} + end + # @api private class Handler < Smithy::Client::Handler def call(context) params = EndpointParameters.create(context) + context[:endpoint_params] = params endpoint = context.config.endpoint_provider.resolve(params) + context[:resolved_endpoint] = endpoint - context.http_request.endpoint = endpoint.uri - apply_endpoint_headers(context, endpoint.headers) - - context[:endpoint_params] = params - context[:endpoint_properties] = endpoint.properties + apply_endpoint(context, endpoint) @handler.call(context) end private - def apply_endpoint_headers(context, headers) - headers.each do |key, value| + def apply_endpoint(context, endpoint) + context.http_request.endpoint = endpoint.uri + endpoint.headers.each do |key, value| context.http_request.headers[key] = value end end diff --git a/projections/shapes/shapes.gemspec b/projections/shapes/shapes.gemspec index 6cbf3c16a..96f0dd8ff 100644 --- a/projections/shapes/shapes.gemspec +++ b/projections/shapes/shapes.gemspec @@ -10,7 +10,7 @@ Gem::Specification.new do |spec| spec.files = Dir['VERSION', 'CHANGELOG.md', 'lib/**/*.rb', base: __dir__] spec.license = 'Apache-2.0' - spec.add_dependency('smithy-client', '~> 1') + spec.add_dependency('smithy-client', '1.0.0.pre1') spec.required_ruby_version = '>= 3.3' end diff --git a/projections/shapes/sig/shapes/client.rbs b/projections/shapes/sig/shapes/client.rbs index fd1846d16..6bd364e69 100644 --- a/projections/shapes/sig/shapes/client.rbs +++ b/projections/shapes/sig/shapes/client.rbs @@ -5,6 +5,7 @@ module ShapeService def self.new: ( ?adaptive_retry_wait_to_fill: bool, ?auth_resolver: AuthResolver, + ?auth_scheme_preference: Array[String], ?convert_params: bool, ?disable_host_prefix_injection: bool, ?disable_request_compression: bool, diff --git a/projections/weather/lib/weather/auth_resolver.rb b/projections/weather/lib/weather/auth_resolver.rb index a7eed4025..75121567a 100644 --- a/projections/weather/lib/weather/auth_resolver.rb +++ b/projections/weather/lib/weather/auth_resolver.rb @@ -6,10 +6,10 @@ module Weather # Resolves the auth scheme from {AuthParameters}. class AuthResolver # @param [AuthParameters] parameters - # @return [String] + # @return [Hash] def resolve(parameters) options = [] - options << 'smithy.api#noAuth' + options << { scheme_id: 'smithy.api#noAuth' } options end end diff --git a/projections/weather/lib/weather/client.rb b/projections/weather/lib/weather/client.rb index 5f6531af5..0a1413c15 100644 --- a/projections/weather/lib/weather/client.rb +++ b/projections/weather/lib/weather/client.rb @@ -54,7 +54,9 @@ class Client < Smithy::Client::Base # the request. When false, the request will raise a `CapacityNotAvailableError` and will # not retry instead of sleeping. # @option options [#resolve(context)] :auth_resolver (AuthResolver.new) - # An object that resolves authentication schemes for request signing + # An object that resolves authentication schemes for request signing. + # @option options [Array] :auth_scheme_preference + # A list of preferred authentication schemes to use when making a request. # @option options [Boolean] :convert_params (true) # When `true`, request parameters are coerced into the required types. # @option options [Boolean] :disable_host_prefix_injection @@ -63,8 +65,8 @@ class Client < Smithy::Client::Base # When `true`, the request body will not be compressed for supported operations. # @option options [String] :endpoint # Custom Endpoint - # @option options [Weather::EndpointProvider] :endpoint_provider - # The endpoint provider used to resolve endpoints. Any object that responds to `#resolve(parameters)`. + # @option options [#resolve(parameters)] :endpoint_provider (Weather::EndpointProvider) + # An object that provides an endpoint to use for the request. # @option options [String] :http_ca_file # The path to a CA certification file in PEM format. Defaults to `nil` which uses # the Net::HTTP default value. diff --git a/projections/weather/lib/weather/endpoint_parameters.rb b/projections/weather/lib/weather/endpoint_parameters.rb index 654b0cacc..e3d9bb0ba 100644 --- a/projections/weather/lib/weather/endpoint_parameters.rb +++ b/projections/weather/lib/weather/endpoint_parameters.rb @@ -5,7 +5,7 @@ module Weather # Endpoint parameters used to resolve endpoints per request. # @!attribute endpoint - # Endpoint used for making requests. Should be formatted as a URI. + # Endpoint used for making requests. Should be formatted as a URI. # # @return [String] # diff --git a/projections/weather/lib/weather/plugins/endpoint.rb b/projections/weather/lib/weather/plugins/endpoint.rb index 0ac00b57d..ba1e0fe5f 100644 --- a/projections/weather/lib/weather/plugins/endpoint.rb +++ b/projections/weather/lib/weather/plugins/endpoint.rb @@ -8,10 +8,11 @@ module Plugins class Endpoint < Smithy::Client::Plugin option( :endpoint_provider, - doc_type: 'Weather::EndpointProvider', - docstring: <<~DOCS) do |_config| - The endpoint provider used to resolve endpoints. Any object that responds to `#resolve(parameters)`. - DOCS + doc_type: '#resolve(parameters)', + doc_default: 'Weather::EndpointProvider', + rbs_type: 'Weather::EndpointProvider', + docstring: 'An object that provides an endpoint to use for the request.' + ) do |_config| EndpointProvider.new end @@ -21,24 +22,27 @@ class Endpoint < Smithy::Client::Plugin docstring: 'Custom Endpoint' ) + option(:endpoint_auth_schemes) do + {"bearer" => "smithy.api#httpBearerAuth", "none" => "smithy.api#noAuth"} + end + # @api private class Handler < Smithy::Client::Handler def call(context) params = EndpointParameters.create(context) + context[:endpoint_params] = params endpoint = context.config.endpoint_provider.resolve(params) + context[:resolved_endpoint] = endpoint - context.http_request.endpoint = endpoint.uri - apply_endpoint_headers(context, endpoint.headers) - - context[:endpoint_params] = params - context[:endpoint_properties] = endpoint.properties + apply_endpoint(context, endpoint) @handler.call(context) end private - def apply_endpoint_headers(context, headers) - headers.each do |key, value| + def apply_endpoint(context, endpoint) + context.http_request.endpoint = endpoint.uri + endpoint.headers.each do |key, value| context.http_request.headers[key] = value end end diff --git a/projections/weather/sig/weather/client.rbs b/projections/weather/sig/weather/client.rbs index 7eedfa32e..003fe6ae7 100644 --- a/projections/weather/sig/weather/client.rbs +++ b/projections/weather/sig/weather/client.rbs @@ -5,6 +5,7 @@ module Weather def self.new: ( ?adaptive_retry_wait_to_fill: bool, ?auth_resolver: AuthResolver, + ?auth_scheme_preference: Array[String], ?convert_params: bool, ?disable_host_prefix_injection: bool, ?disable_request_compression: bool, diff --git a/projections/weather/weather.gemspec b/projections/weather/weather.gemspec index d0c68bd38..33380198d 100644 --- a/projections/weather/weather.gemspec +++ b/projections/weather/weather.gemspec @@ -10,7 +10,7 @@ Gem::Specification.new do |spec| spec.files = Dir['VERSION', 'CHANGELOG.md', 'lib/**/*.rb', base: __dir__] spec.license = 'Apache-2.0' - spec.add_dependency('smithy-client', '~> 1') + spec.add_dependency('smithy-client', '1.0.0.pre1') spec.required_ruby_version = '>= 3.3' end From 49690096edc379ef7c9d28c0cb0936978de3c4bd Mon Sep 17 00:00:00 2001 From: Matt Muller Date: Mon, 29 Sep 2025 18:54:35 -0400 Subject: [PATCH 2/7] Working auth scheme approach --- gems/smithy-client/lib/smithy-client.rb | 2 + .../lib/smithy-client/api_key_signer.rb | 60 +++++++++++++++ gems/smithy-client/lib/smithy-client/auth.rb | 33 +++++---- .../lib/smithy-client/auth_scheme.rb | 23 ++++++ .../lib/smithy-client/bearer_token_signer.rb | 18 +++++ .../lib/smithy-client/login_signer.rb | 31 ++++++++ .../lib/smithy-client/null_signer.rb | 16 ++++ .../plugins/http_api_key_auth.rb | 65 +++------------- .../smithy-client/plugins/http_basic_auth.rb | 30 +++----- .../smithy-client/plugins/http_bearer_auth.rb | 27 +++---- .../smithy-client/plugins/http_digest_auth.rb | 25 +++---- .../lib/smithy-client/plugins/resolve_auth.rb | 4 +- .../smithy-client/plugins/sign_requests.rb | 21 ++++++ .../sig/smithy-client/api_key.rbs | 2 +- .../sig/smithy-client/api_key_provider.rbs | 2 +- .../sig/smithy-client/bearer_token.rbs | 2 +- ...provider.rbs => bearer_token_provider.rbs} | 2 +- .../smithy-client/sig/smithy-client/login.rbs | 2 +- .../sig/smithy-client/login_provider.rbs | 4 +- .../plugins/http_api_key_auth_spec.rb | 4 + .../plugins/http_basic_auth_spec.rb | 6 +- .../plugins/http_bearer_auth_spec.rb | 4 + .../plugins/http_digest_auth_spec.rb | 10 ++- .../plugins/resolve_auth_spec.rb | 74 ++++++++++--------- .../lib/smithy/welds/default_plugins.rb | 2 + 25 files changed, 297 insertions(+), 172 deletions(-) create mode 100644 gems/smithy-client/lib/smithy-client/api_key_signer.rb create mode 100644 gems/smithy-client/lib/smithy-client/auth_scheme.rb create mode 100644 gems/smithy-client/lib/smithy-client/bearer_token_signer.rb create mode 100644 gems/smithy-client/lib/smithy-client/login_signer.rb create mode 100644 gems/smithy-client/lib/smithy-client/null_signer.rb create mode 100644 gems/smithy-client/lib/smithy-client/plugins/sign_requests.rb rename gems/smithy-client/sig/smithy-client/{bearer_provider.rbs => bearer_token_provider.rbs} (63%) diff --git a/gems/smithy-client/lib/smithy-client.rb b/gems/smithy-client/lib/smithy-client.rb index 9dc2ce651..00156cec6 100644 --- a/gems/smithy-client/lib/smithy-client.rb +++ b/gems/smithy-client/lib/smithy-client.rb @@ -51,7 +51,9 @@ # identity and auth require_relative 'smithy-client/auth' +require_relative 'smithy-client/auth_scheme' require_relative 'smithy-client/identity_provider' +require_relative 'smithy-client/null_signer' require_relative 'smithy-client/refreshing_identity_provider' # stubbing diff --git a/gems/smithy-client/lib/smithy-client/api_key_signer.rb b/gems/smithy-client/lib/smithy-client/api_key_signer.rb new file mode 100644 index 000000000..7da952c26 --- /dev/null +++ b/gems/smithy-client/lib/smithy-client/api_key_signer.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +module Smithy + module Client + # Signs requests with the ApiKey identity. + class ApiKeySigner + def initialize(options = {}) + @name = options[:name] + @in = options[:in] + @scheme = options[:scheme] + end + + def sign_request(context) + case @in + when 'header' then sign_in_header(context.http_request, context.config.api_key_provider) + when 'query' then sign_in_query_param(context.http_request, context.config.api_key_provider) + end + end + + def presign_url(_context) + raise NotImplementedError + end + + private + + def sign_in_header(http_request, provider) + http_request.headers.delete(@name) + value = "#{@scheme} #{provider.identity.key}".strip + http_request.headers[@name] = value + end + + def sign_in_query_param(http_request, provider) + remove_query_param(http_request) + append_query_param(http_request, provider) + end + + def append_query_param(http_request, provider) + value = provider.identity.key + if http_request.endpoint.query + http_request.endpoint.query += "&#{@name}=#{value}" + else + http_request.endpoint.query = "#{@name}=#{value}" + end + end + + def remove_query_param(http_request) + return unless http_request.endpoint.query + + parsed = CGI.parse(http_request.endpoint.query) + parsed.delete(@name) + # encode_www_form ignores query params without values + # (CGI parses these as empty lists) + parsed.each do |key, values| + parsed[key] = values.empty? ? nil : values + end + http_request.endpoint.query = URI.encode_www_form(parsed) + end + end + end +end diff --git a/gems/smithy-client/lib/smithy-client/auth.rb b/gems/smithy-client/lib/smithy-client/auth.rb index ca14ff5f3..de8e6350b 100644 --- a/gems/smithy-client/lib/smithy-client/auth.rb +++ b/gems/smithy-client/lib/smithy-client/auth.rb @@ -3,29 +3,29 @@ module Client # @api private module Auth class << self - def resolve_auth(context, endpoint_properties = {}) + def resolve(context, endpoint_properties = {}) if endpoint_properties.key?('authSchemes') - resolve_auth_scheme_with_endpoint(context, endpoint_properties['authSchemes']) + resolve_with_endpoint_auth(context, endpoint_properties['authSchemes']) else - resolve_auth_scheme_without_endpoint(context) + resolve_without_endpoint_auth(context) end end private - def resolve_auth_scheme_with_endpoint(context, endpoint_auth_schemes) + def resolve_with_endpoint_auth(context, endpoint_auth_schemes) normalized_endpoint_schemes = [] endpoint_auth_schemes.each do |scheme| scheme_id = context.config.endpoint_auth_schemes[scheme['name']] next unless scheme_id - normalized_scheme = { scheme_id: scheme_id } + properties = {} scheme.each do |key, value| next if key == 'name' - normalized_scheme[key] = value + properties[key] = value end - normalized_endpoint_schemes << normalized_scheme + normalized_endpoint_schemes << { scheme_id: scheme_id, properties: properties } end resolved_auth_options = prioritize_auth_options( normalized_endpoint_schemes, @@ -34,7 +34,7 @@ def resolve_auth_scheme_with_endpoint(context, endpoint_auth_schemes) resolve_auth_scheme(context.config.auth_schemes, resolved_auth_options) end - def resolve_auth_scheme_without_endpoint(context) + def resolve_without_endpoint_auth(context) auth_parameters = context.client.class.auth_parameters.create(context) auth_options = context.config.auth_resolver.resolve(auth_parameters) resolved_auth_options = prioritize_auth_options(auth_options, context.config.auth_scheme_preference) @@ -66,12 +66,13 @@ def resolve_auth_scheme(auth_schemes, auth_options) failures = [] auth_options.each do |auth_option| scheme_id = auth_option[:scheme_id] + if scheme_id == 'smithy.api#noAuth' + return AuthScheme.new(identity_provider: nil, scheme_id: 'smithy.api#noAuth', signer: NullSigner.new) + end - # Anonymous auth does not have a plugin and does not sign - return auth_option if scheme_id == 'smithy.api#noAuth' - - error = validate_auth_scheme(auth_schemes, scheme_id) - return auth_option unless error + auth_scheme = auth_schemes[scheme_id] + error = validate_auth_scheme(auth_scheme, scheme_id) + return auth_scheme unless error failures << error end @@ -79,10 +80,10 @@ def resolve_auth_scheme(auth_schemes, auth_options) raise failures.join("\n") end - def validate_auth_scheme(auth_schemes, scheme_id) - return "Auth scheme #{scheme_id} was not enabled for this request" unless auth_schemes.key?(scheme_id) + def validate_auth_scheme(auth_scheme, scheme_id) + return "Auth scheme #{scheme_id} was not enabled for this request" unless auth_scheme - identity_provider = auth_schemes[scheme_id] + identity_provider = auth_scheme.identity_provider return "Auth scheme #{scheme_id} did not have an identity provider configured" unless identity_provider return "Auth scheme #{scheme_id} failed to resolve identity" unless identity_provider.set? diff --git a/gems/smithy-client/lib/smithy-client/auth_scheme.rb b/gems/smithy-client/lib/smithy-client/auth_scheme.rb new file mode 100644 index 000000000..db151c4b9 --- /dev/null +++ b/gems/smithy-client/lib/smithy-client/auth_scheme.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Smithy + module Client + # Contains information about candidate authentication schemes. + class AuthScheme + def initialize(options = {}) + @identity_provider = options[:identity_provider] + @scheme_id = options[:scheme_id] + @signer = options[:signer] + end + + # @return [IdentityProvider] + attr_reader :identity_provider + + # @return [String] + attr_reader :scheme_id + + # @return [Signer] + attr_reader :signer + end + end +end diff --git a/gems/smithy-client/lib/smithy-client/bearer_token_signer.rb b/gems/smithy-client/lib/smithy-client/bearer_token_signer.rb new file mode 100644 index 000000000..0ec2c0c9f --- /dev/null +++ b/gems/smithy-client/lib/smithy-client/bearer_token_signer.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Smithy + module Client + # Signs requests with the BearerToken identity. + class BearerTokenSigner + def sign_request(context) + context.http_request.headers.delete('Authorization') + provider = context.config.bearer_token_provider + context.http_request.headers['Authorization'] = "Bearer #{provider.identity.token}" + end + + def presign_url(_context) + raise NotImplementedError + end + 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 new file mode 100644 index 000000000..4f6e5754a --- /dev/null +++ b/gems/smithy-client/lib/smithy-client/login_signer.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Smithy + module Client + # Signs requests with the Login identity. + class LoginSigner + def initialize(options = {}) + @scheme_id = options[:scheme_id] + end + + def sign_request(context) + raise NotImplementedError unless @scheme_id == 'smithy.api#httpBasicAuth' + + sign_with_basic(context.http_request, context.config.login_provider) + end + + def presign_url(_context) + raise NotImplementedError + end + + private + + def sign_with_basic(http_request, provider) + http_request.headers.delete('Authorization') + identity = provider.identity + encoded = Base64.strict_encode64("#{identity.username}:#{identity.password}") + http_request.headers['Authorization'] = "Basic #{encoded}" + end + end + end +end diff --git a/gems/smithy-client/lib/smithy-client/null_signer.rb b/gems/smithy-client/lib/smithy-client/null_signer.rb new file mode 100644 index 000000000..34f3cb693 --- /dev/null +++ b/gems/smithy-client/lib/smithy-client/null_signer.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Smithy + module Client + # A signer that does not sign requests. Used for anonymous authentication. + class NullSigner + def sign_request(_context) + # no-op + end + + def presign_url(_context) + # no-op + end + end + end +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 0fa93f3dd..5da78610b 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 @@ -2,6 +2,7 @@ require_relative '../api_key' require_relative '../api_key_provider' +require_relative '../api_key_signer' module Smithy module Client @@ -27,64 +28,18 @@ class that responds to #identity and returns a {Smithy::Client::ApiKey}. provider if provider.set? end - def after_initialize(client) - client.config.auth_schemes['smithy.api#httpApiKeyAuth'] = client.config.api_key_provider + option(:api_key_signer) do |config| + trait = config.service.traits['smithy.api#httpApiKeyAuth'] + ApiKeySigner.new(name: trait['name'], in: trait['in'], scheme: trait['scheme']) end - # @api private - class Handler < Client::Handler - def call(context) - sign(context) if context.auth[:scheme_id] == 'smithy.api#httpApiKeyAuth' - @handler.call(context) - end - - private - - def sign(context) - properties = context.config.service.traits['smithy.api#httpApiKeyAuth'] - http_request = context.http_request - identity = context.config.api_key_provider.identity - case properties['in'] - when 'header' then sign_in_header(properties, http_request, identity) - when 'query' then sign_in_query_param(properties, http_request, identity) - end - end - - def sign_in_header(properties, http_request, identity) - http_request.headers.delete(properties['name']) - value = "#{properties['scheme']} #{identity.key}".strip - http_request.headers[properties['name']] = value - end - - def sign_in_query_param(properties, http_request, identity) - name = properties['name'] - remove_query_param(http_request, name) - append_query_param(http_request, name, identity.key) - end - - def append_query_param(request, name, value) - if request.endpoint.query - request.endpoint.query += "&#{name}=#{value}" - else - request.endpoint.query = "#{name}=#{value}" - end - end - - def remove_query_param(request, name) - return unless request.endpoint.query - - parsed = CGI.parse(request.endpoint.query) - parsed.delete(name) - # encode_www_form ignores query params without values - # (CGI parses these as empty lists) - parsed.each do |key, values| - parsed[key] = values.empty? ? nil : values - end - request.endpoint.query = URI.encode_www_form(parsed) - end + def after_initialize(client) + client.config.auth_schemes['smithy.api#httpApiKeyAuth'] = AuthScheme.new( + identity_provider: client.config.api_key_provider, + scheme_id: 'smithy.api#httpApiKeyAuth', + signer: client.config.api_key_signer + ) end - - handler(Handler, step: :sign) end end 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 4e0dd0476..c16df797f 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 @@ -2,6 +2,7 @@ require_relative '../login' require_relative '../login_provider' +require_relative '../login_signer' module Smithy module Client @@ -35,30 +36,17 @@ class that responds to #identity and returns a {Smithy::Client::Login}. provider if provider.set? end - def after_initialize(client) - client.config.auth_schemes['smithy.api#httpBasicAuth'] = client.config.login_provider + option(:login_signer) do |_config| + LoginSigner.new(scheme_id: 'smithy.api#httpBasicAuth') end - # @api private - class Handler < Client::Handler - def call(context) - sign(context) if context.auth[:scheme_id] == 'smithy.api#httpBasicAuth' - @handler.call(context) - end - - private - - def sign(context) - http_request = context.http_request - identity = context.config.login_provider.identity - http_request.headers.delete('Authorization') - identity_string = "#{identity.username}:#{identity.password}" - encoded = Base64.strict_encode64(identity_string) - http_request.headers['Authorization'] = "Basic #{encoded}" - end + def after_initialize(client) + client.config.auth_schemes['smithy.api#httpBasicAuth'] = AuthScheme.new( + identity_provider: client.config.login_provider, + scheme_id: 'smithy.api#httpBasicAuth', + signer: client.config.login_signer + ) end - - handler(Handler, step: :sign) end end end 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 de47c3884..0c48363df 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 @@ -2,6 +2,7 @@ require_relative '../bearer_token' require_relative '../bearer_token_provider' +require_relative '../bearer_token_signer' module Smithy module Client @@ -27,27 +28,17 @@ class that responds to #identity and returns a {Smithy::Client::BearerToken}. provider if provider.set? end - def after_initialize(client) - client.config.auth_schemes['smithy.api#httpBearerAuth'] = client.config.bearer_token_provider + option(:bearer_token_signer) do |_config| + BearerTokenSigner.new end - # @api private - class Handler < Client::Handler - def call(context) - sign(context) if context.auth[:scheme_id] == 'smithy.api#httpBearerAuth' - @handler.call(context) - end - - private - - def sign(context) - context.http_request.headers.delete('Authorization') - provider = context.config.bearer_token_provider - context.http_request.headers['Authorization'] = "Bearer #{provider.identity.token}" - end + def after_initialize(client) + client.config.auth_schemes['smithy.api#httpBearerAuth'] = AuthScheme.new( + identity_provider: client.config.bearer_token_provider, + scheme_id: 'smithy.api#httpBearerAuth', + signer: client.config.bearer_token_signer + ) end - - handler(Handler, step: :sign) end end 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 1fcd5734c..a6743a4fb 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 @@ -2,6 +2,7 @@ require_relative '../login' require_relative '../login_provider' +require_relative '../login_signer' module Smithy module Client @@ -35,25 +36,17 @@ class that responds to #identity and returns a {Smithy::Client::Login}. provider if provider.set? end - def after_initialize(client) - client.config.auth_schemes['smithy.api#httpDigestAuth'] = client.config.login_provider + option(:login_signer) do |_config| + LoginSigner.new(scheme_id: 'smithy.api#httpDigestAuth') end - # @api private - class Handler < Client::Handler - def call(context) - sign(context) if context.auth[:scheme_id] == 'smithy.api#httpDigestAuth' - @handler.call(context) - end - - def sign(_context) - # TODO: requires a nonce from the server - # This cannot be implemented unless we rescue from a 401 and retry with the nonce - raise NotImplementedError - end + def after_initialize(client) + client.config.auth_schemes['smithy.api#httpDigestAuth'] = AuthScheme.new( + identity_provider: client.config.login_provider, + scheme_id: 'smithy.api#httpDigestAuth', + signer: client.config.login_signer + ) end - - handler(Handler, step: :sign) end end end diff --git a/gems/smithy-client/lib/smithy-client/plugins/resolve_auth.rb b/gems/smithy-client/lib/smithy-client/plugins/resolve_auth.rb index de834a394..1b8a93e6c 100644 --- a/gems/smithy-client/lib/smithy-client/plugins/resolve_auth.rb +++ b/gems/smithy-client/lib/smithy-client/plugins/resolve_auth.rb @@ -31,12 +31,12 @@ def before_initialize(client_class, options) # @api private class Handler < Smithy::Client::Handler def call(context) - context.auth = Auth.resolve_auth(context, context[:resolved_endpoint].properties) + context.auth = Auth.resolve(context, context[:resolved_endpoint].properties) @handler.call(context) end end - handler(Handler, step: :sign, priority: 70) + handler(Handler, step: :sign, priority: 75) end end end diff --git a/gems/smithy-client/lib/smithy-client/plugins/sign_requests.rb b/gems/smithy-client/lib/smithy-client/plugins/sign_requests.rb new file mode 100644 index 000000000..9df4cc964 --- /dev/null +++ b/gems/smithy-client/lib/smithy-client/plugins/sign_requests.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Smithy + module Client + module Plugins + # @api private + class SignRequests < Plugin + # @api private + class Handler < Smithy::Client::Handler + def call(context) + signer = context.auth.signer + signer.sign_request(context) + @handler.call(context) + end + end + + handler(Handler, step: :sign) + end + end + end +end diff --git a/gems/smithy-client/sig/smithy-client/api_key.rbs b/gems/smithy-client/sig/smithy-client/api_key.rbs index 4208c2da7..1329b9e72 100644 --- a/gems/smithy-client/sig/smithy-client/api_key.rbs +++ b/gems/smithy-client/sig/smithy-client/api_key.rbs @@ -1,7 +1,7 @@ module Smithy module Client class ApiKey - def initialize: (Hash[Symbol, untyped] options) -> void + def initialize: (?Hash[Symbol, untyped] options) -> void attr_reader key: String def set?: () -> bool end diff --git a/gems/smithy-client/sig/smithy-client/api_key_provider.rbs b/gems/smithy-client/sig/smithy-client/api_key_provider.rbs index 54f652df7..885d517fe 100644 --- a/gems/smithy-client/sig/smithy-client/api_key_provider.rbs +++ b/gems/smithy-client/sig/smithy-client/api_key_provider.rbs @@ -2,7 +2,7 @@ module Smithy module Client class ApiKeyProvider include IdentityProvider - def initialize: (Hash[Symbol, untyped] options) -> void + def initialize: (?Hash[Symbol, untyped] options) -> void end end end diff --git a/gems/smithy-client/sig/smithy-client/bearer_token.rbs b/gems/smithy-client/sig/smithy-client/bearer_token.rbs index f229412cd..db213ab55 100644 --- a/gems/smithy-client/sig/smithy-client/bearer_token.rbs +++ b/gems/smithy-client/sig/smithy-client/bearer_token.rbs @@ -1,7 +1,7 @@ module Smithy module Client class BearerToken - def initialize: (Hash[Symbol, untyped] options) -> void + def initialize: (?Hash[Symbol, untyped] options) -> void attr_reader token: String attr_reader password: String def set?: () -> bool diff --git a/gems/smithy-client/sig/smithy-client/bearer_provider.rbs b/gems/smithy-client/sig/smithy-client/bearer_token_provider.rbs similarity index 63% rename from gems/smithy-client/sig/smithy-client/bearer_provider.rbs rename to gems/smithy-client/sig/smithy-client/bearer_token_provider.rbs index 709ede4cd..9cb55bbcf 100644 --- a/gems/smithy-client/sig/smithy-client/bearer_provider.rbs +++ b/gems/smithy-client/sig/smithy-client/bearer_token_provider.rbs @@ -2,7 +2,7 @@ module Smithy module Client class BearerTokenProvider include IdentityProvider - def initialize: (Hash[Symbol, untyped] options) -> void + def initialize: (?Hash[Symbol, untyped] options) -> void end end end diff --git a/gems/smithy-client/sig/smithy-client/login.rbs b/gems/smithy-client/sig/smithy-client/login.rbs index 4ae021de5..e7886d14d 100644 --- a/gems/smithy-client/sig/smithy-client/login.rbs +++ b/gems/smithy-client/sig/smithy-client/login.rbs @@ -1,7 +1,7 @@ module Smithy module Client class Login - def initialize: (Hash[Symbol, untyped] options) -> void + def initialize: (?Hash[Symbol, untyped] options) -> void attr_reader username: String attr_reader password: String def set?: () -> bool diff --git a/gems/smithy-client/sig/smithy-client/login_provider.rbs b/gems/smithy-client/sig/smithy-client/login_provider.rbs index 97e38b9fd..51dc78f3f 100644 --- a/gems/smithy-client/sig/smithy-client/login_provider.rbs +++ b/gems/smithy-client/sig/smithy-client/login_provider.rbs @@ -1,8 +1,8 @@ module Smithy module Client - class HttpLoginProvider + class LoginProvider include IdentityProvider - def initialize: (String, String) -> void + def initialize: (?Hash[Symbol, untyped] options) -> void end end end diff --git a/gems/smithy-client/spec/smithy-client/plugins/http_api_key_auth_spec.rb b/gems/smithy-client/spec/smithy-client/plugins/http_api_key_auth_spec.rb index 4179c1e92..e178bf28b 100644 --- a/gems/smithy-client/spec/smithy-client/plugins/http_api_key_auth_spec.rb +++ b/gems/smithy-client/spec/smithy-client/plugins/http_api_key_auth_spec.rb @@ -17,6 +17,10 @@ module Plugins let(:client) { client_class.new(stub_responses: true) } + before do + shapes['smithy.ruby.tests#SampleClient']['traits']['smithy.api#httpApiKeyAuth'] = {} + end + it 'adds an :api_key option to config' do expect(client.config).to respond_to(:api_key) end diff --git a/gems/smithy-client/spec/smithy-client/plugins/http_basic_auth_spec.rb b/gems/smithy-client/spec/smithy-client/plugins/http_basic_auth_spec.rb index b8092bd12..98ff95977 100644 --- a/gems/smithy-client/spec/smithy-client/plugins/http_basic_auth_spec.rb +++ b/gems/smithy-client/spec/smithy-client/plugins/http_basic_auth_spec.rb @@ -17,6 +17,10 @@ module Plugins let(:client) { client_class.new(stub_responses: true) } + before do + shapes['smithy.ruby.tests#SampleClient']['traits']['smithy.api#httpBasicAuth'] = {} + end + it 'adds an :login_username option to config' do expect(client.config).to respond_to(:login_username) end @@ -62,7 +66,7 @@ module Plugins expect(identity.password).to eq('password') end - it 'does not default a:login_provider when one of the parts is set' do + it 'does not default a :login_provider when one of the parts is set' do client = client_class.new(login_username: 'username') provider = client.config.login_provider expect(provider).to be_nil diff --git a/gems/smithy-client/spec/smithy-client/plugins/http_bearer_auth_spec.rb b/gems/smithy-client/spec/smithy-client/plugins/http_bearer_auth_spec.rb index 7bd670e03..528461605 100644 --- a/gems/smithy-client/spec/smithy-client/plugins/http_bearer_auth_spec.rb +++ b/gems/smithy-client/spec/smithy-client/plugins/http_bearer_auth_spec.rb @@ -17,6 +17,10 @@ module Plugins let(:client) { client_class.new(stub_responses: true) } + before do + shapes['smithy.ruby.tests#SampleClient']['traits']['smithy.api#httpBearerAuth'] = {} + end + it 'adds an :bearer_token option to config' do expect(client.config).to respond_to(:bearer_token) end diff --git a/gems/smithy-client/spec/smithy-client/plugins/http_digest_auth_spec.rb b/gems/smithy-client/spec/smithy-client/plugins/http_digest_auth_spec.rb index 5bf866907..7464bbc5b 100644 --- a/gems/smithy-client/spec/smithy-client/plugins/http_digest_auth_spec.rb +++ b/gems/smithy-client/spec/smithy-client/plugins/http_digest_auth_spec.rb @@ -17,6 +17,10 @@ module Plugins let(:client) { client_class.new(stub_responses: true) } + before do + shapes['smithy.ruby.tests#SampleClient']['traits']['smithy.api#httpDigestAuth'] = {} + end + it 'adds an :login_username option to config' do expect(client.config).to respond_to(:login_username) end @@ -62,7 +66,7 @@ module Plugins expect(identity.password).to eq('password') end - it 'does not default a:login_provider when one of the parts is set' do + it 'does not default a :login_provider when one of the parts is set' do client = client_class.new(login_username: 'username') provider = client.config.login_provider expect(provider).to be_nil @@ -73,7 +77,9 @@ module Plugins end context 'signing' do - it 'signs in the header' + it 'is not supported' do + expect { client.operation }.to raise_error(NotImplementedError) + end end end end 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 d6981c752..68e55595d 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 @@ -19,47 +19,51 @@ module Plugins expect(client.config).to respond_to(:auth_scheme_preference) end - it 'adds scheme ids to auth scheme config hash' do + it 'adds auth schemes to the auth scheme config hash' do + shapes['smithy.ruby.tests#SampleClient']['traits']['smithy.api#httpApiKeyAuth'] = {} + shapes['smithy.ruby.tests#SampleClient']['traits']['smithy.api#httpBasicAuth'] = {} + shapes['smithy.ruby.tests#SampleClient']['traits']['smithy.api#httpBearerAuth'] = {} + shapes['smithy.ruby.tests#SampleClient']['traits']['smithy.api#httpDigestAuth'] = {} client_class.add_plugin(HttpApiKeyAuth) client_class.add_plugin(HttpBasicAuth) client_class.add_plugin(HttpBearerAuth) client_class.add_plugin(HttpDigestAuth) - expect(client.config.auth_schemes['smithy.api#httpApiKeyAuth']).to be_a(ApiKeyProvider) - expect(client.config.auth_schemes['smithy.api#httpBasicAuth']).to be_a(LoginProvider) - expect(client.config.auth_schemes['smithy.api#httpBearerAuth']).to be_a(BearerTokenProvider) - expect(client.config.auth_schemes['smithy.api#httpDigestAuth']).to be_a(LoginProvider) + expect(client.config.auth_schemes.values).to all be_a(AuthScheme) + expect(client.config.auth_schemes.size).to eq(4) + expect(client.config.auth_schemes).to have_key('smithy.api#httpApiKeyAuth') + expect(client.config.auth_schemes).to have_key('smithy.api#httpBasicAuth') + expect(client.config.auth_schemes).to have_key('smithy.api#httpBearerAuth') + expect(client.config.auth_schemes).to have_key('smithy.api#httpDigestAuth') end it 'supports anonymous auth' do resp = client.operation - expect(resp.context.auth[:scheme_id]).to eq('smithy.api#noAuth') + expect(resp.context.auth).to be_a(AuthScheme) + expect(resp.context.auth.scheme_id).to eq('smithy.api#noAuth') end it 'supports http api key auth' do shapes['smithy.ruby.tests#SampleClient']['traits']['smithy.api#httpApiKeyAuth'] = {} client_class.add_plugin(HttpApiKeyAuth) resp = client.operation - expect(resp.context.auth).to eq({ scheme_id: 'smithy.api#httpApiKeyAuth' }) + expect(resp.context.auth).to be_a(AuthScheme) + expect(resp.context.auth.scheme_id).to eq('smithy.api#httpApiKeyAuth') end it 'supports http basic auth' do shapes['smithy.ruby.tests#SampleClient']['traits']['smithy.api#httpBasicAuth'] = {} client_class.add_plugin(HttpBasicAuth) resp = client.operation - expect(resp.context.auth).to eq({ scheme_id: 'smithy.api#httpBasicAuth' }) + expect(resp.context.auth).to be_a(AuthScheme) + expect(resp.context.auth.scheme_id).to eq('smithy.api#httpBasicAuth') end it 'supports http bearer auth' do shapes['smithy.ruby.tests#SampleClient']['traits']['smithy.api#httpBearerAuth'] = {} client_class.add_plugin(HttpBearerAuth) resp = client.operation - expect(resp.context.auth).to eq({ scheme_id: 'smithy.api#httpBearerAuth' }) - end - - it 'supports http digest auth' do - shapes['smithy.ruby.tests#SampleClient']['traits']['smithy.api#httpDigestAuth'] = {} - client_class.add_plugin(HttpDigestAuth) - expect { client.operation }.to raise_error(NotImplementedError) + expect(resp.context.auth).to be_a(AuthScheme) + expect(resp.context.auth.scheme_id).to eq('smithy.api#httpBearerAuth') end it 'resolves the first supported auth scheme' do @@ -70,7 +74,7 @@ module Plugins client_class.add_plugin(HttpApiKeyAuth) client_class.add_plugin(HttpBearerAuth) resp = client.operation - expect(resp.context.auth).to eq({ scheme_id: 'smithy.api#httpBearerAuth' }) + expect(resp.context.auth.scheme_id).to eq('smithy.api#httpBearerAuth') end it 'resolves the first supported auth scheme with an identity provider configured' do @@ -82,7 +86,7 @@ module Plugins client_class.add_plugin(HttpBearerAuth) client = client_class.new(stub_responses: true, bearer_token_provider: nil) resp = client.operation - expect(resp.context.auth).to eq({ scheme_id: 'smithy.api#httpApiKeyAuth' }) + expect(resp.context.auth.scheme_id).to eq('smithy.api#httpApiKeyAuth') end it 'resolves the first supported auth scheme with a resolved identity' do @@ -94,10 +98,11 @@ module Plugins client_class.add_plugin(HttpBearerAuth) client = client_class.new(stub_responses: true, bearer_token_provider: BearerTokenProvider.new) resp = client.operation - expect(resp.context.auth).to eq({ scheme_id: 'smithy.api#httpApiKeyAuth' }) + expect(resp.context.auth.scheme_id).to eq('smithy.api#httpApiKeyAuth') end it 'raises an error when no auth options were resolved' do + shapes['smithy.ruby.tests#SampleClient']['traits']['smithy.api#httpApiKeyAuth'] = {} client_class.add_plugin(HttpApiKeyAuth) auth_resolver = Class.new do def resolve(_) @@ -144,7 +149,7 @@ def resolve(_) auth_scheme_preference: ['smithy.api#httpBasicAuth'] ) resp = client.operation - expect(resp.context.auth).to eq({ scheme_id: 'smithy.api#httpBasicAuth' }) + expect(resp.context.auth.scheme_id).to eq('smithy.api#httpBasicAuth') end it 'ignores unsupported preferred auth schemes' do @@ -153,7 +158,7 @@ def resolve(_) auth_scheme_preference: ['smithy.api#httpDigestAuth', 'smithy.api#httpBasicAuth'] ) resp = client.operation - expect(resp.context.auth).to eq({ scheme_id: 'smithy.api#httpBasicAuth' }) + expect(resp.context.auth.scheme_id).to eq('smithy.api#httpBasicAuth') end it 'falls back to modeled order when no preferred auth schemes are supported' do @@ -162,7 +167,7 @@ def resolve(_) auth_scheme_preference: ['smithy.api#httpDigestAuth'] ) resp = client.operation - expect(resp.context.auth).to eq({ scheme_id: 'smithy.api#httpApiKeyAuth' }) + expect(resp.context.auth.scheme_id).to eq('smithy.api#httpApiKeyAuth') end end @@ -206,7 +211,7 @@ def endpoint_rules(auth_schemes = []) client_class.add_plugin(HttpBearerAuth) # to register the endpoint auth scheme client = client_class.new(stub_responses: true) resp = client.operation - expect(resp.context.auth).to eq({ scheme_id: 'smithy.api#httpBearerAuth' }) + expect(resp.context.auth.scheme_id).to eq('smithy.api#httpBearerAuth') end it 'selects the first supported endpoint auth scheme' do @@ -215,17 +220,18 @@ def endpoint_rules(auth_schemes = []) client_class.add_plugin(HttpBearerAuth) # to register the endpoint auth scheme client = client_class.new(stub_responses: true) resp = client.operation - expect(resp.context.auth).to eq({ scheme_id: 'smithy.api#httpBearerAuth' }) + expect(resp.context.auth.scheme_id).to eq('smithy.api#httpBearerAuth') end - it 'forwards additional auth scheme properties from the endpoint' do - shapes['smithy.ruby.tests#SampleClient']['traits']['smithy.rules#endpointRuleSet'] = - endpoint_rules([{ 'name' => 'bearer', 'foo' => 'bar' }]) - client_class.add_plugin(HttpBearerAuth) # to register the endpoint auth scheme - client = client_class.new(stub_responses: true) - resp = client.operation - expect(resp.context.auth).to eq({ scheme_id: 'smithy.api#httpBearerAuth', 'foo' => 'bar' }) - end + # it 'forwards additional auth scheme properties from the endpoint' do + # shapes['smithy.ruby.tests#SampleClient']['traits']['smithy.rules#endpointRuleSet'] = + # endpoint_rules([{ 'name' => 'bearer', 'foo' => 'bar' }]) + # client_class.add_plugin(HttpBearerAuth) # to register the endpoint auth scheme + # client = client_class.new(stub_responses: true) + # resp = client.operation + # expect(resp.context.auth.scheme_id).to eq('smithy.api#httpBearerAuth') + # expect(resp.context.auth.properties).to eq('foo' => 'bar') + # end context 'with auth scheme preference' do it 'uses the preference list to prioritize endpoint auth schemes' do @@ -237,7 +243,7 @@ def endpoint_rules(auth_schemes = []) auth_scheme_preference: ['smithy.api#noAuth', 'smithy.api#httpBearerAuth'] ) resp = client.operation - expect(resp.context.auth).to eq({ scheme_id: 'smithy.api#httpBearerAuth' }) + expect(resp.context.auth.scheme_id).to eq('smithy.api#httpBearerAuth') end it 'ignores unsupported preferred auth schemes' do @@ -249,7 +255,7 @@ def endpoint_rules(auth_schemes = []) auth_scheme_preference: ['smithy.api#httpDigestAuth', 'smithy.api#httpBearerAuth'] ) resp = client.operation - expect(resp.context.auth).to eq({ scheme_id: 'smithy.api#httpBearerAuth' }) + expect(resp.context.auth.scheme_id).to eq('smithy.api#httpBearerAuth') end it 'falls back to endpoint auth scheme order when no preferred auth schemes are supported' do @@ -261,7 +267,7 @@ def endpoint_rules(auth_schemes = []) auth_scheme_preference: ['smithy.api#httpDigestAuth'] ) resp = client.operation - expect(resp.context.auth).to eq({ scheme_id: 'smithy.api#httpBearerAuth' }) + expect(resp.context.auth.scheme_id).to eq('smithy.api#httpBearerAuth') end end end diff --git a/gems/smithy/lib/smithy/welds/default_plugins.rb b/gems/smithy/lib/smithy/welds/default_plugins.rb index 38cc255f8..4a9db3ac0 100644 --- a/gems/smithy/lib/smithy/welds/default_plugins.rb +++ b/gems/smithy/lib/smithy/welds/default_plugins.rb @@ -15,6 +15,7 @@ require 'smithy-client/plugins/resolve_auth' require 'smithy-client/plugins/response_target' require 'smithy-client/plugins/retry_errors' +require 'smithy-client/plugins/sign_requests' require 'smithy-client/plugins/stub_responses' require 'smithy-client/plugins/user_agent' @@ -45,6 +46,7 @@ def add_plugins Smithy::Client::Plugins::ResolveAuth => { require_path: "#{base_path}/resolve_auth" }, Smithy::Client::Plugins::ResponseTarget => { require_path: "#{base_path}/response_target" }, Smithy::Client::Plugins::RetryErrors => { require_path: "#{base_path}/retry_errors" }, + Smithy::Client::Plugins::SignRequests => { require_path: "#{base_path}/sign_requests" }, Smithy::Client::Plugins::StubResponses => { require_path: "#{base_path}/stub_responses" }, Smithy::Client::Plugins::UserAgent => { require_path: "#{base_path}/user_agent" } } From cac85b6e1ae9b9923f7e9812d871a17f59b39979 Mon Sep 17 00:00:00 2001 From: Matt Muller Date: Mon, 29 Sep 2025 19:04:20 -0400 Subject: [PATCH 3/7] Fix rbs --- gems/smithy-client/sig/smithy-client/auth_scheme.rbs | 10 ++++++++++ .../sig/smithy-client/handler_context.rbs | 2 +- gems/smithy-client/sig/smithy-client/interfaces.rbs | 5 +++++ 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 gems/smithy-client/sig/smithy-client/auth_scheme.rbs diff --git a/gems/smithy-client/sig/smithy-client/auth_scheme.rbs b/gems/smithy-client/sig/smithy-client/auth_scheme.rbs new file mode 100644 index 000000000..1a7e72916 --- /dev/null +++ b/gems/smithy-client/sig/smithy-client/auth_scheme.rbs @@ -0,0 +1,10 @@ +module Smithy + module Client + class AuthScheme + def initialize: (?Hash[Symbol, untyped] options) -> void + attr_reader identity_provider: IdentityProvider? + attr_reader scheme_id: String + attr_reader signer: _Signer + end + end +end diff --git a/gems/smithy-client/sig/smithy-client/handler_context.rbs b/gems/smithy-client/sig/smithy-client/handler_context.rbs index fb6744afc..9f75355e2 100644 --- a/gems/smithy-client/sig/smithy-client/handler_context.rbs +++ b/gems/smithy-client/sig/smithy-client/handler_context.rbs @@ -9,7 +9,7 @@ module Smithy attr_accessor config: Struct[untyped]? attr_accessor http_request: Http::Request | untyped attr_accessor http_response: Http::Response | untyped - attr_accessor auth: Hash[Symbol, untyped] + attr_accessor auth: AuthScheme attr_accessor retries: Integer attr_reader metadata: Hash[untyped, untyped] def []: (untyped) -> untyped diff --git a/gems/smithy-client/sig/smithy-client/interfaces.rbs b/gems/smithy-client/sig/smithy-client/interfaces.rbs index e274fd648..9c76ca51a 100644 --- a/gems/smithy-client/sig/smithy-client/interfaces.rbs +++ b/gems/smithy-client/sig/smithy-client/interfaces.rbs @@ -1,6 +1,11 @@ module Smithy module Client type endpoint_url = String | URI::HTTP | URI::HTTPS + + interface _Signer + def sign_request: (HandlerContext) -> void + def presign_url: (HandlerContext) -> String + end interface _ReadableIO def read: (int length, ?string outbuf) -> String From 66ad30666e2f5a1a7440c7ba1344ee104942f2f6 Mon Sep 17 00:00:00 2001 From: Matt Muller Date: Mon, 29 Sep 2025 19:07:47 -0400 Subject: [PATCH 4/7] Rubocop --- gems/smithy-client/lib/smithy-client/auth.rb | 2 ++ gems/smithy/lib/smithy/welds/default_plugins.rb | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/gems/smithy-client/lib/smithy-client/auth.rb b/gems/smithy-client/lib/smithy-client/auth.rb index de8e6350b..84fb210e7 100644 --- a/gems/smithy-client/lib/smithy-client/auth.rb +++ b/gems/smithy-client/lib/smithy-client/auth.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Smithy module Client # @api private diff --git a/gems/smithy/lib/smithy/welds/default_plugins.rb b/gems/smithy/lib/smithy/welds/default_plugins.rb index 4a9db3ac0..2e8a65804 100644 --- a/gems/smithy/lib/smithy/welds/default_plugins.rb +++ b/gems/smithy/lib/smithy/welds/default_plugins.rb @@ -28,7 +28,7 @@ def for?(_service) true end - def add_plugins + def add_plugins # rubocop:disable Metrics/MethodLength base_path = 'smithy-client/plugins' { Smithy::Client::Plugins::ChecksumRequired => { require_path: "#{base_path}/checksum_required" }, From 7c58f1030243248f43079fb4b7b6279a009cd105 Mon Sep 17 00:00:00 2001 From: Matt Muller Date: Tue, 30 Sep 2025 11:37:54 -0400 Subject: [PATCH 5/7] Forward signer properties --- gems/smithy-client/lib/smithy-client/auth.rb | 47 +++++++++++-------- .../lib/smithy-client/auth_scheme.rb | 10 ++-- .../plugins/resolve_auth_spec.rb | 45 +++++++++++++----- .../smithy/templates/client/auth_resolver.erb | 2 +- 4 files changed, 66 insertions(+), 38 deletions(-) diff --git a/gems/smithy-client/lib/smithy-client/auth.rb b/gems/smithy-client/lib/smithy-client/auth.rb index 84fb210e7..50eb6e6d4 100644 --- a/gems/smithy-client/lib/smithy-client/auth.rb +++ b/gems/smithy-client/lib/smithy-client/auth.rb @@ -4,22 +4,26 @@ module Smithy module Client # @api private module Auth + ResolvedAuth = Struct.new(:scheme_id, :signer, :signer_properties, :identity_provider, keyword_init: true) + class << self def resolve(context, endpoint_properties = {}) if endpoint_properties.key?('authSchemes') - resolve_with_endpoint_auth(context, endpoint_properties['authSchemes']) + resolve_with_endpoint_auth(context.config, endpoint_properties['authSchemes']) else - resolve_without_endpoint_auth(context) + auth_parameters = context.client.class.auth_parameters.create(context) + resolve_without_endpoint_auth(context.config, auth_parameters) end end private - def resolve_with_endpoint_auth(context, endpoint_auth_schemes) + def resolve_with_endpoint_auth(config, endpoint_auth_schemes) + endpoint_auth_schemes_map = config.endpoint_auth_schemes normalized_endpoint_schemes = [] endpoint_auth_schemes.each do |scheme| - scheme_id = context.config.endpoint_auth_schemes[scheme['name']] - next unless scheme_id + normalized_scheme_id = endpoint_auth_schemes_map[scheme['name']] + next unless normalized_scheme_id properties = {} scheme.each do |key, value| @@ -27,20 +31,16 @@ def resolve_with_endpoint_auth(context, endpoint_auth_schemes) properties[key] = value end - normalized_endpoint_schemes << { scheme_id: scheme_id, properties: properties } + normalized_endpoint_schemes << { scheme_id: normalized_scheme_id, signer_properties: properties } end - resolved_auth_options = prioritize_auth_options( - normalized_endpoint_schemes, - context.config.auth_scheme_preference - ) - resolve_auth_scheme(context.config.auth_schemes, resolved_auth_options) + resolved_auth_options = prioritize_auth_options(normalized_endpoint_schemes, config.auth_scheme_preference) + resolve_auth_scheme(config.auth_schemes, resolved_auth_options) end - def resolve_without_endpoint_auth(context) - auth_parameters = context.client.class.auth_parameters.create(context) - auth_options = context.config.auth_resolver.resolve(auth_parameters) - resolved_auth_options = prioritize_auth_options(auth_options, context.config.auth_scheme_preference) - resolve_auth_scheme(context.config.auth_schemes, resolved_auth_options) + def resolve_without_endpoint_auth(config, auth_parameters) + auth_options = config.auth_resolver.resolve(auth_parameters) + resolved_auth_options = prioritize_auth_options(auth_options, config.auth_scheme_preference) + resolve_auth_scheme(config.auth_schemes, resolved_auth_options) end def prioritize_auth_options(auth_options, auth_scheme_preference) @@ -69,12 +69,21 @@ def resolve_auth_scheme(auth_schemes, auth_options) auth_options.each do |auth_option| scheme_id = auth_option[:scheme_id] if scheme_id == 'smithy.api#noAuth' - return AuthScheme.new(identity_provider: nil, scheme_id: 'smithy.api#noAuth', signer: NullSigner.new) + return ResolvedAuth.new( + scheme_id: 'smithy.api#noAuth', signer: NullSigner.new, signer_properties: {}, + identity_provider: nil + ) end - auth_scheme = auth_schemes[scheme_id] error = validate_auth_scheme(auth_scheme, scheme_id) - return auth_scheme unless error + unless error + return ResolvedAuth.new( + scheme_id: scheme_id, + signer: auth_scheme.signer, + signer_properties: auth_option[:signer_properties] || {}, + identity_provider: auth_scheme.identity_provider + ) + end failures << error end diff --git a/gems/smithy-client/lib/smithy-client/auth_scheme.rb b/gems/smithy-client/lib/smithy-client/auth_scheme.rb index db151c4b9..b8b70e07e 100644 --- a/gems/smithy-client/lib/smithy-client/auth_scheme.rb +++ b/gems/smithy-client/lib/smithy-client/auth_scheme.rb @@ -2,20 +2,20 @@ module Smithy module Client - # Contains information about candidate authentication schemes. + # Contains information about configured authentication schemes. class AuthScheme def initialize(options = {}) - @identity_provider = options[:identity_provider] @scheme_id = options[:scheme_id] + @identity_provider = options[:identity_provider] @signer = options[:signer] end - # @return [IdentityProvider] - attr_reader :identity_provider - # @return [String] attr_reader :scheme_id + # @return [IdentityProvider] + attr_reader :identity_provider + # @return [Signer] attr_reader :signer end 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 68e55595d..74b03a225 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 @@ -38,7 +38,7 @@ module Plugins it 'supports anonymous auth' do resp = client.operation - expect(resp.context.auth).to be_a(AuthScheme) + expect(resp.context.auth).to be_a(Auth::ResolvedAuth) expect(resp.context.auth.scheme_id).to eq('smithy.api#noAuth') end @@ -46,7 +46,7 @@ module Plugins shapes['smithy.ruby.tests#SampleClient']['traits']['smithy.api#httpApiKeyAuth'] = {} client_class.add_plugin(HttpApiKeyAuth) resp = client.operation - expect(resp.context.auth).to be_a(AuthScheme) + expect(resp.context.auth).to be_a(Auth::ResolvedAuth) expect(resp.context.auth.scheme_id).to eq('smithy.api#httpApiKeyAuth') end @@ -54,7 +54,7 @@ module Plugins shapes['smithy.ruby.tests#SampleClient']['traits']['smithy.api#httpBasicAuth'] = {} client_class.add_plugin(HttpBasicAuth) resp = client.operation - expect(resp.context.auth).to be_a(AuthScheme) + expect(resp.context.auth).to be_a(Auth::ResolvedAuth) expect(resp.context.auth.scheme_id).to eq('smithy.api#httpBasicAuth') end @@ -62,7 +62,7 @@ module Plugins shapes['smithy.ruby.tests#SampleClient']['traits']['smithy.api#httpBearerAuth'] = {} client_class.add_plugin(HttpBearerAuth) resp = client.operation - expect(resp.context.auth).to be_a(AuthScheme) + expect(resp.context.auth).to be_a(Auth::ResolvedAuth) expect(resp.context.auth.scheme_id).to eq('smithy.api#httpBearerAuth') end @@ -101,6 +101,25 @@ module Plugins expect(resp.context.auth.scheme_id).to eq('smithy.api#httpApiKeyAuth') end + it 'forwards signer properties from auth options' do + shapes['smithy.ruby.tests#SampleClient']['traits']['smithy.api#httpApiKeyAuth'] = {} + client_class.add_plugin(HttpApiKeyAuth) + auth_resolver = Class.new do + def resolve(_) + [ + { + scheme_id: 'smithy.api#httpApiKeyAuth', + signer_properties: { 'location' => 'query', 'name' => 'api_key' } + } + ] + end + end + client = client_class.new(stub_responses: true, auth_resolver: auth_resolver.new) + resp = client.operation + expect(resp.context.auth.scheme_id).to eq('smithy.api#httpApiKeyAuth') + expect(resp.context.auth.signer_properties).to eq('location' => 'query', 'name' => 'api_key') + end + it 'raises an error when no auth options were resolved' do shapes['smithy.ruby.tests#SampleClient']['traits']['smithy.api#httpApiKeyAuth'] = {} client_class.add_plugin(HttpApiKeyAuth) @@ -223,15 +242,15 @@ def endpoint_rules(auth_schemes = []) expect(resp.context.auth.scheme_id).to eq('smithy.api#httpBearerAuth') end - # it 'forwards additional auth scheme properties from the endpoint' do - # shapes['smithy.ruby.tests#SampleClient']['traits']['smithy.rules#endpointRuleSet'] = - # endpoint_rules([{ 'name' => 'bearer', 'foo' => 'bar' }]) - # client_class.add_plugin(HttpBearerAuth) # to register the endpoint auth scheme - # client = client_class.new(stub_responses: true) - # resp = client.operation - # expect(resp.context.auth.scheme_id).to eq('smithy.api#httpBearerAuth') - # expect(resp.context.auth.properties).to eq('foo' => 'bar') - # end + it 'forwards additional auth scheme properties from the endpoint' do + shapes['smithy.ruby.tests#SampleClient']['traits']['smithy.rules#endpointRuleSet'] = + endpoint_rules([{ 'name' => 'bearer', 'foo' => 'bar' }]) + client_class.add_plugin(HttpBearerAuth) # to register the endpoint auth scheme + client = client_class.new(stub_responses: true) + resp = client.operation + expect(resp.context.auth.scheme_id).to eq('smithy.api#httpBearerAuth') + expect(resp.context.auth.signer_properties).to eq('foo' => 'bar') + end context 'with auth scheme preference' do it 'uses the preference list to prioritize endpoint auth schemes' do diff --git a/gems/smithy/lib/smithy/templates/client/auth_resolver.erb b/gems/smithy/lib/smithy/templates/client/auth_resolver.erb index 2c6492e33..a106f57d0 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 [Hash] + # @return [Array] def resolve(parameters) <% auth_rules_code.each do |line| -%> <%= line %> From a4a24b3d84b94291c8433268a9fa83d9fde25650 Mon Sep 17 00:00:00 2001 From: Matt Muller Date: Tue, 30 Sep 2025 14:27:09 -0400 Subject: [PATCH 6/7] Fix rbs and PR feedback --- gems/smithy-client/lib/smithy-client/auth.rb | 7 ++++--- .../lib/smithy-client/plugins/resolve_auth.rb | 5 ++++- gems/smithy-client/sig/smithy-client/handler_context.rbs | 2 +- .../smithy-client/{auth_scheme.rbs => resolved_auth.rbs} | 3 ++- .../spec/smithy-client/plugins/resolve_auth_spec.rb | 8 ++++---- 5 files changed, 15 insertions(+), 10 deletions(-) rename gems/smithy-client/sig/smithy-client/{auth_scheme.rbs => resolved_auth.rbs} (74%) diff --git a/gems/smithy-client/lib/smithy-client/auth.rb b/gems/smithy-client/lib/smithy-client/auth.rb index 50eb6e6d4..87d56a20e 100644 --- a/gems/smithy-client/lib/smithy-client/auth.rb +++ b/gems/smithy-client/lib/smithy-client/auth.rb @@ -2,10 +2,11 @@ module Smithy module Client + # The result of resolving the authentication scheme for a request. + ResolvedAuth = Struct.new(:scheme_id, :signer, :signer_properties, :identity_provider, keyword_init: true) + # @api private module Auth - ResolvedAuth = Struct.new(:scheme_id, :signer, :signer_properties, :identity_provider, keyword_init: true) - class << self def resolve(context, endpoint_properties = {}) if endpoint_properties.key?('authSchemes') @@ -62,7 +63,7 @@ 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) + def resolve_auth_scheme(auth_schemes, auth_options) # rubocop:disable Metrics/MethodLength raise 'No auth options were resolved' if auth_options.empty? failures = [] diff --git a/gems/smithy-client/lib/smithy-client/plugins/resolve_auth.rb b/gems/smithy-client/lib/smithy-client/plugins/resolve_auth.rb index 1b8a93e6c..b8649c68e 100644 --- a/gems/smithy-client/lib/smithy-client/plugins/resolve_auth.rb +++ b/gems/smithy-client/lib/smithy-client/plugins/resolve_auth.rb @@ -17,7 +17,10 @@ class ResolveAuth < Plugin :auth_scheme_preference, doc_type: 'Array', rbs_type: 'Array[String]', - docstring: 'A list of preferred authentication schemes to use when making a request.' + docstring: <<~DOCS + An ordered list of preferred authentication schemes to use when making a request. + The items in the list must be a fully qualified scheme IDs, such as `smithy.api#httpBearerAuth`. + DOCS ) do [] end diff --git a/gems/smithy-client/sig/smithy-client/handler_context.rbs b/gems/smithy-client/sig/smithy-client/handler_context.rbs index 9f75355e2..953f33fde 100644 --- a/gems/smithy-client/sig/smithy-client/handler_context.rbs +++ b/gems/smithy-client/sig/smithy-client/handler_context.rbs @@ -9,7 +9,7 @@ module Smithy attr_accessor config: Struct[untyped]? attr_accessor http_request: Http::Request | untyped attr_accessor http_response: Http::Response | untyped - attr_accessor auth: AuthScheme + attr_accessor auth: ResolvedAuth attr_accessor retries: Integer attr_reader metadata: Hash[untyped, untyped] def []: (untyped) -> untyped diff --git a/gems/smithy-client/sig/smithy-client/auth_scheme.rbs b/gems/smithy-client/sig/smithy-client/resolved_auth.rbs similarity index 74% rename from gems/smithy-client/sig/smithy-client/auth_scheme.rbs rename to gems/smithy-client/sig/smithy-client/resolved_auth.rbs index 1a7e72916..f590cfeed 100644 --- a/gems/smithy-client/sig/smithy-client/auth_scheme.rbs +++ b/gems/smithy-client/sig/smithy-client/resolved_auth.rbs @@ -1,10 +1,11 @@ module Smithy module Client - class AuthScheme + class ResolvedAuth def initialize: (?Hash[Symbol, untyped] options) -> void attr_reader identity_provider: IdentityProvider? attr_reader scheme_id: String attr_reader signer: _Signer + attr_reader signer_properties: Hash[String, String] end end end 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 74b03a225..6bb86ee8d 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 @@ -38,7 +38,7 @@ module Plugins it 'supports anonymous auth' do resp = client.operation - expect(resp.context.auth).to be_a(Auth::ResolvedAuth) + expect(resp.context.auth).to be_a(ResolvedAuth) expect(resp.context.auth.scheme_id).to eq('smithy.api#noAuth') end @@ -46,7 +46,7 @@ module Plugins shapes['smithy.ruby.tests#SampleClient']['traits']['smithy.api#httpApiKeyAuth'] = {} client_class.add_plugin(HttpApiKeyAuth) resp = client.operation - expect(resp.context.auth).to be_a(Auth::ResolvedAuth) + expect(resp.context.auth).to be_a(ResolvedAuth) expect(resp.context.auth.scheme_id).to eq('smithy.api#httpApiKeyAuth') end @@ -54,7 +54,7 @@ module Plugins shapes['smithy.ruby.tests#SampleClient']['traits']['smithy.api#httpBasicAuth'] = {} client_class.add_plugin(HttpBasicAuth) resp = client.operation - expect(resp.context.auth).to be_a(Auth::ResolvedAuth) + expect(resp.context.auth).to be_a(ResolvedAuth) expect(resp.context.auth.scheme_id).to eq('smithy.api#httpBasicAuth') end @@ -62,7 +62,7 @@ module Plugins shapes['smithy.ruby.tests#SampleClient']['traits']['smithy.api#httpBearerAuth'] = {} client_class.add_plugin(HttpBearerAuth) resp = client.operation - expect(resp.context.auth).to be_a(Auth::ResolvedAuth) + expect(resp.context.auth).to be_a(ResolvedAuth) expect(resp.context.auth.scheme_id).to eq('smithy.api#httpBearerAuth') end From 314db9e5fabe556ac70c61b8f516ed906ef904c4 Mon Sep 17 00:00:00 2001 From: Matt Muller Date: Tue, 30 Sep 2025 15:02:20 -0400 Subject: [PATCH 7/7] Regenerate --- projections/shapes/lib/shapes/auth_resolver.rb | 2 +- projections/shapes/lib/shapes/client.rb | 5 ++++- projections/weather/lib/weather/auth_resolver.rb | 2 +- projections/weather/lib/weather/client.rb | 5 ++++- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/projections/shapes/lib/shapes/auth_resolver.rb b/projections/shapes/lib/shapes/auth_resolver.rb index c3f060b0e..d2bb7761a 100644 --- a/projections/shapes/lib/shapes/auth_resolver.rb +++ b/projections/shapes/lib/shapes/auth_resolver.rb @@ -6,7 +6,7 @@ module ShapeService # Resolves the auth scheme from {AuthParameters}. class AuthResolver # @param [AuthParameters] parameters - # @return [Hash] + # @return [Array] def resolve(parameters) options = [] options << { scheme_id: 'smithy.api#noAuth' } diff --git a/projections/shapes/lib/shapes/client.rb b/projections/shapes/lib/shapes/client.rb index 108dcf22f..446574eea 100644 --- a/projections/shapes/lib/shapes/client.rb +++ b/projections/shapes/lib/shapes/client.rb @@ -18,6 +18,7 @@ require 'smithy-client/plugins/resolve_auth' require 'smithy-client/plugins/response_target' require 'smithy-client/plugins/retry_errors' +require 'smithy-client/plugins/sign_requests' require 'smithy-client/plugins/stub_responses' require 'smithy-client/plugins/user_agent' @@ -45,6 +46,7 @@ class Client < Smithy::Client::Base add_plugin(Smithy::Client::Plugins::ResolveAuth) add_plugin(Smithy::Client::Plugins::ResponseTarget) add_plugin(Smithy::Client::Plugins::RetryErrors) + add_plugin(Smithy::Client::Plugins::SignRequests) add_plugin(Smithy::Client::Plugins::StubResponses) add_plugin(Smithy::Client::Plugins::UserAgent) @@ -56,7 +58,8 @@ class Client < Smithy::Client::Base # @option options [#resolve(context)] :auth_resolver (AuthResolver.new) # An object that resolves authentication schemes for request signing. # @option options [Array] :auth_scheme_preference - # A list of preferred authentication schemes to use when making a request. + # An ordered list of preferred authentication schemes to use when making a request. + # The items in the list must be a fully qualified scheme IDs, such as `smithy.api#httpBearerAuth`. # @option options [Boolean] :convert_params (true) # When `true`, request parameters are coerced into the required types. # @option options [Boolean] :disable_host_prefix_injection diff --git a/projections/weather/lib/weather/auth_resolver.rb b/projections/weather/lib/weather/auth_resolver.rb index 75121567a..b8e2d46c8 100644 --- a/projections/weather/lib/weather/auth_resolver.rb +++ b/projections/weather/lib/weather/auth_resolver.rb @@ -6,7 +6,7 @@ module Weather # Resolves the auth scheme from {AuthParameters}. class AuthResolver # @param [AuthParameters] parameters - # @return [Hash] + # @return [Array] def resolve(parameters) options = [] options << { scheme_id: 'smithy.api#noAuth' } diff --git a/projections/weather/lib/weather/client.rb b/projections/weather/lib/weather/client.rb index 0a1413c15..103cc9d4d 100644 --- a/projections/weather/lib/weather/client.rb +++ b/projections/weather/lib/weather/client.rb @@ -18,6 +18,7 @@ require 'smithy-client/plugins/resolve_auth' require 'smithy-client/plugins/response_target' require 'smithy-client/plugins/retry_errors' +require 'smithy-client/plugins/sign_requests' require 'smithy-client/plugins/stub_responses' require 'smithy-client/plugins/user_agent' @@ -45,6 +46,7 @@ class Client < Smithy::Client::Base add_plugin(Smithy::Client::Plugins::ResolveAuth) add_plugin(Smithy::Client::Plugins::ResponseTarget) add_plugin(Smithy::Client::Plugins::RetryErrors) + add_plugin(Smithy::Client::Plugins::SignRequests) add_plugin(Smithy::Client::Plugins::StubResponses) add_plugin(Smithy::Client::Plugins::UserAgent) @@ -56,7 +58,8 @@ class Client < Smithy::Client::Base # @option options [#resolve(context)] :auth_resolver (AuthResolver.new) # An object that resolves authentication schemes for request signing. # @option options [Array] :auth_scheme_preference - # A list of preferred authentication schemes to use when making a request. + # An ordered list of preferred authentication schemes to use when making a request. + # The items in the list must be a fully qualified scheme IDs, such as `smithy.api#httpBearerAuth`. # @option options [Boolean] :convert_params (true) # When `true`, request parameters are coerced into the required types. # @option options [Boolean] :disable_host_prefix_injection