action_policy 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. checksums.yaml +5 -5
  2. data/.rubocop.yml +85 -0
  3. data/.travis.yml +25 -2
  4. data/CHANGELOG.md +7 -0
  5. data/Gemfile +12 -3
  6. data/README.md +71 -12
  7. data/Rakefile +9 -1
  8. data/action_policy.gemspec +11 -5
  9. data/docs/.nojekyll +0 -0
  10. data/docs/CNAME +1 -0
  11. data/docs/README.md +46 -0
  12. data/docs/_sidebar.md +19 -0
  13. data/docs/aliases.md +54 -0
  14. data/docs/assets/docsify.min.js +1 -0
  15. data/docs/assets/fonts/FiraCode-Medium.woff +0 -0
  16. data/docs/assets/fonts/FiraCode-Regular.woff +0 -0
  17. data/docs/assets/images/cache.png +0 -0
  18. data/docs/assets/images/cache.svg +70 -0
  19. data/docs/assets/images/layer.png +0 -0
  20. data/docs/assets/images/layer.svg +92 -0
  21. data/docs/assets/prism-ruby.min.js +1 -0
  22. data/docs/assets/styles.css +317 -0
  23. data/docs/assets/vue.min.css +1 -0
  24. data/docs/authorization_context.md +33 -0
  25. data/docs/caching.md +262 -0
  26. data/docs/custom_lookup_chain.md +48 -0
  27. data/docs/custom_policy.md +51 -0
  28. data/docs/favicon.ico +0 -0
  29. data/docs/i18n.md +3 -0
  30. data/docs/index.html +25 -0
  31. data/docs/instrumentation.md +3 -0
  32. data/docs/lookup_chain.md +16 -0
  33. data/docs/namespaces.md +69 -0
  34. data/docs/non_rails.md +29 -0
  35. data/docs/pre_checks.md +57 -0
  36. data/docs/quick_start.md +102 -0
  37. data/docs/rails.md +110 -0
  38. data/docs/reasons.md +67 -0
  39. data/docs/testing.md +116 -0
  40. data/docs/writing_policies.md +55 -0
  41. data/gemfiles/jruby.gemfile +5 -0
  42. data/gemfiles/rails42.gemfile +5 -0
  43. data/gemfiles/railsmaster.gemfile +6 -0
  44. data/lib/action_policy.rb +34 -2
  45. data/lib/action_policy/authorizer.rb +28 -0
  46. data/lib/action_policy/base.rb +24 -0
  47. data/lib/action_policy/behaviour.rb +94 -0
  48. data/lib/action_policy/behaviours/memoized.rb +56 -0
  49. data/lib/action_policy/behaviours/namespaced.rb +80 -0
  50. data/lib/action_policy/behaviours/policy_for.rb +23 -0
  51. data/lib/action_policy/behaviours/thread_memoized.rb +54 -0
  52. data/lib/action_policy/ext/module_namespace.rb +21 -0
  53. data/lib/action_policy/ext/policy_cache_key.rb +67 -0
  54. data/lib/action_policy/ext/string_constantize.rb +23 -0
  55. data/lib/action_policy/lookup_chain.rb +84 -0
  56. data/lib/action_policy/policy/aliases.rb +69 -0
  57. data/lib/action_policy/policy/authorization.rb +91 -0
  58. data/lib/action_policy/policy/cache.rb +74 -0
  59. data/lib/action_policy/policy/cached_apply.rb +28 -0
  60. data/lib/action_policy/policy/core.rb +64 -0
  61. data/lib/action_policy/policy/defaults.rb +37 -0
  62. data/lib/action_policy/policy/pre_check.rb +210 -0
  63. data/lib/action_policy/policy/reasons.rb +109 -0
  64. data/lib/action_policy/rails/channel.rb +15 -0
  65. data/lib/action_policy/rails/controller.rb +90 -0
  66. data/lib/action_policy/railtie.rb +74 -0
  67. data/lib/action_policy/rspec.rb +3 -0
  68. data/lib/action_policy/rspec/be_authorized_to.rb +93 -0
  69. data/lib/action_policy/rspec/pundit_syntax.rb +48 -0
  70. data/lib/action_policy/test_helper.rb +46 -0
  71. data/lib/action_policy/testing.rb +64 -0
  72. data/lib/action_policy/version.rb +3 -1
  73. metadata +115 -9
@@ -0,0 +1,69 @@
1
+ # Namespaces
2
+
3
+ Action Policy can lookup policies with respect to the current execution _namespace_ (i.e., authorization class module).
4
+
5
+ Consider an example:
6
+
7
+ ```ruby
8
+ module Admin
9
+ class UsersController < ApplictionController
10
+ def index
11
+ # uses Admin::UserPolicy if any, otherwise fallbacks to UserPolicy
12
+ authorize!
13
+ end
14
+ end
15
+ end
16
+ ```
17
+
18
+ Module nesting is also supported:
19
+
20
+ ```ruby
21
+ module Admin
22
+ module Client
23
+ class UsersController < ApplictionController
24
+ def index
25
+ # lookup for Admin::Client::UserPolicy -> Admin::UserPolicy -> UserPolicy
26
+ authorize!
27
+ end
28
+ end
29
+ end
30
+ end
31
+ ```
32
+
33
+ **NOTE**: to support namespaced lookup for non-inferrable resources,
34
+ you should specify `policy_name` at a class level (instead of `policy_class`, which doesn't take namespaces into account):
35
+
36
+ ```ruby
37
+ class Guest < User
38
+ def self.policy_name
39
+ "UserPolicy"
40
+ end
41
+ end
42
+ ```
43
+
44
+ **NOTE**: by default, we use class's name as a policy name; so, for namespaced resources, the namespace part is also included:
45
+
46
+ ```ruby
47
+ class Admin
48
+ class User
49
+ end
50
+ end
51
+
52
+ # search for Admin::UserPolicy, but not for UserPolicy
53
+ authorize! Admin::User.new
54
+ ```
55
+
56
+ You can access the current authorization namespace through `authorization_namespace` method.
57
+
58
+ You can also define your own namespacing logic by overriding `authorization_namespace`:
59
+
60
+ ```ruby
61
+ def authorization_namespace
62
+ return ::Admin if current_user.admin?
63
+ return ::Staff if current_user.staff?
64
+ # fallback to current namespace
65
+ super
66
+ end
67
+ ```
68
+
69
+ **NOTE**: namespace support is an extension for `ActionPolicy::Behaviour` and could be included with `ActionPolicy::Behaviours::Namespaced` (included into Rails controllers and channel integrations by default).
@@ -0,0 +1,29 @@
1
+ # Using with Ruby applications
2
+
3
+ Action Policy is designed to be independent of any framework and does not have specific dependencies on Ruby on Rails.
4
+ You can [write your policies](writing_policies.md) for non-Rails applications the same way as you would do for Rails applications.
5
+
6
+ In order to have `authorize!` / `allowed_to?` methods, you will have to include `ActionPolicy::Behaviour` into your class (where you want to perform authorization):
7
+
8
+ ```ruby
9
+ class PostUpdateAction
10
+ include ActionPolicy::Behaviour
11
+
12
+ # provide authorization subject (performer)
13
+ authorize :user
14
+
15
+ attr_reader :user
16
+
17
+ def initialize(user)
18
+ @user = user
19
+ end
20
+
21
+ def call(post, params)
22
+ authorize! post, to: :update?
23
+
24
+ post.update!(params)
25
+ end
26
+ end
27
+ ```
28
+
29
+ `ActionPolicy::Behaviour` provides `authorize` class-level method to configure [authorization context](authorization_context.rb) and two instance-level methods: `authorize!` and `allowed_to?`.
@@ -0,0 +1,57 @@
1
+ # Pre-Checks
2
+
3
+ Consider a typical situation when you start most—or even all—of your rules with the same predicates.
4
+
5
+ For example, when you have a super-user role in the application:
6
+
7
+ ```ruby
8
+ class PostPolicy < ApplicationPolicy
9
+ def show?
10
+ user.super_admin? || record.published
11
+ end
12
+
13
+ def update?
14
+ user.super_admin? || (user.id == record.user_id)
15
+ end
16
+
17
+ # more rules
18
+ end
19
+ ```
20
+
21
+ Action Policy allows you to extract the common parts from rules into _pre-checks_:
22
+
23
+ ```ruby
24
+ class PostPolicy < ApplicationPolicy
25
+ pre_check :allow_admins
26
+
27
+ def show?
28
+ record.published
29
+ end
30
+
31
+ def update?
32
+ user.id == record.user_id
33
+ end
34
+
35
+ private
36
+
37
+ def allow_admins
38
+ allow! if user.super_admin?
39
+ end
40
+ end
41
+ ```
42
+
43
+ Pre-checks act like _callbacks_: you can add multiple pre-checks, specify `except` and `only` options, and skip already defined pre-checks if necessary:
44
+
45
+ ```ruby
46
+ class UserPolicy < ApplicationPolicy
47
+ skip_pre_check :allow_admins, only: :destroy?
48
+
49
+ def destroy?
50
+ user.admin? && !record.admin?
51
+ end
52
+ end
53
+ ```
54
+
55
+ To halt the authorization process within a pre-check, you must return either `allow!` or `deny!` call value. When any other value is returned, the pre-check is ignored, and the rule is called (or next pre-check).
56
+
57
+ **NOTE**: pre-checks are available only if you inherit from `ActionPolicy::Base` or include `ActionPolicy::Policy::PreCheck` into your `ApplicationPolicy`.
@@ -0,0 +1,102 @@
1
+ # Quick Start
2
+
3
+ ## Installation
4
+
5
+ To install Action Policy with RubyGems:
6
+
7
+ ```ruby
8
+ gem install action_policy
9
+ ```
10
+
11
+ Or add this line to your application's `Gemfile`:
12
+
13
+ ```ruby
14
+ gem "action_policy"
15
+ ```
16
+
17
+ And then execute:
18
+
19
+ $ bundle
20
+
21
+ ## Basic usage
22
+
23
+ The core component of Action Policy is a _policy class_. Policy class describes how you control access to resources.
24
+
25
+ We suggest that you have a separate policy class for each resource and encourage you to follow the conventions:
26
+ - put policies into the `app/policies` folder (when using with Rails);
27
+ - name policies using the corresponding resource name (model name) with a `Policy` suffix, e.g. `Post -> PostPolicy`;
28
+ - name rules using a predicate form of the corresponding activity (typically, a controller's action), e.g. `PostsController#update -> PostsPolicy#update?`.
29
+
30
+ We also recommend to use an application-specific `ApplicationPolicy` with a global configuration to inherit from:
31
+
32
+ ```ruby
33
+ class ApplicationPolicy < ActionPolicy::Base
34
+ end
35
+ ```
36
+
37
+ **NOTE:** it is not necessary to inherit from `ActionPolicy::Base`; instead, you can [construct basic policy](custom_policy.md) choosing only the components you need.
38
+
39
+ Consider a simple example:
40
+
41
+ ```ruby
42
+ class PostPolicy < ApplicationPolicy
43
+ # everyone can see any post
44
+ def show?
45
+ true
46
+ end
47
+
48
+ def update?
49
+ # `user` is a performing subject,
50
+ # `record` is a target object (post we want to update)
51
+ user.admin? || (user.id == record.user_id)
52
+ end
53
+ end
54
+ ```
55
+
56
+ Now you can easily add authorization to your Rails\* controller:
57
+
58
+ ```ruby
59
+ class PostsController < ApplicationController
60
+ def update
61
+ @post = Post.find(params[:id])
62
+ authorize! @post
63
+
64
+ if @post.update(post_params)
65
+ redirect_to @post
66
+ else
67
+ render :edit
68
+ end
69
+ end
70
+ end
71
+ ```
72
+
73
+ \* See [Non-Rails Usage](non_rails.md) on how to add `authorize!` to any Ruby project.
74
+
75
+ In the above case, Action Policy automatically infers a policy class and a rule to verify access: `@post -> Post -> PostPolicy`, rule is inferred from the action name (`update -> update?`), and `current_user` is used as `user` within the policy by default (read more about [authorization context](authorization_context.md)).
76
+
77
+ When authorization is successful (i.e., the corresponding rule returns `true`), nothing happens, but in case of an authorization failure `ActionPolicy::Unauthorized` error is raised.
78
+
79
+ There is also an `allowed_to?` method which returns `true` or `false` and could be used, for example, in views:
80
+
81
+ ```erb
82
+ <% @posts.each do |post| %>
83
+ <li><%= post.title %>
84
+ <% if allowed_to?(:edit?, post) %>
85
+ = link_to "Edit", post
86
+ <% end %>
87
+ </li>
88
+ <% end %>
89
+ ```
90
+
91
+ Although Action Policy tries to [infer the corresponding policy class](policy_lookup.md) and rule itself, there could be a situation when you want to specify those values explicitly:
92
+
93
+ ```ruby
94
+ # specify the rule to verify access
95
+ authorize! @post, to: :update?
96
+
97
+ # specify policy class
98
+ authorize! @post, with: CustomPostPolicy
99
+
100
+ # or
101
+ allowed_to? :edit?, @post, with: CustomPostPolicy
102
+ ```
@@ -0,0 +1,110 @@
1
+ # Using with Rails
2
+
3
+ Action Policy seamlessly integrates Ruby on Rails applications seamlessly.
4
+
5
+ In most cases, you do not have to do anything except writing policy files and adding `authorize!` calls.
6
+
7
+ ## Controllers integration
8
+
9
+ Action Policy assumes that you have a `current_user` method which specifies the current authenticated subject (`user`).
10
+
11
+ You can turn off this behaviour by setting `config.action_policy.controller_authorize_current_user = false` in `application.rb`, or override it:
12
+
13
+ ```ruby
14
+ class ApplicationController < ActionController::Base
15
+ authorize :my_current_user, as: :user
16
+ end
17
+ ```
18
+
19
+ > Read more about [authorization context](authorization_context.md).
20
+
21
+ In case you don't want to include Action Policy to controllers at all,
22
+ you can turn disable the integration by setting `config.action_policy.auto_inject_into_controller = false` in `application.rb`.
23
+
24
+ ### `verify_authorized` hooks
25
+
26
+ Usually, you need all of your actions to be authorized. Action Policy provides a controller hook which ensures that an `authorize!` call has been made during the action:
27
+
28
+ ```ruby
29
+ class ApplicationController < ActionController::Base
30
+ # adds an after_action callback to verify
31
+ # that `authorize!` has been called.
32
+ verify_authorized
33
+
34
+ # you can also pass additional options,
35
+ # like with a usual callback
36
+ verify_authorized except: :index
37
+ end
38
+ ```
39
+
40
+ You can skip this check when necessary:
41
+
42
+ ```ruby
43
+ class PostsController < ApplicationController
44
+ skip_verify_authorized only: :show
45
+ end
46
+ ```
47
+
48
+ When an unauthorized action is encountered, the `ActionPolicy::UnauthorizedAction` error is raised.
49
+
50
+ ### Resource-less `authorize!`
51
+
52
+ You can also call `authorize!` without a resource specified.
53
+ In that case, Action Policy tries to infer the resource class from the controller name:
54
+
55
+ ```ruby
56
+ class PostsController < ApplicationPolicy
57
+ def index
58
+ # Uses Post class as a resource implicitly.
59
+ # NOTE: it just calls `controller_name.classify.safe_constantize`
60
+ authorize!
61
+ end
62
+ end
63
+ ```
64
+
65
+ ### Usage with `API` and `Metal` controllers
66
+
67
+ Action Policy is only included into `ActionController::Base`. If you want to use it with other base Rails controllers, you have to include it manually:
68
+
69
+ ```ruby
70
+ class ApiController < ApplicationController::API
71
+ include ActionPolicy::Controller
72
+
73
+ # NOTE: you have to provide authorization context manually as well
74
+ authorize :current_user, as: :user
75
+ end
76
+ ```
77
+
78
+ ## Channels integration
79
+
80
+ Action Policy also integrates with Action Cable to help you authorize your channels actions:
81
+
82
+ ```ruby
83
+ class ChatChannel < ApplicationCable::Channel
84
+ def follow(data)
85
+ chat = Chat.find(data["chat_id"])
86
+
87
+ # Verify against ChatPolicy#show? rule
88
+ authorize! chat, to: :show?
89
+ stream_from chat
90
+ end
91
+ end
92
+ ```
93
+
94
+ Action Policy assumes that you have `current_user` as a connection identifier.
95
+
96
+ You can turn off this behaviour by setting `config.action_policy.channel_authorize_current_user = false` in `application.rb`, or override it:
97
+
98
+ ```ruby
99
+ module ApplicationCable
100
+ class Channel < ActionCable::Channel::Base
101
+ # assuming that identifier is called `user`
102
+ authorize :user
103
+ end
104
+ end
105
+ ```
106
+
107
+ > Read more about [authorization context](authorization_context.md).
108
+
109
+ In case you do not want to include Action Policy to channels at all,
110
+ you can disable the integration by setting `config.action_policy.auto_inject_into_channel = false` in `application.rb`.
@@ -0,0 +1,67 @@
1
+ # Failure Reasons
2
+
3
+ When you have complex policy rules, it could be helpful to have an ability to define an exact reason for why a specific authorization was rejected.
4
+
5
+ It is especially helpful when you compose policies (i.e., use one policy within another).
6
+
7
+ Action Policy allows you to track failed `allowed_to?` checks in your rules.
8
+
9
+ Consider an example:
10
+
11
+ ```ruby
12
+ class ApplicantPolicy < ApplicationPolicy
13
+ def show?
14
+ user.has_permission?(:view_applicants) &&
15
+ allowed_to?(:show?, object.stage)
16
+ end
17
+ end
18
+ ```
19
+
20
+ When `ApplicantPolicy#show?` check fails, the exception has the `reasons` object, which contains additional information about the failure:
21
+
22
+ ```ruby
23
+ class ApplicationController < ActionController::Base
24
+ rescue_from ActionPolicy::Unauthorized do |ex|
25
+ p ex.reasons.messages #=> { stage: [:show?] }
26
+ end
27
+ end
28
+ ```
29
+
30
+ You can also wrap _local_ rules into `allowed_to?` to populate reasons:
31
+
32
+ ```ruby
33
+ class ApplicantPolicy < ApplicationPolicy
34
+ def show?
35
+ allowed_to?(:view_applicants?) &&
36
+ allowed_to?(:show?, object.stage)
37
+ end
38
+
39
+ def view_applicants?
40
+ user.has_permission?(:view_applicants)
41
+ end
42
+ end
43
+
44
+ # then the reasons object could be
45
+ p ex.reasons.messages #=> { applicant: [:view_applicants?] }
46
+
47
+ # or
48
+ p ex.reasons.messages #=> { stage: [:show?] }
49
+ ```
50
+
51
+ **What is the point of failure reasons?**
52
+
53
+ First, you can provide a user with helpful feedback. For example, in the above scenario, when the reason is `ApplicantPolicy#view_applicants?`, you could show the following message:
54
+
55
+ ```
56
+ You don't have enough permissions to view applicants.
57
+ Please, ask your manager to update your role.
58
+ ```
59
+
60
+ And when the reason is `StagePolicy#show?`:
61
+
62
+ ```
63
+ You don't have access to the stage XYZ.
64
+ Please, ask your manager to grant access to this stage.
65
+ ```
66
+
67
+ Much more useful than just showing "You are not authorized to perform this action," isn't it?
@@ -0,0 +1,116 @@
1
+ # Testing
2
+
3
+ Authorization is one of the crucial parts of your application. Hence, it should be thoroughly tested (that is the place where 100% coverage makes sense).
4
+
5
+ When you use policies for authorization, it is possible to split testing into two parts:
6
+ - Test that **the required authorization is performed** within your authorization layer (controller, channel, etc.);
7
+ - Test the policy class itself.
8
+
9
+ ## Testing authorization
10
+
11
+ To test the act of authorization you have to make sure that the `authorize!` method is called with the appropriate arguments.
12
+
13
+ Action Policy provides tools for such kind of testing for Minitest and RSpec.
14
+
15
+ ### Minitest
16
+
17
+ Include `ActionPolicy::TestHelper` to your test class and you'll be able to use
18
+ `assert_authorized_to` assertion:
19
+
20
+ ```ruby
21
+ # in your controller
22
+ class PostsController < ApplicationController
23
+ def update
24
+ @post = Post.find(params[:id])
25
+ authorize! @post
26
+ if @post.update(post_params)
27
+ redirect_to @post
28
+ else
29
+ render :edit
30
+ end
31
+ end
32
+ end
33
+
34
+ # in your test
35
+ require "action_policy/test_helper"
36
+
37
+ class PostsControllerTest < ActionDispatch::IntegrationTest
38
+ include ActionPolicy::TestHelper
39
+
40
+ test "update is authorized" do
41
+ sign_in users(:john)
42
+
43
+ post = posts(:example)
44
+
45
+ assert_authorized_to(:update?, post, with: PostPolicy) do
46
+ patch :update, id: post.id, name: "Bob"
47
+ end
48
+ end
49
+ end
50
+ ```
51
+
52
+ You can omit the policy (then it would be inferred from the target):
53
+
54
+ ```ruby
55
+ assert_authorized_to(:update?, post) do
56
+ patch :update, id: post.id, name: "Bob"
57
+ end
58
+ ```
59
+
60
+ ### RSpec
61
+
62
+ Add the following to your `rails_helper.rb` (or `spec_helper.rb`):
63
+
64
+ ```ruby
65
+ require "action_policy/rspec"
66
+ ```
67
+
68
+ Now you can use `be_authorized_to` matcher:
69
+
70
+ ```ruby
71
+ describe PostsController do
72
+ subject { patch :update, id: post.id, params: params }
73
+
74
+ it "is authorized" do
75
+ expect { subject }.to be_authorized_to(:update?, post)
76
+ .with(PostPolicy)
77
+ end
78
+ end
79
+ ```
80
+
81
+ If you omit `.with(PostPolicy)` then the inferred policy for the target (`post`) would be used.
82
+
83
+ ## Testing policies
84
+
85
+ You can test policies as plain-old Ruby classes, no special tooling is required.
86
+
87
+ Consider an RSpec example:
88
+
89
+ ```ruby
90
+ describe PostPolicy do
91
+ let(:user) { build_stubbed(:user) }
92
+ let(:post) { build_stubbed(:post) }
93
+
94
+ let(:policy) { described_class.new(post, user: user) }
95
+
96
+ describe "#update?" do
97
+ subject { policy.update? }
98
+
99
+ it "returns false when the user is not admin nor author" do
100
+ is_expected.to eq false
101
+ end
102
+
103
+ context "when the user is admin" do
104
+ let(:user) { build_stubbed(:user, :admin) }
105
+
106
+ it { is_expected.to eq true }
107
+ end
108
+
109
+ context "when the user is an author" do
110
+ let(:post) { build_stubbed(:post, user: user) }
111
+
112
+ it { is_expected.to eq true }
113
+ end
114
+ end
115
+ end
116
+ ```