castle-rb 2.3.2 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/README.md +55 -9
- data/lib/castle.rb +17 -7
- data/lib/castle/api.rb +20 -22
- data/lib/castle/client.rb +50 -19
- data/lib/castle/command.rb +5 -0
- data/lib/castle/commands/authenticate.rb +25 -0
- data/lib/castle/commands/identify.rb +30 -0
- data/lib/castle/commands/review.rb +13 -0
- data/lib/castle/commands/track.rb +25 -0
- data/lib/castle/commands/with_context.rb +28 -0
- data/lib/castle/configuration.rb +46 -14
- data/lib/castle/context_merger.rb +13 -0
- data/lib/castle/default_context.rb +28 -0
- data/lib/castle/errors.rb +2 -0
- data/lib/castle/extractors/client_id.rb +4 -14
- data/lib/castle/extractors/headers.rb +6 -18
- data/lib/castle/failover_auth_response.rb +21 -0
- data/lib/castle/header_formatter.rb +9 -0
- data/lib/castle/request.rb +7 -13
- data/lib/castle/response.rb +2 -0
- data/lib/castle/review.rb +11 -0
- data/lib/castle/secure_mode.rb +11 -0
- data/lib/castle/support/hanami.rb +19 -0
- data/lib/castle/support/padrino.rb +1 -1
- data/lib/castle/support/rails.rb +1 -1
- data/lib/castle/support/sinatra.rb +4 -2
- data/lib/castle/utils.rb +55 -0
- data/lib/castle/utils/cloner.rb +11 -0
- data/lib/castle/utils/merger.rb +23 -0
- data/lib/castle/version.rb +1 -1
- data/spec/lib/castle/api_spec.rb +16 -25
- data/spec/lib/castle/client_spec.rb +175 -39
- data/spec/lib/castle/command_spec.rb +9 -0
- data/spec/lib/castle/commands/authenticate_spec.rb +106 -0
- data/spec/lib/castle/commands/identify_spec.rb +85 -0
- data/spec/lib/castle/commands/review_spec.rb +24 -0
- data/spec/lib/castle/commands/track_spec.rb +107 -0
- data/spec/lib/castle/configuration_spec.rb +75 -27
- data/spec/lib/castle/context_merger_spec.rb +34 -0
- data/spec/lib/castle/default_context_spec.rb +35 -0
- data/spec/lib/castle/extractors/client_id_spec.rb +13 -5
- data/spec/lib/castle/extractors/headers_spec.rb +6 -5
- data/spec/lib/castle/extractors/ip_spec.rb +2 -9
- data/spec/lib/castle/header_formatter_spec.rb +21 -0
- data/spec/lib/castle/request_spec.rb +12 -9
- data/spec/lib/castle/response_spec.rb +1 -3
- data/spec/lib/castle/review_spec.rb +23 -0
- data/spec/lib/castle/secure_mode_spec.rb +9 -0
- data/spec/lib/castle/utils/cloner_spec.rb +18 -0
- data/spec/lib/castle/utils/merger_spec.rb +13 -0
- data/spec/lib/castle/utils_spec.rb +156 -0
- data/spec/lib/castle/version_spec.rb +1 -5
- data/spec/lib/castle_spec.rb +8 -15
- data/spec/spec_helper.rb +3 -9
- metadata +46 -12
- data/lib/castle/cookie_store.rb +0 -52
- data/lib/castle/headers.rb +0 -39
- data/lib/castle/support.rb +0 -11
- data/lib/castle/system.rb +0 -36
- data/spec/lib/castle/headers_spec.rb +0 -82
- data/spec/lib/castle/system_spec.rb +0 -70
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Castle
|
4
|
+
class ContextMerger
|
5
|
+
def initialize(context)
|
6
|
+
@main_context = Castle::Utils::Cloner.call(context)
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(request_context)
|
10
|
+
Castle::Utils::Merger.call(@main_context, request_context)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Castle
|
4
|
+
class DefaultContext
|
5
|
+
def initialize(request, cookies = nil)
|
6
|
+
@client_id = Extractors::ClientId.new(request, cookies || request.cookies).call('__cid')
|
7
|
+
@headers = Extractors::Headers.new(request).call
|
8
|
+
@ip = Extractors::IP.new(request).call
|
9
|
+
end
|
10
|
+
|
11
|
+
def call
|
12
|
+
context = {
|
13
|
+
client_id: @client_id,
|
14
|
+
active: true,
|
15
|
+
origin: 'web',
|
16
|
+
headers: @headers || {},
|
17
|
+
ip: @ip,
|
18
|
+
library: {
|
19
|
+
name: 'castle-rb',
|
20
|
+
version: Castle::VERSION
|
21
|
+
}
|
22
|
+
}
|
23
|
+
context[:locale] = @headers['Accept-Language'] if @headers['Accept-Language']
|
24
|
+
context[:user_agent] = @headers['User-Agent'] if @headers['User-Agent']
|
25
|
+
context
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/castle/errors.rb
CHANGED
@@ -4,25 +4,15 @@ module Castle
|
|
4
4
|
module Extractors
|
5
5
|
# used for extraction of cookies and headers from the request
|
6
6
|
class ClientId
|
7
|
-
def initialize(request)
|
7
|
+
def initialize(request, cookies)
|
8
8
|
@request = request
|
9
|
+
@cookies = cookies || {}
|
9
10
|
end
|
10
11
|
|
11
|
-
def call(
|
12
|
-
|
12
|
+
def call(name)
|
13
|
+
@cookies[name] ||
|
13
14
|
@request.env.fetch('HTTP_X_CASTLE_CLIENT_ID', '')
|
14
15
|
end
|
15
|
-
|
16
|
-
private
|
17
|
-
|
18
|
-
# Extract the cookie set by the Castle JavaScript
|
19
|
-
def extract_cookie(response)
|
20
|
-
if response.class.name == 'ActionDispatch::Cookies::CookieJar'
|
21
|
-
Castle::CookieStore::Rack.new(response)
|
22
|
-
else
|
23
|
-
Castle::CookieStore::Base.new(@request, response)
|
24
|
-
end
|
25
|
-
end
|
26
16
|
end
|
27
17
|
end
|
28
18
|
end
|
@@ -7,29 +7,17 @@ module Castle
|
|
7
7
|
def initialize(request)
|
8
8
|
@request = request
|
9
9
|
@request_env = @request.env
|
10
|
-
@
|
10
|
+
@formatter = HeaderFormatter.new
|
11
11
|
end
|
12
12
|
|
13
13
|
# Serialize HTTP headers
|
14
14
|
def call
|
15
|
-
|
16
|
-
name =
|
17
|
-
unless
|
18
|
-
|
19
|
-
|
15
|
+
@request_env.keys.each_with_object({}) do |header, acc|
|
16
|
+
name = @formatter.call(header)
|
17
|
+
next unless Castle.config.whitelisted.include?(name)
|
18
|
+
next if Castle.config.blacklisted.include?(name)
|
19
|
+
acc[name] = @request_env[header]
|
20
20
|
end
|
21
|
-
|
22
|
-
JSON.generate(headers)
|
23
|
-
end
|
24
|
-
|
25
|
-
private
|
26
|
-
|
27
|
-
def format_header_name(header)
|
28
|
-
header.gsub(/^HTTP_/, '').split('_').map(&:capitalize).join('-')
|
29
|
-
end
|
30
|
-
|
31
|
-
def http_headers
|
32
|
-
@request_env.keys.grep(/^HTTP_/)
|
33
21
|
end
|
34
22
|
end
|
35
23
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Castle
|
4
|
+
# generate failover authentication response
|
5
|
+
class FailoverAuthResponse
|
6
|
+
def initialize(user_id, strategy: Castle.config.failover_strategy, reason:)
|
7
|
+
@strategy = strategy
|
8
|
+
@reason = reason
|
9
|
+
@user_id = user_id
|
10
|
+
end
|
11
|
+
|
12
|
+
def generate
|
13
|
+
{
|
14
|
+
'action' => @strategy.to_s,
|
15
|
+
'user_id' => @user_id,
|
16
|
+
'failover' => true,
|
17
|
+
'failover_reason' => @reason
|
18
|
+
}
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/castle/request.rb
CHANGED
@@ -3,30 +3,24 @@
|
|
3
3
|
module Castle
|
4
4
|
# generate api request
|
5
5
|
class Request
|
6
|
-
def initialize(headers)
|
6
|
+
def initialize(headers = {})
|
7
7
|
@config = Castle.config
|
8
8
|
@headers = headers
|
9
9
|
end
|
10
10
|
|
11
|
-
def build_query(endpoint)
|
12
|
-
request = Net::HTTP::Get.new(
|
13
|
-
"#{@config.api_endpoint.path}/#{endpoint}", @headers
|
14
|
-
)
|
15
|
-
add_basic_auth(request)
|
16
|
-
request
|
17
|
-
end
|
18
|
-
|
19
11
|
def build(endpoint, args, method)
|
20
|
-
request = Net::HTTP.const_get(method.to_s.capitalize).new(
|
21
|
-
|
22
|
-
)
|
23
|
-
request.body = args.to_json
|
12
|
+
request = Net::HTTP.const_get(method.to_s.capitalize).new(build_url(endpoint), @headers)
|
13
|
+
request.body = ::Castle::Utils.replace_invalid_characters(args).to_json unless method == :get
|
24
14
|
add_basic_auth(request)
|
25
15
|
request
|
26
16
|
end
|
27
17
|
|
28
18
|
private
|
29
19
|
|
20
|
+
def build_url(endpoint)
|
21
|
+
"/#{@config.url_prefix}/#{endpoint}"
|
22
|
+
end
|
23
|
+
|
30
24
|
def add_basic_auth(request)
|
31
25
|
request.basic_auth('', @config.api_secret)
|
32
26
|
end
|
data/lib/castle/response.rb
CHANGED
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Castle
|
4
|
+
module Hanami
|
5
|
+
module Action
|
6
|
+
def castle
|
7
|
+
@castle ||= ::Castle::Client.new(request, cookies: (cookies if defined? cookies))
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.included(base)
|
12
|
+
base.configure do
|
13
|
+
controller.prepare do
|
14
|
+
include Castle::Hanami::Action
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/castle/support/rails.rb
CHANGED
@@ -1,15 +1,17 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'sinatra/base'
|
4
|
+
|
3
5
|
module Sinatra
|
4
6
|
module Castle
|
5
7
|
module Helpers
|
6
8
|
def castle
|
7
|
-
@castle ||= ::Castle::Client.new(request
|
9
|
+
@castle ||= ::Castle::Client.new(request)
|
8
10
|
end
|
9
11
|
end
|
10
12
|
|
11
13
|
def self.registered(app)
|
12
|
-
app.helpers Helpers
|
14
|
+
app.helpers Castle::Helpers
|
13
15
|
end
|
14
16
|
end
|
15
17
|
|
data/lib/castle/utils.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Castle
|
4
|
+
module Utils
|
5
|
+
class << self
|
6
|
+
# Returns a new hash with all keys converted to symbols, as long as
|
7
|
+
# they respond to +to_sym+. This includes the keys from the root hash
|
8
|
+
# and from all nested hashes and arrays.
|
9
|
+
#
|
10
|
+
# hash = { 'person' => { 'name' => 'Rob', 'age' => '28' } }
|
11
|
+
#
|
12
|
+
# Castle::Hash.deep_symbolize_keys(hash)
|
13
|
+
# # => {:person=>{:name=>"Rob", :age=>"28"}}
|
14
|
+
def deep_symbolize_keys(object, &block)
|
15
|
+
case object
|
16
|
+
when Hash
|
17
|
+
object.each_with_object({}) do |(key, value), result|
|
18
|
+
result[key.to_sym] = deep_symbolize_keys(value, &block)
|
19
|
+
end
|
20
|
+
when Array
|
21
|
+
object.map { |e| deep_symbolize_keys(e, &block) }
|
22
|
+
else
|
23
|
+
object
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def deep_symbolize_keys!(object, &block)
|
28
|
+
case object
|
29
|
+
when Hash
|
30
|
+
object.keys.each do |key|
|
31
|
+
value = object.delete(key)
|
32
|
+
object[key.to_sym] = deep_symbolize_keys!(value, &block)
|
33
|
+
end
|
34
|
+
object
|
35
|
+
when Array
|
36
|
+
object.map! { |e| deep_symbolize_keys!(e, &block) }
|
37
|
+
else
|
38
|
+
object
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def replace_invalid_characters(arg)
|
43
|
+
if arg.is_a?(::String)
|
44
|
+
arg.encode('UTF-8', invalid: :replace, undef: :replace)
|
45
|
+
elsif arg.is_a?(::Hash)
|
46
|
+
arg.each_with_object({}) { |(k, v), h| h[k] = replace_invalid_characters(v) }
|
47
|
+
elsif arg.is_a?(::Array)
|
48
|
+
arg.map(&method(:replace_invalid_characters))
|
49
|
+
else
|
50
|
+
arg
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Castle
|
4
|
+
module Utils
|
5
|
+
class Merger
|
6
|
+
def self.call(first, second)
|
7
|
+
Castle::Utils.deep_symbolize_keys!(first)
|
8
|
+
Castle::Utils.deep_symbolize_keys!(second)
|
9
|
+
|
10
|
+
second.each do |name, value|
|
11
|
+
if value.nil?
|
12
|
+
first.delete(name)
|
13
|
+
elsif value.is_a?(Hash) && first[name].is_a?(Hash)
|
14
|
+
call(first[name], value)
|
15
|
+
else
|
16
|
+
first[name] = value
|
17
|
+
end
|
18
|
+
end
|
19
|
+
first
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/castle/version.rb
CHANGED
data/spec/lib/castle/api_spec.rb
CHANGED
@@ -1,10 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'spec_helper'
|
4
|
-
|
5
3
|
describe Castle::API do
|
6
|
-
let(:api) { described_class.new('abcd', '1.2.3.4'
|
7
|
-
let(:
|
4
|
+
let(:api) { described_class.new('X-Castle-Client-Id' => 'abcd', 'X-Castle-Ip' => '1.2.3.4') }
|
5
|
+
let(:command) { Castle::Command.new('authenticate', '1234', :post) }
|
6
|
+
let(:result_headers) do
|
7
|
+
{ 'Accept' => '*/*',
|
8
|
+
'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3',
|
9
|
+
'User-Agent' => 'Ruby', 'X-Castle-Client-Id' => 'abcd',
|
10
|
+
'X-Castle-Ip' => '1.2.3.4' }
|
11
|
+
end
|
8
12
|
|
9
13
|
describe 'handles timeout' do
|
10
14
|
before do
|
@@ -12,7 +16,7 @@ describe Castle::API do
|
|
12
16
|
end
|
13
17
|
it do
|
14
18
|
expect do
|
15
|
-
api.request(
|
19
|
+
api.request(command)
|
16
20
|
end.to raise_error(Castle::RequestError)
|
17
21
|
end
|
18
22
|
end
|
@@ -21,34 +25,21 @@ describe Castle::API do
|
|
21
25
|
before do
|
22
26
|
stub_request(:any, /api.castle.io/).to_return(status: 400)
|
23
27
|
end
|
24
|
-
it
|
28
|
+
it do
|
25
29
|
expect do
|
26
|
-
api.request(
|
30
|
+
api.request(command)
|
27
31
|
end.to raise_error(Castle::BadRequestError)
|
28
32
|
end
|
29
33
|
end
|
30
34
|
|
31
|
-
describe 'handles
|
32
|
-
before do
|
33
|
-
stub_request(:any, /new.herokuapp.com/)
|
34
|
-
Castle.config.api_endpoint = api_endpoint
|
35
|
-
end
|
36
|
-
it do
|
37
|
-
api.request('authenticate', user_id: '1234')
|
38
|
-
path = "#{api_endpoint.gsub(/new/, ':secret@new')}/authenticate"
|
39
|
-
assert_requested :post, path, times: 1
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
describe 'handles query request' do
|
35
|
+
describe 'handles missing configuration' do
|
44
36
|
before do
|
45
|
-
|
46
|
-
Castle.config.api_endpoint = api_endpoint
|
37
|
+
allow(Castle.config).to receive(:api_secret).and_return('')
|
47
38
|
end
|
48
39
|
it do
|
49
|
-
|
50
|
-
|
51
|
-
|
40
|
+
expect do
|
41
|
+
api.request(command)
|
42
|
+
end.to raise_error(Castle::ConfigurationError)
|
52
43
|
end
|
53
44
|
end
|
54
45
|
end
|