pundit_roles 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
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: []