Policies provide fine-grained access control for your API resources.
Add this line to your application's Gemfile:
gem 'pragma-policy'
And then execute:
$ bundle
Or install it yourself as:
$ gem install pragma-policy
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!
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
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
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
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
Bug reports and pull requests are welcome on GitHub at https://github.com/pragmarb/pragma-policy.
The gem is available as open source under the terms of the MIT License.