This is more for documentation for myself and my companions, but I think it can add value to others.
TL;DR; Auth Directive queries are processed before applying any other rules. You can not simply check if you already have access to one item and use it to provide access to related items
When writing @auth
directives I have tended to think on basis of queries to what the user can already see (as far as deep data). This was actually a flawed concept.
Consider the following schema:
type Contact @auth(
query: { rule: "query { queryContact(filter: { isDeleted: false }) { id } }" }
) {
id: ID!
name: String
isDeleted: Boolean @search
hasEmails: [Email] @hasInverse(field: forContact)
}
type Email @auth(
query: { rule: "query { queryEmail(filter: { isDeleted: false }) { id } }" }
) {
id: ID!
isDeleted: Boolean @search
forContact: Contact
}
One would think that this would hide all Contacts and Emails that are deleted. And while that is partially right, that is also partially wrong. Think about what you would want to happen if a Contact is deleted, you would want the email to be deleted too. But this query would still return emails for deleted Contacts but not the contact itself:
query {
queryEmail {
email
forContact {
name
}
}
}
Now to make this a tad bit more complicatedā¦
If we made the Email.forContact
field required (ie: forContact: Contact!
), meaning that we donāt want to allow any emails to be entered without an attached contact, now we would get the dreaded GraphQL error that a non nullable field was returned null
The way to fix this requires an extra step in the auth rule:
query {
queryEmail(filter: { isDeleted: false }) {
forContact(filter: { isDeleted: false }) {
id
}
}
}
And now the reason for the title of this post. Notice that we have to add the isDeleted
filter on the forContact predicate event though that is already a rule on the Contact
type.
Auth Directive queries are processed before applying any other rules. You can not simply check if you already have access to one item and use it to provide access to related items like this:
# does no different from the original auth directive rule as it sees ALL contacts
query {
queryEmail(filter: { isDeleted: false }) {
forContact {
id
}
}
}
I donāt foresee this changing because the performance would greatly be impacted and it would be a breaking change and may not be the desired outcome in all circumstances. In some situations it might be helpful to base auth directives on data that is not visible to the end users, and it could be done with this method.
To the devs: Circumventing Errors caused by @auth dreictives and required predicates in the schema?
Would it be possible in a future release to do some internal cascading of required predicates to prevent auth directives from causing errors? This would also entail having parameterized cascade directives.
If a predicate in the schema is required then could it automatically be cascaded at that level if requrested. So in the illustration above with the original rulesā¦
{ queryEmail { email } }
would return all email addresses, while
{ queryEmail { email forContact { id } } }
would return only email addresses for contacts that are not deleted without throwing the error:
Non-nullable field 'forContact' (type Contact!) was not present in result from Dgraph. GraphQL error propagation triggered.