Skip to content

Commit

Permalink
Merge branch 'main' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
Burgestrand authored May 13, 2024
2 parents 32b1145 + 0ab259e commit d65edca
Show file tree
Hide file tree
Showing 18 changed files with 470 additions and 158 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ jobs:
fail-fast: false
matrix:
ruby-version:
- '3.0'
- '3.1'
- '3.2'
- '3.3'
- 'jruby-9.3.10' # oldest supported jruby
- 'jruby'
include: # HEAD-versions
Expand Down
33 changes: 33 additions & 0 deletions .github/workflows/push_gem.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: Push Gem

on:
workflow_dispatch:

permissions:
contents: read

jobs:
push:
if: github.repository == 'varvet/pundit'
runs-on: ubuntu-latest

permissions:
contents: write
id-token: write

steps:
# Set up
- name: Harden Runner
uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1
with:
egress-policy: audit

- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
- name: Set up Ruby
uses: ruby/setup-ruby@cacc9f1c0b3f4eb8a16a6bb0ed10897b43b9de49 # v1.176.0
with:
bundler-cache: true
ruby-version: ruby

# Release
- uses: rubygems/release-gem@612653d273a73bdae1df8453e090060bb4db5f31 # v1
20 changes: 4 additions & 16 deletions .rubocop.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
AllCops:
TargetRubyVersion: 2.6
TargetRubyVersion: 3.1
Exclude:
- "lib/generators/**/templates/**/*"
<% `git status --ignored --porcelain`.lines.grep(/^!! /).each do |path| %>
Expand All @@ -23,15 +23,6 @@ Metrics/ModuleLength:
Layout/LineLength:
Max: 120

Metrics/AbcSize:
Enabled: false

Metrics/CyclomaticComplexity:
Enabled: false

Metrics/PerceivedComplexity:
Enabled: false

Gemspec/RequiredRubyVersion:
Enabled: false

Expand Down Expand Up @@ -62,14 +53,11 @@ Style/StringLiteralsInInterpolation:
Style/StructInheritance:
Enabled: false

Style/AndOr:
Enabled: false

Style/Not:
Enabled: false

Style/DoubleNegation:
Enabled: false

Style/Documentation:
Enabled: false # TODO: Enable again once we have more docs

Style/HashSyntax:
EnforcedShorthandSyntax: never
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,19 @@

## Unreleased

## 2.3.2 (2024-05-08)

- Refactor: First pass of Pundit::Context (#797)

## Changed

- Update `ApplicationPolicy` generator to qualify the `Scope` class name (#792)
- Policy generator uses `NoMethodError` to indicate `#resolve` is not implemented (#776)

## Deprecated

- Dropped support for Ruby 3.0 (#796)

## 2.3.1 (2023-07-17)

### Fixed
Expand Down
43 changes: 36 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,22 @@
# Pundit

[![Main](https://github.com/varvet/pundit/actions/workflows/main.yml/badge.svg)](https://github.com/varvet/pundit/actions/workflows/main.yml)
[![Code Climate](https://codeclimate.com/github/varvet/pundit.svg)](https://codeclimate.com/github/varvet/pundit)
[![Code Climate](https://api.codeclimate.com/v1/badges/a940030f96c9fb43046a/maintainability)](https://codeclimate.com/github/varvet/pundit/maintainability)
[![Inline docs](http://inch-ci.org/github/varvet/pundit.svg?branch=main)](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 straightforward, robust, and
scalable authorization system.

Links:
## Links:

- [API documentation for the most recent version](http://www.rubydoc.info/gems/pundit)
- [Source Code](https://github.com/varvet/pundit)
- [Contributing](https://github.com/varvet/pundit/blob/main/CONTRIBUTING.md)
- [Code of Conduct](https://github.com/varvet/pundit/blob/main/CODE_OF_CONDUCT.md)

Sponsored by:

[<img src="https://www.varvet.com/images/wordmark-red.svg" alt="Varvet" height="50px"/>](https://www.varvet.com)
<strong>Sponsored by:</strong> <a href="https://www.varvet.com">Varvet<br><br><img src="https://github.com/varvet/pundit/assets/99166/aa9efa0a-6903-4037-abee-1824edc57f1a" alt="Varvet logo" height="120"></div>

## Installation

Expand Down Expand Up @@ -279,7 +277,7 @@ generator, or create your own base class to inherit from:

``` ruby
class PostPolicy < ApplicationPolicy
class Scope < Scope
class Scope < ApplicationPolicy::Scope
def resolve
if user.admin?
scope.all
Expand Down Expand Up @@ -483,7 +481,7 @@ example, associations which might be `nil`.

```ruby
class NilClassPolicy < ApplicationPolicy
class Scope < Scope
class Scope < ApplicationPolicy::Scope
def resolve
raise Pundit::NotDefinedError, "Cannot scope NilClass"
end
Expand Down Expand Up @@ -792,6 +790,37 @@ describe PostPolicy do
end
```

You can customize the description used for the `permit` matcher:

``` ruby
Pundit::RSpec::Matchers.description =
"permit the user"
```

given the spec

```ruby
permissions :update?, :show? do
it { expect(policy).to permit(user, record) }
end
```

will change the output from

```
update? and show?
is expected to permit #<User id: 105> and #<User id: 106>
```

to

```
update? and show?
is expected to permit the user
```

which may be desirable when distributing policy specs as documentation.

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/punditcommunity/pundit-matchers) gem.

Expand Down
8 changes: 7 additions & 1 deletion lib/generators/pundit/policy/templates/policy.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
<% module_namespacing do -%>
class <%= class_name %>Policy < ApplicationPolicy
class Scope < Scope
# NOTE: Up to Pundit v2.3.1, the inheritance was declared as
# `Scope < Scope` rather than `Scope < ApplicationPolicy::Scope`.
# In most cases the behavior will be identical, but if updating existing
# code, beware of possible changes to the ancestors:
# https://gist.github.com/Burgestrand/4b4bc22f31c8a95c425fc0e30d7ef1f5

class Scope < ApplicationPolicy::Scope
# NOTE: Be explicit about which records you allow access to!
# def resolve
# scope.all
Expand Down
108 changes: 21 additions & 87 deletions lib/pundit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
require "active_support/core_ext/module/introspection"
require "active_support/dependencies/autoload"
require "pundit/authorization"
require "pundit/context"
require "pundit/cache_store/null_store"
require "pundit/cache_store/legacy_store"

# @api private
# To avoid name clashes with common Error naming when mixing in Pundit,
Expand Down Expand Up @@ -64,104 +67,35 @@ def self.included(base)
end

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 possibly_namespaced_record [Object, Array] 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
# @param cache [#[], #[]=] a Hash-like object to cache the found policy instance in
# @raise [NotAuthorizedError] if the given query method returned false
# @return [Object] Always returns the passed object record
def authorize(user, possibly_namespaced_record, query, policy_class: nil, cache: {})
record = pundit_model(possibly_namespaced_record)
policy = if policy_class
policy_class.new(user, record)
# @see [Pundit::Context#authorize]
def authorize(user, record, query, policy_class: nil, cache: nil)
context = if cache
Context.new(user: user, policy_cache: cache)
else
cache[possibly_namespaced_record] ||= policy!(user, possibly_namespaced_record)
Context.new(user: user)
end

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_class = PolicyFinder.new(scope).scope
return unless policy_scope_class

begin
policy_scope = policy_scope_class.new(user, pundit_model(scope))
rescue ArgumentError
raise InvalidConstructorError, "Invalid #<#{policy_scope_class}> constructor is called"
end

policy_scope.resolve
context.authorize(record, query: query, policy_class: policy_class)
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_class = PolicyFinder.new(scope).scope!
return unless policy_scope_class

begin
policy_scope = policy_scope_class.new(user, pundit_model(scope))
rescue ArgumentError
raise InvalidConstructorError, "Invalid #<#{policy_scope_class}> constructor is called"
end

policy_scope.resolve
# @see [Pundit::Context#policy_scope]
def policy_scope(user, *args, **kwargs, &block)
Context.new(user: user).policy_scope(*args, **kwargs, &block)
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))
rescue ArgumentError
raise InvalidConstructorError, "Invalid #<#{policy}> constructor is called"
# @see [Pundit::Context#policy_scope!]
def policy_scope!(user, *args, **kwargs, &block)
Context.new(user: user).policy_scope!(*args, **kwargs, &block)
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"
# @see [Pundit::Context#policy]
def policy(user, *args, **kwargs, &block)
Context.new(user: user).policy(*args, **kwargs, &block)
end

private

def pundit_model(record)
record.is_a?(Array) ? record.last : record
# @see [Pundit::Context#policy!]
def policy!(user, *args, **kwargs, &block)
Context.new(user: user).policy!(*args, **kwargs, &block)
end
end

Expand Down
16 changes: 12 additions & 4 deletions lib/pundit/authorization.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ module Authorization

protected

# @return [Pundit::Context] a new instance of {Pundit::Context} with the current user
def pundit
@pundit ||= Pundit::Context.new(
user: pundit_user,
policy_cache: Pundit::CacheStore::LegacyStore.new(policies)
)
end

# @return [Boolean] whether authorization has been performed, i.e. whether
# one {#authorize} or {#skip_authorization} has been called
def pundit_policy_authorized?
Expand Down Expand Up @@ -64,7 +72,7 @@ def authorize(record, query = nil, policy_class: nil)

@_pundit_policy_authorized = true

Pundit.authorize(pundit_user, record, query, policy_class: policy_class, cache: policies)
pundit.authorize(record, query: query, policy_class: policy_class)
end

# Allow this action not to perform authorization.
Expand Down Expand Up @@ -98,9 +106,9 @@ def policy_scope(scope, policy_scope_class: nil)
#
# @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
# @return [Object] instance of policy class with query methods
def policy(record)
policies[record] ||= Pundit.policy!(pundit_user, record)
pundit.policy!(record)
end

# Retrieves a set of permitted attributes from the policy by instantiating
Expand Down Expand Up @@ -162,7 +170,7 @@ def pundit_user
private

def pundit_policy_scope(scope)
policy_scopes[scope] ||= Pundit.policy_scope!(pundit_user, scope)
policy_scopes[scope] ||= pundit.policy_scope!(scope)
end
end
end
17 changes: 17 additions & 0 deletions lib/pundit/cache_store/legacy_store.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true

module Pundit
module CacheStore
# @api private
class LegacyStore
def initialize(hash = {})
@store = hash
end

def fetch(user:, record:)
_ = user
@store[record] ||= yield
end
end
end
end
Loading

0 comments on commit d65edca

Please sign in to comment.