policies 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +236 -0
- data/Rakefile +8 -0
- data/lib/policies.rb +3 -0
- data/lib/policies/base.rb +14 -0
- data/lib/policies/exceptions.rb +3 -0
- data/lib/policies/methods.rb +32 -0
- data/policies.gemspec +12 -0
- data/test/policies/project_policy.rb +9 -0
- data/test/policies_test.rb +42 -0
- data/test/test_helper.rb +18 -0
- metadata +82 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: e7d39fff13b803ccfefdae4c420e79e3e0e30223
|
4
|
+
data.tar.gz: 1f18156a9464038dc45230efce8449c4394fe3f4
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a88f256e98bd91de715b40776b27ef26743004ca23e4739b1f555fbcd195a6b74d516bcb540668141ad7438355f0f9fd16fa6fed38950cc35bcc74f1bd9b231b
|
7
|
+
data.tar.gz: ee238a87870c608a0945a1e0dd05eb37972b7d9c8a82917b1494a90d6180b63443e33662afa8c495a0e2d8a01e3a2b1ed48c73503a155ec814fc32ec8d07b00f
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015 Josh Wetzel
|
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 all
|
13
|
+
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 THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,236 @@
|
|
1
|
+
# Policies
|
2
|
+
[](http://badge.fury.io/rb/policies)
|
3
|
+
|
4
|
+
Policies is an authorization control library for Ruby on Rails.
|
5
|
+
|
6
|
+
It was primarily designed for use in applications where a user's authorization may change depending on a particular
|
7
|
+
context. For example, in an application where users may belong to one or more projects, it may be ideal for them to edit
|
8
|
+
the settings of a project they own, but not necessarily edit the settings of a project in which they are a member.
|
9
|
+
|
10
|
+
This gem helps facilitate the creation of those authorization rules through simple, well defined Ruby classes.
|
11
|
+
|
12
|
+
## Installation
|
13
|
+
In your Gemfile, include the `policies` gem.
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
gem 'policies'
|
17
|
+
```
|
18
|
+
|
19
|
+
## Prerequisites
|
20
|
+
Policies makes a few logical assumptions for the ease of implementation.
|
21
|
+
|
22
|
+
1. It requires a `current_user` method to be defined.
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
def current_user
|
26
|
+
@current_user ||= User.find(session[:user_id]) if session[:user_id]
|
27
|
+
end
|
28
|
+
```
|
29
|
+
2. It requires a `current_role` method to be defined.
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
# On an intermediary object, such as membership
|
33
|
+
def current_role
|
34
|
+
if @project.present? && @project.persisted?
|
35
|
+
@current_role ||= @project.memberships.find_by(user: current_user).role
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# On the user
|
40
|
+
def current_role
|
41
|
+
@current_role ||= current_user.role
|
42
|
+
end
|
43
|
+
```
|
44
|
+
3. The names of policy classes must be a combination of an object's class suffixed with `Policy`. For example, a
|
45
|
+
policy for projects should be named `ProjectPolicy`, and a policy for users should be named `UserPolicy`. It is
|
46
|
+
recommended to place policies in an `app/policies` directory.
|
47
|
+
4. Policies should inherit from `Policies::Base`.
|
48
|
+
5. Method names within a policy should be suffixed with a `?`.
|
49
|
+
|
50
|
+
## Getting Started
|
51
|
+
Take the following example, in which a user may belong to one or more projects through an intermediary membership.
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
# app/models/user.rb
|
55
|
+
class User < ActiveRecord::Base
|
56
|
+
has_many :memberships
|
57
|
+
has_many :projects, through: :memberships
|
58
|
+
end
|
59
|
+
|
60
|
+
# app/models/project.rb
|
61
|
+
class Project < ActiveRecord::Base
|
62
|
+
has_many :memberships
|
63
|
+
has_many :users, through: :memberships
|
64
|
+
end
|
65
|
+
|
66
|
+
# app/models/membership.rb
|
67
|
+
class Membership < ActiveRecord::Base
|
68
|
+
belongs_to :user
|
69
|
+
belongs_to :project
|
70
|
+
end
|
71
|
+
|
72
|
+
# app/models/role.rb
|
73
|
+
class Role < ActiveRecord::Base
|
74
|
+
def member?
|
75
|
+
%w(Member Administrator Owner).include?(name)
|
76
|
+
end
|
77
|
+
|
78
|
+
def admin?
|
79
|
+
%w(Administrator Owner).include?(name)
|
80
|
+
end
|
81
|
+
|
82
|
+
def owner?
|
83
|
+
name == 'Owner'
|
84
|
+
end
|
85
|
+
end
|
86
|
+
```
|
87
|
+
|
88
|
+
Imagine a user is an owner of Project A and a member of Project B. In this specific case, the role of the user will
|
89
|
+
change depending on which project they are viewing. Owners of a project should have the ability to edit its settings or
|
90
|
+
invite new members, while members of a project should only be allowed to view it.
|
91
|
+
|
92
|
+
With that in mind, a new policy class may be created to limit the authorization depending on the current role.
|
93
|
+
|
94
|
+
### Creating a New Policy
|
95
|
+
Within `app/policies`, create a new file named `project_policy.rb`. **Remember to restart your application server to
|
96
|
+
pick up the new directory.**
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
# app/policies/project_policy.rb
|
100
|
+
class ProjectPolicy < Policies::Base
|
101
|
+
end
|
102
|
+
```
|
103
|
+
|
104
|
+
### Limiting Access
|
105
|
+
Let's assume we want to limit the edit and update actions to a project owner.
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
# app/policies/project_policy.rb
|
109
|
+
class ProjectPolicy < Policies::Base
|
110
|
+
def edit?
|
111
|
+
current_role.owner?
|
112
|
+
end
|
113
|
+
alias_method :update?, :edit?
|
114
|
+
end
|
115
|
+
```
|
116
|
+
|
117
|
+
An instance variable named after the object's class is also available for use within the policy.
|
118
|
+
|
119
|
+
```ruby
|
120
|
+
# app/policies/project_policy.rb
|
121
|
+
class ProjectPolicy < Policies::Base
|
122
|
+
def destroy?
|
123
|
+
@project.can_be_destroyed? && current_role.owner?
|
124
|
+
end
|
125
|
+
end
|
126
|
+
```
|
127
|
+
|
128
|
+
Using a different example, a user may only be allowed to edit their own account.
|
129
|
+
|
130
|
+
```ruby
|
131
|
+
# app/policies/user_policy.rb
|
132
|
+
class UserPolicy < Policies::Base
|
133
|
+
def edit?
|
134
|
+
current_user == @user
|
135
|
+
end
|
136
|
+
alias_method :update?, :edit?
|
137
|
+
end
|
138
|
+
```
|
139
|
+
|
140
|
+
### Updating Views and Controllers
|
141
|
+
After the policy is written, views may be updated with the `authorized?` helper.
|
142
|
+
|
143
|
+
```erb
|
144
|
+
<% if authorized?(:edit, @project) %>
|
145
|
+
<%= link_to @project, project_path(@project) %>
|
146
|
+
<% end %>
|
147
|
+
```
|
148
|
+
|
149
|
+
Controllers may be updated with the `authorize` and `authorized?` methods.
|
150
|
+
|
151
|
+
```ruby
|
152
|
+
# app/controllers/projects_controller.rb
|
153
|
+
class ProjectsController < ApplicationController
|
154
|
+
def edit
|
155
|
+
@project = current_user.projects.find(params[:id])
|
156
|
+
authorize(@project)
|
157
|
+
end
|
158
|
+
|
159
|
+
def update
|
160
|
+
@project = current_user.projects.find(params[:id])
|
161
|
+
authorize(@project)
|
162
|
+
|
163
|
+
if @project.update(project_params)
|
164
|
+
redirect_to @project, success: translate('.success')
|
165
|
+
else
|
166
|
+
render :edit
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
```
|
171
|
+
|
172
|
+
A better, more DRY approach may be using `authorize` in a `before_action`.
|
173
|
+
|
174
|
+
```ruby
|
175
|
+
# app/controllers/projects_controller.rb
|
176
|
+
class ProjectsController < ApplicationController
|
177
|
+
before_action :set_project, only: [:edit, :update]
|
178
|
+
|
179
|
+
def update
|
180
|
+
if @project.update(project_params)
|
181
|
+
redirect_to @project, success: translate('.success')
|
182
|
+
else
|
183
|
+
render :edit
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
private
|
188
|
+
|
189
|
+
def set_project
|
190
|
+
@project = current_user.projects.find(params[:id])
|
191
|
+
authorize(@project)
|
192
|
+
end
|
193
|
+
end
|
194
|
+
```
|
195
|
+
|
196
|
+
`authorize` will raise `Policies::UnauthorizedError` if the user is restricted from accessing the particular action.
|
197
|
+
|
198
|
+
`authorized?` may be used when a boolean should be returned. If no action argument is passed, it will default to the
|
199
|
+
current action.
|
200
|
+
|
201
|
+
```ruby
|
202
|
+
# app/controllers/projects_controller.rb
|
203
|
+
class ProjectsController < ApplicationController
|
204
|
+
def edit
|
205
|
+
@project = Project.find(params[:id])
|
206
|
+
|
207
|
+
if authorized?(@project)
|
208
|
+
...
|
209
|
+
else
|
210
|
+
redirect_to projects_path, error: translate('.unauthorized')
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
```
|
215
|
+
|
216
|
+
In a situation where an instantiated object is not available, a symbol may be passed to `authorized?` and `authorize`.
|
217
|
+
If no action argument is passed, it defaults to the current `action_name`.
|
218
|
+
|
219
|
+
```erb
|
220
|
+
<% if authorized?(:index, :projects) %>
|
221
|
+
<%= link_to @project, projects_path %>
|
222
|
+
<% end %>
|
223
|
+
```
|
224
|
+
|
225
|
+
```ruby
|
226
|
+
# app/controllers/projects_controller.rb
|
227
|
+
class ProjectsController < ApplicationController
|
228
|
+
def index
|
229
|
+
authorize(:projects)
|
230
|
+
@projects = current_user.projects
|
231
|
+
end
|
232
|
+
end
|
233
|
+
```
|
234
|
+
|
235
|
+
## Acknowledgments
|
236
|
+
Special thanks to [Pundit](https://github.com/elabs/pundit) for the inspiration for this project.
|
data/Rakefile
ADDED
data/lib/policies.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
module Policies
|
2
|
+
class Base
|
3
|
+
attr_reader :current_user, :current_role
|
4
|
+
|
5
|
+
def initialize(current_user, current_role, object)
|
6
|
+
@current_user = current_user
|
7
|
+
@current_role = current_role
|
8
|
+
|
9
|
+
unless object.is_a?(Symbol)
|
10
|
+
instance_variable_set('@' + object.class.to_s.underscore, object)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Policies::Methods
|
2
|
+
def self.included(base)
|
3
|
+
base.send(:helper_method, :authorized?) if base.respond_to?(:helper_method)
|
4
|
+
end
|
5
|
+
|
6
|
+
def authorize(action = action_name, object)
|
7
|
+
raise Policies::UnauthorizedError unless authorized?(action, object)
|
8
|
+
end
|
9
|
+
|
10
|
+
def authorized?(action = action_name, object)
|
11
|
+
policy_class(object).new(current_user, current_role, object).public_send(action.to_s + '?')
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def policy_class(object)
|
17
|
+
class_name =
|
18
|
+
if object.is_a?(Symbol)
|
19
|
+
object.to_s.classify
|
20
|
+
else
|
21
|
+
object.class.to_s
|
22
|
+
end + 'Policy'
|
23
|
+
|
24
|
+
class_name.constantize
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
if defined?(ActionController::Base)
|
29
|
+
ActionController::Base.class_eval do
|
30
|
+
include Policies::Methods
|
31
|
+
end
|
32
|
+
end
|
data/policies.gemspec
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'policies'
|
3
|
+
s.version = '1.1.0'
|
4
|
+
s.files = `git ls-files`.split($/)
|
5
|
+
s.summary = 'Authorization control'
|
6
|
+
s.author = 'Josh Wetzel'
|
7
|
+
s.license = 'MIT'
|
8
|
+
s.required_ruby_version = '~> 2'
|
9
|
+
|
10
|
+
s.add_dependency 'actionpack', '~> 4.2'
|
11
|
+
s.add_dependency 'activesupport', '~> 4.2'
|
12
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class PoliciesTest < Minitest::Test
|
4
|
+
def setup
|
5
|
+
@project = Project.new
|
6
|
+
end
|
7
|
+
|
8
|
+
def test_symbol_conversion
|
9
|
+
assert_equal authorized?(:show, :project), true
|
10
|
+
assert_equal authorized?(:edit, :project), false
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_symbol_conversion_without_action_argument
|
14
|
+
assert_equal authorized?(:project), false
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_plural_symbol_conversion
|
18
|
+
assert_equal authorized?(:show, :projects), true
|
19
|
+
assert_equal authorized?(:edit, :projects), false
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_plural_symbol_conversion_without_action_argument
|
23
|
+
assert_equal authorized?(:projects), false
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_object_conversion
|
27
|
+
assert_equal authorized?(:show, @project), true
|
28
|
+
assert_equal authorized?(:edit, @project), false
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_unauthorized_error_raised
|
32
|
+
assert_raises Policies::UnauthorizedError do
|
33
|
+
authorize(:edit, @project)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_unauthorized_error_raised_without_action_argument
|
38
|
+
assert_raises Policies::UnauthorizedError do
|
39
|
+
authorize(@project)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'active_support/all'
|
3
|
+
require 'policies'
|
4
|
+
require 'policies_test'
|
5
|
+
require 'policies/project_policy'
|
6
|
+
|
7
|
+
Project = Class.new
|
8
|
+
|
9
|
+
Minitest::Test.class_eval do
|
10
|
+
include Policies::Methods
|
11
|
+
end
|
12
|
+
|
13
|
+
def current_user; end
|
14
|
+
def current_role; end
|
15
|
+
|
16
|
+
def action_name
|
17
|
+
'edit'
|
18
|
+
end
|
metadata
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: policies
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Josh Wetzel
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-05-25 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: actionpack
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4.2'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '4.2'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activesupport
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '4.2'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '4.2'
|
41
|
+
description:
|
42
|
+
email:
|
43
|
+
executables: []
|
44
|
+
extensions: []
|
45
|
+
extra_rdoc_files: []
|
46
|
+
files:
|
47
|
+
- LICENSE
|
48
|
+
- README.md
|
49
|
+
- Rakefile
|
50
|
+
- lib/policies.rb
|
51
|
+
- lib/policies/base.rb
|
52
|
+
- lib/policies/exceptions.rb
|
53
|
+
- lib/policies/methods.rb
|
54
|
+
- policies.gemspec
|
55
|
+
- test/policies/project_policy.rb
|
56
|
+
- test/policies_test.rb
|
57
|
+
- test/test_helper.rb
|
58
|
+
homepage:
|
59
|
+
licenses:
|
60
|
+
- MIT
|
61
|
+
metadata: {}
|
62
|
+
post_install_message:
|
63
|
+
rdoc_options: []
|
64
|
+
require_paths:
|
65
|
+
- lib
|
66
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
67
|
+
requirements:
|
68
|
+
- - "~>"
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: '2'
|
71
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
requirements: []
|
77
|
+
rubyforge_project:
|
78
|
+
rubygems_version: 2.4.5
|
79
|
+
signing_key:
|
80
|
+
specification_version: 4
|
81
|
+
summary: Authorization control
|
82
|
+
test_files: []
|