cased-rails 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +591 -0
- data/Rakefile +12 -0
- data/lib/cased/controller_helpers.rb +27 -0
- data/lib/cased/model/automatic.rb +29 -0
- data/lib/cased/rails.rb +6 -0
- data/lib/cased/rails/active_job.rb +36 -0
- data/lib/cased/rails/engine.rb +9 -0
- data/lib/cased/rails/model.rb +42 -0
- data/lib/cased/rails/railtie.rb +49 -0
- data/lib/cased/rails/version.rb +7 -0
- metadata +137 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 1e5a6f29a653a735c26a9189ff897f080e17222fb4466fbd3bed2bad7c09bc82
|
4
|
+
data.tar.gz: df8009f7e0565fb463d7d898968abc7f76fd99dd7c27c3fafbd0cc121a57da32
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 62e55fc3dcd8385135d6303061167a290a1e853325110b2d3d28d274773b67493d7ddec8c18f080f5d57b87ae3a1a400b29e2f404382e5d6cee0399f95644024
|
7
|
+
data.tar.gz: e588a0f6abcdbff9c77b48f84e016458d7666538a41db9a2c2239a4301e2ea87845f266f4ee29c38315d242af731b89accec6486c03936537a7d3fe1744b4c31
|
data/README.md
ADDED
@@ -0,0 +1,591 @@
|
|
1
|
+
# cased-rails
|
2
|
+
|
3
|
+
A Cased client for Ruby on Rails applications in your organization to control and monitor the access of information within your organization.
|
4
|
+
|
5
|
+
## Overview
|
6
|
+
|
7
|
+
- [Installation](#installation)
|
8
|
+
- [Configuration](#configuration)
|
9
|
+
- [Usage](#usage)
|
10
|
+
- [Publishing events to Cased](#publishing-events-to-cased)
|
11
|
+
- [Publishing audit events for all record creation, updates, and deletions automatically](#publishing-audit-events-for-all-record-creation-updates-and-deletions-automatically)
|
12
|
+
- [Retrieving events from a Cased audit trail](#retrieving-events-from-a-cased-audit-trail)
|
13
|
+
- [Retrieving events from multiple Cased audit trails](#retrieving-events-from-multiple-cased-audit-trails)
|
14
|
+
- [Exporting events](#exporting-events)
|
15
|
+
- [Masking & filtering sensitive information](#masking-and-filtering-sensitive-information)
|
16
|
+
- [Disable publishing events](#disable-publishing-events)
|
17
|
+
- [Context](#context)
|
18
|
+
- [Testing](#testing)
|
19
|
+
- [Customizing cased-rails](#customizing-cased-rails)
|
20
|
+
- [Contributing](#contributing)
|
21
|
+
|
22
|
+
## Installation
|
23
|
+
|
24
|
+
Add this line to your application's Gemfile:
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
gem 'cased-rails'
|
28
|
+
```
|
29
|
+
|
30
|
+
And then execute:
|
31
|
+
|
32
|
+
$ bundle
|
33
|
+
|
34
|
+
Or install it yourself as:
|
35
|
+
|
36
|
+
$ gem install cased-rails
|
37
|
+
|
38
|
+
## Configuration
|
39
|
+
|
40
|
+
All configuration options available in cased-rails are available to be configured by an environment variable or manually.
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
Cased.configure do |config|
|
44
|
+
# CASED_POLICY_KEY=policy_live_1dQpY5JliYgHSkEntAbMVzuOROh
|
45
|
+
config.policy_key = 'policy_live_1dQpY5JliYgHSkEntAbMVzuOROh'
|
46
|
+
|
47
|
+
# CASED_USERS_POLICY_KEY=policy_live_1dQpY8bBgEwdpmdpVrrtDzMX4fH
|
48
|
+
# CASED_ORGANIZATIONS_POLICY_KEY=policy_live_1dSHQRurWX8JMYMbkRdfzVoo62d
|
49
|
+
config.policy_keys = {
|
50
|
+
users: 'policy_live_1dQpY8bBgEwdpmdpVrrtDzMX4fH',
|
51
|
+
organizations: 'policy_live_1dSHQRurWX8JMYMbkRdfzVoo62d',
|
52
|
+
}
|
53
|
+
|
54
|
+
# CASED_PUBLISH_KEY=publish_live_1dQpY1jKB48kBd3418PjAotmEwA
|
55
|
+
config.publish_key = 'publish_live_1dQpY1jKB48kBd3418PjAotmEwA'
|
56
|
+
|
57
|
+
# CASED_PUBLISH_URL=https://publish.cased.com
|
58
|
+
config.publish_url = 'https://publish.cased.com'
|
59
|
+
|
60
|
+
# CASED_API_URL=https://api.cased.com
|
61
|
+
config.api_url = 'https://api.cased.com'
|
62
|
+
|
63
|
+
# CASED_RAISE_ON_ERRORS=1
|
64
|
+
config.raise_on_errors = false
|
65
|
+
|
66
|
+
# CASED_SILENCE=1
|
67
|
+
config.silence = false
|
68
|
+
|
69
|
+
# CASED_HTTP_OPEN_TIMEOUT=5
|
70
|
+
config.http_open_timeout = 5
|
71
|
+
|
72
|
+
# CASED_HTTP_READ_TIMEOUT=10
|
73
|
+
config.http_read_timeout = 10
|
74
|
+
end
|
75
|
+
```
|
76
|
+
|
77
|
+
## Usage
|
78
|
+
|
79
|
+
### Publishing events to Cased
|
80
|
+
|
81
|
+
Once Cased is setup there are two ways to publish your first audit trail event.
|
82
|
+
The first is using the `cased` helper method included in all ActiveRecord models.
|
83
|
+
Using the `cased` helper method will automatically include the current model's
|
84
|
+
machine representation and string representation in all audit events published
|
85
|
+
from within the model. In this case the Team model would have a `team` field.
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
class Team < ApplicationRecord
|
89
|
+
def add_member(user)
|
90
|
+
cased :add_member, user: user
|
91
|
+
end
|
92
|
+
end
|
93
|
+
```
|
94
|
+
|
95
|
+
The second way to publish events to Cased is manually using the `Cased.publish` method:
|
96
|
+
|
97
|
+
```ruby
|
98
|
+
Cased.publish(
|
99
|
+
action: 'team.add_member',
|
100
|
+
user: user,
|
101
|
+
team: team,
|
102
|
+
)
|
103
|
+
```
|
104
|
+
|
105
|
+
Both examples above are equivalent in that they publish the following `credit_card.charge` audit event to Cased:
|
106
|
+
|
107
|
+
```json
|
108
|
+
{
|
109
|
+
"action": "team.add_member",
|
110
|
+
"user": "user@cased.com",
|
111
|
+
"user_id": "User;2",
|
112
|
+
"team": "Employees",
|
113
|
+
"team_id": "Team;1",
|
114
|
+
"timestamp": "2020-06-23T02:02:39.932759Z"
|
115
|
+
}
|
116
|
+
```
|
117
|
+
|
118
|
+
It's important when considering where to publish audit trail events in your application you publish them in places you can guarantee information has actually changed. You should also take into account that every model may be created across many places in your application. Only publish audit trail events when you can guarantee something has been created, updated, or deleted.
|
119
|
+
|
120
|
+
For those reasons, we highly recommend using `after_commit` callbacks whenever possible:
|
121
|
+
|
122
|
+
```ruby
|
123
|
+
class User < ApplicationRecord
|
124
|
+
after_commit :publish_user_create_to_cased, on: :create
|
125
|
+
|
126
|
+
private
|
127
|
+
|
128
|
+
def publish_user_create_to_cased
|
129
|
+
cased :create
|
130
|
+
end
|
131
|
+
end
|
132
|
+
```
|
133
|
+
|
134
|
+
If you use any other callback method in the ActiveRecord lifecycle other than `*_commit` you risk publishing an audit event when it does not pass validation or persist to your database.
|
135
|
+
|
136
|
+
Take the example of publishing an audit event for creating a new team in a controller:
|
137
|
+
|
138
|
+
```ruby
|
139
|
+
class TeamsController < ApplicationController
|
140
|
+
def create
|
141
|
+
team = current_organization.teams.new(team_params)
|
142
|
+
if team.save
|
143
|
+
team.cased(:create)
|
144
|
+
# ...
|
145
|
+
else
|
146
|
+
# ...
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
```
|
151
|
+
|
152
|
+
By publishing the `team.create` audit event within the controller directly as shown you risk not having a complete and comprehensive audit trail for each team created in your application as it may happen in your API, model callbacks, and more.
|
153
|
+
|
154
|
+
### Publishing audit events for all record creation, updates, and deletions automatically
|
155
|
+
|
156
|
+
Cased provides a mixin you can include in your models or in `ApplicationRecord` to automatically publish when new models are created, updated, or destroyed.
|
157
|
+
|
158
|
+
```ruby
|
159
|
+
class User < ApplicationRecord
|
160
|
+
include Cased::Model::Automatic
|
161
|
+
end
|
162
|
+
```
|
163
|
+
|
164
|
+
Or for all models in your codebase:
|
165
|
+
|
166
|
+
```ruby
|
167
|
+
class ApplicationRecord < ActiveRecord::Base
|
168
|
+
self.abstract_class = true
|
169
|
+
|
170
|
+
include Cased::Model::Automatic
|
171
|
+
end
|
172
|
+
```
|
173
|
+
|
174
|
+
This mixin is intended to get you up and running quickly. You'll likely need to configure your own callbacks to control what exactly gets published to Cased.
|
175
|
+
|
176
|
+
### Retrieving events from a Cased audit trail
|
177
|
+
|
178
|
+
If you plan on retrieving events from your audit trails to power a user facing audit trail or API you must use a Cased API key.
|
179
|
+
|
180
|
+
```ruby
|
181
|
+
Cased.configure do |config|
|
182
|
+
config.policy_key = 'policy_live_1dQpY5JliYgHSkEntAbMVzuOROh'
|
183
|
+
end
|
184
|
+
|
185
|
+
class AuditTrailController < ApplicationController
|
186
|
+
def index
|
187
|
+
query = Cased.policy.events(phrase: params[:query])
|
188
|
+
results = query.page(params[:page]).limit(params[:limit])
|
189
|
+
|
190
|
+
respond_to do |format|
|
191
|
+
format.json do
|
192
|
+
render json: results
|
193
|
+
end
|
194
|
+
|
195
|
+
format.xml do
|
196
|
+
render xml: results
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
```
|
202
|
+
|
203
|
+
### Retrieving events from multiple Cased audit trails
|
204
|
+
|
205
|
+
To retrieve events from one or more Cased audit trails you can configure multiple Cased API keys and retrieve events for each one by fetching their respective clients.
|
206
|
+
|
207
|
+
```ruby
|
208
|
+
Cased.configure do |config|
|
209
|
+
config.policy_keys = {
|
210
|
+
users: 'policy_live_1dQpY8bBgEwdpmdpVrrtDzMX4fH',
|
211
|
+
organizations: 'policy_live_1dSHQRurWX8JMYMbkRdfzVoo62d',
|
212
|
+
}
|
213
|
+
end
|
214
|
+
|
215
|
+
query = Cased.policies[:users].events.limit(25).page(1)
|
216
|
+
results = query.results
|
217
|
+
results.each do |event|
|
218
|
+
puts event['action'] # => user.login
|
219
|
+
puts event['timestamp'] # => 2020-06-23T02:02:39.932759Z
|
220
|
+
end
|
221
|
+
|
222
|
+
query = Cased.policies[:organizations].events.limit(25).page(1)
|
223
|
+
results = query.results
|
224
|
+
results.each do |event|
|
225
|
+
puts event['action'] # => organization.create
|
226
|
+
puts event['timestamp'] # => 2020-06-22T22:16:31.055655Z
|
227
|
+
end
|
228
|
+
```
|
229
|
+
|
230
|
+
### Exporting events
|
231
|
+
|
232
|
+
Exporting events from Cased allows you to provide users with exports of their own data or to respond to data requests.
|
233
|
+
|
234
|
+
```ruby
|
235
|
+
Cased.configure do |config|
|
236
|
+
config.policy_key = 'policy_live_1dQpY5JliYgHSkEntAbMVzuOROh'
|
237
|
+
end
|
238
|
+
|
239
|
+
export = Cased.policy.exports.create(
|
240
|
+
format: :json,
|
241
|
+
phrase: 'action:credit_card.charge',
|
242
|
+
)
|
243
|
+
export.download_url # => https://api.cased.com/exports/export_1dSHQSNtAH90KA8zGTooMnmMdiD/download?token=eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoidXNlcl8xZFFwWThiQmdFd2RwbWRwVnJydER6TVg0ZkgiLCJ
|
244
|
+
```
|
245
|
+
|
246
|
+
### Masking & filtering sensitive information
|
247
|
+
|
248
|
+
If you are handling sensitive information on behalf of your users you should consider masking or filtering any sensitive information.
|
249
|
+
|
250
|
+
```ruby
|
251
|
+
Cased.configure do |config|
|
252
|
+
config.publish_key = 'publish_live_1dQpY1jKB48kBd3418PjAotmEwA'
|
253
|
+
end
|
254
|
+
|
255
|
+
Cased.publish(
|
256
|
+
action: 'credit_card.charge',
|
257
|
+
user: Cased::Sensitive::String.new('user@domain.com', label: :email),
|
258
|
+
)
|
259
|
+
```
|
260
|
+
|
261
|
+
### Console Usage
|
262
|
+
|
263
|
+
Most Cased events will be created by users from actions on the website from
|
264
|
+
custom defined events or lifecycle callbacks. The exception is any console
|
265
|
+
session where models may generate Cased events as you start to modify records.
|
266
|
+
|
267
|
+
By default any console session will include the hostname of where the console
|
268
|
+
session takes place. Since every event must have an actor, you must set the
|
269
|
+
actor at the beginning of your console session. If you don't know the user,
|
270
|
+
it's recommended you create a system/robot user.
|
271
|
+
|
272
|
+
```ruby
|
273
|
+
Rails.application.console do
|
274
|
+
Cased.context.merge(actor: User.find_by!(login: ENV['USER']))
|
275
|
+
end
|
276
|
+
```
|
277
|
+
|
278
|
+
### Disable publishing events
|
279
|
+
|
280
|
+
Although rare, there may be times where you wish to disable publishing events to Cased. To do so wrap your transaction inside of a `Cased.disable` block:
|
281
|
+
|
282
|
+
```ruby
|
283
|
+
Cased.disable do
|
284
|
+
user.cased(:login)
|
285
|
+
end
|
286
|
+
```
|
287
|
+
|
288
|
+
Or you can configure the entire process to disable publishing events.
|
289
|
+
|
290
|
+
```
|
291
|
+
CASED_DISABLE_PUBLISHING=1 bundle exec ruby crawl.rb
|
292
|
+
```
|
293
|
+
|
294
|
+
### Context
|
295
|
+
|
296
|
+
When you include `cased-rails` in your application your Ruby on Rails application is configures a [Rack middleware](https://github.com/cased/cased-ruby/blob/master/lib/cased/rack_middleware.rb) that populates `Cased.context` with the following information for each request:
|
297
|
+
|
298
|
+
- Request IP address
|
299
|
+
- User agent
|
300
|
+
- Request ID
|
301
|
+
- Request URL
|
302
|
+
- Request HTTP method
|
303
|
+
|
304
|
+
To customize the information included in all events that occur through your controllers you can do so by returning a hash in the `cased_initial_request_context` method:
|
305
|
+
|
306
|
+
```ruby
|
307
|
+
class ApplicationController < ActionController::Base
|
308
|
+
def cased_initial_request_context
|
309
|
+
{
|
310
|
+
location: request.remote_ip,
|
311
|
+
request_http_method: request.method,
|
312
|
+
request_user_agent: request.headers['User-Agent'],
|
313
|
+
request_url: request.original_url,
|
314
|
+
request_id: request.request_id,
|
315
|
+
}
|
316
|
+
end
|
317
|
+
end
|
318
|
+
```
|
319
|
+
|
320
|
+
Any information stored in `Cased.context` will be included for all audit events published to Cased.
|
321
|
+
|
322
|
+
```ruby
|
323
|
+
Cased.context.merge(location: 'hostname.local')
|
324
|
+
|
325
|
+
Cased.publish(
|
326
|
+
action: 'console.start',
|
327
|
+
user: 'john',
|
328
|
+
)
|
329
|
+
```
|
330
|
+
|
331
|
+
Results in:
|
332
|
+
|
333
|
+
```json
|
334
|
+
{
|
335
|
+
"cased_id": "5f8559cd-4cd9-48c3-b1d0-6eedc4019ec1",
|
336
|
+
"action": "user.login",
|
337
|
+
"user": "john",
|
338
|
+
"location": "hostname.local",
|
339
|
+
"timestamp": "2020-06-22T21:43:06.157336"
|
340
|
+
}
|
341
|
+
```
|
342
|
+
|
343
|
+
You can provide a block to `Cased.context.merge` and the provided context will only be present for the duration of the block:
|
344
|
+
|
345
|
+
```ruby
|
346
|
+
Cased.context.merge(location: 'hostname.local') do
|
347
|
+
# Will include { "location": "hostname.local" }
|
348
|
+
Cased.publish(
|
349
|
+
action: 'console.start',
|
350
|
+
user: 'john',
|
351
|
+
)
|
352
|
+
end
|
353
|
+
|
354
|
+
# Will not include { "location": "hostname.local" }
|
355
|
+
Cased.publish(
|
356
|
+
action: 'console.end',
|
357
|
+
user: 'john',
|
358
|
+
)
|
359
|
+
```
|
360
|
+
|
361
|
+
To clear/reset the context:
|
362
|
+
|
363
|
+
```ruby
|
364
|
+
Cased.context.clear
|
365
|
+
```
|
366
|
+
|
367
|
+
### Testing
|
368
|
+
|
369
|
+
`cased-rails` provides a Cased::TestHelper test helper class that you can use to test events are being published to Cased.
|
370
|
+
|
371
|
+
```ruby
|
372
|
+
require 'test-helper'
|
373
|
+
|
374
|
+
class CreditCardTest < Test::Unit::TestCase
|
375
|
+
include Cased::TestHelper
|
376
|
+
|
377
|
+
def test_charging_credit_card_publishes_credit_card_create_event
|
378
|
+
credit_card = credit_cards(:visa)
|
379
|
+
credit_card.charge
|
380
|
+
|
381
|
+
assert_cased_events 1, action: 'credit_card.charge', amount: 2000
|
382
|
+
end
|
383
|
+
|
384
|
+
def test_charging_credit_card_publishes_credit_card_create_event_with_block
|
385
|
+
credit_card = credit_cards(:visa)
|
386
|
+
|
387
|
+
assert_cased_events 1, action: 'credit_card.charge', amount: 2000 do
|
388
|
+
credit_card.charge
|
389
|
+
end
|
390
|
+
end
|
391
|
+
|
392
|
+
def test_charging_credit_card_with_zero_amount_does_not_publish_credit_card_create_event
|
393
|
+
credit_card = credit_cards(:visa)
|
394
|
+
|
395
|
+
assert_no_cased_events do
|
396
|
+
credit_card.charge
|
397
|
+
end
|
398
|
+
end
|
399
|
+
end
|
400
|
+
```
|
401
|
+
|
402
|
+
## Customizing cased-rails
|
403
|
+
|
404
|
+
Out of the box cased-rails takes care of serializing objects for you to the best of its ability, but you can customize cased-rails should you like to fit your products needs.
|
405
|
+
|
406
|
+
Let's look at each of these methods independently as they all work together to
|
407
|
+
create the event.
|
408
|
+
|
409
|
+
`Cased::Model#cased`
|
410
|
+
|
411
|
+
This method is what publishes events for you to Cased. You include information specific to a particular event when calling `Cased::Model#cased`:
|
412
|
+
|
413
|
+
```ruby
|
414
|
+
class CreditCard < ApplicationRecord
|
415
|
+
def charge
|
416
|
+
Stripe::Charge.create(
|
417
|
+
amount: amount,
|
418
|
+
currency: currency,
|
419
|
+
source: source,
|
420
|
+
description: description,
|
421
|
+
)
|
422
|
+
|
423
|
+
cased(:charge, payload: {
|
424
|
+
amount: amount,
|
425
|
+
currency: currency,
|
426
|
+
description: description,
|
427
|
+
})
|
428
|
+
end
|
429
|
+
end
|
430
|
+
```
|
431
|
+
|
432
|
+
Or you can customize information that is included anytime `Cased::Model#cased` is called in your class:
|
433
|
+
|
434
|
+
```ruby
|
435
|
+
class CreditCard < ApplicationRecord
|
436
|
+
def charge
|
437
|
+
Stripe::Charge.create(
|
438
|
+
amount: amount,
|
439
|
+
currency: currency,
|
440
|
+
source: source,
|
441
|
+
description: description,
|
442
|
+
)
|
443
|
+
|
444
|
+
cased(:charge)
|
445
|
+
end
|
446
|
+
|
447
|
+
def cased_payload
|
448
|
+
{
|
449
|
+
credit_card: self,
|
450
|
+
amount: amount,
|
451
|
+
currency: currency,
|
452
|
+
description: description,
|
453
|
+
}
|
454
|
+
end
|
455
|
+
end
|
456
|
+
```
|
457
|
+
|
458
|
+
Both examples are equivelent.
|
459
|
+
|
460
|
+
`Cased::Model#cased_category`
|
461
|
+
|
462
|
+
By default `cased_category` will use the underscore class name to generate the
|
463
|
+
prefix for all events generated by this class. If you published a
|
464
|
+
`CreditCard#charge` event it would be delivered to Cased `credit_card.charge`. If you want to
|
465
|
+
customize what cased-rails uses you can do so by re-opening the method:
|
466
|
+
|
467
|
+
```ruby
|
468
|
+
class CreditCard < ApplicationRecord
|
469
|
+
def cased_category
|
470
|
+
:card
|
471
|
+
end
|
472
|
+
end
|
473
|
+
```
|
474
|
+
|
475
|
+
`Cased::Model#cased_id`
|
476
|
+
|
477
|
+
Per our guide on [Human and machine readable information](https://docs.cased.com/guides/design-audit-trail-events#human-and-machine-readable-information) for [Designing audit trail events](https://docs.cased.com/guides/design-audit-trail-events) we encourage you to publish a unique identifier that will never change to Cased along with your events. This way when you [retrieve events](#retrieving-events-from-a-cased-audit-trail) from Cased you'll be able to locate the corresponding object in your system.
|
478
|
+
|
479
|
+
```ruby
|
480
|
+
class User < ApplicationRecord
|
481
|
+
def cased_id
|
482
|
+
database_id
|
483
|
+
end
|
484
|
+
end
|
485
|
+
```
|
486
|
+
|
487
|
+
`Cased::Model#cased_context`
|
488
|
+
|
489
|
+
To assist you in publishing events to Cased that are consistent and predictable, cased-rails attempts to build your `cased_context` as long as you implement either `to_s` or `cased_id` in your class:
|
490
|
+
|
491
|
+
```ruby
|
492
|
+
class Plan < ApplicationRecord
|
493
|
+
def to_s
|
494
|
+
name
|
495
|
+
end
|
496
|
+
end
|
497
|
+
|
498
|
+
plan = Plan.new(name: 'Free')
|
499
|
+
plan.name # => 'Free'
|
500
|
+
plan.to_s # => 'Free'
|
501
|
+
plan.id # => 1
|
502
|
+
plan.cased_id # => Plan;1
|
503
|
+
plan.cased_context # => { plan: 'Free', plan_id: 'Plan;1' }
|
504
|
+
```
|
505
|
+
|
506
|
+
If your class does not implement `to_s` it will only include `cased_id`:
|
507
|
+
|
508
|
+
```ruby
|
509
|
+
class Plan < ApplicationRecord
|
510
|
+
end
|
511
|
+
|
512
|
+
plan = Plan.new(name: 'Free')
|
513
|
+
plan.to_s # => '#<Plan:0x00007feadf63b7e0>'
|
514
|
+
plan.cased_context # => { plan_id: 'Plan;1' }
|
515
|
+
```
|
516
|
+
|
517
|
+
Or you can customize it if your `to_s` implementation is not suitable for Cased:
|
518
|
+
|
519
|
+
```ruby
|
520
|
+
class Plan < ApplicationRecord
|
521
|
+
has_many :credit_cards
|
522
|
+
|
523
|
+
def to_s
|
524
|
+
name
|
525
|
+
end
|
526
|
+
|
527
|
+
def cased_context(category: cased_category)
|
528
|
+
{
|
529
|
+
"#{category}_id".to_sym => cased_id,
|
530
|
+
category => @name.parameterize,
|
531
|
+
}
|
532
|
+
end
|
533
|
+
end
|
534
|
+
|
535
|
+
class CreditCard < ApplicationRecord
|
536
|
+
belongs_to :plan
|
537
|
+
|
538
|
+
def charge
|
539
|
+
Stripe::Charge.create(
|
540
|
+
amount: amount,
|
541
|
+
currency: currency,
|
542
|
+
source: source,
|
543
|
+
description: description,
|
544
|
+
)
|
545
|
+
|
546
|
+
cased(:charge, payload: {
|
547
|
+
amount: amount,
|
548
|
+
currency: currency,
|
549
|
+
description: description,
|
550
|
+
})
|
551
|
+
end
|
552
|
+
|
553
|
+
def cased_payload
|
554
|
+
{
|
555
|
+
credit_card: self,
|
556
|
+
plan: plan,
|
557
|
+
}
|
558
|
+
end
|
559
|
+
end
|
560
|
+
|
561
|
+
credit_card = CreditCard.new(
|
562
|
+
amount: 2000,
|
563
|
+
currency: 'usd',
|
564
|
+
source: 'tok_amex',
|
565
|
+
description: 'My First Test Charge (created for API docs)',
|
566
|
+
)
|
567
|
+
|
568
|
+
credit_card.charge
|
569
|
+
```
|
570
|
+
|
571
|
+
Results in:
|
572
|
+
|
573
|
+
```json
|
574
|
+
{
|
575
|
+
"cased_id": "5f8559cd-4cd9-48c3-b1d0-6eedc4019ec1",
|
576
|
+
"action": "credit_card.charge",
|
577
|
+
"credit_card": "personal",
|
578
|
+
"credit_card_id": "card_1dQpXqQwXxsQs9sohN9HrzRAV6y",
|
579
|
+
"plan": "Free",
|
580
|
+
"plan_id": "plan_1dQpY1jKB48kBd3418PjAotmEwA",
|
581
|
+
"timestamp": "2020-06-22T20:24:04.815758"
|
582
|
+
}
|
583
|
+
```
|
584
|
+
|
585
|
+
## Contributing
|
586
|
+
|
587
|
+
1. Fork it ( https://github.com/cased/cased-rails/fork )
|
588
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
589
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
590
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
591
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'bundler/setup'
|
5
|
+
rescue LoadError
|
6
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
7
|
+
end
|
8
|
+
|
9
|
+
require 'bundler/gem_tasks'
|
10
|
+
require_relative './test/dummy/config/application'
|
11
|
+
|
12
|
+
Rails.application.load_tasks
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cased
|
4
|
+
module ControllerHelpers
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
before_action :cased_setup_request_context
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def cased_setup_request_context
|
14
|
+
Cased.context.merge(cased_initial_request_context)
|
15
|
+
end
|
16
|
+
|
17
|
+
def cased_initial_request_context
|
18
|
+
{
|
19
|
+
location: request.remote_ip,
|
20
|
+
request_http_method: request.method,
|
21
|
+
request_user_agent: request.headers['User-Agent'],
|
22
|
+
request_url: request.original_url,
|
23
|
+
request_id: request.request_id,
|
24
|
+
}
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cased
|
4
|
+
module Model
|
5
|
+
module Automatic
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
after_commit :publish_cased_create, on: :create
|
10
|
+
after_commit :publish_cased_update, on: :update
|
11
|
+
after_commit :publish_cased_destroy, on: :destroy
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def publish_cased_create
|
17
|
+
cased :create
|
18
|
+
end
|
19
|
+
|
20
|
+
def publish_cased_update
|
21
|
+
cased :update
|
22
|
+
end
|
23
|
+
|
24
|
+
def publish_cased_destroy
|
25
|
+
cased :destroy
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/cased/rails.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cased
|
4
|
+
module Rails
|
5
|
+
module ActiveJob
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
attr_accessor :cased_context
|
10
|
+
|
11
|
+
around_perform do |job, block|
|
12
|
+
context = (job.cased_context || {})
|
13
|
+
context['job_class'] = job.class.name
|
14
|
+
|
15
|
+
Cased.context.merge(context) do
|
16
|
+
block.call
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
after_perform do
|
21
|
+
Cased::Context.clear!
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def deserialize(job_data)
|
26
|
+
super
|
27
|
+
|
28
|
+
self.cased_context = (job_data['cased_context'] || {})
|
29
|
+
end
|
30
|
+
|
31
|
+
def serialize
|
32
|
+
super.merge('cased_context' => Cased::Context.current.context)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cased
|
4
|
+
module Rails
|
5
|
+
module Model
|
6
|
+
def cased_id
|
7
|
+
primary_key_column = self.class.primary_key
|
8
|
+
"#{self.class.name};#{send(primary_key_column)}"
|
9
|
+
end
|
10
|
+
|
11
|
+
def cased_payload
|
12
|
+
{
|
13
|
+
cased_category => self,
|
14
|
+
}.tap do |payload|
|
15
|
+
cased_payload_belongs_to_associations(self, payload)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
# @param payload [Hash] The cased_payload to mutate.
|
22
|
+
# @param object [ActiveRecord::Base] The ActiveRecord instance to continue traversing objects on.
|
23
|
+
# @param prefix [String, Symbol] The cased
|
24
|
+
def cased_payload_belongs_to_associations(object, payload, prefix: nil)
|
25
|
+
klass = object.class
|
26
|
+
klass.reflect_on_all_associations(:belongs_to).each do |association|
|
27
|
+
association_value = object.send(association.name)
|
28
|
+
if association_value.nil?
|
29
|
+
next if association.options[:optional]
|
30
|
+
|
31
|
+
raise ArgumentError, "Expected #{klass}##{association.name} association to not return nil"
|
32
|
+
end
|
33
|
+
|
34
|
+
key = "#{prefix && "#{prefix}_"}#{association.name}".to_sym
|
35
|
+
payload[key] = association_value
|
36
|
+
|
37
|
+
cased_payload_belongs_to_associations(association_value, payload, prefix: association.name)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails/railtie'
|
4
|
+
|
5
|
+
module Cased
|
6
|
+
module Rails
|
7
|
+
class Railtie < ::Rails::Railtie
|
8
|
+
initializer 'cased.include_controller_helpers' do
|
9
|
+
ActiveSupport.on_load(:action_controller) do
|
10
|
+
require 'cased/controller_helpers'
|
11
|
+
include Cased::ControllerHelpers
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
initializer 'cased.instrumentation_controller' do
|
16
|
+
ActiveSupport.on_load(:action_controller) do
|
17
|
+
require 'cased/instrumentation/controller'
|
18
|
+
include Cased::Instrumentation::Controller
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
initializer 'cased.include_model' do
|
23
|
+
ActiveSupport.on_load(:active_record) do
|
24
|
+
require 'cased/model'
|
25
|
+
require 'cased/rails/model'
|
26
|
+
|
27
|
+
include Cased::Model
|
28
|
+
include Cased::Rails::Model
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
initializer 'cased.active_job' do
|
33
|
+
ActiveSupport.on_load(:active_job) do
|
34
|
+
require 'cased/rails/active_job'
|
35
|
+
include Cased::Rails::ActiveJob
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
initializer 'cased.rack_middleware' do |app|
|
40
|
+
app.middleware.use Cased::RackMiddleware
|
41
|
+
end
|
42
|
+
|
43
|
+
# :nocov:
|
44
|
+
console do
|
45
|
+
Cased.console
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
metadata
ADDED
@@ -0,0 +1,137 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: cased-rails
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.3.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Garrett Bjerkhoel
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-11-11 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: cased-ruby
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.3.3
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.3.3
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rails
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 6.0.2
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 6.0.2
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: mocha
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 1.11.2
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 1.11.2
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: pg
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 1.2.1
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 1.2.1
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rubocop
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - '='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 0.77.0
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - '='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 0.77.0
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: webmock
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - '='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 3.8.3
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - '='
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 3.8.3
|
97
|
+
description: Ruby on Rails SDK/client library for Cased
|
98
|
+
email:
|
99
|
+
- garrett@cased.com
|
100
|
+
executables: []
|
101
|
+
extensions: []
|
102
|
+
extra_rdoc_files: []
|
103
|
+
files:
|
104
|
+
- README.md
|
105
|
+
- Rakefile
|
106
|
+
- lib/cased/controller_helpers.rb
|
107
|
+
- lib/cased/model/automatic.rb
|
108
|
+
- lib/cased/rails.rb
|
109
|
+
- lib/cased/rails/active_job.rb
|
110
|
+
- lib/cased/rails/engine.rb
|
111
|
+
- lib/cased/rails/model.rb
|
112
|
+
- lib/cased/rails/railtie.rb
|
113
|
+
- lib/cased/rails/version.rb
|
114
|
+
homepage: https://github.com/cased/cased-rails
|
115
|
+
licenses:
|
116
|
+
- MIT
|
117
|
+
metadata: {}
|
118
|
+
post_install_message:
|
119
|
+
rdoc_options: []
|
120
|
+
require_paths:
|
121
|
+
- lib
|
122
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
123
|
+
requirements:
|
124
|
+
- - ">="
|
125
|
+
- !ruby/object:Gem::Version
|
126
|
+
version: '0'
|
127
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
requirements: []
|
133
|
+
rubygems_version: 3.0.3
|
134
|
+
signing_key:
|
135
|
+
specification_version: 4
|
136
|
+
summary: Ruby on Rails SDK/client library for Cased
|
137
|
+
test_files: []
|