graphql-js
graphql-js copied to clipboard
Limit introspection capability programatically
Many of my types and many fields on some of my types shouldn't be exposed to some subsets of my users. Is there a way to limit visibility on introspection programmatically?
I have the same general question - how should one introduce authorization (different fields available to different users/roles)? I admit haven't gone through the spec with this in mind though, so I don't know if there's some consideration there. If you have just a few well-defined user-role topologies, then I could imagine actually having multiple type systems, and swapping based on who is making the request. Feels awkward and hacky though. Probably would need to extend the schema to have some sort of authorization function.
Here's my understanding of it: http://rmosolgo.github.io/blog/2015/08/04/authorization-in-graphql/
That's tough for introspection though, since it's mostly built-in!
@rmosolgo's approach is how I am securing particular queries/mutations/fields. In our case, we don't care that we might expose some authorized user only fields or mutations via introspection. Even if you can see it, you can't run it, so it doesn't really matter.
If I did want to hide that information, I would probably just wrap the object/interface constructors, add some metadata to each field, and just generate multiple schemas per user or per authentication level as necessary. I'm not really convinced this is an issue that the library or spec should try to solve though.
That being said, I wouldn't be opposed to some hooks in the GraphQLSchema to make something like this a little easier, like being able to preprocess types, instead of wrapping them manually.
Point taken re: scope of library/spec. Might try dynamically generating the type schema.
I don't see why this wouldn't be something the library handles. Limiting introspection will be a very common need. Also I'll be using scopes for authentication so could have literally 100s of thousands of possible schema combinations.
I really like how Hapi.js handles this. You can set a string or array of strings of scopes on routes that are matched against the user object. Very easy to setup and maintain.
@KyleAMathews No argument that this is a feature that a full server would need to implement (and that I would love to have in my hands right now). My rough understanding of this project though is to keep this as agnostic as possible, focusing on core GraphQL concepts while letting the community figure out some different ways to implement. The approach you suggest sounds very reasonable, but I'm thinking that deciding on a single methodology for handling authentication at the core might be quite the can of worms. Encouraging growth of an ecosystem without locking in too much is the stage we're at, I think.
Anyway, as to the scale of possible schema combinations you indicate, and assuming there's currently no baked-in solution on the near-horizon, do you think that full introspection of dynamically-generated type schemas could work in lieu of dynamically-limited introspection of a full type schema? I mean, if you could config it similar to Hapi routes. There's probably good opportunity here to write some preprocessors...
I'd dismissed the idea of dynamically generated schemas as my gut said that'd be expensive to do on every query but after thinking about it a bit more, that should actually work just fine—by far most of the work is in actually executing the query. Stringing together a few objects to create a schema should be very quick.
I noticed your repo @rdooh https://github.com/rdooh/graphql-gateway where you're working on something like this. Will keep an eye on it :)
This isn't something I need to work on right now—just using GraphQL for internal purposes—but will need to move on something in a month or two.
Perhaps I could just add the scope array onto types/fields as I suggested and the preprocessor would take a schema + scope(s) and then "prune" off types and fields that aren't supported. Would that work @leebyron?
Another possibility could be serving different schemas to different people, eg
var schema = user.isAdmin ? fullSchema : limitedSchema
Depends on how fine-grained the permission levels are though, that would become ridiculous pretty fast!
I believe FB just returns null for fields that you do not have permission to. Having multiple type systems with fine grained permissions will make code generation, relay, type checking, etc, from GraphQL schemas much harder.
@KyleAMathews why do you need different schemas for 100s of thousands of possible schema combinations? Is it really bad for users to see the full schema?
If you need secret admin only fields for internal use, I'd suggest exposing a different endpoint along the lines of what @rmosolgo suggests. But this seems like a much more limited use case.
@tristanz I'll be letting people define custom access tokens like 
so the number of combinations grows very fast.
It might not be that bad to expose the full schema in some cases but for general cleanliness reasons + also I don't want to have to think every time I add a new field if this is potentially exposing sensitive information to customers or to the public at large (some types will be public)
Right, but this shouldn't be a schema concern. For code generation, relay, and type checking to work, the schema can't rely on runtime attributes generating new schemas. The schema should just allow nulls. You can then have a middleware layer that uses these tokens to null specific fields and/or short-circuit the resolve logic.
Something like:
resolve: hasPermissions(['user_groups'], resolveFunc)
where hasPermissions returns a resolve function that nulls or short-circuits your real resolveFunc.
@tristanz, probably there are many cases where allowing users to see the full schema isn't a problem, even if their access will be limited to a subset upon making requests for queries/mutations. That said, sometimes projects have security requirements that are dictated by other stakeholders, and this would reduce the number of battles to fight, even if there is technically no security hole. It's probably not a core concern for this library, but I think there will be use cases for limiting introspection.
(Understand that I'm speculating here, based on imagining trying to rebuild some past projects.)
Dynamic Anyway??? If we ignore the introspection question for a moment, and just think about implementing authorization in general, it seems to me there are roughly two cases: 1) content filtering (e.g. comment list based on owner) and 2) structural filtering of terminal leaf-fields or even whole branches of the schema tree. Both could be handled by arbitrary code inside the resolve function. In fact, the first one probably has to be handled there. But the second one could be handled up front by pruning the full schema tree(s) down to some subspace. There would be some cost to this of course, but I think that a filter function could operate pretty quickly. In same cases things might balance out anyway - an over-reaching query/mutation request against the pruned schema would be rejected outright instead of running through the various (valid) field resolvers and being rejected at some particular point. One can imagine that if you have a request with a lot of sub-actions that can be run in parallel, you might want to avoid kicking them all off if you can catch it earlier. Particularly if there is the potential of having to roll back side effects of mutations.
Anyway, I'm starting to think that dynamic schema generation might make sense in some cases anyhow (provided the cost is low) - in which case the limited introspection question becomes a convenient side effect. Balance of pros/cons will depend on the application of course.
Note Another potential (small) win for limiting introspection by pruning might be that you can automate testing required data structures for a particular user class against the schema - monitoring whether proposed changes to permissions are going to impact particular use cases.
Might be going out of scope here - maybe limiting introspection etc is outside of Graph-JS's core mandate, but it's a problem worth addressing elsewhere...
@tristanz a concrete example of why securing introspection is needed.
Say you're releasing a top-secret new feature. You need to expose new fields and types to support product dev and live testing by a few beta customers. If you can't limit introspection then an aspiring reporter or one of your competitors could easily poke around your GraphQL instance and see what's happening.
Security for data matters a ton obviously but what we're saying is security for metadata matters just as much sometimes.
@tristanz btw, sense.io looks awesome :)
This is interesting discussion, thanks for raising this!
At Facebook our schema is static. More specifically we have two schema: an internal and an external schema. The internal schema includes prototype features we don't want to leak as well as site admin tools that we would rather not expose to our mobile clients.
A static schema is important as almost all of our client side tools assume the same schema at development time and runtime. That means your iOS or Relay engineer should see the same schema your users see. This gives us the ability to do code generation for model objects and fast parsers, and validate that queries written on the client are valid before the app is even run, ensuring quality.
Facebook is full of access control mechanisms though. There are user level permissions - like if you can admin a group, there are per content level permissions like privacy rules - and there are site wide limits like access to new features that roll out slowly.
For these, we ensure fields are nullable and return null when access to certain data is not allowed for whatever reason.
This is also nice as it does not differentiate between "there is no data" and "you cannot see this data" - which itself could be a security hole.
@KyleAMathews Absolutely. In that case I think the solution is multiple endpoints. Have a flag for your new secret release and return a different schema if that flag is present. This should not be the recommended approach for general authorization issues though, since it breaks higher level tools that need to point at a single schema.
re Sense: thanks :)
One way we might be able to resolve the core concerns here is to enable or disable introspection on a per-request basis.
The audience for introspection are dev tool builders and users. If you want to limit who can see this metadata to only your developers, that seems like a reasonable thing to want to do.
Thanks @leebyron and @tristanz for the food for thought.
What I'm hearing then about the vision for how a schema grows is that this is usually an append-only situation, where some branches and leaves may be dead/null for some or all users under varying conditions. Depending on the underlying reason, there may be metadata provided about context (e.g. deprecation), or not (ambiguity re: unauthorized vs not available). I'm still trying to figure out if, for the second case, complete omission by user class/context will play nicely.
@leebyron I see the nullable point re: empty vs unauthorized data... wouldn't "I don't/won't even recognize your request" be even more ambiguous in some respects?
I can also see that receiving a partial result (populated with strategic nulls) would be a more graceful way to handle run-time requests that overreach authorizations, rather than all-out rejection by a pruned schema. On the other hand, introspection of a pruned projection of the schema for a particular user class during dev seems like a workable way to detect overreaching queries early... an introspection check against both the global schema and a particular projection would identify whether a given broken query is globally invalid, or just invalid for the current user context. I don't use React (yet), so I'm probably missing a good chunk of the mental framework here, but if I wanted to write a client that can shape itself according to a users context, I might want to do some of this based on the shape of the available schema projection (as I'm apparently calling it) rather than a fully shaped data response with strategic but potentially ambiguous nulls.
Maybe what I'm trying to understand is whether you just consider dynamic schema generation impractical given your product and your development and iteration models, or if you also consider the impact on security to be either null or negative.
I'm probably overthinking it.
wouldn't "I don't/won't even recognize your request" be even more ambiguous in some respects?
Yeah, this is why we shy away from this strategy. Since GraphQL often requests lots of fields across many types, we want to ensure that a permissions issue by any particular end-user could result in the query failing to validate and thus not execute at all.
On the other hand, introspection of a pruned projection of the schema for a particular user class during dev seems like a workable way to detect overreaching queries early... an introspection check against both the global schema and a particular projection would identify whether a given broken query is globally invalid, or just invalid for the current user context.
This can become a huge burden if your permissions system becomes at all complex. You have a 2^N problem where any pair of permissions results in double the amount of validation to perform. On large codebases this could quickly enter the realm of the untenable.
Another source of permissions complexity which causes this approach to fall short is conditional permission. So it's not that a certain type of user cannot access a particular field at all, but they cannot access that particular field only under certain circumstances - like if that object has some flag set or not. Then the field-level permission is too coarse to apply and you again fall back to data-level permissions.
Maybe what I'm trying to understand is whether you just consider dynamic schema generation impractical given your product and your development and iteration models, or if you also consider the impact on security to be either null or negative.
Both of these things.
Dynamic schema generation causes us to either super-exponentially increase the time required to do static build-time query validation, or removes the ability to do so at all, and dramatically reduces the value of tools which rely on a static schema like a developer IDE environment that can provide smart typeahead and inline error detection.
For security, I think the impact is negative as field-level permissions are less powerful than data-level permissions. In fact data-level permissions can accomplish everything that a field-level permission can do. For instance, in the resolve function, you could just insert:
if (userFailsCheck(context.user)) {
return null;
}
Which should be operationally the same as that field being dynamically omitted from the schema.
Now - one point brought up here that I think is worth addressing is to protect against someone using introspection to crawl your application's schema and learning about features not yet exposed in your public product.
I think it is compelling to dynamically enable or disable introspection on a per-request basis, so that you could use request authentication information to determine this. So, for example, you could only allow introspection by admins of your application (e.g. your fellow employees), as these are the people you expect to actually derive value from this feature.
Thanks for taking the time to pull me out of the rabbit hole @leebyron. In addition to your many practical points, dynamic generation would also wind up duplicating some existing permissions work for underlying resources (if considering retrofitting existing brownfield projects), introducing yet another maintenance burden/minefield. I think there's some parallel unresolved discussion happening around Swagger.
In any case, I'm a re-convert to the single static schema, deferring data security concerns to resolve functions or underlying resources. Having the option to effectively make introspection for developers-only and prevent snooping is simple and to the point.
Enabling/disabling introspection seems like a pragmatic next-step.
The scenario where I see this not working is if you want to open up your GraphQL endpoint for users to build on similar to how many applications expose/document their REST api. In that case, less-privileged users would need to be able introspect but still not see experimental/beta GraphQL "APIs".
I guess it depends on whether this is framed as a question of differing internal vs external needs, or different classes of external needs. If you're supporting public introspection, but need your private/internal developer one to be different/enhanced, then I think you'd handle it as a work-around by maintaining two static schemas, as FB apparently does (maintainable as a secondary branch perhaps?). Assuming it is just two-tiered, and that both need to be live/in production, then I suppose you're also looking at two endpoints, as suggested earlier. This is workable for my own foreseeable needs, but if you start needing multiple levels on the public side, then it could get crazy again.
The point was made that ensuring (or assuming) that developers are seeing the same schema as end users keeps things from getting overly-complicated, enables better tooling, etc. But given that FB does in fact maintain two versions, I'm wondering how prototype tracking and release to from internal to external would best be managed. Having some sort of 'prototype' meta on an internal version might have similar benefits as 'deprecated' meta, allowing tooling to help identify things that are valid internally, but not externally. Here, I'm imagining that this could be a dynamic control on the schema that could even be imposed by environment settings, rather than authorization/security rules in the business domain.
So, maybe supporting this additional level of resolution between developer and public schemas could be entertained without getting into application-level security concerns.
The scenario where I see this not working is if you want to open up your GraphQL endpoint for users to build on similar to how many applications expose/document their REST api. In that case, less-privileged users would need to be able introspect but still not see experimental/beta GraphQL "APIs".
Most REST API that I've encountered use data-level permissions rather than field-level permissions. That is to say, when I visit a REST API documentation (the rough equivalent of introspection) they list out all of the available resources and parameters, but usually document which of these require a special authentication permission in order to access.
The translation of this to an equivalent GraphQL API would be an introspection (data-driven documentation) which presents all capabilities, but also describes which require special authentication permissions in order to access. This allows you to still have a single static schema, enabling usage of client tools.
If you find yourself truly in the circumstance where you are building a GraphQL server not for internal product usage, but exposed as a public API surface - and also need to actually limit visibility of certain portions of that schema based on access permissions, then I think the most tenable option you have is to develop multiple schema, each of which is used in the correct case.
I would advise against this if at all possible though, since each schema needs test coverage and by developing multiple schema or some type of dynamic schema, you're dramatically expanding the area of your software that requires test.
I believe that this is going to be a pretty rare case. GraphQL is really designed for end-to-end usage where you control both the server and the client - that is it's designed for internal use for building products. GraphQL is also pretty well suited for external usage as a public facing API, despite our having not invested much time exploring it at Facebook. However most public facing API do not contain secret features in the same way that internal products do, or if they do they typically have a much lower resolution - such as Facebook's two schema: The schema that our iOS/Android apps are exposed to, and the schema that we can access from our internal network to perform administration tasks.
Great stuff @leebyron!
I think two schemas, public and internal should cover everything I'm thinking actually as there's really only two high-level permissions for our systems, internal and user. Customers will be able to create tokens with restricted access abilities e.g. if they wanted to create a read-only token for pulling analytics data but that would always be for programmatic usages. And they'd do it in the context of some sort of API browsing tool like Facebook's API explorer https://developers.facebook.com/tools/explorer so the user would of course be able to introspect everything but the script/app wouldn't need to.
Also for new APIs/types/fields, using code names along with clearly designating them as restricted experimental APIs should be enough.
Great discussion! Thanks for everyone's input.
Great discussion.
Two questions.
From what I read, it was or is deemed untenable to have field level authorization within a GraphQL type system. For the system we want to build, that would be a total no-go. Is this for sure? Nulling inaccessible fields is fine as an answer for, "you can't see this". The fact the field is there shouldn't really be a problem (but only for features that are released of course). To me, the whole two tier system Facebook follows is the old tried and true "develop and master (or production)" branch scheme used in a lot of development workflows. It is cool, when the API can easily follow this development scheme too. Still, we really need field level access. If nulling is ok, would it be tenable.
And going a level higher, it is/ was also deemed untenable to have a dynamic schema. Though, I am not sure what is meant by dynamic schema. Because, since GraphQL is a standard, it should be possible to build types through some sort of code building automation, shouldn't it? I have a hard time imagining many devs hand coding type definitions in a larger system, especially where certain rules or policies need to be abided by (like user access edit: or Facebook's "tracked query"). What if a dev forgets to add the necessary code for a certain business rule or policy? Or does that kind of business logic have to be somewhere else, like in another layer behind the type system?
Or am I completely out in left field with my thinking?
Scott
Hi @smolinari - here's my definitely unauthoritative take..
Data-level vs Field-level
I think of GraphQL as a way to present underlying data in a graph-like manner regardless of how the underlying resources are actually persisted, etc. A GraphQL schema focuses on describing how they relate to one another - how they are connected conceptually. All of the responsibility for authorization, be it per type, or per field within a type, is delegated to the resolver functions and whatever lies behind them... so you could filter out/nullify restricted field data right in the resolver, or (better) pass authorization info on to the underlying service/data store and let it make final judgements about what information to reveal vs nullify. So you retain full field-level control of what data you return - even though the full schema is publicly available. @leebyron's last comment starts off by pointing out that this is essentially what you see in most REST API. The GraphQL schema is all about high-level organization and presentation of your data structures (types and fields), while deferring implementation details, including which data to return, to your custom code and underlying services... Sounds like this meets your needs.
Anyway, if your underlying services were secure enough before, they should continue to be so after putting GraphQL in front of them. If you're building from scratch, then you should continue to think about handling your business rules and policies as close to the data as possible.
Shaping Introspection?
Regarding a dynamic schema, my own original notion was that I didn't want to expose any more fields to a given request than needed - I wanted to do field-level authorization for presentation of the schema itself... so if 'address' wasn't available to a given user, then any introspection they tried would not reveal 'address' as a possible field. I wanted to limit people snooping around... that's where I started from. While I generally think that revealing as little information as possible is ideal, perhaps the optimal version is to 'reveal as little as is practical', given that you're also trying to support client-side developers. Trying to shape what parts of the schema are visible for a given user at a given point in time (as I was contemplating) appears to be the main ingredient in a recipe for over-engineering and probably much poorer performance. There's some discussion about strategies for if you really do need to manage a schema that varies, but I'm feeling convinced that it's not a problem that I desperately need solved.
Automation of Schema-Building
Given my understanding outlined above, I'm not sure I'd be too concerned about automation myself as I keep the logic in the schema to the barest minimum. My resolvers are little more than unsophisticated calls to existing services. I do it this way precisely because I don't want to be opening up the hood on the schema for every little bug fix or modification of business logic. As I think you allude to, it complicates the picture for devs by potentially requiring them to maintain complementary changes at both the schema level, and the underlying services - over-coupling, in other words. I think that in the 'ideal' GraphQL use case, you are only modifying the schema when you add new services/types or fields, or occasionally deprecating something... no business logic to speak of.
In this scenario, most of the code in your schema is just configuration, detailing how things are related - stuff that is largely outside of the scope of the individual underlying services (at least explicitly). I haven't thought about it much, but I suspect most automation would more or less just be some form of syntactic sugar - like Jade to HTML or CoffeeScript to JS, etc. - somewhere, you're still going to need someone to explicitly define how type X is related to type Y. This is in no way meant to suggest there isn't value here - for example, https://github.com/graphql/graphql-relay-js gives you library functions to help wire things up to be relay-ready.
Hope that helps - all my GraphQL experience so far is pretty light proof-of-concept trials right now, so I might not appreciate the problem!
Thanks rdooh. I think we are on the same wavelength for the most part and appreciate your post above a lot.
What I am working toward is a system, where the dev and the even the "business ops" can create any objects they need for an application with point and click functionality. This work, in turn, would also entail creating the relationships you mentioned and with that metadata, we could automate the building of the types.
Don't want to take this too off-topic. So, if anyone would like to chat about these kinds of possibilities some more, I have a Gitter room, where we can discuss it further. Just let me know and I'll invite you.
Scott
Interesting - have toyed with the idea of a gui for quickly mapping out relationships... not so much for maintaining an ongoing large app so much as quickly scaffolding out new ones. Invite would be appreciated.