Skip to content

Commit 02b1d8c

Browse files
committed
update site
1 parent b6b1af3 commit 02b1d8c

File tree

3 files changed

+227
-59
lines changed

3 files changed

+227
-59
lines changed

CHANGELOG-pro.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@
88

99
### Bug Fix
1010

11+
## 1.7.10 (10 Aug 2018)
12+
13+
### New Features
14+
15+
- Update `PunditIntegration` for arguments, unions, interfaces and mutations
16+
1117
## 1.7.9 (9 Aug 2018)
1218

1319
### New Features

guides/authorization/pundit_integration.md

Lines changed: 220 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ pro: true
1010

1111
[GraphQL::Pro](http://graphql.pro) includes an integration for powering GraphQL authorization with [Pundit](https://github.com/varvet/pundit) policies.
1212

13-
1413
__Why bother?__ You _could_ put your authorization code in your GraphQL types themselves, but writing a separate authorization layer gives you a few advantages:
1514

1615
- Since the authorization code isn't embedded in GraphQL, you can use the same logic in non-GraphQL (or legacy) parts of the app.
@@ -27,21 +26,33 @@ gem "graphql-pro", ">=1.7.9"
2726
gem "graphql", ">=1.8.7"
2827
```
2928

30-
To use the Pundit integration, include modules into your base object and base field classes, then set a default role:
29+
Then, `bundle install`.
30+
31+
Whenever you run queries, include `:current_user` in the context:
3132

3233
```ruby
33-
class Types::BaseField < GraphQL::Schema::Field
34-
# Add the Pundit integration:
35-
include GraphQL::Pro::PunditIntegration::FieldIntegration
36-
# By default, don't require a role at field-level:
37-
pundit_role nil
38-
end
34+
context = {
35+
current_user: current_user,
36+
# ...
37+
}
38+
MySchema.execute(..., context: context)
39+
```
3940

40-
# ...
41+
And read on about the different features of the integration:
4142

42-
class Types::BaseObject < GraphQL::Schema::Field
43-
# Hook up the custom field class:
44-
field_class Types::BaseField
43+
- [Authorizing Objects](#authorizing-objects)
44+
- [Scoping Lists and Connections](#scopes)
45+
- [Authorizing Fields](#authorizing-fields)
46+
- [Authorizing Arguments](#authorizing-arguments)
47+
- [Authorizing Mutations](#authorizing-mutations)
48+
49+
## Authorizing Objects
50+
51+
You can specify Pundit roles that must be satisfied in order for viewers to see objects of a certain type. To get started, include the `ObjectIntegration` in your base object class:
52+
53+
```ruby
54+
# app/graphql/types/base_object.rb
55+
class Types::BaseObject < GraphQL::Schema::Object
4556
# Add the Pundit integration:
4657
include GraphQL::Pro::PunditIntegration::ObjectIntegration
4758
# By default, require staff:
@@ -51,39 +62,18 @@ class Types::BaseObject < GraphQL::Schema::Field
5162
end
5263
```
5364

54-
If you haven't already done so, you should also hook up your base field class to your base interface and base mutation:
55-
56-
```ruby
57-
module Types::BaseInterface
58-
include GraphQL::Schema::Interface
59-
field_class Types::BaseField
60-
end
61-
62-
# And:
63-
64-
class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation
65-
field_class Types::BaseField
66-
end
67-
```
65+
Now, anyone trying to read a GraphQL object will have to pass the `#staff?` check on that object's policy.
6866

69-
Then, in your query context, always include `current_user:`
67+
Then, each child class can override that parent configuration. For example, allow _all_ viewers to read the `Query` root:
7068

7169
```ruby
72-
context = {
73-
current_user: current_user,
74-
# ...
75-
}
76-
MySchema.execute(..., context: context)
70+
class Types::Query < Types::BaseObject
71+
# Allow anyone to see the query root
72+
pundit_role nil
73+
end
7774
```
7875

79-
This will add the following behaviors to your schema:
80-
81-
- Before any object is exposed by GraphQL, it will use a Pundit policy for that object
82-
- When lists or connections are exposed by GraphQL, it will use a Pundit scope to filter that list
83-
84-
When any Policy method returns `false`, the unauthorized object is passed to {{ "Schema.unauthorized_object" | api_doc }}, as described in {% internal_link "Handling unauthorized objects", "/authorization/authorization#handling-unauthorized-objects" %}.
85-
86-
## Policies and Methods
76+
#### Policies and Methods
8777

8878
For each object returned by GraphQL, the integration matches it to a policy and method.
8979

@@ -102,32 +92,79 @@ end
10292

10393
That configuration will call `#employer_or_self?` on the corresponding Pundit policy.
10494

105-
### Default Method
95+
#### Bypassing Policies
10696

107-
This configuration is inherited, so you can set a default value in the parent class, for example:
97+
The integration requires that every object with a `pundit_role` has a corresponding policy class. To allow objects to _skip_ authorization, you can pass `nil` as the role:
10898

10999
```ruby
110-
class Types::BaseObject < GraphQL::Schema::Object
111-
# By default, restrict all GraphQL objects to internal staff;
112-
# override this to allow access to any other users.
113-
pundit_role :staff
100+
class Types::PublicProfile < Types::BaseObject
101+
# Anyone can see this
102+
pundit_role nil
114103
end
115104
```
116105

117-
### Bypassing Policies
106+
#### Handling Unauthorized Objects
118107

119-
The integration requires that every object with a `pundit_role` has a corresponding policy class. To allow objects to _skip_ authorization, you can pass `nil` as the role:
108+
When any Policy method returns `false`, the unauthorized object is passed to {{ "Schema.unauthorized_object" | api_doc }}, as described in {% internal_link "Handling unauthorized objects", "/authorization/authorization#handling-unauthorized-objects" %}.
109+
110+
## Scopes
111+
112+
The Pundit integration adds [Pundit scopes](https://github.com/varvet/pundit#scopes) to GraphQL-Ruby's {% internal_link "list scoping", "/authorization/scoping" %} feature. Any list or connection will be scoped. If a scope is missing, the query will crash rather than risk leaking unfiltered data.
113+
114+
To scope lists of interface or union type, include the integration in your base union class and base interface module:
120115

121116
```ruby
122-
class Types::PublicProfile < Types::BaseObject
123-
# Anyone can see this
117+
class BaseUnion < GraphQL::Schema::Union
118+
include GraphQL::Pro::PunditIntegration::UnionIntegration
119+
end
120+
121+
module BaseInterface
122+
include GraphQL::Schema::Interface
123+
include GraphQL::Pro::PunditIntegration::InterfaceIntegration
124+
end
125+
```
126+
127+
Note that Pundit scopes are best for database relations, but don't play well with Arrays. See below for bypassing Pundit if you want to return an Array.
128+
129+
#### Bypassing scopes
130+
131+
To allow an unscoped relation to be returned from a field, disable scoping with `scope: false`, for example:
132+
133+
```ruby
134+
# Allow anyone to browse the job postings
135+
field :job_postings, [Types::JobPosting], null: false,
136+
scope: false
137+
```
138+
139+
## Authorizing Fields
140+
141+
You can also require certain checks on a field-by-field basis. First, include the integration in your base field class:
142+
143+
```ruby
144+
# app/graphql/types/base_field.rb
145+
class Types::BaseField < GraphQL::Schema::Field
146+
# Add the Pundit integration:
147+
include GraphQL::Pro::PunditIntegration::FieldIntegration
148+
# By default, don't require a role at field-level:
124149
pundit_role nil
125150
end
126151
```
127152

128-
## Field-level authorization
153+
If you haven't already done so, you should also hook up your base field class to your base object and base interface:
129154

130-
Sometimes, some fields require higher permission than others. You can add `pundit_role` to `field(...)` calls to specify a method to call. For example:
155+
```ruby
156+
# app/graphql/types/base_object.rb
157+
class Types::BaseObject < GraphQL::Schema::Object
158+
field_class Types::BaseField
159+
end
160+
# app/graphql/types/base_interface.rb
161+
module Types::BaseInterface
162+
# ...
163+
field_class Types::BaseField
164+
end
165+
```
166+
167+
Then, you can add `pundit_role:` options to your fields:
131168

132169
```ruby
133170
class Types::JobPosting < Types::BaseObject
@@ -141,16 +178,140 @@ class Types::JobPosting < Types::BaseObject
141178
end
142179
```
143180

144-
This way, certain fields can have higher permission requirements.
181+
It will call the named role (eg, `#staff?`) on the parent object's policy (eg `JobPostingPolicy`).
145182

146-
## Scopes
183+
## Authorizing Arguments
147184

148-
The Pundit integration adds [Pundit scopes](https://github.com/varvet/pundit#scopes) to GraphQL-Ruby's {% internal_link "list scoping", "/authorization/scoping" %} feature. `ActiveRecord::Relation`s and `Mongoid::Criteria`s will be matched to Policy scopes and filtered accordingly. If a scope is missing, the query will crash rather than risk leaking unfiltered data.
185+
Similar to field-level checks, you can require certain permissions to _use_ certain arguments. To do this, add the integration to your base argument class:
149186

150-
To allow an unscoped relation to be returned from a field, disable scoping with `scope: false`, for example:
187+
```ruby
188+
class Types::BaseArgument < GraphQL::Schema::Argument
189+
# Include the integration and default to no permissions required
190+
include GraphQL::Pro::PunditIntegration::ArgumentIntegration
191+
pundit_role nil
192+
end
193+
```
194+
195+
Then, make sure your base argument is hooked up to your base field and base input object:
151196

152197
```ruby
153-
# Allow anyone to browse the job postings
154-
field :job_postings, [Types::JobPosting], null: false,
155-
scope: false
198+
class Types::BaseField < GraphQL::Schema::Field
199+
argument_class Types::BaseArgument
200+
# PS: see "Authorizing Fields" to make sure your base field is hooked up to objects, intefaces and mutations
201+
end
202+
203+
class Types::BaseInputObject < GraphQL::Schema::InputObject
204+
argument_class Types::BaseArgument
205+
end
206+
```
207+
208+
Now, arguments accept a `pundit_role:` option, for example:
209+
210+
```ruby
211+
class Types::Company < Types::BaseObject
212+
field :employees, Types::Employee.connection_type, null: true do
213+
# Only admins can filter employees by email:
214+
argument :email, String, required: false, pundit_role: :admin
215+
end
216+
end
217+
```
218+
219+
The role will be called on the parent object's policy, for example `CompanyPolicy#admin?` in the case above.
220+
221+
## Authorizing Mutations
222+
223+
There are a few ways to authorize GraphQL mutations with the Pundit integration:
224+
225+
- Add a [mutation-level roles](#mutation-level-roles)
226+
- Run checks on [objects loaded by ID](#authorizing-loaded-objects)
227+
228+
Also, you can configure [unauthorized object handling](#unauthorized-mutations)
229+
230+
#### Setup
231+
232+
Add `MutationIntegration` to your base mutation, for example:
233+
234+
```ruby
235+
class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation
236+
include GraphQL::Pro::PunditIntegration
237+
end
238+
```
239+
240+
Also, you'll probably want a `BaseMutationPayload` where you can set a default role:
241+
242+
```ruby
243+
class Types::BaseMutationPayload < Types::BaseObject
244+
# If `BaseObject` requires some permissions, override that for mutation results.
245+
# Assume that anyone who can run a mutation can read their generated result types.
246+
pundit_role nil
247+
end
248+
```
249+
250+
And hook it up to your base mutation:
251+
252+
```ruby
253+
class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation
254+
object_class Types::BaseMutationPayload
255+
end
256+
```
257+
258+
#### Mutation-level roles
259+
260+
Each mutation can have a class-level `pundit_role` which will be checked before loading objects or resolving, for example:
261+
262+
```ruby
263+
class Mutations::PromoteEmployee < Mutations::BaseMutation
264+
pundit_role :admin
265+
end
266+
```
267+
268+
In the example above, `PromoteEmployeePolicy#admin?` will be checked before running the mutation.
269+
270+
#### Authorizing Loaded Objects
271+
272+
Mutations can automatically load and authorize objects by ID using the `loads:` option.
273+
274+
Beyond the normal [object reading permissions](#authorizing-objects), you can add an additional role for the specific mutation input using a `pundit_role:` option:
275+
276+
```ruby
277+
class Mutations::FireEmployee < Mutations::BaseMutation
278+
argument :employee_id, ID, required: true,
279+
loads: Types::Employee,
280+
pundit_role: :supervisor,
281+
end
282+
```
283+
284+
In the case above, the mutation will halt unless the `EmployeePolicy#supervisor?` method returns true.
285+
286+
#### Unauthorized Mutations
287+
288+
By default, an authorization failure in a mutation will raise a Ruby exception. You can customize this by implementing `#unauthorized_by_pundit(owner, value)` in your base mutation, for example:
289+
290+
```ruby
291+
class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation
292+
def unauthorized_by_pundit(owner, value)
293+
# No error, just return nil:
294+
nil
295+
end
296+
end
297+
```
298+
299+
The method is called with:
300+
301+
- `owner`: the `GraphQL::Schema::Argument` or mutation class whose role was not satisfied
302+
- `value`: the object which didn't pass for `context[:current_user]`
303+
304+
Since it's a mutation method, you can also access `context` in that method.
305+
306+
Whatever that method returns will be treated as an early return value for the mutation, so for example, you could return {% internal_link "errors as data", "/mutations/mutation_errors" %}:
307+
308+
```ruby
309+
class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation
310+
field :errors, [String], null: true
311+
312+
def unauthorized_by_pundit(owner, value)
313+
# Return errors as data:
314+
{ errors: ["Missing required permission: #{owner.pundit_role}, can't access #{value.inspect}"] }
315+
end
316+
end
156317
```
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
9cc3900fa9b04a667146595141586592e1c7f72d4fb88d6af8e95b74d50cb8c096e5abd00d35e295a66dbdc7774b0afd311a20747e6051d786da36564282aeca

0 commit comments

Comments
 (0)