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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 33482164f7f2f2a17ba43bc1219975ee6de6ea19
4
- data.tar.gz: fade6ad6159072a1f3609cbc4e43b5fba2ce100b
3
+ metadata.gz: edc74c4db940ec20e1daa0907380a2db57c57bd5
4
+ data.tar.gz: 984fc4fb2913d17f4c6a331ac8f628aace746125
5
5
  SHA512:
6
- metadata.gz: e198c43bd8b1a4e9ce8de01db82605f1d937efbbd8f6f509e89ae1363e253ec43a70251584fa6f7ef7a44d7b89a4029322be4b93ad9e43b4d8e04a6f3a26bea1
7
- data.tar.gz: f465007bb574373cb6d220aea2b6361d828055a69bf57e91ed5e12a6f6fb31d64043b7636e15763eaac922926618ab1e01c280456dd91b022094df6d9e300bb7
6
+ metadata.gz: e2ab0b88f3daf2acdfdba60814b62fdbb3d36c34aec3f4441da3ebdc83daff068c1115d0680959c552cdd894ecc0d55c59fa3db6ca9bcd80039493b08d2158c5
7
+ data.tar.gz: d3987791bdd7cf4a494572ab2bedd08a5992cd71b1909a9d84572fe048e5bdf47fbd0796af38e21d12bfeaefcebf6506d362f315147d2dd51e4c648cd0fc0e51
data/.travis.yml CHANGED
@@ -2,4 +2,6 @@ sudo: false
2
2
  language: ruby
3
3
  rvm:
4
4
  - 2.3.4
5
+ env:
6
+ - CI=true
5
7
  before_install: gem install bundler -v 1.15.2
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.2.0...HEAD)
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
@@ -3,6 +3,7 @@ source "https://rubygems.org"
3
3
  git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
4
 
5
5
  gem "pry"
6
+ gem 'coveralls', require: false
6
7
 
7
8
  # Specify your gem's dependencies in graphql-guard.gemspec
8
9
  gemspec
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
- # define a type
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
- # define a query
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 ->(_obj, args, _ctx) { Post.where(user_id: args[:user_id]) }
47
+ resolve ->(_, args, _) { Post.where(user_id: args[:user_id]) }
41
48
  end
42
49
  end
43
50
 
44
- # define a schema
51
+ # Define a schema
45
52
  Schema = GraphQL::Schema.define do
46
53
  query QueryType
47
54
  end
48
55
 
49
- # execute query
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
- ```ruby
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
- ```ruby
73
+ <pre>
67
74
  QueryType = GraphQL::ObjectType.define do
68
75
  name "Query"
69
- field :posts, !types[PostType] do
76
+
77
+ <b>field :posts</b>, !types[PostType] do
70
78
  argument :user_id, !types.ID
71
- guard ->(_obj, args, ctx) { args[:user_id] == ctx[:current_user].id } # <======= ʘ‿ʘ
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 all fields in the type:
85
+ You can also define `guard`, which will be executed for every (`*`) field in the type:
78
86
 
79
- ```ruby
87
+ <pre>
80
88
  PostType = GraphQL::ObjectType.define do
81
89
  name "Post"
82
- guard ->(_post, ctx) { ctx[:current_user].admin? } # <======= ʘ‿ʘ
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
- ```ruby
94
- class GraphqlPolicy
101
+ <pre>
102
+ class <b>GraphqlPolicy</b>
95
103
  RULES = {
96
104
  QueryType => {
97
- posts: ->(_obj, args, ctx) { args[:user_id] == ctx[:current_user].id }
105
+ <b>posts: ->(obj, args, ctx) {</b> args[:user_id] == ctx[:current_user].id <b>}</b>
98
106
  },
99
107
  PostType => {
100
- '*': ->(_post, ctx) { ctx[:current_user].admin? }
108
+ <b>'*': ->(obj, ctx) {</b> ctx[:current_user].admin? <b>}</b>
101
109
  }
102
110
  }
103
111
 
104
- def self.guard(type, field)
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
- ```ruby
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
- ```ruby
129
- class GraphqlPolicy
136
+ <pre>
137
+ class <b>GraphqlPolicy</b>
130
138
  RULES = {
131
139
  PostType => {
132
- title: ->(_post, ctx) { ctx[:current_user].admin? }, # <======= 2
133
- '*': ->(_post, ctx) { ctx[:current_user].admin? } # <======= 4
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 ->(_post, ctx) { ctx[:current_user].admin? } # <======= 3
145
- field :title, !types.String, guard: ->(_post, _args, ctx) { ctx[:current_user].admin? } # <======= 1
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
- ```ruby
160
- SchemaWithoutExceptions = GraphQL::Schema.define do
167
+ <pre>
168
+ SchemaWithErrors = GraphQL::Schema.define do
161
169
  query QueryType
162
170
  use GraphQL::Guard.new(
163
- # by default it raises an error
164
- # not_authorized: ->(type, field) { raise GraphQL::Guard::NotAuthorizedError.new("#{type}.#{field}") }
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
- # returns an error in the response
167
- not_authorized: ->(type, field) { GraphQL::ExecutionError.new("Not authorized to access #{type}.#{field}") }
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
- ```ruby
175
- SchemaWithoutExceptions.execute("query { posts(user_id: 1) { id title } }")
182
+ <pre>
183
+ SchemaWithErrors.execute("query { <b>posts</b>(user_id: 1) { id title } }")
176
184
  # => {
177
- # "data" => nil,
178
- # "errors" => [{ "messages" => "Not authorized to access Query.posts", "locations": { ... }, "path" => ["posts"] }]
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
- ```ruby
189
- # define an ability
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 # guest user if not logged in
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
- # use the ability in your guard policy object
204
- class GraphqlPolicy
205
- RULES = {
206
- PostType => {
207
- '*': ->(post, ctx) { ctx[:current_ability].can?(:read, post) },
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
- # pass the ability
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
- ```ruby
223
- # define a policy
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
- # use the policy in your guard policy object
231
- class GraphqlPolicy
232
- RULES = {
233
- PostType => {
234
- '*': ->(post, ctx) { PostPolicy.new(ctx[:current_user], post).show? },
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 = inline_field_guard(field) || policy_object_guard(type, field.name.to_sym)
29
- type_guard_proc = inline_type_guard(type) || policy_object_guard(type, ANY_FIELD_NAME)
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GraphQL
4
4
  class Guard
5
- VERSION = "0.3.0"
5
+ VERSION = "0.4.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: 0.3.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-19 00:00:00.000000000 Z
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: