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
================================================
[](https://badge.fury.io/rb/action_policy-graphql)


[](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
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
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": "[](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.