graphql-guard 0.3.0 → 0.4.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/.travis.yml +2 -0
- data/CHANGELOG.md +5 -1
- data/Gemfile +1 -0
- data/README.md +125 -80
- data/lib/graphql/guard.rb +10 -2
- data/lib/graphql/guard/testing.rb +35 -0
- data/lib/graphql/guard/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: edc74c4db940ec20e1daa0907380a2db57c57bd5
|
4
|
+
data.tar.gz: 984fc4fb2913d17f4c6a331ac8f628aace746125
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e2ab0b88f3daf2acdfdba60814b62fdbb3d36c34aec3f4441da3ebdc83daff068c1115d0680959c552cdd894ecc0d55c59fa3db6ca9bcd80039493b08d2158c5
|
7
|
+
data.tar.gz: d3987791bdd7cf4a494572ab2bedd08a5992cd71b1909a9d84572fe048e5bdf47fbd0796af38e21d12bfeaefcebf6506d362f315147d2dd51e4c648cd0fc0e51
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -8,10 +8,14 @@ one of the following labels: `Added`, `Changed`, `Deprecated`,
|
|
8
8
|
to manage the versions of this gem so
|
9
9
|
that you can set version constraints properly.
|
10
10
|
|
11
|
-
#### [Unreleased](https://github.com/exAspArk/graphql-guard/compare/v0.
|
11
|
+
#### [Unreleased](https://github.com/exAspArk/graphql-guard/compare/v0.4.0...HEAD)
|
12
12
|
|
13
13
|
* WIP
|
14
14
|
|
15
|
+
#### [v0.4.0](https://github.com/exAspArk/graphql-guard/compare/v0.3.0...v0.4.0) – 2017-07-25
|
16
|
+
|
17
|
+
* `Added`: ability to test `guard` lambdas via field.
|
18
|
+
|
15
19
|
#### [v0.3.0](https://github.com/exAspArk/graphql-guard/compare/v0.2.0...v0.3.0) – 2017-07-19
|
16
20
|
|
17
21
|
* `Added`: ability to use custom error handlers.
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,6 +1,10 @@
|
|
1
1
|
# graphql-guard
|
2
2
|
|
3
3
|
[](https://travis-ci.org/exAspArk/graphql-guard)
|
4
|
+
[](https://coveralls.io/github/exAspArk/graphql-guard)
|
5
|
+
[](https://codeclimate.com/github/exAspArk/graphql-guard)
|
6
|
+
[](https://rubygems.org/gems/graphql-guard)
|
7
|
+
[](https://rubygems.org/gems/graphql-guard)
|
4
8
|
|
5
9
|
This tiny gem provides a field-level authorization for [graphql-ruby](https://github.com/rmosolgo/graphql-ruby).
|
6
10
|
|
@@ -15,6 +19,7 @@ This tiny gem provides a field-level authorization for [graphql-ruby](https://gi
|
|
15
19
|
* [CanCanCan](#cancancan)
|
16
20
|
* [Pundit](#pundit)
|
17
21
|
* [Installation](#installation)
|
22
|
+
* [Testing](#testing)
|
18
23
|
* [Development](#development)
|
19
24
|
* [Contributing](#contributing)
|
20
25
|
* [License](#license)
|
@@ -25,28 +30,30 @@ This tiny gem provides a field-level authorization for [graphql-ruby](https://gi
|
|
25
30
|
Define a GraphQL schema:
|
26
31
|
|
27
32
|
```ruby
|
28
|
-
#
|
33
|
+
# Define a type
|
29
34
|
PostType = GraphQL::ObjectType.define do
|
30
35
|
name "Post"
|
36
|
+
|
31
37
|
field :id, !types.ID
|
32
38
|
field :title, !types.String
|
33
39
|
end
|
34
40
|
|
35
|
-
#
|
41
|
+
# Define a query
|
36
42
|
QueryType = GraphQL::ObjectType.define do
|
37
43
|
name "Query"
|
44
|
+
|
38
45
|
field :posts, !types[PostType] do
|
39
46
|
argument :user_id, !types.ID
|
40
|
-
resolve ->(
|
47
|
+
resolve ->(_, args, _) { Post.where(user_id: args[:user_id]) }
|
41
48
|
end
|
42
49
|
end
|
43
50
|
|
44
|
-
#
|
51
|
+
# Define a schema
|
45
52
|
Schema = GraphQL::Schema.define do
|
46
53
|
query QueryType
|
47
54
|
end
|
48
55
|
|
49
|
-
#
|
56
|
+
# Execute query
|
50
57
|
GraphSchema.execute(query, variables: { user_id: 1 }, context: { current_user: current_user })
|
51
58
|
```
|
52
59
|
|
@@ -54,67 +61,68 @@ GraphSchema.execute(query, variables: { user_id: 1 }, context: { current_user: c
|
|
54
61
|
|
55
62
|
Add `GraphQL::Guard` to your schema:
|
56
63
|
|
57
|
-
|
64
|
+
<pre>
|
58
65
|
Schema = GraphQL::Schema.define do
|
59
66
|
query QueryType
|
60
|
-
use GraphQL::Guard.new
|
67
|
+
<b>use GraphQL::Guard.new</b>
|
61
68
|
end
|
62
|
-
|
69
|
+
</pre>
|
63
70
|
|
64
71
|
Now you can define `guard` for a field, which will check permissions before resolving the field:
|
65
72
|
|
66
|
-
|
73
|
+
<pre>
|
67
74
|
QueryType = GraphQL::ObjectType.define do
|
68
75
|
name "Query"
|
69
|
-
|
76
|
+
|
77
|
+
<b>field :posts</b>, !types[PostType] do
|
70
78
|
argument :user_id, !types.ID
|
71
|
-
guard ->(
|
79
|
+
<b>guard ->(obj, args, ctx) {</b> args[:user_id] == ctx[:current_user].id <b>}</b>
|
72
80
|
...
|
73
81
|
end
|
74
82
|
end
|
75
|
-
|
83
|
+
</pre>
|
76
84
|
|
77
|
-
You can also define `guard`, which will be executed for
|
85
|
+
You can also define `guard`, which will be executed for every (`*`) field in the type:
|
78
86
|
|
79
|
-
|
87
|
+
<pre>
|
80
88
|
PostType = GraphQL::ObjectType.define do
|
81
89
|
name "Post"
|
82
|
-
guard ->(
|
90
|
+
<b>guard ->(obj, ctx) {</b> ctx[:current_user].admin? <b>}</b>
|
83
91
|
...
|
84
92
|
end
|
85
|
-
|
93
|
+
</pre>
|
86
94
|
|
87
|
-
If `guard` block returns `false`, then it'll raise a `GraphQL::Guard::NotAuthorizedError` error.
|
95
|
+
If `guard` block returns `nil` or `false`, then it'll raise a `GraphQL::Guard::NotAuthorizedError` error.
|
88
96
|
|
89
97
|
### Policy object
|
90
98
|
|
91
|
-
Alternatively, it's possible to describe all policies by using PORO (Plain Old Ruby Object), which should implement a `guard` method. For example:
|
99
|
+
Alternatively, it's possible to extract and describe all policies by using PORO (Plain Old Ruby Object), which should implement a `guard` method. For example:
|
92
100
|
|
93
|
-
|
94
|
-
class GraphqlPolicy
|
101
|
+
<pre>
|
102
|
+
class <b>GraphqlPolicy</b>
|
95
103
|
RULES = {
|
96
104
|
QueryType => {
|
97
|
-
posts: ->(
|
105
|
+
<b>posts: ->(obj, args, ctx) {</b> args[:user_id] == ctx[:current_user].id <b>}</b>
|
98
106
|
},
|
99
107
|
PostType => {
|
100
|
-
'*': ->(
|
108
|
+
<b>'*': ->(obj, ctx) {</b> ctx[:current_user].admin? <b>}</b>
|
101
109
|
}
|
102
110
|
}
|
103
111
|
|
104
|
-
def self
|
112
|
+
def self.<b>guard(type, field)</b>
|
105
113
|
RULES.dig(type, field)
|
106
114
|
end
|
107
115
|
end
|
108
|
-
|
116
|
+
</pre>
|
109
117
|
|
110
118
|
Pass this object to `GraphQL::Guard`:
|
111
119
|
|
112
|
-
|
120
|
+
<pre>
|
113
121
|
Schema = GraphQL::Schema.define do
|
114
122
|
query QueryType
|
115
|
-
use GraphQL::Guard.new(policy_object: GraphqlPolicy)
|
123
|
+
use GraphQL::Guard.new(<b>policy_object: GraphqlPolicy</b>)
|
116
124
|
end
|
117
|
-
|
125
|
+
</pre>
|
118
126
|
|
119
127
|
## Priority order
|
120
128
|
|
@@ -125,12 +133,12 @@ end
|
|
125
133
|
3. Inline policy on the type.
|
126
134
|
2. Policy from the policy object on the type.
|
127
135
|
|
128
|
-
|
129
|
-
class GraphqlPolicy
|
136
|
+
<pre>
|
137
|
+
class <b>GraphqlPolicy</b>
|
130
138
|
RULES = {
|
131
139
|
PostType => {
|
132
|
-
|
133
|
-
|
140
|
+
<b>'*': ->(_, ctx) {</b> ctx[:current_user].admin? <b>}</b>, # <=== <b>4</b>
|
141
|
+
<b>title: ->(_, _, ctx) {</b> ctx[:current_user].admin? <b>}</b> # <=== <b>2</b>
|
134
142
|
}
|
135
143
|
}
|
136
144
|
|
@@ -141,43 +149,43 @@ end
|
|
141
149
|
|
142
150
|
PostType = GraphQL::ObjectType.define do
|
143
151
|
name "Post"
|
144
|
-
guard ->(
|
145
|
-
field :title
|
152
|
+
<b>guard ->(_, ctx) {</b> ctx[:current_user].admin? <b>}</b> # <=== <b>3</b>
|
153
|
+
<b>field :title</b>, !types.String, <b>guard: ->(_, _, ctx) {</b> ctx[:current_user].admin? <b>}</b> # <=== <b>1</b>
|
146
154
|
end
|
147
155
|
|
148
156
|
Schema = GraphQL::Schema.define do
|
149
157
|
query QueryType
|
150
|
-
use GraphQL::Guard.new(policy_object: GraphqlPolicy)
|
158
|
+
use GraphQL::Guard.new(<b>policy_object: GraphqlPolicy</b>)
|
151
159
|
end
|
152
|
-
|
160
|
+
</pre>
|
153
161
|
|
154
162
|
## Error handling
|
155
163
|
|
156
164
|
By default `GraphQL::Guard` raises a `GraphQL::Guard::NotAuthorizedError` exception if access to field is not authorized.
|
157
165
|
You can change this behavior, by passing custom `not_authorized` lambda. For example:
|
158
166
|
|
159
|
-
|
160
|
-
|
167
|
+
<pre>
|
168
|
+
SchemaWithErrors = GraphQL::Schema.define do
|
161
169
|
query QueryType
|
162
170
|
use GraphQL::Guard.new(
|
163
|
-
#
|
164
|
-
|
171
|
+
# Returns an error in the response
|
172
|
+
<b>not_authorized: ->(type, field) { GraphQL::ExecutionError.new("Not authorized to access #{type}.#{field}") }</b>
|
165
173
|
|
166
|
-
#
|
167
|
-
not_authorized: ->(type, field) { GraphQL::
|
174
|
+
# By default it raises an error
|
175
|
+
# not_authorized: ->(type, field) { raise GraphQL::Guard::NotAuthorizedError.new("#{type}.#{field}") }
|
168
176
|
)
|
169
177
|
end
|
170
|
-
|
178
|
+
</pre>
|
171
179
|
|
172
180
|
In this case executing a query will continue, but return `nil` for not authorized field and also an array of `errors`:
|
173
181
|
|
174
|
-
|
175
|
-
|
182
|
+
<pre>
|
183
|
+
SchemaWithErrors.execute("query { <b>posts</b>(user_id: 1) { id title } }")
|
176
184
|
# => {
|
177
|
-
#
|
178
|
-
#
|
185
|
+
# "data" => <b>nil</b>,
|
186
|
+
# "errors" => [{ "messages" => <b>"Not authorized to access Query.posts"</b>, "locations": { ... }, "path" => [<b>"posts"</b>] }]
|
179
187
|
# }
|
180
|
-
|
188
|
+
</pre>
|
181
189
|
|
182
190
|
## Integration
|
183
191
|
|
@@ -185,13 +193,13 @@ You can simply reuse your existing policies if you really want. You don't need a
|
|
185
193
|
|
186
194
|
### CanCanCan
|
187
195
|
|
188
|
-
|
189
|
-
#
|
190
|
-
class Ability
|
196
|
+
<pre>
|
197
|
+
# Define an ability
|
198
|
+
class <b>Ability</b>
|
191
199
|
include CanCan::Ability
|
192
200
|
|
193
201
|
def initialize(user)
|
194
|
-
user ||= User.new
|
202
|
+
user ||= User.new
|
195
203
|
if user.admin?
|
196
204
|
can :manage, :all
|
197
205
|
else
|
@@ -200,46 +208,37 @@ class Ability
|
|
200
208
|
end
|
201
209
|
end
|
202
210
|
|
203
|
-
#
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
}
|
209
|
-
}
|
210
|
-
|
211
|
-
def self.guard(type, field)
|
212
|
-
RULES.dig(type, field)
|
213
|
-
end
|
211
|
+
# Use the ability in your guard
|
212
|
+
PostType = GraphQL::ObjectType.define do
|
213
|
+
name "Post"
|
214
|
+
<b>guard ->(post, ctx) { ctx[:current_ability].can?(:read, post) }</b>
|
215
|
+
...
|
214
216
|
end
|
215
217
|
|
216
|
-
#
|
217
|
-
GraphSchema.execute(query, context: { current_ability: Ability.new(current_user) })
|
218
|
-
|
218
|
+
# Pass the ability
|
219
|
+
GraphSchema.execute(query, context: { <b>current_ability: Ability.new(current_user)</b> })
|
220
|
+
</pre>
|
219
221
|
|
220
222
|
### Pundit
|
221
223
|
|
222
|
-
|
223
|
-
#
|
224
|
-
class PostPolicy < ApplicationPolicy
|
224
|
+
<pre>
|
225
|
+
# Define a policy
|
226
|
+
class <b>PostPolicy</b> < ApplicationPolicy
|
225
227
|
def show?
|
226
228
|
user.admin? || record.author_id == user.id
|
227
229
|
end
|
228
230
|
end
|
229
231
|
|
230
|
-
#
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
}
|
236
|
-
}
|
237
|
-
|
238
|
-
def self.guard(type, field)
|
239
|
-
RULES.dig(type, field)
|
240
|
-
end
|
232
|
+
# Use the ability in your guard
|
233
|
+
PostType = GraphQL::ObjectType.define do
|
234
|
+
name "Post"
|
235
|
+
<b>guard ->(post, ctx) { PostPolicy.new(ctx[:current_user], post).show? }</b>
|
236
|
+
...
|
241
237
|
end
|
242
|
-
|
238
|
+
|
239
|
+
# Pass current_user
|
240
|
+
GraphSchema.execute(query, context: { <b>current_user: current_user</b> })
|
241
|
+
</pre>
|
243
242
|
|
244
243
|
## Installation
|
245
244
|
|
@@ -257,6 +256,52 @@ Or install it yourself as:
|
|
257
256
|
|
258
257
|
$ gem install graphql-guard
|
259
258
|
|
259
|
+
## Testing
|
260
|
+
|
261
|
+
It's possible to test fields with `guard` in isolation:
|
262
|
+
|
263
|
+
<pre>
|
264
|
+
# Your type
|
265
|
+
QueryType = GraphQL::ObjectType.define do
|
266
|
+
name "Query"
|
267
|
+
<b>field :posts</b>, !types[PostType], <b>guard ->(obj, args, ctx) {</b> ... <b>}</b>
|
268
|
+
end
|
269
|
+
|
270
|
+
# Your test
|
271
|
+
<b>require "graphql/guard/testing"</b>
|
272
|
+
|
273
|
+
posts = QueryType.<b>field_with_guard('posts')</b>
|
274
|
+
result = posts.<b>guard(obj, args, ctx)</b>
|
275
|
+
expect(result).to eq(true)
|
276
|
+
</pre>
|
277
|
+
|
278
|
+
If you would like to test your fields with policy objects:
|
279
|
+
|
280
|
+
|
281
|
+
<pre>
|
282
|
+
# Your type
|
283
|
+
QueryType = GraphQL::ObjectType.define do
|
284
|
+
name "Query"
|
285
|
+
<b>field :posts</b>, !types[PostType]
|
286
|
+
end
|
287
|
+
|
288
|
+
# Your policy object
|
289
|
+
class <b>GraphqlPolicy</b>
|
290
|
+
def self.<b>guard</b>(type, field)
|
291
|
+
<b>->(_obj, args, ctx) {</b> ... <b>}</b>
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
# Your test
|
296
|
+
<b>require "graphql/guard/testing"</b>
|
297
|
+
|
298
|
+
<b>guard_object</b> = <b>GraphQL::Guard.new(policy_object: PolicyObject::GraphqlPolicy)</b>
|
299
|
+
|
300
|
+
posts = QueryType.<b>field_with_guard('posts', guard_object)</b>
|
301
|
+
result = posts.<b>guard(obj, args, ctx)</b>
|
302
|
+
expect(result).to eq(true)
|
303
|
+
</pre>
|
304
|
+
|
260
305
|
## Development
|
261
306
|
|
262
307
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
data/lib/graphql/guard.rb
CHANGED
@@ -25,8 +25,8 @@ module GraphQL
|
|
25
25
|
end
|
26
26
|
|
27
27
|
def instrument(type, field)
|
28
|
-
field_guard_proc =
|
29
|
-
type_guard_proc =
|
28
|
+
field_guard_proc = field_guard_proc(type, field)
|
29
|
+
type_guard_proc = type_guard_proc(type, field)
|
30
30
|
return field if !field_guard_proc && !type_guard_proc
|
31
31
|
|
32
32
|
old_resolve_proc = field.resolve_proc
|
@@ -48,6 +48,14 @@ module GraphQL
|
|
48
48
|
field.redefine { resolve(new_resolve_proc) }
|
49
49
|
end
|
50
50
|
|
51
|
+
def field_guard_proc(type, field)
|
52
|
+
inline_field_guard(field) || policy_object_guard(type, field.name.to_sym)
|
53
|
+
end
|
54
|
+
|
55
|
+
def type_guard_proc(type, field)
|
56
|
+
inline_type_guard(type) || policy_object_guard(type, ANY_FIELD_NAME)
|
57
|
+
end
|
58
|
+
|
51
59
|
private
|
52
60
|
|
53
61
|
def policy_object_guard(type, field_name)
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
class Field
|
5
|
+
NoGuardError = Class.new(StandardError)
|
6
|
+
|
7
|
+
def guard(*args)
|
8
|
+
raise NoGuardError.new("Get your field by calling: Type.field_with_guard('#{name}')") unless @guard_type
|
9
|
+
guard_proc = @guard_object.field_guard_proc(@guard_type, self) || @guard_object.type_guard_proc(@guard_type, self)
|
10
|
+
raise NoGuardError.new("Guard lambda does not exist for #{@guard_type}.#{name}") unless guard_proc
|
11
|
+
|
12
|
+
guard_proc.call(*args)
|
13
|
+
end
|
14
|
+
|
15
|
+
def __guard_object=(guard_object)
|
16
|
+
@guard_object = guard_object || GraphQL::Guard.new
|
17
|
+
end
|
18
|
+
|
19
|
+
def __guard_type=(guard_type)
|
20
|
+
@guard_type = guard_type
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class ObjectType
|
25
|
+
def field_with_guard(field_name, guard_object = nil)
|
26
|
+
field = get_field(field_name)
|
27
|
+
return unless field
|
28
|
+
|
29
|
+
field.clone.tap do |f|
|
30
|
+
f.__guard_object = guard_object
|
31
|
+
f.__guard_type = self
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: graphql-guard
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- exAspArk
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-07-
|
11
|
+
date: 2017-07-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: graphql
|
@@ -93,6 +93,7 @@ files:
|
|
93
93
|
- bin/setup
|
94
94
|
- graphql-guard.gemspec
|
95
95
|
- lib/graphql/guard.rb
|
96
|
+
- lib/graphql/guard/testing.rb
|
96
97
|
- lib/graphql/guard/version.rb
|
97
98
|
homepage: https://github.com/exAspArk/graphql-guard
|
98
99
|
licenses:
|