@auth directives don't apply to nested objects when using interfaces?

Say I have a schema something like this:

type Approval {
  id: ID!
  approvedAt: DateTime!
}

type Author @auth(
  query:  {
    rule: """
    query {
      queryAuthor(filter: { has: approval }) {
        id
      }
    }
    """
  }
) {
  id: ID!
  posts: [Post!] @hasInverse(field: author)
  approval: Approval
}

# Posts can only be viewed by author, for example
type Post @auth(
  query: {
    rule: """
    query ($USER: String!) {
      queryPost {
        author(filter: { id: { eq: $USER } }) {
          id
        }
      }
    }
    """
  }
) {
  id: ID!
  author: Author! @hasInverse(field: posts)
  text: String!
}

This is an extremely simplified version of what I’m actually working with, which also uses interfaces
& inherited auth rules, etc. So my question is about intended behavior.

If I make a post with an approved author, the @auth directive on Post works as expected if I query
as someone other than author:

query QueryPost {
  queryPost {
    id
  }
}

# returns { "post": [] }

But if I access the Post through Author:

query QueryPostThroughAuthor {
  queryAuthor {
    posts {
      text
    }
  }
}

the query returns

{
  "queryAuthor": {
    "posts": [{
      "text": "Myforbiddentext"
    }]
  }
}

showing the forbidden post! Is this intended? I assumed that auth rules would apply even for nested records. Does giving access to a record then give access to everything linked to it in the graph? Or am I maybe doing something wrong?

@amaster507 do you know if we support Auth in custom queries?

I believe you are going amiss with this logic. I am getting confused from your OP by the statement:

By through Author do you mean that the JWT changed or just the query with the same JWT?


I think this is the answer you are looking for though:


Not right now in custom queries but there is this RFC in progress:

I think my example was too complicated.

In short, if I have an @auth rule that forbids me access to Post, it works. But if I then query Author.posts, I can access the post. I would assume that each level of a nested query would need to respect auth rules, else we just can’t nest objects that have auth rules…

I can’t tell what the outcome of this thread was. Either way, I think in the thread you’re asking “How can I make it so I only see A if nested object B also exists / is authorized”, and I’m asking “Why does access to A implicitly grant access to nested object B”.

This is not true, and I have auth rules and examples in use to backup my statement that this is not true. If you can provide a schema/rules/data/token to prove your point then you have found a security bug and it needs to be fixed. In my schema I have:

type Contact {
  id: ID
  access: [ACL]
  notes: [Note]
}
type Note {
  id: ID
  access: [ACL]
  content: String
  forContact: Contact
}

I have auth rules on both Contact and Note that uses the (non-defined here) ACL type to process who has access to the data. I have users who have notes that they can access but not see the related contact and contacts that they can access but not see all of the nested notes.


Can you provide a small working setup that exemplifies the problem?


Example that this works as intended on v20.11.2-rc1-16-g4d041a3a

Schema
type A @auth(
  query: { rule: "query { queryA(filter: {isPublic: true}) { id } }" }
) {
  id: ID!
  isPublic: Boolean! @search
  name: String
  children: [B] @hasInverse(field: "parents")
}
type B @auth(
  query: { rule: "query { queryB(filter: {isPublic: true}) { id } }" }
) {
  id: ID!
  isPublic: Boolean! @search
  name: String
  parents: [A]
}

Mutation
mutation {
  addA(input: [
    { 
      name: "Foo",
      isPublic: true,
      children: [{
        isPublic: true,
        name: "Bar"
      },{
        isPublic: false,
        name: "Baz"
      }]
    },
    {
      isPublic: false,
      name: "Qux",
      children: [{
        isPublic: true,
        name: "Corge"
      }]
    }
  ]) { numUids }
}
Results
{
  "data": {
    "addA": {
      "numUids": 5
    }
  },
  "extensions": {
    "touched_uids": 28,
    "tracing": {
      "version": 1,
      "startTime": "2021-05-28T19:31:42.914002332Z",
      "endTime": "2021-05-28T19:31:42.917496912Z",
      "duration": 3494580,
      "execution": {
        "resolvers": [
          {
            "path": [
              "addA"
            ],
            "parentType": "Mutation",
            "fieldName": "addA",
            "returnType": "AddAPayload",
            "startOffset": 143838,
            "duration": 3334336,
            "dgraph": [
              {
                "label": "mutation",
                "startOffset": 221981,
                "duration": 1901138
              },
              {
                "label": "query",
                "startOffset": 2819439,
                "duration": 642045
              }
            ]
          }
        ]
      }
    }
  }
}

Query
query {
  queryA {
    id
    name
    children {
      id
      name
    }
  }
  queryB {
    id
    name
    parents {
      id
      name
    }
  }
}
Results
{
  "data": {
    "queryA": [
      {
        "id": "0x249f9",
        "name": "Foo",
        "children": [
          {
            "id": "0x249f8",
            "name": "Bar"
          }
        ]
      }
    ],
    "queryB": [
      {
        "id": "0x249f8",
        "name": "Bar",
        "parents": [
          {
            "id": "0x249f9",
            "name": "Foo"
          }
        ]
      },
      {
        "id": "0x249fb",
        "name": "Corge",
        "parents": []
      }
    ]
  },
  "extensions": {
    "touched_uids": 43,
    "tracing": {
      "version": 1,
      "startTime": "2021-05-28T19:33:10.622222559Z",
      "endTime": "2021-05-28T19:33:10.624695245Z",
      "duration": 2472707,
      "execution": {
        "resolvers": [
          {
            "path": [
              "queryA"
            ],
            "parentType": "Query",
            "fieldName": "queryA",
            "returnType": "[A]",
            "startOffset": 168882,
            "duration": 2241614,
            "dgraph": [
              {
                "label": "query",
                "startOffset": 258206,
                "duration": 2109198
              }
            ]
          },
          {
            "path": [
              "queryB"
            ],
            "parentType": "Query",
            "fieldName": "queryB",
            "returnType": "[B]",
            "startOffset": 151451,
            "duration": 2075325,
            "dgraph": [
              {
                "label": "query",
                "startOffset": 229186,
                "duration": 1952906
              }
            ]
          }
        ]
      }
    }
  }
}
2 Likes

Okay, thanks to your very helpful feedback I was able to reproduce. Turns out my example here was oversimplified. Turns out the issue is when interfaces nest one another.


N.B. In the schema below, the wonky auth rules for ApprovableChange just mean “you shouldn’t be able to see this”

Example that works on both standalone:v20.11.3 and standalone:latest as of 2021/05/28:


Schema
interface Approvable
  @auth(
    query: {
      rule: """
      query {
        queryApprovable(filter: { approved: true }) {
          id
        }
      }
      """
    }
  ) {
  id: ID!
  approved: Boolean! @search
  changes: [ApprovableChange!] @hasInverse(field: approvable)
}

interface ApprovableChange
  @auth(
    query: {
      rule: """
      query {
        queryApprovableChange(filter: { x: true, and: { x: false } }) {
          x
        }
      }
      """
    }
  ) {
  x: Boolean! @search
  approvable: Approvable! @hasInverse(field: changes)
}

type PostContent implements ApprovableChange {
  id: ID!
}

type Post implements Approvable {
  id: ID!
}


Data
<0x2> <dgraph.type> "Post"^^<xs:string> <0x0> .
<0x2> <dgraph.type> "Approvable"^^<xs:string> <0x0> .
<0x3> <dgraph.type> "ApprovableChange"^^<xs:string> <0x0> .
<0x3> <dgraph.type> "PostContent"^^<xs:string> <0x0> .
<0x2> <Approvable.changes> <0x3> <0x0> .
<0x3> <ApprovableChange.x> "true"^^<xs:boolean> <0x0> .
<0x2> <Approvable.approved> "true"^^<xs:boolean> <0x0> .
<0x3> <ApprovableChange.approvable> <0x2> <0x0> .

Query
query {
  queryPostContent {
    x
  }
  
  queryPost {
    id
    changes {
      x
    }
  }
}

Result
{
  "data": {
    "queryPostContent": [],
    "queryPost": [
      {
        "id": "0x2",
        "changes": [
          {
            "x": true
          }
        ]
      }
    ]
  },
  "extensions": {
    "touched_uids": 15
  }
}

Let me know if I’ve actually found something and I can file a more formal bug report.

Wanting to respect your time but also keep this moving. I’ll start a new thread tomorrow with my results.

Oh sorry I didn’t circle back on this. I will bookmark this for a review tomorrow.


Walking through this again and this keeps striking me as odd.

What is the purpose of checking that the same value is true and is false? That will never equate out to a true statement. Is that what you are trying to prove here? That this should never be seen but it is. My mind just keeps getting stuck on this and says that’s not right and I can’t think past it.


Did you try to insert this data as a GraphQL mutation instead of RDF triples? Also it looks like you are trying to use namespaces in the rdf which isn’t supported until v21.03 and you are using:

both standalone:v20.11.3 and standalone:latest as of 2021/05/28

latest should be v21.03 but the former might not handle the rdf syntax correctly, idk.


I disabled auth rules and added data using GraphQL Mutations:

Add a Post:

mutation {
  addPost(input: [{approved: true}]) {
    post {
      id
      approved
      changes {
        x
      }
    }
  }
}

Results:

{
  "data": {
    "addPost": {
      "post": [
        {
          "id": "0x2710a",
          "approved": true
        }
      ]
    }
  }
}

Add a PostContent:

mutation {
  addPostContent(input: [{
    x: true,
    approvable: {
      id: "0x2710a"
    }
  }]) {
    postContent {
      id
    }
  }
}

Results:

{
  "data": {
    "addPostContent": {
      "postContent": [
        {
          "id": "0x2710b"
        }
      ]
    }
  }
}

Run a query to see all data with nested back to parent from both queryPost and queryPostContent:

{
  queryPost {
    id
    approved
    changes {
      x
      approvable {
        id
        approved
      }
    }
  }
  queryPostContent {
    id
    x
    approvable {
      id
      approved
      changes {
        x
      }
    }
  }
}

Results:

{
  "data": {
    "queryPost": [
      {
        "id": "0x2710a",
        "approved": true,
        "changes": [
          {
            "x": true,
            "approvable": {
              "id": "0x2710a",
              "approved": true
            }
          }
        ]
      }
    ],
    "queryPostContent": [
      {
        "id": "0x2710b",
        "x": true,
        "approvable": {
          "id": "0x2710a",
          "approved": true,
          "changes": [
            {
              "x": true
            }
          ]
        }
      }
    ]
  }
}

Turn on the @auth rules and run the same query again and see results:

{
  "data": {
    "queryPost": [
      {
        "id": "0x2710a",
        "approved": true,
        "changes": [
          {
            "x": true,
            "approvable": {
              "id": "0x2710a",
              "approved": true
            }
          }
        ]
      }
    ],
    "queryPostContent": []
  }
}

Run the same query using the queryApprovable and queryApprovableChange at the interface level:

{
  queryApprovable {
    id
    approved
    changes {
      x
      approvable {
        id
        approved
      }
    }
  }
  queryApprovableChange {
    x
    approvable {
      id
      approved
      changes {
        x
      }
    }
  }
}

Results:

{
  "data": {
    "queryApprovable": [
      {
        "id": "0x2710a",
        "approved": true,
        "changes": [
          {
            "x": true,
            "approvable": {
              "id": "0x2710a",
              "approved": true
            }
          }
        ]
      }
    ],
    "queryApprovableChange": []
  }
}

Run the query rules themselves:

{
  queryApprovable(filter: { approved: true }) { id }
  queryApprovableChange(filter: { x: true, and: { x: false } }) { x }
}

Results:

{
  "data": {
    "queryApprovable": [
      {
        "id": "0x2710a"
      }
    ],
    "queryApprovableChange": []
  }
}

According to the rules, the queries are returning the wrong responses because we should never be able to see the node with x: true when the rules are enabled.

To clarify for anyone coming to this thread: @amaster507 did reproduce the bug and acknowledged that in a DM. The steps in @auth directives don't apply to nested objects when using interfaces? - #6 by elhil should be enough to reproduce. (I used GraphQL mutations to input the data but those rdfs should work)

@MichelDiz what do I need to do to have this accepted as a bug? It’s a critical security flaw that could keep us from putting dgraph into production. At minimum we’ll have to refactor a lot of code.

I can accept it to have someone looking closer. But don’t expect too much from it. We are at low resources and a really huge backlog.

Let me ping @hardik and accept this.

2 Likes