coryodaniel/regulator

View on GitHub
README.md

Summary

Maintainability
Test Coverage
# Regulator
[![Build Status](https://travis-ci.org/coryodaniel/regulator.svg)](https://travis-ci.org/coryodaniel/regulator)
[![Code Climate](https://codeclimate.com/github/coryodaniel/regulator/badges/gpa.svg)](https://codeclimate.com/github/coryodaniel/regulator)
[![Test Coverage](https://codeclimate.com/github/coryodaniel/regulator/badges/coverage.svg)](https://codeclimate.com/github/coryodaniel/regulator/coverage)

Regulator is a clone of the [Pundit](https://github.com/elabs/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](https://gist.github.com/Systho/3d7632b5aa999cf88d87) pundit in all of my projects to make it work the way I want.

Why not contribute to pundit? [It's](https://github.com/elabs/pundit/issues/12) [been](https://github.com/elabs/pundit/issues/178) an [on going](https://github.com/elabs/pundit/search?q=namespace&type=Issues&utf8=%E2%9C%93) 'issue' in pundit and it doesn't look [like it'll be reality.](https://github.com/elabs/pundit/pull/190#issuecomment-53052356)

## Installation

Add this line to your application's Gemfile:

```ruby
gem 'regulator'
```
And then execute:

    $ bundle

Or install it yourself as:

    $ gem install regulator

Include Regulator in your application controller:

``` ruby
class ApplicationController < ActionController::Base
  include Regulator
  protect_from_forgery
end
```

## Generators

Install regulator
``` sh
  rails g regulator:install
```

Create a new policy and policy test/spec
``` sh
  rails g regulator:policy User
```

Regulator comes with a generator for creating an ActiveAdmin adapter
``` sh
  rails g regulator:activeadmin
```

This will create an adapter in your ```lib``` folder.

Be sure to set the following in your ActiveAdmin initializer:
```ruby
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:

``` ruby
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:

``` ruby
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:

``` ruby
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:

``` ruby
def index
  @posts = PostPolicy::Scope.new(current_user, Post).resolve
end
```

You can, and are encouraged to, use this method in views:

``` erb
<% 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.


``` ruby
# Model level
class Post
  def self.policy_class
    PostablePolicy
  end
end
```

``` ruby
# Controller level
class Api::Post
  # By default, Regulator will look for Api::PostPolicy
  def self.policy_class
    PostPolicy
  end
end
```

``` ruby
# 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
```

``` ruby
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:
``` ruby
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](https://rubygems.org).

## License

The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).

## Contributors
  * [Cory O'Daniel](http://linkedin.com/in/coryodaniel)
  * All the hard work done on [Pundit](https://github.com/elabs/pundit)

  Thanks to Warren G for the inspiration, bro.

  ![Regulator](https://upload.wikimedia.org/wikipedia/commons/a/ac/Nat_Powers_%26_Warren_G.jpg)

## TODOs
  * [ ] documentation
    * [ ] yard doc
    * [ ] Lotus examples
    * [ ] Grape examples
    * [ ] ROM examples
    * [ ] Custom permissions examples
    * [ ] RoleModel gem examples
    * [ ] rolify gem examples
  * [ ] contributing wiki