Skip to content

Commit

Permalink
Use ActiveSupport::Callbacks
Browse files Browse the repository at this point in the history
  • Loading branch information
javierav committed Jan 15, 2023
1 parent 8221205 commit 7e6bad4
Show file tree
Hide file tree
Showing 14 changed files with 472 additions and 117 deletions.
4 changes: 4 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ Metrics/ModuleLength:
- hash
- heredoc

Naming/FileName:
Exclude:
- "**/lib/pundit-before.rb"

Style/Documentation:
Enabled: false

Expand Down
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
## [Unreleased]

## [0.0.1] - 2023-01-03
## [0.0.1] - 2023-01-15
- Initial release
172 changes: 170 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@

![CI](https://github.com/javierav/pundit-before/workflows/CI/badge.svg)

Add before hook to pundit
Adds `before` hook to pundit policy classes to resolve things like varvet/pundit#474. Inspired by action_policy
[pre-checks](https://actionpolicy.evilmartians.io/#/pre_checks).

## Installation

Add this line to your application's Gemfile:

```ruby
gem 'pundit-before'
gem "pundit-before"
```

And then execute:
Expand All @@ -20,6 +21,173 @@ bundle install

## Usage

Use `allow!` inside callback method or block to return `true` without evaluating `edit?` method defined in policy.

```ruby
class UserPolicy < ApplicationPolicy
include Pundit::Before

before :check_admin

def edit?
false
end

private

def check_admin
allow! if user.admin?
end
end

UserPolicy.new(User.new(admin: true), record).edit? # => true
UserPolicy.new(User.new(admin: false), record).edit? # => false
```

Use `deny!` inside callback method or block to return `false` without evaluating `edit?` method defined in policy.

```ruby
class UserPolicy < ApplicationPolicy
include Pundit::Before

before :check_admin

def edit?
true
end

private

def check_admin
deny! unless user.admin?
end
end

UserPolicy.new(User.new(admin: true), record).edit? # => true
UserPolicy.new(User.new(admin: false), record).edit? # => false
```

Internally `before` hook is implemented as `ActiveSupport::Callbacks`, so the callback chain will halt if do any call to
`allow!` or `deny!` method. It's similar as Rails controller action filters works.

### block form

```ruby
class UserPolicy < ApplicationPolicy
include Pundit::Before

before do
allow! if user.admin?
end

def edit?
false
end
end
```

### skip before hook

```ruby
class UserPolicy < ApplicationPolicy
include Pundit::Before

before :check_admin

def edit?
false
end

private

def check_admin
allow! if user.admin?
end
end

class OperatorPolicy < UserPolicy
skip_before :check_admin
end

UserPolicy.new(User.new(admin: true), record).edit? # => true
OperatorPolicy.new(User.new(admin: true), record).edit? # => false
```

### using `only` modifier

```ruby
class UserPolicy < ApplicationPolicy
include Pundit::Before

before :check_admin, only: :update?

def edit?
false
end

private

def check_admin
allow! if user.admin?
end
end

UserPolicy.new(User.new(admin: true), record).edit? # => false
```

### using `except` modifier

```ruby
class UserPolicy < ApplicationPolicy
include Pundit::Before

before :check_admin, except: :edit?

def edit?
false
end

def destroy?
false
end

private

def check_admin
allow! if user.admin?
end
end

UserPolicy.new(User.new(admin: true), record).edit? # => false
UserPolicy.new(User.new(admin: true), record).destroy? # => true
```

### calling multiple methods

```ruby
class UserPolicy < BasePolicy
before :check_presence, :check_admin

def edit?
false
end

private

def check_presence
deny! unless user.present?
end

def check_admin
allow! if user.admin?
end
end

UserPolicy.new(nil, record).edit? # => false
UserPolicy.new(User.new(admin: false), record).edit? # => false
UserPolicy.new(User.new(admin: true), record).edit? # => true
```

## Development

After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests.
Expand Down
3 changes: 3 additions & 0 deletions lib/pundit-before.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# frozen_string_literal: true

require "pundit/before"
74 changes: 56 additions & 18 deletions lib/pundit/before.rb
Original file line number Diff line number Diff line change
@@ -1,60 +1,98 @@
# frozen_string_literal: true

require "active_support/concern"
require "active_support/core_ext/class/attribute"
require "active_support/callbacks"
require "active_support/core_ext/array/wrap"
require "active_support/core_ext/module/redefine_method"
require "active_support/core_ext/object/blank"

require_relative "before/version"

module Pundit
module Before
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
# rubocop:disable Metrics/MethodLength
def self.included(base)
base.extend ClassMethods
base.include ActiveSupport::Callbacks

base.class_eval do
class_attribute :_pundit_before, default: []
define_callbacks :_pundit_before, skip_after_callbacks_if_terminated: true

def self.method_added(method_name)
super

return if @_pundit_before_running
return unless method_name.to_s =~ /.*\?$/ && public_method_defined?(method_name)
return if @_pundit_before_running

@_pundit_before_running = true

old_method = instance_method(method_name)

redefine_method(method_name) do
result = catch :halt do
(_pundit_before.presence || []).each do |name, block|
name.present? ? send(name) : instance_eval(&block)
end
nil
end

result.nil? ? old_method.bind(self).call : result
@_pundit_before_result = nil
@_pundit_before_method = method_name

run_callbacks :_pundit_before

@_pundit_before_result.nil? ? old_method.bind(self).call : @_pundit_before_result
end

@_pundit_before_running = false
end
end
end
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
# rubocop:enable Metrics/MethodLength

class CallbackFilter
def initialize(methods)
@methods = Array(methods).map(&:to_sym)
end

def match?(object)
@methods.include?(object.instance_variable_get(:@_pundit_before_method).to_sym)
end

alias after match?
alias before match?
alias around match?
end

module ClassMethods
def before(method_name=nil, &block)
self._pundit_before = _pundit_before.dup.push([method_name, block])
def before(*method_names, **options, &block)
_normalize_callback_options(options)

if block_given?
set_callback :_pundit_before, :before, **options, &block
else
set_callback :_pundit_before, :before, *method_names, **options
end
end

def skip_before(*method_names, **options)
_normalize_callback_options(options)
skip_callback :_pundit_before, :before, *method_names, **options
end

def _normalize_callback_options(options)
_normalize_callback_option(options, :only, :if)
_normalize_callback_option(options, :except, :unless)
end

def _normalize_callback_option(options, from, to)
return unless (from = options.delete(from))

from = CallbackFilter.new(from)
options[to] = Array(options[to]).unshift(from)
end
end

def allow!
throw :halt, true
@_pundit_before_result = true
throw :abort
end

def deny!
throw :halt, false
@_pundit_before_result = false
throw :abort
end
end
end
2 changes: 1 addition & 1 deletion pundit-before.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Gem::Specification.new do |spec|
#
spec.name = "pundit-before"
spec.version = Pundit::Before.version
spec.summary = "Add before hook to pundit"
spec.summary = "Adds before hook to pundit policy classes"
spec.homepage = "https://github.com/javierav/pundit-before"
spec.license = "MIT"

Expand Down
34 changes: 15 additions & 19 deletions test/test_block.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,27 @@
require "test_helper"

class TestBlock < Minitest::Test
def test_not_admin_and_not_owner
user = User.new(2)
record = Record.new(1, 3)

refute_predicate BeforeWithBlockPolicy.new(user, record), :edit?
class BlockPolicy < BasePolicy
before do
deny! unless user.admin?
end

def edit?
true
end
end

def test_not_admin_and_owner
user = User.new(3)
record = Record.new(1, 3)

refute_predicate BeforeWithBlockPolicy.new(user, record), :edit?
end

def test_admin_and_not_owner
def test_admin
user = User.new(1)
record = Record.new(1, 2)
policy = BlockPolicy.new(user)

refute_predicate BeforeWithBlockPolicy.new(user, record), :edit?
assert_predicate policy, :edit?
end

def test_admin_and_owner
user = User.new(1)
record = Record.new(1, 1)
def test_user
user = User.new(2)
policy = BlockPolicy.new(user)

assert_predicate BeforeWithBlockPolicy.new(user, record), :edit?
refute_predicate policy, :edit?
end
end
Loading

0 comments on commit 7e6bad4

Please sign in to comment.