-
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
Conversation
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 comment
The 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).
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 comment
The reason will be displayed to describe this comment to others. Learn more.
Just an example-- this will hide the user_id
field if there is no user in the jwt.
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.
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).
@@ -0,0 +1,37 @@ | |||
module Util | |||
class AuthorizationInstrumentation |
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.
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...
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.
Looks great! one question i have is probably in general about how things in GraphQL works and my lack of knowledge. Serving schema is mainly helpful during development and possibly schema stitching, right? From a normal service to service call the actual schema interpolation is not that useful, right?
@@ -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 comment
The 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 comment
The reason will be displayed to describe this comment to others. Learn more.
I think you need ''
instead of []
, because otherwise split
might be called on an array.
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.
Interesting illustration.
To confirm, allowing user
s to access a schema property is essentially making it public (because anyone can sign up to be a user), right?
@@ -25,6 +25,10 @@ def set_submission | |||
@submission = Submission.find(submission_id) | |||
end | |||
|
|||
def set_current_user_roles | |||
current_user_roles | |||
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.
@@ -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 comment
The reason will be displayed to describe this comment to others. Learn more.
I think you need ''
instead of []
, because otherwise split
might be called on an array.
@joeyAghion yeah, it probably should verify that you also have an app token (not sure what the best wording on that would be). Maybe? require_app true
permit ['admin'] I was struggling a bit on naming and whether or not this should be a single or multiple concepts (app-level and user-level authorization). |
|
How many roles could there be? Is there a set amount of roles that are used throughout all our systems and have the same semantics? Stitching together many permutations (or per request on-demand) would probably not be great for performance. And does it actually matter that our internal systems can know about the full schema? (Because you do need an app token, so only our internal system could query those services, right?) My naive thought was that it doesn’t really matter, as long as the service won’t return values when querying for those fields without proper authorization. |
It's hard to really imagine this because we agree that this particular case doesn't actually demand protecting the schema, and we probably should avoid that overhead wherever possible. Generally speaking, fields that are relevant to administering content could be fine to expose, especially as that administration might come from partners and not exclusively from Artsy. That said, I can imagine wanting to keep certain areas internal to Artsy. In that case, I think we'd probably expose them to selective clients (rather than users). Every token identifies its associated client (and user, when authenticated). Both have roles, but currently we only return the |
Thank you all for bearing with this confusing prototype. The PR is now in a (mergeable, pending comments) state. This makes convection's schema public (as there is currently no need for masking, although it's possible), and adds authorization on a field-level. I anticipate this logic being added to/changing with @orta 's coming PRs, the main point of this is to nail down how we want the |
d665a5b
to
aafd13d
Compare
This PR lays out a few concepts-- I'll explain and then clean up before it's mergeable.
First thing, it removes the blanket authorization check on the
/graphql
endpoint, which used to require that anapp
anduser
were present. Instead, it allows the base introspection query to be public and adds more fine-grained authorization on a field-level.PermissionBlacklist
(which I will remove before merging) is an example of how we can use schema masking to blacklist (or whitelist) fields from being seen. This can be done on a role basis, and effectively hides certain fields from a given app/user. Right now I think convection's schema can be totally public, but it's good to keep in mind as we start using this pattern for more sensitive apps/fields.graphql-ruby has a nice way of extending the DSL on the
Field
,Type
, andArgument
classes. I added an example here (which I'll explain inline). This part I do intend to merge in, as it provides a nice way for us to define which roles can access various fields. Again, if something is intended to be hidden completely, we can blacklist it at the root level.