action_policy 0.4.0 → 0.5.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 +4 -4
- data/CHANGELOG.md +233 -171
- data/LICENSE.txt +1 -1
- data/README.md +7 -11
- 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 +15 -33
- data/lib/action_policy/ext/{symbol_classify.rb → symbol_camelize.rb} +6 -6
- data/lib/action_policy/i18n.rb +1 -1
- data/lib/action_policy/lookup_chain.rb +41 -21
- data/lib/action_policy/policy/aliases.rb +7 -12
- data/lib/action_policy/policy/authorization.rb +14 -17
- data/lib/action_policy/policy/cache.rb +34 -18
- 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 +30 -20
- data/lib/action_policy/policy/scoping.rb +5 -6
- data/lib/action_policy/rails/controller.rb +6 -1
- data/lib/action_policy/rails/ext/active_record.rb +7 -0
- 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 +3 -3
- data/lib/action_policy/rspec/have_authorized_scope.rb +5 -7
- data/lib/action_policy/testing.rb +1 -1
- 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} +1 -1
- 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 +30 -119
- data/.gitattributes +0 -2
- data/.github/FUNDING.yml +0 -1
- data/.github/ISSUE_TEMPLATE.md +0 -18
- data/.github/PULL_REQUEST_TEMPLATE.md +0 -29
- 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 -71
- 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 -77
- 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 -273
- 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 -17
- 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 -333
- data/docs/writing_policies.md +0 -107
- data/gemfiles/jruby.gemfile +0 -8
- data/gemfiles/rails42.gemfile +0 -8
- 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.
|