From d8863e389aa6f734757d314b078e21407936f7b3 Mon Sep 17 00:00:00 2001 From: Matt Muller Date: Tue, 22 Aug 2023 20:26:57 -0400 Subject: [PATCH] Implement some signers and tests --- hearth/lib/hearth/http/request.rb | 3 +- hearth/lib/hearth/signers.rb | 2 +- hearth/lib/hearth/signers/anonymous.rb | 2 +- hearth/lib/hearth/signers/http_api_key.rb | 11 +- hearth/lib/hearth/signers/http_basic.rb | 11 +- hearth/lib/hearth/signers/http_bearer.rb | 7 +- hearth/lib/hearth/signers/http_digest.rb | 6 +- hearth/spec/hearth/http/field_spec.rb | 12 ++ hearth/spec/hearth/http/fields_spec.rb | 11 ++ hearth/spec/hearth/http/request_spec.rb | 15 +++ hearth/spec/hearth/middleware/sign_spec.rb | 103 ++++++++++++++++++ hearth/spec/hearth/request_spec.rb | 10 ++ .../spec/hearth/signers/http_api_key_spec.rb | 43 ++++++++ hearth/spec/hearth/signers/http_basic_spec.rb | 26 +++++ .../spec/hearth/signers/http_bearer_spec.rb | 22 ++++ 15 files changed, 273 insertions(+), 11 deletions(-) create mode 100644 hearth/spec/hearth/middleware/sign_spec.rb create mode 100644 hearth/spec/hearth/signers/http_api_key_spec.rb create mode 100644 hearth/spec/hearth/signers/http_basic_spec.rb create mode 100644 hearth/spec/hearth/signers/http_bearer_spec.rb diff --git a/hearth/lib/hearth/http/request.rb b/hearth/lib/hearth/http/request.rb index 6ae3d6126..77a0c71e5 100755 --- a/hearth/lib/hearth/http/request.rb +++ b/hearth/lib/hearth/http/request.rb @@ -134,10 +134,11 @@ def prefix_host(prefix) # @api private def initialize_copy(other) - @http_method = other.http_method.dup + @http_method = other.http_method @fields = other.fields.dup @headers = Fields::Proxy.new(@fields, :header) @trailers = Fields::Proxy.new(@fields, :trailer) + super end end end diff --git a/hearth/lib/hearth/signers.rb b/hearth/lib/hearth/signers.rb index 2ba1ce8de..482e43d20 100644 --- a/hearth/lib/hearth/signers.rb +++ b/hearth/lib/hearth/signers.rb @@ -5,7 +5,7 @@ module Signers # Base class for all Signer classes. class Base def sign(request:, identity:, properties: {}) - raise NotImplementedError + # Do nothing by default end end end diff --git a/hearth/lib/hearth/signers/anonymous.rb b/hearth/lib/hearth/signers/anonymous.rb index f205674da..7f85bca9d 100644 --- a/hearth/lib/hearth/signers/anonymous.rb +++ b/hearth/lib/hearth/signers/anonymous.rb @@ -4,7 +4,7 @@ module Hearth module Signers # A signer that does not sign requests. class Anonymous < Signers::Base - def sign(request:, identity:, properties: {}) + def sign(request:, identity:, properties:) # Do nothing. end end diff --git a/hearth/lib/hearth/signers/http_api_key.rb b/hearth/lib/hearth/signers/http_api_key.rb index 9a0bc6413..38748b2c2 100644 --- a/hearth/lib/hearth/signers/http_api_key.rb +++ b/hearth/lib/hearth/signers/http_api_key.rb @@ -4,8 +4,15 @@ module Hearth module Signers # A signer that signs requests using the HTTP API Key scheme. class HTTPApiKey < Signers::Base - def sign(request:, identity:, properties: {}) - # TODO + def sign(request:, identity:, properties:) + case properties[:in] + when 'header' + value = "#{properties[:scheme]} #{identity.key}".strip + request.headers[properties[:name]] = value + when 'query' + name = properties[:name] + request.append_query_param(name, identity.key) + end end end end diff --git a/hearth/lib/hearth/signers/http_basic.rb b/hearth/lib/hearth/signers/http_basic.rb index 50dcf68fb..6592a2add 100644 --- a/hearth/lib/hearth/signers/http_basic.rb +++ b/hearth/lib/hearth/signers/http_basic.rb @@ -1,12 +1,19 @@ # frozen_string_literal: true +require 'base64' + module Hearth module Signers # A signer that signs requests using the HTTP Basic Auth scheme. class HTTPBasic < Signers::Base - def sign(request:, identity:, properties: {}) - # TODO + # rubocop:disable Lint/UnusedMethodArgument + def sign(request:, identity:, properties:) + # TODO: does not handle realm or other properties + identity_string = "#{identity.username}:#{identity.password}" + encoded = Base64.strict_encode64(identity_string) + request.headers['Authorization'] = "Basic #{encoded}" end + # rubocop:enable Lint/UnusedMethodArgument end end end diff --git a/hearth/lib/hearth/signers/http_bearer.rb b/hearth/lib/hearth/signers/http_bearer.rb index 2e5a8c248..8da00e993 100644 --- a/hearth/lib/hearth/signers/http_bearer.rb +++ b/hearth/lib/hearth/signers/http_bearer.rb @@ -4,9 +4,12 @@ module Hearth module Signers # A signer that signs requests using the HTTP Bearer scheme. class HTTPBearer < Signers::Base - def sign(request:, identity:, properties: {}) - # TODO + # rubocop:disable Lint/UnusedMethodArgument + def sign(request:, identity:, properties:) + # TODO: does not handle realm or other properties + request.headers['Authorization'] = "Bearer #{identity.token}" end + # rubocop:enable Lint/UnusedMethodArgument end end end diff --git a/hearth/lib/hearth/signers/http_digest.rb b/hearth/lib/hearth/signers/http_digest.rb index 8cb02e35b..583ba65bf 100644 --- a/hearth/lib/hearth/signers/http_digest.rb +++ b/hearth/lib/hearth/signers/http_digest.rb @@ -4,8 +4,10 @@ module Hearth module Signers # A signer that signs requests using the HTTP Digest Auth scheme. class HTTPDigest < Signers::Base - def sign(request:, identity:, properties: {}) - # TODO + def sign(request:, identity:, properties:) + # TODO: requires a nonce from the server - this cannot + # be implemented unless we rescue from a 401 and retry + # with the nonce end end end diff --git a/hearth/spec/hearth/http/field_spec.rb b/hearth/spec/hearth/http/field_spec.rb index a0563794b..34011d68c 100644 --- a/hearth/spec/hearth/http/field_spec.rb +++ b/hearth/spec/hearth/http/field_spec.rb @@ -86,6 +86,18 @@ module HTTP expect(header.to_h).to eq('X-Header' => 'foo') end end + + describe '#dup' do + it 'returns a copy of the field' do + copy = header.dup + expect(copy.name).to eq(header.name) + expect(copy.name).not_to equal(header.name) + expect(copy.value).to eq(header.value) + expect(copy.value).not_to equal(header.value) + # Symbols are not duped + expect(copy.kind).to eq(header.kind) + end + end end end end diff --git a/hearth/spec/hearth/http/fields_spec.rb b/hearth/spec/hearth/http/fields_spec.rb index 56e1d715b..16436c5ec 100644 --- a/hearth/spec/hearth/http/fields_spec.rb +++ b/hearth/spec/hearth/http/fields_spec.rb @@ -89,6 +89,17 @@ module HTTP end end + describe '#dup' do + it 'duplicates the fields' do + copy = fields.dup + expect(copy['x-header'].value).to eq(fields['x-header'].value) + expect(copy['x-trailer'].value).to eq(fields['x-trailer'].value) + expect(copy['x-header']).not_to equal(fields['x-header']) + expect(copy['x-trailer']).not_to equal(fields['x-trailer']) + expect(copy).not_to equal(fields) + end + end + describe Fields::Proxy do let(:proxy) { Fields::Proxy.new(fields, :header) } diff --git a/hearth/spec/hearth/http/request_spec.rb b/hearth/spec/hearth/http/request_spec.rb index e5044a2a3..51222e7ac 100644 --- a/hearth/spec/hearth/http/request_spec.rb +++ b/hearth/spec/hearth/http/request_spec.rb @@ -132,6 +132,21 @@ module HTTP expect(subject.uri.to_s).to eq('http://data.example.com') end end + + describe '#dup' do + before { subject.headers['name'] = 'value' } + + it 'duplicates the request' do + request = subject.dup + # Symbols are not duped + expect(request.http_method).to eq(http_method) + expect(request.uri).to eq(uri) + expect(request.uri).not_to equal(uri) + expect(request.fields['name'].value).to eq('value') + expect(request.fields).not_to equal(fields) + expect(request.body).to eq(body) + end + end end end end diff --git a/hearth/spec/hearth/middleware/sign_spec.rb b/hearth/spec/hearth/middleware/sign_spec.rb new file mode 100644 index 000000000..7751e3460 --- /dev/null +++ b/hearth/spec/hearth/middleware/sign_spec.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +module Hearth + module Middleware + describe Build do + let(:app) { double('app', call: output) } + + subject { Sign.new(app) } + + describe '#call' do + let(:input) { double('input') } + let(:output) { double('output') } + let(:request) { double('request') } + let(:response) { double('response') } + let(:interceptors) { double('interceptors', apply: nil) } + let(:context) do + Context.new( + request: request, + response: response, + interceptors: interceptors + ) + end + + let(:signer) { double('signer', sign: nil) } + let(:identity) { double('identity') } + let(:auth_option) { double('auth_option', signer_properties: {}) } + let(:auth_scheme) do + double( + 'auth_scheme', + signer: signer, + identity: identity, + auth_option: auth_option + ) + end + + before { context.auth_scheme = auth_scheme } + + it 'signs then calls the next middleware' do + expect(signer).to receive(:sign) + .with(request: request, identity: identity, properties: {}) + .ordered + expect(app).to receive(:call).with(input, context).ordered + + resp = subject.call(input, context) + expect(resp).to be output + end + + it 'calls before_signing interceptors before sign' do + expect(interceptors).to receive(:apply) + .with(hash_including( + hook: Interceptor::Hooks::MODIFY_BEFORE_SIGNING + )).ordered + expect(interceptors).to receive(:apply) + .with(hash_including( + hook: Interceptor::Hooks::READ_BEFORE_SIGNING + )).ordered + expect(signer).to receive(:sign).ordered + expect(app).to receive(:call).ordered + + subject.call(input, context) + end + + context 'modify_before_signing error' do + let(:error) { StandardError.new } + + it 'returns output with the error and skips signing' do + expect(interceptors).to receive(:apply) + .with(hash_including( + hook: Interceptor::Hooks::MODIFY_BEFORE_SIGNING + )) + .and_return(error) + + expect(signer).not_to receive(:sign) + expect(app).not_to receive(:call) + + resp = subject.call(input, context) + + expect(resp.error).to eq(error) + end + end + + context 'read_before_signing error' do + let(:error) { StandardError.new } + + it 'returns output with the error and skips signing' do + expect(interceptors).to receive(:apply) + .with(hash_including( + hook: Interceptor::Hooks::READ_BEFORE_SIGNING + )) + .and_return(error) + + expect(signer).not_to receive(:sign) + expect(app).not_to receive(:call) + + resp = subject.call(input, context) + + expect(resp.error).to eq(error) + end + end + end + end + end +end diff --git a/hearth/spec/hearth/request_spec.rb b/hearth/spec/hearth/request_spec.rb index 6493ee6f1..86bea2814 100644 --- a/hearth/spec/hearth/request_spec.rb +++ b/hearth/spec/hearth/request_spec.rb @@ -14,5 +14,15 @@ module Hearth expect(request.uri).to be_a(URI) end end + + describe '#dup' do + it 'duplicates the request' do + request = subject.dup + expect(request.uri).to eq(uri) + expect(request.uri).not_to equal(uri) + # Body is not copied + expect(request.body).to eq(body) + end + end end end diff --git a/hearth/spec/hearth/signers/http_api_key_spec.rb b/hearth/spec/hearth/signers/http_api_key_spec.rb new file mode 100644 index 000000000..d757f4371 --- /dev/null +++ b/hearth/spec/hearth/signers/http_api_key_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module Hearth + module Signers + describe HTTPApiKey do + let(:request) { HTTP::Request.new } + let(:identity) { Identities::HTTPApiKey.new(key: key) } + let(:key) { 'key' } + + context 'header' do + let(:properties) do + { name: 'X-Api-Key', in: 'header', scheme: 'OAuth' } + end + + it 'signs the request' do + expect(request.headers['x-api-key']).to be_nil + subject.sign( + request: request, + identity: identity, + properties: properties + ) + expect(request.headers['x-api-key']).to eq("OAuth #{key}") + end + end + + context 'query' do + let(:properties) do + { name: 'api_key', in: 'query' } + end + + it 'signs the request' do + expect(request.uri.query).to be_nil + subject.sign( + request: request, + identity: identity, + properties: properties + ) + expect(request.uri.query).to eq("api_key=#{key}") + end + end + end + end +end diff --git a/hearth/spec/hearth/signers/http_basic_spec.rb b/hearth/spec/hearth/signers/http_basic_spec.rb new file mode 100644 index 000000000..2ece84cf8 --- /dev/null +++ b/hearth/spec/hearth/signers/http_basic_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Hearth + module Signers + describe HTTPBasic do + let(:request) { HTTP::Request.new } + let(:identity) do + Identities::HTTPLogin.new(username: username, password: password) + end + let(:username) { 'username' } + let(:password) { 'password' } + let(:properties) { {} } + + it 'signs the request' do + expect(request.headers['authorization']).to be_nil + subject.sign( + request: request, + identity: identity, + properties: properties + ) + encoded = Base64.strict_encode64("#{username}:#{password}") + expect(request.headers['authorization']).to eq("Basic #{encoded}") + end + end + end +end diff --git a/hearth/spec/hearth/signers/http_bearer_spec.rb b/hearth/spec/hearth/signers/http_bearer_spec.rb new file mode 100644 index 000000000..9bb0cdc40 --- /dev/null +++ b/hearth/spec/hearth/signers/http_bearer_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Hearth + module Signers + describe HTTPBearer do + let(:request) { HTTP::Request.new } + let(:identity) { Identities::HTTPBearer.new(token: token) } + let(:token) { 'token' } + let(:properties) { {} } + + it 'signs the request' do + expect(request.headers['authorization']).to be_nil + subject.sign( + request: request, + identity: identity, + properties: properties + ) + expect(request.headers['authorization']).to eq("Bearer #{token}") + end + end + end +end