Bali
Bali is a to-the-point authorization library for Rails. Bali is short for Bulwark Authorization Library.
Why I created Bali?
- Defining authorization rules are complicated, I want to make it natural and so much simper like Ruby
- I want to easily segment authorization rules per roles
- I don't want to marry rules with controllers' actions
- I want an intuitive DSL
- I want to print those rules if I want, to see who can do what
- On top of that, it integrates well with Rails (also, RSpec)
Internally, Bali is quite complicated, but externally, Bali should be very easy and intuitive to use.
Supported versions
- Ruby 2.4.4 until Ruby 2.7 (trunk)
- Rails 5.0, Rails 6.0, until Rails edge (master)
Installation
Add this into your gemfile:
gem 'bali'And then execute:
$ bundle
To generate a rule class, for example for a model named User:
$ bundle rails g rules user
We can suplant User with something else
Usage
In a nutshell, authorization rules are to be defined in a class extending Bali::Rules (located in app/rules). We use can? and cant? to check against those rules we define using can, cant, can_all, and cant_all. Unscoped rules are inherited, otherwise we can scope rules by defining them within a role block.
Given a model as follows:
# == Schema Information
#
# Table name: transactions
#
# id :bigint not null, primary key
# is_settled :boolean not null
class Transaction < ApplicationRecord
alias :settled? :is_settled
endAnd given the TransactionRules defined as follows:
class TransactionRules < Bali::Rules
can :update, :unsettle
can :print
# overwrites :unsettle
can :unsettle do |record, current_user|
record.settled?
end
# will inherit update, print
role :supervisor, :accountant do
# will always be able to unsettle
can :unsettle
end
role :accountant do
# must not be able to perform an update operation
cant :update
end
role :supervisor do
can :comment
end
role :clerk do
cant_all
can :unsettle
end
role :admin do
can_all
end
endWe can do authorization in this way:
transaction = Transaction.new
TransactionRules.can?(current_user, :update, transaction)
TransactionRules.cant?(current_user, :update, transaction)
TransactionRules.can?(:archive, transaction)
TransactionRules.can?(:accept_new_transaction)Inside a controller or a view; we can also do:
if can? current_user, :update, transaction
# snip snip
endBali can automatically detect the rule class to use for such a query. That way, we don't have to manually spell out TransactionRules when it is clear that the transaction is a Transaction.
We may also omit current_user to make the call shorter and more concise:
if can? :update, transaction
# snip snip
endFor more coding examples, please take a look at the written test files. Otherwise, if you may encounter some unclear points, please feel free to suggest for edits. Thank you.
Testing through RSpec
Bali is integrated into RSpec pretty well. There's a be_able_to matcher that we can use to test the rule:
let(:transaction) { Transaction.new }
let(:accountant) { User.new(:accountant) }
# expectation on an instance of a class
it "allows accountant to print, but not update, transaction" do
expect(accountant).to be_able_to :print, transaction
expect(accountant).not_to be_able_to :update, transaction
end
# expectation on a class
it "allows User to sign in" do
expect(User).to be_able_to :sign_in
endScoping data to role
Some user can access all data, and some can only access to data belonging to them. This can be achieved by using a scope block.
An example of defining a scope block for the TransactionRules.
class TransactionRules < Bali::Rules
scope do |data, current_user|
unless current_user.role == "admin"
data.where(user_id: current_user.id)
end
end
# ...
endWe can use rule_scope to execute the scope for a given data:
transactions = TransactionRules.rule_scope(Transaction.all, current_user)Inside a controller/view, we may also omit current_user to make the call concise.
transactions = rule_scope(Transaction.all)It is important to note that a scope block must not be defined within a role block.
Usage outside of Rails
This authorization library might be used outside of Rails, such as with Grape or Sinatra projects. However, although it should work as expected, such integration is not natively supported by Bali itself. Hence, additional works for such integration is needed.
If ActiveRecord is not used, we need to add the following code into a user class.
class User
extend Bali::Statics::Record
extract_roles_from :roles
endOn the controller, we should add these helper modules:
include Bali::Statics::ScopeRuler
include Bali::Statics::AuthorizerDoing so allow us to use the can? and other functions on the controller. Bali should also be able to deduce the user's roles properly.
Printing defined roles
puts Bali::Printer.printableOr execute:
$ rails bali:print_rules
Will print, for example, this definition:
===== Transaction =====
By default
--------------------------------------------------------------------------------
1. By default can update
2. By default can unsettle, with condition
3. By default can print
Supervisor
--------------------------------------------------------------------------------
1. Supervisor can unsettle
2. Supervisor can comment
Accountant
--------------------------------------------------------------------------------
1. Accountant can unsettle
2. Accountant cant update
Clerk
--------------------------------------------------------------------------------
1. Clerk can unsettle
Admin
--------------------------------------------------------------------------------
1. Admin can do anything except if explicitly stated otherwise
===== User =====
By default
--------------------------------------------------------------------------------
1. By default can see_timeline, with condition
2. By default can sign_in, with condition
Printed at 2020-01-01 12:34AM +00:00
License
Bali is available as open source under the terms of the MIT License.
Changelog
Please refer to CHANGELOG.md to see it