graphql-guard 1.0.0 → 1.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 2fee7151ba4f1b935f9bb185bc1ecef61920d0bc
4
- data.tar.gz: d5286b90482d2e5a99827f05a3587d9f596a0669
2
+ SHA256:
3
+ metadata.gz: 1356ec0ce6198540fffd1ca245c4b138792bc81ff776094a8b1ac6e12e2940d6
4
+ data.tar.gz: 1282de4a1c06f4c32bba870d8076fc94a276bf39cb2cb56e0cbd5f880aa73ff9
5
5
  SHA512:
6
- metadata.gz: 36f519bf2ed0879655eced44f1a8062e13fcfc01d08565c7c92d33ee50cef08278e430054cc42ed9834a629dc16d3512b19cf14ef2c395a19f37929c615488c0
7
- data.tar.gz: c9873f1af3b37a07398e853871f661bb45d232d79cc833aa6f0bc2aca1104e961b8e2ecf2bda3a38950de7ad91181918dbe62447cb2267ac88966b88df9c3c26
6
+ metadata.gz: d6c831b4be4415ed4b10f575bab5b7b2d6888965972bd9bdb7dcc1ec4477f46542e66d8b8d1566b27a3e4f044e66bc76a018920567710f4f7f723a8a6e4b02c7
7
+ data.tar.gz: ac3d9a36703f5f206ba40c776d2d0476a287ab99534b2e1fd27e97459ed7af3549d6cc4ef956b7f8a878f26f9b879cd8f42f0746d1c2e8d3b23f246b54f76f76
@@ -8,15 +8,19 @@ 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/v1.0.0...HEAD)
11
+ #### [Unreleased](https://github.com/exAspArk/graphql-guard/compare/v1.1.0...HEAD)
12
12
 
13
13
  * WIP
14
14
 
15
+ #### [v1.1.0](https://github.com/exAspArk/graphql-guard/compare/v1.0.0...v1.1.0) – 2018-05-09
16
+
17
+ * `Added`: support to `mask` fields depending on the context.
18
+
15
19
  #### [v1.0.0](https://github.com/exAspArk/graphql-guard/compare/v0.4.0...v1.0.0) – 2017-07-31
16
20
 
17
21
  * `Changed`: guards for every `*` field also accepts arguments: `->(object, arguments, context) { ... }`:
18
22
 
19
- * Before:
23
+ Before:
20
24
 
21
25
  <pre>
22
26
  GraphQL::ObjectType.define do
@@ -26,7 +30,7 @@ GraphQL::ObjectType.define do
26
30
  end
27
31
  </pre>
28
32
 
29
- * After:
33
+ After:
30
34
 
31
35
  <pre>
32
36
  GraphQL::ObjectType.define do
@@ -38,14 +42,14 @@ end
38
42
 
39
43
  * `Changed`: `.field_with_guard` from `graphql/guard/testing` module accepts policy object as a second argument:
40
44
 
41
- * Before:
45
+ Before:
42
46
 
43
47
  <pre>
44
- guard_object = GraphQL::Guard.new(policy_object: GraphqlPolicy)
48
+ <b>guard_object</b> = GraphQL::Guard.new(policy_object: GraphqlPolicy)
45
49
  posts_field = QueryType.field_with_guard('posts', <b>guard_object</b>)
46
50
  </pre>
47
51
 
48
- * After:
52
+ After:
49
53
 
50
54
  <pre>
51
55
  posts_field = QueryType.field_with_guard('posts', <b>GraphqlPolicy</b>)
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![Build Status](https://travis-ci.org/exAspArk/graphql-guard.svg?branch=master)](https://travis-ci.org/exAspArk/graphql-guard)
4
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)
5
+ [![Code Climate](https://img.shields.io/codeclimate/maintainability/exAspArk/graphql-guard.svg)](https://codeclimate.com/github/exAspArk/graphql-guard/maintainability)
6
6
  [![Downloads](https://img.shields.io/gem/dt/graphql-guard.svg)](https://rubygems.org/gems/graphql-guard)
7
7
  [![Latest Version](https://img.shields.io/gem/v/graphql-guard.svg)](https://rubygems.org/gems/graphql-guard)
8
8
 
@@ -14,10 +14,11 @@ This gem provides a field-level authorization for [graphql-ruby](https://github.
14
14
  * [Inline policies](#inline-policies)
15
15
  * [Policy object](#policy-object)
16
16
  * [Priority order](#priority-order)
17
- * [Error handling](#error-handling)
18
17
  * [Integration](#integration)
19
18
  * [CanCanCan](#cancancan)
20
19
  * [Pundit](#pundit)
20
+ * [Error handling](#error-handling)
21
+ * [Schema masking](#schema-masking)
21
22
  * [Installation](#installation)
22
23
  * [Testing](#testing)
23
24
  * [Development](#development)
@@ -25,6 +26,10 @@ This gem provides a field-level authorization for [graphql-ruby](https://github.
25
26
  * [License](#license)
26
27
  * [Code of Conduct](#code-of-conduct)
27
28
 
29
+ <a href="https://www.universe.com/" target="_blank" rel="noopener noreferrer">
30
+ <img src="images/universe.png" height="41" width="153" alt="Sponsored by Universe" style="max-width:100%;">
31
+ </a>
32
+
28
33
  ## Usage
29
34
 
30
35
  Define a GraphQL schema:
@@ -35,14 +40,14 @@ PostType = GraphQL::ObjectType.define do
35
40
  name "Post"
36
41
 
37
42
  field :id, !types.ID
38
- field :title, !types.String
43
+ field :title, types.String
39
44
  end
40
45
 
41
46
  # Define a query
42
47
  QueryType = GraphQL::ObjectType.define do
43
48
  name "Query"
44
49
 
45
- field :posts, !types[PostType] do
50
+ field :posts, !types[!PostType] do
46
51
  argument :user_id, !types.ID
47
52
  resolve ->(obj, args, ctx) { Post.where(user_id: args[:user_id]) }
48
53
  end
@@ -54,7 +59,7 @@ Schema = GraphQL::Schema.define do
54
59
  end
55
60
 
56
61
  # Execute query
57
- GraphSchema.execute(query, variables: { user_id: 1 }, context: { current_user: current_user })
62
+ Schema.execute(query, variables: { user_id: 1 }, context: { current_user: current_user })
58
63
  ```
59
64
 
60
65
  ### Inline policies
@@ -74,7 +79,7 @@ Now you can define `guard` for a field, which will check permissions before reso
74
79
  QueryType = GraphQL::ObjectType.define do
75
80
  name "Query"
76
81
 
77
- <b>field :posts</b>, !types[PostType] do
82
+ <b>field :posts</b>, !types[!PostType] do
78
83
  argument :user_id, !types.ID
79
84
  <b>guard ->(obj, args, ctx) {</b> args[:user_id] == ctx[:current_user].id <b>}</b>
80
85
  ...
@@ -124,6 +129,14 @@ Schema = GraphQL::Schema.define do
124
129
  end
125
130
  </pre>
126
131
 
132
+ When using a policy object, you may want to allow [introspection queries](http://graphql.org/learn/introspection/) to skip authorization. A simple way to avoid having to whitelist every introspection type in the `RULES` hash of your policy object is to check the `type` parameter in the `guard` method:
133
+
134
+ <pre>
135
+ def self.guard(type, field)
136
+ <b>type.introspection? ? ->(_obj, _args, _ctx) { true } :</b> RULES.dig(type, field) # or "false" to restrict an access
137
+ end
138
+ </pre>
139
+
127
140
  ## Priority order
128
141
 
129
142
  `GraphQL::Guard` will use the policy in the following order of priority:
@@ -159,34 +172,6 @@ Schema = GraphQL::Schema.define do
159
172
  end
160
173
  </pre>
161
174
 
162
- ## Error handling
163
-
164
- By default `GraphQL::Guard` raises a `GraphQL::Guard::NotAuthorizedError` exception if access to field is not authorized.
165
- You can change this behavior, by passing custom `not_authorized` lambda. For example:
166
-
167
- <pre>
168
- SchemaWithErrors = GraphQL::Schema.define do
169
- query QueryType
170
- use GraphQL::Guard.new(
171
- # Returns an error in the response
172
- <b>not_authorized: ->(type, field) { GraphQL::ExecutionError.new("Not authorized to access #{type}.#{field}") }</b>
173
-
174
- # By default it raises an error
175
- # not_authorized: ->(type, field) { raise GraphQL::Guard::NotAuthorizedError.new("#{type}.#{field}") }
176
- )
177
- end
178
- </pre>
179
-
180
- In this case executing a query will continue, but return `nil` for not authorized field and also an array of `errors`:
181
-
182
- <pre>
183
- SchemaWithErrors.execute("query { <b>posts</b>(user_id: 1) { id title } }")
184
- # => {
185
- # "data" => <b>nil</b>,
186
- # "errors" => [{ "messages" => <b>"Not authorized to access Query.posts"</b>, "locations": { ... }, "path" => [<b>"posts"</b>] }]
187
- # }
188
- </pre>
189
-
190
175
  ## Integration
191
176
 
192
177
  You can simply reuse your existing policies if you really want. You don't need any monkey patches or magic for it ;)
@@ -211,12 +196,12 @@ end
211
196
  # Use the ability in your guard
212
197
  PostType = GraphQL::ObjectType.define do
213
198
  name "Post"
214
- <b>guard ->(post, args, ctx) { ctx[:current_ability].can?(:read, post) }</b>
199
+ guard ->(post, args, ctx) { <b>ctx[:current_ability].can?(:read, post)</b> }
215
200
  ...
216
201
  end
217
202
 
218
203
  # Pass the ability
219
- GraphSchema.execute(query, context: { <b>current_ability: Ability.new(current_user)</b> })
204
+ Schema.execute(query, context: { <b>current_ability: Ability.new(current_user)</b> })
220
205
  </pre>
221
206
 
222
207
  ### Pundit
@@ -232,12 +217,100 @@ end
232
217
  # Use the ability in your guard
233
218
  PostType = GraphQL::ObjectType.define do
234
219
  name "Post"
235
- <b>guard ->(post, args, ctx) { PostPolicy.new(ctx[:current_user], post).show? }</b>
220
+ guard ->(post, args, ctx) { <b>PostPolicy.new(ctx[:current_user], post).show?</b> }
236
221
  ...
237
222
  end
238
223
 
239
224
  # Pass current_user
240
- GraphSchema.execute(query, context: { <b>current_user: current_user</b> })
225
+ Schema.execute(query, context: { <b>current_user: current_user</b> })
226
+ </pre>
227
+
228
+ ## Error handling
229
+
230
+ By default `GraphQL::Guard` raises a `GraphQL::Guard::NotAuthorizedError` exception if access to the field is not authorized.
231
+ You can change this behavior, by passing custom `not_authorized` lambda. For example:
232
+
233
+ <pre>
234
+ SchemaWithErrors = GraphQL::Schema.define do
235
+ query QueryType
236
+ use GraphQL::Guard.new(
237
+ # By default it raises an error
238
+ # not_authorized: ->(type, field) { raise GraphQL::Guard::NotAuthorizedError.new("#{type}.#{field}") }
239
+
240
+ # Returns an error in the response
241
+ <b>not_authorized: ->(type, field) { GraphQL::ExecutionError.new("Not authorized to access #{type}.#{field}") }</b>
242
+ )
243
+ end
244
+ </pre>
245
+
246
+ In this case executing a query will continue, but return `nil` for not authorized field and also an array of `errors`:
247
+
248
+ <pre>
249
+ SchemaWithErrors.execute("query { <b>posts</b>(user_id: 1) { id title } }")
250
+ # => {
251
+ # "data" => <b>nil</b>,
252
+ # "errors" => [{
253
+ # "messages" => <b>"Not authorized to access Query.posts"</b>,
254
+ # "locations": { "line" => 1, "column" => 9 },
255
+ # "path" => [<b>"posts"</b>]
256
+ # }]
257
+ # }
258
+ </pre>
259
+
260
+ In more advanced cases, you may want not to return `errors` only for some unauthorized fields. Simply return `nil` if user is not authorized to access the field. You can achieve it, for example, by placing the logic into your `PolicyObject`:
261
+
262
+ <pre>
263
+ class <b>GraphqlPolicy</b>
264
+ RULES = {
265
+ PostType => {
266
+ '*': {
267
+ guard: ->(obj, args, ctx) { ... },
268
+ <b>not_authorized:</b> ->(type, field) { GraphQL::ExecutionError.new("Not authorized to access #{type}.#{field}") }
269
+ }
270
+ title: {
271
+ guard: ->(obj, args, ctx) { ... },
272
+ <b>not_authorized:</b> ->(type, field) { nil } # simply return nil if not authorized, no errors
273
+ }
274
+ }
275
+ }
276
+
277
+ def self.guard(type, field)
278
+ RULES.dig(type, field, :guard)
279
+ end
280
+
281
+ def self.<b>not_authorized_handler</b>(type, field)
282
+ RULES</b>.dig(type, field, <b>:not_authorized</b>) || RULES</b>.dig(type, :'*', <b>:not_authorized</b>)
283
+ end
284
+ end
285
+
286
+ Schema = GraphQL::Schema.define do
287
+ query QueryType
288
+ mutation MutationType
289
+
290
+ use GraphQL::Guard.new(
291
+ policy_object: GraphqlPolicy,
292
+ not_authorized: ->(type, field) {
293
+ handler = GraphqlPolicy.<b>not_authorized_handler</b>(type, field)
294
+ handler.call(type, field)
295
+ }
296
+ )
297
+ end
298
+ </pre>
299
+
300
+ ## Schema masking
301
+
302
+ It's possible to hide fields from being introspectable and accessible based on the context. For example:
303
+
304
+ <pre>
305
+ PostType = GraphQL::ObjectType.define do
306
+ name "Post"
307
+
308
+ field :id, !types.ID
309
+ field :title, types.String do
310
+ # The field "title" is accessible only for beta testers
311
+ <b>mask ->(ctx) {</b> ctx[:current_user].beta_tester? <b>}</b>
312
+ end
313
+ end
241
314
  </pre>
242
315
 
243
316
  ## Installation
@@ -264,7 +337,7 @@ It's possible to test fields with `guard` in isolation:
264
337
  # Your type
265
338
  QueryType = GraphQL::ObjectType.define do
266
339
  name "Query"
267
- <b>field :posts</b>, !types[PostType], <b>guard ->(obj, args, ctx) {</b> ... <b>}</b>
340
+ <b>field :posts</b>, !types[!PostType], <b>guard ->(obj, args, ctx) {</b> ... <b>}</b>
268
341
  end
269
342
 
270
343
  # Your test
@@ -282,7 +355,7 @@ If you would like to test your fields with policy objects:
282
355
  # Your type
283
356
  QueryType = GraphQL::ObjectType.define do
284
357
  name "Query"
285
- <b>field :posts</b>, !types[PostType]
358
+ <b>field :posts</b>, !types[!PostType]
286
359
  end
287
360
 
288
361
  # Your policy object
@@ -15,7 +15,7 @@ Gem::Specification.new do |spec|
15
15
  spec.license = "MIT"
16
16
 
17
17
  spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
- f.match(%r{^(test|spec|features)/})
18
+ f.match(%r{^(spec|images)/})
19
19
  end
20
20
  spec.bindir = "exe"
21
21
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
@@ -5,6 +5,7 @@ require "graphql/guard/version"
5
5
 
6
6
  GraphQL::ObjectType.accepts_definitions(guard: GraphQL::Define.assign_metadata_key(:guard))
7
7
  GraphQL::Field.accepts_definitions(guard: GraphQL::Define.assign_metadata_key(:guard))
8
+ GraphQL::Field.accepts_definitions(mask: GraphQL::Define.assign_metadata_key(:mask))
8
9
 
9
10
  module GraphQL
10
11
  class Guard
@@ -22,6 +23,13 @@ module GraphQL
22
23
 
23
24
  def use(schema_definition)
24
25
  schema_definition.instrument(:field, self)
26
+ schema_definition.target.instance_eval do
27
+ def default_filter
28
+ GraphQL::Filter.new(except: default_mask).merge(only: ->(schema_member, ctx) {
29
+ schema_member.metadata[:mask] ? schema_member.metadata[:mask].call(ctx) : true
30
+ })
31
+ end
32
+ end
25
33
  end
26
34
 
27
35
  def instrument(type, field)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GraphQL
4
4
  class Guard
5
- VERSION = "1.0.0"
5
+ VERSION = "1.1.0"
6
6
  end
7
7
  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: 1.0.0
4
+ version: 1.1.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-31 00:00:00.000000000 Z
11
+ date: 2018-05-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: graphql
@@ -115,7 +115,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
115
115
  version: '0'
116
116
  requirements: []
117
117
  rubyforge_project:
118
- rubygems_version: 2.5.2
118
+ rubygems_version: 2.7.6
119
119
  signing_key:
120
120
  specification_version: 4
121
121
  summary: Simple authorization gem for graphql-ruby