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

Add Validating Directives #583

Merged

Conversation

dackroyd
Copy link
Contributor

@dackroyd dackroyd commented Feb 18, 2023

Adding support for directives which are evaluated prior to executing any resolvers. This allows validation to be performed on the request and prevent it from executing any significant work by rejecting the request early.

The most obvious case for this is authorization: based on the requested fields, we can tell whether the request is valid given the current user, and reject the entire request. If that were applied at resolution time, the request would have partially resolved, only to return errors for the specific fields which are not authorized.

fixes: #570

internal/exec/resolvable/resolvable.go Show resolved Hide resolved
graphql_test.go Outdated
Variables: map[string]interface{}{"episode": "NEWHOPE"},
ExpectedResult: "null",
ExpectedErrors: []*gqlerrors.QueryError{
{Message: `rebel scum cannot request imperial units`, Locations: []gqlerrors.Location{{Line: 58, Column: 3}}, Path: []interface{}{"hero", "height"}},
Copy link
Member

Choose a reason for hiding this comment

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

Maybe

Suggested change
{Message: `rebel scum cannot request imperial units`, Locations: []gqlerrors.Location{{Line: 58, Column: 3}}, Path: []interface{}{"hero", "height"}},
{Message: `rebes cannot request imperial units`, Locations: []gqlerrors.Location{{Line: 58, Column: 3}}, Path: []interface{}{"hero", "height"}},

graphql_test.go Outdated
Unit string
})
if ok && v.Unit == "FOOT" {
return fmt.Errorf("rebel scum cannot request imperial units")
Copy link
Member

Choose a reason for hiding this comment

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

same

@@ -47,26 +47,32 @@ type Field struct {
HasContext bool
HasError bool
ArgsPacker *packer.StructPacker
DirectiveVisitors []directives.ResolverInterceptor
DirectiveVisitors *FieldDirectives
Copy link
Member

Choose a reason for hiding this comment

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

Technically speaking the type here FieldDirectives is a bit misleading because these are being applied to FIELD_DEFINITION. And I would like to add field directives too, which are applied on FIELD and are packed at query exec time.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will have to think on the naming here. Intent is that they are the directives applied to the schema field. In the resolvable package, the schema field is referred to as Field

internal/exec/resolvable/resolvable.go Show resolved Hide resolved
// For now this is the only valid directive function
if _, ok := v.(directives.ResolverInterceptor); !ok {
switch v.(type) {
default:
Copy link
Member

Choose a reason for hiding this comment

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

default last, please

internal/exec/resolvable/resolvable.go Show resolved Hide resolved
errs[i] = &errors.QueryError{
Err: err,
Message: err.Error(),
Locations: []errors.Location{f.field.Loc}, // TODO: is this the correct location?
Copy link
Member

Choose a reason for hiding this comment

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

LGTM

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I should’ve added more context in the comment here: I’m unclear whether the location should be where the field is defined in the schema definition, or where in the requested query the field is

Copy link
Member

Choose a reason for hiding this comment

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

We are validating the query.

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 is the location in the schema being applied here, rather than the query. I'm not sure that we have access to the other

internal/exec/validate.go Outdated Show resolved Hide resolved
@pavelnikolov pavelnikolov marked this pull request as ready for review February 20, 2023 23:09
@pavelnikolov
Copy link
Member

I resolved conflicts in this PR.

social.Schema,
"role: Role!",
`role: Role! @hasRole(role: "ADMIN")`,
),
Copy link
Member

Choose a reason for hiding this comment

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

(nitpick) You could have used schema extension here : )

I'll add examples how to do that soon.

switch sel := sel.(type) {
case *selected.SchemaField:
field, ok := fieldByAlias[sel.Alias]
if !ok { // validation already checked for conflict (TODO)
Copy link
Member

Choose a reason for hiding this comment

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

What does // validation already checked for conflict (TODO) mean in this context?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy-pasted from the execX equivalent, I believe would apply here as well, but not sure what the intended long-term resolution is

Copy link
Member

Choose a reason for hiding this comment

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

I guess we are checking that the alias doesn't exist as a field. This is fine I guess. I'll add a unit test to confirm that. Not related to your PR, though.

@pavelnikolov

This comment was marked as resolved.

@pavelnikolov pavelnikolov added this to the v1.6.0 milestone Feb 23, 2023
@pavelnikolov
Copy link
Member

After thinking a little bit more, directives should not be part of the resolver. Instead, they should indeed be passed as a schema option. The reason for that is the possibility to parse a schema without actually passing any resolver. This would allow us to inspect the schema and to call [Schema.AST] on it. In future we might have some directives on schema for example or on one of the types which would give some schema parsing instructions. Therefore, directives should not be tied to the resolver in order to make them future-proof.
I also created a separate issue for point 2. above, so this PR is ready to merge, IMO.

Adding support for directives which are evaluated prior to executing
any resolvers. This allows validation to be performed on the request
and prevent it from executing any significant work by rejecting the
request early.

The most obvious case for this is authorization: based on the requested
fields, we can tell whether the request is valid given the current
user, and reject the entire request. If that were applied at resolution
time, the request would have partially resolved, only to return errors
for the specific fields which are not authorized.
r.Errs = errs
out.Write([]byte("null"))
return
}
Copy link
Member

Choose a reason for hiding this comment

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

We can add a global short circuit here. If the schema doesn't have any validation directives, then this entire block can be skipped.

@pavelnikolov pavelnikolov merged commit d126bba into graph-gophers:master Feb 23, 2023
@dackroyd dackroyd deleted the add-validating-directives branch February 25, 2023 10:53
KNiepok pushed a commit to spacelift-io/graphql-go that referenced this pull request Feb 28, 2023
Adding support for directives which are evaluated prior to executing
any resolvers. This allows validation to be performed on the request
and prevent it from executing any significant work by rejecting the
request early.

The most obvious case for this is authorization: based on the requested
fields, we can tell whether the request is valid given the current
user, and reject the entire request. If that were applied at resolution
time, the request would have partially resolved, only to return errors
for the specific fields which are not authorized.
KNiepok pushed a commit to spacelift-io/graphql-go that referenced this pull request Feb 28, 2023
Adding support for directives which are evaluated prior to executing
any resolvers. This allows validation to be performed on the request
and prevent it from executing any significant work by rejecting the
request early.

The most obvious case for this is authorization: based on the requested
fields, we can tell whether the request is valid given the current
user, and reject the entire request. If that were applied at resolution
time, the request would have partially resolved, only to return errors
for the specific fields which are not authorized.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add validator directives
2 participants