-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Query-level schema restrictions #300
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
Conversation
Love that you're thinking about this query-based use-case. @xuorig and I were talking about this last week for feature-flagged fields/types. Imagine you want to give a specific API client early-access to some feature being built. There is far too many possible permutations to pre-calculate all possible schemas in advance. Instead you might be able to have a query-analyzer (or a static validator) to prevent unauthorized clients from resolving the feature-flagged fields. You'd also want some way to hook into the introspection schema to omit the feature-flagged fields/types when unauthorized client requests schema. |
We've discussed this approach internally as well and become concerned that you'd need resolved objects to possibly check feature flags against which you couldn't do in a query analyzer. I like the idea of the masks quite a bit but, as @cjoudrey pointed out, eventually there would be some crazy multiple of permutations. Hmm. |
@kdaigle I think you could achieve this with a query analyzer since you have access to the Suppose you added a def call(memo, visit_type, irep_node)
if irep_node.ast_node.is_a?(GraphQL::Language::Nodes::Field) && visit_type == :enter
irep_node.definitions.each do |type_defn, field_defn|
if field_defn.required_feature_flag && memo[:context].has_feature_flag?(field_defn.required_feature_flag)
# raise GraphQL::ExecutionError or AnalysisError or context.errors.add
end
end
end
end Edit: You'd have to check for fragments too. |
Yeah, that'd work assuming you're keying off of something that's always in |
An analyzer might work, depending on your privacy requirements. For example, if a certain enum value is only visible to some users, you couldn't remove it from an introspection result with an analyzer alone. That would tell GraphiQL users that a certain value is valid for them ... even though it's not 😿 Even if an analyzer-based solution did work, it would be bummer to basically reimplement some gem internals (validation, introspection at least) in user code. That's why I want to provide this in the library. It's a really nice feature for large-scale GraphQL schemas and the best implementation is ... right at the heart of the library :) I pushed a hot take on query-level masking: 2260b7e It seems promising! |
Correct, you need both. The analyzer was simply to prevent people from using the hidden things. The introspection resolvers still need to be modified in order to hide the things in the first place.
💯
👍 |
@@ -10,6 +10,11 @@ def types | |||
GraphQL::Define::TypeDefiner.instance | |||
end | |||
|
|||
# TODO: do I actually want to add this? |
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.
Could you think of other use cases where metadata
can be helpful in the future?
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.
It's definitely useful -- it's the basis for things like field_defn.required_feature_flag
in your example above (that is, arbitrary data attached to schema members).
Right now it's only supported via
GraphQL::BaseType.accepts_definitions(
required_feature_flag: GraphQL::Define.assign_metadata_key(:required_feature_flag)
)
and then
MyType = GraphQL::ObjectType.define do
required_feature_flag :top_secret_access
end
But with this you could "just" do
MyType = GraphQL::ObjectType.define do
metadata :required_feature_flag, :top_secret_access
end
I added it because I wanted to mess with metadata
without extending every schema in the test suite! Maybe it would be useful IRL to have a helper like that too.
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.
But, it's more prone to typos, that's a bit sad
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 asked because I'm always a bit wary with exposing key/value stores like that. lol
it's more prone to typos, that's a bit sad
Right!
MyType = GraphQL::ObjectType.define do
required_feature_flag :top_secret_access
end
Letting people expose this on their own via accepts_definitions
felt less prone to errors (and typos). It also means we don't need to expose metadata
in the define
block.
I could be wrong though. :)
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 moved it to spec_helper.rb
so it's there for testing but not default for the gem :)
You can see the API I sketched out here:
How does that look to you? I was considering between these two: # 1) Yield the schema member
Schema::Mask.new { |member| ... }
# 2) Yield the schema member and the query context
Schema::Mask.new { |member, ctx| ... } I opted for You can accomplish the equivalent of @current_user = user_from_session
# Access outer scope in the block
mask = Schema::Mask.new { |member| @current_user.can_see?(member) }
# Or pass it into another object
mask = CustomUserBasedMask.new(@current_user)
Schema.execute(query_string, mask: mask) |
Personally I didn't notice at first that the result of the provided block is negated in One alternative would be to use bare top_secret = ->(schema_member) { schema_member.metadata[:top_secret] }
Schema.execute(query_string, except: top_secret) Just an idea. |
👍 for |
Oops, realized I missed a spot with variables (and fixed it in 7b91e3f), where it was still possible to pass in hidden enum values or input object fields. If anyone gets a chance to think through any other "leaks", I'd love to hear the outcome of that! |
I added some docs and lots of caching on
To be honest, it's not even clear that the caching improved the situation, so maybe I should remove it:
|
I'm gonna push without caching since it doesn't seem to be much of a win, and make up the time someplace else. |
🎉! |
Here's a take on #269
For a given query, make a schema appear smaller than it really is. This way, you can restrict access (even visibility) to certain types, fields, arguments or enum values within a schema.
In fact, the only requirement of a "mask" is that it responds to
#call(member)
, so you can write your own, too!