Skip to content
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

Closed
wants to merge 2 commits into from

Conversation

sweir27
Copy link
Contributor

@sweir27 sweir27 commented Nov 15, 2017

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 an app and user were present. Instead, it allows the base introspection query to be public and adds more fine-grained authorization on a field-level.

  1. 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.

  2. graphql-ruby has a nice way of extending the DSL on the Field, Type, and Argument 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.

@sweir27 sweir27 self-assigned this Nov 15, 2017
current_user: current_user,
current_user_roles: current_user_roles
},
except: Util::PermissionBlacklist
Copy link
Contributor Author

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'
Copy link
Contributor Author

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.

Copy link
Contributor

@alloy alloy Nov 17, 2017

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
Copy link
Contributor Author

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...

Copy link
Contributor

@ashkan18 ashkan18 left a 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(',')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

niiiiice!

Copy link
Contributor

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.

Copy link
Contributor

@joeyAghion joeyAghion left a 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 users 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
Copy link
Contributor

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(',')
Copy link
Contributor

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.

@sweir27
Copy link
Contributor Author

sweir27 commented Nov 16, 2017

@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).

@orta
Copy link
Contributor

orta commented Nov 16, 2017

require_app true could be verify_app_token true ?

@orta orta mentioned this pull request Nov 16, 2017
1 task
@alloy
Copy link
Contributor

alloy commented Nov 16, 2017

This can be done on a role basis, and effectively hides certain fields from a given app/user.

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.

@joeyAghion
Copy link
Contributor

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 roles of the associated client when it's an unauthenticated token. This might work as-is for this purpose, or we might need to extend it to embed client roles alongside users' roles to expose schemas in authenticated cases as well.

@sweir27 sweir27 changed the title [Don't Merge] Permission blacklist example Make convection's schema public + add auth to specific fields Nov 17, 2017
@sweir27 sweir27 assigned joeyAghion and unassigned sweir27 Nov 17, 2017
@sweir27
Copy link
Contributor Author

sweir27 commented Nov 17, 2017

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 permit (or otherwise) behavior to look.

@sweir27 sweir27 force-pushed the permission-blacklist-example branch from d665a5b to aafd13d Compare November 17, 2017 16:15
@sweir27 sweir27 changed the title Make convection's schema public + add auth to specific fields Permission blacklist + field-level auth example Nov 17, 2017
@sweir27 sweir27 closed this Nov 17, 2017
@orta orta deleted the permission-blacklist-example branch November 17, 2017 16:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants