mirror of
https://github.com/varvet/pundit.git
synced 2022-11-09 12:30:11 -05:00
initial
This commit is contained in:
commit
1cad0b598a
8 changed files with 342 additions and 0 deletions
17
.gitignore
vendored
Normal file
17
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
*.gem
|
||||
*.rbc
|
||||
.bundle
|
||||
.config
|
||||
.yardoc
|
||||
Gemfile.lock
|
||||
InstalledFiles
|
||||
_yardoc
|
||||
coverage
|
||||
doc/
|
||||
lib/bundler/man
|
||||
pkg
|
||||
rdoc
|
||||
spec/reports
|
||||
test/tmp
|
||||
test/version_tmp
|
||||
tmp
|
||||
4
Gemfile
Normal file
4
Gemfile
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
source 'https://rubygems.org'
|
||||
|
||||
# Specify your gem's dependencies in pundit.gemspec
|
||||
gemspec
|
||||
22
LICENSE.txt
Normal file
22
LICENSE.txt
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
Copyright (c) 2012 Jonas Nicklas
|
||||
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
271
README.md
Normal file
271
README.md
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
# Pundit
|
||||
|
||||
Pundit isn't really a library, as much as a set of helpers which simplify
|
||||
writing authorization systems for Ruby on Rails according to a pattern of using
|
||||
pure Ruby classes and object oriented design patterns.
|
||||
|
||||
## Installation
|
||||
|
||||
``` ruby
|
||||
gem "pundit"
|
||||
```
|
||||
|
||||
## Policies
|
||||
|
||||
Pundit is focused around the notion of policy classes. We suggest that you put
|
||||
these classes in `app/policies`. This is a simple example:
|
||||
|
||||
``` ruby
|
||||
class PostPolicy
|
||||
attr_reader :user, :post
|
||||
|
||||
def initialize(user, post)
|
||||
@user = user
|
||||
@post = post
|
||||
end
|
||||
|
||||
def create?
|
||||
user.admin? or not post.published?
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
As you can see, this is just a plain Ruby class. As a convenience, we can inherit
|
||||
from Struct:
|
||||
|
||||
``` ruby
|
||||
class PostPolicy < Struct.new(:user, :post)
|
||||
def create?
|
||||
user.admin? or not post.published?
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
Pundit makes the following assumptions about this class:
|
||||
|
||||
- The class has the same name as some kind of model class, only suffixed
|
||||
with the word "Policy".
|
||||
- The first argument is a user. In your controller, Pundit will call the
|
||||
`current_user` method to retrieve what to send into this argument
|
||||
- The second argument is some kind of model object, whose authorization
|
||||
you want to check. This does not need to be an ActiveRecord or even
|
||||
an ActiveModel object, it can be anything really.
|
||||
- The class implements some kind of query method, in this case `create?`.
|
||||
Usually, this will map to the name of a particular controller action.
|
||||
|
||||
That's it really.
|
||||
|
||||
Supposing that you have an instance of class `Post`, Pundit now lets you do
|
||||
this in your controller:
|
||||
|
||||
``` ruby
|
||||
def create
|
||||
@post = Post.new(params[:post])
|
||||
authorize @post
|
||||
if @post.save
|
||||
redirect_to @post
|
||||
else
|
||||
render :new
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
The authorize method automatically infers that `Post` will have a matching
|
||||
`PostPolicy` class, and instantiates this class, handing in the current user
|
||||
and the given record. It then infers from the action name, that it should call
|
||||
`create?` on this instance of the policy. In this case, you can imagine that
|
||||
`authorize` would have done something like this:
|
||||
|
||||
``` ruby
|
||||
raise "not authorized" unless PostPolicy.new(current_user, @post).create?
|
||||
```
|
||||
|
||||
You can easily get a hold of an instance of the policy through the `policy`
|
||||
method in both the view and controller. This is especially useful for
|
||||
conditionally showing links or buttons in the view:
|
||||
|
||||
``` erb
|
||||
<% if policy(@post).create? %>
|
||||
<%= link_to "New post", new_post_path %>
|
||||
<% end %>
|
||||
```
|
||||
|
||||
## Ensuring policies are used
|
||||
|
||||
Pundit adds a method called `verify_authorized` to your controllers. This
|
||||
method will raise an exception if `authorize` has not yet been called. You
|
||||
should run this method in an `after_filter` to ensure that you haven't
|
||||
forgotten to authorize the action. For example:
|
||||
|
||||
``` ruby
|
||||
class ApplicationController < ActionController::Base
|
||||
after_filter :verify_authorized, :except => :index
|
||||
end
|
||||
```
|
||||
|
||||
## Scopes
|
||||
|
||||
Often, you will want to have some kind of view listing records which a
|
||||
particular user has access to. When using Pundit, you are expected to
|
||||
define a class called a policy scope. It can look something like this:
|
||||
|
||||
``` ruby
|
||||
class PostPolicy < Struct.new(:user, :post)
|
||||
class Scope < Struct.new(:user, :scope)
|
||||
def resolve
|
||||
if user.admin?
|
||||
scope
|
||||
else
|
||||
scope.where(:published => true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def create?
|
||||
user.admin? or not post.published?
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
Pundit 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, Pundit 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 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 %>
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
|
||||
As you can see, Pundit doesn't do anything you couldn't have easily done
|
||||
yourself. It's a very small library, it just provides a few neat helpers.
|
||||
Together these give you the power of building a well structured, fully working
|
||||
authorization system without using any special DSLs or funky syntax or
|
||||
anything.
|
||||
|
||||
## The base classes
|
||||
|
||||
Pundit ships with base classes for both scopes and policies. It's important
|
||||
that you understand that these don't do *anything* special. They are just
|
||||
regular Ruby classes, and you're free not to use them if you don't want to.
|
||||
That said, they provide a good set of defaults and starting points for working
|
||||
with Pundit.
|
||||
|
||||
### Pundit::Policy
|
||||
|
||||
This class provides a constructor function, in which the first argument is
|
||||
called `user` and the second argument is called `record`. Our above example
|
||||
could have been written like this:
|
||||
|
||||
``` ruby
|
||||
class PostPolicy < Pundit::Policy
|
||||
def create?
|
||||
user.admin? or not record.published?
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
The abstraction of always calling the second argument `record`, allows us to do
|
||||
a neat trick. The `Pundit::Policy` class has an instance method called `scope`,
|
||||
which as you might guess, return an instance of the corresponding scope class.
|
||||
|
||||
We use this to provide a default for the `show?` permission, which just checks
|
||||
whether the record can be found in the scope. This way, any record which is not
|
||||
found in the scope will be automatically denied access on the show page as
|
||||
well.
|
||||
|
||||
This allows gives you the same power you get in more advanced authorization
|
||||
systems, like [cancan](https://github.com/ryanb/cancan), whereby you can define
|
||||
your permissions just once, through the scope, and they will work for both
|
||||
finding collections of records and for displaying individual records.
|
||||
|
||||
`Pundit::Policy` also implements `new?` and `edit?` as aliases to `create?` and
|
||||
`update?` respectively, so simply implementing `update?` will also define
|
||||
`edit?` at the same time.
|
||||
|
||||
We encourage you to create an `ApplicationPolicy` from which all your other
|
||||
policies inherit. It's up to you whether you want your application policy to
|
||||
inherit from `Pundit::Policy`. This way you can define your own rules for
|
||||
defaults.
|
||||
|
||||
### Pundit::Scope
|
||||
|
||||
This class really doesn't do anything. It just provides a constructor. It's
|
||||
mostly there for symmetry. You could have used it like this:
|
||||
|
||||
``` ruby
|
||||
class PostPolicy < Pundit::Policy
|
||||
class Scope < Pundit::Scope
|
||||
def resolve
|
||||
if user.admin?
|
||||
scope
|
||||
else
|
||||
scope.where(:published => true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def create?
|
||||
user.admin? or not post.published?
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Just plain old Ruby
|
||||
|
||||
Remember that all of the policy and scope classes are just plain Ruby classes,
|
||||
which means you can use the same mechanisms you always use to DRY things up.
|
||||
Encapsulate a set of permissions into a module and include them in multiple
|
||||
policies. Use `alias_method` to make some permissions behave the same as
|
||||
others. Inherit from a base set of permissions. Use metaprogramming if you
|
||||
really have to. The options are endless.
|
||||
|
||||
## Closed systems
|
||||
|
||||
In many applications, only logged in users are really able to do anything. If
|
||||
you're building such a system, it can be kind of cumbersome to check that the
|
||||
user in a policy isn't `nil` for every single permission.
|
||||
|
||||
We suggest that you define a filter that redirects unauthenticated users to the
|
||||
login page. As a secondary defence, if you've defined an ApplicationPolicy, it
|
||||
might be a good idea to raise an exception if somehow an unauthenticated user
|
||||
got through. This way you can fail more gracefully.
|
||||
|
||||
``` ruby
|
||||
class ApplicationPolicy < Pundit::Policy
|
||||
def initialize(user, record)
|
||||
raise Pundit::NotAuthorized, "must be logged in" unless user
|
||||
super
|
||||
end
|
||||
end
|
||||
```
|
||||
1
Rakefile
Normal file
1
Rakefile
Normal file
|
|
@ -0,0 +1 @@
|
|||
require "bundler/gem_tasks"
|
||||
5
lib/pundit.rb
Normal file
5
lib/pundit.rb
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
require "pundit/version"
|
||||
|
||||
module Pundit
|
||||
# Your code goes here...
|
||||
end
|
||||
3
lib/pundit/version.rb
Normal file
3
lib/pundit/version.rb
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
module Pundit
|
||||
VERSION = "0.0.1"
|
||||
end
|
||||
19
pundit.gemspec
Normal file
19
pundit.gemspec
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
lib = File.expand_path('../lib', __FILE__)
|
||||
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
||||
require 'pundit/version'
|
||||
|
||||
Gem::Specification.new do |gem|
|
||||
gem.name = "pundit"
|
||||
gem.version = Pundit::VERSION
|
||||
gem.authors = ["Jonas Nicklas"]
|
||||
gem.email = ["jonas.nicklas@gmail.com"]
|
||||
gem.description = %q{TODO: Write a gem description}
|
||||
gem.summary = %q{TODO: Write a gem summary}
|
||||
gem.homepage = ""
|
||||
|
||||
gem.files = `git ls-files`.split($/)
|
||||
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
||||
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
||||
gem.require_paths = ["lib"]
|
||||
end
|
||||
Loading…
Add table
Add a link
Reference in a new issue