policies 1.1.0

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