graphql-pundit 0.1.0 → 0.2.0
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 +4 -4
- data/.codeclimate.yml +16 -0
- data/.rubocop.yml +1 -1
- data/Gemfile +2 -0
- data/README.md +51 -6
- data/Rakefile +5 -3
- data/bin/console +4 -3
- data/lib/graphql-pundit.rb +23 -7
- data/lib/graphql-pundit/instrumenter.rb +21 -28
- data/lib/graphql-pundit/instrumenters/authorization.rb +52 -0
- data/lib/graphql-pundit/instrumenters/scope.rb +56 -0
- data/lib/graphql-pundit/version.rb +3 -1
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6ce932c3e90158e8692934bac208942e9a10faa9
|
4
|
+
data.tar.gz: 87858e55cab3ec052e5756b66ebb5fa2285e4dc8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5fd1f0da13f241e13bcf145adadb710f256a36418049b93c56674c1d10dc3cf1d3fc3bf75773eb49fd3b72ff0d1057ec2949d3fd1b5bfebbdb6279641fcbe67a
|
7
|
+
data.tar.gz: 86d4a7a066b99752538caf1e60f554939e514af4a7fe86138ac6d85c5a2a389aef2bb0486f756b90972eb5140e4088854fc682173645b0276ac02f10ed29c077
|
data/.codeclimate.yml
ADDED
data/.rubocop.yml
CHANGED
@@ -503,7 +503,7 @@ Layout/ExtraSpacing:
|
|
503
503
|
Style/FileName:
|
504
504
|
# File names listed in `AllCops:Include` are excluded by default. Add extra
|
505
505
|
# excludes here.
|
506
|
-
Exclude: []
|
506
|
+
Exclude: ['lib/graphql-pundit.rb']
|
507
507
|
# When `true`, requires that each source file should define a class or module
|
508
508
|
# with a name which matches the file name (converted to ... case).
|
509
509
|
# It further expects it to be nested inside modules which match the names
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,3 +1,10 @@
|
|
1
|
+
[](https://rubygems.org/gems/graphql-pundit)
|
2
|
+
[](https://travis-ci.org/ontohub/graphql-pundit)
|
3
|
+
[](https://codecov.io/gh/ontohub/graphql-pundit)
|
4
|
+
[](https://codeclimate.com/github/ontohub/graphql-pundit)
|
5
|
+
[](https://gemnasium.com/github.com/ontohub/graphql-pundit)
|
6
|
+
[](https://waffle.io/ontohub/ontohub-backend?source=ontohub%2Fgraphql-pundit)
|
7
|
+
|
1
8
|
# GraphQL::Pundit
|
2
9
|
|
3
10
|
|
@@ -7,8 +14,7 @@
|
|
7
14
|
Add this line to your application's Gemfile:
|
8
15
|
|
9
16
|
```ruby
|
10
|
-
gem 'graphql-pundit'
|
11
|
-
branch: 'master'
|
17
|
+
gem 'graphql-pundit'
|
12
18
|
```
|
13
19
|
|
14
20
|
And then execute:
|
@@ -43,23 +49,30 @@ For each field you want to authorize via Pundit, add the following code to the f
|
|
43
49
|
|
44
50
|
```ruby
|
45
51
|
field :email do
|
46
|
-
authorize
|
52
|
+
authorize # will use UserPolicy#email?
|
47
53
|
resolve ...
|
48
54
|
end
|
49
55
|
```
|
50
56
|
|
51
|
-
By default, this will use the Policy for the parent object (the first argument passed to the resolve proc), checking for `:
|
57
|
+
By default, this will use the Policy for the parent object (the first argument passed to the resolve proc), checking for `:email?` for the current user. Sometimes, the field name will differ from the policy method name, in which case you can specify it explicitly:
|
58
|
+
|
59
|
+
```ruby
|
60
|
+
field :email do
|
61
|
+
authorize :read_email # will use UserPolicy#read_email?
|
62
|
+
resolve ...
|
63
|
+
end
|
64
|
+
```
|
52
65
|
|
53
66
|
Now, in some cases you'll want to use a different policy, or in case of mutations, the passed object might be `nil`:
|
54
67
|
|
55
68
|
```ruby
|
56
69
|
field :createUser
|
57
|
-
authorize! :create, User
|
70
|
+
authorize! :create, User # or User.new; will use UserPolicy#create?
|
58
71
|
resolve ...
|
59
72
|
end
|
60
73
|
```
|
61
74
|
|
62
|
-
This will use the `:create?` method of the `UserPolicy`.
|
75
|
+
This will use the `:create?` method of the `UserPolicy`. You can also pass in objects instead of a class, if you wish to authorize the user for the specific object.
|
63
76
|
|
64
77
|
You might have also noticed the use of `authorize!` instead of `authorize` in this example. The difference between the two is this:
|
65
78
|
|
@@ -67,6 +80,38 @@ You might have also noticed the use of `authorize!` instead of `authorize` in th
|
|
67
80
|
- `authorize!` will set the field to `nil` and add an error to the response if authorization fails
|
68
81
|
|
69
82
|
You would normally want to use `authorize` for fields in queries, that only e.g. the owner of something can see, while `authorize!` would be usually used in mutations, where you want to communicate to the client that the operation failed because the user is unauthorized.
|
83
|
+
|
84
|
+
If you still need more control over how policies are called, you can pass a lambda to `authorize`:
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
field :email
|
88
|
+
authorize ->(obj, args, ctx) { UserPolicy.new(obj, ctx[:me]).private_data?(:email) }
|
89
|
+
resolve ...
|
90
|
+
end
|
91
|
+
```
|
92
|
+
|
93
|
+
If the lambda returns a falsy value or raises a `Pundit::UnauthorizedError` the field will resolve to `nil`, if it returns a truthy value, control will be passed to the resolve function. Of course, this can be used with `authorize!` as well.
|
94
|
+
|
95
|
+
### Scopes
|
96
|
+
|
97
|
+
Pundit scopes are supported by using `scope` in the field definition
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
field :posts
|
101
|
+
scope
|
102
|
+
resolve ...
|
103
|
+
end
|
104
|
+
```
|
105
|
+
|
106
|
+
By default, this will use the Scope definied in the `PostPolicy`. If you do not want to define a scope inside of the policy, you can also pass a lambda to `scope`. The return value will be passed to `resolve` as first argument.
|
107
|
+
|
108
|
+
```ruby
|
109
|
+
field :posts
|
110
|
+
scope ->(_root, _args, ctx) { Post.where(owner: ctx[:current_user]) }
|
111
|
+
resolve ->(posts, args, ctx) { ... }
|
112
|
+
end
|
113
|
+
```
|
114
|
+
|
70
115
|
## Development
|
71
116
|
|
72
117
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
data/Rakefile
CHANGED
data/bin/console
CHANGED
@@ -1,7 +1,8 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
#!/usr/bin/env ruby
|
2
3
|
|
3
|
-
require
|
4
|
-
require
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'graphql/pundit'
|
5
6
|
|
6
7
|
# You can add fixtures and/or initialization code here to make experimenting
|
7
8
|
# with your gem easier. You can also use a different console, if you like.
|
@@ -10,5 +11,5 @@ require "graphql/pundit"
|
|
10
11
|
# require "pry"
|
11
12
|
# Pry.start
|
12
13
|
|
13
|
-
require
|
14
|
+
require 'irb'
|
14
15
|
IRB.start(__FILE__)
|
data/lib/graphql-pundit.rb
CHANGED
@@ -1,17 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'graphql-pundit/instrumenter'
|
2
4
|
require 'graphql-pundit/version'
|
3
5
|
|
4
6
|
require 'graphql'
|
5
7
|
|
8
|
+
# Define `authorize` and `authorize!` helpers
|
6
9
|
module GraphQL
|
7
10
|
def self.assign_authorize(raise_unauthorized)
|
8
|
-
lambda do |defn, query, record = nil|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
)
|
11
|
+
lambda do |defn, query = nil, record = nil|
|
12
|
+
opts = {record: record,
|
13
|
+
query: query || defn.name,
|
14
|
+
raise: raise_unauthorized}
|
15
|
+
if query.respond_to?(:call)
|
16
|
+
opts = {proc: query, raise: raise_unauthorized}
|
17
|
+
end
|
18
|
+
Define::InstanceDefinable::AssignMetadataKey.new(:authorize).
|
19
|
+
call(defn, opts)
|
13
20
|
end
|
14
21
|
end
|
15
|
-
|
16
|
-
|
22
|
+
|
23
|
+
def self.assign_scope
|
24
|
+
lambda do |defn, proc = :infer_scope|
|
25
|
+
Define::InstanceDefinable::AssignMetadataKey.new(:scope).
|
26
|
+
call(defn, proc)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
Field.accepts_definitions(authorize: assign_authorize(false),
|
31
|
+
authorize!: assign_authorize(true),
|
32
|
+
scope: assign_scope)
|
17
33
|
end
|
@@ -1,43 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'pundit'
|
4
|
+
require 'graphql-pundit/instrumenters/authorization'
|
5
|
+
require 'graphql-pundit/instrumenters/scope'
|
2
6
|
|
3
7
|
module GraphQL
|
4
8
|
module Pundit
|
9
|
+
# Intrumenter combining the authorization and scope instrumenters
|
5
10
|
class Instrumenter
|
6
|
-
attr_reader :current_user
|
11
|
+
attr_reader :current_user,
|
12
|
+
:authorization_instrumenter,
|
13
|
+
:scope_instrumenter
|
7
14
|
|
8
15
|
def initialize(current_user = :current_user)
|
9
16
|
@current_user = current_user
|
17
|
+
@authorization_instrumenter = Instrumenters::Authorization.
|
18
|
+
new(current_user)
|
19
|
+
@scope_instrumenter = Instrumenters::Scope.new(current_user)
|
10
20
|
end
|
11
21
|
|
12
|
-
def instrument(
|
13
|
-
|
14
|
-
|
15
|
-
resolve_proc = resolve_proc(current_user,
|
16
|
-
old_resolve,
|
17
|
-
field.metadata[:authorize])
|
18
|
-
field.redefine do
|
19
|
-
resolve resolve_proc
|
20
|
-
end
|
21
|
-
else
|
22
|
-
field
|
23
|
-
end
|
22
|
+
def instrument(type, field)
|
23
|
+
scoped_field = scope_instrumenter.instrument(type, field)
|
24
|
+
authorization_instrumenter.instrument(type, scoped_field)
|
24
25
|
end
|
25
26
|
|
26
|
-
def
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
end
|
34
|
-
old_resolve.call(obj, args, ctx)
|
35
|
-
rescue ::Pundit::NotAuthorizedError
|
36
|
-
if options[:raise]
|
37
|
-
raise GraphQL::ExecutionError,
|
38
|
-
"You're not authorized to do this"
|
39
|
-
end
|
40
|
-
end
|
27
|
+
def authorize(current_user, obj, args, ctx, options)
|
28
|
+
if options[:proc]
|
29
|
+
options[:proc].call(obj, args, ctx)
|
30
|
+
else
|
31
|
+
::Pundit.authorize(ctx[current_user],
|
32
|
+
options[:record] || obj,
|
33
|
+
options[:query].to_s + '?')
|
41
34
|
end
|
42
35
|
end
|
43
36
|
end
|
@@ -0,0 +1,52 @@
|
|
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
|
+
attr_reader :current_user
|
11
|
+
|
12
|
+
def initialize(current_user = :current_user)
|
13
|
+
@current_user = current_user
|
14
|
+
end
|
15
|
+
|
16
|
+
def instrument(_type, field)
|
17
|
+
return field unless field.metadata[:authorize]
|
18
|
+
old_resolve = field.resolve_proc
|
19
|
+
resolve_proc = resolve_proc(current_user,
|
20
|
+
old_resolve,
|
21
|
+
field.metadata[:authorize])
|
22
|
+
field.redefine do
|
23
|
+
resolve resolve_proc
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
28
|
+
def resolve_proc(current_user, old_resolve, options)
|
29
|
+
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
30
|
+
lambda do |obj, args, ctx|
|
31
|
+
begin
|
32
|
+
result = if options[:proc]
|
33
|
+
options[:proc].call(obj, args, ctx)
|
34
|
+
else
|
35
|
+
query = options[:query].to_s + '?'
|
36
|
+
record = options[:record] || obj
|
37
|
+
::Pundit.authorize(ctx[current_user], record, query)
|
38
|
+
end
|
39
|
+
raise ::Pundit::NotAuthorizedError unless result
|
40
|
+
old_resolve.call(obj, args, ctx)
|
41
|
+
rescue ::Pundit::NotAuthorizedError
|
42
|
+
if options[:raise]
|
43
|
+
raise GraphQL::ExecutionError,
|
44
|
+
"You're not authorized to do this"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pundit'
|
4
|
+
|
5
|
+
module GraphQL
|
6
|
+
module Pundit
|
7
|
+
module Instrumenters
|
8
|
+
# Instrumenter that supplies `scope`
|
9
|
+
class Scope
|
10
|
+
attr_reader :current_user
|
11
|
+
|
12
|
+
def initialize(current_user = :current_user)
|
13
|
+
@current_user = current_user
|
14
|
+
end
|
15
|
+
|
16
|
+
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
17
|
+
def instrument(_type, field)
|
18
|
+
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
19
|
+
scope = field.metadata[:scope]
|
20
|
+
return field unless scope
|
21
|
+
unless valid_value?(scope)
|
22
|
+
raise ArgumentError, 'Invalid value passed to `scope`'
|
23
|
+
end
|
24
|
+
|
25
|
+
old_resolve = field.resolve_proc
|
26
|
+
|
27
|
+
scope_proc = lambda do |obj, _args, ctx|
|
28
|
+
::Pundit.policy_scope!(ctx[current_user], obj)
|
29
|
+
end
|
30
|
+
scope_proc = scope if proc?(scope)
|
31
|
+
|
32
|
+
field.redefine do
|
33
|
+
resolve(lambda do |obj, args, ctx|
|
34
|
+
new_scope = scope_proc.call(obj, args, ctx)
|
35
|
+
old_resolve.call(new_scope, args, ctx)
|
36
|
+
end)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def valid_value?(value)
|
43
|
+
inferred?(value) || proc?(value)
|
44
|
+
end
|
45
|
+
|
46
|
+
def proc?(value)
|
47
|
+
value.respond_to?(:call)
|
48
|
+
end
|
49
|
+
|
50
|
+
def inferred?(value)
|
51
|
+
value == :infer_scope
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: graphql-pundit
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ontohub Core Developers
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-09-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: graphql
|
@@ -143,6 +143,7 @@ executables: []
|
|
143
143
|
extensions: []
|
144
144
|
extra_rdoc_files: []
|
145
145
|
files:
|
146
|
+
- ".codeclimate.yml"
|
146
147
|
- ".gitignore"
|
147
148
|
- ".hound.yml"
|
148
149
|
- ".rspec"
|
@@ -161,6 +162,8 @@ files:
|
|
161
162
|
- graphql-pundit.gemspec
|
162
163
|
- lib/graphql-pundit.rb
|
163
164
|
- lib/graphql-pundit/instrumenter.rb
|
165
|
+
- lib/graphql-pundit/instrumenters/authorization.rb
|
166
|
+
- lib/graphql-pundit/instrumenters/scope.rb
|
164
167
|
- lib/graphql-pundit/version.rb
|
165
168
|
homepage: https://github.com/ontohub/graphql-pundit
|
166
169
|
licenses:
|