castle-rb 1.3.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f936a86b329df5e3a500d0b2524b3481bd656aa5
4
- data.tar.gz: 52f49648379d4a37a72ad704ce4a115e8619dc7f
3
+ metadata.gz: a3e6d1f3f4603de74ccc307b9aa5ae5a11f5c060
4
+ data.tar.gz: d059d86a37994320bb208243ba1182cc4d494700
5
5
  SHA512:
6
- metadata.gz: 9eaaae4e815b4fdb06b9d91f8dab1aabf648dc551c6267665de3a0766858c2f2ba0a886e435b13077b4357d5cd45e42c6f88f1a7b2277ebea35fff0f0c68cae1
7
- data.tar.gz: 2b47fbdb65b8c3552180f954fe743592dd1216956aac4823b5ca951f515e5b8dd5e3310bea9b355945e0186a8b85668c169ea231becad8a3d638c452d6000aee
6
+ metadata.gz: 42ac193965b709a27f2b2b66254743707b076a029c300be9004fe09c8370e8fcf07ab462b0549b1300044dc10422bd55f14826a6182ed56f3233854470969012
7
+ data.tar.gz: d97128a24d0abc164a6d60a8a99985dda7bb719a852e1372d5cb4e2bbafc94c562fc28711483c35f8b9d5fb35e3e259b6c0f4383feaba93ef51870eae89bc819
data/README.md CHANGED
@@ -3,7 +3,6 @@
3
3
  [![Build Status](https://travis-ci.org/castle/castle-ruby.png)](https://travis-ci.org/castle/castle-ruby)
4
4
  [![Gem Version](https://badge.fury.io/rb/castle-rb.png)](http://badge.fury.io/rb/castle-rb)
5
5
  [![Dependency Status](https://gemnasium.com/castle/castle-ruby.png)](https://gemnasium.com/castle/castle-ruby)
6
- [![Coverage Status](https://coveralls.io/repos/castle/castle-ruby/badge.png)](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
 
@@ -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/utils'
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
@@ -1,55 +1,31 @@
1
1
  module Castle
2
2
  class Client
3
+ attr_accessor :do_not_track, :api
3
4
 
4
- attr_accessor :request_context, :do_not_track
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
- def initialize(request, response, opts = {})
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 authenticate(user_id)
38
- Castle::Authentication.create(user_id: user_id)
13
+ def identify(args)
14
+ @api.request('identify', args)
39
15
  end
40
16
 
41
- def authentication(authentication_id)
42
- Castle::Authentication.find(authentication_id)
17
+ def authenticate(args)
18
+ @api.request('authenticate', args)
43
19
  end
44
20
 
45
- def authentications
46
- Castle::Authentication
21
+ def track(args)
22
+ @api.request('track', args)
47
23
  end
48
24
 
49
25
  private
50
26
 
51
- def extract_cookies(request, response)
52
- # Extract the cookie set by the Castle Javascript
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
- MultiJson.encode(headers)
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
@@ -1,3 +1,3 @@
1
1
  module Castle
2
- VERSION = "1.3.0"
2
+ VERSION = "2.0.0"
3
3
  end
@@ -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
@@ -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
@@ -2,24 +2,20 @@ require 'rubygems'
2
2
  require 'bundler/setup'
3
3
  require 'rack'
4
4
  require 'webmock/rspec'
5
- require 'simplecov'
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 = 'secretkey'
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: 1.3.0
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-07-04 00:00:00.000000000 Z
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: rspec
28
+ name: pry
85
29
  requirement: !ruby/object:Gem::Requirement
86
30
  requirements:
87
- - - "~>"
31
+ - - ">="
88
32
  - !ruby/object:Gem::Version
89
- version: '3.3'
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: '3.3'
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: webmock
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.8'
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.8'
68
+ version: '0'
139
69
  - !ruby/object:Gem::Dependency
140
- name: coveralls
70
+ name: webmock
141
71
  requirement: !ruby/object:Gem::Requirement
142
72
  requirements:
143
73
  - - "~>"
144
74
  - !ruby/object:Gem::Version
145
- version: '0.8'
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: '0.8'
153
- description: Secure your authentication stack with user account monitoring
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/models/account_spec.rb
182
- - spec/models/authentication_spec.rb
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.5.1
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/models/account_spec.rb
215
- - spec/models/authentication_spec.rb
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
@@ -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,11 +0,0 @@
1
- module Castle
2
- class Account < Model
3
- def self.fetch
4
- get('/v1/account')
5
- end
6
-
7
- def self.update(settings = {})
8
- put('/v1/account', settings: settings)
9
- end
10
- end
11
- end
@@ -1,9 +0,0 @@
1
- module Castle
2
- class Authentication < Model
3
- has_one :context
4
-
5
- instance_post :approve
6
- instance_post :deny
7
- instance_post :reset
8
- end
9
- 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,5 +0,0 @@
1
- module Castle
2
- class Event < Model
3
- has_one :context
4
- end
5
- end
@@ -1,7 +0,0 @@
1
- module Castle
2
- class Label < Model
3
- def self.destroy_all(*args)
4
- self.delete('/v1/labels', *args)
5
- end
6
- end
7
- end
@@ -1,4 +0,0 @@
1
- module Castle
2
- class Location < Model
3
- end
4
- 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
@@ -1,4 +0,0 @@
1
- module Castle
2
- class User < Model
3
- end
4
- end
@@ -1,4 +0,0 @@
1
- module Castle
2
- class UserAgent < Model
3
- end
4
- end
@@ -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
@@ -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
@@ -1,6 +0,0 @@
1
- describe Castle::Account do
2
- it 'extends Castle::Model' do
3
- instance = Castle::Account.new
4
- expect(instance).to be_a Castle::Model
5
- end
6
- end
@@ -1,6 +0,0 @@
1
- describe Castle::Authentication do
2
- it 'extends Castle::Model' do
3
- instance = Castle::Authentication.new
4
- expect(instance).to be_a Castle::Model
5
- end
6
- end
@@ -1,6 +0,0 @@
1
- describe Castle::Context do
2
- it 'extends Castle::Model' do
3
- instance = Castle::Context.new
4
- expect(instance).to be_a Castle::Model
5
- end
6
- end
@@ -1,6 +0,0 @@
1
- describe Castle::Event do
2
- it 'extends Castle::Model' do
3
- instance = Castle::Event.new
4
- expect(instance).to be_a Castle::Model
5
- end
6
- end
@@ -1,6 +0,0 @@
1
- describe Castle::Location do
2
- it 'extends Castle::Model' do
3
- instance = Castle::Location.new
4
- expect(instance).to be_a Castle::Model
5
- end
6
- end
@@ -1,6 +0,0 @@
1
- describe Castle::UserAgent do
2
- it 'extends Castle::Model' do
3
- instance = Castle::UserAgent.new
4
- expect(instance).to be_a Castle::Model
5
- end
6
- end
@@ -1,6 +0,0 @@
1
- describe Castle::User do
2
- it 'extends Castle::Model' do
3
- instance = Castle::User.new
4
- expect(instance).to be_a Castle::Model
5
- end
6
- end