File: can_can_integration.md

package info (click to toggle)
ruby-graphql 1.13.15-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 8,904 kB
  • sloc: ruby: 69,655; yacc: 444; javascript: 330; makefile: 6
file content (387 lines) | stat: -rw-r--r-- 12,772 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
---
layout: guide
search: true
section: Authorization
title: CanCan Integration
desc: Hook up GraphQL to CanCan abilities
index: 4
pro: true
---


[GraphQL::Pro](https://graphql.pro) includes an integration for powering GraphQL authorization with [CanCan](https://github.com/CanCanCommunity/cancancan).

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

- Since the authorization code isn't embedded in GraphQL, you can use the same logic in non-GraphQL (or legacy) parts of the app.
- The authorization logic can be tested in isolation, so your end-to-end GraphQL tests don't have to cover as many possibilities.

## Getting Started

__NOTE__: Requires the latest gems, so make sure your `Gemfile` has:

```ruby
# For CanCanIntegration:
gem "graphql-pro", ">=1.7.11"
# For list scoping:
gem "graphql", ">=1.8.7"
```

Then, `bundle install`.

Whenever you run queries, include `:current_user` in the context:

```ruby
context = {
  current_user: current_user,
  # ...
}
MySchema.execute(..., context: context)
```

And read on about the different features of the integration:

- [Authorizing Objects](#authorizing-objects)
- [Scoping Lists and Connections](#scopes)
- [Authorizing Fields](#authorizing-fields)
- [Authorizing Arguments](#authorizing-arguments)
- [Authorizing Mutations](#authorizing-mutations)
- [Authorizing Resolvers](#authorizing-resolvers)
- [Custom Abilities Class](#custom-abilities-class)

## Authorizing Objects

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:

```ruby
# app/graphql/types/base_object.rb
class Types::BaseObject < GraphQL::Schema::Object
  # Add the CanCan integration:
  include GraphQL::Pro::CanCanIntegration::ObjectIntegration
  # By default, require `can :read, ...`
  can_can_action(:read)
  # Or, to require no permissions by default:
  # can_can_action(nil)
end
```

Now, anyone fetching an object will need `can :read, ...` for that object.

CanCan configurations are inherited, and can be overridden in subclasses. For example, to allow _all_ viewers to see the `Query` root type:

```ruby
class Types::Query < Types::BaseObject
  # Allow anyone to see the query root
  can_can_action nil
end
```

### Bypassing CanCan

`can_can_action(nil)` will override any inherited configuration and skip CanCan checks for an object, field, argument or mutation.

### Handling Unauthorized Objects

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" %}.

## Scopes

#### ActiveRecord::Relation

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" %}

To scope lists of interface or union type, include the integration in your base union class and base interface module _and_ set a base `can_can_action`, if desired:

```ruby
class BaseUnion < GraphQL::Schema::Union
  include GraphQL::Pro::CanCanIntegration::UnionIntegration
  # To provide a default action for scoping lists:
  can_can_action :read
end

module BaseInterface
  include GraphQL::Schema::Interface
  include GraphQL::Pro::CanCanIntegration::InterfaceIntegration
  # To provide a default action for scoping lists:
  can_can_action :read
end
```

#### Array

For Arrays, the CanCan integration will use `.select { ... }` to filter items using the `can_can_action` from the lists's type.

#### Bypassing scopes

To allow an unscoped relation to be returned from a field, disable scoping with `scope: false`, for example:

```ruby
# Allow anyone to browse the job postings
field :job_postings, [Types::JobPosting], null: false,
  scope: false
```

## Authorizing Fields

You can also require certain checks on a field-by-field basis. First, include the integration in your base field class:

```ruby
# app/graphql/types/base_field.rb
class Types::BaseField < GraphQL::Schema::Field
  # Add the CanCan integration:
  include GraphQL::Pro::CanCanIntegration::FieldIntegration
  # By default, don't require a role at field-level:
  can_can_action nil
end
```

If you haven't already done so, you should also hook up your base field class to your base object and base interface:

```ruby
# app/graphql/types/base_object.rb
class Types::BaseObject < GraphQL::Schema::Object
  field_class Types::BaseField
end
# app/graphql/types/base_interface.rb
module Types::BaseInterface
  # ...
  field_class Types::BaseField
end
# app/graphql/mutations/base_mutation.rb
class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation
  field_class Types::BaseField
end
```

Then, you can add `can_can_action:` options to your fields:

```ruby
class Types::JobPosting < Types::BaseObject
  # Only allow `can :review_applications, JobPosting` users
  # to see who has applied
  field :applicants, [Types::User],
    can_can_action: :review_applicants
end
```

It will require the named action (`:review_applicants`) for the object being viewed (a `JobPosting`).

### Authorizing by attribute

CanCan 3.0 added attribute-level authorization ([pull request](https://github.com/CanCanCommunity/cancancan/pull/474)). You can leverage this in your field definitions with the `can_can_attribute:` configuration:

```ruby
# This will call `.can?(:read, user, :email_address)`
field :email_address, String,
  can_can_action: :read,
  can_can_attribute: :email_address
```

You could also provide a _default value_ for `can_can_attribute` in your base field class:

```ruby
class Types::BaseField
  def initialize(*args, **kwargs, &block)
    # pass all configs to the super class:
    super
    # Then set a new `can_can_attribute` value, if applicable
    if can_can_attribute.nil? && can_can_action.present?
      # `method_sym` is the thing GraphQL-Ruby will use to resolve this field
      can_can_attribute(method_sym)
    end
  end
end
```

(See {{ "GraphQL::Schema::Field" | api_doc }} for the different values available for defaults.)

### Providing a Custom CanCan Subject

Authorization checks are _skipped_ whenever the underlying `object` is `nil`. This can happen in root query fields, for example, when no `root_value: ...` is given. To provide a `can_can_subject` in this case, you can add it as a field configuration:

```ruby
field :users, Types::User.connection_type, null: false,
  can_can_action: :manage,
  # `:all` will be used instead of `object` (which is `nil`)
  can_can_subject: :all
```

The configuration above will call `can?(:manage, :all)` whenever that field is requested.

## Authorizing Arguments

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:

```ruby
class Types::BaseArgument < GraphQL::Schema::Argument
  # Include the integration and default to no permissions required
  include GraphQL::Pro::CanCanIntegration::ArgumentIntegration
  can_can_action nil
end
```

Then, make sure your base argument is hooked up to your base field and base input object:

```ruby
class Types::BaseField < GraphQL::Schema::Field
  argument_class Types::BaseArgument
  # PS: see "Authorizing Fields" to make sure your base field is hooked up to objects, interfaces and mutations
end

class Types::BaseInputObject < GraphQL::Schema::InputObject
  argument_class Types::BaseArgument
end

class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation
  argument_class Types::BaseArgument
end
```

Now, arguments accept a `can_can_action:` option, for example:

```ruby
class Types::Company < Types::BaseObject
  field :employees, Types::Employee.connection_type do
    # Only admins can filter employees by email:
    argument :email, String, required: false, can_can_action: :admin
  end
end
```

This will check for `can :admin, Company` (or a similar rule for the `company` being queried) for the current user.

## Authorizing Mutations

There are a few ways to authorize GraphQL mutations with the CanCan integration:

- Add a [mutation-level roles](#mutation-level-roles)
- Run checks on [objects loaded by ID](#authorizing-loaded-objects)

Also, you can configure [unauthorized object handling](#unauthorized-mutations)

#### Setup

Add `MutationIntegration` to your base mutation, for example:

```ruby
class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation
  include GraphQL::Pro::CanCanIntegration::MutationIntegration

  # Also, to use argument-level authorization:
  argument_class Types::BaseArgument
end
```

Also, you'll probably want a `BaseMutationPayload` where you can set a default role:

```ruby
class Types::BaseMutationPayload < Types::BaseObject
  # If `BaseObject` requires some permissions, override that for mutation results.
  # Assume that anyone who can run a mutation can read their generated result types.
  can_can_action nil
end
```

And hook it up to your base mutation:

```ruby
class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation
  object_class Types::BaseMutationPayload
  field_class Types::BaseField
end
```

#### Mutation-level roles

Each mutation can have a class-level `can_can_action` which will be checked before loading objects or resolving, for example:

```ruby
class Mutations::PromoteEmployee < Mutations::BaseMutation
  can_can_action :run_mutation
end
```

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.)

#### Authorizing Loaded Objects

Mutations can automatically load and authorize objects by ID using the `loads:` option.

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:

```ruby
class Mutations::FireEmployee < Mutations::BaseMutation
  argument :employee_id, ID,
    loads: Types::Employee,
    can_can_action: :supervise,
end
```

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.)

#### Unauthorized Mutations

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:

```ruby
class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation
  def unauthorized_by_can_can(owner, value)
    # No error, just return nil:
    nil
  end
end
```

The method is called with:

- `owner`: the `GraphQL::Schema::Argument` instance or mutation class whose role was not satisfied
- `value`: the object which didn't pass for `context[:current_user]`

Since it's a mutation method, you can also access `context` in that method.

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" %}:

```ruby
class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation
  field :errors, [String]

  def unauthorized_by_can_can(owner, value)
    # Return errors as data:
    { errors: ["Missing required permission: #{owner.can_can_action}, can't access #{value.inspect}"] }
  end
end
```

## Authorizing Resolvers

Resolvers are authorized just like [mutations](#authorizing-mutations), and require similar setup:

```ruby
# app/graphql/resolvers/base_resolver.rb
class Resolvers::BaseResolver < GraphQL::Schema::Resolver
  include GraphQL::Pro::CanCanIntegration::ResolverIntegration
  argument_class BaseArgument
  # can_can_action(nil) # to disable authorization by default
end
```

Beyond that, see [Authorizing Mutations](#authorizing-mutations) above for further details.

## Custom Abilities Class

By default, the integration will look for a top-level `::Ability` class.

If you're using a different class, provide an instance ahead-of-time as `context[:can_can_ability]`

For example, you could _always_ add one in your schema's `#execute` method:

```ruby
class MySchema < GraphQL::Schema
  # Override `execute` to provide a custom Abilities instance for the CanCan integration
  def self.execute(*args, context: {}, **kwargs)
    # Assign `context[:can_can_ability]` to an instance of our custom class
    context[:can_can_ability] = MyAuthorization::CustomAbilitiesClass.new(context[:current_user])
    super
  end
end
```