-
Notifications
You must be signed in to change notification settings - Fork 9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Permission blacklist + field-level auth example #122
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -51,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(',') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. niiiiice! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you need |
||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,15 @@ | ||
module Api | ||
class GraphqlController < BaseController | ||
before_action :require_authentication | ||
|
||
def execute | ||
result = RootSchema.execute( | ||
params[:query], | ||
variables: params[:variables], | ||
context: { | ||
current_application: current_app, | ||
current_user: current_user | ||
} | ||
current_user: current_user, | ||
current_user_roles: current_user_roles | ||
}, | ||
except: Util::PermissionBlacklist | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This line will whitelist/blacklist fields on a schema-level (so they can be hidden even from the introspection query). |
||
) | ||
render json: result, status: 200 | ||
end | ||
|
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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
module Util | ||
class AuthorizationInstrumentation | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This also feels like the kind of thing that could be shared across all of our services? Seeing as we are getting all of the same roles from gravity... |
||
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 |
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' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just an example-- this will hide the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I realise this is simply meant as a contrived example of the possibilities, in the same contrived spirit I’d use it as an example of a case where I don’t believe the field needs to be hidden from a privileged schema, the service should simply not return any data for it. (Maybe the service should include a 401-esque error in the response, I think that’s what @ashkan18 and @joeyAghion settled on). |
||
end | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused?
It doesn't appear to do anything beyond the existing method.