graphql-pundit-387 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.codeclimate.yml +16 -0
- data/.gitignore +13 -0
- data/.hound.yml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +100 -0
- data/.ruby-version +1 -0
- data/.stickler.yml +7 -0
- data/.travis.yml +21 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +372 -0
- data/Rakefile +8 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/graphql-pundit.gemspec +39 -0
- data/lib/graphql-pundit.rb +55 -0
- data/lib/graphql-pundit/authorization.rb +91 -0
- data/lib/graphql-pundit/common.rb +29 -0
- data/lib/graphql-pundit/field.rb +18 -0
- data/lib/graphql-pundit/instrumenter.rb +34 -0
- data/lib/graphql-pundit/instrumenters/after_scope.rb +24 -0
- data/lib/graphql-pundit/instrumenters/authorization.rb +78 -0
- data/lib/graphql-pundit/instrumenters/before_scope.rb +30 -0
- data/lib/graphql-pundit/instrumenters/scope.rb +91 -0
- data/lib/graphql-pundit/scope.rb +51 -0
- data/lib/graphql-pundit/version.rb +7 -0
- metadata +258 -0
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
#!/usr/bin/env ruby
|
3
|
+
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'graphql/pundit'
|
6
|
+
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
9
|
+
|
10
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
11
|
+
# require "pry"
|
12
|
+
# Pry.start
|
13
|
+
|
14
|
+
require 'irb'
|
15
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require 'graphql-pundit/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = 'graphql-pundit-387'
|
9
|
+
spec.version = GraphQL::Pundit::VERSION
|
10
|
+
spec.authors = ['Ontohub Core Developers']
|
11
|
+
spec.email = ['ontohub-dev-l@ovgu.de']
|
12
|
+
|
13
|
+
spec.summary = 'Pundit authorization support for graphql'
|
14
|
+
spec.description = spec.summary
|
15
|
+
spec.homepage = 'https://github.com/ontohub/graphql-pundit'
|
16
|
+
spec.license = 'MIT'
|
17
|
+
|
18
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
19
|
+
f.match(%r{^(test|spec|features)/})
|
20
|
+
end
|
21
|
+
spec.bindir = 'exe'
|
22
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
23
|
+
spec.require_paths = ['lib']
|
24
|
+
|
25
|
+
spec.add_dependency 'graphql', '>= 1.6.4', '< 1.10.0'
|
26
|
+
spec.add_dependency 'pundit', '~> 1.1.0'
|
27
|
+
|
28
|
+
spec.add_development_dependency 'bundler', '~> 1.14'
|
29
|
+
spec.add_development_dependency 'codecov', '~> 0.1.10'
|
30
|
+
spec.add_development_dependency 'fuubar', '~> 2.3.0'
|
31
|
+
spec.add_development_dependency 'pry', '~> 0.11.0'
|
32
|
+
spec.add_development_dependency 'pry-byebug', '~> 3.6.0'
|
33
|
+
spec.add_development_dependency 'pry-rescue', '~> 1.4.4'
|
34
|
+
spec.add_development_dependency 'pry-stack_explorer', '~> 0.4.9.2'
|
35
|
+
spec.add_development_dependency 'rake', '~> 12.0'
|
36
|
+
spec.add_development_dependency 'rspec', '~> 3.6'
|
37
|
+
spec.add_development_dependency 'rubocop', '~> 0.57.0'
|
38
|
+
spec.add_development_dependency 'simplecov', '~> 0.16.1'
|
39
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'graphql-pundit/instrumenter'
|
4
|
+
require 'graphql-pundit/field'
|
5
|
+
require 'graphql-pundit/authorization'
|
6
|
+
require 'graphql-pundit/scope'
|
7
|
+
require 'graphql-pundit/version'
|
8
|
+
|
9
|
+
require 'graphql'
|
10
|
+
|
11
|
+
# Defines authorization related helpers
|
12
|
+
module GraphQL
|
13
|
+
# Defines `authorize` and `authorize!` helpers
|
14
|
+
class AuthorizationHelper
|
15
|
+
attr_reader :raise_unauthorized
|
16
|
+
|
17
|
+
def initialize(raise_unauthorized)
|
18
|
+
@raise_unauthorized = raise_unauthorized
|
19
|
+
end
|
20
|
+
|
21
|
+
def call(defn, *args, policy: nil, record: nil)
|
22
|
+
query = args[0] || defn.name
|
23
|
+
opts = {record: record,
|
24
|
+
query: query,
|
25
|
+
policy: policy,
|
26
|
+
raise: raise_unauthorized}
|
27
|
+
if query.respond_to?(:call)
|
28
|
+
opts = {proc: query, raise: raise_unauthorized}
|
29
|
+
end
|
30
|
+
Define::InstanceDefinable::AssignMetadataKey.new(:authorize).
|
31
|
+
call(defn, opts)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Defines `scope` helper
|
36
|
+
class ScopeHelper
|
37
|
+
def initialize(before_or_after, deprecated: false)
|
38
|
+
@before_or_after = before_or_after
|
39
|
+
@deprecated = deprecated
|
40
|
+
end
|
41
|
+
|
42
|
+
def call(defn, proc = :infer_scope)
|
43
|
+
opts = {proc: proc, deprecated: @deprecated}
|
44
|
+
Define::InstanceDefinable::AssignMetadataKey.
|
45
|
+
new(:"#{@before_or_after}_scope").
|
46
|
+
call(defn, opts)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
Field.accepts_definitions(authorize: AuthorizationHelper.new(false),
|
51
|
+
authorize!: AuthorizationHelper.new(true),
|
52
|
+
after_scope: ScopeHelper.new(:after),
|
53
|
+
before_scope: ScopeHelper.new(:before),
|
54
|
+
scope: ScopeHelper.new(:before, deprecated: true))
|
55
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'graphql-pundit/common'
|
4
|
+
|
5
|
+
module GraphQL
|
6
|
+
module Pundit
|
7
|
+
# Authorization methods to be included in the used Field class
|
8
|
+
module Authorization
|
9
|
+
def self.prepended(base)
|
10
|
+
base.include(GraphQL::Pundit::Common)
|
11
|
+
end
|
12
|
+
|
13
|
+
# rubocop:disable Metrics/ParameterLists
|
14
|
+
def initialize(*args, authorize: nil,
|
15
|
+
record: nil,
|
16
|
+
policy: nil,
|
17
|
+
**kwargs, &block)
|
18
|
+
# rubocop:enable Metrics/ParameterLists
|
19
|
+
# authorize! is not a valid variable name
|
20
|
+
authorize_bang = kwargs.delete(:authorize!)
|
21
|
+
@record = record if record
|
22
|
+
@policy = policy if policy
|
23
|
+
@authorize = authorize_bang || authorize
|
24
|
+
@do_raise = !!authorize_bang
|
25
|
+
super(*args, **kwargs, &block)
|
26
|
+
end
|
27
|
+
|
28
|
+
def authorize(*args, record: nil, policy: nil)
|
29
|
+
@authorize = args[0] || true
|
30
|
+
@record = record if record
|
31
|
+
@policy = policy if policy
|
32
|
+
end
|
33
|
+
|
34
|
+
def authorize!(*args, record: nil, policy: nil)
|
35
|
+
@do_raise = true
|
36
|
+
authorize(*args, record: record, policy: policy)
|
37
|
+
end
|
38
|
+
|
39
|
+
def resolve_field(obj, args, ctx)
|
40
|
+
raise ::Pundit::NotAuthorizedError unless do_authorize(obj, args, ctx)
|
41
|
+
super(obj, args, ctx)
|
42
|
+
rescue ::Pundit::NotAuthorizedError
|
43
|
+
if @do_raise
|
44
|
+
raise GraphQL::ExecutionError, "You're not authorized to do this"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def do_authorize(root, arguments, context)
|
51
|
+
return true unless @authorize
|
52
|
+
return @authorize.call(root, arguments, context) if callable? @authorize
|
53
|
+
|
54
|
+
query = infer_query(@authorize)
|
55
|
+
record = infer_record(@record, root, arguments, context)
|
56
|
+
policy = infer_policy(@policy, record, arguments, context)
|
57
|
+
|
58
|
+
policy.new(context[self.class.current_user], record).public_send query
|
59
|
+
end
|
60
|
+
|
61
|
+
def infer_query(auth_value)
|
62
|
+
# authorize can be callable, true (for inference) or a policy query
|
63
|
+
query = auth_value.equal?(true) ? method_sym : auth_value
|
64
|
+
query.to_s + '?'
|
65
|
+
end
|
66
|
+
|
67
|
+
def infer_record(record, root, arguments, context)
|
68
|
+
# record can be callable, nil (for inference) or just any other value
|
69
|
+
if callable?(record)
|
70
|
+
record.call(root, arguments, context)
|
71
|
+
elsif record.equal?(nil)
|
72
|
+
root
|
73
|
+
else
|
74
|
+
record
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def infer_policy(policy, record, arguments, context)
|
79
|
+
# policy can be callable, nil (for inference) or a policy class
|
80
|
+
if callable?(policy)
|
81
|
+
policy.call(record, arguments, context)
|
82
|
+
elsif policy.equal?(nil)
|
83
|
+
infer_from = model?(record) ? record.model : record
|
84
|
+
::Pundit::PolicyFinder.new(infer_from).policy!
|
85
|
+
else
|
86
|
+
policy
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Pundit
|
5
|
+
# Common methods used for authorization and scopes
|
6
|
+
module Common
|
7
|
+
# Class methods to be included in the Field class
|
8
|
+
module ClassMethods
|
9
|
+
def current_user(current_user = nil)
|
10
|
+
return @current_user unless current_user
|
11
|
+
@current_user = current_user
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.included(base)
|
16
|
+
@current_user = :current_user
|
17
|
+
base.extend(ClassMethods)
|
18
|
+
end
|
19
|
+
|
20
|
+
def callable?(thing)
|
21
|
+
thing.respond_to?(:call)
|
22
|
+
end
|
23
|
+
|
24
|
+
def model?(thing)
|
25
|
+
thing.respond_to?(:model)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'graphql'
|
4
|
+
require 'graphql-pundit/authorization'
|
5
|
+
require 'graphql-pundit/scope'
|
6
|
+
|
7
|
+
module GraphQL
|
8
|
+
module Pundit
|
9
|
+
if defined?(GraphQL::Schema::Field)
|
10
|
+
# Field class that contains authorization and scope behavior
|
11
|
+
# This only works with graphql >= 1.8.0
|
12
|
+
class Field < GraphQL::Schema::Field
|
13
|
+
# prepend GraphQL::Pundit::Scope
|
14
|
+
prepend GraphQL::Pundit::Authorization
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pundit'
|
4
|
+
require 'graphql-pundit/instrumenters/authorization'
|
5
|
+
require 'graphql-pundit/instrumenters/before_scope'
|
6
|
+
require 'graphql-pundit/instrumenters/after_scope'
|
7
|
+
|
8
|
+
module GraphQL
|
9
|
+
module Pundit
|
10
|
+
# Intrumenter combining the authorization and scope instrumenters
|
11
|
+
class Instrumenter
|
12
|
+
attr_reader :current_user,
|
13
|
+
:authorization_instrumenter,
|
14
|
+
:before_scope_instrumenter,
|
15
|
+
:after_scope_instrumenter
|
16
|
+
|
17
|
+
def initialize(current_user = :current_user)
|
18
|
+
@current_user = current_user
|
19
|
+
@authorization_instrumenter =
|
20
|
+
Instrumenters::Authorization.new(current_user)
|
21
|
+
@before_scope_instrumenter =
|
22
|
+
Instrumenters::BeforeScope.new(current_user)
|
23
|
+
@after_scope_instrumenter = Instrumenters::AfterScope.new(current_user)
|
24
|
+
end
|
25
|
+
|
26
|
+
def instrument(type, field)
|
27
|
+
before_scoped_field = before_scope_instrumenter.instrument(type, field)
|
28
|
+
after_scoped_field = after_scope_instrumenter.
|
29
|
+
instrument(type, before_scoped_field)
|
30
|
+
authorization_instrumenter.instrument(type, after_scoped_field)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pundit'
|
4
|
+
require_relative 'scope'
|
5
|
+
|
6
|
+
module GraphQL
|
7
|
+
module Pundit
|
8
|
+
module Instrumenters
|
9
|
+
# Instrumenter that supplies `after_scope`
|
10
|
+
class AfterScope < Scope
|
11
|
+
SCOPE_KEY = :after_scope
|
12
|
+
|
13
|
+
# Applies the scoping to the passed object
|
14
|
+
class ScopeResolver < ScopeResolver
|
15
|
+
def call(root, arguments, context)
|
16
|
+
resolver_result = old_resolver.call(root, arguments, context)
|
17
|
+
scope_proc = new_scope(scope)
|
18
|
+
scope_proc.call(resolver_result, arguments, context)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pundit'
|
4
|
+
|
5
|
+
module GraphQL
|
6
|
+
module Pundit
|
7
|
+
module Instrumenters
|
8
|
+
# Instrumenter that supplies `authorize`
|
9
|
+
class Authorization
|
10
|
+
# This does the actual Pundit authorization
|
11
|
+
class AuthorizationResolver
|
12
|
+
attr_reader :current_user, :old_resolver, :options
|
13
|
+
def initialize(current_user, old_resolver, options)
|
14
|
+
@current_user = current_user
|
15
|
+
@old_resolver = old_resolver
|
16
|
+
@options = options
|
17
|
+
end
|
18
|
+
|
19
|
+
def call(root, arguments, context)
|
20
|
+
unless authorize(root, arguments, context)
|
21
|
+
raise ::Pundit::NotAuthorizedError
|
22
|
+
end
|
23
|
+
old_resolver.call(root, arguments, context)
|
24
|
+
rescue ::Pundit::NotAuthorizedError
|
25
|
+
if options[:raise]
|
26
|
+
raise GraphQL::ExecutionError, "You're not authorized to do this"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def authorize(root, arguments, context)
|
33
|
+
if options[:proc]
|
34
|
+
options[:proc].call(root, arguments, context)
|
35
|
+
else
|
36
|
+
record = record(root, arguments, context)
|
37
|
+
::Pundit::PolicyFinder.new(policy(record)).policy!.
|
38
|
+
new(context[current_user], record).public_send(query)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def query
|
43
|
+
@query ||= options[:query].to_s + '?'
|
44
|
+
end
|
45
|
+
|
46
|
+
def policy(record)
|
47
|
+
options[:policy] || record
|
48
|
+
end
|
49
|
+
|
50
|
+
def record(root, arguments, context)
|
51
|
+
if options[:record].respond_to?(:call)
|
52
|
+
options[:record].call(root, arguments, context)
|
53
|
+
else
|
54
|
+
options[:record] || root
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
attr_reader :current_user
|
60
|
+
|
61
|
+
def initialize(current_user = :current_user)
|
62
|
+
@current_user = current_user
|
63
|
+
end
|
64
|
+
|
65
|
+
def instrument(_type, field)
|
66
|
+
return field unless field.metadata[:authorize]
|
67
|
+
old_resolver = field.resolve_proc
|
68
|
+
resolver = AuthorizationResolver.new(current_user,
|
69
|
+
old_resolver,
|
70
|
+
field.metadata[:authorize])
|
71
|
+
field.redefine do
|
72
|
+
resolve resolver
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pundit'
|
4
|
+
require_relative 'scope'
|
5
|
+
|
6
|
+
module GraphQL
|
7
|
+
module Pundit
|
8
|
+
module Instrumenters
|
9
|
+
# Instrumenter that supplies `before_scope`
|
10
|
+
class BeforeScope < Scope
|
11
|
+
SCOPE_KEY = :before_scope
|
12
|
+
|
13
|
+
# Applies the scoping to the passed object
|
14
|
+
class ScopeResolver < ScopeResolver
|
15
|
+
def call(root, arguments, context)
|
16
|
+
if field.metadata[:before_scope][:deprecated]
|
17
|
+
Kernel.warn <<~DEPRECATION_WARNING
|
18
|
+
Using `scope` is deprecated and might be removed in the future.
|
19
|
+
Please use `before_scope` or `after_scope` instead.
|
20
|
+
DEPRECATION_WARNING
|
21
|
+
end
|
22
|
+
scope_proc = new_scope(scope)
|
23
|
+
resolver_result = scope_proc.call(root, arguments, context)
|
24
|
+
old_resolver.call(resolver_result, arguments, context)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pundit'
|
4
|
+
|
5
|
+
module GraphQL
|
6
|
+
module Pundit
|
7
|
+
module Instrumenters
|
8
|
+
# Base instrumenter for `before_scope` and `after_scope`
|
9
|
+
class Scope
|
10
|
+
# Applies the scoping to the passed object
|
11
|
+
class ScopeResolver
|
12
|
+
attr_reader :current_user, :scope, :old_resolver, :field
|
13
|
+
|
14
|
+
def initialize(current_user, scope, old_resolver, field)
|
15
|
+
@current_user = current_user
|
16
|
+
@old_resolver = old_resolver
|
17
|
+
@field = field
|
18
|
+
|
19
|
+
unless valid_value?(scope)
|
20
|
+
raise ArgumentError, 'Invalid value passed to `scope`'
|
21
|
+
end
|
22
|
+
|
23
|
+
@scope = scope
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def new_scope(scope)
|
29
|
+
return scope if proc?(scope)
|
30
|
+
|
31
|
+
lambda do |root, _arguments, context|
|
32
|
+
scope = find_scope(root, scope)
|
33
|
+
scope.new(context[current_user], root).resolve
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def find_scope(root, scope)
|
38
|
+
if !inferred?(scope)
|
39
|
+
scope::Scope
|
40
|
+
else
|
41
|
+
# Special case for Sequel datasets that do not respond to
|
42
|
+
# ActiveModel's model_name
|
43
|
+
infer_from = if root.respond_to?(:model)
|
44
|
+
root.model
|
45
|
+
else
|
46
|
+
root
|
47
|
+
end
|
48
|
+
::Pundit::PolicyFinder.new(infer_from).scope!
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def valid_value?(value)
|
53
|
+
value.is_a?(Class) || inferred?(value) || proc?(value)
|
54
|
+
end
|
55
|
+
|
56
|
+
def proc?(value)
|
57
|
+
value.respond_to?(:call)
|
58
|
+
end
|
59
|
+
|
60
|
+
def inferred?(value)
|
61
|
+
value == :infer_scope
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
attr_reader :current_user
|
66
|
+
|
67
|
+
def initialize(current_user = :current_user)
|
68
|
+
@current_user = current_user
|
69
|
+
end
|
70
|
+
|
71
|
+
# rubocop:disable Metrics/MethodLength
|
72
|
+
def instrument(_type, field)
|
73
|
+
# rubocop:enable Metrics/MethodLength
|
74
|
+
scope_metadata = field.metadata[self.class::SCOPE_KEY]
|
75
|
+
return field unless scope_metadata
|
76
|
+
scope = scope_metadata[:proc]
|
77
|
+
|
78
|
+
old_resolver = field.resolve_proc
|
79
|
+
resolver = self.class::ScopeResolver.new(current_user,
|
80
|
+
scope,
|
81
|
+
old_resolver,
|
82
|
+
field)
|
83
|
+
|
84
|
+
field.redefine do
|
85
|
+
resolve resolver
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|