diff --git a/gems/smithy-client/lib/smithy-client.rb b/gems/smithy-client/lib/smithy-client.rb index 0637aa9df..00156cec6 100644 --- a/gems/smithy-client/lib/smithy-client.rb +++ b/gems/smithy-client/lib/smithy-client.rb @@ -50,7 +50,10 @@ # 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 new file mode 100644 index 000000000..87d56a20e --- /dev/null +++ b/gems/smithy-client/lib/smithy-client/auth.rb @@ -0,0 +1,107 @@ +# frozen_string_literal: true + +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 + class << self + def resolve(context, endpoint_properties = {}) + if endpoint_properties.key?('authSchemes') + resolve_with_endpoint_auth(context.config, endpoint_properties['authSchemes']) + else + 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(config, endpoint_auth_schemes) + endpoint_auth_schemes_map = config.endpoint_auth_schemes + normalized_endpoint_schemes = [] + endpoint_auth_schemes.each do |scheme| + normalized_scheme_id = endpoint_auth_schemes_map[scheme['name']] + next unless normalized_scheme_id + + properties = {} + scheme.each do |key, value| + next if key == 'name' + + properties[key] = value + end + normalized_endpoint_schemes << { scheme_id: normalized_scheme_id, signer_properties: properties } + end + resolved_auth_options = prioritize_auth_options(normalized_endpoint_schemes, config.auth_scheme_preference) + resolve_auth_scheme(config.auth_schemes, resolved_auth_options) + end + + 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) + 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) # rubocop:disable Metrics/MethodLength + raise 'No auth options were resolved' if auth_options.empty? + + failures = [] + auth_options.each do |auth_option| + scheme_id = auth_option[:scheme_id] + if scheme_id == 'smithy.api#noAuth' + return ResolvedAuth.new( + scheme_id: 'smithy.api#noAuth', signer: NullSigner.new, signer_properties: {}, + identity_provider: nil + ) + end + auth_scheme = auth_schemes[scheme_id] + error = validate_auth_scheme(auth_scheme, scheme_id) + unless error + return ResolvedAuth.new( + scheme_id: scheme_id, + signer: auth_scheme.signer, + signer_properties: auth_option[:signer_properties] || {}, + identity_provider: auth_scheme.identity_provider + ) + end + + failures << error + end + + raise failures.join("\n") + end + + 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_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? + + nil + end + end + end + end +end 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..b8b70e07e --- /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 configured authentication schemes. + class AuthScheme + def initialize(options = {}) + @scheme_id = options[:scheme_id] + @identity_provider = options[:identity_provider] + @signer = options[:signer] + end + + # @return [String] + attr_reader :scheme_id + + # @return [IdentityProvider] + attr_reader :identity_provider + + # @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 9cda614c2..b8649c68e 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,21 @@ 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: <<~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 + option(:auth_schemes) { {} } def before_initialize(client_class, options) @@ -23,47 +34,12 @@ 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(context, context[:resolved_endpoint].properties) @handler.call(context) end - - private - - def resolve_auth(context, 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(context, scheme_id) - return auth_option unless error - - failures << error - end - - raise failures.join("\n") - end - - def validate_auth_scheme(context, scheme_id) - unless context.config.auth_schemes.key?(scheme_id) - return "Auth scheme #{scheme_id} was not enabled for this request" - end - - identity_provider = context.config.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 - 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/lib/smithy-client/plugins/stub_responses.rb b/gems/smithy-client/lib/smithy-client/plugins/stub_responses.rb index f93b7e05d..f24d4719f 100644 --- a/gems/smithy-client/lib/smithy-client/plugins/stub_responses.rb +++ b/gems/smithy-client/lib/smithy-client/plugins/stub_responses.rb @@ -31,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/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 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/handler_context.rbs b/gems/smithy-client/sig/smithy-client/handler_context.rbs index fb6744afc..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: Hash[Symbol, untyped] + 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/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 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/sig/smithy-client/resolved_auth.rbs b/gems/smithy-client/sig/smithy-client/resolved_auth.rbs new file mode 100644 index 000000000..f590cfeed --- /dev/null +++ b/gems/smithy-client/sig/smithy-client/resolved_auth.rbs @@ -0,0 +1,11 @@ +module Smithy + module Client + 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/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 f1ed6673d..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 @@ -15,61 +15,113 @@ module Plugins expect(client.config).to respond_to(:auth_resolver) end - it 'adds scheme ids to auth scheme config hash' do + it 'adds an :auth_scheme_preference option to config' do + expect(client.config).to respond_to(:auth_scheme_preference) + end + + 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 'resolves auth for anonymous auth' do + 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(ResolvedAuth) + 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).to eq({ scheme_id: 'smithy.api#httpApiKeyAuth' }) + expect(resp.context.auth).to be_a(ResolvedAuth) + expect(resp.context.auth.scheme_id).to eq('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).to eq({ scheme_id: 'smithy.api#httpBasicAuth' }) + expect(resp.context.auth).to be_a(ResolvedAuth) + expect(resp.context.auth.scheme_id).to eq('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).to eq({ scheme_id: 'smithy.api#httpBearerAuth' }) + expect(resp.context.auth).to be_a(ResolvedAuth) + expect(resp.context.auth.scheme_id).to eq('smithy.api#httpBearerAuth') end - it 'resolves auth for 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) + it 'resolves the first supported auth scheme' 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) + resp = client.operation + expect(resp.context.auth.scheme_id).to eq('smithy.api#httpBearerAuth') end - it 'resolves the first supported auth scheme' do + 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#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) + client = client_class.new(stub_responses: true, bearer_token_provider: nil) + resp = client.operation + expect(resp.context.auth.scheme_id).to eq('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 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).to eq({ scheme_id: 'smithy.api#httpBasicAuth' }) + 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) auth_resolver = Class.new do def resolve(_) @@ -86,19 +138,158 @@ def resolve(_) 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, 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) - client = client_class.new(stub_responses: true, api_key_provider: ApiKeyProvider.new({})) + 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.scheme_id).to eq('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.scheme_id).to eq('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.scheme_id).to eq('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.scheme_id).to eq('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.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.signer_properties).to eq('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.scheme_id).to eq('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.scheme_id).to eq('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.scheme_id).to eq('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/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 %> 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/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/default_plugins.rb b/gems/smithy/lib/smithy/welds/default_plugins.rb index 38cc255f8..2e8a65804 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' @@ -27,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" }, @@ -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" } } 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/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 6d4a334c0..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) @@ -54,7 +56,10 @@ 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 + # 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 @@ -63,8 +68,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/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 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 5f6531af5..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) @@ -54,7 +56,10 @@ 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 + # 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 @@ -63,8 +68,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,