action_policy-graphql 0.0.1 → 0.1.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- 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