Pragma::Policy

Build Status Coverage Status Maintainability

Policies provide fine-grained access control for your API resources.

Installation

Add this line to your application's Gemfile:

gem 'pragma-policy'

And then execute:

$ bundle

Or install it yourself as:

$ gem install pragma-policy

Usage

To create a policy, simply inherit from Pragma::Policy::Base:

module API
  module V1
    module Article
      class Policy < Pragma::Policy::Base
      end
    end
  end
end

By default, the policy does not return any objects when scoping and forbids all operations.

You can start customizing your policy by defining a scope and operation predicates:

module API
  module V1
    module Article
      class Policy < Pragma::Policy::Base
        class Scope < Pragma::Policy::Base::Scope
          def resolve
            scope.where('published = ? OR author_id = ?', true, user.id)
          end
        end

        def show?
          record.published? || record.author_id == user.id
        end

        def update?
          record.author_id == user.id
        end

        def destroy?
          record.author_id == user.id
        end
      end
    end
  end
end

You are ready to use your policy!

Retrieving records

To retrieve all the records accessible by a user, use the .accessible_by class method:

posts = API::V1::Article::Policy::Scope.new(user, Article.all).resolve

Authorizing operations

To authorize an operation, first instantiate the policy, then use the predicate methods:

policy = API::V1::Article::Policy.new(user, post)
fail 'You cannot update this post!' unless policy.update?

Since raising when the operation is forbidden is so common, we provide bang methods a shorthand syntax. Pragma::Policy::NotAuthorizedError is raised if the predicate method returns false:

policy = API::V1::Article::Policy.new(user, post)
policy.update! # raises if the user cannot update the post

Reusing Pundit policies

If you already use Pundit, there's no need to copy-paste policies for your API. You can use Pragma::Policy::Pundit to delegate to your existing policies and scopes:

module API
  module V1
    module Article
      class Policy < Pragma::Pundit::Policy
        # This is optional: the inferred default would be ArticlePolicy.
        self.pundit_klass = CustomArticlePolicy
      end
    end
  end
end

Note that you can still override specific methods if you want, and we'll keep delegating the rest to Pundit:

module API
  module V1
    module Article
      class Policy < Pragma::Pundit::Policy
        def create?
          # Your custom create policy here
        end
      end
    end
  end
end

Passing additional context

If you want to pass additional context to the policy, just pass it instead of the user object. Pragma::Policy never uses your context in any way, so you can pass whatever you want:

policy = API::V1::Article::Policy.new(OpenStruct.new(ip: request.remote_ip, user: user), post)
policy.update!

In your policy, you can use #context as an alias for #user for convenience:

module API
  module V1
    module Article
      class Policy < Pragma::Pundit::Policy
        def update?
          record.author_id == context.user.id || context.ip == '127.0.0.1'
        end
      end
    end
  end
end

If you are using pragma-rails, you may change the context passed to the policy by defining a #policy_context method on your controller. This way you are not forced to override #current_user or #pragma_user:

module API
  module V1
    class PostsController < ApplicationController
      # ...

      private

      def policy_context
        OpenStruct.new(ip: request.remote_ip, user: current_user)
      end
    end
  end
end

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/pragmarb/pragma-policy.

License

The gem is available as open source under the terms of the MIT License.