cased-ruby
A Cased client for Ruby applications in your organization to control and monitor the access of information within your organization.
Overview
- Installation
- Configuration
- Usage
- Cased CLI
- Starting an approval workflow
- Attaching metadata to all CLI requests
- Audit trails
- Publishing events to Cased
- Retrieving events from a Cased audit trail
- Retrieving events from multiple Cased audit trails
- Exporting events
- Masking & filtering sensitive information
- Disable publishing events
- Context
- Testing
- Customizing cased-ruby
- Contributing
Installation
Add this line to your application's Gemfile:
gem 'cased-ruby'
And then execute:
$ bundle
Or install it yourself as:
$ gem install cased-ruby
Configuration
All configuration options available in cased-ruby are available to be configured by an environment variable or manually.
Cased.configure do |config|
# CASED_POLICY_KEY=policy_live_1dQpY5JliYgHSkEntAbMVzuOROh
config.policy_key = 'policy_live_1dQpY5JliYgHSkEntAbMVzuOROh'
# CASED_USERS_POLICY_KEY=policy_live_1dQpY8bBgEwdpmdpVrrtDzMX4fH
# CASED_ORGANIZATIONS_POLICY_KEY=policy_live_1dSHQRurWX8JMYMbkRdfzVoo62d
config.policy_keys = {
users: 'policy_live_1dQpY8bBgEwdpmdpVrrtDzMX4fH',
organizations: 'policy_live_1dSHQRurWX8JMYMbkRdfzVoo62d',
}
# CASED_PUBLISH_KEY=publish_live_1dQpY1jKB48kBd3418PjAotmEwA
config.publish_key = 'publish_live_1dQpY1jKB48kBd3418PjAotmEwA'
# CASED_PUBLISH_URL=https://publish.cased.com
config.publish_url = 'https://publish.cased.com'
# CASED_URL=https://app.cased.com
config.url = 'https://app.cased.com'
# CASED_API_URL=https://api.cased.com
config.api_url = 'https://api.cased.com'
# GUARD_APPLICATION_KEY=guard_application_1ntKX0P4vUbKoc0lMWGiSbrBHcH
config.guard_application_key = 'guard_application_1ntKX0P4vUbKoc0lMWGiSbrBHcH'
# GUARD_USER_TOKEN=user_1oFqlROLNRGVLOXJSsHkJiVmylr
config.guard_user_token = 'user_1oFqlROLNRGVLOXJSsHkJiVmylr'
# DENY_IF_UNREACHABLE=1
config.guard_deny_if_unreachable = true
# CASED_RAISE_ON_ERRORS=1
config.raise_on_errors = false
# CASED_SILENCE=1
config.silence = false
# CASED_HTTP_OPEN_TIMEOUT=5
config.http_open_timeout = 5
# CASED_HTTP_READ_TIMEOUT=10
config.http_read_timeout = 10
# Attach metadata to all CLI requests. This metadata will appear in Cased and
# any notification source such as email or Slack.
#
# You are limited to 20 properties and cannot be a nested dictionary. Metadata
# specified in the CLI request overrides any configured globally.
config.cli. = {
rails_env: ENV['RAILS_ENV'],
heroku_application: ENV['HEROKU_APP_NAME'],
git_commit: ENV['GIT_COMMIT'],
}
end
Usage
Cased CLI
Keep any command line tool available as your team grows — monitor usage, require peer approvals for sensitive operations, and receive intelligent alerts to suspicious activity.
Starting an approval workflow
To start an approval workflow you must first obtain your application key and the user token for who is requesting access.
Cased.configure do |config|
config.guard_application_key = 'guard_application_1pG43HF3aRHjNTTm10zzu0tngBO'
end
authentication = Cased::CLI::Authentication.new(token: 'user_1pG43D1AzTjLR8XWJHj8B3aNZ4Y')
session = Cased::CLI::Session.new(
authentication: authentication,
reason: 'I need export our GitHub issues.',
metadata: {
organization: 'GitHub',
},
)
if session.create && session.approved?
github.issues.each do |issue|
puts issue.title
end
else
puts 'Unauthorized to export GitHub issues.'
end
If you do not have the user token you can always request it interactively. Cased::CLI::Identity#identify is a blocking operation prompting the user to visit Cased to identify themselves, returning their user token upon identifying themselves which can be used to start your session.
Cased.configure do |config|
config.guard_application_key = 'guard_application_1pG43HF3aRHjNTTm10zzu0tngBO'
end
authentication = Cased::CLI::Authentication.new
identity = Cased::CLI::Identity.new
token, ip_address = identity.identify
authentication.token = token
session = Cased::CLI::Session.new(
authentication: authentication,
reason: 'I need export our GitHub issues.',
metadata: {
organization: 'GitHub',
},
)
if session.create && session.approved?
github.issues.each do |issue|
puts issue.title
end
else
puts 'Unauthorized to export GitHub issues.'
end
Starting an interactive approval workflow
If you do not want to manually create sessions and handle each state manually, you can use the interactive approval workflow using Cased::CLI::InteractiveSession.
Cased.configure do |config|
config.guard_application_key = 'guard_application_1pG43HF3aRHjNTTm10zzu0tngBO'
end
session = Cased::CLI::InteractiveSession.start
if session.approved?
github.issues.each do |issue|
puts issue.title
end
else
puts 'Unauthorized to export GitHub issues.'
end
You no longer need to handle obtaining the user token or asking for a reason up
front, Cased::CLI::InteractiveSession
will prompt the user for any reason
being required as necessary.
Attaching metadata to all CLI requests
While you can customize the metadata included for each CLI request, it may prove useful to specify metadata globally that will be included with each CLI request. Some useful information to include may be the current Rails environment, Heroku application, Git commit deployed, and more.
Metadata is limited to 20 properties and cannot be a nested dictionary.
Cased.configure do |config|
config.cli. = {
rails_env: ENV['RAILS_ENV'],
heroku_application: ENV['HEROKU_APP_NAME'],
git_commit: ENV['GIT_COMMIT'],
}
end
Note: Metadata specified in the CLI request overrides any configured globally.
Audit trails
Publishing events to Cased
There are two ways to publish your first Cased event.
Manually
require 'cased-ruby'
Cased.configure do |config|
config.publish_key = 'publish_live_1dQpY1jKB48kBd3418PjAotmEwA'
end
Cased.publish(
action: 'credit_card.charge',
amount: 2000,
currency: 'usd',
source: 'tok_amex',
description: 'My First Test Charge (created for API docs)',
credit_card_id: 'card_1dQpXqQwXxsQs9sohN9HrzRAV6y',
)
Cased::Model
cased-ruby
provides a class mixin that gives you a framework to publish events.
require 'cased-ruby'
Cased.configure do |config|
config.publish_key = 'publish_live_1dQpY1jKB48kBd3418PjAotmEwA'
end
class CreditCard
include Cased::Model
def initialize(amount:, currency:, source:, description:)
@amount = amount
@currency = currency
@source = source
@description = description
end
def charge
Stripe::Charge.create({
amount: @amount,
currency: @currency,
source: @source,
description: @description,
})
cased(:charge, payload: {
amount: @amount,
currency: @currency,
description: @description,
})
end
def cased_id
'card_1dQpXqQwXxsQs9sohN9HrzRAV6y'
end
def cased_payload
{
credit_card: self,
}
end
end
credit_card = CreditCard.new(
amount: 2000,
currency: 'usd',
source: 'tok_amex',
description: 'My First Test Charge (created for API docs)',
)
credit_card.charge
Both examples above are equivelent in that they publish the following credit_card.charge
event to Cased:
{
"cased_id": "5f8559cd-4cd9-48c3-b1d0-6eedc4019ec1",
"action": "credit_card.charge",
"amount": 2000,
"currency": "usd",
"source": "tok_amex",
"description": "My First Test Charge (created for API docs)",
"credit_card_id": "card_1dQpXqQwXxsQs9sohN9HrzRAV6y",
"timestamp": "2020-06-23T02:02:39.932759Z"
}
Retrieving events from a Cased audit trail
If you plan on retrieving audit events from your Cased audit trail you must use a Cased API key.
require 'cased-ruby'
Cased.configure do |config|
config.policy_key = 'policy_live_1dQpY5JliYgHSkEntAbMVzuOROh'
end
query = Cased.policy.events.limit(25).page(1)
results = query.results
results.each do |event|
puts event['action'] # => credit_card.charge
puts event['timestamp'] # => 2020-06-23T02:02:39.932759Z
end
query.total_count # => 2,366
query.total_pages # => 95
query.success? # => true
query.error? # => false
Retrieving events from multiple Cased audit trails
To retrieve audit events from one or more Cased audit trails you can configure multiple Cased Policy API keys and retrieve events for each one.
require 'cased-ruby'
Cased.configure do |config|
config.policy_keys = {
users: 'policy_live_1dQpY8bBgEwdpmdpVrrtDzMX4fH',
organizations: 'policy_live_1dSHQRurWX8JMYMbkRdfzVoo62d',
}
end
query = Cased.policies[:users].events.limit(25).page(1)
results = query.results
results.each do |event|
puts event['action'] # => user.login
puts event['timestamp'] # => 2020-06-23T02:02:39.932759Z
end
query = Cased.policies[:organizations].events.limit(25).page(1)
results = query.results
results.each do |event|
puts event['action'] # => organization.create
puts event['timestamp'] # => 2020-06-22T22:16:31.055655Z
end
Exporting events
Exporting events from Cased allows you to provide users with exports of their own data or to respond to data requests.
require 'cased-ruby'
Cased.configure do |config|
config.policy_key = 'policy_live_1dQpY5JliYgHSkEntAbMVzuOROh'
end
export = Cased.policy.exports.create(
format: :json,
phrase: 'action:credit_card.charge',
)
export.download_url # => https://api.cased.com/exports/export_1dSHQSNtAH90KA8zGTooMnmMdiD/download?token=eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoidXNlcl8xZFFwWThiQmdFd2RwbWRwVnJydER6TVg0ZkgiLCJ
Masking & filtering sensitive information
If you are handling sensitive information on behalf of your users you should consider masking or filtering any sensitive information.
require 'cased-ruby'
Cased.configure do |config|
config.publish_key = 'publish_live_1dQpY1jKB48kBd3418PjAotmEwA'
end
Cased.publish(
action: 'credit_card.charge',
user: Cased::Sensitive::String.new('[email protected]', label: :email)
)
Console Usage
Most Cased events will be created by users from actions on the website from custom defined events or lifecycle callbacks. The exception is any console session where models may generate Cased events as you start to modify records.
By default any console session will include the hostname of where the console session takes place. Since every event must have an actor, you must set the actor at the beginning of your console session. If you don't know the user, it's recommended you create a system/robot user.
# OTHER CONSOLE INITIALIZATION HERE
Cased.context.push(actor: @actor)
Disable publishing events
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:
Cased.disable do
user.cased(:login)
end
Or you can configure the entire process to disable publishing events.
CASED_DISABLE_PUBLISHING=1 bundle exec ruby crawl.rb
Context
One of the most easiest ways to publish detailed events to Cased is to push contextual information on to the Cased context.
require 'cased-ruby'
Cased.configure do |config|
config.publish_key = 'publish_live_1dQpY1jKB48kBd3418PjAotmEwA'
end
Cased.context.merge(location: 'hostname.local')
Cased.publish(
action: 'console.start',
user: 'john',
)
Any information stored in Cased.context
will be included anytime an event is published.
{
"cased_id": "5f8559cd-4cd9-48c3-b1d0-6eedc4019ec1",
"action": "user.login",
"user": "john",
"location": "hostname.local",
"timestamp": "2020-06-22T21:43:06.157336"
}
You can provide Cased.context.merge
a block and the context will only be present for the duration of the block:
Cased.context.merge(location: 'hostname.local') do
# Will include { "location": "hostname.local" }
Cased.publish(
action: 'console.start',
user: 'john',
)
end
# Will not include { "location": "hostname.local" }
Cased.publish(
action: 'console.end',
user: 'john',
)
To clear/reset the context:
Cased.context.clear
Testing
cased-ruby provides a test helper class that you can use to test events are being published to Cased.
require 'test-helper'
class CreditCardTest < Test::Unit::TestCase
include Cased::TestHelper
def test_charging_credit_card_publishes_credit_card_create_event
credit_card = CreditCard.new(
amount: 2000,
currency: 'usd',
source: 'tok_amex',
description: 'My First Test Charge (created for API docs)',
)
credit_card.charge
assert_cased_events 1, action: 'credit_card.charge', amount: 2000
end
def test_charging_credit_card_publishes_credit_card_create_event_with_block
credit_card = CreditCard.new(
amount: 2000,
currency: 'usd',
source: 'tok_amex',
description: 'My First Test Charge (created for API docs)',
)
assert_cased_events 1, action: 'credit_card.charge', amount: 2000 do
credit_card.charge
end
end
def test_charging_credit_card_with_zero_amount_does_not_publish_credit_card_create_event
credit_card = CreditCard.new(
amount: 0,
currency: 'usd',
source: 'tok_amex',
description: 'My First Test Charge (created for API docs)',
)
assert_no_cased_events do
credit_card.charge
end
end
end
Customizing cased-ruby
Out of the box cased-ruby takes care of serializing objects for you to the best of its ability, but you can customize cased-ruby should you like to fit your products needs.
Let's look at each of these methods independently as they all work together to create the event.
Cased::Model#cased
This method is what publishes events for you to Cased. You include information specific to a particular event when calling Cased::Model#cased
:
class CreditCard
include Cased::Model
# ...
def charge
Stripe::Charge.create({
amount: @amount,
currency: @currency,
source: @source,
description: @description,
})
cased(:charge, payload: {
amount: @amount,
currency: @currency,
description: @description,
})
end
end
Or you can customize information that is included anytime Cased::Model#cased
is called in your class:
class CreditCard
include Cased::Model
# ...
def charge
Stripe::Charge.create({
amount: @amount,
currency: @currency,
source: @source,
description: @description,
})
cased(:charge)
end
def cased_payload
{
credit_card: self,
amount: @amount,
currency: @currency,
description: @description,
}
end
end
Both examples are equivelent.
Cased::Model#cased_category
By default cased_category
will use the underscore class name to generate the
prefix for all events generated by this class. If you published a
CreditCard#charge
event it would be delivered to Cased credit_card.charge
. If you want to
customize what cased-ruby uses you can do so by re-opening the method:
class CreditCard
include Cased::Model
def cased_category
:card
end
end
Cased::Model#cased_id
Per our guide on Human and machine readable information for Designing 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 from Cased you'll be able to locate the corresponding object in your system.
class User
include Cased::Model
def cased_id
database_id
end
end
Cased::Model#cased_context
To assist you in publishing events to Cased that are consistent and predictable, cased-ruby attempts to build your cased_context
as long as you implement either to_s
or cased_id
in your class:
class Plan
include Cased::Model
def initialize(name)
@name = name
end
def cased_id
database_id
end
def to_s
@name
end
end
plan = Plan.new('Free')
plan.to_s # => 'Free'
plan.cased_id # => 'plan_1dQpY1jKB48kBd3418PjAotmEwA'
plan.cased_context # => { plan: 'Free', plan_id: 'plan_1dQpY1jKB48kBd3418PjAotmEwA' }
If your class does not implement to_s
it will only include cased_id
:
class Plan
include Cased::Model
def initialize(name)
@name = name
end
def cased_id
database_id
end
end
plan = Plan.new('Free')
plan.to_s # => '#<Plan:0x00007feadf63b7e0>'
plan.cased_context # => { plan_id: 'plan_1dQpY1jKB48kBd3418PjAotmEwA' }
Or you can customize it if your to_s
implementation is not suitable for Cased:
class Plan
include Cased::Model
def initialize(name)
@name = name
end
def cased_id
'plan_1dQpY1jKB48kBd3418PjAotmEwA'
end
def to_s
@name
end
def cased_context(category: cased_category)
{
"#{category}_id".to_sym => cased_id,
category => @name.parameterize,
}
end
end
class CreditCard
include Cased::Model
def initialize(amount:, currency:, source:, description:)
@amount = amount
@currency = currency
@source = source
@description = description
end
def charge
Stripe::Charge.create({
amount: @amount,
currency: @currency,
source: @source,
description: @description,
})
cased(:charge, payload: {
amount: @amount,
currency: @currency,
description: @description,
})
end
def plan
Plan.new('Free')
end
def cased_id
'card_1dQpXqQwXxsQs9sohN9HrzRAV6y'
end
def cased_payload
{
credit_card: self,
plan: plan,
}
end
end
credit_card = CreditCard.new(
amount: 2000,
currency: 'usd',
source: 'tok_amex',
description: 'My First Test Charge (created for API docs)',
)
credit_card.charge
Results in:
{
"cased_id": "5f8559cd-4cd9-48c3-b1d0-6eedc4019ec1",
"action": "credit_card.charge",
"credit_card": "personal",
"credit_card_id": "card_1dQpXqQwXxsQs9sohN9HrzRAV6y",
"plan": "Free",
"plan_id": "plan_1dQpY1jKB48kBd3418PjAotmEwA",
"timestamp": "2020-06-22T20:24:04.815758"
}
Managing multiple dependency versions
Github Actions is configured to test this gem against multiple versions of dependencies.
This is managed by specifying a lockfile with the correct set of dependencies. To add a new set of dependencies, copy the existing file at gemfile-locks/Gemfile.lock
and then update the dependency in that new lockfile with something like:
bundle lock --lockfile=gemfile-locks/Gemfile-activesupport-7.lock --update activesupport
The new lockfile can be added to the matrix at .github/workflows/ruby.yml
.
Contributing
- Fork it ( https://github.com/cased/cased-ruby/fork )
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create a new Pull Request