PickyGuard
PickyGuard is an opinionated authorization library which wraps CanCanCan.
This library helps to write authorization policies in an opinionated hierarchy.
Briefly,
- User has many roles.
- Each role has many policies.
- Each policy has many statements describing which actions has what effect on which resources.
For example,
- User
Paulhas a role namedCampaignManager. - The role
CampaignManagerhas a policy namedcrud_all_campaigns. - The policy
crud_all_campaignsmeans,- Actions :
[:read, :update, :create, :delete]are - Effect :
Allowed - Resources : for
All campaigns under user's company.
- Actions :
Installation
Add this line to your application's Gemfile:
gem 'picky_guard'And then execute:
$ bundle
Or install it yourself as:
$ gem install picky_guard
Usage
To generate initial files, execute:
$ rails generate picky_guard:install
This will create the following files:
app/
- models/
- ability.rb
- picky_guard/
- role_policies.rb
- resource_actions.rb
- user_role_checker.rb
Generated Files
ability.rb
The generated file is like this:
class Ability < PickyGuard::Loader
def initialize(user)
adjust(user, UserRoleChecker, ResourceActions, RolePolicies)
end
endNormally, you don't have to do anything about it.
user_role_checker.rb
The generated file is like this:
class UserRoleChecker < PickyGuard::UserRoleChecker
def self.check(user, role)
# ...
end
endThis class defines the way to check if user has specific role. It assumes some roles already have been given to the user somehow.
You can implement this on your own, or if you're using a gem like rolify, then it should be like this:
class UserRoleChecker < PickyGuard::UserRoleChecker
def self.check(user, role)
user.has_role? role
end
endresource_actions.rb
The generated file is like this:
class ResourceActions < PickyGuard::ResourceActions
def initialize
map Report, [:create, :read, :update, :delete]
end
endThis class defines which resource can have which actions. Actions can be an array of either String or Symbol.
By defining this, you can explicitly manage list of actions per resource and filter out unexpected and unknown actions.
role_policies.rb
The generated file is like this:
class RolePolicies < PickyGuard::RolePolicies
def initialize
map :report_manager, [ManageAllReports]
# map :report_reader, [AnotherPolicy]
end
endThis class defines which role has which policies. The method map takes two parameters.
-
role: It can be a string or a symbol -
policies: An array of policies
From the example code above, we could assume there is a role named :report_manager and it has one policy named ManageAllReports.
Then how do we define policy?
Defining Policies
To generate new policy, execute this:
$ rails generate picky_guard:policy manage_all_reports
From the command line, name should be underscored. Otherwise, it will raise an error.
Once created, you will find the policy file under app/picky_guard/policies/.
If you get to have many policies, you can group them into a folder like this:
$ rails generate picky_guard:policy reports/manage_all_reports
Then it will generate app/picky_guard/policies/reports/manage_all_reports.rb.
Here is a sample of policy.
class ManageAllReports < PickyGuard::Policy
def initialize(current_user)
statement_for Campaign do
allow
actions [:read]
conditions({})
end
statement_for Campaign do
allow
actions [:create]
class_resource
end
end
endregister method takes a parameter and a block.
- The parameter is a resource class. It should extend
ActiveRecord::Base. - The block consists of simple DSL, describing the statement.
Building Statement
There are two types of resources: instance resource and class resource.
can? :read, Campaign.first # Checking permission against an instance resource
can? :create, Campaign # Checking permission against a class resourceInstance Resource
In case of instance resource, we need
- effect(
allowordeny) - actions
- conditions
statement_for Campaign do # Instances of `Campaign` are the resources.
allow # Possibly `deny` instead of `allow`. If omitted, it's `allow` by default.
actions [:create] # Array of `string` or `symbol`.
instance_resource # If omitted, it's an instance resource by default.
conditions({})
endIn a short way,
statement_for Campaign do
actions [:create]
conditions({})
endClass Resource
In case of class resource, we need
- effect
- actions
- class_resource
statement_for Campaign do # `Campaign` is the resource.
allow # Possibly `deny` instead of `allow`. If omitted, it's `allow` by default.
actions [:create] # Array of `string` or `symbol`.
class_resource # You need this explicit declaration when it comes to a class resource.
endYou cannot specify any conditions on class resource.
In a short way,
statement_for Campaign do
actions [:create]
class_resource
end
conditions on instance resource
conditions is a hash. This is directly used to query database, so it should be real database column names. You can refer to Hash of Conditions section from Defining Abilities - CanCanCan.
When things are too complicated and it's hard to express it a hash, then there's a little detour.
ids = extract_campaign_ids_somehow
statement_for Campaign do
actions [:create]
conditions({ id: ids })
endFirst, you can extract ids or other values through some complicated business logic of yours. Then, pass it to conditions like the above.
However we can make this better by wrapping the conditions with proc. This enables lazy-loading.
statement_for Campaign do
actions [:create]
conditions(proc {
ids = extract_campaign_ids_somehow
{ id: ids }
})
endSo basically this conditions method takes a hash or a proc returning a hash as a parameter.
Using Ability
You can use Ability class just as you did with CanCanCan. The constructor takes one parameter: user.
With PickyGuard, you can pass optional second parameter which is resource.
Ability.new(user, Campaign).can? :read, Campaign.firstThis will load only relevant policies.
Development
After checking out the repo, run bin/setup to install dependencies. You can also run bin/console for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.
or
gem install gem-release
gem bump --version NEW_VERSION
gem release
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/eunjae-lee/picky_guard.