exAspArk/graphql-guard

View on GitHub
README.md

Summary

Maintainability
Test Coverage
# graphql-guard

[![Build Status](https://travis-ci.org/exAspArk/graphql-guard.svg?branch=master)](https://travis-ci.org/exAspArk/graphql-guard)
[![Coverage Status](https://coveralls.io/repos/github/exAspArk/graphql-guard/badge.svg)](https://coveralls.io/github/exAspArk/graphql-guard)
[![Code Climate](https://img.shields.io/codeclimate/maintainability/exAspArk/graphql-guard.svg)](https://codeclimate.com/github/exAspArk/graphql-guard/maintainability)
[![Downloads](https://img.shields.io/gem/dt/graphql-guard.svg)](https://rubygems.org/gems/graphql-guard)
[![Latest Version](https://img.shields.io/gem/v/graphql-guard.svg)](https://rubygems.org/gems/graphql-guard)

This gem provides a field-level authorization for [graphql-ruby](https://github.com/rmosolgo/graphql-ruby).

## Contents

* [Usage](#usage)
  * [Inline policies](#inline-policies)
  * [Policy object](#policy-object)
* [Priority order](#priority-order)
* [Integration](#integration)
  * [CanCanCan](#cancancan)
  * [Pundit](#pundit)
* [Error handling](#error-handling)
* [Schema masking](#schema-masking)
* [Installation](#installation)
* [Testing](#testing)
* [Development](#development)
* [Contributing](#contributing)
* [License](#license)
* [Code of Conduct](#code-of-conduct)

## Usage

Define a GraphQL schema:

```ruby
# Define a type
class PostType < GraphQL::Schema::Object
  field :id, ID, null: false
  field :title, String, null: true
end

# Define a query
class QueryType < GraphQL::Schema::Object
  field :posts, [PostType], null: false do
    argument :user_id, ID, required: true
  end

  def posts(user_id:)
    Post.where(user_id: user_id)
  end
end

# Define a schema
class Schema < GraphQL::Schema
  use GraphQL::Execution::Interpreter
  use GraphQL::Analysis::AST
  query QueryType
end

# Execute query
Schema.execute(query, variables: { userId: 1 }, context: { current_user: current_user })
```

### Inline policies

Add `GraphQL::Guard` to your schema:

<pre>
class Schema < GraphQL::Schema
  use GraphQL::Execution::Interpreter
  use GraphQL::Analysis::AST
  query QueryType
  <b>use GraphQL::Guard.new</b>
end
</pre>

Now you can define `guard` for a field, which will check permissions before resolving the field:

<pre>
class QueryType < GraphQL::Schema::Object
  <b>field :posts</b>, [PostType], null: false do
    argument :user_id, ID, required: true
    <b>guard ->(obj, args, ctx) {</b> args[:user_id] == ctx[:current_user].id <b>}</b>
  end
  ...
end
</pre>

You can also define `guard`, which will be executed for every `*` field in the type:

<pre>
class PostType < GraphQL::Schema::Object
  <b>guard ->(obj, args, ctx) {</b> ctx[:current_user].admin? <b>}</b>
  ...
end
</pre>

If `guard` block returns `nil` or `false`, then it'll raise a `GraphQL::Guard::NotAuthorizedError` error.

### Policy object

Alternatively, it's possible to extract and describe all policies by using PORO (Plain Old Ruby Object), which should implement a `guard` method. For example:

<pre>
class <b>GraphqlPolicy</b>
  RULES = {
    QueryType => {
      <b>posts: ->(obj, args, ctx) {</b> args[:user_id] == ctx[:current_user].id <b>}</b>
    },
    PostType => {
      <b>'*': ->(obj, args, ctx) {</b> ctx[:current_user].admin? <b>}</b>
    }
  }

  def self.<b>guard(type, field)</b>
    RULES.dig(type, field)
  end
end
</pre>

Pass this object to `GraphQL::Guard`:

<pre>
class Schema < GraphQL::Schema
  use GraphQL::Execution::Interpreter
  use GraphQL::Analysis::AST
  query QueryType
  use GraphQL::Guard.new(<b>policy_object: GraphqlPolicy</b>)
end
</pre>

When using a policy object, you may want to allow [introspection queries](http://graphql.org/learn/introspection/) to skip authorization. A simple way to avoid having to whitelist every introspection type in the `RULES` hash of your policy object is to check the `type` parameter in the `guard` method:

<pre>
def self.guard(type, field)
  <b>type.introspection? ? ->(_obj, _args, _ctx) { true } :</b> RULES.dig(type, field) # or "false" to restrict an access
end
</pre>

## Priority order

`GraphQL::Guard` will use the policy in the following order of priority:

1. Inline policy on the field.
2. Policy from the policy object on the field.
3. Inline policy on the type.
2. Policy from the policy object on the type.

<pre>
class <b>GraphqlPolicy</b>
  RULES = {
    PostType => {
      <b>'*': ->(obj, args, ctx) {</b> ctx[:current_user].admin? <b>}</b>,                                # <=== <b>4</b>
      <b>title: ->(obj, args, ctx) {</b> ctx[:current_user].admin? <b>}</b>                               # <=== <b>2</b>
    }
  }

  def self.guard(type, field)
    RULES.dig(type, field)
  end
end

class PostType < GraphQL::Schema::Object
  <b>guard ->(obj, args, ctx) {</b> ctx[:current_user].admin? <b>}</b>                                    # <=== <b>3</b>
  field :title, String, null: true, <b>guard: ->(obj, args, ctx) {</b> ctx[:current_user].admin? <b>}</b> # <=== <b>1</b>
end

class Schema < GraphQL::Schema
  use GraphQL::Execution::Interpreter
  use GraphQL::Analysis::AST
  query QueryType
  use GraphQL::Guard.new(<b>policy_object: GraphqlPolicy</b>)
end
</pre>

## Integration

You can simply reuse your existing policies if you really want. You don't need any monkey patches or magic for it ;)

### CanCanCan

<pre>
# Define an ability
class <b>Ability</b>
  include CanCan::Ability

  def initialize(user)
    user ||= User.new
    if user.admin?
      can :manage, :all
    else
      can :read, Post, author_id: user.id
    end
  end
end

# Use the ability in your guard
class PostType < GraphQL::Schema::Object
  guard ->(post, args, ctx) { <b>ctx[:current_ability].can?(:read, post)</b> }
  ...
end

# Pass the ability
Schema.execute(query, context: { <b>current_ability: Ability.new(current_user)</b> })
</pre>

### Pundit

<pre>
# Define a policy
class <b>PostPolicy</b> < ApplicationPolicy
  def show?
    user.admin? || record.author_id == user.id
  end
end

# Use the ability in your guard
class PostType < GraphQL::Schema::Object
  guard ->(post, args, ctx) { <b>PostPolicy.new(ctx[:current_user], post).show?</b> }
  ...
end

# Pass current_user
Schema.execute(query, context: { <b>current_user: current_user</b> })
</pre>

## Error handling

By default `GraphQL::Guard` raises a `GraphQL::Guard::NotAuthorizedError` exception if access to the field is not authorized.
You can change this behavior, by passing custom `not_authorized` lambda. For example:

<pre>
class SchemaWithErrors < GraphQL::Schema
  use GraphQL::Execution::Interpreter
  use GraphQL::Analysis::AST
  query QueryType
  use GraphQL::Guard.new(
    # By default it raises an error
    # not_authorized: ->(type, field) do
    #   raise GraphQL::Guard::NotAuthorizedError.new("#{type}.#{field}")
    # end

    # Returns an error in the response
    <b>not_authorized: ->(type, field) do
      GraphQL::ExecutionError.new("Not authorized to access #{type}.#{field}")
    end</b>
  )
end
</pre>

In this case executing a query will continue, but return `nil` for not authorized field and also an array of `errors`:

<pre>
SchemaWithErrors.execute("query { <b>posts</b>(user_id: 1) { id title } }")
# => {
#   "data" => <b>nil</b>,
#   "errors" => [{
#     "messages" => <b>"Not authorized to access Query.posts"</b>,
#     "locations": { "line" => 1, "column" => 9 },
#     "path" => [<b>"posts"</b>]
#   }]
# }
</pre>

In more advanced cases, you may want not to return `errors` only for some unauthorized fields. Simply return `nil` if user is not authorized to access the field. You can achieve it, for example, by placing the logic into your `PolicyObject`:

<pre>
class <b>GraphqlPolicy</b>
  RULES = {
    PostType => {
      '*': {
        guard: ->(obj, args, ctx) { ... },
        <b>not_authorized:</b> ->(type, field) { GraphQL::ExecutionError.new("Not authorized to access #{type}.#{field}") }
      }
      title: {
        guard: ->(obj, args, ctx) { ... },
        <b>not_authorized:</b> ->(type, field) { nil } # simply return nil if not authorized, no errors
      }
    }
  }

  def self.guard(type, field)
    RULES.dig(type, field, :guard)
  end

  def self.<b>not_authorized_handler</b>(type, field)
    RULES</b>.dig(type, field, <b>:not_authorized</b>) || RULES</b>.dig(type, :'*', <b>:not_authorized</b>)
  end
end

class Schema < GraphQL::Schema
  use GraphQL::Execution::Interpreter
  use GraphQL::Analysis::AST
  query QueryType
  mutation MutationType

  use GraphQL::Guard.new(
    policy_object: GraphqlPolicy,
    not_authorized: ->(type, field) {
      handler = GraphqlPolicy.<b>not_authorized_handler</b>(type, field)
      handler.call(type, field)
    }
  )
end
</pre>

## Schema masking

It's possible to hide fields from being introspectable and accessible based on the context. For example:

<pre>
class PostType < GraphQL::Schema::Object
  field :id, ID, null: false
  field :title, String, null: true do
    # The field "title" is accessible only for beta testers
    <b>mask ->(ctx) {</b> ctx[:current_user].beta_tester? <b>}</b>
  end
end
</pre>

## Installation

Add this line to your application's Gemfile:

```ruby
gem 'graphql-guard'
```

And then execute:

    $ bundle

Or install it yourself as:

    $ gem install graphql-guard

## Testing

It's possible to test fields with `guard` in isolation:

<pre>
# Your type
class QueryType < GraphQL::Schema::Object
  field :posts, [PostType], null: false, <b>guard ->(obj, args, ctx) {</b> ... <b>}</b>
end

# Your test
<b>require "graphql/guard/testing"</b>

posts = QueryType.<b>field_with_guard('posts')</b>
result = posts.<b>guard(obj, args, ctx)</b>
expect(result).to eq(true)
</pre>

If you would like to test your fields with policy objects:


<pre>
# Your type
class QueryType < GraphQL::Schema::Object
  field :posts, [PostType], null: false
end

# Your policy object
class <b>GraphqlPolicy</b>
  def self.<b>guard</b>(type, field)
    <b>->(obj, args, ctx) {</b> ... <b>}</b>
  end
end

# Your test
<b>require "graphql/guard/testing"</b>

posts = QueryType.<b>field_with_guard('posts', GraphqlPolicy)</b>
result = posts.<b>guard(obj, args, ctx)</b>
expect(result).to eq(true)
</pre>

## Development

After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).

## Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/exAspArk/graphql-guard. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.

## License

The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).

## Code of Conduct

Everyone interacting in the Graphql::Guard project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/exAspArk/graphql-guard/blob/master/CODE_OF_CONDUCT.md).