Skip to content

Commit

Permalink
Allow policies to delegate to Pundit
Browse files Browse the repository at this point in the history
  • Loading branch information
aldesantis committed May 21, 2018
1 parent 321568c commit 50bde14
Show file tree
Hide file tree
Showing 12 changed files with 276 additions and 59 deletions.
4 changes: 2 additions & 2 deletions .rubocop.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
require: rubocop-rspec

AllCops:
TargetRubyVersion: 2.3
TargetRubyVersion: 2.5
Include:
- '**/Gemfile'
- '**/Rakefile'
Expand Down Expand Up @@ -53,7 +53,7 @@ Style/SignalException:
Style/BracesAroundHashParameters:
EnforcedStyle: context_dependent

Lint/EndAlignment:
Layout/EndAlignment:
EnforcedStyleAlignWith: variable
AutoCorrect: true

Expand Down
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

## [Unreleased]

### Added

- Added `Pragma::Policy::Pundit` for policies that just delegate to Pundit

### Changed

- `Pragma::Policy::Base::Scope` has been moved to `Pragma::Policy::Scope` (alias provided for BC)
- `Pragma::Policy::UnauthorizedError` no longer inherits from `Pundit::UnauthorizedError`

### Removed

- Dropped Pundit dependency

## [2.0.0]

First Pragma 2 release.
Expand Down
40 changes: 38 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,15 +78,15 @@ end

You are ready to use your policy!

### Retrieving Records
### Retrieving records

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

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

### Authorizing Operations
### Authorizing operations

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

Expand All @@ -103,6 +103,42 @@ 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](https://github.com/varvet/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:

```ruby
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:

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

## Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/pragmarb/pragma-policy.
Expand Down
6 changes: 2 additions & 4 deletions lib/pragma/policy.rb
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
# frozen_string_literal: true

require 'pundit'

require 'pragma/policy/version'
require 'pragma/policy/scope'
require 'pragma/policy/base'
require 'pragma/policy/pundit'
require 'pragma/policy/errors'

module Pragma
# Fine-grained access control for your API resources.
#
# @author Alessandro Desantis
module Policy
end
end
33 changes: 1 addition & 32 deletions lib/pragma/policy/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,40 +7,9 @@ module Policy
# A policy provides predicate methods for determining whether a user can perform a specific
# action on a record.
#
# @author Alessandro Desantis
#
# @abstract Subclass and implement action methods to create a policy.
class Base
# Authorizes AR scopes and other relations by only returning the records accessible by the
# current user. Used, for instance, in index operations.
#
# @author Alessandro Desantis
class Scope
# @!attribute [r] user
# @return [Object] the user accessing the records
#
# @!attribute [r] scope
# @return [Object] the relation to use as a base
attr_reader :user, :scope

# Initializes the scope.
#
# @param user [Object] the user accessing the records
# @param scope [Object] the relation to use as a base
def initialize(user, scope)
@user = user
@scope = scope
end

# Returns the records accessible by the given user.
#
# @return [Object]
#
# @abstract Override to implement retrieving the accessible records
def resolve
fail NotImplementedError
end
end
Scope = ::Pragma::Policy::Scope

# @!attribute [r] user
# @return [Object] the user operating on the record
Expand Down
2 changes: 1 addition & 1 deletion lib/pragma/policy/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

module Pragma
module Policy
class NotAuthorizedError < Pundit::NotAuthorizedError
class NotAuthorizedError < StandardError
end
end
end
59 changes: 59 additions & 0 deletions lib/pragma/policy/pundit.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# frozen_string_literal: true

module Pragma
module Policy
# Provides a simple way for Pragma policies to delegate to Pundit policies/scopes.
#
# @example
# class API::V1::Article::Policy < Pragma::Policy::Pundit
# # The default would be ArticlePolicy.
# self.pundit_klass = CustomArticlePolicy
# end
class Pundit < Base
class << self
def pundit_klass=(klass)
@pundit_klass = klass
end

def pundit_klass
@pundit_klass ||= Object.const_get("#{self.class.name.split('::')[-2]}Policy")
end

def inherited(base)
base.class_eval <<~RUBY
class Scope < Pragma::Policy::Scope
def initialize(user, scope)
super
@pundit_scope = pundit_scope_klass.new(user, scope)
end
def resolve
@pundit_scope.resolve
end
private
def pundit_scope_klass
policy_klass.pundit_klass.const_get('Scope')
end
end
RUBY
end
end

def initialize(user, record)
super
@pundit_policy = self.class.pundit_klass.new(user, record)
end

def respond_to_missing?(method_name, include_private = false)
super || @pundit_policy.respond_to?("#{method_name[0..-2]}?", include_private)
end

def method_missing(method_name, *args, &block)
return super unless @pundit_policy.respond_to?(method_name)
@pundit_policy.send(method_name, *args, &block)
end
end
end
end
38 changes: 38 additions & 0 deletions lib/pragma/policy/scope.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
module Pragma
module Policy
# Authorizes AR scopes and other relations by only returning the records accessible by the
# current user. Used, for instance, in index operations.
class Scope
# @!attribute [r] user
# @return [Object] the user accessing the records
#
# @!attribute [r] scope
# @return [Object] the relation to use as a base
attr_reader :user, :scope

# Initializes the scope.
#
# @param user [Object] the user accessing the records
# @param scope [Object] the relation to use as a base
def initialize(user, scope)
@user = user
@scope = scope
end

# Returns the records accessible by the given user.
#
# @return [Object]
#
# @abstract Override to implement retrieving the accessible records
def resolve
fail NotImplementedError
end

private

def policy_klass
Object.const_get(self.class.name.split('::')[0..-2].join('::'))
end
end
end
end
2 changes: 0 additions & 2 deletions pragma-policy.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@ Gem::Specification.new do |spec|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
spec.require_paths = ['lib']

spec.add_dependency 'pundit', '~> 1.1'

spec.add_development_dependency 'bundler'
spec.add_development_dependency 'rake'
spec.add_development_dependency 'rspec'
Expand Down
16 changes: 0 additions & 16 deletions spec/pragma/policy/base_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,6 @@

let(:policy_klass) do
Class.new(described_class) do
class Scope < Pragma::Policy::Base::Scope
def resolve
[OpenStruct.new(id: 1)]
end
end

def show?
user.id == record.author_id
end
Expand Down Expand Up @@ -55,14 +49,4 @@ def show?
end
end
end

describe Pragma::Policy::Base::Scope do
subject { policy_klass.const_get('Scope').new(user, nil) }

describe '#resolve' do
it 'returns the records accessible by the user' do
expect(subject.resolve.first.id).to eq(1)
end
end
end
end

0 comments on commit 50bde14

Please sign in to comment.