pundit_roles 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f9a596e75302e60e5ac89fbd28e1d411f1e80e85
4
- data.tar.gz: 70ef89deb9dbc45c8c3c3a3386f858662a1480ab
3
+ metadata.gz: 37198b58ba83c3e0f973f544dcdc7d202159820d
4
+ data.tar.gz: febd757aadd6d847ce4fa5997ddc3d7ba8248cf4
5
5
  SHA512:
6
- metadata.gz: 8f1e0eea9562fa2add0c1c9faebba5cd4a8ccb1441dde646c58eabd76193691da924eb3775cd251ce6658dfe90f5a1eb879f767944032aa5f4b75d3f4cebcf83
7
- data.tar.gz: 2dcef9da90cad672759c3d315b1259b1a999bd0445fc5a4139d272aef0b1e78ce9fc445fe9ab9260329c773af0a5ecc0924c14af11768dd92000736df00b8558
6
+ metadata.gz: fc8990e3c886ff9f425359896c99518813db8d7dc93c71503f25927cc6eb7d056a3c0c408943a726ebffef28949770fee765288ff29537d6405d3e20325b48db
7
+ data.tar.gz: e5b8efa011b23dfebd9f9bf324b4907e808d5a28626c0f04da8d3efffc34fd3d36d9b0e00372be7fc0d6f31098ff675ee1473df852eec29e2d2cff7645151d17
data/.coveralls.yml ADDED
@@ -0,0 +1 @@
1
+ service_name: travis-ci
data/.travis.yml CHANGED
@@ -1,5 +1,8 @@
1
- sudo: false
2
1
  language: ruby
2
+ cache: bundler
3
+
3
4
  rvm:
4
5
  - 2.4.0
5
6
  before_install: gem install bundler -v 1.14.6
7
+
8
+ script: 'bundle exec rake'
data/CHANGELOG.md ADDED
@@ -0,0 +1,10 @@
1
+ # PunditRoles
2
+
3
+ ## 0.2.0 (2017-10-30)
4
+
5
+ - Roles and permitted options are no longer separately declared with `role` and
6
+ `permitted_for` methods. Declaration of options has been consolidated into the
7
+ `role` method.
8
+ - Roles can no longer be inherited from the superclass.
9
+ - Test conditions for the roles are now guessed from the name of the role,
10
+ instead of being declared explicitly with the `authorize_with` option.
data/Gemfile CHANGED
@@ -8,10 +8,9 @@ gem 'activemodel'
8
8
  gem 'pundit', '~> 1.1.0'
9
9
 
10
10
  group :development, :test do
11
- gem "actionpack"
12
- gem "activemodel"
13
11
  gem "bundler"
14
12
  gem "pry"
15
13
  gem "rake"
16
14
  gem "rspec"
15
+ gem 'coveralls', require: false
17
16
  end
data/README.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # PunditRoles
2
2
 
3
+ [![Gem Version](https://badge.fury.io/rb/pundit_roles.svg)](https://badge.fury.io/rb/pundit_roles.svg)
4
+ [![Build Status](https://travis-ci.org/StairwayB/pundit_roles.svg?branch=master)](https://travis-ci.org/StairwayB/pundit_roles)
5
+ [![Coverage Status](https://coveralls.io/repos/github/StairwayB/pundit_roles/badge.svg?branch=master)](https://coveralls.io/github/StairwayB/pundit_roles?branch=master)
6
+ [![Maintainability](https://api.codeclimate.com/v1/badges/030ffce3612160c8e7f0/maintainability)](https://codeclimate.com/github/StairwayB/pundit_roles/maintainability)
7
+
3
8
  PunditRoles is a helper gem which works on top of [Pundit](https://github.com/elabs/pundit)
4
9
  (if you are not familiar with Pundit, it is recommended you read it's documentation before continuing).
5
10
  It allows you to extend Pundit's authorization system to include attributes and associations.
@@ -10,10 +15,9 @@ You may use Pundit's features as well as the features from this gem interchangea
10
15
  Please note that this gem is not affiliated with Pundit or it's creators, but it very much
11
16
  appreciates the work that they did with their great authorization system.
12
17
 
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.
18
+ * **Important**: This gem is **not** yet considered production ready.
19
+
20
+ * [**_Changelog_**](https://github.com/StairwayB/pundit_roles/blob/master/CHANGELOG.md)
17
21
 
18
22
  ## Installation
19
23
 
@@ -34,43 +38,40 @@ end
34
38
  And inherit your ApplicationPolicy from Policy::Base
35
39
  ```ruby
36
40
  class ApplicationPolicy < Policy::Base
37
- # def show?
38
- # [...]
39
- # end
40
41
  end
41
42
  ```
42
43
 
43
44
  ## Roles
44
45
 
45
46
  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
+ and provided with a conditional method that determines whether the `@user`(the `current_user` in the context of a Policy)
47
48
  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
+ _**attributes**_ and _**associations**_(from here on collectively referred to as _**options**_)
49
50
  defined for it. A basic example for a UserPolicy would be:
50
51
  ```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
- }
52
+ role :regular_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,
61
+ attributes: {
62
+ show: %i(email phone_number confirmed_at updated_at sign_in_count),
63
+ update: %i(username email password password_confirmation current_password name avatar)
64
+ },
65
+ associations: {
66
+ show: %i(settings),
67
+ save: %i(settings)
68
+ }
70
69
  ```
71
70
 
72
- This assumes that there are two methods defined in the UserPolicy called `logged_in_user?` and
73
- `correct_user?`. More on that later.
71
+ This assumes that there are two methods defined in the UserPolicy called `regular_user?` and
72
+ `correct_user?`.
73
+
74
+ * Please note, that there was a breaking change since `0.1.2`. View the changelog for additional details.
74
75
 
75
76
  And then in you query method, you simply say:
76
77
  ```ruby
@@ -104,124 +105,124 @@ end
104
105
  The `authorize` method will return a hash of permitted attributes and associations for the corresponding action that the
105
106
  user has access to. What you do with that is your business. Accessors for each segment look like this:
106
107
  ```ruby
107
- permitted[:attributes][:show]
108
- permitted[:attributes][:create]
108
+ permitted[:attributes][:show] # ex. returns => [:username, :name, :avatar, :is_confirmed, :created_at]
109
+ permitted[:attributes][:create] # ex. returns => [:username, :email, :password, :password_confirmation]
109
110
 
110
111
  permitted[:associations][:show]
111
112
  permitted[:associations][:update]
112
113
  ```
113
114
 
115
+ It hash also contains the roles that the user has fulfilled:
116
+ ```ruby
117
+ permitted[:roles] # ex. returns => [:regular_user, :correct_user]
118
+ ```
119
+
114
120
  If the user does not fall into any roles permitted by a query, the `authorize` method will raise `Pundit::NotAuthorizedError`
115
121
 
116
122
  ### Defining roles
117
123
 
118
124
  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.
125
+ options for the role as it's second(multiple `roles` can be defined at once, if you wish to do that,
126
+ but all of these will have identical options, so what's the point, really). Valid options for the role are:
127
+ `:attributes, :associations, :scope, :uses_db, :extend`.
122
128
 
123
- Currently there are no more options, but some, like database permissions, are planned for future updates.
129
+ Currently only attributes and associations are supported, scopes and db permissions will be coming Soon&trade;.
124
130
 
125
131
  ```ruby
126
- role :user, authorize_with: :logged_in_user
132
+ role :regular_user,
133
+ attributes: {show: [:name]},
134
+ associations: {show: [:posts]}
127
135
 
128
- def logged_in_user?
136
+ def regular_user?
129
137
  @user.present?
130
138
  end
131
139
  ```
132
140
 
141
+ One thing to watch out for is that roles are not inherited, because each is unique to the model in question.
142
+ But since the name of the role is just the conditional method for the role,
143
+ without the '?' question mark, it is encouraged to inherit from an `ApplicationPolicy`,
144
+ and define common `role` conditional methods there.
145
+
133
146
  ### Users with multiple roles
134
147
 
135
148
  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.
149
+ defined than `regular_user`. That is because PunditRoles does not treat roles as exclusionary.
137
150
  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
151
+ In the previous example, a `correct_user`, meaning a `regular_user` trying to access it's own model, is naturally
152
+ also a `regular_user`, so it will have access to all options a `regular_user` has access to plus the
140
153
  options that a `correct_user` has access to.
141
154
 
142
155
  Take this example, to better illustrate what is happening:
143
156
 
144
157
  ```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
- }
158
+ role :regular_user,
159
+ attributes: {
160
+ show: %i(username name avatar)
161
+ }
162
+
163
+ role :correct_user,
164
+ attributes: {
165
+ show: %i(email phone_number)
166
+ }
167
+
168
+ role :admin_user,
169
+ attributes: {
170
+ show: %i(email is_admin)
171
+ }
162
172
  ```
163
173
 
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:
174
+ Here, a user which fulfills the `admin_user` condition trying to access it's own model, would receive the
175
+ options of all three roles, without any duplicates, meaning the `permitted[:attributes][:show]` would look like:
166
176
  ```ruby
167
177
  [:username, :name, :avatar, :email, :phone_number, :is_admin]
168
178
  ```
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
179
 
173
180
  If the user is an `admin`, but is not a `correct_user`, it will not receive the `phone_number` attribute,
174
181
  because that is unique to `correct_user` and vice versa.
175
182
 
176
183
  At present, there is no way to prevent merging of roles. Such a feature may be coming in a future update.
177
184
 
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).
185
+ ### The :guest role
184
186
 
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
187
+ PunditRoles comes with a default `:guest` role, which simply checks if
189
188
  the user is nil. If you wish to permit guest users for a particular action, simply define the
190
189
  options for it and allow it in your query method.
191
190
 
192
191
  ```ruby
193
192
  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: {}
193
+
194
+ role :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
200
 
201
201
  def show?
202
- allow :guest
202
+ allow :guest, :some, :other, :roles
203
203
  end
204
204
 
205
205
  def create?
206
- allow :guest
206
+ allow :guest, :admin_user
207
207
  end
208
208
 
209
209
  end
210
210
  ```
211
211
 
212
- * **Important!** The `:guest` role is exclusionary by default, meaning it cannot be merged
212
+ **Important**
213
+
214
+ * The `:guest` role is exclusionary by default, meaning it cannot be merged
213
215
  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.
216
+ attributes if `:guest` is allowed, or raise `Pundit::NotAuthorizedError` if not.
217
+
218
+ * Do **not** use a custom role for `nil` or `undefined` `@user`, use `:guest`.
219
+ If you do, it will most likely lead to unwanted exceptions.
217
220
 
218
221
  ### Explicit declaration of options
219
222
 
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.
223
+ Options are declared with the `attributes` and `associations` options of the role method.
222
224
 
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
+ Valid options for both `:attributes` and `:associations` are `:show`,`:create`,`:update` and `:save` or the implicit options.
225
226
 
226
227
  ### Implicit declaration of options
227
228
 
@@ -239,46 +240,42 @@ attribute that is added to a model later down the line.
239
240
  Will be able to view all non-restricted options.
240
241
 
241
242
  ```ruby
242
- role :admin, authorize_with: :admin
243
- permitted_for :admin,
244
- attributes: :show_all,
245
- associations: :show_all
243
+ role :admin,
244
+ attributes: :show_all,
245
+ associations: :show_all
246
246
  ```
247
247
  * **create_all, update_all, save_all**
248
248
 
249
249
  Will be able to create, update or save all non-restricted attributes. These options also
250
250
  imply that the role will be able to `show_all` options.
251
251
  ```ruby
252
- role :admin, authorize_with: :admin
253
- permitted_for :admin,
254
- attributes: :save_all,
255
- associations: :update_all
252
+ role :admin,
253
+ attributes: :save_all,
254
+ associations: :update_all
256
255
  ```
257
256
 
258
257
  * **all**
259
258
 
260
259
  Declare on a per-action basis whether the role has access to all options.
261
260
  ```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
- }
261
+ role :admin,
262
+ attributes: {
263
+ show: :all,
264
+ save: %i(name username email)
265
+ },
266
+ associations: {
267
+ show: :all
268
+ }
271
269
  ```
272
270
 
273
271
  * **all_minus**
274
272
 
275
273
  Can be used to allow all attributes, except those declared.
276
274
  ```ruby
277
- role :admin, authorize_with: :admin
278
- permitted_for :admin,
279
- attributes: {
280
- show: [:all_minus, :password_digest]
281
- }
275
+ role :admin,
276
+ attributes: {
277
+ show: [:all_minus, :password_digest]
278
+ }
282
279
  ```
283
280
  The `:admin` role will now be able to view all attributes, except `password_digest`.
284
281
 
@@ -287,36 +284,24 @@ attribute that is added to a model later down the line.
287
284
  PunditRoles allows you to define restricted options which will be removed when declaring
288
285
  implicitly. By default, only the `:id`, `:created_at`, `:updated_at` attributes are restricted
289
286
  for `create`,`update` and `save` actions. You may overwrite this behaviour on a per-policy basis:
290
- ```ruby
291
- private
292
287
 
293
- def restricted_show_attributes
294
- [:attr_one, :attr_two]
295
- end
288
+ ```ruby
289
+ RESTRICTED_SHOW_ATTRIBUTES = [:attr_one, :attr_two]
296
290
  ```
297
- Or if you want to add to it, instead of overwriting, use `super`:
291
+ Or if you want to add to it, instead of overwriting(here,
292
+ the second `RESTRICTED_SHOW_ATTRIBUTES` resolves to the one declared on the parent):
298
293
  ```ruby
299
- private
300
-
301
- def restricted_create_attributes
302
- super + [:attr_one, :attr_two]
303
- end
294
+ RESTRICTED_SHOW_ATTRIBUTES = RESTRICTED_SHOW_ATTRIBUTES + [:attr_one, :attr_two]
304
295
  ```
305
296
 
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`.
297
+ There are 8 `RESTRICTED_#{action}_#{option_type}` constants in total, where `option_type` refers
298
+ to either `ATTRIBUTES` or `ASSOCIATIONS` and `action` refers to `SHOW`, `CREATE`, `UPDATE` or `SAVE`.
308
299
 
309
300
  ## Planned updates
310
301
 
311
302
  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.
303
+ generators, and possibly rspec helpers.
318
304
 
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
305
 
321
306
  ## Contributing
322
307
 
data/Rakefile CHANGED
@@ -1,2 +1,9 @@
1
- require "bundler/gem_tasks"
2
- task :default => :spec
1
+ require 'rspec/core/rake_task'
2
+ require 'bundler/gem_tasks'
3
+
4
+ # Run with `rake spec`
5
+ RSpec::Core::RakeTask.new(:spec) do |task|
6
+ task.rspec_opts = %w(--color)
7
+ end
8
+
9
+ task :default => :spec
@@ -1,35 +1,26 @@
1
1
  require_relative 'role'
2
- require_relative 'policy_defaults/defaults'
3
-
2
+ require_relative 'policy_defaults'
4
3
 
4
+ # Namespace for aesthetic reasons
5
5
  module Policy
6
6
 
7
7
  # Base policy class to be extended by all other policies, authorizes users based on roles they fall into,
8
8
  # return a uniquely merged hash of permitted attributes and associations of each role the @user has.
9
9
  #
10
- # @param user [Object] the user that initiated the action
11
- # @param record [Object] the object we're checking permissions of
10
+ # @attr_reader user [Object] the user that initiated the action
11
+ # @attr_reader record [Object] the object we're checking permissions of
12
12
  class Base
13
13
  extend Role
14
+ include PolicyDefaults
14
15
 
15
- include PolicyDefaults::Defaults
16
-
17
- role :guest, authorize_with: :user_guest
18
-
19
- attr_reader :user, :record
20
-
21
- def initialize(user, record)
16
+ attr_reader :user, :resource
17
+ def initialize(user, resource)
22
18
  @user = user
23
- @record = record
24
- end
25
-
26
- # Is here
27
- def scope
28
- Pundit.authorize_scope!(user, record.class, fields)
19
+ @resource = resource
29
20
  end
30
21
 
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.
22
+ # Retrieves the permitted roles for the current query, checks if user is one or more of these roles
23
+ # and return a hash of attributes and associations that the user has access to.
33
24
  #
34
25
  # @param query [Symbol, String] the predicate method to check on the policy (e.g. `:show?`)
35
26
  def resolve_query(query)
@@ -40,16 +31,11 @@ module Policy
40
31
 
41
32
  permissions_hash = self.class.permissions_hash
42
33
 
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
34
+ # Always checks if user is a guest, and return the appropriate permission if true
35
+ # the guest role cannot be merged with other roles
36
+ if guest?
37
+ return handle_guest_user(permitted_roles, permissions_hash)
51
38
  end
52
-
53
39
  current_roles = determine_current_roles(permitted_roles, permissions_hash)
54
40
 
55
41
  unless current_roles.present?
@@ -57,8 +43,7 @@ module Policy
57
43
  end
58
44
 
59
45
  if current_roles.length == 1
60
- current_roles.values[0][:roles] = current_roles.keys[0]
61
- return current_roles.values[0]
46
+ return current_roles.values[0].merge({roles: [current_roles.keys[0]]})
62
47
  end
63
48
 
64
49
  return unique_merge(current_roles)
@@ -66,6 +51,17 @@ module Policy
66
51
 
67
52
  private
68
53
 
54
+ # Return the default :guest role if guest is present in @permitted_roles. Return false otherwise
55
+ #
56
+ # @param permitted_roles [Hash] roles returned by the query
57
+ # @param permissions_hash [Hash] unrefined hash of options defined by all permitted_for methods
58
+ def handle_guest_user(permitted_roles, permissions_hash)
59
+ if permitted_roles.include? :guest
60
+ return permissions_hash[:guest].merge({roles: [:guest]})
61
+ end
62
+ return false
63
+ end
64
+
69
65
  # Build a hash of the roles that the user fulfills and the roles' attributes and associations,
70
66
  # based on the test_condition of the role.
71
67
  #
@@ -75,20 +71,16 @@ module Policy
75
71
  current_roles = {}
76
72
 
77
73
  permitted_roles.each do |permitted_role|
78
- if permitted_role == :guest
74
+ if permitted_role == :guest or permitted_role == :guest_user
79
75
  next
80
76
  end
81
77
 
82
78
  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
79
+ if send("#{permitted_role}?")
80
+ current_roles[permitted_role] = permissions_hash[permitted_role]
87
81
  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} "
82
+ rescue NoMethodError => e
83
+ raise NoMethodError, "Undefined test condition, it must be defined as 'role?', where, role is :#{permitted_role}, => #{e.message}"
92
84
  end
93
85
  end
94
86
 
@@ -102,14 +94,8 @@ module Policy
102
94
  merged_hash = {attributes: {}, associations: {}, roles: []}
103
95
 
104
96
  roles.each do |role, option|
105
- unless option.present?
106
- next
107
- end
108
97
  merged_hash[:roles] << role
109
98
  option.each do |type, actions|
110
- unless actions.present?
111
- next
112
- end
113
99
  actions.each do |key, value|
114
100
  unless merged_hash[type][key]
115
101
  merged_hash[type][key] = []
@@ -129,6 +115,11 @@ module Policy
129
115
  return roles
130
116
  end
131
117
 
118
+ # Default :guest role
119
+ def guest?
120
+ @user.nil?
121
+ end
122
+
132
123
  # Scope class from Pundit, to be used for limiting scopes. Unchanged from Pundit,
133
124
  # possible implementation forthcoming in a future update
134
125
  class Scope
@@ -0,0 +1,52 @@
1
+ # Defaults for Policy::Base
2
+ module PolicyDefaults
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
+ # restricted attributes for show
29
+ RESTRICTED_SHOW_ATTRIBUTES = []
30
+
31
+ # restricted attributes for save
32
+ RESTRICTED_SAVE_ATTRIBUTES = [:id, :created_at, :updated_at]
33
+
34
+ # restricted attributes for create
35
+ RESTRICTED_CREATE_ATTRIBUTES = [:id, :created_at, :updated_at]
36
+
37
+ # restricted attributes for update
38
+ RESTRICTED_UPDATE_ATTRIBUTES = [:id, :created_at, :updated_at]
39
+
40
+
41
+ # restricted associations for show
42
+ RESTRICTED_SHOW_ASSOCIATIONS = []
43
+
44
+ # restricted associations for save
45
+ RESTRICTED_SAVE_ASSOCIATIONS = []
46
+
47
+ # restricted associations for create
48
+ RESTRICTED_CREATE_ASSOCIATIONS = []
49
+
50
+ # restricted associations for update
51
+ RESTRICTED_UPDATE_ASSOCIATIONS = []
52
+ end
@@ -1,59 +1,50 @@
1
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
2
+ # Helper class, which handles building of a role's #permissions_hash.
4
3
  #
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
4
+ # @param policy [Class] the class used to store a reference to the policy which instantiated this
5
+ # @param attr_opts [Hash, :Symbol] the requested options for attributes to be refined by the permitted method
6
+ # @param assoc_opts [Hash, :Symbol] the requested options for associations to be refined by the permitted method
7
+ # @param scope [String] the scope for the role in String format
16
8
 
17
- @authorize_with = :default_authorization
18
- @disable_merge = nil
9
+ class OptionBuilder
19
10
 
20
11
  attr_reader :policy
21
12
 
22
- def initialize(policy, permission_options)
13
+ def initialize(policy, attr_opts, assoc_opts, scope)
23
14
  @policy = policy
24
- @permission_options = permission_options
15
+ @attr_opts = attr_opts
16
+ @assoc_opts = assoc_opts
17
+ @scope = scope
25
18
  freeze
26
19
  end
27
20
 
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)
21
+ # Returns a refined hash of attributes and associations the current_user has access to
22
+ def permitted
23
+ return {
24
+ attributes: permitted_options(@attr_opts, 'attributes'),
25
+ associations: permitted_options(@assoc_opts, 'associations')
26
+ }
36
27
  end
37
28
 
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: {}}
29
+ private
43
30
 
44
- elsif @permission_options.is_a? Symbol
45
- permitted = {attributes: handle_default_options(@permission_options, 'attributes'),
46
- associations: handle_default_options(@permission_options, 'associations')}
31
+ # Determines the the kind of method used to declare the option
32
+ #
33
+ # @param *opts [Hash] the hash of attributes or associations for the role
34
+ # @param type [String] the kind of option, can be 'attributes' or 'associations'
35
+ def permitted_options(opts, type)
36
+ if not opts
37
+ permitted = {}
38
+
39
+ elsif opts.is_a? Symbol
40
+ permitted = handle_default_options(opts, type)
47
41
  else
48
- permitted = {attributes: init_options(@permission_options[:attributes], 'attributes'),
49
- associations: init_options(@permission_options[:associations], 'associations')}
42
+ permitted = init_options(opts, type)
50
43
  end
51
44
 
52
45
  return permitted
53
46
  end
54
47
 
55
- private
56
-
57
48
  # Build hash of options when options are explicitly declared as a Hash
58
49
  #
59
50
  # @param options [Hash] unrefined hash containing either attributes or associations
@@ -63,10 +54,6 @@ module Role
63
54
  return {}
64
55
  end
65
56
 
66
- if options.is_a? Symbol
67
- return handle_default_options(options, type)
68
- end
69
-
70
57
  raise ArgumentError, "Permitted #{type}, if declared, must be declared as a Hash or Symbol, expected something along the lines of
71
58
  {show: [:id, :name], create: [:name], update: :all} or :all, got #{options}" unless options.is_a? Hash
72
59
 
@@ -79,6 +66,7 @@ module Role
79
66
  next
80
67
  end
81
68
 
69
+ # todo: This needs some rethinking
82
70
  if value.is_a? Array
83
71
  case value.first
84
72
  when :all_minus
@@ -103,18 +91,17 @@ module Role
103
91
  parsed_options = {}
104
92
  case option
105
93
  when :show_all
106
- parsed_options[:show] = send("get_all_#{type}")
94
+ parsed_options[:show] = get_all(type)
107
95
  else
108
96
  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}")
97
+ parsed_options[:show] = get_all(type)
98
+ parsed_options[of_type] = get_all(type)
111
99
  end
112
100
 
113
101
  return remove_restricted(parsed_options, type)
114
102
  end
115
103
 
116
- # Remove restricted attributes declared in the @policy restricted_#{key}_#{type} methods,
117
- # ex: restricted_show_attributes
104
+ # Remove restricted attributes declared in the #policy RESTRICTED_#{key}_#{type} constants
118
105
  #
119
106
  # @param obj [Hash] refined hash containing either attributes or associations
120
107
  # @param type [String] the type of option to be built, can be 'attributes' or 'associations'
@@ -122,36 +109,28 @@ module Role
122
109
  permitted_obj_values = {}
123
110
 
124
111
  obj.each do |key, value|
125
- restricted = @policy.send("restricted_#{key}_#{type}")
112
+ restricted = "#{@policy}::RESTRICTED_#{key.upcase}_#{type.upcase}".constantize
126
113
  permitted_obj_values[key] = restricted.present? ? value - restricted : value
127
114
  end
128
115
 
129
116
  return permitted_obj_values
130
117
  end
131
118
 
132
- # Returns all attributes of a record or scope defined in the @policy
119
+ # Returns all attributes of a record or scope defined in the #policy
133
120
  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
121
+ @policy.to_s.gsub('Policy', '').constantize.column_names.map(&:to_sym)
143
122
  end
144
123
 
145
- # Returns all associations of a record or scope defined in the @policy
124
+ # Returns all associations of a record or scope defined in the #policy
146
125
  def get_all_associations
126
+ @policy.to_s.gsub('Policy', '').constantize.reflect_on_all_associations.map(&:name)
127
+ end
128
+
129
+ def get_all(type)
147
130
  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
131
+ send("get_all_#{type}")
132
+ rescue NameError => e
133
+ raise ArgumentError, "#{@policy} does not have a corresponding model: #{@policy.to_s.gsub('Policy', '')}, implicit declarations are not allowed => #{e.message}"
155
134
  end
156
135
  end
157
136
 
@@ -1,99 +1,49 @@
1
- require_relative 'role/base'
1
+ require_relative 'role/option_builder'
2
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
3
+ # Extended by Policy::Base. Defines the methods necessary for declaring roles.
8
4
  module Role
9
- attr_accessor :permissions_hash, :scope_hash
10
- @permissions_hash = {}
11
- @scope_hash = {}
12
5
 
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
6
+ attr_accessor :permissions_hash
29
7
 
30
- # Dynamically generates a class with the options and sets the constant on the @policy
8
+ # Builds a new role by saving it into the #permissions_hash class instance variable
9
+ # Valid options are :attributes, :associations, :scope, :uses_db, :extend
31
10
  #
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
11
+ # @param *opts [Array] the roles, and the options which define the roles
12
+ # @raise [ArgumentError] if the options are incorrectly defined, or no options are present
13
+ # @return [Hash] Returns the permissions hash or the record
14
+ def role(*opts)
15
+ user_opts = opts.extract_options!.dup
16
+ options = user_opts.slice(*_role_default_keys)
45
17
 
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)
18
+ raise ArgumentError, 'Please provide at least one role' unless opts.present?
52
19
 
53
20
  @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
21
 
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)
22
+ options.each do |key, value|
23
+ if value.present?
24
+ expected = _role_option_validations[key]
25
+ do_raise = true
26
+ expected.each do |type|
27
+ do_raise = false if value.is_a? type
28
+ end
29
+ raise ArgumentError, "Expected #{expected} for #{key}, got #{value.class}" if do_raise
30
+ end
31
+ end
74
32
 
75
- @permissions_hash = {} if @permissions_hash.nil?
76
- @permissions_hash[role] = {:associations => options}
33
+ opts.each do |role|
34
+ raise ArgumentError, "Expected Symbol for #{role}, got #{role.class}" unless role.is_a? Symbol
35
+ @permissions_hash[role] = OptionBuilder.new(self, options[:attributes], options[:associations], options[:scope]).permitted
36
+ end
77
37
  end
78
38
 
79
- # default options for role declaration
39
+ # @api private
80
40
  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]
41
+ [:attributes, :associations, :scope, :uses_db, :extend]
92
42
  end
93
43
 
94
- # permitted options for permitted_assoc_for and permitted_attr_for declaration
95
- private def _permitted_opt_for_keys
96
- [:show, :create, :update, :save]
44
+ # @api private
45
+ private def _role_option_validations
46
+ {attributes: [Hash, Symbol], associations: [Hash, Symbol], scope: [String], uses_db: [Symbol], extend: [Symbol]}
97
47
  end
98
48
 
99
- end
49
+ end
@@ -1,34 +1,36 @@
1
+ # Contains the overwritten #authorize method
1
2
  module PunditOverwrite
2
3
 
3
4
  # Overwrite for Pundit's default authorization, to be able to use PunditRoles. Does not conflict with existing
4
5
  # Pundit implementations
5
6
  #
6
- # @param record [Object] the object we're checking permissions of
7
+ # @param resource [Object] the object we're checking permissions of
7
8
  # @param query [Symbol, String] the predicate method to check on the policy (e.g. `:show?`).
8
9
  # If omitted then this defaults to the Rails controller action name.
9
10
  # @raise [NotAuthorizedError] if the given query method returned false
10
- # @return [Object] Always returns the passed object record
11
- def authorize(record, query = nil)
11
+ # @return [Object, Hash] Returns the permissions hash or the record
12
+ def authorize(resource, query = nil)
12
13
  query ||= params[:action].to_s + '?'
13
14
 
14
15
  @_pundit_policy_authorized = true
15
16
 
16
- policy = policy(record)
17
+ policy = policy(resource)
17
18
 
18
19
  permitted_records = policy.resolve_query(query)
19
20
 
20
21
  unless permitted_records
21
- raise Pundit::NotAuthorizedError, query: query, record: record, policy: policy
22
+ raise Pundit::NotAuthorizedError, query: query, record: resource, policy: policy
22
23
  end
23
24
 
24
25
  if permitted_records.is_a? TrueClass
25
- return record
26
+ return resource
26
27
  end
27
28
 
28
29
  return permitted_records
29
30
  end
30
31
  end
31
32
 
33
+ # Prepends the PunditOverwrite to Pundit, in order to overwrite the default Pundit #authorize method
32
34
  module Pundit
33
35
  prepend PunditOverwrite
34
36
  end
@@ -1,3 +1,3 @@
1
1
  module PunditRoles
2
- VERSION = "0.1.2"
2
+ VERSION = "0.2.0"
3
3
  end
data/lib/pundit_roles.rb CHANGED
@@ -8,6 +8,7 @@ require 'pundit_roles/pundit'
8
8
  require 'pundit_roles/policy/base'
9
9
  require 'pundit'
10
10
 
11
+ # Enhances Pundit with roles, allows definition of attribute and association level authorization
11
12
  module PunditRoles
12
13
  include Pundit
13
14
  end
data/pundit_roles.gemspec CHANGED
@@ -23,4 +23,5 @@ Gem::Specification.new do |spec|
23
23
 
24
24
  spec.required_ruby_version = '>= 2.3.1'
25
25
  spec.add_dependency "activesupport", ">= 3.0.0"
26
+ spec.add_dependency 'pundit', '>=1.1.0'
26
27
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pundit_roles
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Balogh
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-10-24 00:00:00.000000000 Z
11
+ date: 2017-10-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: 3.0.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: pundit
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 1.1.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 1.1.0
27
41
  description: Extends Pundit with roles
28
42
  email:
29
43
  - danielferencbalogh@gmail.com
@@ -31,8 +45,10 @@ executables: []
31
45
  extensions: []
32
46
  extra_rdoc_files: []
33
47
  files:
48
+ - ".coveralls.yml"
34
49
  - ".gitignore"
35
50
  - ".travis.yml"
51
+ - CHANGELOG.md
36
52
  - Gemfile
37
53
  - LICENSE.txt
38
54
  - README.md
@@ -41,9 +57,9 @@ files:
41
57
  - bin/setup
42
58
  - lib/pundit_roles.rb
43
59
  - lib/pundit_roles/policy/base.rb
44
- - lib/pundit_roles/policy/policy_defaults/defaults.rb
60
+ - lib/pundit_roles/policy/policy_defaults.rb
45
61
  - lib/pundit_roles/policy/role.rb
46
- - lib/pundit_roles/policy/role/base.rb
62
+ - lib/pundit_roles/policy/role/option_builder.rb
47
63
  - lib/pundit_roles/pundit.rb
48
64
  - lib/pundit_roles/version.rb
49
65
  - pundit_roles.gemspec
@@ -1,78 +0,0 @@
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