/regulator

Controller-based minimal authorization through OO design and pure Ruby classes

Primary LanguageRubyOtherNOASSERTION

Regulator

Build Status Code Climate Test Coverage

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 an ActiveRecord::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

Thanks to Warren G for the inspiration, bro.

Regulator

TODOs

  • documentation
    • yard doc
    • Lotus examples
    • Grape examples
    • ROM examples
    • Custom permissions examples
    • RoleModel gem examples
    • rolify gem examples
  • contributing wiki