-
Notifications
You must be signed in to change notification settings - Fork 7
Endpoints auth #328
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Endpoints auth #328
Changes from 6 commits
c885c23
427c44e
4969009
cac85b6
66ad306
7c58f10
a4a24b3
314db9e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| 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.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 } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm assuming we'll need to make changes to sigv4 auth in V4 as well following these changes. Have you tested with V4?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I tested with v4 and I opened a separate PR. |
||
| 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) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If I'm understanding this correctly, endpoint auth completely overrides the modeled auths?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah. I think we should do it that way. It's less complex and I think endpoint auth is guaranteed to work. |
||
| 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) | ||
| 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 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are these intended to be public interfaces - if so, we should include more documentation about init options? Here, other signers and auth scheme.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I intended them to be API private for now. I'm also not sure yet so I'm going to wait on documentation.