Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,26 @@
- name: Test
run: bundle exec rake smithy:spec

smithy-rbs:
needs: [smithy]
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ env.latest_ruby_version }}
bundler-cache: true

- name: Install rbs collection
run: rbs collection install

- name: RBS
run: bundle exec rake smithy:rbs

smithy-schema:

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}
uses: ./.github/workflows/test.yml
with:
gem-name: 'smithy-schema'
Expand Down
1 change: 1 addition & 0 deletions gems/smithy-client/lib/smithy-client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
# identity and auth

require_relative 'smithy-client/auth'
require_relative 'smithy-client/auth_option'
require_relative 'smithy-client/auth_scheme'
require_relative 'smithy-client/identity_provider'
require_relative 'smithy-client/null_signer'
Expand Down
7 changes: 7 additions & 0 deletions gems/smithy-client/lib/smithy-client/api_key_signer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ module Smithy
module Client
# Signs requests with the ApiKey identity.
class ApiKeySigner
# @param [Hash] options
# @option options [String] :name
# The name of the header or query parameter to which the API key value will be assigned.
# @option options [String] :in
# Where to place the API key value. Valid values are "header" and "query".
# @option options [String] :scheme
# An optional scheme to be prepended to the API key value when signing in a header.
def initialize(options = {})
@name = options[:name]
@in = options[:in]
Expand Down
22 changes: 11 additions & 11 deletions gems/smithy-client/lib/smithy-client/auth.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ def resolve_with_endpoint_auth(config, endpoint_auth_schemes)

properties[key] = value
end
normalized_endpoint_schemes << { scheme_id: normalized_scheme_id, signer_properties: properties }
normalized_endpoint_schemes << AuthOption.new(
scheme_id: normalized_scheme_id,
signer_properties: properties
)
end
resolved_auth_options = prioritize_auth_options(normalized_endpoint_schemes, config.auth_scheme_preference)
resolve_auth_scheme(config.auth_schemes, resolved_auth_options)
Expand All @@ -49,7 +52,7 @@ def prioritize_auth_options(auth_options, auth_scheme_preference)

auth_options_by_id = {}
auth_options.each do |option|
auth_options_by_id[option[:scheme_id]] = option
auth_options_by_id[option.scheme_id] = option
end

preferred_options = []
Expand All @@ -63,25 +66,22 @@ def prioritize_auth_options(auth_options, auth_scheme_preference)
preferred_options.empty? ? auth_options : preferred_options
end

def resolve_auth_scheme(auth_schemes, auth_options) # rubocop:disable Metrics/MethodLength
def resolve_auth_scheme(auth_schemes, auth_options)
raise 'No auth options were resolved' if auth_options.empty?

failures = []
auth_options.each do |auth_option|
scheme_id = auth_option[:scheme_id]
if scheme_id == 'smithy.api#noAuth'
return ResolvedAuth.new(
scheme_id: 'smithy.api#noAuth', signer: NullSigner.new, signer_properties: {},
identity_provider: nil
)
end
scheme_id = auth_option.scheme_id
return ResolvedAuth.new(scheme_id: scheme_id, signer: NullSigner.new) if scheme_id == 'smithy.api#noAuth'

auth_scheme = auth_schemes[scheme_id]
error = validate_auth_scheme(auth_scheme, scheme_id)

unless error
return ResolvedAuth.new(
scheme_id: scheme_id,
signer: auth_scheme.signer,
signer_properties: auth_option[:signer_properties] || {},
signer_properties: auth_option.signer_properties,
identity_provider: auth_scheme.identity_provider
)
end
Expand Down
24 changes: 24 additions & 0 deletions gems/smithy-client/lib/smithy-client/auth_option.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# frozen_string_literal: true

module Smithy
module Client
# Contains information about candidate authentication schemes.
class AuthOption
# @param [Hash] options
# @option options [String] :scheme_id The auth scheme ID.
# @option options [Hash] :signer_properties
# Additional properties to pass to the signer.
# This is a hash of String keys to String values and is signer-specific.
def initialize(options = {})
@scheme_id = options[:scheme_id]
@signer_properties = options[:signer_properties] || {}
end

# @return [String]
attr_reader :scheme_id

# @return [Hash]
attr_reader :signer_properties
end
end
end
7 changes: 5 additions & 2 deletions gems/smithy-client/lib/smithy-client/login_signer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ module Smithy
module Client
# Signs requests with the Login identity.
class LoginSigner
# @param [Hash] options
# @option options [Symbol] :type
# The type of login authentication. Valid values are: :http_basic.
def initialize(options = {})
@scheme_id = options[:scheme_id]
@type = options[:type]
end

def sign_request(context)
raise NotImplementedError unless @scheme_id == 'smithy.api#httpBasicAuth'
raise NotImplementedError unless @type == :http_basic

sign_with_basic(context.http_request, context.config.login_provider)
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ class that responds to #identity and returns a {Smithy::Client::ApiKey}.
provider if provider.set?
end

option(:api_key_signer) do |config|
option(
:api_key_signer,
doc_type: Smithy::Client::ApiKeySigner,
docstring: 'A signer class that signs requests with an API key.'
) do |config|
trait = config.service.traits['smithy.api#httpApiKeyAuth']
ApiKeySigner.new(name: trait['name'], in: trait['in'], scheme: trait['scheme'])
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,12 @@ class that responds to #identity and returns a {Smithy::Client::Login}.
provider if provider.set?
end

option(:login_signer) do |_config|
LoginSigner.new(scheme_id: 'smithy.api#httpBasicAuth')
option(
:login_signer,
doc_type: Smithy::Client::LoginSigner,
docstring: 'A signer class that signs requests with login credentials.'
) do |_config|
LoginSigner.new(type: :http_basic)
end

def after_initialize(client)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ class that responds to #identity and returns a {Smithy::Client::BearerToken}.
provider if provider.set?
end

option(:bearer_token_signer) do |_config|
option(
:bearer_token_signer,
doc_type: Smithy::Client::BearerTokenSigner,
docstring: 'A signer class that signs requests with a bearer token.'
) do |_config|
BearerTokenSigner.new
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,12 @@ class that responds to #identity and returns a {Smithy::Client::Login}.
provider if provider.set?
end

option(:login_signer) do |_config|
LoginSigner.new(scheme_id: 'smithy.api#httpDigestAuth')
option(
:login_signer,
doc_type: Smithy::Client::LoginSigner,
docstring: 'A signer class that signs requests with login credentials.'
) do |_config|
LoginSigner.new(type: :http_digest)
end

def after_initialize(client)
Expand Down
9 changes: 9 additions & 0 deletions gems/smithy-client/sig/smithy-client/auth_option.rbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module Smithy
module Client
module AuthOption
def initialize: (?Hash[Symbol, untyped] options) -> void
attr_reader scheme_id: String
attr_reader signer_properties: Hash[String, String]
end
end
end
6 changes: 3 additions & 3 deletions gems/smithy-client/sig/smithy-client/service_error.rbs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
module Smithy
module Client
class ServiceError < RuntimeError
def initialize: (HandlerContext, Schema::Structure) -> void
def initialize: (HandlerContext?, Schema::Structure?) -> void

attr_reader code: String?
attr_reader context: HandlerContext
attr_accessor data: untyped?
attr_reader context: HandlerContext?
attr_accessor data: Schema::Structure?

def self.code: () -> String?
def self.code=: (String) -> void
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,10 @@ module Plugins
auth_resolver = Class.new do
def resolve(_)
[
{
AuthOption.new(
scheme_id: 'smithy.api#httpApiKeyAuth',
signer_properties: { 'location' => 'query', 'name' => 'api_key' }
}
)
]
end
end
Expand Down
2 changes: 1 addition & 1 deletion gems/smithy/lib/smithy/templates/client/auth_resolver.erb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ module <%= module_name %>
# Resolves the auth scheme from {AuthParameters}.
class AuthResolver
# @param [AuthParameters] parameters
# @return [Array<Hash>]
# @return [Array<Smithy::Client::AuthOption>]
def resolve(parameters)
<% auth_rules_code.each do |line| -%>
<%= line %>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module <%= module_name %>
class AuthResolver
def resolve: (AuthParameters) -> Array[String]
def resolve: (AuthParameters) -> Array[Smithy::Client::AuthOption]
end
end
4 changes: 2 additions & 2 deletions gems/smithy/lib/smithy/views/client/auth_resolver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def auth_rules_code

def add_service_auth_schemes_to_code(lines)
service_auth_schemes.each do |auth_scheme|
lines << "options << { scheme_id: '#{auth_scheme}' }"
lines << "options << Smithy::Client::AuthOption.new(scheme_id: '#{auth_scheme}')"
end
end

Expand All @@ -53,7 +53,7 @@ def add_operation_case_to_code(lines, auth_operations)

def add_operation_auth_options_to_code(lines, operation)
operation_auth_schemes(operation).each do |auth_scheme|
lines << " options << { scheme_id: '#{auth_scheme}' }"
lines << " options << Smithy::Client::AuthOption.new(scheme_id: '#{auth_scheme}')"
end
end

Expand Down
55 changes: 29 additions & 26 deletions gems/smithy/spec/interfaces/client/auth_resolver_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,30 +16,30 @@
params = NoAuthTrait::AuthParameters.new(operation_name: :operation_a)
auth_options = subject.resolve(params)
expected = [
{ scheme_id: 'smithy.api#httpBasicAuth' },
{ scheme_id: 'smithy.api#httpBearerAuth' },
{ scheme_id: 'smithy.api#httpDigestAuth' }
'smithy.api#httpBasicAuth',
'smithy.api#httpBearerAuth',
'smithy.api#httpDigestAuth'
]
expect(auth_options).to eq(expected)
expect(auth_options.map(&:scheme_id)).to eq(expected)
end

it 'returns the auth options for the operation with the auth trait' do
params = NoAuthTrait::AuthParameters.new(operation_name: :operation_b)
auth_options = subject.resolve(params)
expect(auth_options).to eq([{ scheme_id: 'smithy.api#httpDigestAuth' }])
expected = ['smithy.api#httpDigestAuth']
expect(auth_options.map(&:scheme_id)).to eq(expected)
end

it 'returns the auth options for the operation with the optionalAuth trait' do
params = NoAuthTrait::AuthParameters.new(operation_name: :operation_g)
auth_options = subject.resolve(params)
expect(auth_options).to eq(
[
{ scheme_id: 'smithy.api#httpBasicAuth' },
{ scheme_id: 'smithy.api#httpBearerAuth' },
{ scheme_id: 'smithy.api#httpDigestAuth' },
{ scheme_id: 'smithy.api#noAuth' }
]
)
expected = [
'smithy.api#httpBasicAuth',
'smithy.api#httpBearerAuth',
'smithy.api#httpDigestAuth',
'smithy.api#noAuth'
]
expect(auth_options.map(&:scheme_id)).to eq(expected)
end
end
end
Expand All @@ -53,32 +53,33 @@
it 'returns the auth options with the service auth trait' do
params = AuthTrait::AuthParameters.new(operation_name: :operation_c)
auth_options = subject.resolve(params)
expected = [{ scheme_id: 'smithy.api#httpBasicAuth' }, { scheme_id: 'smithy.api#httpDigestAuth' }]
expect(auth_options).to eq(expected)
expected = ['smithy.api#httpBasicAuth', 'smithy.api#httpDigestAuth']
expect(auth_options.map(&:scheme_id)).to eq(expected)
end

it 'returns the auth options for the operation overriding the service auth trait' do
params = AuthTrait::AuthParameters.new(operation_name: :operation_d)
auth_options = subject.resolve(params)
expect(auth_options).to eq([{ scheme_id: 'smithy.api#httpBearerAuth' }])
expected = ['smithy.api#httpBearerAuth']
expect(auth_options.map(&:scheme_id)).to eq(expected)
end

it 'returns a noAuth option when the auth trait is empty' do
params = AuthTrait::AuthParameters.new(operation_name: :operation_e)
auth_options = subject.resolve(params)
expect(auth_options).to eq([{ scheme_id: 'smithy.api#noAuth' }])
expected = ['smithy.api#noAuth']
expect(auth_options.map(&:scheme_id)).to eq(expected)
end

it 'returns the auth options for the operation with the optionalAuth trait' do
params = AuthTrait::AuthParameters.new(operation_name: :operation_f)
auth_options = subject.resolve(params)
expect(auth_options).to eq(
[
{ scheme_id: 'smithy.api#httpBasicAuth' },
{ scheme_id: 'smithy.api#httpDigestAuth' },
{ scheme_id: 'smithy.api#noAuth' }
]
)
expected = [
'smithy.api#httpBasicAuth',
'smithy.api#httpDigestAuth',
'smithy.api#noAuth'
]
expect(auth_options.map(&:scheme_id)).to eq(expected)
end
end
end
Expand All @@ -92,13 +93,15 @@
it 'returns the auth options for the operation with no auth traits' do
params = NoAuth::AuthParameters.new(operation_name: :operation_h)
auth_options = subject.resolve(params)
expect(auth_options).to eq([{ scheme_id: 'smithy.api#noAuth' }])
expected = ['smithy.api#noAuth']
expect(auth_options.map(&:scheme_id)).to eq(expected)
end

it 'returns the auth options for the operation with the optionalAuth trait' do
params = NoAuth::AuthParameters.new(operation_name: :operation_i)
auth_options = subject.resolve(params)
expect(auth_options).to eq([{ scheme_id: 'smithy.api#noAuth' }])
expected = ['smithy.api#noAuth']
expect(auth_options.map(&:scheme_id)).to eq(expected)
end
end
end
Expand Down
4 changes: 2 additions & 2 deletions gems/smithy/spec/interfaces/welds/auth_schemes_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,14 @@ def remove_auth_schemes
auth_resolver = ServiceWithAuthTrait::AuthResolver.new
auth_parameters = ServiceWithAuthTrait::AuthParameters.new(operation_name: :operation_c)
resolved_auths = auth_resolver.resolve(auth_parameters)
expect(resolved_auths).to include({ scheme_id: 'smithy.api#httpBasicAuth' })
expect(resolved_auths.map(&:scheme_id)).to include('smithy.api#httpBasicAuth')
end

it 'removes auth schemes from the client' do
auth_resolver = ServiceWithAuthTrait::AuthResolver.new
auth_parameters = ServiceWithAuthTrait::AuthParameters.new(operation_name: :operation_d)
resolved_auths = auth_resolver.resolve(auth_parameters)
expect(resolved_auths).to_not include({ scheme_id: 'smithy.api#httpBearerAuth' })
expect(resolved_auths.map(&:scheme_id)).to_not include('smithy.api#httpBearerAuth')
end
end
end
Expand Down