Skip to content
This repository has been archived by the owner on Jan 30, 2020. It is now read-only.

Commit

Permalink
2 support scopes (#11)
Browse files Browse the repository at this point in the history
* Add scope support

* Fix or ignore style issues

* Add a bit of documentation

* Obey review
  • Loading branch information
phyrog authored Sep 6, 2017
1 parent 13ce645 commit 195039a
Show file tree
Hide file tree
Showing 7 changed files with 274 additions and 30 deletions.
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,26 @@ end

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.

### Scopes

Pundit scopes are supported by using `scope` in the field definition

```ruby
field :posts
scope
resolve ...
end
```

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.

```ruby
field :posts
scope ->(_root, _args, ctx) { Post.where(owner: ctx[:current_user]) }
resolve ->(posts, args, ctx) { ... }
end
```

## Development

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.
Expand Down
13 changes: 11 additions & 2 deletions lib/graphql-pundit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ def self.assign_authorize(raise_unauthorized)
call(defn, opts)
end
end
Field.accepts_definitions authorize: assign_authorize(false)
Field.accepts_definitions authorize!: assign_authorize(true)

def self.assign_scope
lambda do |defn, proc = :infer_scope|
Define::InstanceDefinable::AssignMetadataKey.new(:scope).
call(defn, proc)
end
end

Field.accepts_definitions(authorize: assign_authorize(false),
authorize!: assign_authorize(true),
scope: assign_scope)
end
39 changes: 12 additions & 27 deletions lib/graphql-pundit/instrumenter.rb
Original file line number Diff line number Diff line change
@@ -1,42 +1,27 @@
# frozen_string_literal: true

require 'pundit'
require 'graphql-pundit/instrumenters/authorization'
require 'graphql-pundit/instrumenters/scope'

module GraphQL
module Pundit
# The authorization Instrumenter
# Intrumenter combining the authorization and scope instrumenters
class Instrumenter
attr_reader :current_user
attr_reader :current_user,
:authorization_instrumenter,
:scope_instrumenter

def initialize(current_user = :current_user)
@current_user = current_user
@authorization_instrumenter = Instrumenters::Authorization.
new(current_user)
@scope_instrumenter = Instrumenters::Scope.new(current_user)
end

def instrument(_type, field)
return field unless field.metadata[:authorize]

old_resolve = field.resolve_proc
resolve_proc = resolve_proc(current_user,
old_resolve,
field.metadata[:authorize])
field.redefine do
resolve resolve_proc
end
end

private

def resolve_proc(current_user, old_resolve, options)
lambda do |obj, args, ctx|
begin
result = authorize(current_user, obj, args, ctx, options)
raise ::Pundit::NotAuthorizedError unless result
old_resolve.call(obj, args, ctx)
rescue ::Pundit::NotAuthorizedError
error_message = "You're not authorized to do this"
raise GraphQL::ExecutionError, error_message if options[:raise]
end
end
def instrument(type, field)
scoped_field = scope_instrumenter.instrument(type, field)
authorization_instrumenter.instrument(type, scoped_field)
end

def authorize(current_user, obj, args, ctx, options)
Expand Down
52 changes: 52 additions & 0 deletions lib/graphql-pundit/instrumenters/authorization.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# frozen_string_literal: true

require 'pundit'

module GraphQL
module Pundit
module Instrumenters
# Instrumenter that supplies `authorize`
class Authorization
attr_reader :current_user

def initialize(current_user = :current_user)
@current_user = current_user
end

def instrument(_type, field)
return field unless field.metadata[:authorize]
old_resolve = field.resolve_proc
resolve_proc = resolve_proc(current_user,
old_resolve,
field.metadata[:authorize])
field.redefine do
resolve resolve_proc
end
end

# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
def resolve_proc(current_user, old_resolve, options)
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
lambda do |obj, args, ctx|
begin
result = if options[:proc]
options[:proc].call(obj, args, ctx)
else
query = options[:query].to_s + '?'
record = options[:record] || obj
::Pundit.authorize(ctx[current_user], record, query)
end
raise ::Pundit::NotAuthorizedError unless result
old_resolve.call(obj, args, ctx)
rescue ::Pundit::NotAuthorizedError
if options[:raise]
raise GraphQL::ExecutionError,
"You're not authorized to do this"
end
end
end
end
end
end
end
end
56 changes: 56 additions & 0 deletions lib/graphql-pundit/instrumenters/scope.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# frozen_string_literal: true

require 'pundit'

module GraphQL
module Pundit
module Instrumenters
# Instrumenter that supplies `scope`
class Scope
attr_reader :current_user

def initialize(current_user = :current_user)
@current_user = current_user
end

# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
def instrument(_type, field)
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
scope = field.metadata[:scope]
return field unless scope
unless valid_value?(scope)
raise ArgumentError, 'Invalid value passed to `scope`'
end

old_resolve = field.resolve_proc

scope_proc = lambda do |obj, _args, ctx|
::Pundit.policy_scope!(ctx[current_user], obj)
end
scope_proc = scope if proc?(scope)

field.redefine do
resolve(lambda do |obj, args, ctx|
new_scope = scope_proc.call(obj, args, ctx)
old_resolve.call(new_scope, args, ctx)
end)
end
end

private

def valid_value?(value)
inferred?(value) || proc?(value)
end

def proc?(value)
value.respond_to?(:call)
end

def inferred?(value)
value == :infer_scope
end
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ def test?
end
end

RSpec.describe GraphQL::Pundit::Instrumenter do
RSpec.describe GraphQL::Pundit::Instrumenters::Authorization do
let(:instrumenter) { GraphQL::Pundit::Instrumenter.new }
let(:instrumented_field) { instrumenter.instrument(nil, field) }
let(:fail_test) { Test.new(:fail) }
Expand Down
122 changes: 122 additions & 0 deletions spec/graphql-pundit/instrumenters/scope_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# frozen_string_literal: true

require 'spec_helper'

class ScopeTest
def initialize(value)
@value = value
end

def where(&block)
ScopeTest.new(@value.select(&block))
end

def to_a
@value
end
end

class ScopeTestPolicy
class Scope
attr_reader :scope

def initialize(_, scope)
@scope = scope
end

def resolve
scope.where { |e| e.to_i < 20 }
end
end

def initialize(_, _); end

def test?
nil
end
end

RSpec.describe GraphQL::Pundit::Instrumenters::Scope do
let(:instrumenter) { GraphQL::Pundit::Instrumenter.new }
let(:instrumented_field) { instrumenter.instrument(nil, field) }
let(:result) { instrumented_field.resolve(subject, {}, {}) }

subject { ScopeTest.new([1, 2, 3, 22, 48]) }

context 'without authorization' do
context 'inferred scope' do
let(:field) do
GraphQL::Field.define(type: 'String') do
name :notTest
scope
resolve ->(obj, _args, _ctx) { obj.to_a }
end
end

it 'filters the list' do
expect(result).to match_array([1, 2, 3])
end
end

context 'explicit scope' do
let(:field) do
GraphQL::Field.define(type: 'String') do
name :notTest
scope ->(scope, _args, _ctx) { scope.where { |e| e > 20 } }
resolve ->(obj, _args, _ctx) { obj.to_a }
end
end

it 'filters the list' do
expect(result).to match_array([22, 48])
end
end
end

context 'with authorization' do
context 'inferred scope' do
let(:field) do
GraphQL::Field.define(type: 'String') do
name :test
authorize
scope
resolve ->(obj, _args, _ctx) { obj.to_a }
end
end

it 'returns nil' do
expect(result).to eq(nil)
end
end

context 'explicit scope' do
let(:field) do
GraphQL::Field.define(type: 'String') do
name :test
authorize
scope ->(scope, _args, _ctx) { scope.where { |e| e > 20 } }
resolve ->(obj, _args, _ctx) { obj.to_a }
end
end

it 'returns nil' do
expect(result).to eq(nil)
end
end
end

context 'invalid scope argument' do
let(:field) do
GraphQL::Field.define(type: 'String') do
name :test
authorize
scope 'invalid value'
resolve ->(obj, _args, _ctx) { obj.to_a }
end
end

it 'raises an error' do
expect { result }.to raise_error(ArgumentError)
end
end
end

0 comments on commit 195039a

Please sign in to comment.