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.
@@ -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.
@@ -0,0 +1,236 @@
1
+ # Policies
2
+ [![Gem Version](https://badge.fury.io/rb/policies.svg)](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.
@@ -0,0 +1,8 @@
1
+ require 'rake/testtask'
2
+
3
+ Rake::TestTask.new do |t|
4
+ t.libs << 'test'
5
+ end
6
+
7
+ desc 'Run tests'
8
+ task default: :test
@@ -0,0 +1,3 @@
1
+ require 'policies/base'
2
+ require 'policies/exceptions'
3
+ require 'policies/methods'
@@ -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,3 @@
1
+ module Policies
2
+ UnauthorizedError = Class.new(StandardError)
3
+ 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
@@ -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,9 @@
1
+ class ProjectPolicy < Policies::Base
2
+ def show?
3
+ true
4
+ end
5
+
6
+ def edit?
7
+ false
8
+ end
9
+ 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
@@ -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: []