You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[GraphQL::Pro](http://graphql.pro) includes an integration for powering GraphQL authorization with [Pundit](https://github.com/varvet/pundit) policies.
12
12
13
-
14
13
__Why bother?__ You _could_ put your authorization code in your GraphQL types themselves, but writing a separate authorization layer gives you a few advantages:
15
14
16
15
- 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"
27
26
gem "graphql", ">=1.8.7"
28
27
```
29
28
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:
# 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
+
```
39
40
40
-
# ...
41
+
And read on about the different features of the integration:
41
42
42
-
classTypes::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:
Now, anyone trying to read a GraphQL object will have to pass the `#staff?` check on that object's policy.
68
66
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:
70
68
71
69
```ruby
72
-
context = {
73
-
current_user: current_user,
74
-
# ...
75
-
}
76
-
MySchema.execute(..., context: context)
70
+
classTypes::Query < Types::BaseObject
71
+
# Allow anyone to see the query root
72
+
pundit_role nil
73
+
end
77
74
```
78
75
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
87
77
88
78
For each object returned by GraphQL, the integration matches it to a policy and method.
89
79
@@ -102,32 +92,79 @@ end
102
92
103
93
That configuration will call `#employer_or_self?` on the corresponding Pundit policy.
104
94
105
-
###Default Method
95
+
#### Bypassing Policies
106
96
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:
108
98
109
99
```ruby
110
-
classTypes::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
+
classTypes::PublicProfile < Types::BaseObject
101
+
# Anyone can see this
102
+
pundit_role nil
114
103
end
115
104
```
116
105
117
-
###Bypassing Policies
106
+
#### Handling Unauthorized Objects
118
107
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:
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:
# By default, don't require a role at field-level:
124
149
pundit_role nil
125
150
end
126
151
```
127
152
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:
129
154
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
+
classTypes::BaseObject < GraphQL::Schema::Object
158
+
field_class Types::BaseField
159
+
end
160
+
# app/graphql/types/base_interface.rb
161
+
moduleTypes::BaseInterface
162
+
# ...
163
+
field_class Types::BaseField
164
+
end
165
+
```
166
+
167
+
Then, you can add `pundit_role:` options to your fields:
131
168
132
169
```ruby
133
170
classTypes::JobPosting < Types::BaseObject
@@ -141,16 +178,140 @@ class Types::JobPosting < Types::BaseObject
141
178
end
142
179
```
143
180
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`).
145
182
146
-
## Scopes
183
+
## Authorizing Arguments
147
184
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:
149
186
150
-
To allow an unscoped relation to be returned from a field, disable scoping with `scope: false`, for example:
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:
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:
-`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" %}:
0 commit comments