|
| 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 | +``` |
0 commit comments