graphql-guard 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![Build Status](https://travis-ci.org/exAspArk/graphql-guard.svg?branch=master)](https://travis-ci.org/exAspArk/graphql-guard)
|
4
|
+
[![Coverage Status](https://coveralls.io/repos/github/exAspArk/graphql-guard/badge.svg)](https://coveralls.io/github/exAspArk/graphql-guard)
|
5
|
+
[![Code Climate](https://img.shields.io/codeclimate/github/exAspArk/graphql-guard.svg)](https://codeclimate.com/github/exAspArk/graphql-guard)
|
6
|
+
[![Downloads](https://img.shields.io/gem/dt/graphql-guard.svg)](https://rubygems.org/gems/graphql-guard)
|
7
|
+
[![Latest Version](https://img.shields.io/gem/v/graphql-guard.svg)](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:
|