1
0
Fork 0
mirror of https://github.com/varvet/pundit.git synced 2022-11-09 12:30:11 -05:00
This commit is contained in:
Jonas Nicklas 2012-11-04 10:20:45 +01:00
commit 1cad0b598a
8 changed files with 342 additions and 0 deletions

17
.gitignore vendored Normal file
View 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
View file

@ -0,0 +1,4 @@
source 'https://rubygems.org'
# Specify your gem's dependencies in pundit.gemspec
gemspec

22
LICENSE.txt Normal file
View 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
View 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
View file

@ -0,0 +1 @@
require "bundler/gem_tasks"

5
lib/pundit.rb Normal file
View file

@ -0,0 +1,5 @@
require "pundit/version"
module Pundit
# Your code goes here...
end

3
lib/pundit/version.rb Normal file
View file

@ -0,0 +1,3 @@
module Pundit
VERSION = "0.0.1"
end

19
pundit.gemspec Normal file
View 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