castle-rb 2.3.2 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|