A Cased client for Ruby applications in your organization to control and monitor the access of information within your organization.
Add this line to your application's Gemfile:
gem 'cased-ruby'And then execute:
$ bundle
Or install it yourself as:
$ gem install cased-ruby
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.metadata = {
rails_env: ENV['RAILS_ENV'],
heroku_application: ENV['HEROKU_APP_NAME'],
git_commit: ENV['GIT_COMMIT'],
}
endKeep any command line tool available as your team grows — monitor usage, require peer approvals for sensitive operations, and receive intelligent alerts to suspicious activity.
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.'
endIf 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.'
endIf 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.'
endYou 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.
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.metadata = {
rails_env: ENV['RAILS_ENV'],
heroku_application: ENV['HEROKU_APP_NAME'],
git_commit: ENV['GIT_COMMIT'],
}
endNote: Metadata specified in the CLI request overrides any configured globally.
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.chargeBoth 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"
}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? # => falseTo 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
endExporting 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.eyJ1c2VyX2lkIjoidXNlcl8xZFFwWThiQmdFd2RwbWRwVnJydER6TVg0ZkgiLCJIf 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('john@organization.com', label: :email)
)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)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)
endOr you can configure the entire process to disable publishing events.
CASED_DISABLE_PUBLISHING=1 bundle exec ruby crawl.rb
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.clearcased-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
endOut 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
endOr 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
endBoth 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
endCased::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
endCased::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.chargeResults 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"
}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.
- 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