graphql-pundit-387 0.7.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|