pundit-2.0.0/0000755000004100000410000000000013337760034013052 5ustar www-datawww-datapundit-2.0.0/.travis.yml0000644000004100000410000000063013337760034015162 0ustar www-datawww-datalanguage: ruby sudo: false before_install: - gem update --system - gem install bundler matrix: include: - rvm: 2.5.1 script: bundle exec rake rubocop # ONLY lint once, first - rvm: 2.1 - rvm: 2.2.8 - rvm: 2.3.5 - rvm: 2.4.2 - rvm: 2.5.1 - rvm: jruby-9.1.8.0 env: - JRUBY_OPTS="--debug" - rvm: jruby-9.2.0.0 env: - JRUBY_OPTS="--debug" pundit-2.0.0/README.md0000644000004100000410000005471213337760034014342 0ustar www-datawww-data# Pundit [![Build Status](https://secure.travis-ci.org/varvet/pundit.svg?branch=master)](https://travis-ci.org/varvet/pundit) [![Code Climate](https://codeclimate.com/github/varvet/pundit.svg)](https://codeclimate.com/github/varvet/pundit) [![Inline docs](http://inch-ci.org/github/varvet/pundit.svg?branch=master)](http://inch-ci.org/github/varvet/pundit) [![Gem Version](https://badge.fury.io/rb/pundit.svg)](http://badge.fury.io/rb/pundit) Pundit provides a set of helpers which guide you in leveraging regular Ruby classes and object oriented design patterns to build a simple, robust and scaleable authorization system. Links: - [API documentation](http://www.rubydoc.info/gems/pundit) - [Source Code](https://github.com/varvet/pundit) - [Contributing](https://github.com/varvet/pundit/blob/master/CONTRIBUTING.md) - [Code of Conduct](https://github.com/varvet/pundit/blob/master/CODE_OF_CONDUCT.md) Sponsored by: [Varvet](https://www.varvet.com) ## Installation ``` ruby gem "pundit" ``` Include Pundit in your application controller: ``` ruby class ApplicationController < ActionController::Base include Pundit protect_from_forgery end ``` Optionally, you can run the generator, which will set up an application policy with some useful defaults for you: ``` sh rails g pundit:install ``` After generating your application policy, restart the Rails server so that Rails can pick up any classes in the new `app/policies/` directory. ## 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 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 ``` As you can see, this is just a plain Ruby class. 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 `update?`. Usually, this will map to the name of a particular controller action. That's it really. Usually you'll want to inherit from the application policy created by the generator, or set up your own base class to inherit from: ``` ruby class PostPolicy < ApplicationPolicy def update? user.admin? or not record.published? end end ``` In the generated `ApplicationPolicy`, the model object is called `record`. Supposing that you have an instance of class `Post`, Pundit now lets you do this in your controller: ``` ruby def update @post = Post.find(params[:id]) authorize @post if @post.update(post_params) redirect_to @post else render :edit 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 `update?` on this instance of the policy. In this case, you can imagine that `authorize` would have done something like this: ``` ruby unless PostPolicy.new(current_user, @post).update? raise Pundit::NotAuthorizedError, "not allowed to update? this #{@post.inspect}" end ``` You can pass a second argument to `authorize` if the name of the permission you want to check doesn't match the action name. For example: ``` ruby def publish @post = Post.find(params[:id]) authorize @post, :update? @post.publish! redirect_to @post end ``` You can pass an argument to override the policy class if necessary. For example: ```ruby def create @publication = find_publication # assume this method returns any model that behaves like a publication # @publication.class => Post authorize @publication, policy_class: PublicationPolicy @publication.publish! redirect_to @publication end ``` If you don't have an instance for the first argument to `authorize`, then you can pass the class. For example: Policy: ```ruby class PostPolicy < ApplicationPolicy def admin_list? user.admin? end end ``` Controller: ```ruby def admin_list authorize Post # we don't have a particular post to authorize # Rest of controller action end ``` `authorize` returns the object passed to it, so you can chain it like this: Controller: ```ruby def show @user = authorize User.find(params[:id]) end ``` 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).update? %> <%= link_to "Edit post", edit_post_path(@post) %> <% end %> ``` ## Headless policies Given there is a policy without a corresponding model / ruby class, you can retrieve it by passing a symbol. ```ruby # app/policies/dashboard_policy.rb class DashboardPolicy < Struct.new(:user, :dashboard) # ... end ``` ```ruby # In controllers authorize :dashboard, :show? ``` ```erb # In views <% if policy(:dashboard).show? %> <%= link_to 'Dashboard', dashboard_path %> <% 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 < ApplicationPolicy class Scope attr_reader :user, :scope def initialize(user, scope) @user = user @scope = scope end def resolve if user.admin? scope.all else scope.where(published: true) end end end def update? user.admin? or not record.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'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 record.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 def show @post = policy_scope(Post).find(params[:id]) end ``` Like with the authorize method, you can also override the policy scope class: ``` ruby def index # publication_class => Post @publications = policy_scope(publication_class, policy_scope_class: PublicationPolicy::Scope) 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| %>

<%= link_to post.title, post_path(post) %>

<% end %> ``` ## Ensuring policies and scopes are used When you are developing an application with Pundit it can be easy to forget to authorize some action. People are forgetful after all. Since Pundit encourages you to add the `authorize` call manually to each controller action, it's really easy to miss one. Thankfully, Pundit has a handy feature which reminds you in case you forget. Pundit tracks whether you have called `authorize` anywhere in your controller action. Pundit also adds a method to your controllers called `verify_authorized`. This method will raise an exception if `authorize` has not yet been called. You should run this method in an `after_action` hook to ensure that you haven't forgotten to authorize the action. For example: ``` ruby class ApplicationController < ActionController::Base include Pundit after_action :verify_authorized end ``` Likewise, Pundit also adds `verify_policy_scoped` to your controller. This will raise an exception similar to `verify_authorized`. However, it tracks if `policy_scope` is used instead of `authorize`. This is mostly useful for controller actions like `index` which find collections with a scope and don't authorize individual instances. ``` ruby class ApplicationController < ActionController::Base include Pundit after_action :verify_authorized, except: :index after_action :verify_policy_scoped, only: :index end ``` **This verification mechanism only exists to aid you while developing your application, so you don't forget to call `authorize`. It is not some kind of failsafe mechanism or authorization mechanism. You should be able to remove these filters without affecting how your app works in any way.** Some people have found this feature confusing, while many others find it extremely helpful. If you fall into the category of people who find it confusing then you do not need to use it. Pundit will work just fine without using `verify_authorized` and `verify_policy_scoped`. ### Conditional verification If you're using `verify_authorized` in your controllers but need to conditionally bypass verification, you can use `skip_authorization`. For bypassing `verify_policy_scoped`, use `skip_policy_scope`. These are useful in circumstances where you don't want to disable verification for the entire action, but have some cases where you intend to not authorize. ```ruby def show record = Record.find_by(attribute: "value") if record.present? authorize record else skip_authorization end end ``` ## Manually specifying policy classes Sometimes you might want to explicitly declare which policy to use for a given class, instead of letting Pundit infer it. This can be done like so: ``` ruby class Post def self.policy_class PostablePolicy end end ``` ## Just plain old Ruby 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. 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. ## Generator Use the supplied generator to generate policies: ``` sh rails g pundit:policy post ``` ## 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. Aside from policies, you can add this check to the base class for scopes. 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 def initialize(user, record) raise Pundit::NotAuthorizedError, "must be logged in" unless user @user = user @record = record end class Scope attr_reader :user, :scope def initialize(user, scope) raise Pundit::NotAuthorizedError, "must be logged in" unless user @user = user @scope = scope end end end ``` ## NilClassPolicy To support a [null object pattern](https://en.wikipedia.org/wiki/Null_Object_pattern) you may find that you want to implement a `NilClassPolicy`. This might be useful where you want to extend your ApplicationPolicy to allow some tolerance of, for example, associations which might be `nil`. ```ruby class NilClassPolicy < ApplicationPolicy class Scope < Scope def resolve raise Pundit::NotDefinedError, "Cannot scope NilClass" end end def show? false # Nobody can see nothing end end ``` ## Rescuing a denied Authorization in Rails Pundit raises a `Pundit::NotAuthorizedError` you can [rescue_from](http://guides.rubyonrails.org/action_controller_overview.html#rescue-from) in your `ApplicationController`. You can customize the `user_not_authorized` method in every controller. ```ruby class ApplicationController < ActionController::Base protect_from_forgery include Pundit rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized private def user_not_authorized flash[:alert] = "You are not authorized to perform this action." redirect_to(request.referrer || root_path) end end ``` Alternatively, you can globally handle Pundit::NotAuthorizedError's by having rails handle them as a 403 error and serving a 403 error page. Add the following to application.rb: ```config.action_dispatch.rescue_responses["Pundit::NotAuthorizedError"] = :forbidden``` ## Creating custom error messages `NotAuthorizedError`s provide information on what query (e.g. `:create?`), what record (e.g. an instance of `Post`), and what policy (e.g. an instance of `PostPolicy`) caused the error to be raised. One way to use these `query`, `record`, and `policy` properties is to connect them with `I18n` to generate error messages. Here's how you might go about doing that. ```ruby class ApplicationController < ActionController::Base rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized private def user_not_authorized(exception) policy_name = exception.policy.class.to_s.underscore flash[:error] = t "#{policy_name}.#{exception.query}", scope: "pundit", default: :default redirect_to(request.referrer || root_path) end end ``` ```yaml en: pundit: default: 'You cannot perform this action.' post_policy: update?: 'You cannot edit this post!' create?: 'You cannot create posts!' ``` Of course, this is just an example. Pundit is agnostic as to how you implement your error messaging. ## Manually retrieving policies and scopes Sometimes you want to retrieve a policy for a record outside the controller or view. For example when you delegate permissions from one policy to another. You can easily retrieve policies and scopes like this: ``` ruby Pundit.policy!(user, post) Pundit.policy(user, post) Pundit.policy_scope!(user, Post) Pundit.policy_scope(user, Post) ``` The bang methods will raise an exception if the policy does not exist, whereas those without the bang will return nil. ## Customize Pundit user In some cases your controller might not have access to `current_user`, or your `current_user` is not the method that should be invoked by Pundit. Simply define a method in your controller called `pundit_user`. ```ruby def pundit_user User.find_by_other_means end ``` ## Policy Namespacing In some cases it might be helpful to have multiple policies that serve different contexts for a resource. A prime example of this is the case where User policies differ from Admin policies. To authorize with a namespaced policy, pass the namespace into the `authorize` helper in an array: ```ruby authorize(post) # => will look for a PostPolicy authorize([:admin, post]) # => will look for an Admin::PostPolicy authorize([:foo, :bar, post]) # => will look for a Foo::Bar::PostPolicy policy_scope(Post) # => will look for a PostPolicy::Scope policy_scope([:admin, Post]) # => will look for an Admin::PostPolicy::Scope policy_scope([:foo, :bar, Post]) # => will look for a Foo::Bar::PostPolicy::Scope ``` If you are using namespaced policies for something like Admin views, it can be useful to override the `policy_scope` and `authorize` helpers in your `AdminController` to automatically apply the namespacing: ```ruby class AdminController < ApplicationController def policy_scope(scope) super([:admin, scope]) end def authorize(record, query = nil) super([:admin, record], query) end end class Admin::PostController < AdminController def index policy_scope(Post) end def show post = Post.find(params[:id]) authorize(post) end end ``` ## Additional context Pundit strongly encourages you to model your application in such a way that the only context you need for authorization is a user object and a domain model that you want to check authorization for. If you find yourself needing more context than that, consider whether you are authorizing the right domain model, maybe another domain model (or a wrapper around multiple domain models) can provide the context you need. Pundit does not allow you to pass additional arguments to policies for precisely this reason. However, in very rare cases, you might need to authorize based on more context than just the currently authenticated user. Suppose for example that authorization is dependent on IP address in addition to the authenticated user. In that case, one option is to create a special class which wraps up both user and IP and passes it to the policy. ``` ruby class UserContext attr_reader :user, :ip def initialize(user, ip) @user = user @ip = ip end end class ApplicationController include Pundit def pundit_user UserContext.new(current_user, request.ip) end end ``` ## Strong parameters In Rails 4 (or Rails 3.2 with the [strong_parameters](https://github.com/rails/strong_parameters) gem), mass-assignment protection is handled in the controller. With Pundit you can control which attributes a user has access to update via your policies. You can set up a `permitted_attributes` method in your policy like this: ```ruby # app/policies/post_policy.rb class PostPolicy < ApplicationPolicy def permitted_attributes if user.admin? || user.owner_of?(post) [:title, :body, :tag_list] else [:tag_list] end end end ``` You can now retrieve these attributes from the policy: ```ruby # app/controllers/posts_controller.rb class PostsController < ApplicationController def update @post = Post.find(params[:id]) if @post.update_attributes(post_params) redirect_to @post else render :edit end end private def post_params params.require(:post).permit(policy(@post).permitted_attributes) end end ``` However, this is a bit cumbersome, so Pundit provides a convenient helper method: ```ruby # app/controllers/posts_controller.rb class PostsController < ApplicationController def update @post = Post.find(params[:id]) if @post.update_attributes(permitted_attributes(@post)) redirect_to @post else render :edit end end end ``` If you want to permit different attributes based on the current action, you can define a `permitted_attributes_for_#{action}` method on your policy: ```ruby # app/policies/post_policy.rb class PostPolicy < ApplicationPolicy def permitted_attributes_for_create [:title, :body] end def permitted_attributes_for_edit [:body] end end ``` If you have defined an action-specific method on your policy for the current action, the `permitted_attributes` helper will call it instead of calling `permitted_attributes` on your controller. If you need to fetch parameters based on namespaces different from the suggested one, override the below method, in your controller, and return an instance of `ActionController::Parameters`. ```ruby def pundit_params_for(record) params.require(PolicyFinder.new(record).param_key) end ``` For example: ```ruby # If you don't want to use require def pundit_params_for(record) params.fetch(PolicyFinder.new(record).param_key, {}) end # If you are using something like the JSON API spec def pundit_params_for(_record) params.fetch(:data, {}).fetch(:attributes, {}) end ``` ## RSpec ### Policy Specs Pundit includes a mini-DSL for writing expressive tests for your policies in RSpec. Require `pundit/rspec` in your `spec_helper.rb`: ``` ruby require "pundit/rspec" ``` Then put your policy specs in `spec/policies`, and make them look somewhat like this: ``` ruby describe PostPolicy do subject { described_class } permissions :update?, :edit? do it "denies access if post is published" do expect(subject).not_to permit(User.new(admin: false), Post.new(published: true)) end it "grants access if post is published and user is an admin" do expect(subject).to permit(User.new(admin: true), Post.new(published: true)) end it "grants access if post is unpublished" do expect(subject).to permit(User.new(admin: false), Post.new(published: false)) end end end ``` An alternative approach to Pundit policy specs is scoping them to a user context as outlined in this [excellent post](http://thunderboltlabs.com/blog/2013/03/27/testing-pundit-policies-with-rspec/) and implemented in the third party [pundit-matchers](https://github.com/chrisalley/pundit-matchers) gem. ### Scope Specs Pundit does not provide a DSL for testing scopes. Just test it like a regular Ruby class! # External Resources - [RailsApps Example Application: Pundit and Devise](https://github.com/RailsApps/rails-devise-pundit) - [Migrating to Pundit from CanCan](http://blog.carbonfive.com/2013/10/21/migrating-to-pundit-from-cancan/) - [Testing Pundit Policies with RSpec](http://thunderboltlabs.com/blog/2013/03/27/testing-pundit-policies-with-rspec/) - [Using Pundit outside of a Rails controller](https://github.com/varvet/pundit/pull/136) - [Straightforward Rails Authorization with Pundit](http://www.sitepoint.com/straightforward-rails-authorization-with-pundit/) # License Licensed under the MIT license, see the separate LICENSE.txt file. pundit-2.0.0/spec/0000755000004100000410000000000013337760034014004 5ustar www-datawww-datapundit-2.0.0/spec/policy_finder_spec.rb0000644000004100000410000000627213337760034020200 0ustar www-datawww-datarequire "spec_helper" describe Pundit::PolicyFinder do let(:user) { double } let(:post) { Post.new(user) } let(:comment) { CommentFourFiveSix.new } let(:article) { Article.new } describe "#scope" do subject { described_class.new(post) } it "returns a policy scope" do expect(subject.scope).to eq PostPolicy::Scope end context "policy is nil" do it "returns nil" do allow(subject).to receive(:policy).and_return nil expect(subject.scope).to eq nil end end end describe "#policy" do subject { described_class.new(post) } it "returns a policy" do expect(subject.policy).to eq PostPolicy end context "with a string" do it "returns a policy" do allow(subject).to receive(:find).and_return "PostPolicy" expect(subject.policy).to eq PostPolicy end end context "with a class" do it "returns a policy" do allow(subject).to receive(:find).and_return PostPolicy expect(subject.policy).to eq PostPolicy end end context "with nil" do it "returns nil" do allow(subject).to receive(:find).and_return nil expect(subject.policy).to eq nil end end context "with a string that can't be constantized" do it "returns nil" do allow(subject).to receive(:find).and_return "FooPolicy" expect(subject.policy).to eq nil end end end describe "#scope!" do context "@object is nil" do subject { described_class.new(nil) } it "returns the NilClass policy's scope class" do expect(subject.scope!).to eq NilClassPolicy::Scope end end context "@object is defined" do subject { described_class.new(post) } it "returns the scope" do expect(subject.scope!).to eq PostPolicy::Scope end end end describe "#param_key" do context "object responds to model_name" do subject { described_class.new(comment) } it "returns the param_key" do expect(subject.object).to respond_to(:model_name) expect(subject.param_key).to eq "comment_four_five_six" end end context "object is a class" do subject { described_class.new(Article) } it "returns the param_key" do expect(subject.object).not_to respond_to(:model_name) expect(subject.object).to be_a Class expect(subject.param_key).to eq "article" end end context "object is an instance of a class" do subject { described_class.new(article) } it "returns the param_key" do expect(subject.object).not_to respond_to(:model_name) expect(subject.object).not_to be_a Class expect(subject.object).to be_an_instance_of Article expect(subject.param_key).to eq "article" end end context "object is an array" do subject { described_class.new([:project, article]) } it "returns the param_key for the last element of the array" do expect(subject.object).not_to respond_to(:model_name) expect(subject.object).not_to be_a Class expect(subject.object).to be_an_instance_of Array expect(subject.param_key).to eq "article" end end end end pundit-2.0.0/spec/policies/0000755000004100000410000000000013337760034015613 5ustar www-datawww-datapundit-2.0.0/spec/policies/post_policy_spec.rb0000644000004100000410000000100113337760034021506 0ustar www-datawww-datarequire "spec_helper" describe PostPolicy do let(:user) { double } let(:own_post) { double(user: user) } let(:other_post) { double(user: double) } subject { described_class } permissions :update?, :show? do it "is successful when all permissions match" do should permit(user, own_post) end it "fails when any permissions do not match" do expect do should permit(user, other_post) end.to raise_error(RSpec::Expectations::ExpectationNotMetError) end end end pundit-2.0.0/spec/pundit_spec.rb0000644000004100000410000005150013337760034016647 0ustar www-datawww-datarequire "spec_helper" describe Pundit do let(:user) { double } let(:post) { Post.new(user) } let(:customer_post) { Customer::Post.new(user) } let(:post_four_five_six) { PostFourFiveSix.new(user) } let(:comment) { Comment.new } let(:comment_four_five_six) { CommentFourFiveSix.new } let(:article) { Article.new } let(:controller) { Controller.new(user, "update", {}) } let(:artificial_blog) { ArtificialBlog.new } let(:article_tag) { ArticleTag.new } let(:comments_relation) { CommentsRelation.new } let(:empty_comments_relation) { CommentsRelation.new(true) } let(:tag_four_five_six) { ProjectOneTwoThree::TagFourFiveSix.new(user) } let(:avatar_four_five_six) { ProjectOneTwoThree::AvatarFourFiveSix.new } let(:wiki) { Wiki.new } describe ".authorize" do it "infers the policy and authorizes based on it" do expect(Pundit.authorize(user, post, :update?)).to be_truthy end it "can be given a different policy class" do expect(Pundit.authorize(user, post, :create?, policy_class: PublicationPolicy)).to be_truthy end it "works with anonymous class policies" do expect(Pundit.authorize(user, article_tag, :show?)).to be_truthy expect { Pundit.authorize(user, article_tag, :destroy?) }.to raise_error(Pundit::NotAuthorizedError) end it "raises an error with a query and action" do # rubocop:disable Style/MultilineBlockChain expect do Pundit.authorize(user, post, :destroy?) end.to raise_error(Pundit::NotAuthorizedError, "not allowed to destroy? this #") do |error| expect(error.query).to eq :destroy? expect(error.record).to eq post expect(error.policy).to eq Pundit.policy(user, post) end # rubocop:enable Style/MultilineBlockChain end it "raises an error with a invalid policy constructor" do expect do Pundit.authorize(user, wiki, :update?) end.to raise_error(Pundit::InvalidConstructorError, "Invalid # constructor is called") end end describe ".policy_scope" do it "returns an instantiated policy scope given a plain model class" do expect(Pundit.policy_scope(user, Post)).to eq :published end it "returns an instantiated policy scope given an active model class" do expect(Pundit.policy_scope(user, Comment)).to eq CommentScope.new(Comment) end it "returns an instantiated policy scope given an active record relation" do expect(Pundit.policy_scope(user, comments_relation)).to eq CommentScope.new(comments_relation) end it "returns an instantiated policy scope given an empty active record relation" do expect(Pundit.policy_scope(user, empty_comments_relation)).to eq CommentScope.new(empty_comments_relation) end it "returns an instantiated policy scope given an array of a symbol and plain model class" do expect(Pundit.policy_scope(user, [:project, Post])).to eq :read end it "returns an instantiated policy scope given an array of a symbol and active model class" do expect(Pundit.policy_scope(user, [:project, Comment])).to eq Comment end it "returns nil if the given policy scope can't be found" do expect(Pundit.policy_scope(user, Article)).to be_nil end it "raises an exception if nil object given" do expect { Pundit.policy_scope(user, nil) }.to raise_error(Pundit::NotDefinedError) end it "raises an error with a invalid policy scope constructor" do expect do Pundit.policy_scope(user, Wiki) end.to raise_error(Pundit::InvalidConstructorError, "Invalid # constructor is called") end end describe ".policy_scope!" do it "returns an instantiated policy scope given a plain model class" do expect(Pundit.policy_scope!(user, Post)).to eq :published end it "returns an instantiated policy scope given an active model class" do expect(Pundit.policy_scope!(user, Comment)).to eq CommentScope.new(Comment) end it "throws an exception if the given policy scope can't be found" do expect { Pundit.policy_scope!(user, Article) }.to raise_error(Pundit::NotDefinedError) end it "throws an exception if the given policy scope can't be found" do expect { Pundit.policy_scope!(user, ArticleTag) }.to raise_error(Pundit::NotDefinedError) end it "throws an exception if the given policy scope is nil" do expect do Pundit.policy_scope!(user, nil) end.to raise_error(Pundit::NotDefinedError, "Cannot scope NilClass") end it "returns an instantiated policy scope given an array of a symbol and plain model class" do expect(Pundit.policy_scope!(user, [:project, Post])).to eq :read end it "returns an instantiated policy scope given an array of a symbol and active model class" do expect(Pundit.policy_scope!(user, [:project, Comment])).to eq Comment end it "raises an error with a invalid policy scope constructor" do expect do Pundit.policy_scope(user, Wiki) end.to raise_error(Pundit::InvalidConstructorError, "Invalid # constructor is called") end end describe ".policy" do it "returns an instantiated policy given a plain model instance" do policy = Pundit.policy(user, post) expect(policy.user).to eq user expect(policy.post).to eq post end it "returns an instantiated policy given an active model instance" do policy = Pundit.policy(user, comment) expect(policy.user).to eq user expect(policy.comment).to eq comment end it "returns an instantiated policy given a plain model class" do policy = Pundit.policy(user, Post) expect(policy.user).to eq user expect(policy.post).to eq Post end it "returns an instantiated policy given an active model class" do policy = Pundit.policy(user, Comment) expect(policy.user).to eq user expect(policy.comment).to eq Comment end it "returns an instantiated policy given a symbol" do policy = Pundit.policy(user, :criteria) expect(policy.class).to eq CriteriaPolicy expect(policy.user).to eq user expect(policy.criteria).to eq :criteria end it "returns an instantiated policy given an array of symbols" do policy = Pundit.policy(user, %i[project criteria]) expect(policy.class).to eq Project::CriteriaPolicy expect(policy.user).to eq user expect(policy.criteria).to eq :criteria end it "returns an instantiated policy given an array of a symbol and plain model instance" do policy = Pundit.policy(user, [:project, post]) expect(policy.class).to eq Project::PostPolicy expect(policy.user).to eq user expect(policy.post).to eq post end it "returns an instantiated policy given an array of a symbol and a model instance with policy_class override" do policy = Pundit.policy(user, [:project, customer_post]) expect(policy.class).to eq Project::PostPolicy expect(policy.user).to eq user expect(policy.post).to eq customer_post end it "returns an instantiated policy given an array of a symbol and an active model instance" do policy = Pundit.policy(user, [:project, comment]) expect(policy.class).to eq Project::CommentPolicy expect(policy.user).to eq user expect(policy.comment).to eq comment end it "returns an instantiated policy given an array of a symbol and a plain model class" do policy = Pundit.policy(user, [:project, Post]) expect(policy.class).to eq Project::PostPolicy expect(policy.user).to eq user expect(policy.post).to eq Post end it "raises an error with a invalid policy constructor" do expect do Pundit.policy(user, Wiki) end.to raise_error(Pundit::InvalidConstructorError, "Invalid # constructor is called") end it "returns an instantiated policy given an array of a symbol and an active model class" do policy = Pundit.policy(user, [:project, Comment]) expect(policy.class).to eq Project::CommentPolicy expect(policy.user).to eq user expect(policy.comment).to eq Comment end it "returns an instantiated policy given an array of a symbol and a class with policy_class override" do policy = Pundit.policy(user, [:project, Customer::Post]) expect(policy.class).to eq Project::PostPolicy expect(policy.user).to eq user expect(policy.post).to eq Customer::Post end it "returns correct policy class for an array of a multi-word symbols" do policy = Pundit.policy(user, %i[project_one_two_three criteria_four_five_six]) expect(policy.class).to eq ProjectOneTwoThree::CriteriaFourFiveSixPolicy end it "returns correct policy class for an array of a multi-word symbol and a multi-word plain model instance" do policy = Pundit.policy(user, [:project_one_two_three, post_four_five_six]) expect(policy.class).to eq ProjectOneTwoThree::PostFourFiveSixPolicy end it "returns correct policy class for an array of a multi-word symbol and a multi-word active model instance" do policy = Pundit.policy(user, [:project_one_two_three, comment_four_five_six]) expect(policy.class).to eq ProjectOneTwoThree::CommentFourFiveSixPolicy end it "returns correct policy class for an array of a multi-word symbol and a multi-word plain model class" do policy = Pundit.policy(user, [:project_one_two_three, PostFourFiveSix]) expect(policy.class).to eq ProjectOneTwoThree::PostFourFiveSixPolicy end it "returns correct policy class for an array of a multi-word symbol and a multi-word active model class" do policy = Pundit.policy(user, [:project_one_two_three, CommentFourFiveSix]) expect(policy.class).to eq ProjectOneTwoThree::CommentFourFiveSixPolicy end it "returns correct policy class for a multi-word scoped plain model class" do policy = Pundit.policy(user, ProjectOneTwoThree::TagFourFiveSix) expect(policy.class).to eq ProjectOneTwoThree::TagFourFiveSixPolicy end it "returns correct policy class for a multi-word scoped plain model instance" do policy = Pundit.policy(user, tag_four_five_six) expect(policy.class).to eq ProjectOneTwoThree::TagFourFiveSixPolicy end it "returns correct policy class for a multi-word scoped active model class" do policy = Pundit.policy(user, ProjectOneTwoThree::AvatarFourFiveSix) expect(policy.class).to eq ProjectOneTwoThree::AvatarFourFiveSixPolicy end it "returns correct policy class for a multi-word scoped active model instance" do policy = Pundit.policy(user, avatar_four_five_six) expect(policy.class).to eq ProjectOneTwoThree::AvatarFourFiveSixPolicy end it "returns nil if the given policy can't be found" do expect(Pundit.policy(user, article)).to be_nil expect(Pundit.policy(user, Article)).to be_nil end it "returns the specified NilClassPolicy for nil" do expect(Pundit.policy(user, nil)).to be_a NilClassPolicy end describe "with .policy_class set on the model" do it "returns an instantiated policy given a plain model instance" do policy = Pundit.policy(user, artificial_blog) expect(policy.user).to eq user expect(policy.blog).to eq artificial_blog end it "returns an instantiated policy given a plain model class" do policy = Pundit.policy(user, ArtificialBlog) expect(policy.user).to eq user expect(policy.blog).to eq ArtificialBlog end it "returns an instantiated policy given a plain model instance providing an anonymous class" do policy = Pundit.policy(user, article_tag) expect(policy.user).to eq user expect(policy.tag).to eq article_tag end it "returns an instantiated policy given a plain model class providing an anonymous class" do policy = Pundit.policy(user, ArticleTag) expect(policy.user).to eq user expect(policy.tag).to eq ArticleTag end end end describe ".policy!" do it "returns an instantiated policy given a plain model instance" do policy = Pundit.policy!(user, post) expect(policy.user).to eq user expect(policy.post).to eq post end it "returns an instantiated policy given an active model instance" do policy = Pundit.policy!(user, comment) expect(policy.user).to eq user expect(policy.comment).to eq comment end it "returns an instantiated policy given a plain model class" do policy = Pundit.policy!(user, Post) expect(policy.user).to eq user expect(policy.post).to eq Post end it "returns an instantiated policy given an active model class" do policy = Pundit.policy!(user, Comment) expect(policy.user).to eq user expect(policy.comment).to eq Comment end it "returns an instantiated policy given a symbol" do policy = Pundit.policy!(user, :criteria) expect(policy.class).to eq CriteriaPolicy expect(policy.user).to eq user expect(policy.criteria).to eq :criteria end it "returns an instantiated policy given an array of symbols" do policy = Pundit.policy!(user, %i[project criteria]) expect(policy.class).to eq Project::CriteriaPolicy expect(policy.user).to eq user expect(policy.criteria).to eq :criteria end it "throws an exception if the given policy can't be found" do expect { Pundit.policy!(user, article) }.to raise_error(Pundit::NotDefinedError) expect { Pundit.policy!(user, Article) }.to raise_error(Pundit::NotDefinedError) end it "returns the specified NilClassPolicy for nil" do expect(Pundit.policy!(user, nil)).to be_a NilClassPolicy end it "raises an error with a invalid policy constructor" do expect do Pundit.policy(user, Wiki) end.to raise_error(Pundit::InvalidConstructorError, "Invalid # constructor is called") end end describe "#verify_authorized" do it "does nothing when authorized" do controller.authorize(post) controller.verify_authorized end it "raises an exception when not authorized" do expect { controller.verify_authorized }.to raise_error(Pundit::AuthorizationNotPerformedError) end end describe "#verify_policy_scoped" do it "does nothing when policy_scope is used" do controller.policy_scope(Post) controller.verify_policy_scoped end it "raises an exception when policy_scope is not used" do expect { controller.verify_policy_scoped }.to raise_error(Pundit::PolicyScopingNotPerformedError) end end describe "#pundit_policy_authorized?" do it "is true when authorized" do controller.authorize(post) expect(controller.pundit_policy_authorized?).to be true end it "is false when not authorized" do expect(controller.pundit_policy_authorized?).to be false end end describe "#pundit_policy_scoped?" do it "is true when policy_scope is used" do controller.policy_scope(Post) expect(controller.pundit_policy_scoped?).to be true end it "is false when policy scope is not used" do expect(controller.pundit_policy_scoped?).to be false end end describe "#authorize" do it "infers the policy name and authorizes based on it" do expect(controller.authorize(post)).to be_truthy end it "returns the record on successful authorization" do expect(controller.authorize(post)).to be(post) end it "can be given a different permission to check" do expect(controller.authorize(post, :show?)).to be_truthy expect { controller.authorize(post, :destroy?) }.to raise_error(Pundit::NotAuthorizedError) end it "can be given a different policy class" do expect(controller.authorize(post, :create?, policy_class: PublicationPolicy)).to be_truthy end it "works with anonymous class policies" do expect(controller.authorize(article_tag, :show?)).to be_truthy expect { controller.authorize(article_tag, :destroy?) }.to raise_error(Pundit::NotAuthorizedError) end it "throws an exception when the permission check fails" do expect { controller.authorize(Post.new) }.to raise_error(Pundit::NotAuthorizedError) end it "throws an exception when a policy cannot be found" do expect { controller.authorize(Article) }.to raise_error(Pundit::NotDefinedError) end it "caches the policy" do expect(controller.policies[post]).to be_nil controller.authorize(post) expect(controller.policies[post]).not_to be_nil end it "raises an error when the given record is nil" do expect { controller.authorize(nil, :destroy?) }.to raise_error(Pundit::NotAuthorizedError) end it "raises an error with a invalid policy constructor" do expect { controller.authorize(wiki, :destroy?) }.to raise_error(Pundit::InvalidConstructorError) end end describe "#skip_authorization" do it "disables authorization verification" do controller.skip_authorization expect { controller.verify_authorized }.not_to raise_error end end describe "#skip_policy_scope" do it "disables policy scope verification" do controller.skip_policy_scope expect { controller.verify_policy_scoped }.not_to raise_error end end describe "#pundit_user" do it "returns the same thing as current_user" do expect(controller.pundit_user).to eq controller.current_user end end describe "#policy" do it "returns an instantiated policy" do policy = controller.policy(post) expect(policy.user).to eq user expect(policy.post).to eq post end it "throws an exception if the given policy can't be found" do expect { controller.policy(article) }.to raise_error(Pundit::NotDefinedError) end it "raises an error with a invalid policy constructor" do expect { controller.policy(wiki) }.to raise_error(Pundit::InvalidConstructorError) end it "allows policy to be injected" do new_policy = OpenStruct.new controller.policies[post] = new_policy expect(controller.policy(post)).to eq new_policy end end describe "#policy_scope" do it "returns an instantiated policy scope" do expect(controller.policy_scope(Post)).to eq :published end it "allows policy scope class to be overriden" do expect(controller.policy_scope(Post, policy_scope_class: PublicationPolicy::Scope)).to eq :published end it "throws an exception if the given policy can't be found" do expect { controller.policy_scope(Article) }.to raise_error(Pundit::NotDefinedError) end it "raises an error with a invalid policy scope constructor" do expect { controller.policy_scope(Wiki) }.to raise_error(Pundit::InvalidConstructorError) end it "allows policy_scope to be injected" do new_scope = OpenStruct.new controller.policy_scopes[Post] = new_scope expect(controller.policy_scope(Post)).to eq new_scope end end describe "#permitted_attributes" do it "checks policy for permitted attributes" do params = ActionController::Parameters.new(post: { title: "Hello", votes: 5, admin: true }) action = "update" expect(Controller.new(user, action, params).permitted_attributes(post).to_h).to eq( "title" => "Hello", "votes" => 5 ) expect(Controller.new(double, action, params).permitted_attributes(post).to_h).to eq("votes" => 5) end it "checks policy for permitted attributes for record of a ActiveModel type" do params = ActionController::Parameters.new(customer_post: { title: "Hello", votes: 5, admin: true }) action = "update" expect(Controller.new(user, action, params).permitted_attributes(customer_post).to_h).to eq( "title" => "Hello", "votes" => 5 ) expect(Controller.new(double, action, params).permitted_attributes(customer_post).to_h).to eq( "votes" => 5 ) end end describe "#permitted_attributes_for_action" do it "is checked if it is defined in the policy" do params = ActionController::Parameters.new(post: { title: "Hello", body: "blah", votes: 5, admin: true }) action = "revise" expect(Controller.new(user, action, params).permitted_attributes(post).to_h).to eq("body" => "blah") end it "can be explicitly set" do params = ActionController::Parameters.new(post: { title: "Hello", body: "blah", votes: 5, admin: true }) action = "update" expect(Controller.new(user, action, params).permitted_attributes(post, :revise).to_h).to eq("body" => "blah") end end describe "Pundit::NotAuthorizedError" do it "can be initialized with a string as message" do error = Pundit::NotAuthorizedError.new("must be logged in") expect(error.message).to eq "must be logged in" end end end pundit-2.0.0/spec/spec_helper.rb0000644000004100000410000001043013337760034016620 0ustar www-datawww-datarequire "pundit" require "pundit/rspec" require "rack" require "rack/test" require "pry" require "active_support" require "active_support/core_ext" require "active_model/naming" require "action_controller/metal/strong_parameters" I18n.enforce_available_locales = false module PunditSpecHelper extend RSpec::Matchers::DSL matcher :be_truthy do match do |actual| actual end end end RSpec.configure do |config| config.include PunditSpecHelper end class PostPolicy < Struct.new(:user, :post) class Scope < Struct.new(:user, :scope) def resolve scope.published end end def update? post.user == user end def destroy? false end def show? true end def permitted_attributes if post.user == user %i[title votes] else [:votes] end end def permitted_attributes_for_revise [:body] end end class Post < Struct.new(:user) def self.published :published end def self.read :read end def to_s "Post" end def inspect "#" end end module Customer class Post < Post def model_name OpenStruct.new(param_key: "customer_post") end def self.policy_class PostPolicy end def policy_class self.class.policy_class end end end class CommentScope attr_reader :original_object def initialize(original_object) @original_object = original_object end def ==(other) original_object == other.original_object end end class CommentPolicy < Struct.new(:user, :comment) class Scope < Struct.new(:user, :scope) def resolve CommentScope.new(scope) end end end class PublicationPolicy < Struct.new(:user, :publication) class Scope < Struct.new(:user, :scope) def resolve scope.published end end def create? true end end class Comment extend ActiveModel::Naming end class CommentsRelation def initialize(empty = false) @empty = empty end def blank? @empty end def model_name Comment.model_name end end class Article; end class BlogPolicy < Struct.new(:user, :blog); end class Blog; end class ArtificialBlog < Blog def self.policy_class BlogPolicy end end class ArticleTagOtherNamePolicy < Struct.new(:user, :tag) def show? true end def destroy? false end end class ArticleTag def self.policy_class ArticleTagOtherNamePolicy end end class CriteriaPolicy < Struct.new(:user, :criteria); end module Project class CommentPolicy < Struct.new(:user, :comment) class Scope < Struct.new(:user, :scope) def resolve scope end end end class CriteriaPolicy < Struct.new(:user, :criteria); end class PostPolicy < Struct.new(:user, :post) class Scope < Struct.new(:user, :scope) def resolve scope.read end end end end class DenierPolicy < Struct.new(:user, :record) def update? false end end class Controller include Pundit # Mark protected methods public so they may be called in test # rubocop:disable Layout/AccessModifierIndentation, Style/AccessModifierDeclarations public(*Pundit.protected_instance_methods) # rubocop:enable Layout/AccessModifierIndentation, Style/AccessModifierDeclarations attr_reader :current_user, :action_name, :params def initialize(current_user, action_name, params) @current_user = current_user @action_name = action_name @params = params end end class NilClassPolicy < Struct.new(:user, :record) class Scope def initialize(*) raise Pundit::NotDefinedError, "Cannot scope NilClass" end end def show? false end def destroy? false end end class Wiki; end class WikiPolicy class Scope # deliberate typo method def initalize; end end end class PostFourFiveSix < Struct.new(:user); end class CommentFourFiveSix; extend ActiveModel::Naming; end module ProjectOneTwoThree class CommentFourFiveSixPolicy < Struct.new(:user, :post); end class CriteriaFourFiveSixPolicy < Struct.new(:user, :criteria); end class PostFourFiveSixPolicy < Struct.new(:user, :post); end class TagFourFiveSix < Struct.new(:user); end class TagFourFiveSixPolicy < Struct.new(:user, :tag); end class AvatarFourFiveSix; extend ActiveModel::Naming; end class AvatarFourFiveSixPolicy < Struct.new(:user, :avatar); end end pundit-2.0.0/CHANGELOG.md0000644000004100000410000000556313337760034014674 0ustar www-datawww-data# Pundit ## 2.0.0 (2018-07-21) No changes since beta1 ## 2.0.0.beta1 (2018-07-04) - Add `policy_class` option to `authorize` to be able to override the policy. (#441) - Add `policy_scope_class` option to `authorize` to be able to override the policy scope. (#441) - Fix `param_key` issue when passed an array. (#529) - Only pass last element of "namespace array" to policy and scope. (#529) - Allow specification of a `NilClassPolicy`. (#525) - Make sure `policy_class` override is called when passed an array. (#475) - Raise `InvalidConstructorError` if a policy or policy scope with an invalid constructor is called. (#462) - Use `action_name` instead of `params[:action]`. (#419) - Add `pundit_params_for` method to make it easy to customize params fetching. (#502) - Return passed object from `#authorize` method to make chaining possible. (#385) ## 1.1.0 (2016-01-14) - Can retrieve policies via an array of symbols/objects. - Add autodetection of param key to `permitted_attributes` helper. - Hide some methods which should not be actions. - Permitted attributes should be expanded. - Generator uses `RSpec.describe` according to modern best practices. ## 1.0.1 (2015-05-27) - Fixed a regression where NotAuthorizedError could not be ininitialized with a string. - Use `camelize` instead of `classify` for symbol policies to prevent weird pluralizations. ## 1.0.0 (2015-04-19) - Caches policy scopes and policies. - Explicitly setting the policy for the controller via `controller.policy = foo` has been removed. Instead use `controller.policies[record] = foo`. - Explicitly setting the policy scope for the controller via `controller.policy_policy = foo` has been removed. Instead use `controller.policy_scopes[scope] = foo`. - Add `permitted_attributes` helper to fetch attributes from policy. - Add `pundit_policy_authorized?` and `pundit_policy_scoped?` methods. - Instance variables are prefixed to avoid collisions. - Add `Pundit.authorize` method. - Add `skip_authorization` and `skip_policy_scope` helpers. - Better errors when checking multiple permissions in RSpec tests. - Better errors in case `nil` is passed to `policy` or `policy_scope`. - Use `inspect` when printing object for better errors. - Dropped official support for Ruby 1.9.3 ## 0.3.0 (2014-08-22) - Extend the default `ApplicationPolicy` with an `ApplicationPolicy::Scope` (#120) - Fix RSpec 3 deprecation warnings for built-in matchers (#162) - Generate blank policy spec/test files for Rspec/MiniTest/Test::Unit in Rails (#138) ## 0.2.3 (2014-04-06) - Customizable error messages: `#query`, `#record` and `#policy` methods on `Pundit::NotAuthorizedError` (#114) - Raise a different `Pundit::AuthorizationNotPerformedError` when `authorize` call is expected in controller action but missing (#109) - Update Rspec matchers for Rspec 3 (#124) ## 0.2.2 (2014-02-07) - Customize the user to be passed into policies: `pundit_user` (#42) pundit-2.0.0/.rubocop.yml0000644000004100000410000000311313337760034015322 0ustar www-datawww-dataAllCops: DisplayCopNames: true TargetRubyVersion: 2.1 Exclude: - "gemfiles/**/*" - "vendor/**/*" - "lib/generators/**/*" Metrics/BlockLength: Exclude: - "**/*_spec.rb" Metrics/MethodLength: Max: 40 Metrics/ModuleLength: Max: 200 Exclude: - "**/*_spec.rb" Metrics/LineLength: Max: 120 Metrics/AbcSize: Enabled: false Metrics/CyclomaticComplexity: Enabled: false Metrics/PerceivedComplexity: Enabled: false Style/StructInheritance: Enabled: false Layout/AlignParameters: EnforcedStyle: with_fixed_indentation Style/StringLiterals: EnforcedStyle: double_quotes Style/StringLiteralsInInterpolation: EnforcedStyle: double_quotes Layout/ClosingParenthesisIndentation: Enabled: false Style/OneLineConditional: Enabled: false Style/AndOr: Enabled: false Style/Not: Enabled: false Documentation: Enabled: false # TODO: Enable again once we have more docs Layout/CaseIndentation: EnforcedStyle: case SupportedStyles: - case - end IndentOneStep: true Style/PercentLiteralDelimiters: PreferredDelimiters: '%w': "[]" '%W': "[]" Layout/AccessModifierIndentation: EnforcedStyle: outdent Style/SignalException: Enabled: false Layout/IndentationWidth: Enabled: false Style/TrivialAccessors: ExactNameMatch: true Layout/EndAlignment: EnforcedStyleAlignWith: variable Layout/DefEndAlignment: Enabled: false Lint/HandleExceptions: Enabled: false Style/SpecialGlobalVars: Enabled: false Style/TrivialAccessors: Enabled: false Layout/IndentHash: Enabled: false Style/DoubleNegation: Enabled: false pundit-2.0.0/.gitignore0000644000004100000410000000023613337760034015043 0ustar www-datawww-data*.gem *.rbc .bundle .config .yardoc Gemfile.lock InstalledFiles _yardoc coverage doc/ lib/bundler/man pkg rdoc spec/reports test/tmp test/version_tmp tmp bin pundit-2.0.0/CODE_OF_CONDUCT.md0000644000004100000410000000261513337760034015655 0ustar www-datawww-data# Contributor Code of Conduct As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion. Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. This Code of Conduct is adapted from the [Contributor Covenant](http:contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/) pundit-2.0.0/Rakefile0000644000004100000410000000051213337760034014515 0ustar www-datawww-datarequire "rubygems" require "bundler/gem_tasks" require "rspec/core/rake_task" require "yard" require "rubocop/rake_task" RuboCop::RakeTask.new desc "Run all examples" RSpec::Core::RakeTask.new(:spec) do |t| t.rspec_opts = %w[--color] end YARD::Rake::YardocTask.new do |t| t.files = ["lib/**/*.rb"] end task default: :spec pundit-2.0.0/lib/0000755000004100000410000000000013337760034013620 5ustar www-datawww-datapundit-2.0.0/lib/pundit.rb0000644000004100000410000002713013337760034015453 0ustar www-datawww-data# frozen_string_literal: true require "pundit/version" require "pundit/policy_finder" require "active_support/concern" require "active_support/core_ext/string/inflections" require "active_support/core_ext/object/blank" require "active_support/core_ext/module/introspection" require "active_support/dependencies/autoload" # @api public module Pundit SUFFIX = "Policy".freeze # @api private module Generators; end # @api private class Error < StandardError; end # Error that will be raised when authorization has failed class NotAuthorizedError < Error attr_reader :query, :record, :policy def initialize(options = {}) if options.is_a? String message = options else @query = options[:query] @record = options[:record] @policy = options[:policy] message = options.fetch(:message) { "not allowed to #{query} this #{record.inspect}" } end super(message) end end # Error that will be raised if a policy or policy scope constructor is not called correctly. class InvalidConstructorError < Error; end # Error that will be raised if a controller action has not called the # `authorize` or `skip_authorization` methods. class AuthorizationNotPerformedError < Error; end # Error that will be raised if a controller action has not called the # `policy_scope` or `skip_policy_scope` methods. class PolicyScopingNotPerformedError < AuthorizationNotPerformedError; end # Error that will be raised if a policy or policy scope is not defined. class NotDefinedError < Error; end extend ActiveSupport::Concern class << self # Retrieves the policy for the given record, initializing it with the # record and user and finally throwing an error if the user is not # authorized to perform the given action. # # @param user [Object] the user that initiated the action # @param record [Object] the object we're checking permissions of # @param query [Symbol, String] the predicate method to check on the policy (e.g. `:show?`) # @param policy_class [Class] the policy class we want to force use of # @raise [NotAuthorizedError] if the given query method returned false # @return [Object] Always returns the passed object record def authorize(user, record, query, policy_class: nil) policy = policy_class ? policy_class.new(user, record) : policy!(user, record) raise NotAuthorizedError, query: query, record: record, policy: policy unless policy.public_send(query) record end # Retrieves the policy scope for the given record. # # @see https://github.com/varvet/pundit#scopes # @param user [Object] the user that initiated the action # @param scope [Object] the object we're retrieving the policy scope for # @raise [InvalidConstructorError] if the policy constructor called incorrectly # @return [Scope{#resolve}, nil] instance of scope class which can resolve to a scope def policy_scope(user, scope) policy_scope = PolicyFinder.new(scope).scope policy_scope.new(user, pundit_model(scope)).resolve if policy_scope rescue ArgumentError raise InvalidConstructorError, "Invalid #<#{policy_scope}> constructor is called" end # Retrieves the policy scope for the given record. # # @see https://github.com/varvet/pundit#scopes # @param user [Object] the user that initiated the action # @param scope [Object] the object we're retrieving the policy scope for # @raise [NotDefinedError] if the policy scope cannot be found # @raise [InvalidConstructorError] if the policy constructor called incorrectly # @return [Scope{#resolve}] instance of scope class which can resolve to a scope def policy_scope!(user, scope) policy_scope = PolicyFinder.new(scope).scope! policy_scope.new(user, pundit_model(scope)).resolve rescue ArgumentError raise InvalidConstructorError, "Invalid #<#{policy_scope}> constructor is called" end # Retrieves the policy for the given record. # # @see https://github.com/varvet/pundit#policies # @param user [Object] the user that initiated the action # @param record [Object] the object we're retrieving the policy for # @raise [InvalidConstructorError] if the policy constructor called incorrectly # @return [Object, nil] instance of policy class with query methods def policy(user, record) policy = PolicyFinder.new(record).policy policy.new(user, pundit_model(record)) if policy rescue ArgumentError raise InvalidConstructorError, "Invalid #<#{policy}> constructor is called" end # Retrieves the policy for the given record. # # @see https://github.com/varvet/pundit#policies # @param user [Object] the user that initiated the action # @param record [Object] the object we're retrieving the policy for # @raise [NotDefinedError] if the policy cannot be found # @raise [InvalidConstructorError] if the policy constructor called incorrectly # @return [Object] instance of policy class with query methods def policy!(user, record) policy = PolicyFinder.new(record).policy! policy.new(user, pundit_model(record)) rescue ArgumentError raise InvalidConstructorError, "Invalid #<#{policy}> constructor is called" end private def pundit_model(record) record.is_a?(Array) ? record.last : record end end # @api private module Helper def policy_scope(scope) pundit_policy_scope(scope) end end included do helper Helper if respond_to?(:helper) if respond_to?(:helper_method) helper_method :policy helper_method :pundit_policy_scope helper_method :pundit_user end end protected # @return [Boolean] whether authorization has been performed, i.e. whether # one {#authorize} or {#skip_authorization} has been called def pundit_policy_authorized? !!@_pundit_policy_authorized end # @return [Boolean] whether policy scoping has been performed, i.e. whether # one {#policy_scope} or {#skip_policy_scope} has been called def pundit_policy_scoped? !!@_pundit_policy_scoped end # Raises an error if authorization has not been performed, usually used as an # `after_action` filter to prevent programmer error in forgetting to call # {#authorize} or {#skip_authorization}. # # @see https://github.com/varvet/pundit#ensuring-policies-and-scopes-are-used # @raise [AuthorizationNotPerformedError] if authorization has not been performed # @return [void] def verify_authorized raise AuthorizationNotPerformedError, self.class unless pundit_policy_authorized? end # Raises an error if policy scoping has not been performed, usually used as an # `after_action` filter to prevent programmer error in forgetting to call # {#policy_scope} or {#skip_policy_scope} in index actions. # # @see https://github.com/varvet/pundit#ensuring-policies-and-scopes-are-used # @raise [AuthorizationNotPerformedError] if policy scoping has not been performed # @return [void] def verify_policy_scoped raise PolicyScopingNotPerformedError, self.class unless pundit_policy_scoped? end # Retrieves the policy for the given record, initializing it with the record # and current user and finally throwing an error if the user is not # authorized to perform the given action. # # @param record [Object] the object we're checking permissions of # @param query [Symbol, String] the predicate method to check on the policy (e.g. `:show?`). # If omitted then this defaults to the Rails controller action name. # @param policy_class [Class] the policy class we want to force use of # @raise [NotAuthorizedError] if the given query method returned false # @return [Object] Always returns the passed object record def authorize(record, query = nil, policy_class: nil) query ||= "#{action_name}?" @_pundit_policy_authorized = true policy = policy_class ? policy_class.new(pundit_user, record) : policy(record) raise NotAuthorizedError, query: query, record: record, policy: policy unless policy.public_send(query) record end # Allow this action not to perform authorization. # # @see https://github.com/varvet/pundit#ensuring-policies-and-scopes-are-used # @return [void] def skip_authorization @_pundit_policy_authorized = true end # Allow this action not to perform policy scoping. # # @see https://github.com/varvet/pundit#ensuring-policies-and-scopes-are-used # @return [void] def skip_policy_scope @_pundit_policy_scoped = true end # Retrieves the policy scope for the given record. # # @see https://github.com/varvet/pundit#scopes # @param scope [Object] the object we're retrieving the policy scope for # @param policy_scope_class [Class] the policy scope class we want to force use of # @return [Scope{#resolve}, nil] instance of scope class which can resolve to a scope def policy_scope(scope, policy_scope_class: nil) @_pundit_policy_scoped = true policy_scope_class ? policy_scope_class.new(pundit_user, scope).resolve : pundit_policy_scope(scope) end # Retrieves the policy for the given record. # # @see https://github.com/varvet/pundit#policies # @param record [Object] the object we're retrieving the policy for # @return [Object, nil] instance of policy class with query methods def policy(record) policies[record] ||= Pundit.policy!(pundit_user, record) end # Retrieves a set of permitted attributes from the policy by instantiating # the policy class for the given record and calling `permitted_attributes` on # it, or `permitted_attributes_for_{action}` if `action` is defined. It then infers # what key the record should have in the params hash and retrieves the # permitted attributes from the params hash under that key. # # @see https://github.com/varvet/pundit#strong-parameters # @param record [Object] the object we're retrieving permitted attributes for # @param action [Symbol, String] the name of the action being performed on the record (e.g. `:update`). # If omitted then this defaults to the Rails controller action name. # @return [Hash{String => Object}] the permitted attributes def permitted_attributes(record, action = action_name) policy = policy(record) method_name = if policy.respond_to?("permitted_attributes_for_#{action}") "permitted_attributes_for_#{action}" else "permitted_attributes" end pundit_params_for(record).permit(*policy.public_send(method_name)) end # Retrieves the params for the given record. # # @param record [Object] the object we're retrieving params for # @return [ActionController::Parameters] the params def pundit_params_for(record) params.require(PolicyFinder.new(record).param_key) end # Cache of policies. You should not rely on this method. # # @api private # rubocop:disable Naming/MemoizedInstanceVariableName def policies @_pundit_policies ||= {} end # rubocop:enable Naming/MemoizedInstanceVariableName # Cache of policy scope. You should not rely on this method. # # @api private # rubocop:disable Naming/MemoizedInstanceVariableName def policy_scopes @_pundit_policy_scopes ||= {} end # rubocop:enable Naming/MemoizedInstanceVariableName # Hook method which allows customizing which user is passed to policies and # scopes initialized by {#authorize}, {#policy} and {#policy_scope}. # # @see https://github.com/varvet/pundit#customize-pundit-user # @return [Object] the user object to be used with pundit def pundit_user current_user end private def pundit_policy_scope(scope) policy_scopes[scope] ||= Pundit.policy_scope!(pundit_user, scope) end end pundit-2.0.0/lib/generators/0000755000004100000410000000000013337760034015771 5ustar www-datawww-datapundit-2.0.0/lib/generators/test_unit/0000755000004100000410000000000013337760034020007 5ustar www-datawww-datapundit-2.0.0/lib/generators/test_unit/templates/0000755000004100000410000000000013337760034022005 5ustar www-datawww-datapundit-2.0.0/lib/generators/test_unit/templates/policy_test.rb0000644000004100000410000000032113337760034024664 0ustar www-datawww-datarequire 'test_helper' class <%= class_name %>PolicyTest < ActiveSupport::TestCase def test_scope end def test_show end def test_create end def test_update end def test_destroy end end pundit-2.0.0/lib/generators/test_unit/policy_generator.rb0000644000004100000410000000047313337760034023705 0ustar www-datawww-datamodule TestUnit module Generators class PolicyGenerator < ::Rails::Generators::NamedBase source_root File.expand_path('templates', __dir__) def create_policy_test template 'policy_test.rb', File.join('test/policies', class_path, "#{file_name}_policy_test.rb") end end end end pundit-2.0.0/lib/generators/rspec/0000755000004100000410000000000013337760034017105 5ustar www-datawww-datapundit-2.0.0/lib/generators/rspec/templates/0000755000004100000410000000000013337760034021103 5ustar www-datawww-datapundit-2.0.0/lib/generators/rspec/templates/policy_spec.rb0000644000004100000410000000121213337760034023735 0ustar www-datawww-datarequire '<%= File.exists?('spec/rails_helper.rb') ? 'rails_helper' : 'spec_helper' %>' RSpec.describe <%= class_name %>Policy do let(:user) { User.new } subject { described_class } permissions ".scope" do pending "add some examples to (or delete) #{__FILE__}" end permissions :show? do pending "add some examples to (or delete) #{__FILE__}" end permissions :create? do pending "add some examples to (or delete) #{__FILE__}" end permissions :update? do pending "add some examples to (or delete) #{__FILE__}" end permissions :destroy? do pending "add some examples to (or delete) #{__FILE__}" end end pundit-2.0.0/lib/generators/rspec/policy_generator.rb0000644000004100000410000000047013337760034023000 0ustar www-datawww-datamodule Rspec module Generators class PolicyGenerator < ::Rails::Generators::NamedBase source_root File.expand_path('templates', __dir__) def create_policy_spec template 'policy_spec.rb', File.join('spec/policies', class_path, "#{file_name}_policy_spec.rb") end end end end pundit-2.0.0/lib/generators/pundit/0000755000004100000410000000000013337760034017274 5ustar www-datawww-datapundit-2.0.0/lib/generators/pundit/policy/0000755000004100000410000000000013337760034020573 5ustar www-datawww-datapundit-2.0.0/lib/generators/pundit/policy/templates/0000755000004100000410000000000013337760034022571 5ustar www-datawww-datapundit-2.0.0/lib/generators/pundit/policy/templates/policy.rb0000644000004100000410000000024213337760034024413 0ustar www-datawww-data<% module_namespacing do -%> class <%= class_name %>Policy < ApplicationPolicy class Scope < Scope def resolve scope.all end end end <% end -%> pundit-2.0.0/lib/generators/pundit/policy/policy_generator.rb0000644000004100000410000000051113337760034024462 0ustar www-datawww-datamodule Pundit module Generators class PolicyGenerator < ::Rails::Generators::NamedBase source_root File.expand_path('templates', __dir__) def create_policy template 'policy.rb', File.join('app/policies', class_path, "#{file_name}_policy.rb") end hook_for :test_framework end end end pundit-2.0.0/lib/generators/pundit/policy/USAGE0000644000004100000410000000026013337760034021360 0ustar www-datawww-dataDescription: Generates a policy for a model with the given name. Example: rails generate pundit:policy user This will create: app/policies/user_policy.rb pundit-2.0.0/lib/generators/pundit/install/0000755000004100000410000000000013337760034020742 5ustar www-datawww-datapundit-2.0.0/lib/generators/pundit/install/templates/0000755000004100000410000000000013337760034022740 5ustar www-datawww-datapundit-2.0.0/lib/generators/pundit/install/templates/application_policy.rb0000644000004100000410000000101013337760034027137 0ustar www-datawww-dataclass ApplicationPolicy attr_reader :user, :record def initialize(user, record) @user = user @record = record end def index? false end def show? false end def create? false end def new? create? end def update? false end def edit? update? end def destroy? false end class Scope attr_reader :user, :scope def initialize(user, scope) @user = user @scope = scope end def resolve scope.all end end end pundit-2.0.0/lib/generators/pundit/install/install_generator.rb0000644000004100000410000000044013337760034025001 0ustar www-datawww-datamodule Pundit module Generators class InstallGenerator < ::Rails::Generators::Base source_root File.expand_path('templates', __dir__) def copy_application_policy template 'application_policy.rb', 'app/policies/application_policy.rb' end end end end pundit-2.0.0/lib/generators/pundit/install/USAGE0000644000004100000410000000013313337760034021526 0ustar www-datawww-dataDescription: Generates an application policy as a starting point for your application. pundit-2.0.0/lib/pundit/0000755000004100000410000000000013337760034015123 5ustar www-datawww-datapundit-2.0.0/lib/pundit/version.rb0000644000004100000410000000011413337760034017131 0ustar www-datawww-data# frozen_string_literal: true module Pundit VERSION = "2.0.0".freeze end pundit-2.0.0/lib/pundit/policy_finder.rb0000644000004100000410000000550713337760034020305 0ustar www-datawww-datamodule Pundit # Finds policy and scope classes for given object. # @api public # @example # user = User.find(params[:id]) # finder = PolicyFinder.new(user) # finder.policy #=> UserPolicy # finder.scope #=> UserPolicy::Scope # class PolicyFinder attr_reader :object # @param object [any] the object to find policy and scope classes for # def initialize(object) @object = object end # @return [nil, Scope{#resolve}] scope class which can resolve to a scope # @see https://github.com/varvet/pundit#scopes # @example # scope = finder.scope #=> UserPolicy::Scope # scope.resolve #=> <#ActiveRecord::Relation ...> # def scope "#{policy}::Scope".safe_constantize end # @return [nil, Class] policy class with query methods # @see https://github.com/varvet/pundit#policies # @example # policy = finder.policy #=> UserPolicy # policy.show? #=> true # policy.update? #=> false # def policy klass = find(object) klass.is_a?(String) ? klass.safe_constantize : klass end # @return [Scope{#resolve}] scope class which can resolve to a scope # @raise [NotDefinedError] if scope could not be determined # def scope! scope or raise NotDefinedError, "unable to find scope `#{find(object)}::Scope` for `#{object.inspect}`" end # @return [Class] policy class with query methods # @raise [NotDefinedError] if policy could not be determined # def policy! policy or raise NotDefinedError, "unable to find policy `#{find(object)}` for `#{object.inspect}`" end # @return [String] the name of the key this object would have in a params hash # def param_key model = object.is_a?(Array) ? object.last : object if model.respond_to?(:model_name) model.model_name.param_key.to_s elsif model.is_a?(Class) model.to_s.demodulize.underscore else model.class.to_s.demodulize.underscore end end private def find(subject) if subject.is_a?(Array) modules = subject.dup last = modules.pop context = modules.map { |x| find_class_name(x) }.join("::") [context, find(last)].join("::") elsif subject.respond_to?(:policy_class) subject.policy_class elsif subject.class.respond_to?(:policy_class) subject.class.policy_class else klass = find_class_name(subject) "#{klass}#{SUFFIX}" end end def find_class_name(subject) if subject.respond_to?(:model_name) subject.model_name elsif subject.class.respond_to?(:model_name) subject.class.model_name elsif subject.is_a?(Class) subject elsif subject.is_a?(Symbol) subject.to_s.camelize else subject.class end end end end pundit-2.0.0/lib/pundit/rspec.rb0000644000004100000410000000542713337760034016574 0ustar www-datawww-datarequire "active_support/core_ext/array/conversions" module Pundit module RSpec module Matchers extend ::RSpec::Matchers::DSL # rubocop:disable Metrics/BlockLength matcher :permit do |user, record| match_proc = lambda do |policy| @violating_permissions = permissions.find_all do |permission| !policy.new(user, record).public_send(permission) end @violating_permissions.empty? end match_when_negated_proc = lambda do |policy| @violating_permissions = permissions.find_all do |permission| policy.new(user, record).public_send(permission) end @violating_permissions.empty? end failure_message_proc = lambda do |policy| was_were = @violating_permissions.count > 1 ? "were" : "was" "Expected #{policy} to grant #{permissions.to_sentence} on " \ "#{record} but #{@violating_permissions.to_sentence} #{was_were} not granted" end failure_message_when_negated_proc = lambda do |policy| was_were = @violating_permissions.count > 1 ? "were" : "was" "Expected #{policy} not to grant #{permissions.to_sentence} on " \ "#{record} but #{@violating_permissions.to_sentence} #{was_were} granted" end if respond_to?(:match_when_negated) match(&match_proc) match_when_negated(&match_when_negated_proc) failure_message(&failure_message_proc) failure_message_when_negated(&failure_message_when_negated_proc) else match_for_should(&match_proc) match_for_should_not(&match_when_negated_proc) failure_message_for_should(&failure_message_proc) failure_message_for_should_not(&failure_message_when_negated_proc) end def permissions current_example = ::RSpec.respond_to?(:current_example) ? ::RSpec.current_example : example current_example.metadata[:permissions] end end # rubocop:enable Metrics/BlockLength end module DSL def permissions(*list, &block) describe(list.to_sentence, permissions: list, caller: caller) { instance_eval(&block) } end end module PolicyExampleGroup include Pundit::RSpec::Matchers def self.included(base) base.metadata[:type] = :policy base.extend Pundit::RSpec::DSL super end end end end RSpec.configure do |config| if RSpec::Core::Version::STRING.split(".").first.to_i >= 3 config.include( Pundit::RSpec::PolicyExampleGroup, type: :policy, file_path: %r{spec/policies} ) else config.include( Pundit::RSpec::PolicyExampleGroup, type: :policy, example_group: { file_path: %r{spec/policies} } ) end end pundit-2.0.0/CONTRIBUTING.md0000644000004100000410000000224213337760034015303 0ustar www-datawww-data## Security issues If you have found a security related issue, please do not file an issue on GitHub or send a PR addressing the issue. Contact [Jonas](mailto:jonas.nicklas@gmail.com) directly. You will be given public credit for your disclosure. ## Reporting issues Please try to answer the following questions in your bug report: - What did you do? - What did you expect to happen? - What happened instead? Make sure to include as much relevant information as possible. Ruby version, Pundit version, OS version and any stack traces you have are very valuable. ## Pull Requests - **Add tests!** Your patch won't be accepted if it doesn't have tests. - **Document any change in behaviour**. Make sure the README and any other relevant documentation are kept up-to-date. - **Create topic branches**. Please don't ask us to pull from your master branch. - **One pull request per feature**. If you want to do more than one thing, send multiple pull requests. - **Send coherent history**. Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please squash them before sending them to us. pundit-2.0.0/.yardopts0000644000004100000410000000006213337760034014716 0ustar www-datawww-data--api public --hide-void-return --markup markdown pundit-2.0.0/Gemfile0000644000004100000410000000032713337760034014347 0ustar www-datawww-datasource "https://rubygems.org" ruby RUBY_VERSION gemspec group :development, :test do gem "actionpack" gem "activemodel" gem "bundler" gem "pry" gem "rake" gem "rspec" gem "rubocop" gem "yard" end pundit-2.0.0/pundit.gemspec0000644000004100000410000000147313337760034015727 0ustar www-datawww-datalib = File.expand_path("lib", __dir__) $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", "Elabs AB"] gem.email = ["jonas.nicklas@gmail.com", "dev@elabs.se"] gem.description = "Object oriented authorization for Rails applications" gem.summary = "OO authorization for Rails" gem.homepage = "https://github.com/varvet/pundit" gem.license = "MIT" 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"] gem.add_dependency "activesupport", ">= 3.0.0" end pundit-2.0.0/LICENSE.txt0000644000004100000410000000207013337760034014674 0ustar www-datawww-dataCopyright (c) 2012 Jonas Nicklas, Elabs AB 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.