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 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