action_policy-graphql 0.0.1 → 0.1.0.rc1
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/README.md +105 -1
- data/lib/action_policy/graphql/authorized_field.rb +71 -0
- data/lib/action_policy/graphql/behaviour.rb +26 -0
- data/lib/action_policy/graphql/fields.rb +48 -0
- data/lib/action_policy/graphql/types/authorization_result.rb +25 -0
- data/lib/action_policy/graphql/types/failure_reasons.rb +16 -0
- data/lib/action_policy/graphql/version.rb +1 -1
- data/lib/action_policy/graphql.rb +20 -2
- metadata +9 -5
- data/.rspec +0 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3c8a905655156e38c918dc3b222dff1d8642d61416892b3ce89bf033adc08dcf
|
4
|
+
data.tar.gz: 4d9994c62eb097299b2e7199dd656cb64d859983b6da0778929ba86bc21f58b8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bc7c087373d6ddf13d6f659a923ee786589d79861fb11f772104e4e5ae3a262d6160a55062340fda17c3729f3ece2461fdd25cc2f55412c2119e73374241cad1
|
7
|
+
data.tar.gz: 1ee3ad7f30ad9a37ed728fe8bde93d2405d0e57cc26aae78a204930e22abedd40d3b3381a061da7904eeb21f6af4023423627fa8533e7776d80b646fe5d77af6
|
data/README.md
CHANGED
@@ -6,6 +6,11 @@
|
|
6
6
|
|
7
7
|
This gem provides an integration for using [Action Policy](https://github.com/palkan/action_policy) as an authorization framework for GraphQL applications (built with [`graphql` ruby gem](https://graphql-ruby.org)).
|
8
8
|
|
9
|
+
This integration includes the following features:
|
10
|
+
- Fields & mutations authorization
|
11
|
+
- List and connections scoping
|
12
|
+
- **Exposing permissions/authorization rules in the API**.
|
13
|
+
|
9
14
|
📑 [Documentation](https://actionpolicy.evilmartians.io/#/graphql)
|
10
15
|
|
11
16
|
<a href="https://evilmartians.com/?utm_source=action_policy-graphql">
|
@@ -25,7 +30,106 @@ And then execute:
|
|
25
30
|
|
26
31
|
## Usage
|
27
32
|
|
28
|
-
|
33
|
+
**NOTE:** this is a quick overview of the functionality provided by the gem. For more information see the [documentation](https://actionpolicy.evilmartians.io/#/graphql).
|
34
|
+
|
35
|
+
To start using Action Policy in GraphQL-related code, you need to enhance your base classes with `ActionPolicy::GraphQL::Behaviour`:
|
36
|
+
|
37
|
+
```ruby
|
38
|
+
# For fields authorization, lists scoping and rules exposing
|
39
|
+
class Types::BaseObject < GraphQL::Schema::Object
|
40
|
+
include ActionPolicy::GraphQL::Behaviour
|
41
|
+
end
|
42
|
+
|
43
|
+
# For using authorization helpers in mutations
|
44
|
+
class Types::BaseMutation < GraphQL::Schema::Mutation
|
45
|
+
include ActionPolicy::GraphQL::Behaviour
|
46
|
+
end
|
47
|
+
```
|
48
|
+
|
49
|
+
### `authorize: *`
|
50
|
+
|
51
|
+
You can add authorization to the fields by specifying the `authorize: *` option:
|
52
|
+
|
53
|
+
```
|
54
|
+
field :home, Home, null: false, authorize: true do
|
55
|
+
argument :id, ID, required: true
|
56
|
+
end
|
57
|
+
|
58
|
+
# field resolver method
|
59
|
+
def home(id:)
|
60
|
+
Home.find(id)
|
61
|
+
end
|
62
|
+
```
|
63
|
+
|
64
|
+
The code above is equal to:
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
field :home, Home, null: false do
|
68
|
+
argument :id, ID, required: true
|
69
|
+
end
|
70
|
+
|
71
|
+
def home(id:)
|
72
|
+
Home.find(id).tap { |home| authorize! home, to: :show? }
|
73
|
+
end
|
74
|
+
```
|
75
|
+
|
76
|
+
You can customize the authorization options, e.g. `authorize: {to: :preview?, with: CustomPolicy}`.
|
77
|
+
|
78
|
+
### `authorized_scope`
|
79
|
+
|
80
|
+
You can add `authorized_scope: true` option to the field (list or _connection_ field) to
|
81
|
+
apply the corresponding policy rules to the data:
|
82
|
+
|
83
|
+
```ruby
|
84
|
+
class CityType < ::Common::Graphql::Type
|
85
|
+
# It would automatically apply the relation scope from the EventPolicy to
|
86
|
+
# the relation (city.events)
|
87
|
+
field :events, EventType.connection_type, null: false, authorized_scope: true
|
88
|
+
|
89
|
+
# you can specify the policy explicitly
|
90
|
+
field :events, EventType.connection_type, null: false, authorized_scope: {with: CustomEventPolicy}
|
91
|
+
end
|
92
|
+
```
|
93
|
+
|
94
|
+
### `expose_authorization_rules`
|
95
|
+
|
96
|
+
You can add permissions/authorization exposing fields to "tell" clients which actions could be performed against the object or not (and why).
|
97
|
+
|
98
|
+
For example:
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
class ProfileType < ::Common::Graphql::Type
|
102
|
+
# Adds can_edit, can_destroy fields with
|
103
|
+
# AuthorizationResult type.
|
104
|
+
|
105
|
+
# NOTE: prefix "can_" is used by default, no need to specify it explicitly
|
106
|
+
expose_authorization_rules :edit?, :destroy?, prefix: "can_"
|
107
|
+
end
|
108
|
+
```
|
109
|
+
|
110
|
+
Then the client could perform the following query:
|
111
|
+
|
112
|
+
```gql
|
113
|
+
{
|
114
|
+
post(id: $id) {
|
115
|
+
canEdit {
|
116
|
+
# (bool) true|false; not null
|
117
|
+
value
|
118
|
+
# top-level decline message ("Not authorized" by default); null if value is true
|
119
|
+
message
|
120
|
+
# detailed information about the decline reasons; null if value is true
|
121
|
+
reasons {
|
122
|
+
details # JSON-encoded hash of the form { "community/event" => [:privacy_off?] }
|
123
|
+
fullMessages # Array of human-readable reasons
|
124
|
+
}
|
125
|
+
}
|
126
|
+
|
127
|
+
canDestroy {
|
128
|
+
# ...
|
129
|
+
}
|
130
|
+
}
|
131
|
+
}
|
132
|
+
```
|
29
133
|
|
30
134
|
## Contributing
|
31
135
|
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionPolicy
|
4
|
+
module GraphQL
|
5
|
+
# Add `authorized` option to the field
|
6
|
+
#
|
7
|
+
# Example:
|
8
|
+
#
|
9
|
+
# class PostType < ::GraphQL::Schema::Object
|
10
|
+
# field :comments, null: false, authorized: true
|
11
|
+
#
|
12
|
+
# # or with options
|
13
|
+
# field :comments, null: false, authorized: { type: :relation, with: MyPostPolicy }
|
14
|
+
# end
|
15
|
+
module AuthorizedField
|
16
|
+
class AuthorizeExtension < ::GraphQL::Schema::FieldExtension
|
17
|
+
def initialize(*)
|
18
|
+
super
|
19
|
+
options[:to] ||= ::ActionPolicy::GraphQL.default_authorize_rule
|
20
|
+
options[:raise] = ::ActionPolicy::GraphQL.authorize_raise_exception unless options.key?(:raise)
|
21
|
+
end
|
22
|
+
|
23
|
+
def after_resolve(value:, context:, object:, **_rest)
|
24
|
+
return value if value.nil?
|
25
|
+
|
26
|
+
if options[:raise]
|
27
|
+
object.authorize! value, **options
|
28
|
+
value
|
29
|
+
else
|
30
|
+
object.allowed_to?(options[:to], value, options) ? value : nil
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class ScopeExtension < ::GraphQL::Schema::FieldExtension
|
36
|
+
def after_resolve(value:, context:, object:, **_rest)
|
37
|
+
return value if value.nil?
|
38
|
+
|
39
|
+
object.authorized_scope(value, **options)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def initialize(*args, authorize: nil, authorized_scope: nil, **kwargs, &block)
|
44
|
+
if authorize && authorized_scope
|
45
|
+
raise ArgumentError, "Only one of `authorize` and `authorized_scope` " \
|
46
|
+
"options could be specified"
|
47
|
+
end
|
48
|
+
|
49
|
+
options = authorize || authorized_scope
|
50
|
+
|
51
|
+
if options
|
52
|
+
options = {} if options == true
|
53
|
+
|
54
|
+
extension_class = authorized_scope ? ScopeExtension : AuthorizeExtension
|
55
|
+
|
56
|
+
extension = {extension_class => options}
|
57
|
+
|
58
|
+
extensions = (kwargs[:extensions] ||= [])
|
59
|
+
|
60
|
+
if extensions.is_a?(Hash)
|
61
|
+
extensions.merge!(extension)
|
62
|
+
else
|
63
|
+
extensions << extension
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
super(*args, **kwargs, &block)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "action_policy/graphql/fields"
|
4
|
+
require "action_policy/graphql/authorized_field"
|
5
|
+
|
6
|
+
module ActionPolicy
|
7
|
+
module GraphQL
|
8
|
+
module Behaviour
|
9
|
+
def self.included(base)
|
10
|
+
base.include ActionPolicy::Behaviour
|
11
|
+
base.include ActionPolicy::Behaviours::ThreadMemoized
|
12
|
+
base.include ActionPolicy::Behaviours::Memoized
|
13
|
+
base.include ActionPolicy::Behaviours::Namespaced
|
14
|
+
|
15
|
+
base.field_class.prepend(ActionPolicy::GraphQL::AuthorizedField)
|
16
|
+
base.authorize :user, through: :current_user
|
17
|
+
|
18
|
+
base.include ActionPolicy::GraphQL::Fields
|
19
|
+
end
|
20
|
+
|
21
|
+
def current_user
|
22
|
+
context[:current_user]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "action_policy/graphql/types/authorization_result"
|
4
|
+
|
5
|
+
module ActionPolicy
|
6
|
+
module GraphQL
|
7
|
+
# Add DSL to add policy rules as fields
|
8
|
+
#
|
9
|
+
# Example:
|
10
|
+
#
|
11
|
+
# class PostType < ::GraphQL::Schema::Object
|
12
|
+
# # Adds can_edit, can_destroy fields with
|
13
|
+
# # AuthorizationResult type.
|
14
|
+
#
|
15
|
+
# expose_authorization_rules :edit?, :destroy?, prefix: "can_"
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# Prefix is "can_" by default.
|
19
|
+
module Fields
|
20
|
+
def self.included(base)
|
21
|
+
base.extend ClassMethods
|
22
|
+
end
|
23
|
+
|
24
|
+
def allowance_to(rule, target = object, **options)
|
25
|
+
policy_for(record: target, **options).yield_self do |policy|
|
26
|
+
policy.apply(authorization_rule_for(policy, rule))
|
27
|
+
policy.result
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
module ClassMethods
|
32
|
+
def expose_authorization_rules(*rules, prefix: ::ActionPolicy::GraphQL.default_authorization_field_prefix, **options)
|
33
|
+
rules.each do |rule|
|
34
|
+
field_name = "#{prefix}#{rule.to_s.delete("?")}"
|
35
|
+
|
36
|
+
field field_name,
|
37
|
+
ActionPolicy::GraphQL::Types::AuthorizationResult,
|
38
|
+
null: false
|
39
|
+
|
40
|
+
define_method(field_name) do
|
41
|
+
allowance_to(rule, options)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "action_policy/graphql/types/failure_reasons"
|
4
|
+
|
5
|
+
module ActionPolicy
|
6
|
+
module GraphQL
|
7
|
+
module Types
|
8
|
+
class AuthorizationResult < ::GraphQL::Schema::Object
|
9
|
+
field :value, Boolean, null: false, description: "Result of applying a policy rule"
|
10
|
+
field :message, String, null: true, description: "Human-readable error message"
|
11
|
+
field :reasons, FailureReasons, null: true, description: "Reasons of check failure"
|
12
|
+
|
13
|
+
def message
|
14
|
+
return if object.value == true
|
15
|
+
object.message
|
16
|
+
end
|
17
|
+
|
18
|
+
def reasons
|
19
|
+
return if object.value == true
|
20
|
+
object.reasons
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionPolicy
|
4
|
+
module GraphQL
|
5
|
+
module Types
|
6
|
+
class FailureReasons < ::GraphQL::Schema::Object
|
7
|
+
field :details, String, null: false, description: "JSON-encoded map of reasons"
|
8
|
+
field :full_messages, [String], null: false, description: "Human-readable errors"
|
9
|
+
|
10
|
+
def details
|
11
|
+
object.details.to_json
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -1,11 +1,29 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "action_policy"
|
4
3
|
require "graphql"
|
4
|
+
require "action_policy"
|
5
5
|
|
6
|
-
require "action_policy/graphql/
|
6
|
+
require "action_policy/graphql/behaviour"
|
7
7
|
|
8
8
|
module ActionPolicy
|
9
9
|
module GraphQL
|
10
|
+
class << self
|
11
|
+
# Which rule to use when no specified (e.g. `authorize: true`)
|
12
|
+
# Defaults to `:show?`
|
13
|
+
attr_accessor :default_authorize_rule
|
14
|
+
|
15
|
+
# Whether to raise an exeption if field is not authorized
|
16
|
+
# or return `nil`.
|
17
|
+
# Defaults to `true`.
|
18
|
+
attr_accessor :authorize_raise_exception
|
19
|
+
|
20
|
+
# Which prefix to use for authorization fields
|
21
|
+
# Defaults to `"can_"`
|
22
|
+
attr_accessor :default_authorization_field_prefix
|
23
|
+
end
|
24
|
+
|
25
|
+
self.default_authorize_rule = :show?
|
26
|
+
self.authorize_raise_exception = true
|
27
|
+
self.default_authorization_field_prefix = "can_"
|
10
28
|
end
|
11
29
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: action_policy-graphql
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.1.0.rc1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Vladimir Dementyev
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-05-
|
11
|
+
date: 2019-05-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: action_policy
|
@@ -144,7 +144,6 @@ extensions: []
|
|
144
144
|
extra_rdoc_files: []
|
145
145
|
files:
|
146
146
|
- ".gitignore"
|
147
|
-
- ".rspec"
|
148
147
|
- ".rubocop.yml"
|
149
148
|
- ".travis.yml"
|
150
149
|
- CHANGELOG.md
|
@@ -159,6 +158,11 @@ files:
|
|
159
158
|
- gemfiles/jruby.gemfile
|
160
159
|
- lib/action_policy-graphql.rb
|
161
160
|
- lib/action_policy/graphql.rb
|
161
|
+
- lib/action_policy/graphql/authorized_field.rb
|
162
|
+
- lib/action_policy/graphql/behaviour.rb
|
163
|
+
- lib/action_policy/graphql/fields.rb
|
164
|
+
- lib/action_policy/graphql/types/authorization_result.rb
|
165
|
+
- lib/action_policy/graphql/types/failure_reasons.rb
|
162
166
|
- lib/action_policy/graphql/version.rb
|
163
167
|
homepage: https://github.com/palkan/action_policy-graphql
|
164
168
|
licenses:
|
@@ -180,9 +184,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
180
184
|
version: 2.4.0
|
181
185
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
182
186
|
requirements:
|
183
|
-
- - "
|
187
|
+
- - ">"
|
184
188
|
- !ruby/object:Gem::Version
|
185
|
-
version:
|
189
|
+
version: 1.3.1
|
186
190
|
requirements: []
|
187
191
|
rubygems_version: 3.0.3
|
188
192
|
signing_key:
|
data/.rspec
DELETED