pundit_roles 0.1.2

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
+ SHA1:
3
+ metadata.gz: f9a596e75302e60e5ac89fbd28e1d411f1e80e85
4
+ data.tar.gz: 70ef89deb9dbc45c8c3c3a3386f858662a1480ab
5
+ SHA512:
6
+ metadata.gz: 8f1e0eea9562fa2add0c1c9faebba5cd4a8ccb1441dde646c58eabd76193691da924eb3775cd251ce6658dfe90f5a1eb879f767944032aa5f4b75d3f4cebcf83
7
+ data.tar.gz: 2dcef9da90cad672759c3d315b1259b1a999bd0445fc5a4139d272aef0b1e78ce9fc445fe9ab9260329c773af0a5ecc0924c14af11768dd92000736df00b8558
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ .idea/
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.4.0
5
+ before_install: gem install bundler -v 1.14.6
data/Gemfile ADDED
@@ -0,0 +1,17 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in pundit_roles.gemspec
4
+ gemspec
5
+
6
+ gem 'actionpack'
7
+ gem 'activemodel'
8
+ gem 'pundit', '~> 1.1.0'
9
+
10
+ group :development, :test do
11
+ gem "actionpack"
12
+ gem "activemodel"
13
+ gem "bundler"
14
+ gem "pry"
15
+ gem "rake"
16
+ gem "rspec"
17
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 StairwayB
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,329 @@
1
+ # PunditRoles
2
+
3
+ PunditRoles is a helper gem which works on top of [Pundit](https://github.com/elabs/pundit)
4
+ (if you are not familiar with Pundit, it is recommended you read it's documentation before continuing).
5
+ It allows you to extend Pundit's authorization system to include attributes and associations.
6
+
7
+ If you are already using Pundit, this should not conflict with any of Pundit's existing functionality.
8
+ You may use Pundit's features as well as the features from this gem interchangeably.
9
+
10
+ Please note that this gem is not affiliated with Pundit or it's creators, but it very much
11
+ appreciates the work that they did with their great authorization system.
12
+
13
+ * **Important** This is still early in it's development and is **NOT** considered production
14
+ ready. Consider what is here as a prototype for what will, in the future, be a reliable gem.
15
+ As of yet, bugs and unforeseen issues may be present. If you happen to find any, please feel free
16
+ to raise an issue.
17
+
18
+ ## Installation
19
+
20
+ Add this line to your application's Gemfile:
21
+
22
+ ```ruby
23
+ gem 'pundit_roles'
24
+ ```
25
+
26
+ Add PunditRoles to your ApplicationController(Pundit is included in PunditRoles,
27
+ so no need to add both)
28
+ ```ruby
29
+ class ApplicationController < ActionController::Base
30
+ include PunditRoles
31
+ end
32
+ ```
33
+
34
+ And inherit your ApplicationPolicy from Policy::Base
35
+ ```ruby
36
+ class ApplicationPolicy < Policy::Base
37
+ # def show?
38
+ # [...]
39
+ # end
40
+ end
41
+ ```
42
+
43
+ ## Roles
44
+
45
+ PunditRoles operates around the notion of _**roles**_. Each role needs to be defined at the Policy level
46
+ and provided with a conditional method that determines whether the `@user`(`current_user` in the context of a Policy)
47
+ falls into this role. Additionally, each role can have a set of permitted
48
+ _**attributes**_ and _**associations**_(from here on collectively referred to as **_options_**)
49
+ defined for it. A basic example for a UserPolicy would be:
50
+ ```ruby
51
+ role :user, authorize_with: :logged_in_user
52
+ permitted_for :user,
53
+ attributes: {
54
+ show: %i(username name avatar is_confirmed created_at)
55
+ },
56
+ associations: {
57
+ show: %i(posts followers following)
58
+ }
59
+
60
+ role :correct_user, authorize_with: :correct_user
61
+ permitted_for :correct_user,
62
+ attributes: {
63
+ show: %i(email phone_number confirmed_at updated_at sign_in_count),
64
+ update: %i(username email password password_confirmation current_password name avatar)
65
+ },
66
+ associations: {
67
+ show: %i(settings),
68
+ save: %i(settings)
69
+ }
70
+ ```
71
+
72
+ This assumes that there are two methods defined in the UserPolicy called `logged_in_user?` and
73
+ `correct_user?`. More on that later.
74
+
75
+ And then in you query method, you simply say:
76
+ ```ruby
77
+ def show?
78
+ %i(user correct_user)
79
+ end
80
+
81
+ def update?
82
+ %i(correct_user)
83
+ end
84
+ ```
85
+ Or you may use the `allow` helper method:
86
+ ```ruby
87
+ def show?
88
+ allow :user, :correct_user
89
+ end
90
+ ```
91
+
92
+ Finally, in your controller you call Pundit's `authorize` method and pass it's return value
93
+ to a variable:
94
+ ```ruby
95
+ class UserController < ApplicationController
96
+ def show
97
+ @user = User.find(params[:id])
98
+ permitted = authorize @user
99
+ # [...]
100
+ end
101
+ end
102
+ ```
103
+
104
+ The `authorize` method will return a hash of permitted attributes and associations for the corresponding action that the
105
+ user has access to. What you do with that is your business. Accessors for each segment look like this:
106
+ ```ruby
107
+ permitted[:attributes][:show]
108
+ permitted[:attributes][:create]
109
+
110
+ permitted[:associations][:show]
111
+ permitted[:associations][:update]
112
+ ```
113
+
114
+ If the user does not fall into any roles permitted by a query, the `authorize` method will raise `Pundit::NotAuthorizedError`
115
+
116
+ ### Defining roles
117
+
118
+ Roles are defined with the `role` method. It receives the name of the role as it's first argument and the
119
+ options for the role as it's second. The required option is the `authorize_with` attribute, which is the method
120
+ that validates the role. The validation method must be passed as a symbol without the question mark, and declared
121
+ as a method with a question mark.
122
+
123
+ Currently there are no more options, but some, like database permissions, are planned for future updates.
124
+
125
+ ```ruby
126
+ role :user, authorize_with: :logged_in_user
127
+
128
+ def logged_in_user?
129
+ @user.present?
130
+ end
131
+ ```
132
+
133
+ ### Users with multiple roles
134
+
135
+ You may have noticed that in the first example `correct_user` has fewer permitted options
136
+ defined than `user`. That is because PunditRoles does not treat roles as exclusionary.
137
+ Users may have a single role or they may have multiple roles, within the context of the model they are trying to access.
138
+ In the previous example, a `correct_user`, meaning a `user` trying to access it's own model, is naturally
139
+ also a regular `user`, so it will have access to all options a regular `user` has access to plus the
140
+ options that a `correct_user` has access to.
141
+
142
+ Take this example, to better illustrate what is happening:
143
+
144
+ ```ruby
145
+ role :user, authorize_with: :logged_in_user
146
+ permitted_for :user,
147
+ attributes: {
148
+ show: %i(username name avatar)
149
+ }
150
+
151
+ role :correct_user, authorize_with: :correct_user
152
+ permitted_for :correct_user,
153
+ attributes: {
154
+ show: %i(email phone_number)
155
+ }
156
+
157
+ role :admin, authorize_with: :admin
158
+ permitted_for :admin,
159
+ attributes: {
160
+ show: %i(email is_admin)
161
+ }
162
+ ```
163
+
164
+ Here, a user which fulfills the `admin` condition trying to access it's own model, would receive the
165
+ options of all three roles, meaning the `permitted[:attributes][:show]` would look like:
166
+ ```ruby
167
+ [:username, :name, :avatar, :email, :phone_number, :is_admin]
168
+ ```
169
+ Notice that there are no duplicates. This is because whenever a user tries to access an action,
170
+ PunditRoles will evaluate whether the user falls into the roles permitted to perform said action,
171
+ and if they do, it will uniquely merge the options hashes of all of these.
172
+
173
+ If the user is an `admin`, but is not a `correct_user`, it will not receive the `phone_number` attribute,
174
+ because that is unique to `correct_user` and vice versa.
175
+
176
+ At present, there is no way to prevent merging of roles. Such a feature may be coming in a future update.
177
+
178
+ ### Inheritance and the default Guest role
179
+
180
+ One thing to watch out for is that roles are inherited but options are not.
181
+ This means that you may declare commonly used roles(whose validations are
182
+ independent of the `@record` of the Policy) in the ApplicationPolicy, and may reuse them
183
+ further down the line. You may also overwrite roles defined in a parent class(these will not affect those in the parent).
184
+
185
+ However, it is important to declare the options with the `permitted_for` method for each role that you permit
186
+ in your Policy, otherwise the role will return an empty hash.
187
+
188
+ With that in mind, PunditRoles comes with a default `:guest` role, which simply checks if
189
+ the user is nil. If you wish to permit guest users for a particular action, simply define the
190
+ options for it and allow it in your query method.
191
+
192
+ ```ruby
193
+ class UserPolicy < ApplicationPolicy
194
+ permitted_for :guest,
195
+ attributes: {
196
+ show: %i(username first_name last_name avatar),
197
+ create: %i(username email password password_confirmation first_name last_name avatar)
198
+ },
199
+ associations: {}
200
+
201
+ def show?
202
+ allow :guest
203
+ end
204
+
205
+ def create?
206
+ allow :guest
207
+ end
208
+
209
+ end
210
+ ```
211
+
212
+ * **Important!** The `:guest` role is exclusionary by default, meaning it cannot be merged
213
+ with other roles. It is also the first role that is evaluated, and if the user is a `:guest`, it will return the guest
214
+ attributes if `:guest` is allowed, or raise `PunditNotAuthorized` if not.
215
+ Do **NOT** overwrite the `:guest` role, that can lead to unexpected side effects, and if you wish to allow guest, use
216
+ the existing role and not a custom one.
217
+
218
+ ### Explicit declaration of options
219
+
220
+ Options are declared with the `permitted_for` method, which receives the role as it's first argument,
221
+ and the options as it's second.
222
+
223
+ Valid options for the `permitted_for` method are `:attributes` and `:associations`.
224
+ Within these, valid options are `:show`,`:create`,`:update` and `:save` or the implicit options.
225
+
226
+ ### Implicit declaration of options
227
+
228
+ PunditRoles provides a set of helpers to be able to implicitly declare the options of a role.
229
+
230
+ ---
231
+
232
+ Although this is a possibility, it is _highly recommended_ that you explicitly declare
233
+ attributes for each role, to avoid any issues further in development, like say, an extra
234
+ attribute that is added to a model later down the line.
235
+
236
+ ---
237
+ * **show_all**
238
+
239
+ Will be able to view all non-restricted options.
240
+
241
+ ```ruby
242
+ role :admin, authorize_with: :admin
243
+ permitted_for :admin,
244
+ attributes: :show_all,
245
+ associations: :show_all
246
+ ```
247
+ * **create_all, update_all, save_all**
248
+
249
+ Will be able to create, update or save all non-restricted attributes. These options also
250
+ imply that the role will be able to `show_all` options.
251
+ ```ruby
252
+ role :admin, authorize_with: :admin
253
+ permitted_for :admin,
254
+ attributes: :save_all,
255
+ associations: :update_all
256
+ ```
257
+
258
+ * **all**
259
+
260
+ Declare on a per-action basis whether the role has access to all options.
261
+ ```ruby
262
+ role :admin, authorize_with: :admin
263
+ permitted_for :admin,
264
+ attributes: {
265
+ show: :all,
266
+ save: %i(name username email)
267
+ },
268
+ associations: {
269
+ show: :all
270
+ }
271
+ ```
272
+
273
+ * **all_minus**
274
+
275
+ Can be used to allow all attributes, except those declared.
276
+ ```ruby
277
+ role :admin, authorize_with: :admin
278
+ permitted_for :admin,
279
+ attributes: {
280
+ show: [:all_minus, :password_digest]
281
+ }
282
+ ```
283
+ The `:admin` role will now be able to view all attributes, except `password_digest`.
284
+
285
+ ### Restricted options
286
+
287
+ PunditRoles allows you to define restricted options which will be removed when declaring
288
+ implicitly. By default, only the `:id`, `:created_at`, `:updated_at` attributes are restricted
289
+ for `create`,`update` and `save` actions. You may overwrite this behaviour on a per-policy basis:
290
+ ```ruby
291
+ private
292
+
293
+ def restricted_show_attributes
294
+ [:attr_one, :attr_two]
295
+ end
296
+ ```
297
+ Or if you want to add to it, instead of overwriting, use `super`:
298
+ ```ruby
299
+ private
300
+
301
+ def restricted_create_attributes
302
+ super + [:attr_one, :attr_two]
303
+ end
304
+ ```
305
+
306
+ There are 8 `restricted_#{action}_#{option_type}` methods in total, where `option_type` refers
307
+ to either `attributes` or `associations` and `action` refers to `show`, `create`, `update` or `save`.
308
+
309
+ ## Planned updates
310
+
311
+ Support for Pundit's scope method should be added in the near future, along with authorizing associations,
312
+ generators, and rspec helpers. And once the test suite is finished for this gem, it should be production
313
+ ready.
314
+
315
+ ## Development
316
+
317
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake 'spec'` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
318
+
319
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
320
+
321
+ ## Contributing
322
+
323
+ Bug reports are welcome on GitHub at [StairwayB](https://github.com/StairwayB/pundit_roles).
324
+
325
+
326
+ ## License
327
+
328
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
329
+
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "pundit_roles"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,148 @@
1
+ require_relative 'role'
2
+ require_relative 'policy_defaults/defaults'
3
+
4
+
5
+ module Policy
6
+
7
+ # Base policy class to be extended by all other policies, authorizes users based on roles they fall into,
8
+ # return a uniquely merged hash of permitted attributes and associations of each role the @user has.
9
+ #
10
+ # @param user [Object] the user that initiated the action
11
+ # @param record [Object] the object we're checking permissions of
12
+ class Base
13
+ extend Role
14
+
15
+ include PolicyDefaults::Defaults
16
+
17
+ role :guest, authorize_with: :user_guest
18
+
19
+ attr_reader :user, :record
20
+
21
+ def initialize(user, record)
22
+ @user = user
23
+ @record = record
24
+ end
25
+
26
+ # Is here
27
+ def scope
28
+ Pundit.authorize_scope!(user, record.class, fields)
29
+ end
30
+
31
+ # Retrieves the permitted roles for the current @query, checks if @user is one or more of these roles
32
+ # and return a hash of attributes and associations that the @user has access to.
33
+ #
34
+ # @param query [Symbol, String] the predicate method to check on the policy (e.g. `:show?`)
35
+ def resolve_query(query)
36
+ permitted_roles = public_send(query)
37
+ if permitted_roles.is_a? TrueClass or permitted_roles.is_a? FalseClass
38
+ return permitted_roles
39
+ end
40
+
41
+ permissions_hash = self.class.permissions_hash
42
+
43
+ # Always checks if the @user is a :guest first. :guest users cannot only have the one :guest role
44
+ guest = self.class::Guest.new(self, permissions_hash[:guest])
45
+ if guest.test_condition
46
+ if permitted_roles.include? :guest
47
+ return guest.permitted
48
+ else
49
+ return false
50
+ end
51
+ end
52
+
53
+ current_roles = determine_current_roles(permitted_roles, permissions_hash)
54
+
55
+ unless current_roles.present?
56
+ return false
57
+ end
58
+
59
+ if current_roles.length == 1
60
+ current_roles.values[0][:roles] = current_roles.keys[0]
61
+ return current_roles.values[0]
62
+ end
63
+
64
+ return unique_merge(current_roles)
65
+ end
66
+
67
+ private
68
+
69
+ # Build a hash of the roles that the user fulfills and the roles' attributes and associations,
70
+ # based on the test_condition of the role.
71
+ #
72
+ # @param permitted_roles [Hash] roles returned by the query
73
+ # @param permissions_hash [Hash] unrefined hash of options defined by all permitted_for methods
74
+ def determine_current_roles(permitted_roles, permissions_hash)
75
+ current_roles = {}
76
+
77
+ permitted_roles.each do |permitted_role|
78
+ if permitted_role == :guest
79
+ next
80
+ end
81
+
82
+ begin
83
+ current_role = {class: "#{self.class}::#{permitted_role.to_s.classify}".constantize}
84
+ current_role_obj = current_role[:class].new(self, permissions_hash[permitted_role])
85
+ if current_role_obj.test_condition
86
+ current_roles[permitted_role] = current_role_obj.permitted
87
+ end
88
+ rescue NoMethodError =>e
89
+ raise NoMethodError, "Could not find test condition, needs to be defined as 'test_condition?' and passed to the role as 'authorize_with: :test_condition' => #{e.message}"
90
+ rescue NameError => e
91
+ raise NameError, "#{current_role[:role]} not defined => #{e.message} "
92
+ end
93
+ end
94
+
95
+ return current_roles
96
+ end
97
+
98
+ # Uniquely merge the options of all roles that the user fulfills
99
+ #
100
+ # @param roles [Hash] roles and options that the user fulfills
101
+ def unique_merge(roles)
102
+ merged_hash = {attributes: {}, associations: {}, roles: []}
103
+
104
+ roles.each do |role, option|
105
+ unless option.present?
106
+ next
107
+ end
108
+ merged_hash[:roles] << role
109
+ option.each do |type, actions|
110
+ unless actions.present?
111
+ next
112
+ end
113
+ actions.each do |key, value|
114
+ unless merged_hash[type][key]
115
+ merged_hash[type][key] = []
116
+ end
117
+ merged_hash[type][key] |= value
118
+ end
119
+ end
120
+ end
121
+
122
+ return merged_hash
123
+ end
124
+
125
+ # Helper method to be able to define allow: :guest, :user, etc. in the query methods
126
+ #
127
+ # @param *roles [Array] an array of permitted roles for a particular action
128
+ def allow(*roles)
129
+ return roles
130
+ end
131
+
132
+ # Scope class from Pundit, to be used for limiting scopes. Unchanged from Pundit,
133
+ # possible implementation forthcoming in a future update
134
+ class Scope
135
+ attr_reader :user, :scope
136
+
137
+ def initialize(user, scope)
138
+ @user = user
139
+ @scope = scope
140
+ end
141
+
142
+ def resolve
143
+ scope
144
+ end
145
+
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,78 @@
1
+ module PolicyDefaults
2
+ module Defaults
3
+ # default index? method
4
+ def index?
5
+ false
6
+ end
7
+
8
+ # default show? method
9
+ def show?
10
+ false
11
+ end
12
+
13
+ # default create? method
14
+ def create?
15
+ false
16
+ end
17
+
18
+ # default update? method
19
+ def update?
20
+ false
21
+ end
22
+
23
+ # default destroy? method
24
+ def destroy?
25
+ false
26
+ end
27
+
28
+ # default authorization method
29
+ def default_authorization?
30
+ return false
31
+ end
32
+
33
+ # @authorize_with method for :guest role
34
+ def user_guest?
35
+ @user.nil?
36
+ end
37
+
38
+ # restricted attributes for show
39
+ def restricted_show_attributes
40
+ []
41
+ end
42
+
43
+ # restricted attributes for save
44
+ def restricted_save_attributes
45
+ [:id, :created_at, :updated_at]
46
+ end
47
+
48
+ # restricted attributes for create
49
+ def restricted_create_attributes
50
+ [:id, :created_at, :updated_at]
51
+ end
52
+
53
+ # restricted attributes for update
54
+ def restricted_update_attributes
55
+ [:id, :created_at, :updated_at]
56
+ end
57
+
58
+ # restricted associations for show
59
+ def restricted_show_associations
60
+ []
61
+ end
62
+
63
+ # restricted associations for save
64
+ def restricted_save_associations
65
+ []
66
+ end
67
+
68
+ # restricted associations for create
69
+ def restricted_create_associations
70
+ []
71
+ end
72
+
73
+ # restricted associations for update
74
+ def restricted_update_associations
75
+ []
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,169 @@
1
+ module Role
2
+ # Base class that all roles inherit, stores role options in class instance variables
3
+ # and creates a hash of attributes and associations from the options defined in permitted_for methods
4
+ #
5
+ # @param authorize_with [Symbol, String] class instance attribute which stores the method that is used to
6
+ # authorize users
7
+ # @param disable_merge [TrueClass, FalseClass] unused as of yet
8
+ # @param policy [Object] instance variable used to store a reference to the policy which instantiated the class
9
+ # @param permission_options [Hash] unrefined hash of options to be refined by the permitted method
10
+ class Base
11
+
12
+ # Class instance variable accessors
13
+ class << self
14
+ attr_accessor :authorize_with, :disable_merge
15
+ end
16
+
17
+ @authorize_with = :default_authorization
18
+ @disable_merge = nil
19
+
20
+ attr_reader :policy
21
+
22
+ def initialize(policy, permission_options)
23
+ @policy = policy
24
+ @permission_options = permission_options
25
+ freeze
26
+ end
27
+
28
+ # Helper instance method to retrieve the class instance variable @authorize_with
29
+ def authorize_with
30
+ return self.class.authorize_with
31
+ end
32
+
33
+ # Send the method to the policy to check if user falls into this role
34
+ def test_condition
35
+ @policy.send(authorize_with)
36
+ end
37
+
38
+ # Returns a refined hash of attributes and associations this user has access to
39
+ def permitted
40
+ if not @permission_options
41
+ permitted = {attributes: {},
42
+ associations: {}}
43
+
44
+ elsif @permission_options.is_a? Symbol
45
+ permitted = {attributes: handle_default_options(@permission_options, 'attributes'),
46
+ associations: handle_default_options(@permission_options, 'associations')}
47
+ else
48
+ permitted = {attributes: init_options(@permission_options[:attributes], 'attributes'),
49
+ associations: init_options(@permission_options[:associations], 'associations')}
50
+ end
51
+
52
+ return permitted
53
+ end
54
+
55
+ private
56
+
57
+ # Build hash of options when options are explicitly declared as a Hash
58
+ #
59
+ # @param options [Hash] unrefined hash containing either attributes or associations
60
+ # @param type [String] the type of option to be built, can be 'attributes' or 'associations'
61
+ def init_options(options, type)
62
+ unless options.present?
63
+ return {}
64
+ end
65
+
66
+ if options.is_a? Symbol
67
+ return handle_default_options(options, type)
68
+ end
69
+
70
+ raise ArgumentError, "Permitted #{type}, if declared, must be declared as a Hash or Symbol, expected something along the lines of
71
+ {show: [:id, :name], create: [:name], update: :all} or :all, got #{options}" unless options.is_a? Hash
72
+
73
+ parsed_options = {}
74
+ options.each do |key, value|
75
+ raise ArgumentError, "Expected Symbol or Array, for #{key} attribute, got #{value} of kind #{value.class}" unless _permitted_value_types value
76
+
77
+ if value.is_a? Symbol and value == :all
78
+ parsed_options[key] = send("get_all_#{type}")
79
+ next
80
+ end
81
+
82
+ if value.is_a? Array
83
+ case value.first
84
+ when :all_minus
85
+ parsed_options[key] = send("get_all_#{type}") - (value - [value.first])
86
+ else
87
+ parsed_options[key] = value
88
+ end
89
+ next
90
+ end
91
+ end
92
+
93
+ return parsed_options
94
+ end
95
+
96
+ # Build hash of options when options are implicitly declared as a Symbol, ex: :show_all
97
+ #
98
+ # @param option [Symbol] unrefined hash containing either attributes or associations
99
+ # @param type [String] the type of option to be built, can be 'attributes' or 'associations'
100
+ def handle_default_options(option, type)
101
+ raise ArgumentError, "Permitted options for implicit permission declaration are #{_allowed_access_options},
102
+ got #{option} instead" unless _allowed_access_options.include? option
103
+ parsed_options = {}
104
+ case option
105
+ when :show_all
106
+ parsed_options[:show] = send("get_all_#{type}")
107
+ else
108
+ of_type = option.to_s.gsub('_all', '').to_sym
109
+ parsed_options[:show] = send("get_all_#{type}")
110
+ parsed_options[of_type] = send("get_all_#{type}")
111
+ end
112
+
113
+ return remove_restricted(parsed_options, type)
114
+ end
115
+
116
+ # Remove restricted attributes declared in the @policy restricted_#{key}_#{type} methods,
117
+ # ex: restricted_show_attributes
118
+ #
119
+ # @param obj [Hash] refined hash containing either attributes or associations
120
+ # @param type [String] the type of option to be built, can be 'attributes' or 'associations'
121
+ def remove_restricted(obj, type)
122
+ permitted_obj_values = {}
123
+
124
+ obj.each do |key, value|
125
+ restricted = @policy.send("restricted_#{key}_#{type}")
126
+ permitted_obj_values[key] = restricted.present? ? value - restricted : value
127
+ end
128
+
129
+ return permitted_obj_values
130
+ end
131
+
132
+ # Returns all attributes of a record or scope defined in the @policy
133
+ def get_all_attributes
134
+ begin
135
+ @policy.record.class.column_names.map(&:to_sym)
136
+ rescue NoMethodError
137
+ begin
138
+ @policy.scope.column_names.map(&:to_sym)
139
+ rescue NoMethodError
140
+ raise NoMethodError, "#{@policy} does not have a record or scope defined(or scope is not an ActiveRecord::Association), this is a problem."
141
+ end
142
+ end
143
+ end
144
+
145
+ # Returns all associations of a record or scope defined in the @policy
146
+ def get_all_associations
147
+ begin
148
+ @policy.record.class.reflect_on_all_associations.map(&:name)
149
+ rescue NoMethodError
150
+ begin
151
+ @policy.scope.reflect_on_all_associations.map(&:name)
152
+ rescue NoMethodError
153
+ raise NoMethodError, "#{@policy} does not have a record or scope defined(or scope is not an ActiveRecord::Association), this is a problem."
154
+ end
155
+ end
156
+ end
157
+
158
+ # allowed options for implicit declaration
159
+ def _allowed_access_options
160
+ [:show_all, :save_all, :create_all, :update_all]
161
+ end
162
+
163
+ # allowed options for explicit declaration
164
+ def _permitted_value_types(value)
165
+ value.is_a? Symbol or value.is_a? Array
166
+ end
167
+
168
+ end
169
+ end
@@ -0,0 +1,99 @@
1
+ require_relative 'role/base'
2
+
3
+ # Module which handles all class-level methods-and-instance variables. Add the ability for a class to define roles
4
+ # as dynamically generated classes and permitted options for those roles as class instance variables on the @policy.
5
+ #
6
+ # @param permission_hash [Hash] hash containing the unrefined attributes and association options
7
+ # @param scope_hash [Hash] unused as of yet
8
+ module Role
9
+ attr_accessor :permissions_hash, :scope_hash
10
+ @permissions_hash = {}
11
+ @scope_hash = {}
12
+
13
+ # Method to define a role with the opts used for those roles, checks if all is kosher and calls the the method
14
+ # to create the role
15
+ #
16
+ # @param role [Symbol, String] the role name
17
+ # @param opts [Hash] options for the role
18
+ def role(role, opts)
19
+ options = opts.slice(*_role_default_keys)
20
+
21
+ raise ArgumentError, 'You need to supply :authorize_with' unless options.slice(*_required_attributes).present?
22
+
23
+ unless role.is_a? Symbol or role.is_a? String
24
+ raise ArgumentError, "Expected Symbol or String for role, got #{role.class}"
25
+ end
26
+
27
+ create_role(role, self, options)
28
+ end
29
+
30
+ # Dynamically generates a class with the options and sets the constant on the @policy
31
+ #
32
+ # @param role [Symbol, String] the name of the role
33
+ # @param policy [Object] the reference to the policy to set the constant on, should be passed a 'self' reference
34
+ # @param opts [Hash] options for the role
35
+ def create_role(role, policy, opts)
36
+ begin
37
+ policy.const_set role.to_s.classify, Class.new(Role::Base) {
38
+ @authorize_with = "#{opts[:authorize_with]}?"
39
+ @disable_merge = opts[:disable_merge]
40
+ }
41
+ rescue NameError => e
42
+ raise ArgumentError, "Something went wrong, possible NameError with #{policy} or #{role} => #{e.message}"
43
+ end
44
+ end
45
+
46
+ # Saves the unrefined options into the @permission_hash class instance variable
47
+ #
48
+ # @param role [Symbol, String] the name of the role to which the opts are associated
49
+ # @param opts [Hash] the hash of options
50
+ def permitted_for(role, opts)
51
+ options = opts.slice(*_permitted_for_keys)
52
+
53
+ @permissions_hash = {} if @permissions_hash.nil?
54
+ @permissions_hash[role] = options
55
+ end
56
+
57
+ # Helper method to declare attributes directly
58
+ #
59
+ # @param role [Symbol, String] the name of the role to which the opts are associated
60
+ # @param attr [Hash] the hash of attributes
61
+ def permitted_attr_for(role, attr)
62
+ options = attr.slice(*_permitted_opt_for_keys)
63
+
64
+ @permissions_hash = {} if @permissions_hash.nil?
65
+ @permissions_hash[role] = {:attributes => options}
66
+ end
67
+
68
+ # Helper method to declare associations directly
69
+ #
70
+ # @param role [Symbol, String] the name of the role to which the opts are associated
71
+ # @param assoc [Hash] the hash of associations
72
+ def permitted_assoc_for(role, assoc)
73
+ options = assoc.slice(*_permitted_opt_for_keys)
74
+
75
+ @permissions_hash = {} if @permissions_hash.nil?
76
+ @permissions_hash[role] = {:associations => options}
77
+ end
78
+
79
+ # default options for role declaration
80
+ private def _role_default_keys
81
+ [:authorize_with, :disable_merge]
82
+ end
83
+
84
+ # required options for role declaration
85
+ private def _required_attributes
86
+ [:authorize_with]
87
+ end
88
+
89
+ # permitted options for permitted_for declaration
90
+ private def _permitted_for_keys
91
+ [:attributes, :associations]
92
+ end
93
+
94
+ # permitted options for permitted_assoc_for and permitted_attr_for declaration
95
+ private def _permitted_opt_for_keys
96
+ [:show, :create, :update, :save]
97
+ end
98
+
99
+ end
@@ -0,0 +1,34 @@
1
+ module PunditOverwrite
2
+
3
+ # Overwrite for Pundit's default authorization, to be able to use PunditRoles. Does not conflict with existing
4
+ # Pundit implementations
5
+ #
6
+ # @param record [Object] the object we're checking permissions of
7
+ # @param query [Symbol, String] the predicate method to check on the policy (e.g. `:show?`).
8
+ # If omitted then this defaults to the Rails controller action name.
9
+ # @raise [NotAuthorizedError] if the given query method returned false
10
+ # @return [Object] Always returns the passed object record
11
+ def authorize(record, query = nil)
12
+ query ||= params[:action].to_s + '?'
13
+
14
+ @_pundit_policy_authorized = true
15
+
16
+ policy = policy(record)
17
+
18
+ permitted_records = policy.resolve_query(query)
19
+
20
+ unless permitted_records
21
+ raise Pundit::NotAuthorizedError, query: query, record: record, policy: policy
22
+ end
23
+
24
+ if permitted_records.is_a? TrueClass
25
+ return record
26
+ end
27
+
28
+ return permitted_records
29
+ end
30
+ end
31
+
32
+ module Pundit
33
+ prepend PunditOverwrite
34
+ end
@@ -0,0 +1,3 @@
1
+ module PunditRoles
2
+ VERSION = "0.1.2"
3
+ end
@@ -0,0 +1,13 @@
1
+ require 'active_support/core_ext/hash/slice'
2
+ require 'active_support/core_ext/array/extract_options'
3
+ require 'active_support/core_ext/string/inflections'
4
+ require 'active_support/core_ext/object/blank'
5
+
6
+ require 'pundit_roles/version'
7
+ require 'pundit_roles/pundit'
8
+ require 'pundit_roles/policy/base'
9
+ require 'pundit'
10
+
11
+ module PunditRoles
12
+ include Pundit
13
+ end
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'pundit_roles/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "pundit_roles"
8
+ spec.version = PunditRoles::VERSION
9
+ spec.authors = ["Daniel Balogh"]
10
+ spec.email = ["danielferencbalogh@gmail.com"]
11
+
12
+ spec.summary = %q{Extends Pundit with roles, which allow attribute and association level authorizations}
13
+ spec.description = %q{Extends Pundit with roles}
14
+ spec.homepage = "https://github.com/StairwayB/pundit_roles"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
+ f.match(%r{^(test|spec|features)/})
19
+ end
20
+ spec.bindir = "exe"
21
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
+ spec.require_paths = ["lib"]
23
+
24
+ spec.required_ruby_version = '>= 2.3.1'
25
+ spec.add_dependency "activesupport", ">= 3.0.0"
26
+ end
metadata ADDED
@@ -0,0 +1,74 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pundit_roles
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.2
5
+ platform: ruby
6
+ authors:
7
+ - Daniel Balogh
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-10-24 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 3.0.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 3.0.0
27
+ description: Extends Pundit with roles
28
+ email:
29
+ - danielferencbalogh@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - ".gitignore"
35
+ - ".travis.yml"
36
+ - Gemfile
37
+ - LICENSE.txt
38
+ - README.md
39
+ - Rakefile
40
+ - bin/console
41
+ - bin/setup
42
+ - lib/pundit_roles.rb
43
+ - lib/pundit_roles/policy/base.rb
44
+ - lib/pundit_roles/policy/policy_defaults/defaults.rb
45
+ - lib/pundit_roles/policy/role.rb
46
+ - lib/pundit_roles/policy/role/base.rb
47
+ - lib/pundit_roles/pundit.rb
48
+ - lib/pundit_roles/version.rb
49
+ - pundit_roles.gemspec
50
+ homepage: https://github.com/StairwayB/pundit_roles
51
+ licenses:
52
+ - MIT
53
+ metadata: {}
54
+ post_install_message:
55
+ rdoc_options: []
56
+ require_paths:
57
+ - lib
58
+ required_ruby_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: 2.3.1
63
+ required_rubygems_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ requirements: []
69
+ rubyforge_project:
70
+ rubygems_version: 2.6.11
71
+ signing_key:
72
+ specification_version: 4
73
+ summary: Extends Pundit with roles, which allow attribute and association level authorizations
74
+ test_files: []