Skip to content

Commit 5d71dc2

Browse files
committed
Merge branch 'master' into 1.9-dev
2 parents e6b4695 + 5098895 commit 5d71dc2

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

63 files changed

+1328
-1007
lines changed

CHANGELOG-pro.md

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

99
### Bug Fix
1010

11+
## 1.7.12 (29 Aug 2018)
12+
13+
### New Features
14+
15+
- Add `GraphQL::Pro::CanCanIntegration` which leverages GraphQL-Ruby's built-in auth
16+
17+
## 1.7.11 (21 Aug 2018)
18+
19+
### Bug Fix
20+
21+
- `PunditIntegration`: Don't try to authorize loaded objects when they're `nil`
22+
1123
## 1.7.10 (10 Aug 2018)
1224

1325
### New Features

CHANGELOG.md

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

99
### Bug fixes
1010

11+
## 1.8.8 (27 Aug 2018)
12+
13+
### Bug fixes
14+
15+
- When using `RelayClassicMutation`, `client_mutation_id` will no longer be passed to `authorized?` method #1771
16+
- Fix issue in schema upgrader script which would cause `.to_non_null_type` calls in type definition to be ignored #1783
17+
- Ensure enum values respond to `graphql_name` #1792
18+
- Fix infinite resolution bug that could occur when an exception not inheriting from `StandardError` is thrown #1804
19+
20+
### New features
21+
22+
- Add `#path` method to schema members #1766
23+
- Add `as:` argument to allow overriding the name of the argument when using `loads:` #1773
24+
- Add support for list of IDs when using `loads:` in an argument definition #1797
25+
1126
## 1.8.7 (9 Aug 2018)
1227

1328
### Breaking changes
Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
---
2+
layout: guide
3+
search: true
4+
section: Authorization
5+
title: CanCan Integration
6+
desc: Hook up GraphQL to CanCan abilities
7+
index: 4
8+
pro: true
9+
---
10+
11+
12+
[GraphQL::Pro](http://graphql.pro) includes an integration for powering GraphQL authorization with [CanCan](https://github.com/CanCanCommunity/cancancan).
13+
14+
__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+
16+
- Since the authorization code isn't embedded in GraphQL, you can use the same logic in non-GraphQL (or legacy) parts of the app.
17+
- The authorization logic can be tested in isolation, so your end-to-end GraphQL tests don't have to cover as many possibilities.
18+
19+
## Getting Started
20+
21+
__NOTE__: Requires the latest gems, so make sure your `Gemfile` has:
22+
23+
```ruby
24+
# For CanCanIntegration:
25+
gem "graphql-pro", ">=1.7.11"
26+
# For list scoping:
27+
gem "graphql", ">=1.8.7"
28+
```
29+
30+
Then, `bundle install`.
31+
32+
Whenever you run queries, include `:current_user` in the context:
33+
34+
```ruby
35+
context = {
36+
current_user: current_user,
37+
# ...
38+
}
39+
MySchema.execute(..., context: context)
40+
```
41+
42+
And read on about the different features of the integration:
43+
44+
- [Authorizing Objects](#authorizing-objects)
45+
- [Scoping Lists and Connections](#scopes)
46+
- [Authorizing Fields](#authorizing-fields)
47+
- [Authorizing Arguments](#authorizing-arguments)
48+
- [Authorizing Mutations](#authorizing-mutations)
49+
- [Custom Abilities Class](#custom-abilities-class)
50+
51+
## Authorizing Objects
52+
53+
For each object type, you can assign a required action for Ruby objects of that type. To get started, include the `ObjectIntegration` in your base object class:
54+
55+
```ruby
56+
# app/graphql/types/base_object.rb
57+
class Types::BaseObject < GraphQL::Schema::Object
58+
# Add the CanCan integration:
59+
include GraphQL::Pro::CanCanIntegration::ObjectIntegration
60+
# By default, require `can :read, ...`
61+
can_can_action(:read)
62+
# Or, to require no permissions by default:
63+
# can_can_action(nil)
64+
end
65+
```
66+
67+
Now, anyone fetching an object will need `can :read, ...` for that object.
68+
69+
CanCan configurations are inherited, and can be overridden in subclasses. For example, to allow _all_ viewers to see the `Query` root type:
70+
71+
```ruby
72+
class Types::Query < Types::BaseObject
73+
# Allow anyone to see the query root
74+
can_can_action nil
75+
end
76+
```
77+
78+
### Bypassing CanCan
79+
80+
`can_can_action(nil)` will override any inherited configuration and skip CanCan checks for an object, field, argument or mutation.
81+
82+
### Handling Unauthorized Objects
83+
84+
When any CanCan check 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+
## Scopes
87+
88+
The CanCan integration adds [CanCan's `.accessible_by`](https://github.com/cancancommunity/cancancan/wiki/Fetching-Records) to GraphQL-Ruby's {% internal_link "list scoping", "/authorization/scoping" %}
89+
90+
To scope lists of interface or union type, include the integration in your base union class and base interface module:
91+
92+
```ruby
93+
class BaseUnion < GraphQL::Schema::Union
94+
include GraphQL::Pro::CanCanIntegration::UnionIntegration
95+
end
96+
97+
module BaseInterface
98+
include GraphQL::Schema::Interface
99+
include GraphQL::Pro::CanCanIntegration::InterfaceIntegration
100+
end
101+
```
102+
103+
Note that `.accessible_by` is best for database relations, but doesn't play well with Arrays. See below for bypassing CanCan if you want to return an Array.
104+
105+
#### Bypassing scopes
106+
107+
To allow an unscoped relation to be returned from a field, disable scoping with `scope: false`, for example:
108+
109+
```ruby
110+
# Allow anyone to browse the job postings
111+
field :job_postings, [Types::JobPosting], null: false,
112+
scope: false
113+
```
114+
115+
## Authorizing Fields
116+
117+
You can also require certain checks on a field-by-field basis. First, include the integration in your base field class:
118+
119+
```ruby
120+
# app/graphql/types/base_field.rb
121+
class Types::BaseField < GraphQL::Schema::Field
122+
# Add the CanCan integration:
123+
include GraphQL::Pro::CanCanIntegration::FieldIntegration
124+
# By default, don't require a role at field-level:
125+
can_can_action nil
126+
end
127+
```
128+
129+
If you haven't already done so, you should also hook up your base field class to your base object and base interface:
130+
131+
```ruby
132+
# app/graphql/types/base_object.rb
133+
class Types::BaseObject < GraphQL::Schema::Object
134+
field_class Types::BaseField
135+
end
136+
# app/graphql/types/base_interface.rb
137+
module Types::BaseInterface
138+
# ...
139+
field_class Types::BaseField
140+
end
141+
```
142+
143+
Then, you can add `can_can_action:` options to your fields:
144+
145+
```ruby
146+
class Types::JobPosting < Types::BaseObject
147+
# Only allow `can :review_applications, JobPosting` users
148+
# to see who has applied
149+
field :applicants, [Types::User], null: true,
150+
can_can_action: :review_applicants
151+
end
152+
```
153+
154+
It will require the named action (`:review_applicants`) for the object being viewed (a `JobPosting`).
155+
156+
## Authorizing Arguments
157+
158+
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:
159+
160+
```ruby
161+
class Types::BaseArgument < GraphQL::Schema::Argument
162+
# Include the integration and default to no permissions required
163+
include GraphQL::Pro::CanCanIntegration::ArgumentIntegration
164+
can_can_action nil
165+
end
166+
```
167+
168+
Then, make sure your base argument is hooked up to your base field and base input object:
169+
170+
```ruby
171+
class Types::BaseField < GraphQL::Schema::Field
172+
argument_class Types::BaseArgument
173+
# PS: see "Authorizing Fields" to make sure your base field is hooked up to objects, interfaces and mutations
174+
end
175+
176+
class Types::BaseInputObject < GraphQL::Schema::InputObject
177+
argument_class Types::BaseArgument
178+
end
179+
```
180+
181+
Now, arguments accept a `can_can_action:` option, for example:
182+
183+
```ruby
184+
class Types::Company < Types::BaseObject
185+
field :employees, Types::Employee.connection_type, null: true do
186+
# Only admins can filter employees by email:
187+
argument :email, String, required: false, can_can_action: :admin
188+
end
189+
end
190+
```
191+
192+
This will check for `can :admin, Company` (or a similar rule for the `company` being queried) for the current user.
193+
194+
## Authorizing Mutations
195+
196+
There are a few ways to authorize GraphQL mutations with the CanCan integration:
197+
198+
- Add a [mutation-level roles](#mutation-level-roles)
199+
- Run checks on [objects loaded by ID](#authorizing-loaded-objects)
200+
201+
Also, you can configure [unauthorized object handling](#unauthorized-mutations)
202+
203+
#### Setup
204+
205+
Add `MutationIntegration` to your base mutation, for example:
206+
207+
```ruby
208+
class Mutations::BaseMutation < GraphQL::Schema::Mutation
209+
include GraphQL::Pro::CanCanIntegration::MutationIntegration
210+
211+
# Also, to use argument-level authorization:
212+
argument_class Types::BaseArgument
213+
end
214+
```
215+
216+
Also, you'll probably want a `BaseMutationPayload` where you can set a default role:
217+
218+
```ruby
219+
class Types::BaseMutationPayload < Types::BaseObject
220+
# If `BaseObject` requires some permissions, override that for mutation results.
221+
# Assume that anyone who can run a mutation can read their generated result types.
222+
can_can_action nil
223+
end
224+
```
225+
226+
And hook it up to your base mutation:
227+
228+
```ruby
229+
class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation
230+
object_class Types::BaseMutationPayload
231+
end
232+
```
233+
234+
#### Mutation-level roles
235+
236+
Each mutation can have a class-level `can_can_action` which will be checked before loading objects or resolving, for example:
237+
238+
```ruby
239+
class Mutations::PromoteEmployee < Mutations::BaseMutation
240+
can_can_action :run_mutation
241+
end
242+
```
243+
244+
In the example above, `can :run_mutation, Mutations::PromoteEmployee` will be checked before running the mutation. (The currently-running instance of `Mutations::PromoteEmployee` is passed to the ability checker.)
245+
246+
#### Authorizing Loaded Objects
247+
248+
Mutations can automatically load and authorize objects by ID using the `loads:` option.
249+
250+
Beyond the normal [object reading permissions](#authorizing-objects), you can add an additional role for the specific mutation input using a `can_can_action:` option:
251+
252+
```ruby
253+
class Mutations::FireEmployee < Mutations::BaseMutation
254+
argument :employee_id, ID, required: true,
255+
loads: Types::Employee,
256+
can_can_action: :supervise,
257+
end
258+
```
259+
260+
In the case above, the mutation will halt unless the `can :supervise, ...` check returns true. (The fetched instance of `Employee` is passed to the ability checker.)
261+
262+
#### Unauthorized Mutations
263+
264+
By default, an authorization failure in a mutation will raise a Ruby exception. You can customize this by implementing `#unauthorized_by_can_can(owner, value)` in your base mutation, for example:
265+
266+
```ruby
267+
class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation
268+
def unauthorized_by_can_can(owner, value)
269+
# No error, just return nil:
270+
nil
271+
end
272+
end
273+
```
274+
275+
The method is called with:
276+
277+
- `owner`: the `GraphQL::Schema::Argument` instance or mutation class whose role was not satisfied
278+
- `value`: the object which didn't pass for `context[:current_user]`
279+
280+
Since it's a mutation method, you can also access `context` in that method.
281+
282+
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" %}:
283+
284+
```ruby
285+
class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation
286+
field :errors, [String], null: true
287+
288+
def unauthorized_by_can_can(owner, value)
289+
# Return errors as data:
290+
{ errors: ["Missing required permission: #{owner.can_can_action}, can't access #{value.inspect}"] }
291+
end
292+
end
293+
```
294+
295+
## Custom Abilities Class
296+
297+
By default, the integration will look for a top-level `::Ability` class.
298+
299+
If you're using a different class, provide an instance ahead-of-time as `context[:can_can_ability]`
300+
301+
For example, you could _always_ add one in your schema's `#execute` method:
302+
303+
```ruby
304+
class MySchema < GraphQL::Schema
305+
# Override `execute` to provide a custom Abilities instance for the CanCan integration
306+
def self.execute(*args, context: {}, **kwargs)
307+
# Assign `context[:can_can_ability]` to an instance of our custom class
308+
context[:can_can_ability] = MyAuthorization::CustomAbilitiesClass.new(context[:current_user])
309+
super
310+
end
311+
end
312+
```

guides/authorization/overview.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,4 +135,4 @@ To accomplish these, you can use GraphQL-Ruby's authorization framework. The fra
135135
- {% internal_link "Accessibility", "/authorization/accessibility" %} prevents running queries which access parts of the GraphQL schema, unless users have the required permission.
136136
- {% internal_link "Authorization", "/authorization/authorization" %} checks application objects during execution to be sure the user has permission to access them.
137137

138-
Also, [GraphQL::Pro](http://graphql.pro) has integrations for CanCan and {% internal_link "Pundit", "/authorization/pundit_integration" %}.
138+
Also, [GraphQL::Pro](http://graphql.pro) has integrations for {% internal_link "CanCan", "/authorization/can_can_integration" %} and {% internal_link "Pundit", "/authorization/pundit_integration" %}.

guides/authorization/pundit_integration.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ Then, make sure your base argument is hooked up to your base field and base inpu
197197
```ruby
198198
class Types::BaseField < GraphQL::Schema::Field
199199
argument_class Types::BaseArgument
200-
# PS: see "Authorizing Fields" to make sure your base field is hooked up to objects, intefaces and mutations
200+
# PS: see "Authorizing Fields" to make sure your base field is hooked up to objects, interfaces and mutations
201201
end
202202

203203
class Types::BaseInputObject < GraphQL::Schema::InputObject
@@ -232,8 +232,11 @@ Also, you can configure [unauthorized object handling](#unauthorized-mutations)
232232
Add `MutationIntegration` to your base mutation, for example:
233233

234234
```ruby
235-
class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation
236-
include GraphQL::Pro::PunditIntegration
235+
class Mutations::BaseMutation < GraphQL::Schema::Mutation
236+
include GraphQL::Pro::PunditIntegration::MutationIntegration
237+
238+
# Also, to use argument-level authorization:
239+
argument_class Types::BaseArgument
237240
end
238241
```
239242

0 commit comments

Comments
 (0)