Auth for Interfaces

Motivation

Support Authorization rules on interfaces

User Impact

Users will now be able to use @auth rules on interfaces and the implementing types will inherit those rules.

Implementation

Auth Rules on Interfaces.

We plan to support @auth directive on interfaces.

@auth directive will work just like it works for types which are to provide authorization to perform query/update/delete on interfaces. The rules provided inside the @auth directive on an interface will also be applied as an AND rule to those on the implementing types.

type Author {
  id: ID!
  name: String! @search(by: [hash])
  posts: [Post] @hasInverse(field: author)
}

interface Post @auth(
    query: { rule: """
        query ($USER: String!) { 
            queryPost(filter: { author : { id: { eq: $USER } } } ) { 
                id 
            } 
        }"""
    }
){
  id: ID!
  text: String @search(by: [fulltext])
  datePublished: DateTime @search
  author: Author! 
}

type Question implements Post @auth(
    query: { rule: """
        query ($ANSWERED: Boolean!) { 
            queryQuestion(filter: { answered: $ANSWERED } ) { 
                id 
            } 
        }"""
    }
){
  answered: Boolean
}

type Answer implements Post @auth(
    query: { rule: """
        query ($USEFUL:Boolean!) { 
            queryAnswer(filter: { markedUseful: $USEFUL } ) { 
                id 
            } 
        }"""
    }
){
  markedUseful: Boolean
}

How auth works on the implementing types?

In the above example, the Question and Answer would automatically inherit the auth rules of the Post type. This would mean that a user can only query a subset of questions and answers that are accessible through the queryPost query and not anymore. We want to disallow the situation in which a user can query more posts through queryAnswer or queryQuestion than they can through queryPost.

So a user would only be able to query the questions that they are the author of (auth rule coming from Post) and which have the value of answered coming from $ANSWERED. A type would inherit the auth rules of all the interfaces that it implements and the final auth rules would be an AND of a types auth rules and of all the interfaces that it implements.

So in the generated schema the auth rules of Question would look like

type Question implements Post @auth(
    query: { and: [
        { rule: """
            query ($USER: String!) { 
                queryPost(filter: { id: { eq: $USER } } ) { 
                    id 
                } 
            }"""
        }
        {
            rule: """
            query ($ANSWERED: Boolean!) { 
                queryQuestion(filter: { answered: $ANSWERED } ) { 
                    id 
                } 
            }"""
        }]
    }
){
  id: ID!
  text: String @search(by: [fulltext])
  datePublished: DateTime @search
  author: Author! 
  answered: Boolean
}

If there were more interfaces that the Question type implements, then rules for those would also be added in an AND condition to the auth rules of the `Question type.

How auth on interfaces works with all of this?

When it comes to applying auth rules on interfaces themselves, there we’ll have to do something different. We’ll have to do a union query where we query all the implementing types and apply the auth rules on them. Then the final query would be an OR query joining the results from all the implementing types. So

{
  queryPost {
    id
    text
    datePublished
  }
}

would translate to a DQL query like the following

{

  # Note all the auth rules for answers are applied here (these include the auth rules for a Post as well)
  qa as queryAnswer(func: type(Answer)) {
  }

  # All the auth rules for a question are applied here (these include the auth rules for a Post as well)
  qq as queryQuestion(func: type(Question) {
  }

  queryPost(func: uid(qa, qq)) {
    id
    text
    datePublished
  }
}

The above is a simplified example but this would work with filters, ordering and pagination as well.

Mutations on the interface will also work in the same manner. For example, in case of delete mutation on an interface, It will be broken into the delete mutation on implementing types and the nodes which satisfy auth rules of a corresponding implementing type + interface will get deleted.

How would this look on the implementation part? An example above would help. Will there be some kind of @inheritAuth directive or some way in a rule to say inherit?

Hey @amaster507, thanks for your reply. Actually we are also figuring this out how to keep the inheritance of auth rules optional for implementing types. Soon we will update this with an example.

I don’t like these interpretations with @auth and @baseAuth, here’s why…

The auth rules define what you can see in the graph. If I define an @auth rule on an interface and that doesn’t apply to the implementing types, that means I can see things when I query through the types that I can’t see through the interface and that breaks the idea that an auth rule defines access to a node. For example, let’s assume a schema like

interface Post { ... }

type Question implements Post { ... }

type Answer implements Post { ... }

If I put an @auth on Post that would define a set of posts P that I could see, but if that rule doesn’t apply to the implementing types, then I can query set Q from Question and A from Answer and have A union Q be greater than P … i.e. I can see everything about the posts that the interface doesn’t let me see. What’s a valid use case where, for example, I can’t see a post’s id and text if I go query post, but I can see exactly that id and text if I go query question?

For me, there’s two issues here:

  • at the moment, you can’t put @auth on interfaces
  • at the moment, @auth from implementing types isn’t applied to interfaces.

I think the first, simplest iteration here is to

  • allow @auth on interfaces
  • when querying a type we and in any auth from the interfaces it implements
  • when querying an interface we split into N queries for each of the implementing types, apply auth to those, and union the result.
1 Like

Fair points, I have updated the RFC in accordance with this.

Looking at version as of 30/9 looks good to me :+1:

We have changed the design of how auth on interfaces will work for the implementing types and interfaces. Please have a look at the updated RFC.

The USER var is never used and the ANSWERED var is never define

Again the USER var is never used and the useful var is never defined.

Thanks for pointing out @amaster507, we have corrected it.

Is this still on track? It would reduce the loc of my schema drastically! :slight_smile:

Edit: Just found out that it is working in latest master! Great!

Edit2:

Could we allow empty interfaces? This would be very useful for easy access-control like e.g. ReadOnly Interface:

interface ReadOnly  @auth(
    query: { rule: "{$IS_LOGGED_IN: { eq: \"true\" }}" }
    add: { rule: "{$ROLE: { eq: \"ADMIN\" }}" }
    delete: { rule: "{$ROLE: { eq: \"ADMIN\" }}" }
    update: { rule: "{$ROLE: { eq: \"ADMIN\" }}" }
  ) {
# I don't want to add new fields here
}

type GuardedType implements ReadOnly {
 ... 
}

2 Likes

I believe an interface has to have at least one field, even if it is not utilized. Maybe something with a leading underscore to help distinguish it as a placeholder field only.

Yes, I also think that this is GQL spec. Do you know if the GQL spec is still developed or is it fixed?

It is still active:

1 Like