pundit_extraextra 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: efe9e9d3572cd753f9834532b4e6e913ea62a839146f9f02506aed669de8ddd6
4
+ data.tar.gz: '0384a52b21b8329de96b08106200b80bbb3b5206997a3e5ca53562d8b7c89643'
5
+ SHA512:
6
+ metadata.gz: 75ae6276ffd31361b3924b8b5a7eb82b9e87821231cbce03155dc2790ce8d6b6444aaf01fb686b9743e6db4bc8014905f51d7b7f5477be8207328ff5d39313f0
7
+ data.tar.gz: 67498abe57e9e1f160f9b176e8cc388a37a434c87379acc707890ae15a69dc1a7033901c41d99ce8c0b735aae8a242d9e645cec62d8197aba77cb494de537e85
data/README.md ADDED
@@ -0,0 +1,126 @@
1
+ # PunditExtra
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/pundit_extra.svg)](https://badge.fury.io/rb/pundit_extra)
4
+ [![Build Status](https://github.com/DannyBen/pundit_extra/workflows/Test/badge.svg)](https://github.com/DannyBen/pundit_extra/actions?query=workflow%3ATest)
5
+ [![Maintainability](https://api.codeclimate.com/v1/badges/61990b2b88d45ea6c89d/maintainability)](https://codeclimate.com/github/DannyBen/pundit_extra/maintainability)
6
+
7
+ ---
8
+
9
+ This library borrows functionality from [CanCan(Can)][2] and adds it to [Pundit][1].
10
+
11
+ - `can?` and `cannot?` view helpers
12
+ - `load_resource`, `authorize_resource`, `load_and_authorize_resource` and
13
+ `skip_authorization` controller filters
14
+
15
+ The design intentions were:
16
+
17
+ 1. To ease the transition from CanCanCan to Pundit.
18
+ 2. To reduce boilerplate code in controller methods.
19
+ 3. To keep things simple and intentionally avoid dealing with edge cases or
20
+ endless magical options you need to memorize.
21
+
22
+ ---
23
+
24
+ ## Install
25
+
26
+ Add to your Gemfile:
27
+
28
+ ```
29
+ gem 'pundit_extra'
30
+ ```
31
+
32
+ Add to your `ApplicationController`:
33
+
34
+ ```ruby
35
+ class ApplicationController < ActionController::Base
36
+ include Pundit::Authorization
37
+ include PunditExtra
38
+ end
39
+ ```
40
+
41
+
42
+ ## View Helpers: `can?` and `cannot?`
43
+
44
+ You can use the convenience methods `can?` and `cannot?` in any controller
45
+ and view.
46
+
47
+ - `if can? :assign, @task` is the same as Pundit's `policy(@task).assign?`
48
+ - `if can? :index, Task` is the same as Pundit's `policy(Task).index?`
49
+ - `if cannot? :assign, @task` is the opposite of `can?`
50
+
51
+
52
+ ## Autoload and Authorize Resource
53
+
54
+ You can add these to your controllers to automatically load the resource
55
+ and/or authorize it.
56
+
57
+ ```ruby
58
+ class TasksController < ApplicationController
59
+ before_action :authenticate_user!
60
+ load_resource except: [:index, :create]
61
+ authorize_resource except: [:create]
62
+ end
63
+ ```
64
+
65
+ The `load_resource` filter will create the appropriate instance variable
66
+ based on the current action.
67
+
68
+ The `authorize_resource` filter will call Pundit's `authorize @model` in each
69
+ action.
70
+
71
+ You can use `except: :action`, or `only: :action` to limit the filter to a
72
+ given action or an array of actions.
73
+
74
+ Example:
75
+
76
+ ```ruby
77
+ class TasksController < ApplicationController
78
+ before_action :authenticate_user!
79
+ load_resource except: [:edit, :complete]
80
+ authorize_resource except: :index
81
+
82
+ def index
83
+ # this happens automatically
84
+ # @tasks = policy_scope(Task)
85
+ end
86
+
87
+ def show
88
+ # this happens automatically
89
+ # @task = Task.find params[:id]
90
+ # authorize @task
91
+ end
92
+
93
+ def new
94
+ # this happens automatically
95
+ # @task = Task.new
96
+ # authorize @task
97
+ end
98
+
99
+ def create
100
+ # this happens automatically
101
+ # @task = Task.new task_params
102
+ # authorize @task
103
+ end
104
+
105
+ end
106
+ ```
107
+
108
+ In addition, you can use:
109
+
110
+ - `load_and_authorize_resource` which is a combination shortcut for
111
+ `load_resource` and `authorize_resource`
112
+ - `skip_authorization` which sends `skip_authorization` and
113
+ `skip_policy_scope` to Pundit for all (or the specified) actions.
114
+
115
+ ## Credits
116
+
117
+ - [Jonas Nicklas](https://github.com/jnicklas) @ [Pundit][1]
118
+ - [Bryan Rite](https://github.com/bryanrite), [Ryan Bates](https://github.com/ryanb), [Richard Wilson](https://github.com/Senjai) @ [CanCanCan][2]
119
+ - [Tom Morgan](https://github.com/seven1m)
120
+
121
+ Thanks for building awesome stuff.
122
+
123
+ ---
124
+
125
+ [1]: https://github.com/elabs/pundit
126
+ [2]: https://github.com/CanCanCommunity/cancancan
@@ -0,0 +1,10 @@
1
+ module PunditExtra
2
+ def self.included(_base)
3
+ return unless defined? ActionController::Base
4
+
5
+ ActionController::Base.class_eval do
6
+ include PunditExtra::Helpers
7
+ include PunditExtra::ResourceAutoload
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,15 @@
1
+ module PunditExtra
2
+ module Helpers
3
+ def self.included(base)
4
+ base.helper_method :can?, :cannot? if base.respond_to? :helper_method
5
+ end
6
+
7
+ def can?(action, resource)
8
+ policy(resource).send :"#{action}?"
9
+ end
10
+
11
+ def cannot?(*args)
12
+ !can?(*args)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,345 @@
1
+ require 'active_support/concern'
2
+
3
+ module PunditExtra
4
+ module ResourceAutoload
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ class_attribute :resource_options
9
+ self.resource_options = []
10
+ before_action :process_resource_callbacks
11
+ end
12
+
13
+ module ClassMethods
14
+ def load_resource(resource_name_or_options = {}, options = {})
15
+ store_resource_options(:load, resource_name_or_options, options)
16
+ end
17
+
18
+ def authorize_resource(resource_name_or_options = {}, options = {})
19
+ store_resource_options(:authorize, resource_name_or_options, options)
20
+ end
21
+
22
+ def skip_authorization(options = {})
23
+ before_action :skip_authorization_and_scope, options.dup
24
+ end
25
+
26
+ def load_and_authorize_resource(resource_name_or_options = {}, options = {})
27
+ store_resource_options(:load_and_authorize, resource_name_or_options, options)
28
+ end
29
+
30
+ private
31
+ def store_resource_options(action, resource_name_or_options, options)
32
+ resource_name, options = extract_resource_name_and_options(resource_name_or_options, options)
33
+ self.resource_options += [{ action: action, resource_name: resource_name, options: options }]
34
+ end
35
+
36
+ def extract_resource_name_and_options(resource_name_or_options, options)
37
+ if resource_name_or_options.is_a?(Hash)
38
+ [nil, resource_name_or_options]
39
+ elsif resource_name_or_options.is_a?(Symbol) || resource_name_or_options.is_a?(String)
40
+ [resource_name_or_options.to_s, options]
41
+ else
42
+ [nil, options]
43
+ end
44
+ end
45
+ end
46
+
47
+ def process_resource_callbacks
48
+ self.class.resource_options.each do |resource_option|
49
+ next if skip_action?(resource_option[:options])
50
+
51
+ if resource_option[:action] == :load
52
+ load_resource(resource_option[:resource_name], resource_option[:options])
53
+ elsif resource_option[:action] == :authorize
54
+ authorize_resource(resource_option[:resource_name], resource_option[:options])
55
+ elsif resource_option[:action] == :load_and_authorize
56
+ load_resource(resource_option[:resource_name], resource_option[:options])
57
+ authorize_resource(resource_option[:resource_name], resource_option[:options])
58
+ end
59
+ end
60
+ end
61
+
62
+ def skip_action?(options)
63
+ action = params[:action].to_sym
64
+ (options[:except] && Array(options[:except]).include?(action)) ||
65
+ (options[:only] && !Array(options[:only]).include?(action))
66
+ end
67
+
68
+ def load_resource(resource_name = nil, options = {})
69
+ resource_name = (resource_name || controller_name.singularize).to_s
70
+ instance_name = options[:instance_name] || resource_name
71
+ scope = resource_name.classify.constantize
72
+ action = params[:action]
73
+ varname = instance_name
74
+
75
+ # Use id_param option if provided, otherwise fallback to default pattern
76
+ resource_id_param = options[:id_param] || "#{resource_name}_id"
77
+ resource_id = params[resource_id_param] || params[:id]
78
+
79
+ if resource_name != controller_name.singularize
80
+ # If the resource being loaded isn't the primary resource for the controller
81
+ # we assume we are loading a single instance of it
82
+
83
+ if options[:through]
84
+ # If there's a through option, find the parent instance
85
+ current_instance = find_parent_instance(options[:through])
86
+
87
+ if current_instance
88
+ if options[:singleton]
89
+ # If the relationship is has_one, we load the single associated instance
90
+ resource = current_instance.public_send(resource_name)
91
+ else
92
+ # Otherwise, we find by resource_id or simply the first matching resource
93
+ resource = resource_id ? current_instance.public_send(resource_name.pluralize).find(resource_id) : current_instance.public_send(resource_name.pluralize).first
94
+ end
95
+ else
96
+ resource = nil
97
+ end
98
+ else
99
+ # Load the resource directly if no `through` option or if `resource_id_param` is provided
100
+ resource = scope.find(resource_id)
101
+ end
102
+
103
+ raise ActiveRecord::RecordNotFound, "No valid parent instance found through #{options[:through].join(', ')}" if resource == nil
104
+
105
+ # Authorize the loaded resource for the 'show' action
106
+ authorize resource, "show?"
107
+ else
108
+ resource = if options[:through]
109
+ load_through_resource(options[:through], resource_name, resource_id, action, options)
110
+ else
111
+ load_direct_resource(scope, action, resource_id, options)
112
+ end
113
+ end
114
+
115
+ raise ActiveRecord::RecordNotFound, "Couldn't find #{resource_name.to_s.capitalize} with #{resource_id_param} == #{resource_id}"if resource.nil?
116
+
117
+ if resource.is_a?(ActiveRecord::Relation) || resource.is_a?(Array)
118
+ varname = varname.to_s.pluralize
119
+ end
120
+
121
+ instance_variable_set("@#{varname}", resource)
122
+ end
123
+
124
+ def find_parent_instance(parents)
125
+ Array(parents).each do |parent|
126
+ parent_resource_name = parent.to_s.singularize
127
+ parent_instance = instance_variable_get("@#{parent_resource_name}")
128
+
129
+ return parent_instance if parent_instance
130
+ end
131
+
132
+ nil
133
+ end
134
+
135
+ def load_singleton_resource(current_instance, resource_name, action, options)
136
+ if action == 'create'
137
+ new_resource = resource_name.classify.constantize.new
138
+ new_resource.attributes = resource_attributes(new_resource, action) if new_resource.respond_to?(:attributes=)
139
+ new_resource
140
+ else
141
+ current_instance.public_send(resource_name)
142
+ end
143
+ end
144
+
145
+ def load_index_resource(current_instance, resource_name)
146
+ resource = current_instance.public_send(resource_name.pluralize)
147
+ policy_scope(resource)
148
+ end
149
+
150
+ def find_resource_by_id(current_instance, resource_name, resource_id, find_by_attribute)
151
+ current_instance.public_send(resource_name.pluralize).find_by(find_by_attribute => resource_id)
152
+ end
153
+
154
+ def create_new_resource(resource_name, action)
155
+ new_resource = resource_name.classify.constantize.new
156
+ new_resource.attributes = resource_attributes(new_resource, action)
157
+ new_resource
158
+ end
159
+
160
+ def update_resource(current_instance, resource_name, resource_id, find_by_attribute, action)
161
+ resource = current_instance.public_send(resource_name.pluralize).find_by(find_by_attribute => resource_id)
162
+ unless record.nil?
163
+ authorize resource, "#{action}?"
164
+ resource.attributes = resource_attributes(resource, action) if resource.respond_to?(:attributes=)
165
+ else
166
+ resource = nil
167
+ end
168
+
169
+ resource
170
+ end
171
+
172
+ def load_nested_resource(parent_instances, resource_name, current_instance)
173
+ if parent_instances.size > 1
174
+ query = parent_instances.inject({}) do |hash, parent_instance|
175
+ association_name = parent_instance.class.name.underscore.to_sym
176
+ hash.merge!(association_name => parent_instance)
177
+ end
178
+ resource_name.classify.constantize.find_by(query)
179
+ else
180
+ current_instance.public_send(resource_name.pluralize)
181
+ policy_scope(resource_name.classify.constantize)
182
+ end
183
+ end
184
+
185
+ def load_through_resource(parents, resource_name, resource_id, action, options)
186
+ parent_instances = Array(parents).map do |parent|
187
+ instance_variable_get("@#{parent.to_s.singularize}")
188
+ end.compact
189
+
190
+ if parent_instances.empty?
191
+ raise ActiveRecord::RecordNotFound, "No parent instance found for #{resource_name}"
192
+ end
193
+
194
+ current_instance = parent_instances.first
195
+ find_by_attribute = options[:find_by] || :id
196
+
197
+ resource = if options[:singleton]
198
+ load_singleton_resource(current_instance, resource_name, action, options)
199
+ elsif action == 'index' && (!resource_id && !options[:singleton])
200
+ load_index_resource(current_instance, resource_name)
201
+ elsif resource_id
202
+ find_resource_by_id(current_instance, resource_name, resource_id, find_by_attribute)
203
+ elsif action == 'create'
204
+ create_new_resource(resource_name, action)
205
+ elsif action == 'update'
206
+ update_resource(current_instance, resource_name, resource_id, find_by_attribute, action)
207
+ else
208
+ load_nested_resource(parent_instances, resource_name, current_instance)
209
+ end
210
+
211
+ resource
212
+ end
213
+
214
+ def load_direct_resource(scope, action, resource_id, options = {})
215
+ # Determine the attribute to find by and the parameter to use for the ID
216
+ # if it isn't specified we assume we're finding by the 'id' column
217
+ find_by_attribute = options[:find_by] || :id
218
+
219
+ if action == 'create'
220
+ if resource_id
221
+ resource = scope.find_by(find_by_attribute => resource_id) # Use the custom find_by attribute
222
+ else
223
+ new_resource = scope.new
224
+ new_resource.attributes = resource_attributes(new_resource, action) if new_resource.respond_to?(:attributes=)
225
+ resource = new_resource
226
+ end
227
+ elsif action == 'update'
228
+ resource = scope.find_by(find_by_attribute => resource_id) # Use the custom find_by attribute
229
+ unless resource.nil?
230
+ authorize resource, "#{action}?"
231
+ resource.attributes = resource_attributes(resource, action)
232
+ resource = resource
233
+ else
234
+ resource = nil
235
+ end
236
+ elsif action == 'index'
237
+ resource = policy_scope(scope) # Treat as collection for index
238
+ elsif resource_id
239
+ resource = scope.find_by(find_by_attribute => resource_id) # Use the custom find_by attribute
240
+ else
241
+ resource = policy_scope(scope) # Treat as collection for non-standard actions
242
+ end
243
+
244
+ resource
245
+ end
246
+
247
+ def load_parent_resources(parents)
248
+ Array(parents).each do |parent|
249
+ parent_resource_name = parent.to_s.singularize
250
+ parent_id = params["#{parent_resource_name}_id"]
251
+ parent_instance = instance_variable_get("@#{parent_resource_name}")
252
+
253
+ unless parent_instance
254
+ parent_scope = parent_resource_name.classify.constantize
255
+ parent_instance = parent_scope.find(parent_id)
256
+ instance_variable_set("@#{parent_resource_name}", parent_instance)
257
+ authorize parent_instance, :show?
258
+ end
259
+
260
+ parent_instance
261
+ end
262
+ end
263
+
264
+ def authorize_resource(resource_name = nil, options = {})
265
+ resource_name = (resource_name || controller_name.singularize).to_s
266
+ instance_name = (options[:instance_name] || resource_name).to_s
267
+ resource = instance_variable_get("@#{instance_name}") || resource_name.classify.constantize
268
+
269
+ # Determine if this is a parent resource by checking if it was listed as a `through` resource
270
+ is_parent_resource = self.class.resource_options.any? do |opt|
271
+ opt[:options][:through] && Array(opt[:options][:through]).include?(resource_name.to_sym)
272
+ end
273
+
274
+ action = is_parent_resource ? :show : params[:action].to_sym
275
+ if resource_name != controller_name.singularize
276
+ action = :show
277
+ end
278
+
279
+ if resource.is_a?(Class)
280
+ authorize resource, "#{params[:action].to_sym}?"
281
+ else
282
+ authorize resource, "#{action}?"
283
+ end
284
+ end
285
+
286
+ def skip_authorization_and_scope
287
+ action = params[:action]
288
+ skip_policy_scope if action == 'index'
289
+ skip_authorization
290
+ end
291
+
292
+ def resource_name
293
+ controller_name.singularize
294
+ end
295
+
296
+ def resource_class
297
+ resource_name.classify.constantize
298
+ end
299
+
300
+ def resource_instance
301
+ instance_variable_get "@#{resource_name}"
302
+ end
303
+
304
+ def resource_attributes(resource, action)
305
+ attributes = {}
306
+
307
+ # Get permitted attributes if they are defined
308
+ if has_permitted_attributes?(resource, action)
309
+ attributes = permitted_attributes(resource)
310
+ else
311
+ candidates = ["#{action}_params", "#{resource_name}_params"]
312
+ candidates.each do |candidate|
313
+ if respond_to?(candidate, true)
314
+ attributes.merge!(send(candidate)) { |key, old_val, new_val| old_val }
315
+ break
316
+ end
317
+ end
318
+ end
319
+
320
+ # Extract URL parameters that are part of the resource's attributes
321
+ url_param_keys = request.path_parameters.keys.map(&:to_sym)
322
+
323
+ # Remove :id from the keys to ensure it isn't included
324
+ url_param_keys.delete(:id)
325
+
326
+ relevant_url_params = params.slice(*url_param_keys).permit!.to_h
327
+
328
+ # Merge only the relevant URL parameters that match resource's column names
329
+ relevant_url_params.each do |key, value|
330
+ if resource.class.column_names.include?(key.to_s)
331
+ attributes[key.to_sym] ||= value
332
+ end
333
+ end
334
+
335
+ attributes
336
+ end
337
+
338
+ def has_permitted_attributes?(resource, action)
339
+ return true if policy(resource).respond_to? :"permitted_attributes_for_#{action}"
340
+ return true if policy(resource).respond_to? :permitted_attributes
341
+
342
+ false
343
+ end
344
+ end
345
+ end
@@ -0,0 +1,3 @@
1
+ module PunditExtraExtra
2
+ VERSION = '1.0.0'
3
+ end
@@ -0,0 +1,4 @@
1
+ require 'pundit_extraextra/controller_mixin'
2
+ require 'pundit_extraextra/helpers'
3
+ require 'pundit_extraextra/resource_autoload'
4
+ require 'pundit_extraextra/version'
metadata ADDED
@@ -0,0 +1,50 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pundit_extraextra
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Danny Ben Shitrit
8
+ - Andrew Michael Fahmy
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2024-09-18 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: Add CanCanCan like load and authorize to Pundit.
15
+ email: andrew.michael.fahmy@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - README.md
21
+ - lib/pundit_extraextra.rb
22
+ - lib/pundit_extraextra/controller_mixin.rb
23
+ - lib/pundit_extraextra/helpers.rb
24
+ - lib/pundit_extraextra/resource_autoload.rb
25
+ - lib/pundit_extraextra/version.rb
26
+ homepage: https://github.com/sayre1000/pundit_extraextra
27
+ licenses:
28
+ - MIT
29
+ metadata:
30
+ rubygems_mfa_required: 'true'
31
+ post_install_message:
32
+ rdoc_options: []
33
+ require_paths:
34
+ - lib
35
+ required_ruby_version: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: 3.0.0
40
+ required_rubygems_version: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: '0'
45
+ requirements: []
46
+ rubygems_version: 3.3.7
47
+ signing_key:
48
+ specification_version: 4
49
+ summary: Additions for PunditExtra
50
+ test_files: []