Skip to content

Commit

Permalink
adds example of authorizing on a field-level
Browse files Browse the repository at this point in the history
  • Loading branch information
sweir27 committed Nov 15, 2017
1 parent 9ea7eb6 commit aafd13d
Show file tree
Hide file tree
Showing 13 changed files with 88 additions and 26 deletions.
1 change: 0 additions & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ gem 'gemini_upload-rails', gemini_gem_spec # for admins to upload images

gem 'graphql' # A lovely API
gem 'graphiql-rails' # A lovely interface to the API
gem 'graphql-guard' # Authorization helpers for graphQL

watt_gem_spec = { git: 'https://github.com/artsy/watt.git', branch: 'master' }
gem 'watt', watt_gem_spec # artsy bootstrap
Expand Down
3 changes: 0 additions & 3 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,6 @@ GEM
graphiql-rails (1.4.7)
rails
graphql (1.6.7)
graphql-guard (1.0.0)
graphql (>= 1.6.0, < 2)
haml (5.0.1)
temple (>= 0.8.0)
tilt
Expand Down Expand Up @@ -358,7 +356,6 @@ DEPENDENCIES
gemini_upload-rails!
graphiql-rails
graphql
graphql-guard
haml-rails
hyperclient
jquery-rails
Expand Down
12 changes: 8 additions & 4 deletions app/controllers/api/base_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ def set_submission
@submission = Submission.find(submission_id)
end

def set_current_user_roles
current_user_roles
end

def require_authentication
raise ApplicationController::NotAuthorized unless current_app && current_user
end
Expand All @@ -33,10 +37,6 @@ def require_authorized_submission
raise ApplicationController::NotAuthorized unless current_user && current_user == @submission.user_id
end

def require_app
raise ApplicationController::NotAuthorized unless current_app
end

private

# For now, require that signature is valid by verifying payload w/ secret.
Expand All @@ -55,5 +55,9 @@ def current_app
def current_user
@current_user ||= jwt_payload&.fetch('sub', nil)
end

def current_user_roles
@current_user_roles ||= jwt_payload&.fetch('roles', [])&.split(',')
end
end
end
5 changes: 3 additions & 2 deletions app/controllers/api/graphql_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ def execute
variables: params[:variables],
context: {
current_application: current_app,
current_user: current_user
current_user: current_user,
current_user_roles: current_user_roles
},
except: PermissionBlacklist
except: Util::PermissionBlacklist
)
render json: result, status: 200
end
Expand Down
4 changes: 4 additions & 0 deletions app/graph/mutations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ module Mutations
field :createSubmission, Types::SubmissionType do
description 'Create a submission'
argument :submission, Inputs::SubmissionInput::Create
permit ['user']

resolve ->(_obj, args, context) {
Submission.create!(args[:submission].to_h.merge(user_id: context[:current_user]))
}
Expand All @@ -13,6 +15,8 @@ module Mutations
field :updateSubmission, Types::SubmissionType do
description 'Create a submission'
argument :submission, Inputs::SubmissionInput::Update
permit ['user']

resolve ->(_obj, args, _context) {
submission = Submission.find_by(id: args[:submission][:id]) ||
raise(GraphQL::ExecutionError, 'Submission Not Found')
Expand Down
7 changes: 0 additions & 7 deletions app/graph/permission_blacklist.rb

This file was deleted.

4 changes: 3 additions & 1 deletion app/graph/root_schema.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
GraphQL::Field.accepts_definitions(permit: GraphQL::Define.assign_metadata_key(:permit))

RootSchema = GraphQL::Schema.define do
query Types::QueryType
mutation Mutations::Root

instrument(:field, Util::AuthorizationInstrumentation.new)
max_depth 5
end
1 change: 1 addition & 0 deletions app/graph/types/query_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ module Types
description 'Find Submissions'
argument :ids, types[types.ID]
argument :id, types.ID
permit ['admin']

resolve ->(_object, args, _context) {
args[:ids] ? Submission.where(id: args[:ids]) : [Submission.find(args[:id])]
Expand Down
37 changes: 37 additions & 0 deletions app/graph/util/authorization_instrumentation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
module Util
class AuthorizationInstrumentation
def instrument(_type, field)
if requires_authorization?(field)
old_resolve_proc = field.resolve_proc
new_resolve_proc = ->(obj, args, ctx) do
if can_access?(field, ctx)
resolved = old_resolve_proc.call(obj, args, ctx)
resolved
else
err = GraphQL::ExecutionError.new("Can't access #{field.name}")
ctx.add_error(err)
end
end

field.redefine do
resolve(new_resolve_proc)
end
else
field
end
end

def requires_authorization?(field)
field.metadata[:permit].present?
end

def can_access?(field, ctx)
if field.metadata[:permit]
return false unless ctx[:current_user_roles]
!(ctx[:current_user_roles] & field.metadata[:permit]).empty?
else
field
end
end
end
end
7 changes: 7 additions & 0 deletions app/graph/util/permission_blacklist.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module Util
class PermissionBlacklist
def self.call(schema_member, context)
return context[:current_user].blank? if schema_member.name == 'user_id'
end
end
end
7 changes: 5 additions & 2 deletions spec/requests/api/graphql/create_spec.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
require 'rails_helper'

describe 'Create Submission With Graphql' do
let(:jwt_token) { JWT.encode({ aud: 'gravity', sub: 'userid' }, Convection.config.jwt_secret) }
let(:jwt_token) { JWT.encode({ aud: 'gravity', sub: 'userid', roles: 'user' }, Convection.config.jwt_secret) }
let(:headers) { { 'Authorization' => "Bearer #{jwt_token}" } }

let(:create_mutation) do
Expand Down Expand Up @@ -31,7 +31,10 @@
post '/api/graphql', params: {
query: create_mutation
}, headers: { 'Authorization' => 'Bearer foo.bar.baz' }
expect(response.status).to eq 401
expect(response.status).to eq 200
body = JSON.parse(response.body)
expect(body['data']['createSubmission']).to eq nil
expect(body['errors'][0]['message']).to eq "Can't access createSubmission"
end

it 'rejects when missing artist_id' do
Expand Down
19 changes: 15 additions & 4 deletions spec/requests/api/graphql/query_spec.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
require 'rails_helper'

describe 'Query Submissions With Graphql' do
let(:jwt_token) { JWT.encode({ aud: 'gravity', sub: 'userid' }, Convection.config.jwt_secret) }
let(:headers) { { 'Authorization' => "Bearer #{jwt_token}" } }
let(:admin_jwt_token) { JWT.encode({ aud: 'gravity', sub: 'userid', roles: 'admin' }, Convection.config.jwt_secret) }
let(:user_jwt_token) { JWT.encode({ aud: 'gravity', sub: 'userid', roles: 'user' }, Convection.config.jwt_secret) }
let(:headers) { { 'Authorization' => "Bearer #{admin_jwt_token}" } }
let(:submission) { Fabricate(:submission, artist_id: 'abbas-kiarostami', title: 'rain') }
let!(:submission2) { Fabricate(:submission, artist_id: 'andy-warhol', title: 'no-rain') }
let(:asset) { Fabricate(:asset, submission: submission) }
Expand Down Expand Up @@ -34,7 +35,7 @@
post '/api/graphql', params: {
query: introspection_query
}
expect(JSON.parse(response.body)['data']['__type']['fields'].map{|f| f['name']}).to_not include('user_id')
expect(JSON.parse(response.body)['data']['__type']['fields'].map { |f| f['name'] }).to_not include('user_id')
expect(response.status).to eq 200
end

Expand All @@ -52,7 +53,7 @@
post '/api/graphql', params: {
query: introspection_query
}, headers: headers
expect(JSON.parse(response.body)['data']['__type']['fields'].map{|f| f['name']}).to include('user_id')
expect(JSON.parse(response.body)['data']['__type']['fields'].map { |f| f['name'] }).to include('user_id')
expect(response.status).to eq 200
end

Expand All @@ -64,5 +65,15 @@
body = JSON.parse(response.body)
expect(body['data']['submission'].count).to eq 2
end

it 'throws an error if a user tries to access' do
post '/api/graphql', params: {
query: query_submissions
}, headers: { 'Authorization' => "Bearer #{user_jwt_token}" }
expect(response.status).to eq 200
body = JSON.parse(response.body)
expect(body['data']['submission']).to eq nil
expect(body['errors'][0]['message']).to eq "Can't access submission"
end
end
end
7 changes: 5 additions & 2 deletions spec/requests/api/graphql/update_spec.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
require 'rails_helper'

describe 'Update Submission With Graphql' do
let(:jwt_token) { JWT.encode({ aud: 'gravity', sub: 'userid' }, Convection.config.jwt_secret) }
let(:jwt_token) { JWT.encode({ aud: 'gravity', sub: 'userid', roles: 'user' }, Convection.config.jwt_secret) }
let(:headers) { { 'Authorization' => "Bearer #{jwt_token}" } }
let(:submission) { Fabricate(:submission, artist_id: 'abbas-kiarostami', title: 'rain') }

Expand Down Expand Up @@ -33,7 +33,10 @@
post '/api/graphql', params: {
query: update_mutation
}, headers: { 'Authorization' => 'Bearer foo.bar.baz' }
expect(response.status).to eq 401
expect(response.status).to eq 200
body = JSON.parse(response.body)
expect(body['data']['updateSubmission']).to eq nil
expect(body['errors'][0]['message']).to eq "Can't access updateSubmission"
end

it 'errors for unkown submission id' do
Expand Down

0 comments on commit aafd13d

Please sign in to comment.