critic 0.1.1 → 0.2.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 +4 -4
- data/.rubocop.yml +2 -0
- data/.travis.yml +1 -3
- data/README.md +128 -4
- data/Rakefile +8 -0
- data/lib/critic/authorization.rb +3 -2
- data/lib/critic/authorization_denied.rb +17 -0
- data/lib/critic/callbacks.rb +105 -0
- data/lib/critic/controller.rb +5 -9
- data/lib/critic/policy.rb +18 -28
- data/lib/critic/version.rb +1 -1
- data/lib/critic.rb +6 -2
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 119a637d018051dfe1517011f155c2d78155ccdc
|
4
|
+
data.tar.gz: eb1a100714ade7c9d2b8e15f1bd99b023fe07a1c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0a0e3a0ec214a2a081a3b3725e2271f9bd5e975195f3663c7015600f2647d406a60ffa4c0363bd60e6071a63077a5fbd9cbaec9365102dad49ba98711f267901
|
7
|
+
data.tar.gz: ea2def4856cc138ed661bc309cf48c64ab5820e111e9f60a5f5e89581610c60a7aa04d2a4786f438548f7e83df297587a3075abd085c45ad0829689b1e00bc12
|
data/.rubocop.yml
CHANGED
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -1,8 +1,6 @@
|
|
1
1
|
# Critic
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
TODO: Delete this and the text above, and describe your gem
|
3
|
+
Critic inserts an easily verifiable authorization layer into your MVC application using resource policies.
|
6
4
|
|
7
5
|
## Installation
|
8
6
|
|
@@ -22,7 +20,133 @@ Or install it yourself as:
|
|
22
20
|
|
23
21
|
## Usage
|
24
22
|
|
25
|
-
|
23
|
+
### Policies
|
24
|
+
|
25
|
+
A policy contains authorization logic for a resource and an authenticated subject.
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
# app/policies/post_policy.rb
|
29
|
+
class PostPolicy
|
30
|
+
include Critic::Policy
|
31
|
+
end
|
32
|
+
```
|
33
|
+
|
34
|
+
There are two types of methods:
|
35
|
+
|
36
|
+
* *action* - determines if subject is authorized to perform a specific operation on the resource
|
37
|
+
* *scope* - returns a list of resources available to the subject
|
38
|
+
|
39
|
+
|
40
|
+
#### Actions
|
41
|
+
|
42
|
+
The most basic actions return `true` or `false` to indicate the authorization status.
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
# app/policies/post_policy.rb
|
46
|
+
class PostPolicy
|
47
|
+
include Critic::Policy
|
48
|
+
|
49
|
+
def update
|
50
|
+
!resource.locked
|
51
|
+
end
|
52
|
+
end
|
53
|
+
```
|
54
|
+
|
55
|
+
This policy will only allow updates if the post is not `locked`.
|
56
|
+
|
57
|
+
Verify authorization using `#authorize`.
|
58
|
+
|
59
|
+
```ruby
|
60
|
+
Post = Struct.new(:locked)
|
61
|
+
User = Struct.new
|
62
|
+
|
63
|
+
PostPolicy.authorize(:update, User.new, Post.new(false)).granted? #=> true
|
64
|
+
PostPolicy.authorize(:update, User.new, Post.new(true)).granted? #=> false
|
65
|
+
```
|
66
|
+
|
67
|
+
#### Scopes
|
68
|
+
|
69
|
+
Scopes treat `resource` as a starting point and return a restricted set of associated resources. Policies can have any number of scopes. The default scope is `#index`.
|
70
|
+
|
71
|
+
```
|
72
|
+
# app/policies/post_policy.rb
|
73
|
+
class PostPolicy
|
74
|
+
include Critic::Policy
|
75
|
+
|
76
|
+
def index
|
77
|
+
resource.where(deleted_at: nil, author_id: subject.id)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
```
|
81
|
+
|
82
|
+
### Controller
|
83
|
+
|
84
|
+
Controllers are the primary consumer of policies. Controllers ask the policy if an authenticated subject is authorized to perform a specific action on a specific resource.
|
85
|
+
|
86
|
+
In Rails, the policy action is inferred from `params[:action]` which corresponds to the controller action method name.
|
87
|
+
|
88
|
+
When `authorize` fails, a `Critic::AuthorizationDenied` exception is raised with reference to the performed authorization.
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
# app/controllers/post_controller.rb
|
92
|
+
class PostController < ApplicationController
|
93
|
+
include Critic::Controller
|
94
|
+
|
95
|
+
rescue_from Critic::AuthorizationDenied do |exception|
|
96
|
+
messages = exception.authorization.messages || exception.message
|
97
|
+
render json: {errors: [messages]}, status: :unauthorized
|
98
|
+
end
|
99
|
+
|
100
|
+
def update
|
101
|
+
post = Post.find(params[:id])
|
102
|
+
authorize post # calls PostPolicy#update
|
103
|
+
|
104
|
+
render json: post
|
105
|
+
end
|
106
|
+
end
|
107
|
+
```
|
108
|
+
|
109
|
+
When action cannot be inferred, pass the intended action to `authorize`.
|
110
|
+
|
111
|
+
```ruby
|
112
|
+
# app/controllers/post_controller.rb
|
113
|
+
class PostController < Sinatra::Base
|
114
|
+
include Critic::Controller
|
115
|
+
|
116
|
+
error Critic::AuthorizationDenied do |exception|
|
117
|
+
messages = exception.authorization.messages || exception.message
|
118
|
+
|
119
|
+
body {errors: [messages]}
|
120
|
+
halt 403
|
121
|
+
end
|
122
|
+
|
123
|
+
put '/posts/:id' do |id|
|
124
|
+
post = Post.find(id)
|
125
|
+
authorize post, :update
|
126
|
+
|
127
|
+
post.to_json
|
128
|
+
end
|
129
|
+
end
|
130
|
+
```
|
131
|
+
|
132
|
+
By default, the policy's subject is referenced by `current_user`. Override `critic` to customize.
|
133
|
+
|
134
|
+
```ruby
|
135
|
+
# app/controllers/application_controller.rb
|
136
|
+
class ApplicationController < ActionController::Base
|
137
|
+
include Critic::Controller
|
138
|
+
|
139
|
+
protected
|
140
|
+
|
141
|
+
def critic
|
142
|
+
token
|
143
|
+
end
|
144
|
+
end
|
145
|
+
```
|
146
|
+
|
147
|
+
|
148
|
+
#### Testing
|
149
|
+
|
26
150
|
|
27
151
|
## Development
|
28
152
|
|
data/Rakefile
CHANGED
@@ -1,7 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
require 'bundler/gem_tasks'
|
3
3
|
require 'rspec/core/rake_task'
|
4
|
+
require 'rubocop/rake_task'
|
4
5
|
|
5
6
|
RSpec::Core::RakeTask.new(:spec)
|
6
7
|
|
7
8
|
task default: :spec
|
9
|
+
|
10
|
+
desc 'Run rubocop'
|
11
|
+
task :rubocop do
|
12
|
+
task = RuboCop::RakeTask.new
|
13
|
+
task.patterns = ['lib/**/*.rb']
|
14
|
+
task
|
15
|
+
end
|
data/lib/critic/authorization.rb
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
class Critic::Authorization
|
3
3
|
attr_reader :policy, :action
|
4
|
-
attr_accessor :messages, :granted, :result
|
4
|
+
attr_accessor :messages, :granted, :result, :metadata
|
5
5
|
|
6
6
|
def initialize(policy, action)
|
7
7
|
@policy = policy
|
8
|
-
@action = action
|
8
|
+
@action = action&.to_sym
|
9
9
|
|
10
|
+
@metadata = {}
|
10
11
|
@granted, @result = nil
|
11
12
|
@messages = []
|
12
13
|
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
class Critic::AuthorizationDenied < Critic::Error
|
3
|
+
DEFAULT_MESSAGE = 'Authorization denied'
|
4
|
+
|
5
|
+
attr_reader :authorization
|
6
|
+
|
7
|
+
def initialize(authorization)
|
8
|
+
@authorization = authorization
|
9
|
+
|
10
|
+
message = if authorization.messages.any?
|
11
|
+
authorization.messages.join(',')
|
12
|
+
else
|
13
|
+
DEFAULT_MESSAGE
|
14
|
+
end
|
15
|
+
super(message)
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# Adds callbacks to {Critic::Policy#authorize}
|
3
|
+
module Critic::Callbacks
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
include ActiveSupport::Callbacks
|
8
|
+
|
9
|
+
if ActiveSupport::VERSION::MAJOR < 4
|
10
|
+
define_callbacks :authorize,
|
11
|
+
terminator: 'authorization.result == false || result == false',
|
12
|
+
skip_after_callbacks_if_terminated: true
|
13
|
+
else
|
14
|
+
define_callbacks :authorize,
|
15
|
+
terminator: ->(target, result) { target.authorization.result == false || false == result },
|
16
|
+
skip_after_callbacks_if_terminated: true
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Adds callback management functions to {Critic::Policy}
|
21
|
+
module ClassMethods
|
22
|
+
def before_authorize(*names, &blk)
|
23
|
+
_insert_callbacks(names, blk) do |name, options|
|
24
|
+
set_callback(:authorize, :before, name, options)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def after_authorize(*names, &blk)
|
29
|
+
_insert_callbacks(names, blk) do |name, options|
|
30
|
+
set_callback(:authorize, :after, name, options)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def around_authorize(*names, &blk)
|
35
|
+
_insert_callbacks(names, blk) do |name, options|
|
36
|
+
set_callback(:authorize, :around, name, options)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def skip_before_authorize(*names, &blk)
|
41
|
+
_insert_callbacks(names, blk) do |name, options|
|
42
|
+
skip_callback(:authorize, :before, name, options)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# If :only or :except are used, convert the options into the
|
47
|
+
# :unless and :if options of ActiveSupport::Callbacks.
|
48
|
+
# The basic idea is that :only => :index gets converted to
|
49
|
+
# :if => proc {|c| c.action_name == "index" }.
|
50
|
+
#
|
51
|
+
# ==== Options
|
52
|
+
# * <tt>only</tt> - The callback should be run only for this action
|
53
|
+
# * <tt>except</tt> - The callback should be run for all actions except this action
|
54
|
+
def _normalize_callback_options(options)
|
55
|
+
_normalize_callback_option(options, :only, :if)
|
56
|
+
_normalize_callback_option(options, :except, :unless)
|
57
|
+
end
|
58
|
+
|
59
|
+
def _normalize_callback_option(options, from, to) # :nodoc:
|
60
|
+
from = options[from]
|
61
|
+
if from
|
62
|
+
from = Array(from).map { |o| "authorization.action.to_s == '#{o}'" }
|
63
|
+
options[to] = Array(options[to]).unshift(from).join(' || ')
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Skip before, after, and around action callbacks matching any of the names.
|
68
|
+
#
|
69
|
+
# ==== Parameters
|
70
|
+
# * <tt>names</tt> - A list of valid names that could be used for
|
71
|
+
# callbacks. Note that skipping uses Ruby equality, so it's
|
72
|
+
# impossible to skip a callback defined using an anonymous proc
|
73
|
+
# using #skip_action_callback
|
74
|
+
def skip_authorize(*names)
|
75
|
+
skip_before_action(*names)
|
76
|
+
skip_after_action(*names)
|
77
|
+
skip_around_action(*names)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Take callback names and an optional callback proc, normalize them,
|
81
|
+
# then call the block with each callback. This allows us to abstract
|
82
|
+
# the normalization across several methods that use it.
|
83
|
+
#
|
84
|
+
# ==== Parameters
|
85
|
+
# * <tt>callbacks</tt> - An array of callbacks, with an optional
|
86
|
+
# options hash as the last parameter.
|
87
|
+
# * <tt>block</tt> - A proc that should be added to the callbacks.
|
88
|
+
#
|
89
|
+
# ==== Block Parameters
|
90
|
+
# * <tt>name</tt> - The callback to be added
|
91
|
+
# * <tt>options</tt> - A hash of options to be used when adding the callback
|
92
|
+
def _insert_callbacks(callbacks, block = nil)
|
93
|
+
options = callbacks.extract_options!
|
94
|
+
_normalize_callback_options(options)
|
95
|
+
callbacks.push(block) if block
|
96
|
+
callbacks.each do |callback|
|
97
|
+
yield callback, options
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def process_authorization(*)
|
103
|
+
run_callbacks(:authorize) { super }
|
104
|
+
end
|
105
|
+
end
|
data/lib/critic/controller.rb
CHANGED
@@ -9,7 +9,7 @@ module Critic::Controller
|
|
9
9
|
end
|
10
10
|
end
|
11
11
|
|
12
|
-
def authorize(resource, action=default_action, policy: policy(resource), with: nil)
|
12
|
+
def authorize(resource, action = default_action, policy: policy(resource), with: nil)
|
13
13
|
authorizing!
|
14
14
|
|
15
15
|
args = [with] if !with.is_a?(Array) && !with.nil?
|
@@ -27,10 +27,10 @@ module Critic::Controller
|
|
27
27
|
false
|
28
28
|
end
|
29
29
|
|
30
|
-
def authorize_scope(scope, *args, action: nil, policy: policy(scope))
|
30
|
+
def authorize_scope(scope, *args, action: nil, policy: policy(scope), **options)
|
31
31
|
authorization_action = action || policy.scope
|
32
32
|
|
33
|
-
authorize(scope, authorization_action, *args, policy: policy)
|
33
|
+
authorize(scope, authorization_action, *args, policy: policy, **options)
|
34
34
|
end
|
35
35
|
|
36
36
|
protected
|
@@ -38,7 +38,7 @@ module Critic::Controller
|
|
38
38
|
attr_reader :authorization
|
39
39
|
|
40
40
|
def authorization_failed!
|
41
|
-
raise Critic::AuthorizationDenied, authorization
|
41
|
+
raise Critic::AuthorizationDenied, authorization
|
42
42
|
end
|
43
43
|
|
44
44
|
def authorization_missing!
|
@@ -46,11 +46,7 @@ module Critic::Controller
|
|
46
46
|
end
|
47
47
|
|
48
48
|
def verify_authorized
|
49
|
-
|
50
|
-
authorization_missing!
|
51
|
-
else
|
52
|
-
true
|
53
|
-
end
|
49
|
+
(true == @_authorizing) || authorization_missing!
|
54
50
|
end
|
55
51
|
|
56
52
|
def authorizing!
|
data/lib/critic/policy.rb
CHANGED
@@ -25,41 +25,25 @@ module Critic::Policy
|
|
25
25
|
end
|
26
26
|
|
27
27
|
included do
|
28
|
-
include
|
29
|
-
|
30
|
-
if ActiveSupport::VERSION::MAJOR < 4
|
31
|
-
define_callbacks :authorize, terminator: 'authorization.result == false || result == false'
|
32
|
-
else
|
33
|
-
define_callbacks :authorize, terminator: ->(target, result) { target.authorization.result == false || false == result }
|
34
|
-
end
|
28
|
+
include Critic::Callbacks
|
35
29
|
end
|
36
30
|
|
37
31
|
# Policy entry points
|
38
32
|
module ClassMethods
|
39
|
-
def authorize(action, subject, resource, args=nil)
|
33
|
+
def authorize(action, subject, resource, args = nil)
|
40
34
|
new(subject, resource).authorize(action, *args)
|
41
35
|
end
|
42
36
|
|
43
37
|
def scope(action = nil)
|
44
38
|
action.nil? ? (@scope || :index) : (@scope = action)
|
45
39
|
end
|
46
|
-
|
47
|
-
def before_authorize(*args, **options, &block)
|
48
|
-
set_callback(:authorize, :before, *args, **options, &block)
|
49
|
-
end
|
50
|
-
|
51
|
-
def after_authorize(*args, **options, &block)
|
52
|
-
set_callback(:authorize, :after, *args, **options, &block)
|
53
|
-
end
|
54
|
-
|
55
|
-
def around_authorize(*args, **options, &block)
|
56
|
-
set_callback(:authorize, :around, *args, **options, &block)
|
57
|
-
end
|
58
40
|
end
|
59
41
|
|
60
42
|
attr_reader :subject, :resource, :errors
|
61
43
|
attr_accessor :authorization
|
62
44
|
|
45
|
+
delegate :messages, :metadata, to: :authorization
|
46
|
+
|
63
47
|
def initialize(subject, resource)
|
64
48
|
@subject = subject
|
65
49
|
@resource = resource
|
@@ -73,15 +57,9 @@ module Critic::Policy
|
|
73
57
|
def authorize(action, *args)
|
74
58
|
self.authorization = Critic::Authorization.new(self, action)
|
75
59
|
|
76
|
-
result =
|
60
|
+
result = catch(:halt) { process_authorization(action, args) }
|
77
61
|
|
78
|
-
|
79
|
-
run_callbacks(:authorize) { result = public_send(action, *args) }
|
80
|
-
rescue Critic::AuthorizationDenied
|
81
|
-
authorization.granted = false
|
82
|
-
ensure
|
83
|
-
authorization.result = result if authorization.result.nil?
|
84
|
-
end
|
62
|
+
authorization.result = result if authorization.result.nil?
|
85
63
|
|
86
64
|
case authorization.result
|
87
65
|
when Critic::Authorization
|
@@ -98,4 +76,16 @@ module Critic::Policy
|
|
98
76
|
|
99
77
|
authorization
|
100
78
|
end
|
79
|
+
|
80
|
+
protected
|
81
|
+
|
82
|
+
def halt(*response)
|
83
|
+
throw :halt, *response
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def process_authorization(action, args)
|
89
|
+
public_send(action, *args)
|
90
|
+
end
|
101
91
|
end
|
data/lib/critic/version.rb
CHANGED
data/lib/critic.rb
CHANGED
@@ -4,13 +4,17 @@ require 'active_support/concern'
|
|
4
4
|
require 'active_support/callbacks'
|
5
5
|
require 'active_support/version'
|
6
6
|
require 'active_support/core_ext/string/inflections'
|
7
|
+
require 'active_support/core_ext/module/delegation'
|
7
8
|
|
8
9
|
# Namespace
|
9
10
|
module Critic; end
|
10
11
|
|
11
|
-
Critic::
|
12
|
-
|
12
|
+
Critic::Error = Class.new(StandardError)
|
13
|
+
|
14
|
+
Critic::AuthorizationMissing = Class.new(Critic::Error)
|
13
15
|
|
14
16
|
require 'critic/policy'
|
15
17
|
require 'critic/authorization'
|
18
|
+
require 'critic/authorization_denied'
|
16
19
|
require 'critic/controller'
|
20
|
+
require 'critic/callbacks'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: critic
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Josh Lane
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-06-
|
11
|
+
date: 2016-06-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -93,6 +93,8 @@ files:
|
|
93
93
|
- critic.gemspec
|
94
94
|
- lib/critic.rb
|
95
95
|
- lib/critic/authorization.rb
|
96
|
+
- lib/critic/authorization_denied.rb
|
97
|
+
- lib/critic/callbacks.rb
|
96
98
|
- lib/critic/controller.rb
|
97
99
|
- lib/critic/policy.rb
|
98
100
|
- lib/critic/version.rb
|