critic 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d45e116551e704334c5a3c1ddd1d021edbc19fc6
4
- data.tar.gz: a881f09af89ed7e572cd3d89724d13b6b62ef180
3
+ metadata.gz: 119a637d018051dfe1517011f155c2d78155ccdc
4
+ data.tar.gz: eb1a100714ade7c9d2b8e15f1bd99b023fe07a1c
5
5
  SHA512:
6
- metadata.gz: 9df1a366cdae643e0ff01bc896146257c94335a4893e29ff7a30ab922b6e486c20be280de5b1e0d5718bcf4a6bfb43ce29140cc4fd6096e31a1935151ffc23f5
7
- data.tar.gz: 9be579f0737285e880be3e5ca4976c3ecca7f7deaeb8ef1ff99e4b7b26796e1fceb29bf2ec4402e3966c051007291d32ec45d313e1e2b2bf5a7dfebec1ea9c38
6
+ metadata.gz: 0a0e3a0ec214a2a081a3b3725e2271f9bd5e975195f3663c7015600f2647d406a60ffa4c0363bd60e6071a63077a5fbd9cbaec9365102dad49ba98711f267901
7
+ data.tar.gz: ea2def4856cc138ed661bc309cf48c64ab5820e111e9f60a5f5e89581610c60a7aa04d2a4786f438548f7e83df297587a3075abd085c45ad0829689b1e00bc12
data/.rubocop.yml CHANGED
@@ -32,3 +32,5 @@ Style/StructInheritance:
32
32
  Enabled: false
33
33
  Style/AccessorMethodName:
34
34
  Enabled: false
35
+ Lint/CircularArgumentReference:
36
+ Enabled: false # this does not work very well
data/.travis.yml CHANGED
@@ -4,10 +4,8 @@ cache:
4
4
  - bundler
5
5
  rvm:
6
6
  - 2.3.0
7
- - 2.2.4
8
- - 2.1.8
9
7
  script:
10
- - bundle exec rubocop -D
8
+ - bundle exec rake rubocop
11
9
  - bundle exec rake spec
12
10
  notifications:
13
11
  email: false
data/README.md CHANGED
@@ -1,8 +1,6 @@
1
1
  # Critic
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/critic`. To experiment with that code, run `bin/console` for an interactive prompt.
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
- TODO: Write usage instructions here
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
@@ -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
@@ -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.messages
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
- unless true == @_authorizing
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 ActiveSupport::Callbacks
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 = false
60
+ result = catch(:halt) { process_authorization(action, args) }
77
61
 
78
- begin
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
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module Critic
3
- VERSION = '0.1.1'
3
+ VERSION = '0.2.0'
4
4
  end
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::AuthorizationDenied = Class.new(StandardError)
12
- Critic::AuthorizationMissing = Class.new(StandardError)
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.1.1
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-07 00:00:00.000000000 Z
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