crewd_policies 0.2.0

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: a6801f11573ca176ae1286e76e831686a20293bc
4
+ data.tar.gz: 338f36c0cd2de727c70739e63ce29d45b27e6a43
5
+ SHA512:
6
+ metadata.gz: aaa121814498240b46bbe2733f0d8e7e2ca51bba3713d980c11530e612bca9fff79742775623793eb30f474b7c083d42461fca7aa0a3d5fde89fe569f26f8811
7
+ data.tar.gz: 572eedaa8be5c3185fa04534e4b3984dfbac68d87d209b999a4d9fa0ea93261cb982258e3d2e468c8dfbacfa8f4817bea57c8b164e5a669dbd430a26395b873b
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.iml
11
+ .idea
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.2.2
5
+ before_install: gem install bundler -v 1.12.4
@@ -0,0 +1,49 @@
1
+ # Contributor Code of Conduct
2
+
3
+ As contributors and maintainers of this project, and in the interest of
4
+ fostering an open and welcoming community, we pledge to respect all people who
5
+ contribute through reporting issues, posting feature requests, updating
6
+ documentation, submitting pull requests or patches, and other activities.
7
+
8
+ We are committed to making participation in this project a harassment-free
9
+ experience for everyone, regardless of level of experience, gender, gender
10
+ identity and expression, sexual orientation, disability, personal appearance,
11
+ body size, race, ethnicity, age, religion, or nationality.
12
+
13
+ Examples of unacceptable behavior by participants include:
14
+
15
+ * The use of sexualized language or imagery
16
+ * Personal attacks
17
+ * Trolling or insulting/derogatory comments
18
+ * Public or private harassment
19
+ * Publishing other's private information, such as physical or electronic
20
+ addresses, without explicit permission
21
+ * Other unethical or unprofessional conduct
22
+
23
+ Project maintainers have the right and responsibility to remove, edit, or
24
+ reject comments, commits, code, wiki edits, issues, and other contributions
25
+ that are not aligned to this Code of Conduct, or to ban temporarily or
26
+ permanently any contributor for other behaviors that they deem inappropriate,
27
+ threatening, offensive, or harmful.
28
+
29
+ By adopting this Code of Conduct, project maintainers commit themselves to
30
+ fairly and consistently applying these principles to every aspect of managing
31
+ this project. Project maintainers who do not follow or enforce the Code of
32
+ Conduct may be permanently removed from the project team.
33
+
34
+ This code of conduct applies both within project spaces and in public spaces
35
+ when an individual is representing the project or its community.
36
+
37
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
38
+ reported by contacting a project maintainer at gary@buzzware.com.au. All
39
+ complaints will be reviewed and investigated and will result in a response that
40
+ is deemed necessary and appropriate to the circumstances. Maintainers are
41
+ obligated to maintain confidentiality with regard to the reporter of an
42
+ incident.
43
+
44
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
45
+ version 1.3.0, available at
46
+ [http://contributor-covenant.org/version/1/3/0/][version]
47
+
48
+ [homepage]: http://contributor-covenant.org
49
+ [version]: http://contributor-covenant.org/version/1/3/0/
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in crewd_policies.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Gary McGhee
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,300 @@
1
+ # CrewdPolicies
2
+
3
+ CrewdPolicies enables conventional Pundit (https://github.com/elabs/pundit) policies to be written using an opinionated pattern based on declarative Create, Read, Execute (optional), Write and Destroy (CREWD) permissions for each resource. Conventional pundit create?, show?, update? and destroy? permissions are automatically derived from these, as well as permitted_attributes/strong parameters.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'crewd_policies'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install crewd_policies
20
+
21
+ ## Usage
22
+
23
+ The happy path that CREWD policies enables is as follows :
24
+
25
+ 1) include CrewdPolicies::Model into your models
26
+
27
+ ```ruby
28
+ class Person < ActiveRecord::Base
29
+ include CrewdPolicies::Model
30
+ end
31
+ ```
32
+
33
+
34
+ 2) declare constant arrays of field names, grouped to suit your application
35
+
36
+ ```ruby
37
+ USER_EDITABLE_FIELDS = [:name,:address]
38
+ ADMIN_FIELDS = [:roles]
39
+ ALL_FIELDS = ADMIN_FIELDS + USER_EDITABLE_FIELDS
40
+ ```
41
+
42
+
43
+ 3) declare permissions using *allow()* and your constant arrays in your model
44
+
45
+ ```ruby
46
+ class Customer < ActiveRecord::Base
47
+ include CrewdPolicies::Model
48
+
49
+ PUBLIC_FIELDS = [:name]
50
+ USER_EDITABLE_FIELDS = [:name,:address]
51
+ ADMIN_FIELDS = [:roles]
52
+ ALL_FIELDS = ADMIN_FIELDS + USER_EDITABLE_FIELDS
53
+
54
+ allow :sales, :create => :this
55
+ allow :sales, :read => ALL_FIELDS
56
+ allow :sales, :write => USER_EDITABLE_FIELDS
57
+
58
+ allow :admin, :write => ALL_FIELDS
59
+ allow :admin, :destroy => :this
60
+ end
61
+ ```
62
+
63
+ 4) include CrewdPolicies::Policy into your ApplicationPolicy or individual model policies. You will also need the Scope inner class defined on your application and/or individual model policies :
64
+
65
+ ```ruby
66
+ class ApplicationPolicy < Struct.new(:identity, :subject)
67
+ include CrewdPolicies::Policy
68
+
69
+ class Scope < Struct.new(:identity, :scope)
70
+ def resolve
71
+ scope.where(...your criteria...)
72
+ end
73
+ end
74
+ end
75
+
76
+ class CustomerPolicy < Struct.new(:identity, :subject)
77
+ end
78
+ ```
79
+
80
+ 5) your User or Identity model must have a has_role?(aRole) method
81
+
82
+ You now have a valid pundit policy that can be used like any other.
83
+
84
+ **Parameters**
85
+
86
+ `aRole`: a single string or symbol; or an array of strings and/or symbols
87
+ `aAbilities`: a hash where -
88
+ - `keys` are a single string or symbol; or an array of strings and/or symbols
89
+ - `values` are true, or a single string or symbol; or an array of strings and/or symbols
90
+
91
+ ### Allow Syntax
92
+
93
+ The allow method is declared as :
94
+
95
+ ```ruby
96
+ def allow(aRole, aAbilities)
97
+ end
98
+ ```
99
+
100
+ It is used on the model class as follows :
101
+
102
+ ```ruby
103
+ allow <role>, <abilities> => <fields>
104
+ ```
105
+
106
+ Typical examples :
107
+
108
+ ```ruby
109
+ allow :sales, :index => true # sales role can create any record in scope
110
+ allow [:finance, :marketing], [:create,:destroy,:index] => true
111
+ allow :sales, :read => :name
112
+ allow :sales, :read => [:address,:phone]
113
+ allow :reception, [:read,:write] => [:address,:phone]
114
+ ```
115
+
116
+ ### Allow Conditions
117
+
118
+ An allow statement can be made conditional by adding an :if or :unless key.
119
+ The value should be a symbol matching the name of a method with no parameters on the policy.
120
+
121
+ For example, we want users to be able to edit their own password :
122
+
123
+ on model :
124
+
125
+ ```ruby
126
+ allow :user, write: :password, if: :is_self?
127
+ ```
128
+
129
+ on policy :
130
+
131
+ ```ruby
132
+ def is_self?
133
+ record and !record.is_a?(Class) and record.id==identity.id
134
+ end
135
+ ```
136
+
137
+ Note that without the if condition, any user would be able to write any user's password.
138
+
139
+ ### Required allow declarations for a full CRUD policy in Rails
140
+
141
+ In order to implement a policy that allows full CRUD on a resource in a Rails application, you will need to write allow declarations for each of the following abilities :
142
+
143
+ | CREWD policy method | Example | Probably also needed
144
+ | :--- | :--- | :---
145
+ | create? | allow :user, create: true | allow :user, :write => %w(name address)
146
+ | read? | allow :user, read: %w(name address) |
147
+ | write? | allow :user, write: %w(name address password) |
148
+ | destroy? | allow :user, destroy: true |
149
+ | index? | allow :user, index: true | allow :user, :read => %w(name address)
150
+
151
+ Note that for normal Rails CRUD requirements, fields are only declared for read and write, while true is only given for create, destroy and index. Your own abilities may be declared and queried with either true or an array of fields with no special requirements.
152
+
153
+ The above CREWD policy methods are then aliased to provide typical Rails policy methods as follows
154
+
155
+ | Rails policy method | CREWD policy method |
156
+ | :--- | :--- |
157
+ | create? | create? |
158
+ | show? | read? |
159
+ | update? | write? |
160
+ | edit? | write? |
161
+ | index? | index? |
162
+ | delete? | destroy? |
163
+ | destroy? | destroy? |
164
+
165
+ Other Pundit conventional Rails methods are also provided :
166
+
167
+ * the following permitted attributes for "strong parameters" :
168
+ * permitted_attributes (equivalent to permitted_attributes_for_write)
169
+ * permitted_attributes_for_write
170
+ * permitted_attributes_for_read
171
+ * permitted_attributes_for_create
172
+ * permitted_attributes_for_update
173
+ * permitted_attributes_for_edit
174
+ * permitted_attributes_for_show
175
+ * permitted_attributes_for_index
176
+
177
+ This should meet the access control needs for the vast majority of Rails projects.
178
+
179
+ ### Controller Examples
180
+
181
+ ```ruby
182
+ def index
183
+ @posts = authorize policy_scope!(Post)
184
+ # use per post @attributes = @post.attributes.slice permitted_attributes(@post)
185
+ end
186
+
187
+ def show
188
+ @post = authorize policy_scope!(Post).find(params[:id])
189
+ @attributes = @post.attributes.slice permitted_attributes(@post)
190
+ end
191
+
192
+ def create
193
+ pars = params.require(:post).permit policy!(Post).permitted_attributes
194
+ @post = authorize policy_scope!(Post).create!(pars)
195
+ end
196
+
197
+ def update
198
+ @post = authorize policy_scope!(Post).find(params[:id])
199
+ pars = params.require(:post).permit policy!(Post).permitted_attributes
200
+ @post.update_attributes(pars)
201
+ @post.save!
202
+ end
203
+
204
+ def destroy
205
+ @post = authorize policy_scope!(Post).find(params[:id])
206
+ @post.destroy!
207
+ end
208
+ ```
209
+
210
+ ## Core Assumptions
211
+
212
+ CREWD Policies builds policies based on the core assumption that by declaring the following permissions, a complete permissions system can be derived by code for 90+% of models down to the field level :
213
+
214
+ - scope for the resource
215
+ - create for the resource
216
+ - readable fields
217
+ - writeable fields
218
+ - delete for a record
219
+ - normal Rails model validations for validating field values
220
+
221
+ Expanding on the above :
222
+
223
+ 1. The relevant policy scope should be used as a normal practice for all operations, unless there is a good reason. Rails scopes limit access for select, update and delete queries; and set default values for insert queries. The other permissions assume the proper scope has been applied.
224
+
225
+ 1. create? permission requires :
226
+ 1. a resource level permission (ie. "Can this role create customers at all?")
227
+ 1. field level _write_ permissions (ie. "When creating customers, what fields can be provided by this role?"
228
+ 1. field values that pass the normal Rails model validations - this is left to the user and out of the scope of this gem.
229
+
230
+ 1. read? permission requires :
231
+ 1. at least 1 readable field
232
+
233
+ 1. write? permission requires :
234
+ 1. at least 1 writeable field
235
+ 1. field values that pass the normal Rails model validations - this is left to the user and out of the scope of this gem.
236
+
237
+ 1. destroy? permission requires :
238
+ 1. a record level permission (ie. "Can this role destroy this customer?")
239
+
240
+ ## User/Identity Model Assumptions
241
+
242
+ 1. *User or Identity Model* : Traditional Rails applications have a User model which maps to a database table of users. An emerging architecture pattern uses JSON Web Tokens (http://jwt.io) to represent an identity managed by an external provider. Applications then will typically need an additional model eg. `Person` for attaching persisted data to that provided by the identity token. I have had success creating an `Identity` model; not backed by the database but created in memory by decoding the JWT. It then has methods for loading a `Person` model if required. This is how we intend to do things in future, and so the property name I am using here is `identity`, but I also use an alias of user pointing referring to it.
243
+
244
+ 2. `identity.has_role?(aRole)` : In order to interrogate the roles assigned the `identity` has, the method `has_role?(aRole)` must be implemented to receive a role string or symbol, and return true or false.
245
+
246
+ ## Why Pundit::NotAuthorizedError is misleading
247
+
248
+ `Pundit` defines this error, and raises it when the authorize method rejects a query. Unfortunately, in this case, Pundit users could easily assume they should return the HTTP status `401 Unauthorized`, but this would be against the definition for this status code.
249
+
250
+ > "The request has not been applied because it lacks valid authentication credentials for the target resource" - https://httpstatuses.com/401
251
+
252
+ Failing pundit checks rarely has anything to do with a lack of credentials, the failure is more likely a case of
253
+
254
+ > "The server understood the request but refuses to authorize it." - https://httpstatuses.com/403
255
+
256
+ It gets even worse if you follow a pattern in your client of forcing a logout of the user when they receive a 401, which makes sense when 401 is used correctly. The result is that attempting anything not allowed by a policy causes the user to be logged out, when they should simply be shown an alert and given the opportunity to correct the error or do something else while maintaining their session.
257
+
258
+ Pundit does have this in the README https://github.com/elabs/pundit#rescuing-a-denied-authorization-in-rails, but it is easily missed and naming mismatch is still likely to trip up new users.
259
+
260
+ https://github.com/elabs/pundit/issues/412
261
+
262
+ As an initial mitigation, crewd-policies provides the `CrewdPolicies::ForbiddenError` exception and the `forbidden!` method.
263
+
264
+ ## Development
265
+
266
+ After checking out the repo, to install dependencies run:
267
+
268
+ bin/setup
269
+
270
+ Then, run tests with:
271
+
272
+ rake spec
273
+
274
+ For an interactive prompt that will allow you to experiment, you can also run
275
+
276
+ bin/console
277
+
278
+ To install this gem onto your local machine, run
279
+
280
+ bundle exec rake install
281
+
282
+ To release a new version, update the version number in `version.rb`, and then run
283
+
284
+ bundle exec rake release
285
+
286
+ `rake release` will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
287
+
288
+ To experiment with this gem with interactive prompt, run
289
+
290
+ bin/console
291
+
292
+ ## Contributing
293
+
294
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/crewd_policies. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
295
+
296
+
297
+ ## License
298
+
299
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
300
+
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "crewd_policies"
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
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,37 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'crewd_policies/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "crewd_policies"
8
+ spec.version = CrewdPolicies::VERSION
9
+ spec.authors = ["Gary McGhee"]
10
+ spec.email = ["gary@buzzware.com.au"]
11
+
12
+ spec.summary = %q{A happy path for writing DRY Pundit policies}
13
+ spec.description = %q{CrewdPolicies enables conventional Pundit (https://github.com/elabs/pundit) policies to be written using an opinionated pattern based on declarative Create, Read, Execute (optional), Write and Destroy (CREWD) permissions for each resource. Conventional pundit create?, show?, update? and destroy? permissions are automatically derived from these, as well as permitted_attributes/strong parameters.}
14
+ spec.homepage = "https://github.com/buzzware/crewd_policies"
15
+ spec.license = "MIT"
16
+
17
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
18
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
19
+ # if spec.respond_to?(:metadata)
20
+ # spec.metadata['allowed_push_host'] = "TODO: Set to 'http://mygemserver.com'"
21
+ # else
22
+ # raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
23
+ # end
24
+
25
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
26
+ spec.bindir = "exe"
27
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
28
+ spec.require_paths = ["lib"]
29
+
30
+ spec.add_runtime_dependency "pundit", '~> 1.1', '>= 1.1.0'
31
+ spec.add_runtime_dependency "standard_exceptions", "~> 0.1.4"
32
+ spec.add_development_dependency "bundler", "~> 1.12"
33
+ spec.add_development_dependency "rake", "~> 10.0"
34
+ spec.add_development_dependency "rspec", "~> 3.0"
35
+ spec.add_development_dependency "activerecord", "~> 4.2"
36
+ spec.add_development_dependency "activesupport", "~> 4.2"
37
+ end
@@ -0,0 +1,9 @@
1
+ require "crewd_policies/version"
2
+ require "crewd_policies/model"
3
+ require "crewd_policies/policy"
4
+ require "crewd_policies/base_policy"
5
+ require "crewd_policies/jsonapi_resources"
6
+
7
+ module CrewdPolicies
8
+ # Your code goes here...
9
+ end
@@ -0,0 +1,42 @@
1
+ require 'active_support/core_ext/class/attribute'
2
+ require 'active_support/core_ext/string/inflections'
3
+ require 'pundit'
4
+
5
+ module CrewdPolicies
6
+ # optional
7
+ class BasePolicy
8
+ include Policy
9
+
10
+ attr_reader :identity, :record
11
+
12
+ def initialize(identity, record)
13
+ @identity = identity
14
+ @record = record
15
+ end
16
+
17
+ def model_class
18
+ @policy_class ||= self.class.name.sub(/Policy$/,'').safe_constantize # record ? record.class
19
+ end
20
+
21
+ def scope
22
+ Pundit.policy_scope!(identity, model_class)
23
+ end
24
+
25
+ class Scope
26
+ attr_reader :identity, :scope
27
+
28
+ def initialize(identity, scope)
29
+ @identity = identity
30
+ @scope = scope
31
+ end
32
+
33
+ def model_class
34
+ @policy_class ||= (self.class.name.sub(/Policy::Scope$/,'').safe_constantize or @scope)
35
+ end
36
+
37
+ def resolve
38
+ scope
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,39 @@
1
+ # This is for use with https://github.com/cerebris/jsonapi-resources
2
+ # It was developed with https://github.com/venuu/jsonapi-authorization but it may not be required because it doesn't seem to deal with attributes, just scope and record permissions?
3
+ # eg.
4
+ # class BaseResource < JSONAPI::Resource
5
+ # include JSONAPI::Authorization::PunditScopedResource
6
+ # include CrewdPolicies::JSONAPIResource
7
+ # abstract
8
+ # end
9
+
10
+ module CrewdPolicies
11
+ module JSONAPIResource
12
+
13
+ def self.included(aClass)
14
+ aClass.send :extend, ClassMethods
15
+ end
16
+
17
+ module ClassMethods
18
+
19
+ def inherited(subclass)
20
+ super
21
+ attrs = ::Pundit.policy!(nil,subclass._model_class).all_attributes.map(&:to_sym)
22
+ attrs -= [:id]
23
+ subclass.send(:attributes, *attrs) unless attrs.empty?
24
+ end
25
+
26
+ def updatable_fields(context)
27
+ ::Pundit.policy!(context[:user],_model).permitted_attributes_for_update.map(&:to_sym)
28
+ end
29
+
30
+ def self.creatable_fields(context)
31
+ ::Pundit.policy!(context[:user],_model).permitted_attributes_for_create.map(&:to_sym)
32
+ end
33
+ end
34
+
35
+ def fetchable_fields
36
+ ::Pundit.policy!(context[:user],_model).permitted_attributes_for_read.map(&:to_sym)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,52 @@
1
+ # these methods should actually work on any object with any abilities
2
+
3
+ module CrewdPolicies::Model
4
+
5
+ def self.included(aClass)
6
+ aClass.cattr_accessor :roles_rules
7
+ aClass.roles_rules = {} # [:sales] => [
8
+ # {ability: 'read', fields: [:name,:address]}
9
+ # {ability: 'destroy', allowed: true}
10
+ # ]
11
+ aClass.send :extend, ClassMethods
12
+ end
13
+
14
+ module ClassMethods
15
+
16
+ # supports different formats : allow <role>, <abilities> => <attributes>
17
+ #
18
+ # allow :sales, :write => [:name,:address] ie. sales can write the name and address fields
19
+ def allow(aRole,aAbilities)
20
+ if aRole.is_a? Array
21
+ aRole.each {|r| allow(r,aAbilities.dup) }
22
+ return
23
+ end
24
+ raise "aRole must be a string or a symbol" unless aRole.is_a?(String) or aRole.is_a?(Symbol)
25
+ aRole = aRole.to_s
26
+ raise "aAbilities must be a Hash" unless aAbilities.is_a? Hash # eg. :write => [:name,:address]
27
+
28
+ role_rules = (self.roles_rules[aRole] ||= [])
29
+ conditions = {}
30
+ conditions[:if] = aAbilities.delete(:if) if aAbilities.include?(:if)
31
+ conditions[:unless] = aAbilities.delete(:unless) if aAbilities.include?(:unless)
32
+ aAbilities.each do |abilities, fields|
33
+ abilities = [abilities] unless abilities.is_a?(Array)
34
+ fields = [fields] unless fields==true or fields.is_a?(Array)
35
+ abilities = abilities.map{|a| a.to_s} # now an array of strings
36
+ fields = fields.map{|a| a.to_s}.sort unless fields==true # now an array of strings or true
37
+ next if fields==[]
38
+ abilities.each do |a|
39
+ role_rules << (rule = {})
40
+ rule[:ability] = a
41
+ rule[:conditions] = conditions unless conditions.empty?
42
+ if fields==true # special "field" value to mean the record or class
43
+ rule[:allowed] = true
44
+ else
45
+ raise "create, destroy and index must have true as a value, not an array of fields" if a=='create' or a=='destroy' or a=='index'
46
+ rule[:fields] = fields
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,233 @@
1
+ require 'active_support/core_ext/class/attribute'
2
+ require 'active_support/core_ext/string/inflections'
3
+ require 'standard_exceptions'
4
+
5
+ module CrewdPolicies
6
+ module Policy
7
+
8
+ include ::StandardExceptions::Methods
9
+
10
+ public
11
+
12
+ attr_reader :identity, :record
13
+
14
+ # typical pundit/rails methods
15
+
16
+ def create? # resource level
17
+ inner_query_ability(:create)
18
+ end
19
+
20
+ def index?
21
+ inner_query_ability(:index)
22
+ end
23
+
24
+ def show?
25
+ inner_query_ability(:read)
26
+ end
27
+
28
+ def new?
29
+ inner_query_ability(:create)
30
+ end
31
+
32
+ def update?
33
+ inner_query_ability(:write)
34
+ end
35
+
36
+ def edit?
37
+ inner_query_ability(:write)
38
+ end
39
+
40
+ def destroy?
41
+ inner_query_ability(:destroy)
42
+ end
43
+
44
+ %w(write read create update edit show index).each do |m|
45
+ define_method "permitted_attributes_for_#{m}" do
46
+ allowed_attributes(m)
47
+ end
48
+ end
49
+
50
+ def all_attributes
51
+ result = []
52
+ record_class.roles_rules.each do |role,rules|
53
+ rules.each do |rule|
54
+ result |= rule[:fields] if rule[:fields]
55
+ end
56
+ end
57
+ result.sort
58
+ end
59
+
60
+ def permitted_attributes
61
+ inner_query_fields('write')
62
+ end
63
+
64
+ # CREWD permission methods
65
+
66
+ def read?
67
+ inner_query_ability(:read)
68
+ end
69
+
70
+ def write?
71
+ inner_query_ability(:write)
72
+ end
73
+
74
+ # utility methods
75
+
76
+ def scope
77
+ Pundit.policy_scope!(user, record_class)
78
+ end
79
+
80
+ def unauthorized!(aMessage=nil)
81
+ raise Pundit::NotAuthorizedError,(aMessage || "must be logged in")
82
+ end
83
+
84
+ def forbidden!(aMessage=nil)
85
+ raise ForbiddenError,(aMessage || "That operation was not allowed")
86
+ end
87
+
88
+ def record_class
89
+ record.is_a?(Class) ? record : record.class
90
+ end
91
+
92
+ def allowed?(aAbility,aFields=nil)
93
+ if aFields
94
+ pf = allowed_fields(aAbility)
95
+ if aFields.is_a? Array
96
+ aFields = aFields.map(&:to_s)
97
+ return (aFields - pf).empty?
98
+ else
99
+ aFields = aFields.to_s
100
+ return pf.include? aFields
101
+ end
102
+ else
103
+ inner_query_resource(aAbility)
104
+ end
105
+ end
106
+
107
+ # fields may be attributes or associations
108
+ def allowed_fields(aAbility)
109
+ inner_query_fields(aAbility)
110
+ end
111
+
112
+ def allowed_attributes(aAbility)
113
+ result = allowed_fields(aAbility)
114
+ cls = record_class
115
+ result.delete_if { |f| cls.reflections.has_key? f } if cls.respond_to? :reflections
116
+ result
117
+ end
118
+
119
+ def allowed_associations(aAbility=nil)
120
+ result = allowed_fields(aAbility)
121
+ cls = record_class
122
+ result.delete_if { |f| !cls.reflections.has_key? f }
123
+ result
124
+ end
125
+
126
+ protected # internal methods below here
127
+
128
+ def coalesce_field_ability(aAbility)
129
+ aAbility = aAbility.to_s
130
+ case aAbility
131
+ when 'write','read' then aAbility
132
+ when 'create','update','edit' then 'write'
133
+ when 'show','index' then 'read'
134
+ else
135
+ aAbility
136
+ end
137
+ end
138
+
139
+
140
+ # what fields does the identity have this ability for ?
141
+ def inner_query_fields(aAbility)
142
+ internal_server_error! "roles_rules not found on #{record_class.name}, make sure it has \"include CrewdPolicies::Model\"" unless ra = record_class.roles_rules rescue nil
143
+ unauthorized! "identity not given" if !identity
144
+ internal_server_error! "identity must implement has_role?" if !identity.responds_to? :has_role?
145
+
146
+ ability = coalesce_field_ability(aAbility)
147
+
148
+ # for each role in roles_rules, if identity.has_role?(role) && any conditions pass then merge in fields
149
+ result = []
150
+ ra.each do |role,rules|
151
+ next unless identity.has_role? role
152
+ rules.each do |rule| #ab, fields|
153
+ next unless rule[:ability]==ability
154
+ next unless eval_conditions rule
155
+ result |= rule[:fields]
156
+ end
157
+ end
158
+ result.sort!
159
+ result
160
+ end
161
+
162
+ # does the identity have this ability on this record?
163
+ def inner_query_resource(aAbility)
164
+ internal_server_error! "aAbility must be a string or a symbol" unless aAbility.is_a?(String) or aAbility.is_a?(Symbol)
165
+ internal_server_error! "roles_rules not found on #{record_class.name}, make sure it has \"include CrewdPolicies::Model\"" unless ra = record_class.roles_rules rescue nil
166
+ unauthorized! "identity not given" if !identity
167
+ internal_server_error! "identity must implement has_role?" if !identity.respond_to? :has_role?
168
+
169
+ aAbility = aAbility.to_s
170
+
171
+ ra.each do |role,rules|
172
+ next unless identity.has_role? role
173
+ rules.each do |rule|
174
+ next unless eval_conditions rule
175
+ next unless rule[:ability]==aAbility
176
+ return true if rule[:allowed]==true or rule[:fields].is_a?(Array) && !rule[:fields].empty?
177
+ end
178
+ end
179
+ false
180
+ end
181
+
182
+ def eval_conditions(aRule)
183
+ return true unless conds = aRule[:conditions]
184
+ if_cond = conds[:if]
185
+ unless_cond = conds[:unless]
186
+
187
+ if_cond = if if_cond.is_a? Symbol
188
+ send(if_cond)
189
+ elsif if_cond.is_a? Proc
190
+ if_cond.call()
191
+ elsif if_cond==nil
192
+ true
193
+ else
194
+ if_cond
195
+ end
196
+
197
+ unless_cond = if unless_cond.is_a? Symbol
198
+ send(unless_cond)
199
+ elsif unless_cond.is_a? Proc
200
+ unless_cond.call()
201
+ elsif unless_cond==nil
202
+ false
203
+ else
204
+ unless_cond
205
+ end
206
+
207
+ !!if_cond and !unless_cond
208
+ end
209
+
210
+ # does the identity have this ability on the record/resource at all?
211
+ def inner_query_ability(aAbility)
212
+ raise "aAbility must be a string or a symbol" unless aAbility.is_a?(String) or aAbility.is_a?(Symbol)
213
+ aAbility = aAbility.to_s
214
+
215
+ case aAbility
216
+ when 'write','read','update','show','edit'
217
+ inner_query_fields(aAbility).length > 0
218
+ when 'create','destroy','index'
219
+ inner_query_resource(aAbility)
220
+ else
221
+ raise 'this ability is unknown'
222
+ end
223
+ end
224
+ end
225
+
226
+ class ForbiddenError < ::StandardExceptions::Http::Forbidden
227
+ attr_accessor :query, :record, :policy
228
+ end
229
+
230
+ ::Pundit::NotAuthorizedError.class_eval do
231
+ include ::StandardExceptions::ExceptionInterface
232
+ end
233
+ end
@@ -0,0 +1,3 @@
1
+ module CrewdPolicies
2
+ VERSION = "0.2.0"
3
+ end
metadata ADDED
@@ -0,0 +1,169 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: crewd_policies
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Gary McGhee
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-03-31 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: pundit
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.1'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 1.1.0
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '1.1'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 1.1.0
33
+ - !ruby/object:Gem::Dependency
34
+ name: standard_exceptions
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: 0.1.4
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: 0.1.4
47
+ - !ruby/object:Gem::Dependency
48
+ name: bundler
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.12'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.12'
61
+ - !ruby/object:Gem::Dependency
62
+ name: rake
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '10.0'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '10.0'
75
+ - !ruby/object:Gem::Dependency
76
+ name: rspec
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '3.0'
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '3.0'
89
+ - !ruby/object:Gem::Dependency
90
+ name: activerecord
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '4.2'
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '4.2'
103
+ - !ruby/object:Gem::Dependency
104
+ name: activesupport
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '4.2'
110
+ type: :development
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '4.2'
117
+ description: CrewdPolicies enables conventional Pundit (https://github.com/elabs/pundit)
118
+ policies to be written using an opinionated pattern based on declarative Create,
119
+ Read, Execute (optional), Write and Destroy (CREWD) permissions for each resource.
120
+ Conventional pundit create?, show?, update? and destroy? permissions are automatically
121
+ derived from these, as well as permitted_attributes/strong parameters.
122
+ email:
123
+ - gary@buzzware.com.au
124
+ executables: []
125
+ extensions: []
126
+ extra_rdoc_files: []
127
+ files:
128
+ - ".gitignore"
129
+ - ".rspec"
130
+ - ".travis.yml"
131
+ - CODE_OF_CONDUCT.md
132
+ - Gemfile
133
+ - LICENSE.txt
134
+ - README.md
135
+ - Rakefile
136
+ - bin/console
137
+ - bin/setup
138
+ - crewd_policies.gemspec
139
+ - lib/crewd_policies.rb
140
+ - lib/crewd_policies/base_policy.rb
141
+ - lib/crewd_policies/jsonapi_resources.rb
142
+ - lib/crewd_policies/model.rb
143
+ - lib/crewd_policies/policy.rb
144
+ - lib/crewd_policies/version.rb
145
+ homepage: https://github.com/buzzware/crewd_policies
146
+ licenses:
147
+ - MIT
148
+ metadata: {}
149
+ post_install_message:
150
+ rdoc_options: []
151
+ require_paths:
152
+ - lib
153
+ required_ruby_version: !ruby/object:Gem::Requirement
154
+ requirements:
155
+ - - ">="
156
+ - !ruby/object:Gem::Version
157
+ version: '0'
158
+ required_rubygems_version: !ruby/object:Gem::Requirement
159
+ requirements:
160
+ - - ">="
161
+ - !ruby/object:Gem::Version
162
+ version: '0'
163
+ requirements: []
164
+ rubyforge_project:
165
+ rubygems_version: 2.4.8
166
+ signing_key:
167
+ specification_version: 4
168
+ summary: A happy path for writing DRY Pundit policies
169
+ test_files: []