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 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