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

Neo4jError: Invalid input 'WHERE': expected when using auth in specific scenarios - 2 #5497

Closed
khoi-fish opened this issue Aug 24, 2024 · 5 comments · Fixed by #5507
Closed
Labels
bug Something isn't working confirmed Confirmed bug

Comments

@khoi-fish
Copy link

khoi-fish commented Aug 24, 2024

Type definitions

type User {
  id: ID!
  name: String
  cabinets: [Cabinet!]! @relationship(type: "HAS_CABINET", direction: OUT)
}

extend type User @authorization(
  validate: [
    { operations: [CREATE, DELETE], where: { jwt: { roles_INCLUDES: "admin" } } },
    { operations: [READ, UPDATE], where: { node: { id: "$jwt.sub" } } },
  ],
  filter: [
    {
      where: {
        node: {
          id: "$jwt.sub"
        }
      }
    }
  ]
)

type Cabinet {
  id: ID! @id
  name: String!
  folders: [Folder!]! @relationship(type: "HAS_FOLDER", direction: OUT)
  categories: [Category!]! @relationship(type: "HAS_CATEGORY", direction: OUT)
  user: User! @relationship(type: "HAS_CABINET", direction: IN)
}

extend type Cabinet @authorization(
  filter: [
    {
      where: {
        node: {
          user: {
            id: "$jwt.sub"
          }
        }
      }
    }
  ]
)

type Category {
  id: ID! @id
  name: String!
  files: [File!]! @relationship(type: "HAS_FILE", direction: OUT)
  folder: [Folder!]! @relationship(type: "HAS_CATEGORY", properties: "ColorType", direction: IN)
  cabinet: Cabinet! @relationship(type: "HAS_CATEGORY", direction: IN)
}

extend type Category @authorization(
  filter: [{ where: { node: { cabinet: { user: { id: "$jwt.sub" } } } } }]
)

type Folder {
  id: ID! @id
  name: String!
  categories: [Category!]!
    @relationship(
      type: "HAS_CATEGORY"
      properties: "ColorType"
      direction: OUT
    )
  files: [File!]! @relationship(type: "HAS_FILE", direction: OUT)
  cabinet: Cabinet! @relationship(type: "HAS_CABINET", direction: IN)
}

extend type Folder
  @authorization(filter: [{ where: { node: { cabinet: { user: { id: "$jwt.sub" } } } } }])

type ColorType @relationshipProperties {
  color: String
}


type File {
  id: ID! @unique
  name: String!
  category: Category @relationship(type: "HAS_FILE", direction: IN)
  folder: Folder! @relationship(type: "HAS_FILE", direction: IN)
}

extend type File @authorization(
  filter: [
    { 
      where: {
        node: {
          folder: {
            cabinet: {
              user: {
                id: "$jwt.sub"
              }
            }
          }
        }
      }
    }
  ]
)

(Note this can also be found under `src/types/index.ts`

Test data

I've created an AuraDB instance with a throwaway account. Credentials are provided in the repo under Settings -> Secrets and variables -> Actions -> "Variables" tab

Steps to reproduce

https://github.com/khoi-fish/neo4j-graphql-where-bug

How to run dev mode

  1. Create a .env file with the following contents

    ENVIRONMENT=local
    NEO4J_URL=****
    NEO4J_USER=****
    NEO4J_PASS=****
    

    All variables can be found under this project's Settings -> Secrets and variables -> Actions -> "Variables" tab

  2. Be on Node 20

  3. Run npm install

  4. Run npm run dev

    a. (optional) Run npm run dev:debug to get Cypher output

Reproducing the error

  1. Go to localhost:4000/graphql
  2. Run the following mutation:
    mutation UpdateFileCategory($fileId: ID!, $newCategoryId: ID) {
      updateFiles(
        where: { id: $fileId }
        disconnect: {
          category: { where: { node: { NOT: { id: $newCategoryId } } } }
        }
        connect: { category: { where: { node: { id: $newCategoryId } } } }
      ) {
        info {
          relationshipsDeleted
          relationshipsCreated
        }
      }
    }
    // Variables
    {
        "fileId": "file-1",
        "newCategoryId": "category-image"
    }
  3. The response should return an error "Invalid input 'WHERE': expected 'FOREACH', 'CALL' ..."

What happened

Basically the same as #5023. Seems like the original fix didn't catch all cases.

Expected behaviour

Disconnecting category from file should not error out

Version

"@neo4j/graphql": "^5.6.0",

Database version

AuraDB Neo4J Version 5

Relevant log output

MATCH (this:File)
CALL {
    WITH this
    MATCH (this)<-[:HAS_FILE]-(this0:Folder)
    CALL {
        WITH this0
        MATCH (this0)<-[:HAS_CABINET]-(this1:Cabinet)
        OPTIONAL MATCH (this1)<-[:HAS_CABINET]-(this2:User)
        WITH *, count(this2) AS userCount
        WITH *
        WHERE (userCount <> 0 AND ($jwt.sub IS NOT NULL AND this2.id = $jwt.sub))
        RETURN count(this1) = 1 AS var3
    }
    WITH *
    WHERE var3 = true
    RETURN count(this0) = 1 AS var4
}
WITH *
WHERE (this.id = $param1 AND ($isAuthenticated = true AND var4 = true))
WITH this
CALL {
WITH this
OPTIONAL MATCH (this)<-[this_disconnect_category0_rel:HAS_FILE]-(this_disconnect_category0:Category)
CALL {
    WITH this
    MATCH (this)<-[:HAS_FILE]-(authorization__before_this2:Folder)
    CALL {
        WITH authorization__before_this2
        MATCH (authorization__before_this2)<-[:HAS_CABINET]-(authorization__before_this3:Cabinet)
        OPTIONAL MATCH (authorization__before_this3)<-[:HAS_CABINET]-(authorization__before_this4:User)
        WITH *, count(authorization__before_this4) AS userCount
        WITH *
        WHERE (userCount <> 0 AND ($jwt.sub IS NOT NULL AND authorization__before_this4.id = $jwt.sub))
        RETURN count(authorization__before_this3) = 1 AS authorization__before_var5
    }
    WITH *
    WHERE authorization__before_var5 = true
    RETURN count(authorization__before_this2) = 1 AS authorization__before_var0
}
CALL {
    WITH this_disconnect_category0
    MATCH (this_disconnect_category0)<-[:HAS_CATEGORY]-(authorization__before_this6:Cabinet)
    OPTIONAL MATCH (authorization__before_this6)<-[:HAS_CABINET]-(authorization__before_this7:User)
    WITH *, count(authorization__before_this7) AS userCount
    WITH *
    WHERE (userCount <> 0 AND ($jwt.sub IS NOT NULL AND authorization__before_this7.id = $jwt.sub))
    RETURN count(authorization__before_this6) = 1 AS authorization__before_var1
}
WHERE NOT (this_disconnect_category0.id = $updateFiles_args_disconnect_category_where_Category_this_disconnect_category0param0) AND (($isAuthenticated = true AND authorization__before_var0 = true) AND ($isAuthenticated = true AND authorization__before_var1 = true))
CALL {
	WITH this_disconnect_category0, this_disconnect_category0_rel, this
	WITH collect(this_disconnect_category0) as this_disconnect_category0, this_disconnect_category0_rel, this
	UNWIND this_disconnect_category0 as x
	DELETE this_disconnect_category0_rel
}
RETURN count(*) AS disconnect_this_disconnect_category_Category
}
WITH *
CALL {
	WITH this
	OPTIONAL MATCH (this_connect_category0_node:Category)
CALL {
    WITH this_connect_category0_node
    MATCH (this_connect_category0_node)<-[:HAS_CATEGORY]-(authorization__before_this2:Cabinet)
    OPTIONAL MATCH (authorization__before_this2)<-[:HAS_CABINET]-(authorization__before_this3:User)
    WITH *, count(authorization__before_this3) AS userCount
    WITH *
    WHERE (userCount <> 0 AND ($jwt.sub IS NOT NULL AND authorization__before_this3.id = $jwt.sub))
    RETURN count(authorization__before_this2) = 1 AS authorization__before_var0
}
CALL {
    WITH this
    MATCH (this)<-[:HAS_FILE]-(authorization__before_this4:Folder)
    CALL {
        WITH authorization__before_this4
        MATCH (authorization__before_this4)<-[:HAS_CABINET]-(authorization__before_this5:Cabinet)
        OPTIONAL MATCH (authorization__before_this5)<-[:HAS_CABINET]-(authorization__before_this6:User)
        WITH *, count(authorization__before_this6) AS userCount
        WITH *
        WHERE (userCount <> 0 AND ($jwt.sub IS NOT NULL AND authorization__before_this6.id = $jwt.sub))
        RETURN count(authorization__before_this5) = 1 AS authorization__before_var7
    }
    WITH *
    WHERE authorization__before_var7 = true
    RETURN count(authorization__before_this4) = 1 AS authorization__before_var1
}
WITH *
	WHERE this_connect_category0_node.id = $this_connect_category0_node_param0 AND (($isAuthenticated = true AND authorization__before_var0 = true) AND ($isAuthenticated = true AND authorization__before_var1 = true))
	CALL {
		WITH *
		WITH collect(this_connect_category0_node) as connectedNodes, collect(this) as parentNodes
		CALL {
			WITH connectedNodes, parentNodes
			UNWIND parentNodes as this
			UNWIND connectedNodes as this_connect_category0_node
			MERGE (this)<-[:HAS_FILE]-(this_connect_category0_node)
		}
	}
WITH this, this_connect_category0_node
	RETURN count(*) AS connect_this_connect_category_Category0
}
WITH *
WITH *
CALL {
	WITH this
	MATCH (this)<-[this_category_Category_unique:HAS_FILE]-(:Category)
	WITH count(this_category_Category_unique) as c
	WHERE apoc.util.validatePredicate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDFile.category must be less than or equal to one', [0])
	RETURN c AS this_category_Category_unique_ignored
}
CALL {
	WITH this
	MATCH (this)<-[this_folder_Folder_unique:HAS_FILE]-(:Folder)
	WITH count(this_folder_Folder_unique) as c
	WHERE apoc.util.validatePredicate(NOT (c = 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDFile.folder required exactly once', [0])
	RETURN c AS this_folder_Folder_unique_ignored
}
RETURN "Query cannot conclude with CALL" +0ms
cypher params: {
  jwt: {
    iat: 1724442925,
    exp: 9724445955,
    sub: '4252a2b0-9492-4bef-8a94-fa20c97e7f71',
    tId: 'public',
    sessionHandle: 'a9021a8e-0d39-4639-8cb4-276e1724ba69',
    'st-ev': { v: true, t: 1724442925149 },
    'st-role': { v: [Array], t: 1724172602771 },
    'st-perm': { v: [], t: 1724172602871 }
  },
  param1: 'file-1',
  isAuthenticated: true,
  updateFiles_args_disconnect_category_where_Category_this_disconnect_category0param0: 'category-image',
  this_connect_category0_node_param0: 'category-image',
  updateFiles: { args: { disconnect: [Object] } },
  resolvedCallbacks: {}
}
@khoi-fish khoi-fish added the bug Something isn't working label Aug 24, 2024
@neo4j-team-graphql
Copy link
Collaborator

Many thanks for raising this bug report @khoi-fish. 🐛 We will now attempt to reproduce the bug based on the steps you have provided.

Please ensure that you've provided the necessary information for a minimal reproduction, including but not limited to:

  • Type definitions
  • Resolvers
  • Query and/or Mutation (or multiple) needed to reproduce

If you have a support agreement with Neo4j, please link this GitHub issue to a new or existing Zendesk ticket.

Thanks again! 🙏

@khoi-fish
Copy link
Author

khoi-fish commented Aug 24, 2024

Repo is here: https://github.com/khoi-fish/neo4j-graphql-where-bug

Didn't want to deal with secrets so the DB credentials are just plain text variables in the repo settings.

@mjfwebb I've added you as a collaborator. I can add others as needed.

If anyone has any issues getting things up and running, please let me know.

@mjfwebb mjfwebb added the confirmed Confirmed bug label Aug 26, 2024
@neo4j-team-graphql
Copy link
Collaborator

We've been able to confirm this bug using the steps to reproduce that you provided - many thanks @khoi-fish! 🙏 We will now prioritise the bug and address it appropriately.

@mjfwebb
Copy link
Contributor

mjfwebb commented Aug 29, 2024

Minimal reproduction for testing:

No data required.

Type definitions:

type JWT @jwt {
    roles: [String!]!
}

type User
    @authorization(
        validate: [
            { operations: [CREATE, DELETE], where: { jwt: { roles_INCLUDES: "admin" } } }
            { operations: [READ, UPDATE], where: { node: { id: "$jwt.sub" } } }
        ]
        filter: [{ where: { node: { id: "$jwt.sub" } } }]
    ) {
    id: ID!
    cabinets: [Cabinet!]! @relationship(type: "HAS_CABINET", direction: OUT)
}

type Cabinet @authorization(filter: [{ where: { node: { user: { id: "$jwt.sub" } } } }]) {
    id: ID! @id
    categories: [Category!]! @relationship(type: "HAS_CATEGORY", direction: OUT)
    user: User! @relationship(type: "HAS_CABINET", direction: IN)
}

type Category @authorization(filter: [{ where: { node: { cabinet: { user: { id: "$jwt.sub" } } } } }]) {
    id: ID! @id
    files: [File!]! @relationship(type: "HAS_FILE", direction: OUT)
    cabinet: Cabinet! @relationship(type: "HAS_CATEGORY", direction: IN)
}

type File {
    id: ID! @unique
    category: Category @relationship(type: "HAS_FILE", direction: IN)
}

Mutation:

mutation ($fileId: ID!, $newCategoryId: ID) {
  updateFiles(
    where: { id: $fileId }
    disconnect: {
      category: { where: { node: { NOT: { id: $newCategoryId } } } }
    }
    connect: { category: { where: { node: { id: $newCategoryId } } } }
  ) {
    info {
      relationshipsDeleted
      relationshipsCreated
    }
  }
}

@angrykoala angrykoala mentioned this issue Aug 29, 2024
@angrykoala angrykoala linked a pull request Aug 29, 2024 that will close this issue
@khoi-fish
Copy link
Author

Hey team. I pulled in @angrykoala 's patch into my local env and confirmed that the issue has been fixed. Thanks all 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working confirmed Confirmed bug
Projects
Status: Confirmed
Development

Successfully merging a pull request may close this issue.

3 participants