action_policy 0.0.1 → 0.1.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.
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
+ ```