action_policy 0.4.4 → 0.5.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +211 -175
- data/README.md +5 -4
- data/config/rubocop-rspec.yml +17 -0
- data/lib/.rbnext/2.7/action_policy/behaviours/policy_for.rb +62 -0
- data/lib/.rbnext/2.7/action_policy/i18n.rb +56 -0
- data/lib/.rbnext/2.7/action_policy/policy/cache.rb +101 -0
- data/lib/.rbnext/2.7/action_policy/policy/pre_check.rb +162 -0
- data/lib/.rbnext/2.7/action_policy/rspec/be_authorized_to.rb +89 -0
- data/lib/.rbnext/2.7/action_policy/rspec/have_authorized_scope.rb +124 -0
- data/lib/.rbnext/2.7/action_policy/utils/pretty_print.rb +159 -0
- data/lib/.rbnext/3.0/action_policy/behaviour.rb +115 -0
- data/lib/.rbnext/3.0/action_policy/behaviours/policy_for.rb +62 -0
- data/lib/.rbnext/3.0/action_policy/behaviours/scoping.rb +35 -0
- data/lib/.rbnext/3.0/action_policy/behaviours/thread_memoized.rb +59 -0
- data/lib/.rbnext/3.0/action_policy/ext/policy_cache_key.rb +72 -0
- data/lib/.rbnext/3.0/action_policy/policy/aliases.rb +69 -0
- data/lib/.rbnext/3.0/action_policy/policy/authorization.rb +87 -0
- data/lib/.rbnext/3.0/action_policy/policy/cache.rb +101 -0
- data/lib/.rbnext/3.0/action_policy/policy/core.rb +161 -0
- data/lib/.rbnext/3.0/action_policy/policy/defaults.rb +31 -0
- data/lib/.rbnext/3.0/action_policy/policy/execution_result.rb +37 -0
- data/lib/.rbnext/3.0/action_policy/policy/pre_check.rb +162 -0
- data/lib/.rbnext/3.0/action_policy/policy/reasons.rb +212 -0
- data/lib/.rbnext/3.0/action_policy/policy/scoping.rb +160 -0
- data/lib/.rbnext/3.0/action_policy/rspec/be_authorized_to.rb +89 -0
- data/lib/.rbnext/3.0/action_policy/rspec/have_authorized_scope.rb +124 -0
- data/lib/.rbnext/3.0/action_policy/utils/pretty_print.rb +159 -0
- data/lib/.rbnext/3.0/action_policy/utils/suggest_message.rb +19 -0
- data/lib/action_policy.rb +7 -1
- data/lib/action_policy/behaviour.rb +22 -16
- data/lib/action_policy/behaviours/policy_for.rb +10 -3
- data/lib/action_policy/behaviours/scoping.rb +2 -1
- data/lib/action_policy/behaviours/thread_memoized.rb +1 -3
- data/lib/action_policy/ext/module_namespace.rb +1 -6
- data/lib/action_policy/ext/policy_cache_key.rb +10 -30
- data/lib/action_policy/i18n.rb +1 -1
- data/lib/action_policy/lookup_chain.rb +26 -28
- data/lib/action_policy/policy/aliases.rb +7 -12
- data/lib/action_policy/policy/authorization.rb +8 -7
- data/lib/action_policy/policy/cache.rb +11 -17
- data/lib/action_policy/policy/core.rb +25 -12
- data/lib/action_policy/policy/defaults.rb +3 -9
- data/lib/action_policy/policy/execution_result.rb +3 -9
- data/lib/action_policy/policy/pre_check.rb +19 -58
- data/lib/action_policy/policy/reasons.rb +31 -19
- data/lib/action_policy/policy/scoping.rb +5 -6
- data/lib/action_policy/rails/controller.rb +6 -1
- data/lib/action_policy/rails/policy/instrumentation.rb +1 -1
- data/lib/action_policy/rspec/be_authorized_to.rb +5 -9
- data/lib/action_policy/rspec/dsl.rb +1 -1
- data/lib/action_policy/rspec/have_authorized_scope.rb +5 -7
- data/lib/action_policy/utils/pretty_print.rb +21 -24
- data/lib/action_policy/utils/suggest_message.rb +1 -3
- data/lib/action_policy/version.rb +1 -1
- data/lib/generators/action_policy/install/templates/{application_policy.rb → application_policy.rb.tt} +0 -0
- data/lib/generators/action_policy/policy/policy_generator.rb +4 -1
- data/lib/generators/action_policy/policy/templates/{policy.rb → policy.rb.tt} +0 -0
- data/lib/generators/rspec/templates/{policy_spec.rb → policy_spec.rb.tt} +0 -0
- data/lib/generators/test_unit/templates/{policy_test.rb → policy_test.rb.tt} +0 -0
- metadata +55 -119
- data/.gitattributes +0 -2
- data/.github/ISSUE_TEMPLATE.md +0 -21
- data/.github/PULL_REQUEST_TEMPLATE.md +0 -29
- data/.github/bug_report_template.rb +0 -175
- data/.gitignore +0 -15
- data/.rubocop.yml +0 -54
- data/.tidelift.yml +0 -6
- data/.travis.yml +0 -31
- data/Gemfile +0 -22
- data/Rakefile +0 -27
- data/action_policy.gemspec +0 -44
- data/benchmarks/namespaced_lookup_cache.rb +0 -74
- data/benchmarks/pre_checks.rb +0 -73
- data/bin/console +0 -14
- data/bin/setup +0 -8
- data/docs/.nojekyll +0 -0
- data/docs/CNAME +0 -1
- data/docs/README.md +0 -79
- data/docs/_sidebar.md +0 -27
- data/docs/aliases.md +0 -122
- data/docs/assets/docsify-search.js +0 -364
- data/docs/assets/docsify.min.js +0 -3
- data/docs/assets/fonts/FiraCode-Medium.woff +0 -0
- data/docs/assets/fonts/FiraCode-Regular.woff +0 -0
- data/docs/assets/images/banner.png +0 -0
- data/docs/assets/images/cache.png +0 -0
- data/docs/assets/images/cache.svg +0 -70
- data/docs/assets/images/layer.png +0 -0
- data/docs/assets/images/layer.svg +0 -35
- data/docs/assets/prism-ruby.min.js +0 -1
- data/docs/assets/styles.css +0 -347
- data/docs/assets/vue.min.css +0 -1
- data/docs/authorization_context.md +0 -92
- data/docs/behaviour.md +0 -113
- data/docs/caching.md +0 -291
- data/docs/controller_action_aliases.md +0 -109
- data/docs/custom_lookup_chain.md +0 -48
- data/docs/custom_policy.md +0 -53
- data/docs/debugging.md +0 -55
- data/docs/decorators.md +0 -27
- data/docs/favicon.ico +0 -0
- data/docs/graphql.md +0 -302
- data/docs/i18n.md +0 -44
- data/docs/index.html +0 -43
- data/docs/instrumentation.md +0 -84
- data/docs/lookup_chain.md +0 -22
- data/docs/namespaces.md +0 -77
- data/docs/non_rails.md +0 -28
- data/docs/pre_checks.md +0 -57
- data/docs/pundit_migration.md +0 -80
- data/docs/quick_start.md +0 -118
- data/docs/rails.md +0 -120
- data/docs/reasons.md +0 -120
- data/docs/scoping.md +0 -255
- data/docs/testing.md +0 -390
- data/docs/writing_policies.md +0 -107
- data/gemfiles/jruby.gemfile +0 -8
- data/gemfiles/rails42.gemfile +0 -9
- data/gemfiles/rails6.gemfile +0 -8
- data/gemfiles/railsmaster.gemfile +0 -6
- data/lib/action_policy/ext/string_match.rb +0 -14
- data/lib/action_policy/ext/yield_self_then.rb +0 -25
@@ -1,109 +0,0 @@
|
|
1
|
-
# Controller Action Aliases
|
2
|
-
|
3
|
-
**This is a feature proposed here: https://github.com/palkan/action_policy/issues/25**
|
4
|
-
|
5
|
-
If you'd like to see this feature implemented, please comment on the issue to show your support.
|
6
|
-
|
7
|
-
## Outline
|
8
|
-
|
9
|
-
Say you have abstracted your `authorize!` call to a controller superclass because your policy can
|
10
|
-
be executed without regard to the record in any of the subclass controllers:
|
11
|
-
|
12
|
-
```ruby
|
13
|
-
class AbstractController < ApplicationController
|
14
|
-
authorize :context
|
15
|
-
before_action :authorize_context
|
16
|
-
|
17
|
-
def context
|
18
|
-
# Some code to get your policy context
|
19
|
-
end
|
20
|
-
|
21
|
-
private
|
22
|
-
|
23
|
-
def authorize_context
|
24
|
-
authorize! Context
|
25
|
-
end
|
26
|
-
end
|
27
|
-
```
|
28
|
-
|
29
|
-
Your policy might look like this:
|
30
|
-
|
31
|
-
```ruby
|
32
|
-
class ContextPolicy < ApplicationPolicy
|
33
|
-
authorize :context
|
34
|
-
|
35
|
-
alias_rule :index?, :show?, to: :view?
|
36
|
-
alias_rule :new?, :create?, :update?, :destroy?, to: :edit?
|
37
|
-
|
38
|
-
def view?
|
39
|
-
context.has_permission_to(:view, user)
|
40
|
-
end
|
41
|
-
|
42
|
-
def edit?
|
43
|
-
context.has_permission_to(:edit, user)
|
44
|
-
end
|
45
|
-
end
|
46
|
-
```
|
47
|
-
|
48
|
-
We can safely add aliases for the common REST actions in the policy.
|
49
|
-
|
50
|
-
You may then want to include a concern in your subclass controller(s) that add extra actions to the controller.
|
51
|
-
|
52
|
-
|
53
|
-
```ruby
|
54
|
-
class ConcreteController < AbstractController
|
55
|
-
include AdditionalFunctionalityConcern
|
56
|
-
|
57
|
-
def index
|
58
|
-
# Index Action
|
59
|
-
end
|
60
|
-
|
61
|
-
def new
|
62
|
-
# New Action
|
63
|
-
end
|
64
|
-
|
65
|
-
# etc...
|
66
|
-
end
|
67
|
-
```
|
68
|
-
|
69
|
-
At this point you may be wondering how to tell your abstracted policy that these new methods map to either
|
70
|
-
the `view?` or `edit?` rule. You can currently provide the rule to execute to the `authorize!` method with
|
71
|
-
the `to:` parameter but since our call to `authorize!` is in a superclass it has no idea about our concern.
|
72
|
-
I propose the following controller method:
|
73
|
-
|
74
|
-
```ruby
|
75
|
-
alias_action(*actions, to_rule: rule)
|
76
|
-
```
|
77
|
-
|
78
|
-
Here's an example:
|
79
|
-
|
80
|
-
```ruby
|
81
|
-
module AdditionalFunctionalityConcern
|
82
|
-
extend ActiveSupport::Concern
|
83
|
-
|
84
|
-
included do
|
85
|
-
alias_action [:first_action, :second_action], to_rule: :view?
|
86
|
-
alias_action [:third_action], to_rule: :edit?
|
87
|
-
end
|
88
|
-
|
89
|
-
def first_action
|
90
|
-
# First Action
|
91
|
-
end
|
92
|
-
|
93
|
-
def second_action
|
94
|
-
# Second Action
|
95
|
-
end
|
96
|
-
|
97
|
-
def third_action
|
98
|
-
# Third Action
|
99
|
-
end
|
100
|
-
end
|
101
|
-
```
|
102
|
-
|
103
|
-
When `authorize!` is called in a controller, it will first check the action aliases for a corresponding
|
104
|
-
rule. If one is found, it will execute that rule instead of a rule matching the name of the current action.
|
105
|
-
The rule may point at a concrete rule in the policy, or a rule alias in the policy, it doens't matter, the
|
106
|
-
alias in the policy will be resolved like normal.
|
107
|
-
|
108
|
-
If you'd like to see this feature implemented, please show your support on the
|
109
|
-
[Github Issue](https://github.com/palkan/action_policy/issues/25).
|
data/docs/custom_lookup_chain.md
DELETED
@@ -1,48 +0,0 @@
|
|
1
|
-
# Custom Lookup Chain
|
2
|
-
|
3
|
-
Action Policy's lookup chain is just an array of _probes_ (lambdas with a specific interface).
|
4
|
-
|
5
|
-
The lookup process itself is pretty simple:
|
6
|
-
- Call the first probe;
|
7
|
-
- Return the result if it is not `nil`;
|
8
|
-
- Go to the next probe.
|
9
|
-
|
10
|
-
You can override the default chain with your own. For example:
|
11
|
-
|
12
|
-
```ruby
|
13
|
-
ActionPolicy::LookupChain.chain = [
|
14
|
-
# Probe accepts record as the first argument
|
15
|
-
# and arbitrary options (passed to `authorize!` / `allowed_to?` call)
|
16
|
-
lambda do |record, **options|
|
17
|
-
# your custom lookup logic
|
18
|
-
end
|
19
|
-
]
|
20
|
-
```
|
21
|
-
|
22
|
-
## NullPolicy example
|
23
|
-
|
24
|
-
Let's consider a simple example of extending the existing lookup chain with one more probe.
|
25
|
-
|
26
|
-
Suppose that we want to have a fallback policy (policy used when none found for the resource) instead of raising an `ActionPolicy::NotFound` error.
|
27
|
-
|
28
|
-
Let's call this policy a `NullPolicy`:
|
29
|
-
|
30
|
-
```ruby
|
31
|
-
class NullPolicy < ActionPolicy::Base
|
32
|
-
default_rule :any?
|
33
|
-
|
34
|
-
def any?
|
35
|
-
false
|
36
|
-
end
|
37
|
-
end
|
38
|
-
```
|
39
|
-
|
40
|
-
Here we use the [default rule](aliases.md#default-rule) to handle any rule applied.
|
41
|
-
|
42
|
-
Now we need to add a simple probe to the end of our lookup chain:
|
43
|
-
|
44
|
-
```ruby
|
45
|
-
ActionPolicy::LookupChain.chain << ->(_, _) { NullPolicy }
|
46
|
-
```
|
47
|
-
|
48
|
-
That's it!
|
data/docs/custom_policy.md
DELETED
@@ -1,53 +0,0 @@
|
|
1
|
-
# Custom Base Policy
|
2
|
-
|
3
|
-
`ActionPolicy::Base` is a combination of all available policy extensions with the default configuration.
|
4
|
-
|
5
|
-
It looks like this:
|
6
|
-
|
7
|
-
|
8
|
-
```ruby
|
9
|
-
class ActionPolicy::Base
|
10
|
-
include ActionPolicy::Policy::Core
|
11
|
-
include ActionPolicy::Policy::Authorization
|
12
|
-
include ActionPolicy::Policy::PreCheck
|
13
|
-
include ActionPolicy::Policy::Reasons
|
14
|
-
include ActionPolicy::Policy::Aliases
|
15
|
-
include ActionPolicy::Policy::Scoping
|
16
|
-
include ActionPolicy::Policy::Cache
|
17
|
-
include ActionPolicy::Policy::CachedApply
|
18
|
-
include ActionPolicy::Policy::Defaults
|
19
|
-
|
20
|
-
# ActionPolicy::Policy::Defaults module adds the following
|
21
|
-
|
22
|
-
authorize :user
|
23
|
-
|
24
|
-
default_rule :manage?
|
25
|
-
alias_rule :new?, to: :create?
|
26
|
-
|
27
|
-
def index?
|
28
|
-
false
|
29
|
-
end
|
30
|
-
|
31
|
-
def create?
|
32
|
-
false
|
33
|
-
end
|
34
|
-
|
35
|
-
def manage?
|
36
|
-
false
|
37
|
-
end
|
38
|
-
end
|
39
|
-
```
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
You can write your `ApplicationPolicy` from scratch instead of inheriting from `ActionPolicy::Base`
|
44
|
-
if the defaults above do not fit your needs. The only required component is `ActionPolicy::Policy::Core`:
|
45
|
-
|
46
|
-
```ruby
|
47
|
-
# minimal ApplicationPolicy
|
48
|
-
class ApplicationPolicy
|
49
|
-
include ActionPolicy::Policy::Core
|
50
|
-
end
|
51
|
-
```
|
52
|
-
|
53
|
-
The `Core` module provides `apply` and `allowed_to?` methods.
|
data/docs/debugging.md
DELETED
@@ -1,55 +0,0 @@
|
|
1
|
-
# Debug Helpers
|
2
|
-
|
3
|
-
**NOTE:** this functionality requires two additional gems to be available in the app:
|
4
|
-
- [unparser](https://github.com/mbj/unparser)
|
5
|
-
- [method_source](https://github.com/banister/method_source).
|
6
|
-
|
7
|
-
We usually describe policy rules using _boolean expressions_ (e.g. `A or (B and C)` where each of `A`, `B` and `C` is a simple boolean expression or predicate method).
|
8
|
-
|
9
|
-
When dealing with complex policies, it could be hard to figure out which predicate/check made policy to fail.
|
10
|
-
|
11
|
-
The `Policy#pp(rule)` method aims to help debug such situations.
|
12
|
-
|
13
|
-
Consider a (synthetic) example:
|
14
|
-
|
15
|
-
```ruby
|
16
|
-
def feed?
|
17
|
-
(admin? || allowed_to?(:access_feed?)) &&
|
18
|
-
(user.name == "Jack" || user.name == "Kate")
|
19
|
-
end
|
20
|
-
|
21
|
-
```
|
22
|
-
|
23
|
-
Suppose that you want to debug this rule ("Why does it return false?").
|
24
|
-
You can drop a [`binding.pry`](https://github.com/deivid-rodriguez/pry-byebug) (or `binding.irb`) right at the beginning of the method:
|
25
|
-
|
26
|
-
```ruby
|
27
|
-
def feed?
|
28
|
-
binding.pry # rubocop:disable Lint/Debugger
|
29
|
-
#...
|
30
|
-
end
|
31
|
-
```
|
32
|
-
|
33
|
-
Now, run your code and trigger the breakpoint (i.e., run the method):
|
34
|
-
|
35
|
-
```
|
36
|
-
# now you can preview the execution of the rule using the `pp` method (defined on the policy instance)
|
37
|
-
pry> pp :feed?
|
38
|
-
MyPolicy#feed?
|
39
|
-
↳ (
|
40
|
-
admin? #=> false
|
41
|
-
OR
|
42
|
-
allowed_to?(:access_feed?) #=> true
|
43
|
-
)
|
44
|
-
AND
|
45
|
-
(
|
46
|
-
user.name == "Jack" #=> false
|
47
|
-
OR
|
48
|
-
user.name == "Kate" #=> true
|
49
|
-
)
|
50
|
-
|
51
|
-
# you can also check other rules or methods as well
|
52
|
-
pry> pp :admin?
|
53
|
-
MyPolicy#admin?
|
54
|
-
↳ user.admin? #=> false
|
55
|
-
```
|
data/docs/decorators.md
DELETED
@@ -1,27 +0,0 @@
|
|
1
|
-
# Dealing with Decorators
|
2
|
-
|
3
|
-
Ref: [action_policy#7](https://github.com/palkan/action_policy/issues/7).
|
4
|
-
|
5
|
-
Since Action Policy [lookup mechanism](./lookup_chain.md) relies on the target
|
6
|
-
record's class properties (names, methods) it could break when using with _decorators_.
|
7
|
-
|
8
|
-
To make `authorize!` and other [behaviour](./behaviour.md) methods work seamlessly with decorated
|
9
|
-
objects, you might want to _enhance_ the `policy_for` method.
|
10
|
-
|
11
|
-
For example, when using the [Draper](https://github.com/drapergem/draper) gem:
|
12
|
-
|
13
|
-
```ruby
|
14
|
-
module ActionPolicy
|
15
|
-
module Draper
|
16
|
-
def policy_for(record:, **opts)
|
17
|
-
# From https://github.com/GoodMeasuresLLC/draper-cancancan/blob/master/lib/draper/cancancan.rb
|
18
|
-
record = record.model while record.is_a?(::Draper::Decorator)
|
19
|
-
super(record: record, **opts)
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
|
-
class ApplicationController < ActionController::Base
|
25
|
-
prepend ActionPolicy::Draper
|
26
|
-
end
|
27
|
-
```
|
data/docs/favicon.ico
DELETED
Binary file
|
data/docs/graphql.md
DELETED
@@ -1,302 +0,0 @@
|
|
1
|
-
# GraphQL integration
|
2
|
-
|
3
|
-
You can use Action Policy as an authorization library for your [GraphQL Ruby](https://graphql-ruby.org/) application via the [`action_policy-graphql` gem](https://github.com/palkan/action_policy-graphql).
|
4
|
-
|
5
|
-
This integration provides the following features:
|
6
|
-
- Fields & mutations authorization
|
7
|
-
- List and connections scoping
|
8
|
-
- [**Exposing permissions/authorization rules in the API**](https://evilmartians.com/chronicles/exposing-permissions-in-graphql-apis-with-action-policy).
|
9
|
-
|
10
|
-
## Getting Started
|
11
|
-
|
12
|
-
First, add the `action_policy-graphql` gem to your Gemfile (see [installation instructions](https://github.com/palkan/action_policy-graphql#installation)).
|
13
|
-
|
14
|
-
Then, include `ActionPolicy::GraphQL::Behaviour` to your base type (or any other type/mutation where you want to use authorization features):
|
15
|
-
|
16
|
-
```ruby
|
17
|
-
# For fields authorization, lists scoping and rules exposing
|
18
|
-
class Types::BaseObject < GraphQL::Schema::Object
|
19
|
-
include ActionPolicy::GraphQL::Behaviour
|
20
|
-
end
|
21
|
-
|
22
|
-
# For using authorization helpers in mutations
|
23
|
-
class Types::BaseMutation < GraphQL::Schema::Mutation
|
24
|
-
include ActionPolicy::GraphQL::Behaviour
|
25
|
-
end
|
26
|
-
|
27
|
-
# For using authorization helpers in resolvers
|
28
|
-
class Types::BaseResolver < GraphQL::Schema::Resolver
|
29
|
-
include ActionPolicy::GraphQL::Behaviour
|
30
|
-
end
|
31
|
-
```
|
32
|
-
|
33
|
-
## Authorization Context
|
34
|
-
|
35
|
-
By default, Action Policy uses `context[:current_user]` as the `user` [authorization context](./authorization_context.md).
|
36
|
-
|
37
|
-
**NOTE:** see below for more information on what's included into `ActionPolicy::GraphQL::Behaviour`.
|
38
|
-
|
39
|
-
## Authorizing Fields
|
40
|
-
|
41
|
-
You can add `authorize: true` option to any field (=underlying object) to protect the access (it's equal to calling `authorize! object, to: :show?`):
|
42
|
-
|
43
|
-
```ruby
|
44
|
-
# authorization could be useful for find-like methods,
|
45
|
-
# where the object is resolved from the provided params (e.g., ID)
|
46
|
-
field :home, Home, null: false, authorize: true do
|
47
|
-
argument :id, ID, required: true
|
48
|
-
end
|
49
|
-
|
50
|
-
def home(id:)
|
51
|
-
Home.find(id)
|
52
|
-
end
|
53
|
-
|
54
|
-
# Without `authorize: true` the code would look like this
|
55
|
-
def home(id:)
|
56
|
-
Home.find(id).tap { |home| authorize! home, to: :show? }
|
57
|
-
end
|
58
|
-
```
|
59
|
-
|
60
|
-
You can use authorization options to customize the behaviour, e.g. `authorize: {to: :preview?, with: CustomPolicy}`.
|
61
|
-
|
62
|
-
By default, if a user is not authorized to access the field, an `ActionPolicy::Unauthorized` exception is raised.
|
63
|
-
|
64
|
-
If you want to return a `nil` instead, you should add `raise: false` to the options:
|
65
|
-
|
66
|
-
```ruby
|
67
|
-
# NOTE: don't forget to mark your field as nullable
|
68
|
-
field :home, Home, null: true, authorize: {raise: false}
|
69
|
-
```
|
70
|
-
|
71
|
-
You can make non-raising behaviour a default by setting a configuration option:
|
72
|
-
|
73
|
-
```ruby
|
74
|
-
ActionPolicy::GraphQL.authorize_raise_exception = false
|
75
|
-
```
|
76
|
-
|
77
|
-
You can also change the default `show?` rule globally:
|
78
|
-
|
79
|
-
```ruby
|
80
|
-
ActionPolicy::GraphQL.default_authorize_rule = :show_graphql_field?
|
81
|
-
```
|
82
|
-
|
83
|
-
If you want to perform authorization before resolving the field value, you can use `preauthorize: *` option:
|
84
|
-
|
85
|
-
```ruby
|
86
|
-
field :homes, [Home], null: false, preauthorize: {with: HomePolicy}
|
87
|
-
|
88
|
-
def homes
|
89
|
-
Home.all
|
90
|
-
end
|
91
|
-
```
|
92
|
-
|
93
|
-
The code above is equal to:
|
94
|
-
|
95
|
-
```ruby
|
96
|
-
field :homes, [Home], null: false
|
97
|
-
|
98
|
-
def homes
|
99
|
-
authorize! "homes", to: :index?, with: HomePolicy
|
100
|
-
Home.all
|
101
|
-
end
|
102
|
-
```
|
103
|
-
|
104
|
-
**NOTE:** we pass the field's name as the `record` to the policy rule. We assume that preauthorization rules do not depend on
|
105
|
-
the record itself and pass the field's name for debugging purposes only.
|
106
|
-
|
107
|
-
You can customize the authorization options, e.g. `authorize: {to: :preview?, with: CustomPolicy}`.
|
108
|
-
|
109
|
-
**NOTE:** unlike `authorize: *` you MUST specify the `with: SomePolicy` option.
|
110
|
-
The default authorization rule depends on the type of the field:
|
111
|
-
|
112
|
-
- for lists we use `index?` (configured by `ActionPolicy::GraphQL.default_preauthorize_list_rule` parameter)
|
113
|
-
- for _singleton_ fields we use `show?` (configured by `ActionPolicy::GraphQL.default_preauthorize_node_rule` parameter)
|
114
|
-
|
115
|
-
### Class-level authorization
|
116
|
-
|
117
|
-
You can use Action Policy in the class-level [authorization hooks](https://graphql-ruby.org/authorization/authorization.html) (`self.authorized?`) like this:
|
118
|
-
|
119
|
-
```ruby
|
120
|
-
class Types::Friendship < Types::BaseObject
|
121
|
-
def self.authorized?(object, context)
|
122
|
-
super &&
|
123
|
-
allowed_to?(
|
124
|
-
:show?,
|
125
|
-
object,
|
126
|
-
# NOTE: you must provide context explicitly
|
127
|
-
context: {user: context[:current_user]}
|
128
|
-
)
|
129
|
-
end
|
130
|
-
end
|
131
|
-
```
|
132
|
-
|
133
|
-
## Authorizing Mutations
|
134
|
-
|
135
|
-
A mutation is just a Ruby class with a single API method. There is nothing specific in authorizing mutations: from the Action Policy point of view, they are just [_behaviours_](./behaviour.md).
|
136
|
-
|
137
|
-
If you want to authorize the mutation, you call `authorize!` method. For example:
|
138
|
-
|
139
|
-
```ruby
|
140
|
-
class Mutations::DestroyUser < Types::BaseMutation
|
141
|
-
argument :id, ID, required: true
|
142
|
-
|
143
|
-
def resolve(id:)
|
144
|
-
user = User.find(id)
|
145
|
-
|
146
|
-
# Raise an exception if the user has not enough permissions
|
147
|
-
authorize! user, to: :destroy?
|
148
|
-
# Or check without raising and do what you want
|
149
|
-
#
|
150
|
-
# if allowed_to?(:destroy?, user)
|
151
|
-
|
152
|
-
user.destroy!
|
153
|
-
|
154
|
-
{deleted_id: user.id}
|
155
|
-
end
|
156
|
-
end
|
157
|
-
```
|
158
|
-
|
159
|
-
## Handling exceptions
|
160
|
-
|
161
|
-
The query would fail with `ActionPolicy::Unauthorized` exception when using `authorize: true` (in raising mode) or calling `authorize!` explicitly.
|
162
|
-
|
163
|
-
That could be useful to handle this exception and send a more detailed error message to the client, for example:
|
164
|
-
|
165
|
-
```ruby
|
166
|
-
# in your schema file
|
167
|
-
rescue_from(ActionPolicy::Unauthorized) do |exp|
|
168
|
-
raise GraphQL::ExecutionError.new(
|
169
|
-
# use result.message (backed by i18n) as an error message
|
170
|
-
exp.result.message,
|
171
|
-
# use GraphQL error extensions to provide more context
|
172
|
-
extensions: {
|
173
|
-
code: :unauthorized,
|
174
|
-
fullMessages: exp.result.reasons.full_messages,
|
175
|
-
details: exp.result.reasons.details
|
176
|
-
}
|
177
|
-
)
|
178
|
-
end
|
179
|
-
```
|
180
|
-
|
181
|
-
## Scoping Data
|
182
|
-
|
183
|
-
You can add `authorized_scope: true` option to a field (list or [_connection_](https://graphql-ruby.org/relay/connections.html)) to apply the corresponding policy rules to the data:
|
184
|
-
|
185
|
-
```ruby
|
186
|
-
class CityType < ::Common::Graphql::Type
|
187
|
-
# It would automatically apply the relation scope from the EventPolicy to
|
188
|
-
# the relation (city.events)
|
189
|
-
field :events, EventType.connection_type,
|
190
|
-
null: false,
|
191
|
-
authorized_scope: true
|
192
|
-
|
193
|
-
# you can specify the policy explicitly
|
194
|
-
field :events, EventType.connection_type,
|
195
|
-
null: false,
|
196
|
-
authorized_scope: {with: CustomEventPolicy}
|
197
|
-
|
198
|
-
# without the option you would write the following code
|
199
|
-
def events
|
200
|
-
authorized_scope object.events
|
201
|
-
# or if `with` option specified
|
202
|
-
authorized_scope object.events, with: CustomEventPolicy
|
203
|
-
end
|
204
|
-
end
|
205
|
-
```
|
206
|
-
|
207
|
-
**NOTE:** you cannot use `authorize: *` and `authorized_scope: *` at the same time but you can combine `preauthorize: *` with `authorized_scope: *`.
|
208
|
-
|
209
|
-
See the documenation on [scoping](./scoping.md).
|
210
|
-
|
211
|
-
## Exposing Authorization Rules
|
212
|
-
|
213
|
-
With `action_policy-graphql` gem, you can easily expose your authorization logic to the client in a standardized way.
|
214
|
-
|
215
|
-
For example, if you want to "tell" the client which actions could be performed against the object you
|
216
|
-
can use the `expose_authorization_rules` macro to add authorization-related fields to your type:
|
217
|
-
|
218
|
-
```ruby
|
219
|
-
class ProfileType < Types::BaseType
|
220
|
-
# Adds can_edit, can_destroy fields with
|
221
|
-
# AuthorizationResult type.
|
222
|
-
|
223
|
-
# NOTE: prefix "can_" is used by default, no need to specify it explicitly
|
224
|
-
expose_authorization_rules :edit?, :destroy?, prefix: "can_"
|
225
|
-
end
|
226
|
-
```
|
227
|
-
|
228
|
-
**NOTE:** you can use [aliases](./aliases.md) here as well as defined rules.
|
229
|
-
|
230
|
-
**NOTE:** This feature relies the [_failure reasons_](./reasons.md) and
|
231
|
-
the [i18n integration](./i18n.md) extensions. If your policies don't include any of these,
|
232
|
-
you won't be able to use it.
|
233
|
-
|
234
|
-
Then the client could perform the following query:
|
235
|
-
|
236
|
-
```gql
|
237
|
-
{
|
238
|
-
post(id: $id) {
|
239
|
-
canEdit {
|
240
|
-
# (bool) true|false; not null
|
241
|
-
value
|
242
|
-
# top-level decline message ("Not authorized" by default); null if value is true
|
243
|
-
message
|
244
|
-
# detailed information about the decline reasons; null if value is true or you don't have "failure reasons" extension enabled
|
245
|
-
reasons {
|
246
|
-
details # JSON-encoded hash of the form { "event" => [:privacy_off?] }
|
247
|
-
fullMessages # Array of human-readable reasons
|
248
|
-
}
|
249
|
-
}
|
250
|
-
|
251
|
-
canDestroy {
|
252
|
-
# ...
|
253
|
-
}
|
254
|
-
}
|
255
|
-
}
|
256
|
-
```
|
257
|
-
|
258
|
-
You can override a custom authorization field prefix (`can_`):
|
259
|
-
|
260
|
-
```ruby
|
261
|
-
ActionPolicy::GraphQL.default_authorization_field_prefix = "allowed_to_"
|
262
|
-
```
|
263
|
-
|
264
|
-
You can specify a custom field name as well (only for a single rule):
|
265
|
-
|
266
|
-
```ruby
|
267
|
-
class ProfileType < ::Common::Graphql::Type
|
268
|
-
# Adds can_create_post field.
|
269
|
-
|
270
|
-
expose_authorization_rules :create?, with: PostPolicy, field_name: "can_create_post"
|
271
|
-
end
|
272
|
-
```
|
273
|
-
|
274
|
-
## Custom Behaviour
|
275
|
-
|
276
|
-
Including the default `ActionPolicy::GraphQL::Behaviour` is equal to adding the following to your base class:
|
277
|
-
|
278
|
-
```ruby
|
279
|
-
class Types::BaseObject < GraphQL::Schema::Object
|
280
|
-
# include Action Policy behaviour and its extensions
|
281
|
-
include ActionPolicy::Behaviour
|
282
|
-
include ActionPolicy::Behaviours::ThreadMemoized
|
283
|
-
include ActionPolicy::Behaviours::Memoized
|
284
|
-
include ActionPolicy::Behaviours::Namespaced
|
285
|
-
|
286
|
-
# define authorization context
|
287
|
-
authorize :user, through: :current_user
|
288
|
-
|
289
|
-
# add a method helper to get the current_user from the context
|
290
|
-
def current_user
|
291
|
-
context[:current_user]
|
292
|
-
end
|
293
|
-
|
294
|
-
# extend the field class to add `authorize` and `authorized_scope` options
|
295
|
-
field_class.prepend(ActionPolicy::GraphQL::AuthorizedField)
|
296
|
-
|
297
|
-
# add `expose_authorization_rules` macro
|
298
|
-
include ActionPolicy::GraphQL::Fields
|
299
|
-
end
|
300
|
-
```
|
301
|
-
|
302
|
-
Feel free to create your own behaviour by adding only the functionality you need.
|