castle-rb 1.3.0 → 2.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 +4 -4
- data/README.md +0 -1
- data/lib/castle-rb.rb +2 -22
- data/lib/castle-rb/api.rb +93 -0
- data/lib/castle-rb/client.rb +15 -39
- data/lib/castle-rb/configuration.rb +10 -0
- data/lib/castle-rb/version.rb +1 -1
- data/spec/api_spec.rb +43 -0
- data/spec/client_spec.rb +24 -0
- data/spec/spec_helper.rb +10 -14
- metadata +22 -113
- data/lib/castle-rb/ext/her.rb +0 -14
- data/lib/castle-rb/models/account.rb +0 -11
- data/lib/castle-rb/models/authentication.rb +0 -9
- data/lib/castle-rb/models/context.rb +0 -15
- data/lib/castle-rb/models/event.rb +0 -5
- data/lib/castle-rb/models/label.rb +0 -7
- data/lib/castle-rb/models/location.rb +0 -4
- data/lib/castle-rb/models/model.rb +0 -62
- data/lib/castle-rb/models/user.rb +0 -4
- data/lib/castle-rb/models/user_agent.rb +0 -4
- data/lib/castle-rb/request.rb +0 -144
- data/lib/castle-rb/utils.rb +0 -21
- data/spec/models/account_spec.rb +0 -6
- data/spec/models/authentication_spec.rb +0 -6
- data/spec/models/context_spec.rb +0 -6
- data/spec/models/event_spec.rb +0 -6
- data/spec/models/location_spec.rb +0 -6
- data/spec/models/user_agent_spec.rb +0 -6
- data/spec/models/user_spec.rb +0 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a3e6d1f3f4603de74ccc307b9aa5ae5a11f5c060
|
4
|
+
data.tar.gz: d059d86a37994320bb208243ba1182cc4d494700
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 42ac193965b709a27f2b2b66254743707b076a029c300be9004fe09c8370e8fcf07ab462b0549b1300044dc10422bd55f14826a6182ed56f3233854470969012
|
7
|
+
data.tar.gz: d97128a24d0abc164a6d60a8a99985dda7bb719a852e1372d5cb4e2bbafc94c562fc28711483c35f8b9d5fb35e3e259b6c0f4383feaba93ef51870eae89bc819
|
data/README.md
CHANGED
@@ -3,7 +3,6 @@
|
|
3
3
|
[](https://travis-ci.org/castle/castle-ruby)
|
4
4
|
[](http://badge.fury.io/rb/castle-rb)
|
5
5
|
[](https://gemnasium.com/castle/castle-ruby)
|
6
|
-
[](https://coveralls.io/r/castle/castle-ruby)
|
7
6
|
|
8
7
|
**[Castle](https://castle.io) adds real-time monitoring of your authentication stack, instantly notifying you and your users on potential account hijacks.**
|
9
8
|
|
data/lib/castle-rb.rb
CHANGED
@@ -1,10 +1,5 @@
|
|
1
|
-
require 'castle-her'
|
2
|
-
require 'castle-rb/ext/her'
|
3
|
-
require 'faraday_middleware'
|
4
|
-
require 'multi_json'
|
5
1
|
require 'openssl'
|
6
2
|
require 'net/http'
|
7
|
-
require 'request_store'
|
8
3
|
require 'active_support/core_ext/hash/indifferent_access'
|
9
4
|
|
10
5
|
require 'castle-rb/version'
|
@@ -12,11 +7,10 @@ require 'castle-rb/version'
|
|
12
7
|
require 'castle-rb/configuration'
|
13
8
|
require 'castle-rb/client'
|
14
9
|
require 'castle-rb/errors'
|
15
|
-
require 'castle-rb/
|
16
|
-
require 'castle-rb/request'
|
17
|
-
|
10
|
+
require 'castle-rb/api'
|
18
11
|
require 'castle-rb/support/cookie_store'
|
19
12
|
require 'castle-rb/support/rails' if defined?(Rails::Railtie)
|
13
|
+
|
20
14
|
if defined?(Sinatra::Base)
|
21
15
|
if defined?(Padrino)
|
22
16
|
require 'castle-rb/support/padrino'
|
@@ -25,17 +19,3 @@ if defined?(Sinatra::Base)
|
|
25
19
|
end
|
26
20
|
end
|
27
21
|
|
28
|
-
module Castle
|
29
|
-
API = Castle.setup_api
|
30
|
-
end
|
31
|
-
|
32
|
-
# These need to be required after setting up Her
|
33
|
-
require 'castle-rb/models/model'
|
34
|
-
require 'castle-rb/models/account'
|
35
|
-
require 'castle-rb/models/event'
|
36
|
-
require 'castle-rb/models/location'
|
37
|
-
require 'castle-rb/models/user_agent'
|
38
|
-
require 'castle-rb/models/context'
|
39
|
-
require 'castle-rb/models/user'
|
40
|
-
require 'castle-rb/models/label'
|
41
|
-
require 'castle-rb/models/authentication'
|
@@ -0,0 +1,93 @@
|
|
1
|
+
module Castle
|
2
|
+
class API
|
3
|
+
attr_accessor :http, :headers
|
4
|
+
|
5
|
+
def initialize(cookie_id, ip, headers)
|
6
|
+
@http = Net::HTTP.new(Castle.config.api_endpoint.host,
|
7
|
+
Castle.config.api_endpoint.port)
|
8
|
+
|
9
|
+
@http.read_timeout = Castle.config.request_timeout
|
10
|
+
|
11
|
+
if Castle.config.api_endpoint.scheme == 'https'
|
12
|
+
@http.use_ssl = true
|
13
|
+
@http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
14
|
+
end
|
15
|
+
|
16
|
+
@headers = {
|
17
|
+
"Content-Type" => "application/json",
|
18
|
+
"X-Castle-Cookie-Id" => cookie_id,
|
19
|
+
"X-Castle-Ip" => ip,
|
20
|
+
"X-Castle-Headers" => headers,
|
21
|
+
"X-Castle-Client-User-Agent" => JSON.generate(client_user_agent),
|
22
|
+
"X-Castle-Source" => Castle.config.source_header,
|
23
|
+
"User-Agent" => "Castle/v1 RubyBindings/#{Castle::VERSION}"
|
24
|
+
}
|
25
|
+
|
26
|
+
@headers.delete_if { |k, v| v.nil? }
|
27
|
+
end
|
28
|
+
|
29
|
+
def request(endpoint, args)
|
30
|
+
req = Net::HTTP::Post.new(
|
31
|
+
"#{Castle.config.api_endpoint.path}/#{endpoint}", @headers)
|
32
|
+
req.basic_auth("", Castle.config.api_secret)
|
33
|
+
req.body = args.to_json
|
34
|
+
|
35
|
+
begin
|
36
|
+
response = @http.request(req)
|
37
|
+
rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, EOFError,
|
38
|
+
Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError,
|
39
|
+
Net::ProtocolError, Net::ReadTimeout => e
|
40
|
+
raise Castle::RequestError, 'Castle API connection error'
|
41
|
+
end
|
42
|
+
|
43
|
+
case response.code.to_i
|
44
|
+
when 200..299
|
45
|
+
# OK
|
46
|
+
when 400
|
47
|
+
raise Castle::BadRequestError, response[:message]
|
48
|
+
when 401
|
49
|
+
raise Castle::UnauthorizedError, response[:message]
|
50
|
+
when 403
|
51
|
+
raise Castle::ForbiddenError, response[:message]
|
52
|
+
when 404
|
53
|
+
raise Castle::NotFoundError, response[:message]
|
54
|
+
when 419
|
55
|
+
raise Castle::UserUnauthorizedError, response[:message]
|
56
|
+
when 422
|
57
|
+
raise Castle::InvalidParametersError, response[:message]
|
58
|
+
else
|
59
|
+
raise Castle::ApiError, response[:message]
|
60
|
+
end
|
61
|
+
|
62
|
+
if response.body.nil? || response.body.empty?
|
63
|
+
{}
|
64
|
+
else
|
65
|
+
begin
|
66
|
+
JSON.parse(response.body, :symbolize_names => true)
|
67
|
+
rescue JSON::ParserError
|
68
|
+
raise Castle::ApiError, 'Invalid response from Castle API'
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def client_user_agent
|
74
|
+
@uname ||= get_uname
|
75
|
+
lang_version = "#{RUBY_VERSION} p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE})"
|
76
|
+
|
77
|
+
{
|
78
|
+
:bindings_version => Castle::VERSION,
|
79
|
+
:lang => 'ruby',
|
80
|
+
:lang_version => lang_version,
|
81
|
+
:platform => RUBY_PLATFORM,
|
82
|
+
:publisher => 'castle',
|
83
|
+
:uname => @uname
|
84
|
+
}
|
85
|
+
end
|
86
|
+
|
87
|
+
def get_uname
|
88
|
+
`uname -a 2>/dev/null`.strip if RUBY_PLATFORM =~ /linux|darwin/i
|
89
|
+
rescue Errno::ENOMEM # couldn't create subprocess
|
90
|
+
"uname lookup failed"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
data/lib/castle-rb/client.rb
CHANGED
@@ -1,55 +1,31 @@
|
|
1
1
|
module Castle
|
2
2
|
class Client
|
3
|
+
attr_accessor :do_not_track, :api
|
3
4
|
|
4
|
-
|
5
|
+
def initialize(request, response)
|
6
|
+
cookie_id = extract_cookie(request, response)['__cid'] || ''
|
7
|
+
ip = request.ip
|
8
|
+
headers = header_string(request)
|
5
9
|
|
6
|
-
|
7
|
-
# Save a reference in the per-request store so that the request
|
8
|
-
# middleware in request.rb can access it
|
9
|
-
RequestStore.store[:castle] = self
|
10
|
-
|
11
|
-
@request_context = {
|
12
|
-
ip: request.ip,
|
13
|
-
user_agent: request.user_agent,
|
14
|
-
cookie_id: extract_cookies(request, response)['__cid'] || '',
|
15
|
-
headers: header_string(request)
|
16
|
-
}
|
17
|
-
end
|
18
|
-
|
19
|
-
def identify(user_id, opts = {})
|
20
|
-
return if @do_not_track
|
21
|
-
Castle::User.save_existing(user_id, opts)
|
22
|
-
end
|
23
|
-
|
24
|
-
def track(opts = {})
|
25
|
-
return if @do_not_track
|
26
|
-
Castle::Event.create(opts)
|
27
|
-
end
|
28
|
-
|
29
|
-
def do_not_track!
|
30
|
-
@do_not_track = true
|
31
|
-
end
|
32
|
-
|
33
|
-
def track!
|
34
|
-
@do_not_track = false
|
10
|
+
@api = API.new(cookie_id, ip, headers)
|
35
11
|
end
|
36
12
|
|
37
|
-
def
|
38
|
-
|
13
|
+
def identify(args)
|
14
|
+
@api.request('identify', args)
|
39
15
|
end
|
40
16
|
|
41
|
-
def
|
42
|
-
|
17
|
+
def authenticate(args)
|
18
|
+
@api.request('authenticate', args)
|
43
19
|
end
|
44
20
|
|
45
|
-
def
|
46
|
-
|
21
|
+
def track(args)
|
22
|
+
@api.request('track', args)
|
47
23
|
end
|
48
24
|
|
49
25
|
private
|
50
26
|
|
51
|
-
|
52
|
-
|
27
|
+
# Extract the cookie set by the Castle Javascript
|
28
|
+
def extract_cookie(request, response)
|
53
29
|
if response.class.name == 'ActionDispatch::Cookies::CookieJar'
|
54
30
|
Castle::CookieStore::Rack.new(response)
|
55
31
|
else
|
@@ -68,7 +44,7 @@ module Castle
|
|
68
44
|
end
|
69
45
|
end.compact.inject(:merge)
|
70
46
|
|
71
|
-
|
47
|
+
JSON.generate(headers || {})
|
72
48
|
end
|
73
49
|
end
|
74
50
|
end
|
@@ -25,6 +25,8 @@ module Castle
|
|
25
25
|
|
26
26
|
def initialize
|
27
27
|
self.request_timeout = 30.0
|
28
|
+
self.api_endpoint =
|
29
|
+
ENV['CASTLE_API_ENDPOINT'] || 'https://api.castle.io/v1'
|
28
30
|
end
|
29
31
|
|
30
32
|
def api_secret
|
@@ -34,5 +36,13 @@ module Castle
|
|
34
36
|
def api_secret=(value)
|
35
37
|
@_api_secret = value
|
36
38
|
end
|
39
|
+
|
40
|
+
def api_endpoint
|
41
|
+
@_api_endpoint
|
42
|
+
end
|
43
|
+
|
44
|
+
def api_endpoint=(value)
|
45
|
+
@_api_endpoint = URI(value)
|
46
|
+
end
|
37
47
|
end
|
38
48
|
end
|
data/lib/castle-rb/version.rb
CHANGED
data/spec/api_spec.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Castle::API do
|
4
|
+
let(:api) { Castle::API.new('abcd', '1.2.3.4', '{}') }
|
5
|
+
|
6
|
+
it 'identifies' do
|
7
|
+
api.request('identify', user_id: '1234', traits: { name: 'Jo' })
|
8
|
+
assert_requested :post, 'https://:secret@api.castle.io/v1/identify',
|
9
|
+
times: 1
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'authenticates' do
|
13
|
+
api.request('authenticate', user_id: '1234')
|
14
|
+
assert_requested :post, 'https://:secret@api.castle.io/v1/authenticate',
|
15
|
+
times: 1
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'handles timeout' do
|
19
|
+
stub_request(:any, /api.castle.io/).to_timeout
|
20
|
+
expect {
|
21
|
+
api.request('authenticate', user_id: '1234')
|
22
|
+
}.to raise_error(Castle::RequestError)
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'handles non-OK response code' do
|
26
|
+
stub_request(:any, /api.castle.io/).to_return(status: 400)
|
27
|
+
expect {
|
28
|
+
api.request('authenticate', user_id: '1234')
|
29
|
+
}.to raise_error(Castle::BadRequestError)
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'handles custom API endpoint' do
|
33
|
+
stub_request(:any, /new.herokuapp.com/)
|
34
|
+
|
35
|
+
api_endpoint = 'http://new.herokuapp.com:3000/v2'
|
36
|
+
Castle.config.api_endpoint = api_endpoint
|
37
|
+
uri = URI(api_endpoint)
|
38
|
+
|
39
|
+
api.request('authenticate', user_id: '1234')
|
40
|
+
assert_requested :post,
|
41
|
+
"#{api_endpoint.gsub(/new/, ":secret@new")}/authenticate", times: 1
|
42
|
+
end
|
43
|
+
end
|
data/spec/client_spec.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class Request < Rack::Request
|
4
|
+
def delegate?; false; end
|
5
|
+
end
|
6
|
+
|
7
|
+
describe Castle::Client do
|
8
|
+
let(:ip) {'1.2.3.4' }
|
9
|
+
let(:cookie_id) { 'abcd' }
|
10
|
+
|
11
|
+
it 'parses the request' do
|
12
|
+
env = Rack::MockRequest.env_for('/',
|
13
|
+
'HTTP_X_FORWARDED_FOR' => '1.2.3.4',
|
14
|
+
'HTTP_COOKIE' => "__cid=#{cookie_id};other=efgh"
|
15
|
+
)
|
16
|
+
req = Request.new(env)
|
17
|
+
|
18
|
+
expect(Castle::API).to receive(:new).with(cookie_id, ip,
|
19
|
+
"{\"X-Forwarded-For\":\"#{ip}\"}").and_call_original
|
20
|
+
|
21
|
+
client = Castle::Client.new(req, nil)
|
22
|
+
client.authenticate({name: '$login.succeeded', user_id: '1234'})
|
23
|
+
end
|
24
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -2,24 +2,20 @@ require 'rubygems'
|
|
2
2
|
require 'bundler/setup'
|
3
3
|
require 'rack'
|
4
4
|
require 'webmock/rspec'
|
5
|
-
require '
|
6
|
-
require 'coveralls'
|
7
|
-
|
8
|
-
SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
|
9
|
-
SimpleCov::Formatter::HTMLFormatter,
|
10
|
-
Coveralls::SimpleCov::Formatter
|
11
|
-
]
|
12
|
-
SimpleCov.start do
|
13
|
-
add_filter 'spec'
|
14
|
-
add_group 'Models', 'lib/castle-rb/models'
|
15
|
-
add_group 'Support', 'lib/castle-rb/support'
|
16
|
-
end
|
17
|
-
|
5
|
+
require 'pry'
|
18
6
|
require 'castle-rb'
|
19
7
|
|
20
8
|
Castle.configure do |config|
|
21
|
-
config.api_secret = '
|
9
|
+
config.api_secret = 'secret'
|
22
10
|
end
|
23
11
|
|
12
|
+
WebMock.disable_net_connect!(allow_localhost: true)
|
13
|
+
|
24
14
|
RSpec.configure do |config|
|
15
|
+
config.before(:each) do
|
16
|
+
Castle.config.api_endpoint = 'https://api.castle.io/v1'
|
17
|
+
Castle.config.request_timeout = 30.0
|
18
|
+
stub_request(:any, /api.castle.io/).
|
19
|
+
to_return(status: 200, body: "{}", headers: {})
|
20
|
+
end
|
25
21
|
end
|
metadata
CHANGED
@@ -1,71 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: castle-rb
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Johan Brissmyr
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-12-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
-
- !ruby/object:Gem::Dependency
|
14
|
-
name: castle-her
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
16
|
-
requirements:
|
17
|
-
- - "~>"
|
18
|
-
- !ruby/object:Gem::Version
|
19
|
-
version: 1.0.1
|
20
|
-
type: :runtime
|
21
|
-
prerelease: false
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
23
|
-
requirements:
|
24
|
-
- - "~>"
|
25
|
-
- !ruby/object:Gem::Version
|
26
|
-
version: 1.0.1
|
27
|
-
- !ruby/object:Gem::Dependency
|
28
|
-
name: faraday_middleware
|
29
|
-
requirement: !ruby/object:Gem::Requirement
|
30
|
-
requirements:
|
31
|
-
- - "~>"
|
32
|
-
- !ruby/object:Gem::Version
|
33
|
-
version: '0.10'
|
34
|
-
type: :runtime
|
35
|
-
prerelease: false
|
36
|
-
version_requirements: !ruby/object:Gem::Requirement
|
37
|
-
requirements:
|
38
|
-
- - "~>"
|
39
|
-
- !ruby/object:Gem::Version
|
40
|
-
version: '0.10'
|
41
|
-
- !ruby/object:Gem::Dependency
|
42
|
-
name: multi_json
|
43
|
-
requirement: !ruby/object:Gem::Requirement
|
44
|
-
requirements:
|
45
|
-
- - "~>"
|
46
|
-
- !ruby/object:Gem::Version
|
47
|
-
version: '1.11'
|
48
|
-
type: :runtime
|
49
|
-
prerelease: false
|
50
|
-
version_requirements: !ruby/object:Gem::Requirement
|
51
|
-
requirements:
|
52
|
-
- - "~>"
|
53
|
-
- !ruby/object:Gem::Version
|
54
|
-
version: '1.11'
|
55
|
-
- !ruby/object:Gem::Dependency
|
56
|
-
name: request_store
|
57
|
-
requirement: !ruby/object:Gem::Requirement
|
58
|
-
requirements:
|
59
|
-
- - "~>"
|
60
|
-
- !ruby/object:Gem::Version
|
61
|
-
version: '1.2'
|
62
|
-
type: :runtime
|
63
|
-
prerelease: false
|
64
|
-
version_requirements: !ruby/object:Gem::Requirement
|
65
|
-
requirements:
|
66
|
-
- - "~>"
|
67
|
-
- !ruby/object:Gem::Version
|
68
|
-
version: '1.2'
|
69
13
|
- !ruby/object:Gem::Dependency
|
70
14
|
name: activesupport
|
71
15
|
requirement: !ruby/object:Gem::Requirement
|
@@ -81,19 +25,19 @@ dependencies:
|
|
81
25
|
- !ruby/object:Gem::Version
|
82
26
|
version: '2'
|
83
27
|
- !ruby/object:Gem::Dependency
|
84
|
-
name:
|
28
|
+
name: pry
|
85
29
|
requirement: !ruby/object:Gem::Requirement
|
86
30
|
requirements:
|
87
|
-
- - "
|
31
|
+
- - ">="
|
88
32
|
- !ruby/object:Gem::Version
|
89
|
-
version: '
|
33
|
+
version: '0'
|
90
34
|
type: :development
|
91
35
|
prerelease: false
|
92
36
|
version_requirements: !ruby/object:Gem::Requirement
|
93
37
|
requirements:
|
94
|
-
- - "
|
38
|
+
- - ">="
|
95
39
|
- !ruby/object:Gem::Version
|
96
|
-
version: '
|
40
|
+
version: '0'
|
97
41
|
- !ruby/object:Gem::Dependency
|
98
42
|
name: rack
|
99
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -109,48 +53,34 @@ dependencies:
|
|
109
53
|
- !ruby/object:Gem::Version
|
110
54
|
version: '1.6'
|
111
55
|
- !ruby/object:Gem::Dependency
|
112
|
-
name:
|
113
|
-
requirement: !ruby/object:Gem::Requirement
|
114
|
-
requirements:
|
115
|
-
- - "~>"
|
116
|
-
- !ruby/object:Gem::Version
|
117
|
-
version: '1.21'
|
118
|
-
type: :development
|
119
|
-
prerelease: false
|
120
|
-
version_requirements: !ruby/object:Gem::Requirement
|
121
|
-
requirements:
|
122
|
-
- - "~>"
|
123
|
-
- !ruby/object:Gem::Version
|
124
|
-
version: '1.21'
|
125
|
-
- !ruby/object:Gem::Dependency
|
126
|
-
name: timecop
|
56
|
+
name: rspec
|
127
57
|
requirement: !ruby/object:Gem::Requirement
|
128
58
|
requirements:
|
129
|
-
- - "
|
59
|
+
- - ">="
|
130
60
|
- !ruby/object:Gem::Version
|
131
|
-
version: '0
|
61
|
+
version: '0'
|
132
62
|
type: :development
|
133
63
|
prerelease: false
|
134
64
|
version_requirements: !ruby/object:Gem::Requirement
|
135
65
|
requirements:
|
136
|
-
- - "
|
66
|
+
- - ">="
|
137
67
|
- !ruby/object:Gem::Version
|
138
|
-
version: '0
|
68
|
+
version: '0'
|
139
69
|
- !ruby/object:Gem::Dependency
|
140
|
-
name:
|
70
|
+
name: webmock
|
141
71
|
requirement: !ruby/object:Gem::Requirement
|
142
72
|
requirements:
|
143
73
|
- - "~>"
|
144
74
|
- !ruby/object:Gem::Version
|
145
|
-
version: '
|
75
|
+
version: '1.21'
|
146
76
|
type: :development
|
147
77
|
prerelease: false
|
148
78
|
version_requirements: !ruby/object:Gem::Requirement
|
149
79
|
requirements:
|
150
80
|
- - "~>"
|
151
81
|
- !ruby/object:Gem::Version
|
152
|
-
version: '
|
153
|
-
description:
|
82
|
+
version: '1.21'
|
83
|
+
description: The easiest way to protect your users
|
154
84
|
email: johan@castle.io
|
155
85
|
executables: []
|
156
86
|
extensions: []
|
@@ -158,33 +88,17 @@ extra_rdoc_files: []
|
|
158
88
|
files:
|
159
89
|
- README.md
|
160
90
|
- lib/castle-rb.rb
|
91
|
+
- lib/castle-rb/api.rb
|
161
92
|
- lib/castle-rb/client.rb
|
162
93
|
- lib/castle-rb/configuration.rb
|
163
94
|
- lib/castle-rb/errors.rb
|
164
|
-
- lib/castle-rb/ext/her.rb
|
165
|
-
- lib/castle-rb/models/account.rb
|
166
|
-
- lib/castle-rb/models/authentication.rb
|
167
|
-
- lib/castle-rb/models/context.rb
|
168
|
-
- lib/castle-rb/models/event.rb
|
169
|
-
- lib/castle-rb/models/label.rb
|
170
|
-
- lib/castle-rb/models/location.rb
|
171
|
-
- lib/castle-rb/models/model.rb
|
172
|
-
- lib/castle-rb/models/user.rb
|
173
|
-
- lib/castle-rb/models/user_agent.rb
|
174
|
-
- lib/castle-rb/request.rb
|
175
95
|
- lib/castle-rb/support/cookie_store.rb
|
176
96
|
- lib/castle-rb/support/padrino.rb
|
177
97
|
- lib/castle-rb/support/rails.rb
|
178
98
|
- lib/castle-rb/support/sinatra.rb
|
179
|
-
- lib/castle-rb/utils.rb
|
180
99
|
- lib/castle-rb/version.rb
|
181
|
-
- spec/
|
182
|
-
- spec/
|
183
|
-
- spec/models/context_spec.rb
|
184
|
-
- spec/models/event_spec.rb
|
185
|
-
- spec/models/location_spec.rb
|
186
|
-
- spec/models/user_agent_spec.rb
|
187
|
-
- spec/models/user_spec.rb
|
100
|
+
- spec/api_spec.rb
|
101
|
+
- spec/client_spec.rb
|
188
102
|
- spec/spec_helper.rb
|
189
103
|
homepage: https://castle.io
|
190
104
|
licenses:
|
@@ -206,16 +120,11 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
206
120
|
version: '0'
|
207
121
|
requirements: []
|
208
122
|
rubyforge_project:
|
209
|
-
rubygems_version: 2.
|
123
|
+
rubygems_version: 2.6.8
|
210
124
|
signing_key:
|
211
125
|
specification_version: 4
|
212
126
|
summary: Castle
|
213
127
|
test_files:
|
214
|
-
- spec/
|
215
|
-
- spec/
|
216
|
-
- spec/models/context_spec.rb
|
217
|
-
- spec/models/event_spec.rb
|
218
|
-
- spec/models/location_spec.rb
|
219
|
-
- spec/models/user_agent_spec.rb
|
220
|
-
- spec/models/user_spec.rb
|
128
|
+
- spec/api_spec.rb
|
129
|
+
- spec/client_spec.rb
|
221
130
|
- spec/spec_helper.rb
|
data/lib/castle-rb/ext/her.rb
DELETED
@@ -1,14 +0,0 @@
|
|
1
|
-
#
|
2
|
-
# Add destroy to association: user.events.destroy(id)
|
3
|
-
#
|
4
|
-
module Her::Model::Associations
|
5
|
-
class AssociationProxy
|
6
|
-
install_proxy_methods :association, :destroy
|
7
|
-
end
|
8
|
-
|
9
|
-
class HasManyAssociation < Association ## remove inheritance
|
10
|
-
def destroy(id)
|
11
|
-
@klass.destroy_existing(id, :"#{@parent.singularized_resource_name}_id" => @parent.id)
|
12
|
-
end
|
13
|
-
end
|
14
|
-
end
|
@@ -1,15 +0,0 @@
|
|
1
|
-
module Castle
|
2
|
-
class Context < Model
|
3
|
-
def user_agent
|
4
|
-
if attributes['user_agent']
|
5
|
-
Castle::UserAgent.new(attributes['user_agent'])
|
6
|
-
end
|
7
|
-
end
|
8
|
-
|
9
|
-
def location
|
10
|
-
if attributes['location']
|
11
|
-
Castle::Location.new(attributes['location'])
|
12
|
-
end
|
13
|
-
end
|
14
|
-
end
|
15
|
-
end
|
@@ -1,62 +0,0 @@
|
|
1
|
-
require 'castle-her'
|
2
|
-
|
3
|
-
class Her::Collection
|
4
|
-
# Call the overridden to_json in Castle::Model
|
5
|
-
def to_json
|
6
|
-
self.map { |m| m.to_json }
|
7
|
-
end
|
8
|
-
end
|
9
|
-
|
10
|
-
module Castle
|
11
|
-
class Model
|
12
|
-
include Her::Model
|
13
|
-
use_api Castle::API
|
14
|
-
|
15
|
-
def initialize(args = {})
|
16
|
-
# allow initializing with id as a string
|
17
|
-
args = { id: args } if args.is_a? String
|
18
|
-
super(args)
|
19
|
-
end
|
20
|
-
|
21
|
-
# Transform model.user.id to model.user_id to allow calls on nested models
|
22
|
-
# def attributes
|
23
|
-
# attrs = super
|
24
|
-
# if attrs['user'] && attrs['user']['id']
|
25
|
-
# attrs.merge!('user_id' => attrs['user']['id'])
|
26
|
-
# attrs.delete 'user'
|
27
|
-
# end
|
28
|
-
# attrs
|
29
|
-
# end
|
30
|
-
|
31
|
-
METHODS.each do |method|
|
32
|
-
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
33
|
-
def self.instance_#{method}(action)
|
34
|
-
instance_custom(:#{method}, action)
|
35
|
-
end
|
36
|
-
RUBY
|
37
|
-
end
|
38
|
-
|
39
|
-
def self.instance_custom(method, action)
|
40
|
-
#
|
41
|
-
# Add method calls to association: user.events.some_method(id, attributes)
|
42
|
-
#
|
43
|
-
AssociationProxy.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
44
|
-
install_proxy_methods :association, :#{action}
|
45
|
-
RUBY
|
46
|
-
HasManyAssociation.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
47
|
-
def #{action}(id, attributes={})
|
48
|
-
@klass.build({:id => id, :"\#{@parent.singularized_resource_name}_id" => @parent.id}).#{action}(attributes)
|
49
|
-
end
|
50
|
-
RUBY
|
51
|
-
|
52
|
-
#
|
53
|
-
# Add method call to instance: user.enable_something
|
54
|
-
#
|
55
|
-
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
56
|
-
def #{action}!(params={})
|
57
|
-
self.class.#{method}("\#{request_path}/#{action.to_s.delete('!')}", params)
|
58
|
-
end
|
59
|
-
RUBY
|
60
|
-
end
|
61
|
-
end
|
62
|
-
end
|
data/lib/castle-rb/request.rb
DELETED
@@ -1,144 +0,0 @@
|
|
1
|
-
module Castle
|
2
|
-
module Request
|
3
|
-
|
4
|
-
def self.client_user_agent
|
5
|
-
@uname ||= get_uname
|
6
|
-
lang_version = "#{RUBY_VERSION} p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE})"
|
7
|
-
|
8
|
-
{
|
9
|
-
:bindings_version => Castle::VERSION,
|
10
|
-
:lang => 'ruby',
|
11
|
-
:lang_version => lang_version,
|
12
|
-
:platform => RUBY_PLATFORM,
|
13
|
-
:publisher => 'castle',
|
14
|
-
:uname => @uname
|
15
|
-
}
|
16
|
-
end
|
17
|
-
|
18
|
-
def self.get_uname
|
19
|
-
`uname -a 2>/dev/null`.strip if RUBY_PLATFORM =~ /linux|darwin/i
|
20
|
-
rescue Errno::ENOMEM # couldn't create subprocess
|
21
|
-
"uname lookup failed"
|
22
|
-
end
|
23
|
-
|
24
|
-
#
|
25
|
-
# Faraday middleware
|
26
|
-
#
|
27
|
-
module Middleware
|
28
|
-
# Sets credentials dynamically, allowing them to change between requests.
|
29
|
-
#
|
30
|
-
class BasicAuth < Faraday::Middleware
|
31
|
-
def initialize(app, api_secret)
|
32
|
-
super(app)
|
33
|
-
@api_secret = api_secret
|
34
|
-
end
|
35
|
-
|
36
|
-
def call(env)
|
37
|
-
value = Base64.encode64(":#{@api_secret || Castle.config.api_secret}")
|
38
|
-
value.delete!("\n")
|
39
|
-
env[:request_headers]["Authorization"] = "Basic #{value}"
|
40
|
-
@app.call(env)
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
44
|
-
# Handle request errors
|
45
|
-
#
|
46
|
-
class RequestErrorHandler < Faraday::Middleware
|
47
|
-
def call(env)
|
48
|
-
env.request.timeout = Castle.config.request_timeout
|
49
|
-
begin
|
50
|
-
@app.call(env)
|
51
|
-
rescue Faraday::ConnectionFailed
|
52
|
-
raise Castle::RequestError, 'Could not connect to Castle API'
|
53
|
-
rescue Faraday::TimeoutError
|
54
|
-
raise Castle::RequestError, 'Castle API timed out'
|
55
|
-
end
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
# Adds details about current environment
|
60
|
-
#
|
61
|
-
class EnvironmentHeaders < Faraday::Middleware
|
62
|
-
def call(env)
|
63
|
-
begin
|
64
|
-
env[:request_headers]["X-Castle-Client-User-Agent"] =
|
65
|
-
MultiJson.encode(Castle::Request.client_user_agent)
|
66
|
-
rescue # ignored
|
67
|
-
end
|
68
|
-
|
69
|
-
if Castle.config.source_header
|
70
|
-
env[:request_headers]["X-Castle-Source"] =
|
71
|
-
Castle.config.source_header
|
72
|
-
end
|
73
|
-
|
74
|
-
env[:request_headers]["User-Agent"] =
|
75
|
-
"Castle/v1 RubyBindings/#{Castle::VERSION}"
|
76
|
-
|
77
|
-
@app.call(env)
|
78
|
-
end
|
79
|
-
end
|
80
|
-
|
81
|
-
# Adds request context like IP address and user agent to any request.
|
82
|
-
#
|
83
|
-
class ContextHeaders < Faraday::Middleware
|
84
|
-
def call(env)
|
85
|
-
castle = RequestStore.store[:castle]
|
86
|
-
return @app.call(env) unless castle
|
87
|
-
|
88
|
-
castle.request_context.each do |key, value|
|
89
|
-
if value
|
90
|
-
header =
|
91
|
-
"X-Castle-#{key.to_s.gsub('_', '-').gsub(/\w+/) {|m| m.capitalize}}"
|
92
|
-
env[:request_headers][header] = value
|
93
|
-
end
|
94
|
-
end
|
95
|
-
@app.call(env)
|
96
|
-
end
|
97
|
-
end
|
98
|
-
|
99
|
-
class JSONParser < Faraday::Response::Middleware
|
100
|
-
def on_complete(env)
|
101
|
-
response = if env[:body].nil? || env[:body].empty?
|
102
|
-
{}
|
103
|
-
else
|
104
|
-
begin
|
105
|
-
MultiJson.load(env[:body], :symbolize_keys => true)
|
106
|
-
rescue MultiJson::LoadError
|
107
|
-
raise Castle::ApiError, 'Invalid response from Castle API'
|
108
|
-
end
|
109
|
-
end
|
110
|
-
|
111
|
-
case env[:status]
|
112
|
-
when 200..299
|
113
|
-
# OK
|
114
|
-
when 400
|
115
|
-
raise Castle::BadRequestError, response[:message]
|
116
|
-
when 401
|
117
|
-
raise Castle::UnauthorizedError, response[:message]
|
118
|
-
when 403
|
119
|
-
raise Castle::ForbiddenError, response[:message]
|
120
|
-
when 404
|
121
|
-
raise Castle::NotFoundError, response[:message]
|
122
|
-
when 419
|
123
|
-
# session token is invalid so clear it
|
124
|
-
RequestStore.store[:castle].session_token = nil
|
125
|
-
|
126
|
-
raise Castle::UserUnauthorizedError, response[:message]
|
127
|
-
when 422
|
128
|
-
raise Castle::InvalidParametersError, response[:message]
|
129
|
-
else
|
130
|
-
raise Castle::ApiError, response[:message]
|
131
|
-
end
|
132
|
-
|
133
|
-
env[:body] = {
|
134
|
-
data: response,
|
135
|
-
metadata: [],
|
136
|
-
errors: {}
|
137
|
-
}
|
138
|
-
end
|
139
|
-
end
|
140
|
-
|
141
|
-
end
|
142
|
-
|
143
|
-
end
|
144
|
-
end
|
data/lib/castle-rb/utils.rb
DELETED
@@ -1,21 +0,0 @@
|
|
1
|
-
# TODO: scope Castle::Utils
|
2
|
-
|
3
|
-
module Castle
|
4
|
-
class << self
|
5
|
-
def setup_api(api_secret = nil)
|
6
|
-
api_endpoint = ENV.fetch('CASTLE_API_ENDPOINT') {
|
7
|
-
"https://api.castle.io/v1"
|
8
|
-
}
|
9
|
-
|
10
|
-
Her::API.setup url: api_endpoint do |c|
|
11
|
-
c.use Castle::Request::Middleware::BasicAuth, api_secret
|
12
|
-
c.use Castle::Request::Middleware::RequestErrorHandler
|
13
|
-
c.use Castle::Request::Middleware::EnvironmentHeaders
|
14
|
-
c.use Castle::Request::Middleware::ContextHeaders
|
15
|
-
c.use FaradayMiddleware::EncodeJson
|
16
|
-
c.use Castle::Request::Middleware::JSONParser
|
17
|
-
c.use Faraday::Adapter::NetHttp
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|
data/spec/models/account_spec.rb
DELETED
data/spec/models/context_spec.rb
DELETED
data/spec/models/event_spec.rb
DELETED