neo4j / graphql

A GraphQL to Cypher query execution layer for Neo4j and JavaScript GraphQL implementations.
https://neo4j.com/docs/graphql-manual/current/
Apache License 2.0
511 stars 150 forks source link

authorization directive validate rule generates invalid Cypher query #5635

Closed daveajrussell closed 1 month ago

daveajrussell commented 1 month ago

Describe the bug When utilising the validate rule within the @authorization directive on a type, the generated Cypher includes what looks like it should be a wildcard WITH clause, only it is surrounded by backticks, invalidating the query.

Type definitions

type Owner {
  id: ID! @unique @id
  owns: [MyNode!]! @relationship(type: "OWNS", direction: OUT)
}

type MyNode
  @authorization(
    validate: [
      {
        operations: [
          READ
          UPDATE
          DELETE
          DELETE_RELATIONSHIP
          CREATE_RELATIONSHIP
        ]
        where: { node: { owner: { id: "$jwt.sub" } } }
        when: [AFTER]
      }
    ]
  ) {
  id: ID! @unique @id
  name: String!
  owner: Owner! @relationship(type: "OWNS", direction: IN)
}

To Reproduce Steps to reproduce the behavior:

  1. Run a server with the following code...
import { ApolloServer } from "@apollo/server";
import { startStandaloneServer } from "@apollo/server/standalone";
import { Neo4jGraphQL } from "@neo4j/graphql";
import neo4j from "neo4j-driver";

const typeDefs = `
  type Owner {
    id: ID! @unique @id
    owns: [MyNode!]! @relationship(type: "OWNS", direction: OUT)
  }

  type MyNode
    @authorization(
      validate: [
        {
          operations: [
            READ
            UPDATE
            DELETE
            DELETE_RELATIONSHIP
            CREATE_RELATIONSHIP
          ]
          where: { node: { owner: { id: "$jwt.sub" } } }
          when: [AFTER]
        }
      ]
    ) {
    id: ID! @unique @id
    name: String!
    owner: Owner! @relationship(type: "OWNS", direction: IN)
  }
`;

const driver = neo4j.driver(
  "neo4j://localhost:7687",
  neo4j.auth.basic("username", "password")
);

const neoSchema = new Neo4jGraphQL({
  typeDefs,
  driver,
});

const server = new ApolloServer({
  schema: await neoSchema.getSchema(),
});

const { url } = await startStandaloneServer(server, {
  listen: { port: 4000 },
});

console.log(`🚀 Server ready at ${url}`);
  1. Execute the following Mutation...

Operation:

mutation CreateMyNodes($input: [MyNodeCreateInput!]!) {
  createMyNodes(input: $input) {
    myNodes {
      id
      name
      owner {
        id
      }
    }
  }
}

Variables:

{
  "input": {
    "name": "Test",
    "owner": {
      "connectOrCreate": {
        "onCreate": {
          "node": {}
        },
        "where": {
          "node": {
            "id": "my-user-id"
          }
        }
      }
    }
  },
}
  1. With debug logging enabled, we can observe the following query was executed:
CALL {
  CREATE (this0:MyNode)
  SET this0.id = randomUUID()
  SET this0.name = $this0_name
  WITH this0
  CALL {
    WITH this0
    MERGE (this0_owner_connectOrCreate0:Owner { id: $this0_owner_connectOrCreate_param0 })
    MERGE (this0)<-[this0_owner_connectOrCreate_this0:OWNS]-(this0_owner_connectOrCreate0)
    WITH `*`
    OPTIONAL MATCH (this0)<-[:OWNS]-(this0_owner_connectOrCreate_this1:Owner)
    WITH *, count(this0_owner_connectOrCreate_this1) AS ownerCount
    WITH `*`
    WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND (ownerCount <> 0 AND ($jwt.sub IS NOT NULL AND this0_owner_connectOrCreate_this1.id = $jwt.sub))), "@neo4j/graphql/FORBIDDEN", [0])
    RETURN count(*) AS _
  }
  WITH *
  CALL {
    WITH this0
    MATCH (this0)<-[this0_owner_Owner_unique:OWNS]-(:Owner)
    WITH count(this0_owner_Owner_unique) as c
    WHERE apoc.util.validatePredicate(NOT (c = 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMyNode.owner required exactly once', [0])
    RETURN c AS this0_owner_Owner_unique_ignored
  }
  RETURN this0
}
CALL {
  WITH this0
  CALL {
    WITH this0
    MATCH (this0)<-[create_this0:OWNS]-(create_this1:Owner)
    WITH create_this1 { .id } AS create_this1
    RETURN head(collect(create_this1)) AS create_var2
  }
  RETURN this0 { .id, .name, owner: create_var2 } AS create_var3
}
RETURN [create_var3] AS data
  1. See error
    Neo4jError: Variable `*` not defined (line 10, column 10 (offset: 310))
    "    WITH `*`"

Expected behavior The OGM to generate a valid Cypher query for the operation

System (please complete the following information):

neo4j-team-graphql commented 1 month ago

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

MacondoExpress commented 1 month ago

Hi @daveajrussell , Thank you for raising it, We were able to confirm and fix the bug, it will be released ASAP!