pundit-expected-attribute-values 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 13d3ca03cc6ed67002240c06411113474f4627e91ada88e42d023b565df7faba
4
+ data.tar.gz: c8b6536f9e9c3c111a5eb80b0a35ff775d66bf690f3c30136bb20168c72b61b8
5
+ SHA512:
6
+ metadata.gz: f9bc11575eef2a6ccc2caf923fcff8464e442a834f25fe08db3bfd0b5325e131ebef94d2ac5e4fbd81386c99d5e0d37537f966d3696adc16ccabb6f91d9920c0
7
+ data.tar.gz: 628277f5210d88d74c470fb9f113d3bbe32ab59bbed6b5858db6f4c935236c7b64133dded419fb17168550eee847cf45b70c3a9e56612f4d3f60757e0b45dbeb
data/CHANGELOG.md ADDED
@@ -0,0 +1,26 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.0.0] - 2026-06-13
9
+
10
+ ### Added
11
+
12
+ - Per-attribute allowed values for Pundit `expected_attributes`, declared in policy classes via `expected_attribute_values_for_action` and action-specific helpers such as `expected_attribute_values_for_update`.
13
+ - Controller integration through `Pundit::ExpectedAttributeValues::Authorization`, including `expected_attributes` value filtering and `pundit_expected_attribute_values_for`.
14
+ - `Pundit::ExpectedAttributeValues.filter` for manual filtering of permitted params or plain hashes.
15
+ - Configurable invalid-value behavior (`:strip` or `:raise`) via `Pundit::ExpectedAttributeValues.configure`.
16
+ - `Pundit::ExpectedAttributeValues::UnexpectedValue` exception when `invalid_behavior` is `:raise`.
17
+ - Value sources: static arrays, callables, and method references resolved through the policy instance.
18
+ - RSpec matchers (`permit_expected_value`, `permit_expected_values`) and Minitest assertions.
19
+ - Rails install generator: `rails generate pundit:expected_attribute_values:install`.
20
+ - Compatibility shim for Pundit 2.5 controllers that do not yet ship `expected_attributes`.
21
+
22
+ ### Fixed
23
+
24
+ - Preserve the permitted state of `ActionController::Parameters` when filtering already-permitted params from `params.expect`, avoiding `ActiveModel::ForbiddenAttributesError` on assignment.
25
+
26
+ [1.0.0]: https://github.com/davedkg/pundit-expected-attribute-values/releases/tag/v1.0.0
@@ -0,0 +1,129 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our
6
+ community a harassment-free experience for everyone, regardless of age, body
7
+ size, visible or invisible disability, ethnicity, sex characteristics, gender
8
+ identity and expression, level of experience, education, socio-economic status,
9
+ nationality, personal appearance, race, religion, or sexual identity
10
+ and orientation.
11
+
12
+ We pledge to act and interact in ways that contribute to an open, welcoming,
13
+ diverse, inclusive, and healthy community.
14
+
15
+ ## Our Standards
16
+
17
+ Examples of behavior that contributes to a positive environment for our
18
+ community include:
19
+
20
+ * Demonstrating empathy and kindness toward other people
21
+ * Being respectful of differing opinions, viewpoints, and experiences
22
+ * Giving and gracefully accepting constructive feedback
23
+ * Accepting responsibility and apologizing to those affected by our mistakes,
24
+ and learning from the experience
25
+ * Focusing on what is best not just for us as individuals, but for the
26
+ overall community
27
+
28
+ Examples of unacceptable behavior include:
29
+
30
+ * The use of sexualized language or imagery, and sexual attention or
31
+ advances of any kind
32
+ * Trolling, insulting or derogatory comments, and personal or political attacks
33
+ * Public or private harassment
34
+ * Publishing others' private information, such as a physical or email
35
+ address, without their explicit permission
36
+ * Other conduct which could reasonably be considered inappropriate in a
37
+ professional setting
38
+
39
+ ## Enforcement Responsibilities
40
+
41
+ Community leaders are responsible for clarifying and enforcing our standards of
42
+ acceptable behavior and will take appropriate and fair corrective action in
43
+ response to any behavior that they deem inappropriate, threatening, offensive,
44
+ or harmful.
45
+
46
+ Community leaders have the right and responsibility to remove, edit, or reject
47
+ comments, commits, code, wiki edits, issues, and other contributions that are
48
+ not aligned to this Code of Conduct, and will communicate reasons for moderation
49
+ decisions when appropriate.
50
+
51
+ ## Scope
52
+
53
+ This Code of Conduct applies within all community spaces, and also applies when
54
+ an individual is officially representing the community in public spaces.
55
+ Examples of representing our community include using an official e-mail address,
56
+ posting via an official social media account, or acting as an appointed
57
+ representative at an online or offline event.
58
+
59
+ ## Enforcement
60
+
61
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
62
+ reported to the community leaders responsible for enforcement at
63
+ **davedkg@users.noreply.github.com**.
64
+
65
+ All complaints will be reviewed and investigated promptly and fairly.
66
+
67
+ All community leaders are obligated to respect the privacy and security of the
68
+ reporter of any incident.
69
+
70
+ ## Enforcement Guidelines
71
+
72
+ Community leaders will follow these Community Impact Guidelines in determining
73
+ the consequences for any action they deem in violation of this Code of Conduct:
74
+
75
+ ### 1. Correction
76
+
77
+ **Community Impact**: Use of inappropriate language or other behavior deemed
78
+ unprofessional or unwelcome in the community.
79
+
80
+ **Consequence**: A private, written warning from community leaders, providing
81
+ clarity around the nature of the violation and an explanation of why the
82
+ behavior was inappropriate. A public apology may be requested.
83
+
84
+ ### 2. Warning
85
+
86
+ **Community Impact**: A violation through a single incident or series
87
+ of actions.
88
+
89
+ **Consequence**: A warning with consequences for continued behavior. No
90
+ interaction with the people involved, including unsolicited interaction with
91
+ those enforcing the Code of Conduct, for a specified period of time. This
92
+ includes avoiding interactions in community spaces as well as external channels
93
+ like social media. Violating these terms may lead to a temporary or
94
+ permanent ban.
95
+
96
+ ### 3. Temporary Ban
97
+
98
+ **Community Impact**: A serious violation of community standards, including
99
+ sustained inappropriate behavior.
100
+
101
+ **Consequence**: A temporary ban from any sort of interaction or public
102
+ communication with the community for a specified period of time. No public or
103
+ private interaction with the people involved, including unsolicited interaction
104
+ with those enforcing the Code of Conduct, is allowed during this period.
105
+ Violating these terms may lead to a permanent ban.
106
+
107
+ ### 4. Permanent Ban
108
+
109
+ **Community Impact**: Demonstrating a pattern of violation of community
110
+ standards, including sustained inappropriate behavior, harassment of an
111
+ individual, or aggression toward or disparagement of classes of individuals.
112
+
113
+ **Consequence**: A permanent ban from any sort of public interaction within
114
+ the community.
115
+
116
+ ## Attribution
117
+
118
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
119
+ version 2.0, available at
120
+ https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
121
+
122
+ Community Impact Guidelines were inspired by [Mozilla's code of conduct
123
+ enforcement ladder](https://github.com/mozilla/diversity).
124
+
125
+ [homepage]: https://www.contributor-covenant.org
126
+
127
+ For answers to common questions about this code of conduct, see the FAQ at
128
+ https://www.contributor-covenant.org/faq. Translations are available at
129
+ https://www.contributor-covenant.org/translations.
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,39 @@
1
+ # Contributing
2
+
3
+ Thanks for your interest in contributing to **pundit-expected-attribute-values**.
4
+
5
+ ## Development setup
6
+
7
+ ```bash
8
+ git clone https://github.com/davedkg/pundit-expected-attribute-values.git
9
+ cd pundit-expected-attribute-values
10
+ bin/setup
11
+ bundle exec rake
12
+ ```
13
+
14
+ `bin/setup` installs dependencies. `bundle exec rake` runs Minitest, RSpec, and is the default task.
15
+
16
+ ## Pull requests
17
+
18
+ 1. Open an issue first for substantial changes so we can agree on the approach.
19
+ 2. Add or update tests for the behavior you change.
20
+ 3. Run the full test suite and RuboCop before opening a PR:
21
+
22
+ ```bash
23
+ bundle exec rake
24
+ bundle exec rubocop
25
+ ```
26
+
27
+ 4. Keep commits focused and write clear commit messages.
28
+
29
+ ## Reporting bugs
30
+
31
+ Open a [GitHub issue](https://github.com/davedkg/pundit-expected-attribute-values/issues) with:
32
+
33
+ - Ruby, Rails, Pundit, and gem versions
34
+ - Steps to reproduce
35
+ - Expected vs. actual behavior
36
+
37
+ ## Code of conduct
38
+
39
+ This project follows the [Contributor Covenant](CODE_OF_CONDUCT.md). By participating, you agree to uphold it.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 David Guilfoyle
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,165 @@
1
+ # pundit-expected-attribute-values
2
+
3
+ [![CI](https://github.com/davedkg/pundit-expected-attribute-values/actions/workflows/main.yml/badge.svg)](https://github.com/davedkg/pundit-expected-attribute-values/actions/workflows/main.yml)
4
+ [![Gem Version](https://badge.fury.io/rb/pundit-expected-attribute-values.svg)](https://badge.fury.io/rb/pundit-expected-attribute-values)
5
+
6
+ Expected **values** for [Pundit](https://github.com/varvet/pundit) strong parameters. Works with Pundit 2.6+ `expected_attributes` / `expected_attributes_for_action` and Rails `params.expect`.
7
+
8
+ Declare which scalar values each attribute may have during mass assignment (for example, admins may set `role` to `manager` or `user`, while managers may only set `role` to `user`).
9
+
10
+ Keys stay in `expected_attributes_for_action`; allowed values live in `expected_attribute_values_for_action`.
11
+
12
+ ## Requirements
13
+
14
+ - Ruby >= 3.3
15
+ - Pundit >= 2.5 (ships a compatibility shim for `expected_attributes` until Pundit 2.6 is released)
16
+ - Rails >= 7.0 (uses `params.expect`)
17
+
18
+ ## Installation
19
+
20
+ ```ruby
21
+ gem "pundit-expected-attribute-values"
22
+ ```
23
+
24
+ ```bash
25
+ bundle install
26
+ rails generate pundit:expected_attribute_values:install
27
+ ```
28
+
29
+ ### Manual setup
30
+
31
+ ```ruby
32
+ # app/policies/application_policy.rb
33
+ class ApplicationPolicy
34
+ include Pundit::ExpectedAttributeValues::Policy
35
+ end
36
+
37
+ # app/controllers/application_controller.rb
38
+ class ApplicationController < ActionController::Base
39
+ include Pundit::Authorization
40
+ include Pundit::ExpectedAttributeValues::Authorization
41
+ end
42
+ ```
43
+
44
+ ## Usage
45
+
46
+ ### Policy
47
+
48
+ ```ruby
49
+ class UserPolicy < ApplicationPolicy
50
+ def expected_attributes_for_action(_action)
51
+ [:name, :email, :role]
52
+ end
53
+
54
+ def expected_attribute_values_for_action(_action)
55
+ { role: :allowed_roles }
56
+ end
57
+
58
+ private
59
+
60
+ def allowed_roles
61
+ return %w[user manager admin] if user.admin?
62
+ return %w[user] if user.manager?
63
+
64
+ []
65
+ end
66
+ end
67
+ ```
68
+
69
+ Action-specific value rules (optional):
70
+
71
+ ```ruby
72
+ def expected_attribute_values_for_update
73
+ { role: %w[user] }
74
+ end
75
+ ```
76
+
77
+ Value sources: static arrays, callables (`-> { ... }`), or method references (`:allowed_roles`).
78
+
79
+ ### Controller
80
+
81
+ ```ruby
82
+ def update
83
+ authorize @user
84
+ if @user.update(expected_attributes(@user))
85
+ redirect_to @user
86
+ else
87
+ render :edit
88
+ end
89
+ end
90
+ ```
91
+
92
+ Allowed values for forms or APIs:
93
+
94
+ ```ruby
95
+ pundit_expected_attribute_values_for(@user, :role)
96
+ # => ["user", "manager"]
97
+ ```
98
+
99
+ ### Unexpected values
100
+
101
+ ```ruby
102
+ # config/initializers/pundit-expected-attribute-values.rb
103
+ Pundit::ExpectedAttributeValues.configure do |config|
104
+ config.invalid_behavior = :strip # default — omit unexpected values
105
+ # config.invalid_behavior = :raise
106
+ end
107
+ ```
108
+
109
+ With `:raise`, unexpected values raise `Pundit::ExpectedAttributeValues::UnexpectedValue`:
110
+
111
+ ```ruby
112
+ rescue_from Pundit::ExpectedAttributeValues::UnexpectedValue, with: :unprocessable
113
+ ```
114
+
115
+ ### Manual filtering
116
+
117
+ ```ruby
118
+ attrs = expected_attributes(@user)
119
+ # or on an extracted hash:
120
+ Pundit::ExpectedAttributeValues.filter(attrs, policy(@user), action: "update")
121
+ ```
122
+
123
+ ## Testing
124
+
125
+ ### RSpec
126
+
127
+ ```ruby
128
+ # spec/support/pundit-expected-attribute-values.rb
129
+ require "pundit/expected_attribute_values/rspec"
130
+
131
+ expect(user_policy).to permit_expected_value(:role, "user")
132
+ expect(user_policy).not_to permit_expected_value(:role, "admin")
133
+ expect(user_policy).to permit_expected_values(:role).matching(%w[user manager])
134
+ ```
135
+
136
+ ### Minitest
137
+
138
+ ```ruby
139
+ require "pundit/expected_attribute_values/minitest"
140
+
141
+ assert_permits_expected_value user_policy, :role, "user"
142
+ refute_permits_expected_value user_policy, :role, "admin"
143
+ assert_expected_values user_policy, :role, %w[user manager]
144
+ ```
145
+
146
+ ## Development
147
+
148
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
149
+
150
+ To install this gem onto your local machine, run `bundle exec rake install`.
151
+
152
+ ## Releasing
153
+
154
+ 1. Update the version in `lib/pundit/expected_attribute_values/version.rb`.
155
+ 2. Add a dated section to [CHANGELOG.md](CHANGELOG.md).
156
+ 3. Commit, tag (`git tag vX.Y.Z`), and push the tag.
157
+ 4. GitHub Actions publishes the gem to [RubyGems](https://rubygems.org) when a `v*` tag is pushed. Configure [trusted publishing](https://guides.rubygems.org/trusted-publishing/) on RubyGems.org for this repository (recommended), or publish locally with `bundle exec rake release` after configuring RubyGems credentials.
158
+
159
+ ## Contributing
160
+
161
+ See [CONTRIBUTING.md](CONTRIBUTING.md). Bug reports and pull requests are welcome on GitHub.
162
+
163
+ ## License
164
+
165
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/SECURITY.md ADDED
@@ -0,0 +1,19 @@
1
+ # Security Policy
2
+
3
+ ## Supported Versions
4
+
5
+ | Version | Supported |
6
+ | ------- | ------------------ |
7
+ | 1.0.x | :white_check_mark: |
8
+
9
+ ## Reporting a Vulnerability
10
+
11
+ Please report security vulnerabilities privately by emailing
12
+ **davedkg@users.noreply.github.com** or using
13
+ [GitHub Security Advisories](https://github.com/davedkg/pundit-expected-attribute-values/security/advisories/new).
14
+
15
+ Do not open a public issue for security-sensitive reports.
16
+
17
+ You can expect an initial response within a few business days. If the report is
18
+ accepted, we will work on a fix and coordinate disclosure. If declined, we will
19
+ explain why.
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/base"
4
+
5
+ module Pundit
6
+ module ExpectedAttributeValues
7
+ module Generators
8
+ class InstallGenerator < Rails::Generators::Base
9
+ source_root File.expand_path("templates", __dir__)
10
+
11
+ desc "Install Pundit::ExpectedAttributeValues in ApplicationPolicy and ApplicationController"
12
+
13
+ class_option :test_framework, type: :string, default: nil,
14
+ desc: "Test framework (rspec or minitest)"
15
+
16
+ def add_policy_concern
17
+ inject_into_class policy_path, "ApplicationPolicy", <<-RUBY
18
+
19
+ include Pundit::ExpectedAttributeValues::Policy
20
+ RUBY
21
+ end
22
+
23
+ def add_controller_concern
24
+ inject_into_class controller_path, "ApplicationController", <<-RUBY
25
+
26
+ include Pundit::ExpectedAttributeValues::Authorization
27
+ RUBY
28
+ end
29
+
30
+ def add_test_helper_snippet
31
+ template "test_helper.#{detected_test_framework}.rb", test_helper_destination
32
+ end
33
+
34
+ private
35
+
36
+ def policy_path
37
+ "app/policies/application_policy.rb"
38
+ end
39
+
40
+ def controller_path
41
+ "app/controllers/application_controller.rb"
42
+ end
43
+
44
+ def detected_test_framework
45
+ return options[:test_framework] if options[:test_framework]
46
+
47
+ File.directory?("spec") ? "rspec" : "minitest"
48
+ end
49
+
50
+ def test_helper_destination
51
+ if detected_test_framework == "rspec"
52
+ "spec/support/pundit-expected-attribute-values.rb"
53
+ else
54
+ "test/support/pundit-expected-attribute-values.rb"
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pundit/expected_attribute_values/minitest"
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pundit/expected_attribute_values/rspec"
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pundit
4
+ module ExpectedAttributeValues
5
+ module Authorization
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ prepend ExpectedAttributesCompat unless Pundit::Authorization.method_defined?(:expected_attributes)
10
+ prepend ControllerMethods
11
+ end
12
+
13
+ module ControllerMethods
14
+ def pundit_expected_attribute_values_for(record, attribute, action: action_name)
15
+ policy(record).pundit_expected_attribute_values_for_attribute(attribute, action: action)
16
+ end
17
+
18
+ def expected_attributes(record, action: action_name, **options)
19
+ raw = super
20
+ apply_expected_values_filter(record, raw, action)
21
+ end
22
+
23
+ private
24
+
25
+ def apply_expected_values_filter(record, params, action)
26
+ policy_instance = policy(record)
27
+ constraints = ValueResolver.resolve_hash_for_action(policy_instance, action)
28
+ return params if constraints.empty?
29
+
30
+ Filter.call(
31
+ params,
32
+ constraints,
33
+ invalid: ExpectedAttributeValues.invalid_behavior,
34
+ policy: policy_instance
35
+ )
36
+ end
37
+ end
38
+
39
+ class << self
40
+ def filter(params, policy, action:)
41
+ raise ArgumentError, "action is required" if action.nil?
42
+
43
+ constraints = ValueResolver.resolve_hash_for_action(policy, action)
44
+ Filter.call(
45
+ params,
46
+ constraints,
47
+ invalid: ExpectedAttributeValues.invalid_behavior,
48
+ policy: policy
49
+ )
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pundit
4
+ module ExpectedAttributeValues
5
+ class << self
6
+ attr_accessor :invalid_behavior, :symbolize_values
7
+
8
+ def configure
9
+ yield self if block_given?
10
+ self
11
+ end
12
+ end
13
+
14
+ self.invalid_behavior = :strip
15
+ self.symbolize_values = false
16
+ end
17
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pundit
4
+ module ExpectedAttributeValues
5
+ class UnexpectedValue < Pundit::NotAuthorizedError
6
+ attr_reader :attribute, :value, :expected
7
+
8
+ def initialize(attribute:, value:, expected:)
9
+ @attribute = attribute
10
+ @value = value
11
+ @expected = expected
12
+ super(
13
+ "Value #{value.inspect} is not expected for #{attribute}; " \
14
+ "expected: #{expected.inspect}"
15
+ )
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pundit
4
+ module ExpectedAttributeValues
5
+ # Provides Pundit 2.6's +expected_attributes+ on controllers until a released
6
+ # Pundit version includes it. Not used when +Pundit::Authorization+ already
7
+ # defines the method.
8
+ module ExpectedAttributesCompat
9
+ def expected_attributes(record, action: action_name, param_key: pundit_param_key(record))
10
+ policy_instance = policy(record)
11
+ shape = policy_instance.expected_attributes_for_action(action)
12
+ params.expect(param_key => shape)
13
+ end
14
+
15
+ def pundit_param_key(record)
16
+ Pundit::PolicyFinder.new(record).param_key
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pundit
4
+ module ExpectedAttributeValues
5
+ class Filter
6
+ def self.call(params, constraints, invalid: ExpectedAttributeValues.invalid_behavior, policy: nil)
7
+ new(params, constraints, invalid: invalid, policy: policy).call
8
+ end
9
+
10
+ def initialize(params, constraints, invalid: ExpectedAttributeValues.invalid_behavior, policy: nil)
11
+ @params = params
12
+ @constraints = constraints
13
+ @invalid = invalid
14
+ @policy = policy
15
+ end
16
+
17
+ def call
18
+ return @params if @constraints.empty?
19
+
20
+ result = @params.to_unsafe_h.dup
21
+ @constraints.each do |attribute, source|
22
+ key = find_key(result, attribute)
23
+ next unless key
24
+ next unless result.key?(key)
25
+
26
+ expected = expected_values_for(attribute, source)
27
+ filter_attribute!(result, key, expected)
28
+ end
29
+
30
+ build_parameters(result)
31
+ end
32
+
33
+ private
34
+
35
+ def find_key(hash, attribute)
36
+ return attribute if hash.key?(attribute)
37
+ return attribute.to_s if hash.key?(attribute.to_s)
38
+ return attribute.to_sym if hash.key?(attribute.to_sym)
39
+
40
+ nil
41
+ end
42
+
43
+ def expected_values_for(_attribute, source)
44
+ values = ValueResolver.resolve(source, @policy)
45
+ ValueResolver.normalize_list(values)
46
+ end
47
+
48
+ def filter_attribute!(result, key, expected)
49
+ value = result[key]
50
+
51
+ if value.is_a?(Array)
52
+ filtered = value.select { |element| expected.include?(ValueResolver.normalize_value(element)) }
53
+ handle_filtered(result, key, filtered, value, expected)
54
+ else
55
+ normalized = ValueResolver.normalize_value(value)
56
+ if expected.include?(normalized)
57
+ result[key] = normalized
58
+ else
59
+ handle_unexpected(result, key, value, expected)
60
+ end
61
+ end
62
+ end
63
+
64
+ def handle_filtered(result, key, filtered, original, expected)
65
+ if filtered.empty? && !original.empty?
66
+ handle_unexpected(result, key, original, expected)
67
+ elsif filtered.empty?
68
+ result.delete(key)
69
+ else
70
+ result[key] = filtered.map { |v| ValueResolver.normalize_value(v) }
71
+ end
72
+ end
73
+
74
+ def handle_unexpected(result, key, value, expected)
75
+ case @invalid
76
+ when :raise
77
+ raise UnexpectedValue.new(attribute: key, value: value, expected: expected)
78
+ else
79
+ result.delete(key)
80
+ end
81
+ end
82
+
83
+ def build_parameters(result)
84
+ if defined?(ActionController::Parameters) && @params.is_a?(ActionController::Parameters)
85
+ filtered = ActionController::Parameters.new(result)
86
+ # Preserve the permitted state of the source params. Callers such as
87
+ # the +expected_attributes+ controller helper pass already-permitted
88
+ # params (via +params.expect+); rebuilding would otherwise reset the
89
+ # flag and raise ActiveModel::ForbiddenAttributesError on assignment.
90
+ filtered.permit! if @params.permitted?
91
+ filtered
92
+ else
93
+ result
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "test_helpers"
4
+
5
+ module Pundit
6
+ module ExpectedAttributeValues
7
+ module MinitestAssertions
8
+ def assert_permits_expected_value(policy, attribute, value, action: "update")
9
+ assert ExpectedAttributeValues::TestHelpers.expects_value?(policy, attribute, value, action: action),
10
+ "Expected policy to allow value #{value.inspect} for :#{attribute} on #{action}"
11
+ end
12
+
13
+ def refute_permits_expected_value(policy, attribute, value, action: "update")
14
+ refute ExpectedAttributeValues::TestHelpers.expects_value?(policy, attribute, value, action: action),
15
+ "Expected policy not to allow value #{value.inspect} for :#{attribute} on #{action}"
16
+ end
17
+
18
+ def assert_expected_values(policy, attribute, expected, action: "update")
19
+ assert ExpectedAttributeValues::TestHelpers.matches_expected_values?(
20
+ policy, attribute, expected, action: action
21
+ ),
22
+ lambda {
23
+ actual = ExpectedAttributeValues::TestHelpers.expected_values_for(policy, attribute, action: action)
24
+ "Expected values #{expected.inspect} for :#{attribute} on #{action}, got #{actual.inspect}"
25
+ }
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ Minitest::Test.include Pundit::ExpectedAttributeValues::MinitestAssertions if defined?(Minitest::Test)
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pundit
4
+ module ExpectedAttributeValues
5
+ module Policy
6
+ extend ActiveSupport::Concern
7
+
8
+ def expected_attribute_values_for_action(action_name)
9
+ action_method = "expected_attribute_values_for_#{action_name}"
10
+ if respond_to?(action_method, true)
11
+ public_send(action_method) || {}
12
+ elsif respond_to?(:expected_attribute_values, true)
13
+ expected_attribute_values || {}
14
+ else
15
+ {}
16
+ end
17
+ end
18
+
19
+ def pundit_expected_attribute_values_for_attribute(attribute, action:)
20
+ raise ArgumentError, "action is required" if action.nil?
21
+
22
+ hash = expected_attribute_values_for_action(action.to_s)
23
+ source = hash[attribute.to_sym] || hash[attribute.to_s]
24
+ return [] unless source
25
+
26
+ ValueResolver.normalize_list(ValueResolver.resolve(source, self))
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pundit
4
+ module ExpectedAttributeValues
5
+ class Railtie < Rails::Railtie
6
+ initializer "pundit-expected-attribute-values.configure" do
7
+ # Host apps may call Pundit::ExpectedAttributeValues.configure in an initializer.
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rspec/expectations"
4
+ require_relative "test_helpers"
5
+
6
+ RSpec::Matchers.define :permit_expected_value do |attribute, value|
7
+ match do |policy|
8
+ action = @action || "update"
9
+ Pundit::ExpectedAttributeValues::TestHelpers.expects_value?(policy, attribute, value, action: action)
10
+ end
11
+
12
+ chain :for_action do |action|
13
+ @action = action
14
+ end
15
+
16
+ failure_message do |policy|
17
+ action = @action || "update"
18
+ expected = Pundit::ExpectedAttributeValues::TestHelpers.expected_values_for(policy, attribute, action: action)
19
+ "expected policy to allow value #{value.inspect} for :#{attribute} on #{action}, " \
20
+ "but expected values are #{expected.inspect}"
21
+ end
22
+
23
+ failure_message_when_negated do |policy|
24
+ action = @action || "update"
25
+ expected = Pundit::ExpectedAttributeValues::TestHelpers.expected_values_for(policy, attribute, action: action)
26
+ "expected policy not to allow value #{value.inspect} for :#{attribute} on #{action}, " \
27
+ "but it is in #{expected.inspect}"
28
+ end
29
+
30
+ description do
31
+ "permit expected value :#{attribute} => #{value.inspect}"
32
+ end
33
+ end
34
+
35
+ RSpec::Matchers.define :permit_expected_values do |attribute|
36
+ chain :matching do |values|
37
+ @expected = values
38
+ end
39
+
40
+ chain :for_action do |action|
41
+ @action = action
42
+ end
43
+
44
+ match do |policy|
45
+ raise ArgumentError, "Use .matching(%w[...]) to specify expected values" unless @expected
46
+
47
+ action = @action || "update"
48
+ Pundit::ExpectedAttributeValues::TestHelpers.matches_expected_values?(
49
+ policy, attribute, @expected, action: action
50
+ )
51
+ end
52
+
53
+ failure_message do |policy|
54
+ action = @action || "update"
55
+ actual = Pundit::ExpectedAttributeValues::TestHelpers.expected_values_for(policy, attribute, action: action)
56
+ "expected policy to allow values #{@expected.inspect} for :#{attribute} on #{action}, " \
57
+ "but expected values are #{actual.inspect}"
58
+ end
59
+
60
+ description do
61
+ "permit expected values for :#{attribute}"
62
+ end
63
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pundit
4
+ module ExpectedAttributeValues
5
+ module TestHelpers
6
+ module_function
7
+
8
+ def expected_values_for(policy, attribute, action:)
9
+ raise ArgumentError, "action is required" if action.nil?
10
+
11
+ policy.pundit_expected_attribute_values_for_attribute(attribute, action: action)
12
+ end
13
+
14
+ def expects_value?(policy, attribute, value, action:)
15
+ expected_values_for(policy, attribute, action: action).include?(
16
+ ValueResolver.normalize_value(value)
17
+ )
18
+ end
19
+
20
+ def refutes_expected_value?(policy, attribute, value, action:)
21
+ !expects_value?(policy, attribute, value, action: action)
22
+ end
23
+
24
+ def matches_expected_values?(policy, attribute, expected, action:)
25
+ actual = expected_values_for(policy, attribute, action: action)
26
+ expected_list = ValueResolver.normalize_list(expected)
27
+ actual.sort == expected_list.sort
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pundit
4
+ module ExpectedAttributeValues
5
+ module ValueResolver
6
+ module_function
7
+
8
+ def resolve(source, policy)
9
+ case source
10
+ when Proc
11
+ policy.instance_exec(&source)
12
+ when Symbol
13
+ policy.public_send(source)
14
+ else
15
+ source
16
+ end
17
+ end
18
+
19
+ def resolve_hash_for_action(policy, action)
20
+ action_method = "expected_attribute_values_for_#{action}"
21
+ if policy.respond_to?(action_method, true)
22
+ policy.public_send(action_method) || {}
23
+ elsif policy.respond_to?(:expected_attribute_values_for_action, true)
24
+ policy.expected_attribute_values_for_action(action) || {}
25
+ elsif policy.respond_to?(:expected_attribute_values, true)
26
+ policy.expected_attribute_values || {}
27
+ else
28
+ {}
29
+ end
30
+ end
31
+
32
+ def normalize_list(values)
33
+ Array(values).map { |v| normalize_value(v) }
34
+ end
35
+
36
+ def normalize_value(value)
37
+ return value.to_sym if ExpectedAttributeValues.symbolize_values && value.respond_to?(:to_sym)
38
+
39
+ value.is_a?(Symbol) ? value.to_s : value
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pundit
4
+ module ExpectedAttributeValues
5
+ VERSION = "1.0.0"
6
+ end
7
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pundit"
4
+ require "active_support/concern"
5
+
6
+ require_relative "pundit/expected_attribute_values/version"
7
+ require_relative "pundit/expected_attribute_values/configuration"
8
+ require_relative "pundit/expected_attribute_values/errors"
9
+ require_relative "pundit/expected_attribute_values/value_resolver"
10
+ require_relative "pundit/expected_attribute_values/filter"
11
+ require_relative "pundit/expected_attribute_values/policy"
12
+ require_relative "pundit/expected_attribute_values/expected_attributes_compat"
13
+ require_relative "pundit/expected_attribute_values/authorization"
14
+ require_relative "pundit/expected_attribute_values/test_helpers"
15
+
16
+ module Pundit
17
+ module ExpectedAttributeValues
18
+ class Error < StandardError; end
19
+
20
+ def self.filter(params, policy, action:)
21
+ Authorization.filter(params, policy, action: action)
22
+ end
23
+ end
24
+ end
25
+
26
+ require_relative "pundit/expected_attribute_values/railtie" if defined?(Rails::Railtie)
@@ -0,0 +1,6 @@
1
+ module Pundit
2
+ module ExpectedAttributeValues
3
+ VERSION: String
4
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
5
+ end
6
+ end
metadata ADDED
@@ -0,0 +1,117 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pundit-expected-attribute-values
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - davedkg
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: actionpack
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '7.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '7.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: activesupport
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '7.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '7.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: pundit
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '2.5'
47
+ - - "<"
48
+ - !ruby/object:Gem::Version
49
+ version: '3.0'
50
+ type: :runtime
51
+ prerelease: false
52
+ version_requirements: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: '2.5'
57
+ - - "<"
58
+ - !ruby/object:Gem::Version
59
+ version: '3.0'
60
+ description: Extend Pundit expected_attributes with per-attribute allowed values,
61
+ declared in policy classes alongside expected_attributes_for_action.
62
+ email:
63
+ - davedkg@users.noreply.github.com
64
+ executables: []
65
+ extensions: []
66
+ extra_rdoc_files: []
67
+ files:
68
+ - CHANGELOG.md
69
+ - CODE_OF_CONDUCT.md
70
+ - CONTRIBUTING.md
71
+ - LICENSE.txt
72
+ - README.md
73
+ - SECURITY.md
74
+ - lib/generators/pundit/expected_attribute_values/install_generator.rb
75
+ - lib/generators/pundit/expected_attribute_values/templates/test_helper.minitest.rb
76
+ - lib/generators/pundit/expected_attribute_values/templates/test_helper.rspec.rb
77
+ - lib/pundit/expected_attribute_values/authorization.rb
78
+ - lib/pundit/expected_attribute_values/configuration.rb
79
+ - lib/pundit/expected_attribute_values/errors.rb
80
+ - lib/pundit/expected_attribute_values/expected_attributes_compat.rb
81
+ - lib/pundit/expected_attribute_values/filter.rb
82
+ - lib/pundit/expected_attribute_values/minitest.rb
83
+ - lib/pundit/expected_attribute_values/policy.rb
84
+ - lib/pundit/expected_attribute_values/railtie.rb
85
+ - lib/pundit/expected_attribute_values/rspec.rb
86
+ - lib/pundit/expected_attribute_values/test_helpers.rb
87
+ - lib/pundit/expected_attribute_values/value_resolver.rb
88
+ - lib/pundit/expected_attribute_values/version.rb
89
+ - lib/pundit_expected_attribute_values.rb
90
+ - sig/pundit-expected-attribute-values.rbs
91
+ homepage: https://github.com/davedkg/pundit-expected-attribute-values
92
+ licenses:
93
+ - MIT
94
+ metadata:
95
+ homepage_uri: https://github.com/davedkg/pundit-expected-attribute-values
96
+ source_code_uri: https://github.com/davedkg/pundit-expected-attribute-values
97
+ changelog_uri: https://github.com/davedkg/pundit-expected-attribute-values/blob/main/CHANGELOG.md
98
+ bug_tracker_uri: https://github.com/davedkg/pundit-expected-attribute-values/issues
99
+ rubygems_mfa_required: 'true'
100
+ rdoc_options: []
101
+ require_paths:
102
+ - lib
103
+ required_ruby_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: 3.3.0
108
+ required_rubygems_version: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ version: '0'
113
+ requirements: []
114
+ rubygems_version: 3.6.9
115
+ specification_version: 4
116
+ summary: Expected attribute values for Pundit policies
117
+ test_files: []