Regulator
Regulator is a clone of the Pundit gem and provides a pundit compatible DSL that has controller namespaced authorization polices instead of model namespaced.
It uses Ruby classes and object oriented design patterns to build a simple, robust and scaleable authorization system.
Existing pundit policies can be used, although they will have to be namespaced properly, or have the controller accessing set Controller.policy_class
or Controller.policy_namespace
I built this because I believe authorization should be controller-based, not model based, but really enjoyed using the Pundit DSL and I was over monkey-patching pundit in all of my projects to make it work the way I want.
Why not contribute to pundit? It's been an on going 'issue' in pundit and it doesn't look like it'll be reality.
Installation
Add this line to your application's Gemfile:
gem 'regulator'
And then execute:
$ bundle
Or install it yourself as:
$ gem install regulator
Include Regulator in your application controller:
class ApplicationController < ActionController::Base
include Regulator
protect_from_forgery
end
Generators
Install regulator
rails g regulator:install
Create a new policy and policy test/spec
rails g regulator:policy User
Regulator comes with a generator for creating an ActiveAdmin adapter
rails g regulator:activeadmin
This will create an adapter in your lib
folder.
Be sure to set the following in your ActiveAdmin initializer:
config.authorization_adapter = "ActiveAdmin::RegulatorAdapter"
# Optional
# Sets a scope for all ActiveAdmin polices to exist in
#
# Example
# app/policies/admin_policies/user_policy.rb #=> AdminPolicies::UserPolicy
#
# config.regulator_policy_namespace = "AdminPolicies"
config.regulator_policy_namespace = nil
# Optional
# Sets the default policy to use if no policy is found
#
# config.regulator_default_policy = BlackListPolicy
config.regulator_default_policy = nil
Policies
Regulator is focused around the notion of policy classes. We suggest that you put
these classes in app/policies
. This is a simple example that allows updating
a post if the user is an admin, or if the post is unpublished:
class PostPolicy
attr_reader :user, :post
def initialize(user, post)
@user = user
@post = post
end
def update?
user.admin? or not post.published?
end
end
Regulator makes the following assumptions about this class:
- The class has the name
Scope
and is nested under the policy class. - The first argument is a user. In your controller, Regulator will call the
current_user
method to retrieve what to send into this argument. - The second argument is a scope of some kind on which to perform some kind of
query. It will usually be an ActiveRecord class or a
ActiveRecord::Relation
, but it could be something else entirely. - Instances of this class respond to the method
resolve
, which should return some kind of result which can be iterated over. For ActiveRecord classes, this would usually be anActiveRecord::Relation
.
You'll probably want to inherit from the application policy scope generated by the generator, or create your own base class to inherit from:
class PostPolicy < ApplicationPolicy
class Scope < Scope
def resolve
if user.admin?
scope.all
else
scope.where(:published => true)
end
end
end
def update?
user.admin? or not post.published?
end
end
You can now use this class from your controller via the policy_scope
method:
def index
@posts = policy_scope(Post)
end
Just as with your policy, this will automatically infer that you want to use
the PostPolicy::Scope
class, it will instantiate this class and call
resolve
on the instance. In this case it is a shortcut for doing:
def index
@posts = PostPolicy::Scope.new(current_user, Post).resolve
end
You can, and are encouraged to, use this method in views:
<% policy_scope(@user.posts).each do |post| %>
<p><%= link_to post.title, post_path(post) %></p>
<% end %>
Manually specifying policy classes
Sometimes you might want to explicitly declare which policy to use for a given class, instead of letting Regulator infer it. This can be done like so:
Regulator supports the Pundit-style model "policy_class", but also implements it at the controller level. You can also set a controller's policy_namespace if you want to use an alternate namespace to the one the controller is in.
# Model level
class Post
def self.policy_class
PostablePolicy
end
end
# Controller level
class Api::Post
# By default, Regulator will look for Api::PostPolicy
def self.policy_class
PostPolicy
end
end
# Here the admin namespace could be told to use the same policy as the API namespace
class Admin::Post
# By default, Regulator will look for Admin::PostPolicy
def self.policy_class
PostPolicy
end
# You can also set it at the instance level
def policy_class
if current_user.is_a_high_paying_member?
HighClassPostPolicy
else
LowClassPostPolicy
end
end
end
class Admin::Comment
def self.policy_namespace
# Will make regulator look for ActiveAdmin::CommentPolicy instead of
# Admin::CommentPolicy
ActiveAdmin
end
end
Of course policy_namespace
and policy_class
can be used together.
Policy Namespaces
This table explains what policies Regulator will look for in different scenarios:
Controller Name | Model Name | Expected Policy |
---|---|---|
AlbumController | Album | AlbumPolicy |
Api::AlbumController | Album | Api::AlbumPolicy |
Admin::AlbumController | Album | Admin::AlbumPolicy |
Admin::AlbumController.policy_namespace = 'SuperUser' | Album | SuperUser::AlbumPolicy |
Admin::AlbumController.policy_namespace = nil | Album | AlbumPolicy |
Admin::AlbumContoller | MySongGem::Album | Admin::MySongGem::AlbumPolicy |
SongController#policy_class = TrackPolicy | Song | TrackPolicy |
SongController.policy_class = Legacy::TrackPolicy | Song | Legacy::TrackPolicy |
policy_class
at the controller-level is king. Setting it will override all logic for determining the policy to use.
ActiveAdmin Auth Adapter
There is a generator and an included adapter. Using the generator will place a more complex customizable adapter in your lib
directory.
A simple adapter is also provided, to use add the following to your active_admin initializer:
ActiveAdmin::Dependency.regulator!
require 'regulator'
require 'regulator/active_admin_adapter'
ActiveAdmin.setup do |config|
config.authorization_adapter = "Regulator::ActiveAdminAdapter"
...
Development
After checking out the repo, run bin/setup
to install dependencies. Then, run rake rspec
to run the tests. 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.
License
The gem is available as open source under the terms of the MIT License.
Contributors
- Cory O'Daniel
- All the hard work done on Pundit
Thanks to Warren G for the inspiration, bro.
TODOs
- documentation
- yard doc
- Lotus examples
- Grape examples
- ROM examples
- Custom permissions examples
- RoleModel gem examples
- rolify gem examples
- contributing wiki