Skip to content

Commit

Permalink
Implement some signers and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
mullermp committed Aug 23, 2023
1 parent a890fb3 commit d8863e3
Show file tree
Hide file tree
Showing 15 changed files with 273 additions and 11 deletions.
3 changes: 2 additions & 1 deletion hearth/lib/hearth/http/request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion hearth/lib/hearth/signers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion hearth/lib/hearth/signers/anonymous.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 9 additions & 2 deletions hearth/lib/hearth/signers/http_api_key.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 9 additions & 2 deletions hearth/lib/hearth/signers/http_basic.rb
Original file line number Diff line number Diff line change
@@ -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
7 changes: 5 additions & 2 deletions hearth/lib/hearth/signers/http_bearer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 4 additions & 2 deletions hearth/lib/hearth/signers/http_digest.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions hearth/spec/hearth/http/field_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
11 changes: 11 additions & 0 deletions hearth/spec/hearth/http/fields_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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) }

Expand Down
15 changes: 15 additions & 0 deletions hearth/spec/hearth/http/request_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
103 changes: 103 additions & 0 deletions hearth/spec/hearth/middleware/sign_spec.rb
Original file line number Diff line number Diff line change
@@ -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
10 changes: 10 additions & 0 deletions hearth/spec/hearth/request_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
43 changes: 43 additions & 0 deletions hearth/spec/hearth/signers/http_api_key_spec.rb
Original file line number Diff line number Diff line change
@@ -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
26 changes: 26 additions & 0 deletions hearth/spec/hearth/signers/http_basic_spec.rb
Original file line number Diff line number Diff line change
@@ -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
22 changes: 22 additions & 0 deletions hearth/spec/hearth/signers/http_bearer_spec.rb
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit d8863e3

Please sign in to comment.