Full Code of palkan/action_policy-graphql for AI

master 9338cefef3ab cached
35 files
52.1 KB
14.2k tokens
113 symbols
1 requests
Download .txt
Repository: palkan/action_policy-graphql
Branch: master
Commit: 9338cefef3ab
Files: 35
Total size: 52.1 KB

Directory structure:
gitextract_7d3k2x6f/

├── .github/
│   ├── ISSUE_TEMPLATE.md
│   ├── PULL_REQUEST_TEMPLATE.md
│   └── workflows/
│       ├── release.yml
│       ├── rubocop.yml
│       ├── test-jruby.yml
│       └── test.yml
├── .gitignore
├── .rubocop-md.yml
├── .rubocop.yml
├── CHANGELOG.md
├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── action_policy-graphql.gemspec
├── bin/
│   ├── console
│   └── setup
├── gemfiles/
│   ├── action_policy/
│   │   └── master.gemfile
│   ├── graphql/
│   │   └── master.gemfile
│   ├── jruby.gemfile
│   └── rubocop.gemfile
├── lib/
│   ├── action_policy/
│   │   ├── graphql/
│   │   │   ├── authorized_field.rb
│   │   │   ├── behaviour.rb
│   │   │   ├── fields.rb
│   │   │   ├── types/
│   │   │   │   ├── authorization_result.rb
│   │   │   │   └── failure_reasons.rb
│   │   │   └── version.rb
│   │   └── graphql.rb
│   └── action_policy-graphql.rb
└── spec/
    ├── action_policy/
    │   └── graphql/
    │       ├── authorized_spec.rb
    │       ├── behaviour_spec.rb
    │       └── expose_authorization_rules_spec.rb
    ├── spec_helper.rb
    └── support/
        ├── graphql_context.rb
        └── schema.rb

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/ISSUE_TEMPLATE.md
================================================
<!--
  This template is for bug reports. If you are reporting a bug, please continue on. If you are here for another reason,
  feel free to skip the rest of this template.
-->

### Tell us about your environment

**Ruby Version:**

**Framework Version (Rails, whatever):**

**Action Policy Version:**

**Action Policy GraphQL Version:**

### What did you do?

### What did you expect to happen?

### What actually happened?


================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
<!--
  First of all, thanks for contributing!

  If it's a typo fix or minor documentation update feel free to skip the rest of this template!
-->

<!--
  If it's a bug fix, then link it to the issue, for example:

  Fixes #xxx
-->


<!--
  Otherwise, describe the changes: 

### What is the purpose of this pull request?

### What changes did you make? (overview)

### Is there anything you'd like reviewers to focus on?

-->

PR checklist:

- [ ] Tests included
- [ ] Documentation updated
- [ ] Changelog entry added


================================================
FILE: .github/workflows/release.yml
================================================
name: Release gems
on:
  workflow_dispatch:
  push:
    tags:
      - v*

jobs:
  release:
    runs-on: ubuntu-latest
    permissions:
      contents: write
      id-token: write

    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0 # Fetch current tag as annotated. See https://github.com/actions/checkout/issues/290
      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: 3.2
          bundler-cache: true
      - name: Configure RubyGems Credentials
        uses: rubygems/configure-rubygems-credentials@main
      - name: Publish to RubyGems
        run: |
          gem install gem-release
          gem release


================================================
FILE: .github/workflows/rubocop.yml
================================================
name: Lint Ruby

on:
  push:
    branches:
    - master
  pull_request:

jobs:
  rubocop:
    runs-on: ubuntu-latest
    env:
      BUNDLE_JOBS: 4
      BUNDLE_RETRY: 3
      BUNDLE_GEMFILE: "gemfiles/rubocop.gemfile"
      CI: true
    steps:
    - uses: actions/checkout@v4
    - uses: ruby/setup-ruby@v1
      with:
        ruby-version: 3.2
        bundler-cache: true
    - name: Lint Ruby code with RuboCop
      run: |
        bundle exec rubocop


================================================
FILE: .github/workflows/test-jruby.yml
================================================
name: JRuby Build

on:
  push:
    branches:
    - master
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    env:
      BUNDLE_JOBS: 4
      BUNDLE_RETRY: 3
      CI: true
    steps:
    - uses: actions/checkout@v4
    - uses: ruby/setup-ruby@v1
      with:
        ruby-version: jruby
        bundler-cache: true
    - name: Run RSpec tests
      run: |
        bundle exec rspec --force-color


================================================
FILE: .github/workflows/test.yml
================================================
name: Build

on:
  push:
    branches:
    - master
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    env:
      BUNDLE_JOBS: 4
      BUNDLE_RETRY: 3
      CI: true
    strategy:
      fail-fast: false
      matrix:
        ruby: ["3.3"]
        gemfile: [
          "Gemfile"
        ]
        include:
        - ruby: "3.2"
          gemfile: "gemfiles/action_policy/master.gemfile"
        - ruby: "3.0"
          gemfile: "Gemfile"
        - ruby: "3.0"
          gemfile: "gemfiles/action_policy/master.gemfile"
        - ruby: "2.7"
          gemfile: "Gemfile"
    steps:
    - uses: actions/checkout@v4
    - uses: ruby/setup-ruby@v1
      with:
        ruby-version: ${{ matrix.ruby }}
        bundler-cache: true
    - name: Run RSpec tests
      run: |
        bundle exec rspec --force-color


================================================
FILE: .gitignore
================================================
/.bundle/
/.yardoc
/Gemfile.lock
/_yardoc/
/coverage/
/doc/
/pkg/
/spec/reports/
/tmp/
Gemfile.local
*.lock

================================================
FILE: .rubocop-md.yml
================================================
inherit_from: ".rubocop.yml"

require:
  - rubocop-md

AllCops:
  Include:
    - '**/*.md'

Lint/Void:
  Exclude:
    - '**/*.md'

Lint/DuplicateMethods:
  Exclude:
    - '**/*.md'


================================================
FILE: .rubocop.yml
================================================
require:
  - standard/cop/block_single_line_braces

inherit_gem:
  standard: config/base.yml

AllCops:
  Exclude:
    - 'bin/*'
    - 'tmp/**/*'
    - 'Gemfile'
    - 'vendor/**/*'
    - 'gemfiles/**/*'
    - 'lib/generators/**/templates/**/*'
  DisplayCopNames: true
  SuggestExtensions: false
  TargetRubyVersion: 2.6

Standard/BlockSingleLineBraces:
  Enabled: false

Style/FrozenStringLiteralComment:
  Enabled: true

Naming/FileName:
  Exclude:
   - '**/*.md'
   - 'lib/action_policy-graphql.rb'


================================================
FILE: CHANGELOG.md
================================================
# Change log

## master (unreleased)

## 0.6.0 (2024-07-08)

- Fix compatibility with Action Policy 0.7.0. ([@palkan][])

## 0.5.4 (2023-12-22)

- Do not mutate passed field options. ([@palkan][])

  Fixes compatibility with `with_options` approach.

## 0.5.3 (2021-02-26)

- Fix compatibility with graphql-ruby 1.12.4 ([@haines][])

## 0.5.2 (2020-10-20)

- Fix modules reloading in development. ([@rzaharenkov][])

## 0.5.1 (2020-10-08)

- Fix mutations authorization (clean up around `authorize_mutation_raise_exception` configuration parameter). ([@rzaharenkov][])

- Add deprecation for using `authorize` for mutation fields. ([@rzaharenkov][])

## 0.5.0 (2020-10-07)

- Add `preauthorize_mutation_raise_exception` configuration parameter. ([@palkan][])

Similar to `preauthorize_raise_exception` but only for mutations.
Fallbacks to `preauthorize_raise_exception` unless explicitly specified.

- Add `preauthorize_raise_exception` configuration parameter. ([@palkan][])

Similar to `authorize_raise_exception` but for `preauthorize: true` fields.
Fallbacks to `authorize_raise_exception` unless explicitly specified.

- Add ability to specify custom field options for `expose_authorization_rules`. ([@bibendi][])

Now you can add additional options for underflying `field` call via `field_options` parameter:

```ruby
expose_authorization_rules :show?, field_options: {camelize: false}

# equals to
field :can_show, ActionPolicy::GraphQL::Types::AuthorizationResult, null: false, camelize: false
```

## 0.4.0 (2020-03-11)

- **Require Ruby 2.5+**. ([@palkan][])

- Add `authorized_field: *` option to perform authorization on the base of the upper object policy prior to resolving fields. ([@sponomarev][])

## 0.3.2 (2019-12-12)

- Fix compatibility with Action Policy 0.4.0 ([@haines][])

## 0.3.1 (2019-10-23)

- Add support for using Action Policy methods in `self.authorized?`. ([@palkan][])

## 0.3.0 (2019-10-21)

- Add `preauthorize: *` option to perform authorization prior to resolving fields. ([@palkan][])

## 0.2.0 (2019-08-15)

- Add ability to specify a field name explicitly. ([@palkan][])

Now you can write, for example:

```ruby
expose_authorization_rules :create?, with: PostPolicy, field_name: :can_create_post
```

- Add support for resolvers. ([@palkan][])

Now it's possible to `include ActionPolicy::GraphQL::Behaviour` into resolver class to use
Action Policy helpers there.

## 0.1.0 (2019-05-20)

- Initial version. ([@palkan][])

[@palkan]: https://github.com/palkan
[@haines]: https://github.com/haines
[@sponomarev]: https://github.com/sponomarev
[@bibendi]: https://github.com/bibendi
[@rzaharenkov]: https://github.com/rzaharenkov


================================================
FILE: Gemfile
================================================
source "https://rubygems.org"

# Specify your gem's dependencies in action_policy-graphql.gemspec
gemspec

gem "debug", platform: :mri

eval_gemfile "gemfiles/rubocop.gemfile"

local_gemfile = File.join(__dir__, "Gemfile.local")

if File.exist?(local_gemfile)
  # Specify custom action_policy/graphql-ruby version in Gemfile.local
  eval(File.read(local_gemfile)) # rubocop:disable Security/Eval
else
  gem "action_policy", ">= 0.5.0"
  gem "graphql", ">= 1.9.3"
end


================================================
FILE: LICENSE.txt
================================================
The MIT License (MIT)

Copyright (c) 2019 Vladimir Dementyev

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.


================================================
FILE: README.md
================================================
[![Gem Version](https://badge.fury.io/rb/action_policy-graphql.svg)](https://badge.fury.io/rb/action_policy-graphql)
![Build](https://github.com/palkan/action_policy-graphql/workflows/Build/badge.svg)
![JRuby Build](https://github.com/palkan/action_policy-graphql/workflows/JRuby%20Build/badge.svg)
[![Documentation](https://img.shields.io/badge/docs-link-brightgreen.svg)](https://actionpolicy.evilmartians.io/#/graphql)

# Action Policy GraphQL

<img align="right" height="150" width="129"
     title="Action Policy logo" src="./assets/logo.svg">

This gem provides an integration for using [Action Policy](https://github.com/palkan/action_policy) as an authorization framework for GraphQL applications (built with [`graphql` ruby gem](https://graphql-ruby.org)).

This integration includes the following features:

- Fields & mutations authorization
- List and connections scoping
- [**Exposing permissions/authorization rules in the API**](https://evilmartians.com/chronicles/exposing-permissions-in-graphql-apis-with-action-policy).

📑 [Documentation](https://actionpolicy.evilmartians.io/#/graphql)

<a href="https://evilmartians.com/?utm_source=action_policy-graphql">
<img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg" alt="Sponsored by Evil Martians" width="236" height="54"></a>

## Installation

Add this line to your application's Gemfile:

```ruby
gem "action_policy-graphql"
```

## Usage

**NOTE:** this is a quick overview of the functionality provided by the gem. For more information see the [documentation](https://actionpolicy.evilmartians.io/#/graphql).

To start using Action Policy in GraphQL-related code, you need to enhance your base classes with `ActionPolicy::GraphQL::Behaviour`:

```ruby
# For fields authorization, lists scoping and rules exposing
class Types::BaseObject < GraphQL::Schema::Object
  include ActionPolicy::GraphQL::Behaviour
end

# For using authorization helpers in mutations
class Types::BaseMutation < GraphQL::Schema::Mutation
  include ActionPolicy::GraphQL::Behaviour
end

# For using authorization helpers in resolvers
class Types::BaseResolver < GraphQL::Schema::Resolver
  include ActionPolicy::GraphQL::Behaviour
end
```

### `authorize: *`

You can add authorization to the fields by specifying the `authorize: *` option:

```ruby
field :home, Home, null: false, authorize: true do
  argument :id, ID, required: true
end

# field resolver method
def home(id:)
  Home.find(id)
end
```

The code above is equal to:

```ruby
field :home, Home, null: false do
  argument :id, ID, required: true
end

def home(id:)
  Home.find(id).tap { |home| authorize! home, to: :show? }
end
```

You can customize the authorization options, e.g. `authorize: {to: :preview?, with: CustomPolicy}`.

If you don't want to raise an exception but return a null instead, you should set a `raise: false` option.

Note: it does not make too much sense to use `authorize` in mutations since it's checking authorization rules after mutation is executed. Therefore `authorize` marked as deprecated when used in mutations and will raise error in future releases.

### `authorized_scope: *`

You can add `authorized_scope: true` option to the field (list or _connection_ field) to
apply the corresponding policy rules to the data:

```ruby
class CityType < ::Common::Graphql::Type
  # It would automatically apply the relation scope from the EventPolicy to
  # the relation (city.events)
  field :events, EventType.connection_type, null: false, authorized_scope: true

  # you can specify the policy explicitly
  field :events, EventType.connection_type, null: false, authorized_scope: {with: CustomEventPolicy}
end
```

**NOTE:** you cannot use `authorize: *` and `authorized_scope: *` at the same time but you can combine `preauthorize: *` or `authorize_field: *` with `authorized_scope: *`.

### `preauthorize: *`

If you want to perform authorization before resolving the field value, you can use `preauthorize: *` option:

```ruby
field :homes, [Home], null: false, preauthorize: {with: HomePolicy}

def homes
  Home.all
end
```

The code above is equal to:

```ruby
field :homes, [Home], null: false

def homes
  authorize! "homes", to: :index?, with: HomePolicy
  Home.all
end
```

**NOTE:** we pass the field's name as the `record` to the policy rule. We assume that preauthorization rules do not depend on
the record itself and pass the field's name for debugging purposes only.

You can customize the authorization options, e.g. `preauthorize: {to: :preview?, with: CustomPolicy}`.

**NOTE:** unlike `authorize: *` you MUST specify the `with: SomePolicy` option.
The default authorization rule depends on the type of the field:

- for lists we use `index?` (configured by `ActionPolicy::GraphQL.default_preauthorize_list_rule` parameter)
- for _singleton_ fields we use `show?` (configured by `ActionPolicy::GraphQL.default_preauthorize_node_rule` parameter)

### `authorize_field: *`

If you want to perform authorization before resolving the field value _on the base of the upper object_, you can use `authorize_field: *` option:

```ruby
field :homes, Home, null: false, authorize_field: true

def homes
  Home.all
end
```

The code above is equal to:

```ruby
field :homes, [Home], null: false

def homes
  authorize! object, to: :homes?
  Home.all
end
```
By default we use `#{underscored_field_name}?` authorization rule.

You can customize the authorization options, e.g. `authorize_field: {to: :preview?, with: CustomPolicy}`.

### `expose_authorization_rules`

You can add permissions/authorization exposing fields to "tell" clients which actions could be performed against the object or not (and why).

For example:

```ruby
class ProfileType < ::Common::Graphql::Type
  # Adds can_edit, can_destroy fields with
  # AuthorizationResult type.

  # NOTE: prefix "can_" is used by default, no need to specify it explicitly
  expose_authorization_rules :edit?, :destroy?, prefix: "can_"
end
```

Then the client could perform the following query:

```gql
{
  post(id: $id) {
    canEdit {
      # (bool) true|false; not null
      value
      # top-level decline message ("Not authorized" by default); null if value is true
      message
      # detailed information about the decline reasons; null if value is true
      reasons {
        details # JSON-encoded hash of the failure reasons (e.g., {"event" => [:seats_available?]})
        fullMessages # Array of human-readable reasons (e.g., ["This event is sold out"])
      }
    }

    canDestroy {
      # ...
    }
  }
}
```

You can specify a custom field name as well (only for a single rule):

```ruby
class ProfileType < ::Common::Graphql::Type
  # Adds can_create_post field.

  expose_authorization_rules :create?, with: PostPolicy, field_name: "can_create_post"
end
```

## Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/palkan/action_policy-graphql.

## License

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


================================================
FILE: Rakefile
================================================
# frozen_string_literal: true

require "bundler/gem_tasks"
require "rspec/core/rake_task"

RSpec::Core::RakeTask.new(:spec)

begin
  require "rubocop/rake_task"
  RuboCop::RakeTask.new

  RuboCop::RakeTask.new("rubocop:md") do |task|
    task.options << %w[-c .rubocop-md.yml]
  end
rescue LoadError
  task(:rubocop) {}
  task("rubocop:md") {}
end

task default: %w[rubocop rubocop:md spec]


================================================
FILE: action_policy-graphql.gemspec
================================================
# frozen_string_literal: true

require_relative "lib/action_policy/graphql/version"

Gem::Specification.new do |spec|
  spec.name = "action_policy-graphql"
  spec.version = ActionPolicy::GraphQL::VERSION
  spec.authors = ["Vladimir Dementyev"]
  spec.email = ["dementiev.vm@gmail.com"]

  spec.summary = "Action Policy integration for GraphQL-Ruby"
  spec.description = "Action Policy integration for GraphQL-Ruby"
  spec.homepage = "https://github.com/palkan/action_policy-graphql"
  spec.license = "MIT"

  spec.files = Dir.glob("lib/**/*") + %w[README.md LICENSE.txt CHANGELOG.md]

  spec.metadata = {
    "bug_tracker_uri" => "https://github.com/palkan/action_policy-graphql/issues",
    "changelog_uri" => "https://github.com/palkan/action_policy-graphql/blob/master/CHANGELOG.md",
    "documentation_uri" => "https://actionpolicy.evilmartians.io/#/graphql",
    "homepage_uri" => "https://github.com/palkan/action_policy-graphql",
    "source_code_uri" => "https://github.com/palkan/action_policy-graphql"
  }

  spec.require_paths = ["lib"]

  spec.required_ruby_version = ">= 2.5.0"

  spec.add_dependency "action_policy", "~> 0.7"
  spec.add_dependency "ruby-next-core", "~> 1.0"
  spec.add_dependency "graphql", ">= 1.9.3"

  spec.add_development_dependency "bundler", ">= 1.15"
  spec.add_development_dependency "rake", ">= 13.0"
  spec.add_development_dependency "rspec", ">= 3.8"
  spec.add_development_dependency "i18n"
end


================================================
FILE: bin/console
================================================
#!/usr/bin/env ruby

require "bundler/setup"
require "action_policy/graphql"

# You can add fixtures and/or initialization code here to make experimenting
# with your gem easier. You can also use a different console, if you like.

# (If you use this, don't forget to add pry to your Gemfile!)
# require "pry"
# Pry.start

require "irb"
IRB.start(__FILE__)


================================================
FILE: bin/setup
================================================
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
set -vx

bundle install

# Do any other automated setup that you need to do here


================================================
FILE: gemfiles/action_policy/master.gemfile
================================================
source "https://rubygems.org"

gem "action_policy", github: "palkan/action_policy"

gemspec path: "../.."


================================================
FILE: gemfiles/graphql/master.gemfile
================================================
source "https://rubygems.org"

gem "graphql", github: "rmosolgo/graphql-ruby"

gemspec path: "../.."


================================================
FILE: gemfiles/jruby.gemfile
================================================
source "https://rubygems.org"

gemspec path: ".."


================================================
FILE: gemfiles/rubocop.gemfile
================================================
source "https://rubygems.org" do
  gem "rubocop-md", "~> 1.0"
  gem "standard", "~> 1.0"
  gem "ruby-next", ">= 0.15.0"
end


================================================
FILE: lib/action_policy/graphql/authorized_field.rb
================================================
# frozen_string_literal: true

module ActionPolicy
  module GraphQL
    # Add `authorized` option to the field
    #
    # Example:
    #
    #   class PostType < ::GraphQL::Schema::Object
    #     field :comments, null: false, authorized: true
    #
    #     # or with options
    #     field :comments, null: false, authorized: { type: :relation, with: MyPostPolicy }
    #   end
    module AuthorizedField
      class Extension < ::GraphQL::Schema::FieldExtension
        def initialize(field:, options:)
          super(field: field, options: options&.dup || {})
        end

        def extract_option(key, &default)
          value = options.fetch(key, &default)
          options.delete key
          value
        end
      end

      class AuthorizeExtension < Extension
        DEPRECATION_MESSAGE = "`authorize: *` for mutation fields is deprecated.  Please use `preauthorize: *` instead."

        class << self
          def show_authorize_mutation_deprecation
            return if defined?(@authorize_mutation_deprecation_shown)

            if defined?(ActiveSupport::Deprecation)
              ActiveSupport::Deprecation.warn(DEPRECATION_MESSAGE)
            else
              warn(DEPRECATION_MESSAGE)
            end

            @authorize_mutation_deprecation_shown = true
          end
        end

        def apply
          self.class.show_authorize_mutation_deprecation if field.mutation && field.mutation < ::GraphQL::Schema::Mutation

          @to = extract_option(:to) { ::ActionPolicy::GraphQL.default_authorize_rule }
          @raise = extract_option(:raise) { ::ActionPolicy::GraphQL.authorize_raise_exception }
        end

        def after_resolve(value:, context:, object:, **_rest)
          return value if value.nil?

          if @raise
            object.authorize! value, to: @to, **options
            value
          else
            object.allowed_to?(@to, value, **options) ? value : nil
          end
        end
      end

      class PreauthorizeExtension < Extension
        def apply
          if options[:with].nil?
            raise ArgumentError, "You must specify the policy for preauthorization: " \
                                 "`field :#{field.name}, preauthorize: {with: SomePolicy}`"
          end

          @to = extract_option(:to) do
            if field.type.list?
              ::ActionPolicy::GraphQL.default_preauthorize_list_rule
            else
              ::ActionPolicy::GraphQL.default_preauthorize_node_rule
            end
          end

          @raise = extract_option(:raise) do
            if field.mutation
              ::ActionPolicy::GraphQL.preauthorize_mutation_raise_exception
            else
              ::ActionPolicy::GraphQL.preauthorize_raise_exception
            end
          end
        end

        def resolve(context:, object:, arguments:, **_rest)
          if @raise
            object.authorize! field.name, to: @to, **options
            yield object, arguments
          elsif object.allowed_to?(@to, field.name, **options)
            yield object, arguments
          end
        end
      end

      class AuthorizeFieldExtension < Extension
        def apply
          @to = extract_option(:to) { underscored_field_name }
          @raise = extract_option(:raise) { ::ActionPolicy::GraphQL.authorize_raise_exception }
        end

        def resolve(context:, object:, arguments:, **_rest)
          if @raise
            object.authorize! object.object, to: @to, **options
            yield object, arguments
          elsif object.allowed_to?(@to, object.object, **options)
            yield object, arguments
          end
        end

        private

        def underscored_field_name
          :"#{field.instance_variable_get(:@underscored_name)}?"
        end
      end

      class ScopeExtension < Extension
        def resolve(context:, object:, arguments:, **_rest)
          value = yield(object, arguments)
          return value if value.nil?

          object.authorized_scope(value, **options)
        end
      end

      def initialize(*args, preauthorize: nil, authorize: nil, authorized_scope: nil, authorize_field: nil, **kwargs, &block)
        if authorize && authorized_scope
          raise ArgumentError, "Only one of `authorize` and `authorized_scope` " \
                               "options could be specified. You can use `preauthorize` or `authorize_field` along with scoping"
        end

        if (!!authorize == !!preauthorize) ? authorize : authorize_field
          raise ArgumentError, "Only one of `authorize`, `preauthorize` or `authorize_field` " \
                               "options could be specified."
        end

        extensions = (kwargs[:extensions] ||= [])

        add_extension! extensions, AuthorizeExtension, authorize
        add_extension! extensions, ScopeExtension, authorized_scope
        add_extension! extensions, PreauthorizeExtension, preauthorize
        add_extension! extensions, AuthorizeFieldExtension, authorize_field

        super(*args, **kwargs, &block)
      end

      private

      def add_extension!(extensions, extension_class, options)
        return unless options

        options = {} if options == true

        extension = {extension_class => options}

        if extensions.is_a?(Hash)
          extensions.merge!(extension)
        else
          extensions << extension
        end
      end
    end
  end
end


================================================
FILE: lib/action_policy/graphql/behaviour.rb
================================================
# frozen_string_literal: true

require "action_policy/graphql/fields"
require "action_policy/graphql/authorized_field"

module ActionPolicy
  module GraphQL
    module Behaviour
      require "action_policy/ext/module_namespace"
      using ActionPolicy::Ext::ModuleNamespace

      # When used with self.authorized?
      def self.extended(base)
        base.extend ActionPolicy::Behaviour
        base.extend ActionPolicy::Behaviours::ThreadMemoized
        base.extend ActionPolicy::Behaviours::Memoized
        base.extend ActionPolicy::Behaviours::Namespaced

        # Authorization context could't be defined for the class
        def base.build_authorization_context
          {}
        end

        # Override authorization_namespace to use the class itself
        def base.authorization_namespace
          return @authorization_namespace if instance_variable_defined?(:@authorization_namespace)
          @authorization_namespace = namespace
        end
      end

      def self.included(base)
        base.include ActionPolicy::Behaviour
        base.include ActionPolicy::Behaviours::ThreadMemoized
        base.include ActionPolicy::Behaviours::Memoized
        base.include ActionPolicy::Behaviours::Namespaced

        base.authorize :user, through: :current_user

        if base.respond_to?(:field_class)
          unless base.field_class < ActionPolicy::GraphQL::AuthorizedField
            base.field_class.prepend(ActionPolicy::GraphQL::AuthorizedField)
          end

          unless base < ActionPolicy::GraphQL::Fields
            base.include ActionPolicy::GraphQL::Fields
          end
        end

        base.extend self
      end

      def current_user
        context[:current_user]
      end
    end
  end
end


================================================
FILE: lib/action_policy/graphql/fields.rb
================================================
# frozen_string_literal: true

require "action_policy/graphql/types/authorization_result"

module ActionPolicy
  using RubyNext

  module GraphQL
    # Add DSL to add policy rules as fields
    #
    # Example:
    #
    #   class PostType < ::GraphQL::Schema::Object
    #     # Adds can_edit, can_destroy fields with
    #     # AuthorizationResult type.
    #
    #     expose_authorization_rules :edit?, :destroy?, prefix: "can_"
    #   end
    #
    # Prefix is "can_" by default.
    module Fields
      def self.included(base)
        base.extend ClassMethods
      end

      module ClassMethods
        def expose_authorization_rules(*rules, field_name: nil, prefix: ::ActionPolicy::GraphQL.default_authorization_field_prefix, field_options: {}, **options)
          raise ArgumentError, "Cannot specify field_name for multiple rules" if rules.size > 1 && !field_name.nil?

          rules.each do |rule|
            gql_field_name = field_name || "#{prefix}#{rule.to_s.delete("?")}"

            field gql_field_name,
              ActionPolicy::GraphQL::Types::AuthorizationResult,
              null: false,
              **field_options

            define_method(gql_field_name) do
              allowance_to(rule, object, **options)
            end
          end
        end
      end
    end
  end
end


================================================
FILE: lib/action_policy/graphql/types/authorization_result.rb
================================================
# frozen_string_literal: true

require "action_policy/graphql/types/failure_reasons"

module ActionPolicy
  module GraphQL
    module Types
      class AuthorizationResult < ::GraphQL::Schema::Object
        field :value, Boolean, null: false, description: "Result of applying a policy rule"
        field :message, String, null: true, description: "Human-readable error message"
        field :reasons, FailureReasons, null: true, description: "Reasons of check failure"

        def message
          return if object.value == true
          object.message
        end

        def reasons
          return if object.value == true
          object.reasons
        end
      end
    end
  end
end


================================================
FILE: lib/action_policy/graphql/types/failure_reasons.rb
================================================
# frozen_string_literal: true

module ActionPolicy
  module GraphQL
    module Types
      class FailureReasons < ::GraphQL::Schema::Object
        field :details, String, null: false, description: "JSON-encoded map of reasons"
        field :full_messages, [String], null: false, description: "Human-readable errors"

        def details
          object.details.to_json
        end
      end
    end
  end
end


================================================
FILE: lib/action_policy/graphql/version.rb
================================================
# frozen_string_literal: true

module ActionPolicy
  module GraphQL
    VERSION = "0.6.0"
  end
end


================================================
FILE: lib/action_policy/graphql.rb
================================================
# frozen_string_literal: true

require "graphql"
require "action_policy"

require "action_policy/graphql/behaviour"

module ActionPolicy
  module GraphQL
    class << self
      # Which rule to use when no specified (e.g. `authorize: true`)
      # Defaults to `:show?`
      attr_accessor :default_authorize_rule

      # Which rule to use when no specified for preauthorization (e.g. `preauthorize: true`)
      # of a list-like field.
      # Defaults to `:index?`
      attr_accessor :default_preauthorize_list_rule

      # Which rule to use when no specified for preauthorization (e.g. `preauthorize: true`)
      # of a singleton-like field.
      # Defaults to `:show?`
      attr_accessor :default_preauthorize_node_rule

      # Whether to raise an exeption if field is not authorized
      # or return `nil`.
      # Defaults to `true`.
      attr_accessor :authorize_raise_exception

      # Which prefix to use for authorization fields
      # Defaults to `"can_"`
      attr_accessor :default_authorization_field_prefix

      attr_writer :preauthorize_raise_exception

      # Whether to raise an exception if preauthorization fails
      # Equals to authorize_raise_exception unless explicitly set
      def preauthorize_raise_exception
        return authorize_raise_exception if @preauthorize_raise_exception.nil?
        @preauthorize_raise_exception
      end

      # Whether to raise an exception if preauthorization fails
      # Equals to preauthorize_raise_exception unless explicitly set
      attr_writer :preauthorize_mutation_raise_exception

      def preauthorize_mutation_raise_exception
        return preauthorize_raise_exception if @preauthorize_mutation_raise_exception.nil?

        @preauthorize_mutation_raise_exception
      end
    end

    self.default_authorize_rule = :show?
    self.default_preauthorize_list_rule = :index?
    self.default_preauthorize_node_rule = :show?
    self.authorize_raise_exception = true
    self.preauthorize_raise_exception = nil
    self.preauthorize_mutation_raise_exception = nil
    self.default_authorization_field_prefix = "can_"
  end
end


================================================
FILE: lib/action_policy-graphql.rb
================================================
# frozen_string_literal: true

require "ruby-next"
require "action_policy/graphql"


================================================
FILE: spec/action_policy/graphql/authorized_spec.rb
================================================
# frozen_string_literal: true

require "spec_helper"

describe "field extensions", :aggregate_failures do
  include_context "common:graphql"

  let(:user) { :user }

  let(:schema) { Schema }
  let(:context) { {user: user} }

  context "authorized_scope: *" do
    let(:posts) { [Post.new("private-a"), Post.new("public-b")] }
    let(:query) do
      %({
          posts {
            title
          }
        })
    end

    before do
      allow(Schema).to receive(:posts) { PostList.new(posts) }
    end

    it "has authorized scope" do
      expect { subject }.to have_authorized_scope(:data)
        .with(PostPolicy)
    end

    specify "as user" do
      expect(data.size).to eq 1
      expect(data.first.fetch("title")).to eq "public-b"
    end

    context "as admin" do
      let(:user) { :admin }

      specify do
        expect(data.size).to eq 2
        expect(data.map { |v| v.fetch("title") }).to match_array(
          [
            "private-a",
            "public-b"
          ]
        )
      end
    end

    context "namespaced" do
      let(:posts) { [Post.new("not mine"), Post.new("story of my life")] }
      let(:query) do
        %({
            me {
              posts {
                title
              }
            }
          })
      end

      before do
        allow(Me).to receive(:posts) { PostList.new(posts) }
      end

      it "has authorized scope" do
        expect { subject }.to have_authorized_scope(:data)
          .with(Me::PostPolicy)
      end
    end

    context "connections" do
      let(:query) do
        %({
          connectedPosts(first: 1) {
            nodes {
              title
            }
          }
        })
      end

      it "has authorized scope" do
        expect { subject }.to have_authorized_scope(:data)
          .with(PostPolicy)
      end

      context "with connection: true" do
        let(:query) do
          %({
            anotherConnectedPosts(first: 1) {
              totalCount
              nodes {
                title
              }
            }
          })
        end

        it "has authorized scope" do
          expect { subject }.to have_authorized_scope(:data)
            .with(PostPolicy)
        end

        specify do
          expect(data["totalCount"]).to eq 1
          expect(data["nodes"][0]["title"]).to eq "public-b"
        end
      end
    end
  end

  context "authorize: *" do
    let(:post) { Post.new("private-a") }
    let(:query) do
      %({
          authPost {
            title
          }
        })
    end

    before do
      allow(Schema).to receive(:post) { post }
    end

    it "is authorized" do
      expect { subject }.to be_authorized_to(:show?, post)
        .with(PostPolicy)
    end

    specify "as user" do
      expect { subject }.to raise_error(ActionPolicy::Unauthorized)
    end

    context "accessible resource" do
      let(:post) { Post.new("post-c-visible") }

      specify do
        expect(data.fetch("title")).to eq "post-c-visible"
      end
    end

    context "as admin" do
      let(:user) { :admin }

      specify do
        expect(data.fetch("title")).to eq "private-a"
      end
    end

    context "with options" do
      let(:query) do
        %({
            anotherPost {
              title
            }
          })
      end

      it "is authorized" do
        expect { subject }.to be_authorized_to(:preview?, post)
          .with(AnotherPostPolicy)
      end
    end

    context "non-raising authorize" do
      let(:query) do
        %({
            nonRaisingPost {
              title
            }
          })
      end

      it "returns nil" do
        expect(data).to be_nil
      end
    end

    context "namespaced" do
      let(:query) do
        %({
            me {
              bio {
                title
              }
            }
          })
      end

      before do
        allow(Me).to receive(:post) { post }
      end

      it "is authorized" do
        expect { subject }.to be_authorized_to(:show?, post)
          .with(Me::PostPolicy)
      end
    end

    context "with resolver" do
      let(:query) do
        %({
            resolvedPost {
              title
            }
          })
      end

      before do
        allow(Me).to receive(:post) { post }
      end

      it "is authorized" do
        expect { subject }.to be_authorized_to(:show?, post)
          .with(PostPolicy)
      end
    end

    context "with mutation" do
      let(:query) do
        %(mutation {
          createPost(title: "GQL") {
            post {
              title
            }
          }
        })
      end

      it "is authorized" do
        expect { subject }.to be_authorized_to(:publish?, anything)
          .with(PostPolicy)
      end

      # See spec_helper.rb for default settings
      it "raises if authorize_raise_exception is set to true" do
        expect { subject }.to raise_error(ActionPolicy::Unauthorized)
      end
    end
  end

  context "preauthorize: *" do
    context "collection" do
      let(:posts) { [Post.new("private-a"), Post.new("public-b")] }
      let(:query) do
        %({
            secretPosts {
              title
            }
          })
      end

      before do
        allow(Schema).to receive(:posts) { PostList.new(posts) }
      end

      it "is authorized" do
        expect { subject }.to be_authorized_to(:view_secret_posts?, "secretPosts")
          .with(PostPolicy)
      end

      specify "as user" do
        expect { subject }.to raise_error(ActionPolicy::Unauthorized)
      end

      context "as admin" do
        let(:user) { :admin }

        specify do
          expect(data.size).to eq 2
        end
      end

      context "with default collection rule" do
        let(:query) do
          %({
              allPosts {
                title
              }
            })
        end

        it "is authorized" do
          expect { subject }.to be_authorized_to(:index?, "allPosts")
            .with(PostPolicy)
        end
      end
    end

    context "field" do
      let(:post) { Post.new("private-a") }
      let(:query) do
        %({
            secretPost {
              title
            }
          })
      end

      before do
        allow(Schema).to receive(:post) { post }
      end

      it "doesn't resolve field if auth failed" do
        expect(data).to be_nil
        expect(Schema).to_not have_received(:post)
      end

      context "as admin" do
        let(:user) { :admin }

        specify do
          expect(data.fetch("title")).to eq(post.title)
          expect(Schema).to have_received(:post)
        end
      end
    end
  end

  context "authorize_field: *" do
    let(:post) { Post.new("title") }
    let(:query) do
      %({
          post {
            secretTitle
          }
        })
    end

    before do
      allow(post).to receive(:title).and_call_original
      allow(Schema).to receive(:post) { post }
    end

    it "is authorized with object policy" do
      expect { subject }.to be_authorized_to(:secret_title?, post)
        .with(PostPolicy)
    end

    it "doesn't resolve field if auth failed" do
      expect { subject }.to raise_error(ActionPolicy::Unauthorized)
      expect(post).to_not have_received(:title)
    end

    context "as admin" do
      let(:user) { :admin }

      specify do
        expect(data.fetch("secretTitle")).to eq("Secret #{post.title}")
      end
    end

    context "non-raising authorize" do
      let(:query) do
        %({
            post {
              silentSecretTitle
            }
          })
      end

      it "returns nil" do
        expect(data.fetch("silentSecretTitle")).to be_nil
        expect(post).to_not have_received(:title)
      end

      context "as admin" do
        let(:user) { :admin }

        specify do
          expect(data.fetch("silentSecretTitle")).to eq("Secret #{post.title}")
        end
      end
    end

    context "with options" do
      let(:query) do
        %({
            post {
              anotherSecretTitle
            }
          })
      end

      it "is authorized" do
        expect { subject }.to be_authorized_to(:preview?, post)
          .with(AnotherPostPolicy)
      end
    end
  end
end


================================================
FILE: spec/action_policy/graphql/behaviour_spec.rb
================================================
# frozen_string_literal: true

require "spec_helper"

describe ActionPolicy::GraphQL::Behaviour do
  include_context "common:graphql"

  let(:user) { :user }
  let(:post) { Post.new("private") }

  let(:schema) { Schema }
  let(:context) { {current_user: user} }

  describe ".authorized? + allowed_to?" do
    let(:query) do
      %({
        authorizedPost {
          title
        }
      })
    end

    before { allow(Schema).to receive(:post) { post } }

    specify do
      expect(data).to be_nil
    end

    context "when admin" do
      let(:user) { :admin }

      specify do
        expect(data.fetch("title")).to eq "private"
      end
    end

    context "namespaced" do
      let(:post) { Post.new("namespaced") }

      let(:query) do
        %({
          authorizedNamespacedPost {
            title
          }
        })
      end

      specify do
        expect(data.fetch("title")).to eq "namespaced"
      end

      context "mutation" do
        let(:query) do
          %(mutation {
            adminCreatePost(title: "GQL") {
              post {
                title
              }
            }
          })
        end

        it "is authorized" do
          expect { subject }.to be_authorized_to(:create?, Post)
            .with(MyNamespace::PostPolicy)
        end
      end

      context "authorized mutation" do
        let(:query) do
          %(mutation {
            adminCreatePostAuthorized(title: "GQL") {
              post {
                title
              }
            }
          })
        end

        it "is authorized" do
          expect { subject }.to be_authorized_to(:create?, Post)
            .with(MyNamespace::PostPolicy)
        end
      end

      context "overriden namespace" do
        let(:post) { Post.new("deleted") }

        let(:query) do
          %(mutation {
            deletePost(id: "42") {
              deletedId
            }
          })
        end

        it "is authorized" do
          expect { subject }.to be_authorized_to(:destroy?, post)
            .with(MyNamespace::PostPolicy)
        end
      end
    end
  end
end


================================================
FILE: spec/action_policy/graphql/expose_authorization_rules_spec.rb
================================================
# frozen_string_literal: true

require "spec_helper"

describe "#expose_authorization_rules", :aggregate_failures do
  include_context "common:graphql"

  let(:post) { Post.new("private") }

  let(:schema) { Schema }
  let(:query) do
    %({
        post {
          title
          canShow {
            value
            message
            reasons {
              details
              fullMessages
            }
          }
          canEdit {
            value
            message
            reasons {
              details
              fullMessages
            }
          }
          can_i_destroy {
            value
            message
            reasons {
              details
              fullMessages
            }
          }
        }
      })
  end

  before { allow(Schema).to receive(:post) { post } }

  context "when failure" do
    specify do
      expect(data.fetch("canShow").fetch("value")).to eq false
      expect(data.fetch("canShow").fetch("message")).to eq "Cannot show post"

      expect(data.fetch("canEdit").fetch("value")).to eq false
      expect(data.fetch("canEdit").fetch("message")).to eq "You shall not do this"

      expect(data.fetch("can_i_destroy").fetch("value")).to eq false
      expect(data.fetch("can_i_destroy").fetch("message")).to eq "You shall not do this"
    end

    specify "#reasons" do
      reasons = data.fetch("can_i_destroy").fetch("reasons")

      expect(reasons.fetch("details")).to eq(
        {post: [:public?]}.to_json
      )
      expect(reasons.fetch("fullMessages")).to eq(
        [
          "Post is not public"
        ]
      )
    end
  end

  context "when success" do
    let(:post) { Post.new("public-visible") }

    specify do
      expect(data.fetch("canShow").fetch("value")).to eq true
      expect(data.fetch("canShow").fetch("message")).to be_nil
      expect(data.fetch("canShow").fetch("reasons")).to be_nil

      expect(data.fetch("can_i_destroy").fetch("value")).to eq true
      expect(data.fetch("can_i_destroy").fetch("message")).to be_nil
      expect(data.fetch("can_i_destroy").fetch("reasons")).to be_nil
    end
  end

  context "namespaced" do
    let(:query) do
      %({
          me {
            allPosts {
              title
              canShow {
                value
                message
                reasons {
                  details
                  fullMessages
                }
              }
            }
          }
        })
    end

    let(:field) { "me->allPosts" }

    before do
      allow(Me).to receive(:posts) { [post] }
    end

    context "when failure" do
      let(:post) { Post.new("not mine") }

      specify do
        expect(data.first.fetch("canShow").fetch("value")).to eq false
        expect(data.first.fetch("canShow").fetch("message")).to eq "Cannot show post"
      end
    end

    context "when success" do
      let(:post) { Post.new("story of my life") }

      specify do
        expect(data.first.fetch("canShow").fetch("value")).to eq true
        expect(data.first.fetch("canShow").fetch("message")).to be_nil
        expect(data.first.fetch("canShow").fetch("reasons")).to be_nil
      end
    end
  end

  context "with field_name" do
    let(:query) do
      %({
        me {
          canCreatePost {
            value
            message
            reasons {
              details
              fullMessages
            }
          }
        }
      })
    end

    let(:field) { "me" }

    specify do
      expect(data.fetch("canCreatePost").fetch("value")).to eq true
      expect(data.fetch("canCreatePost").fetch("message")).to be_nil
      expect(data.fetch("canCreatePost").fetch("reasons")).to be_nil
    end
  end
end


================================================
FILE: spec/spec_helper.rb
================================================
# frozen_string_literal: true

$LOAD_PATH.unshift File.expand_path("../lib", __dir__)

# This turns off per-thread caching in action_policy
ENV["RACK_ENV"] = "test"

require "i18n"
require "action_policy-graphql"
begin
  require "debug" unless ENV["CI"]
rescue LoadError
end

require "action_policy/rspec"

ActionPolicy::GraphQL.preauthorize_raise_exception = false
ActionPolicy::GraphQL.preauthorize_mutation_raise_exception = true

Dir["#{__dir__}/support/**/*.rb"].sort.each { |f| require f }

RSpec.configure do |config|
  config.mock_with :rspec

  config.example_status_persistence_file_path = "tmp/rspec_examples.txt"
  config.filter_run :focus
  config.run_all_when_everything_filtered = true

  config.order = :random
  Kernel.srand config.seed
end


================================================
FILE: spec/support/graphql_context.rb
================================================
# frozen_string_literal: true

shared_context "common:graphql" do
  let(:context) { {} }
  let(:variables) { {} }
  let(:field) { result.fetch("data").keys.first }
  let(:schema) { raise NotImplementedError.new("Specify schema under test, e.g. `let(:schema) { MySchema }`") }

  let(:data) do
    raise "API Query failed:\n\tquery: #{query}\n\terrors: #{result["errors"]}" if result.key?("errors")
    result.fetch("data").dig(*field.split("->"))
  end

  let(:errors) { result["errors"]&.map { |err| err["message"] } }

  # for connection responses
  let(:edges) { data.fetch("edges").map { |node| node.fetch("node") } }
  let(:page_info) { data.fetch("pageInfo") }

  subject(:result) do
    schema.execute(
      query,
      context: context,
      variables: variables
    )
  end
end


================================================
FILE: spec/support/schema.rb
================================================
# frozen_string_literal: true

class Post < Struct.new(:title); end

class PostList < Array
  def policy_name
    "#{first.class}Policy"
  end
end

class PostPolicy < ActionPolicy::Base
  scope_matcher :data, PostList

  scope_for :data do |data|
    next data if admin?

    data.select { |post| post.title.start_with? "public" }
  end

  pre_check :allow_admins

  def index?
    true
  end

  def view_secret_posts?
    # allow_admins pre-check allows admin access
    false
  end

  def create?
    true
  end

  def publish?
    false
  end

  def public?
    record.title.start_with?("public")
  end

  def show?
    record.title.end_with?("visible")
  end

  def manage?
    check?(:public?) && allowed_to?(:show?)
  end

  def secret_title?
    false
  end

  def silent_secret_title?
    false
  end

  private

  def allow_admins
    allow! if admin?
  end

  def admin?
    user == :admin
  end
end

class AnotherPostPolicy < PostPolicy
  def preview?
    public? && show?
  end

  def maybe_preview?
    # only admins can preview (controlled by pre-check)
    false
  end
end

I18n.backend = I18n::Backend::Simple.new.tap do |backend|
  backend.store_translations(
    :en,
    {
      action_policy: {
        policy: {
          post: {
            manage?: "You shall not do this",
            edit?: "Cannot edit post",
            show?: "Cannot show post",
            public?: "Post is not public"
          }
        }
      }
    }
  )
end

class BaseType < ::GraphQL::Schema::Object
  include ActionPolicy::GraphQL::Behaviour

  def current_user
    context.fetch(:user, :user)
  end
end

class BaseMutation < ::GraphQL::Schema::Mutation
  include ActionPolicy::GraphQL::Behaviour

  def current_user
    context.fetch(:user, :user)
  end
end

class BaseResolver < ::GraphQL::Schema::Resolver
  include ActionPolicy::GraphQL::Behaviour

  def current_user
    context.fetch(:user, :user)
  end
end

class PostType < BaseType
  field :title, String, null: false
  field :secret_title, String, null: false, authorize_field: true
  field :silent_secret_title, String, null: true, authorize_field: {raise: false}
  field :another_secret_title, String, null: true, authorize_field: {to: :preview?, with: AnotherPostPolicy}

  expose_authorization_rules :edit?, :show?, prefix: "can_"
  expose_authorization_rules :destroy?, prefix: "can_i_", field_options: {camelize: false}

  def secret_title
    "Secret #{object.title}"
  end

  alias_method :silent_secret_title, :secret_title
  alias_method :another_secret_title, :secret_title
end

class PostConnectionWithTotalCountType < GraphQL::Types::Relay::BaseConnection
  edge_type(PostType.edge_type)

  field :total_count, Integer, null: false

  def total_count
    object.nodes.size
  end
end

class AuthorizedPostType < PostType
  def self.authorized?(object, context)
    super &&
      allowed_to?(
        :show?,
        object,
        context: {user: context[:current_user]}
      )
  end
end

module MyNamespace
  class PostPolicy < ::PostPolicy
    def show?
      true
    end

    def create?
      admin?
    end

    def destroy?
      true
    end
  end

  class PostType < ::AuthorizedPostType
    graphql_name "MyNamespacePost"
  end
end

# Namespaced types
module Me
  class << self
    attr_accessor :posts, :post
  end

  class PostPolicy < ::PostPolicy
    scope_for :data do |data|
      data.select { |post| post.title.match?(/\bmy\b/) }
    end

    def show?
      record.title.match?(/\bmy\b/)
    end
  end

  class PostType < BaseType
    graphql_name "MyPostType"

    field :title, String, null: false

    expose_authorization_rules :show?, prefix: "can_"

    def title
      "My #{object.title}"
    end
  end

  class RootType < BaseType
    field :bio, PostType, null: false, authorize: true
    field :posts, [PostType], null: false, authorized_scope: true
    field :all_posts, [PostType], null: false

    expose_authorization_rules :create?, with: PostPolicy, field_name: :can_create_post

    def bio
      Me.post
    end

    def posts
      Me.posts
    end

    alias_method :all_posts, :posts
  end
end

class CreatePostMutation < BaseMutation
  argument :title, String, required: true

  field :post, PostType, null: false

  def resolve(title:)
    {post: Post.new(title: title)}
  end
end

module WithMyNamespace
  def authorization_namespace
    ::MyNamespace
  end
end

class DeletePostMutation < BaseMutation
  include WithMyNamespace

  argument :id, ID, required: true

  field :deleted_id, String, null: false

  def resolve(id:)
    post = Schema.post
    authorize! post, to: :destroy?
    {deleted_id: post.title}
  end
end

# Namespaced mutation
module Mutations
  module MyNamespace
    class CreatePostMutation < BaseMutation
      graphql_name "MyCreatePostMutation"

      argument :title, String, required: true

      field :post, PostType, null: false

      def resolve(title:)
        authorize! Post, to: :create?
        {post: Post.new(title: title)}
      end

      private

      def authorization_namespace
        ::MyNamespace
      end
    end
  end
end

class Schema < GraphQL::Schema
  class << self
    attr_accessor :posts, :post
  end

  class PostResolver < BaseResolver
    type PostType, null: false

    def resolve
      Schema.post.tap do |post|
        authorize! post, to: :show?
      end
    end
  end

  query(Class.new(BaseType) do
    def self.name
      "Query"
    end

    field :me, Me::RootType, null: false

    field :post, PostType, null: false
    field :authorized_post, AuthorizedPostType, null: true
    field :authorized_namespaced_post, MyNamespace::PostType, null: true
    field :resolved_post, resolver: PostResolver
    field :auth_post, PostType, null: false, authorize: true
    field :non_raising_post, PostType, null: true, authorize: {raise: false}
    field :secret_post, PostType, null: true, preauthorize: {with: AnotherPostPolicy, to: :maybe_preview?}
    field :another_post, PostType, null: false, authorize: {to: :preview?, with: AnotherPostPolicy}

    field :posts, [PostType], null: false, authorized_scope: {type: :data, with: PostPolicy}

    # Test that shared options object is not mutated
    base_options = {with: PostPolicy, raise: true}
    field :all_posts_new, [PostType], null: false, preauthorize: base_options
    field :all_posts, [PostType], null: false, preauthorize: base_options

    field :secret_posts, [PostType], null: false, preauthorize: {to: :view_secret_posts?, with: PostPolicy, raise: true}
    field :connected_posts, PostType.connection_type, null: false, authorized_scope: true
    field :another_connected_posts, PostConnectionWithTotalCountType, null: false, authorized_scope: true, connection: true

    def me
      {}
    end

    def post
      Schema.post
    end

    alias_method :authorized_post, :post
    alias_method :authorized_namespaced_post, :post
    alias_method :auth_post, :post
    alias_method :another_post, :post
    alias_method :non_raising_post, :post
    alias_method :secret_post, :post

    def posts
      Schema.posts
    end

    alias_method :secret_posts, :posts
    alias_method :all_posts, :posts
    alias_method :connected_posts, :posts
    alias_method :another_connected_posts, :posts
  end)

  mutation(Class.new(BaseType) do
    def self.name
      "Mutation"
    end

    field :create_post, mutation: CreatePostMutation, null: false, preauthorize: {with: PostPolicy, to: :publish?}
    field :delete_post, mutation: DeletePostMutation, null: false
    field :admin_create_post, mutation: Mutations::MyNamespace::CreatePostMutation, null: false
    field :admin_create_post_authorized, mutation: Mutations::MyNamespace::CreatePostMutation, null: false, authorize: {to: :create?}
  end)
end
Download .txt
gitextract_7d3k2x6f/

├── .github/
│   ├── ISSUE_TEMPLATE.md
│   ├── PULL_REQUEST_TEMPLATE.md
│   └── workflows/
│       ├── release.yml
│       ├── rubocop.yml
│       ├── test-jruby.yml
│       └── test.yml
├── .gitignore
├── .rubocop-md.yml
├── .rubocop.yml
├── CHANGELOG.md
├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── action_policy-graphql.gemspec
├── bin/
│   ├── console
│   └── setup
├── gemfiles/
│   ├── action_policy/
│   │   └── master.gemfile
│   ├── graphql/
│   │   └── master.gemfile
│   ├── jruby.gemfile
│   └── rubocop.gemfile
├── lib/
│   ├── action_policy/
│   │   ├── graphql/
│   │   │   ├── authorized_field.rb
│   │   │   ├── behaviour.rb
│   │   │   ├── fields.rb
│   │   │   ├── types/
│   │   │   │   ├── authorization_result.rb
│   │   │   │   └── failure_reasons.rb
│   │   │   └── version.rb
│   │   └── graphql.rb
│   └── action_policy-graphql.rb
└── spec/
    ├── action_policy/
    │   └── graphql/
    │       ├── authorized_spec.rb
    │       ├── behaviour_spec.rb
    │       └── expose_authorization_rules_spec.rb
    ├── spec_helper.rb
    └── support/
        ├── graphql_context.rb
        └── schema.rb
Download .txt
SYMBOL INDEX (113 symbols across 8 files)

FILE: lib/action_policy/graphql.rb
  type ActionPolicy (line 8) | module ActionPolicy
    type GraphQL (line 9) | module GraphQL
      function preauthorize_raise_exception (line 38) | def preauthorize_raise_exception
      function preauthorize_mutation_raise_exception (line 47) | def preauthorize_mutation_raise_exception

FILE: lib/action_policy/graphql/authorized_field.rb
  type ActionPolicy (line 3) | module ActionPolicy
    type GraphQL (line 4) | module GraphQL
      type AuthorizedField (line 15) | module AuthorizedField
        class Extension (line 16) | class Extension < ::GraphQL::Schema::FieldExtension
          method initialize (line 17) | def initialize(field:, options:)
          method extract_option (line 21) | def extract_option(key, &default)
        class AuthorizeExtension (line 28) | class AuthorizeExtension < Extension
          method show_authorize_mutation_deprecation (line 32) | def show_authorize_mutation_deprecation
          method apply (line 45) | def apply
          method after_resolve (line 52) | def after_resolve(value:, context:, object:, **_rest)
        class PreauthorizeExtension (line 64) | class PreauthorizeExtension < Extension
          method apply (line 65) | def apply
          method resolve (line 88) | def resolve(context:, object:, arguments:, **_rest)
        class AuthorizeFieldExtension (line 98) | class AuthorizeFieldExtension < Extension
          method apply (line 99) | def apply
          method resolve (line 104) | def resolve(context:, object:, arguments:, **_rest)
          method underscored_field_name (line 115) | def underscored_field_name
        class ScopeExtension (line 120) | class ScopeExtension < Extension
          method resolve (line 121) | def resolve(context:, object:, arguments:, **_rest)
        function initialize (line 129) | def initialize(*args, preauthorize: nil, authorize: nil, authorize...
        function add_extension! (line 152) | def add_extension!(extensions, extension_class, options)

FILE: lib/action_policy/graphql/behaviour.rb
  type ActionPolicy (line 6) | module ActionPolicy
    type GraphQL (line 7) | module GraphQL
      type Behaviour (line 8) | module Behaviour
        function extended (line 13) | def self.extended(base)
        function included (line 31) | def self.included(base)
        function current_user (line 52) | def current_user

FILE: lib/action_policy/graphql/fields.rb
  type ActionPolicy (line 5) | module ActionPolicy
    type GraphQL (line 8) | module GraphQL
      type Fields (line 21) | module Fields
        function included (line 22) | def self.included(base)
        type ClassMethods (line 26) | module ClassMethods
          function expose_authorization_rules (line 27) | def expose_authorization_rules(*rules, field_name: nil, prefix: ...

FILE: lib/action_policy/graphql/types/authorization_result.rb
  type ActionPolicy (line 5) | module ActionPolicy
    type GraphQL (line 6) | module GraphQL
      type Types (line 7) | module Types
        class AuthorizationResult (line 8) | class AuthorizationResult < ::GraphQL::Schema::Object
          method message (line 13) | def message
          method reasons (line 18) | def reasons

FILE: lib/action_policy/graphql/types/failure_reasons.rb
  type ActionPolicy (line 3) | module ActionPolicy
    type GraphQL (line 4) | module GraphQL
      type Types (line 5) | module Types
        class FailureReasons (line 6) | class FailureReasons < ::GraphQL::Schema::Object
          method details (line 10) | def details

FILE: lib/action_policy/graphql/version.rb
  type ActionPolicy (line 3) | module ActionPolicy
    type GraphQL (line 4) | module GraphQL

FILE: spec/support/schema.rb
  class Post (line 3) | class Post < Struct.new(:title); end
  class PostList (line 5) | class PostList < Array
    method policy_name (line 6) | def policy_name
  class PostPolicy (line 11) | class PostPolicy < ActionPolicy::Base
    method index? (line 22) | def index?
    method view_secret_posts? (line 26) | def view_secret_posts?
    method create? (line 31) | def create?
    method publish? (line 35) | def publish?
    method public? (line 39) | def public?
    method show? (line 43) | def show?
    method manage? (line 47) | def manage?
    method secret_title? (line 51) | def secret_title?
    method silent_secret_title? (line 55) | def silent_secret_title?
    method allow_admins (line 61) | def allow_admins
    method admin? (line 65) | def admin?
  class AnotherPostPolicy (line 70) | class AnotherPostPolicy < PostPolicy
    method preview? (line 71) | def preview?
    method maybe_preview? (line 75) | def maybe_preview?
  class BaseType (line 99) | class BaseType < ::GraphQL::Schema::Object
    method current_user (line 102) | def current_user
  class BaseMutation (line 107) | class BaseMutation < ::GraphQL::Schema::Mutation
    method current_user (line 110) | def current_user
  class BaseResolver (line 115) | class BaseResolver < ::GraphQL::Schema::Resolver
    method current_user (line 118) | def current_user
  class PostType (line 123) | class PostType < BaseType
    method secret_title (line 132) | def secret_title
  class PostConnectionWithTotalCountType (line 140) | class PostConnectionWithTotalCountType < GraphQL::Types::Relay::BaseConn...
    method total_count (line 145) | def total_count
  class AuthorizedPostType (line 150) | class AuthorizedPostType < PostType
    method authorized? (line 151) | def self.authorized?(object, context)
  type MyNamespace (line 161) | module MyNamespace
    class PostPolicy (line 162) | class PostPolicy < ::PostPolicy
      method show? (line 163) | def show?
      method create? (line 167) | def create?
      method destroy? (line 171) | def destroy?
    class PostType (line 176) | class PostType < ::AuthorizedPostType
  type Me (line 182) | module Me
    class PostPolicy (line 187) | class PostPolicy < ::PostPolicy
      method show? (line 192) | def show?
    class PostType (line 197) | class PostType < BaseType
      method title (line 204) | def title
    class RootType (line 209) | class RootType < BaseType
      method bio (line 216) | def bio
      method posts (line 220) | def posts
  class CreatePostMutation (line 228) | class CreatePostMutation < BaseMutation
    method resolve (line 233) | def resolve(title:)
  type WithMyNamespace (line 238) | module WithMyNamespace
    function authorization_namespace (line 239) | def authorization_namespace
  class DeletePostMutation (line 244) | class DeletePostMutation < BaseMutation
    method resolve (line 251) | def resolve(id:)
  type Mutations (line 259) | module Mutations
    type MyNamespace (line 260) | module MyNamespace
      class CreatePostMutation (line 261) | class CreatePostMutation < BaseMutation
        method resolve (line 268) | def resolve(title:)
        method authorization_namespace (line 275) | def authorization_namespace
  class Schema (line 282) | class Schema < GraphQL::Schema
    class PostResolver (line 287) | class PostResolver < BaseResolver
      method resolve (line 290) | def resolve
    method name (line 298) | def self.name
    method me (line 324) | def me
    method post (line 328) | def post
    method posts (line 339) | def posts
    method name (line 350) | def self.name
Condensed preview — 35 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (58K chars).
[
  {
    "path": ".github/ISSUE_TEMPLATE.md",
    "chars": 424,
    "preview": "<!--\n  This template is for bug reports. If you are reporting a bug, please continue on. If you are here for another rea"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "chars": 520,
    "preview": "<!--\n  First of all, thanks for contributing!\n\n  If it's a typo fix or minor documentation update feel free to skip the "
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 663,
    "preview": "name: Release gems\non:\n  workflow_dispatch:\n  push:\n    tags:\n      - v*\n\njobs:\n  release:\n    runs-on: ubuntu-latest\n  "
  },
  {
    "path": ".github/workflows/rubocop.yml",
    "chars": 454,
    "preview": "name: Lint Ruby\n\non:\n  push:\n    branches:\n    - master\n  pull_request:\n\njobs:\n  rubocop:\n    runs-on: ubuntu-latest\n   "
  },
  {
    "path": ".github/workflows/test-jruby.yml",
    "chars": 406,
    "preview": "name: JRuby Build\n\non:\n  push:\n    branches:\n    - master\n  pull_request:\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n    "
  },
  {
    "path": ".github/workflows/test.yml",
    "chars": 816,
    "preview": "name: Build\n\non:\n  push:\n    branches:\n    - master\n  pull_request:\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n    env:\n "
  },
  {
    "path": ".gitignore",
    "chars": 107,
    "preview": "/.bundle/\n/.yardoc\n/Gemfile.lock\n/_yardoc/\n/coverage/\n/doc/\n/pkg/\n/spec/reports/\n/tmp/\nGemfile.local\n*.lock"
  },
  {
    "path": ".rubocop-md.yml",
    "chars": 181,
    "preview": "inherit_from: \".rubocop.yml\"\n\nrequire:\n  - rubocop-md\n\nAllCops:\n  Include:\n    - '**/*.md'\n\nLint/Void:\n  Exclude:\n    - "
  },
  {
    "path": ".rubocop.yml",
    "chars": 501,
    "preview": "require:\n  - standard/cop/block_single_line_braces\n\ninherit_gem:\n  standard: config/base.yml\n\nAllCops:\n  Exclude:\n    - "
  },
  {
    "path": "CHANGELOG.md",
    "chars": 2671,
    "preview": "# Change log\n\n## master (unreleased)\n\n## 0.6.0 (2024-07-08)\n\n- Fix compatibility with Action Policy 0.7.0. ([@palkan][])"
  },
  {
    "path": "Gemfile",
    "chars": 467,
    "preview": "source \"https://rubygems.org\"\n\n# Specify your gem's dependencies in action_policy-graphql.gemspec\ngemspec\n\ngem \"debug\", "
  },
  {
    "path": "LICENSE.txt",
    "chars": 1085,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2019 Vladimir Dementyev\n\nPermission is hereby granted, free of charge, to any perso"
  },
  {
    "path": "README.md",
    "chars": 7044,
    "preview": "[![Gem Version](https://badge.fury.io/rb/action_policy-graphql.svg)](https://badge.fury.io/rb/action_policy-graphql)\n![B"
  },
  {
    "path": "Rakefile",
    "chars": 391,
    "preview": "# frozen_string_literal: true\n\nrequire \"bundler/gem_tasks\"\nrequire \"rspec/core/rake_task\"\n\nRSpec::Core::RakeTask.new(:sp"
  },
  {
    "path": "action_policy-graphql.gemspec",
    "chars": 1438,
    "preview": "# frozen_string_literal: true\n\nrequire_relative \"lib/action_policy/graphql/version\"\n\nGem::Specification.new do |spec|\n  "
  },
  {
    "path": "bin/console",
    "chars": 356,
    "preview": "#!/usr/bin/env ruby\n\nrequire \"bundler/setup\"\nrequire \"action_policy/graphql\"\n\n# You can add fixtures and/or initializati"
  },
  {
    "path": "bin/setup",
    "chars": 131,
    "preview": "#!/usr/bin/env bash\nset -euo pipefail\nIFS=$'\\n\\t'\nset -vx\n\nbundle install\n\n# Do any other automated setup that you need "
  },
  {
    "path": "gemfiles/action_policy/master.gemfile",
    "chars": 106,
    "preview": "source \"https://rubygems.org\"\n\ngem \"action_policy\", github: \"palkan/action_policy\"\n\ngemspec path: \"../..\"\n"
  },
  {
    "path": "gemfiles/graphql/master.gemfile",
    "chars": 101,
    "preview": "source \"https://rubygems.org\"\n\ngem \"graphql\", github: \"rmosolgo/graphql-ruby\"\n\ngemspec path: \"../..\"\n"
  },
  {
    "path": "gemfiles/jruby.gemfile",
    "chars": 50,
    "preview": "source \"https://rubygems.org\"\n\ngemspec path: \"..\"\n"
  },
  {
    "path": "gemfiles/rubocop.gemfile",
    "chars": 124,
    "preview": "source \"https://rubygems.org\" do\n  gem \"rubocop-md\", \"~> 1.0\"\n  gem \"standard\", \"~> 1.0\"\n  gem \"ruby-next\", \">= 0.15.0\"\n"
  },
  {
    "path": "lib/action_policy/graphql/authorized_field.rb",
    "chars": 5435,
    "preview": "# frozen_string_literal: true\n\nmodule ActionPolicy\n  module GraphQL\n    # Add `authorized` option to the field\n    #\n   "
  },
  {
    "path": "lib/action_policy/graphql/behaviour.rb",
    "chars": 1747,
    "preview": "# frozen_string_literal: true\n\nrequire \"action_policy/graphql/fields\"\nrequire \"action_policy/graphql/authorized_field\"\n\n"
  },
  {
    "path": "lib/action_policy/graphql/fields.rb",
    "chars": 1319,
    "preview": "# frozen_string_literal: true\n\nrequire \"action_policy/graphql/types/authorization_result\"\n\nmodule ActionPolicy\n  using R"
  },
  {
    "path": "lib/action_policy/graphql/types/authorization_result.rb",
    "chars": 698,
    "preview": "# frozen_string_literal: true\n\nrequire \"action_policy/graphql/types/failure_reasons\"\n\nmodule ActionPolicy\n  module Graph"
  },
  {
    "path": "lib/action_policy/graphql/types/failure_reasons.rb",
    "chars": 412,
    "preview": "# frozen_string_literal: true\n\nmodule ActionPolicy\n  module GraphQL\n    module Types\n      class FailureReasons < ::Grap"
  },
  {
    "path": "lib/action_policy/graphql/version.rb",
    "chars": 100,
    "preview": "# frozen_string_literal: true\n\nmodule ActionPolicy\n  module GraphQL\n    VERSION = \"0.6.0\"\n  end\nend\n"
  },
  {
    "path": "lib/action_policy/graphql.rb",
    "chars": 2120,
    "preview": "# frozen_string_literal: true\n\nrequire \"graphql\"\nrequire \"action_policy\"\n\nrequire \"action_policy/graphql/behaviour\"\n\nmod"
  },
  {
    "path": "lib/action_policy-graphql.rb",
    "chars": 83,
    "preview": "# frozen_string_literal: true\n\nrequire \"ruby-next\"\nrequire \"action_policy/graphql\"\n"
  },
  {
    "path": "spec/action_policy/graphql/authorized_spec.rb",
    "chars": 8260,
    "preview": "# frozen_string_literal: true\n\nrequire \"spec_helper\"\n\ndescribe \"field extensions\", :aggregate_failures do\n  include_cont"
  },
  {
    "path": "spec/action_policy/graphql/behaviour_spec.rb",
    "chars": 2122,
    "preview": "# frozen_string_literal: true\n\nrequire \"spec_helper\"\n\ndescribe ActionPolicy::GraphQL::Behaviour do\n  include_context \"co"
  },
  {
    "path": "spec/action_policy/graphql/expose_authorization_rules_spec.rb",
    "chars": 3705,
    "preview": "# frozen_string_literal: true\n\nrequire \"spec_helper\"\n\ndescribe \"#expose_authorization_rules\", :aggregate_failures do\n  i"
  },
  {
    "path": "spec/spec_helper.rb",
    "chars": 758,
    "preview": "# frozen_string_literal: true\n\n$LOAD_PATH.unshift File.expand_path(\"../lib\", __dir__)\n\n# This turns off per-thread cachi"
  },
  {
    "path": "spec/support/graphql_context.rb",
    "chars": 790,
    "preview": "# frozen_string_literal: true\n\nshared_context \"common:graphql\" do\n  let(:context) { {} }\n  let(:variables) { {} }\n  let("
  },
  {
    "path": "spec/support/schema.rb",
    "chars": 7762,
    "preview": "# frozen_string_literal: true\n\nclass Post < Struct.new(:title); end\n\nclass PostList < Array\n  def policy_name\n    \"#{fir"
  }
]

About this extraction

This page contains the full source code of the palkan/action_policy-graphql GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 35 files (52.1 KB), approximately 14.2k tokens, and a symbol index with 113 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!