Skip to content

Commit 858a98f

Browse files
authored
Only apply auth update rules if properties values are being updated (#6696)
* only apply auth update rules if properties values are being updated * review changes * add changeset * fix validate rules as well * change default for ignoreOperationAuthorization flag * remove double negation
1 parent 2832b55 commit 858a98f

File tree

18 files changed

+1182
-171
lines changed

18 files changed

+1182
-171
lines changed

.changeset/modern-vans-care.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@neo4j/graphql": patch
3+
---
4+
5+
Fix update rules incorrectly applied on relationship creation and deletion

packages/graphql/src/translate/authorization/compatibility/create-authorization-after-and-params.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import {
2828
import type { NodeMap } from "../types/node-map";
2929
import { compilePredicateReturn } from "./compile-predicate-return";
3030

31-
type AuthorizationAfterAndParams = {
31+
export type AuthorizationAfterAndParams = {
3232
cypher: string;
3333
params: Record<string, any>;
3434
subqueries?: string;

packages/graphql/src/translate/create-update-and-params.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ describe("createUpdateAndParams", () => {
104104
withVars: ["this"],
105105
parameterPrefix: "this",
106106
callbackBucket: new CallbackBucketDeprecated(context),
107+
ignoreOperationAuthorization: false,
107108
});
108109

109110
expect(trimmer(result[0])).toEqual(

packages/graphql/src/translate/create-update-and-params.ts

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { caseWhere } from "../utils/case-where";
2727
import { getRelationshipType } from "../utils/get-relationship-type";
2828
import { wrapStringInApostrophes } from "../utils/wrap-string-in-apostrophes";
2929
import { checkAuthentication } from "./authorization/check-authentication";
30+
import type { AuthorizationAfterAndParams } from "./authorization/compatibility/create-authorization-after-and-params";
3031
import {
3132
createAuthorizationAfterAndParams,
3233
createAuthorizationAfterAndParamsField,
@@ -74,6 +75,7 @@ export default function createUpdateAndParams({
7475
context,
7576
callbackBucket,
7677
parameterPrefix,
78+
ignoreOperationAuthorization,
7779
}: {
7880
parentVar: string;
7981
updateInput: any;
@@ -84,6 +86,7 @@ export default function createUpdateAndParams({
8486
context: Neo4jGraphQLTranslationContext;
8587
callbackBucket: CallbackBucketDeprecated;
8688
parameterPrefix: string;
89+
ignoreOperationAuthorization: boolean;
8790
}): [string, any] {
8891
let hasAppliedTimeStamps = false;
8992

@@ -312,6 +315,7 @@ export default function createUpdateAndParams({
312315
parameterPrefix: `${parameterPrefix}.${key}${
313316
relationField.union ? `.${refNode.name}` : ""
314317
}${relationField.typeMeta.array ? `[${index}]` : ``}.update.node`,
318+
ignoreOperationAuthorization,
315319
});
316320
res.params = { ...res.params, ...updateAndParams[1] };
317321
innerUpdate.push(updateAndParams[0]);
@@ -599,23 +603,28 @@ export default function createUpdateAndParams({
599603
const authorizationAfterSubqueries = meta.authorizationAfterSubqueries;
600604

601605
const withStr = `WITH ${withVars.join(", ")}`;
606+
let authorizationAfterAndParams: AuthorizationAfterAndParams | undefined;
602607

603-
const authorizationAfterAndParams = createAuthorizationAfterAndParams({
604-
context,
605-
nodes: [{ node, variable: varName }],
606-
operations: ["UPDATE"],
607-
});
608+
if (ignoreOperationAuthorization) {
609+
authorizationAfterAndParams = undefined;
610+
} else {
611+
authorizationAfterAndParams = createAuthorizationAfterAndParams({
612+
context,
613+
nodes: [{ node, variable: varName }],
614+
operations: ["UPDATE"],
615+
});
608616

609-
if (authorizationAfterAndParams) {
610-
const { cypher, params: authWhereParams, subqueries } = authorizationAfterAndParams;
617+
if (authorizationAfterAndParams) {
618+
const { cypher, params: authWhereParams, subqueries } = authorizationAfterAndParams;
611619

612-
if (cypher) {
613-
if (subqueries) {
614-
authorizationAfterSubqueries.push(subqueries);
615-
}
620+
if (cypher) {
621+
if (subqueries) {
622+
authorizationAfterSubqueries.push(subqueries);
623+
}
616624

617-
authorizationAfterStrs.push(cypher);
618-
params = { ...params, ...authWhereParams };
625+
authorizationAfterStrs.push(cypher);
626+
params = { ...params, ...authWhereParams };
627+
}
619628
}
620629
}
621630

packages/graphql/src/translate/translate-top-level-match.ts

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
import Cypher from "@neo4j/cypher-builder";
2121
import type { Node } from "../classes";
2222
import type { AuthorizationOperation } from "../schema-model/annotation/AuthorizationAnnotation";
23-
import type { GraphQLWhereArg } from "../types";
23+
import type { GraphQLWhereArg, PredicateReturn } from "../types";
2424
import type { Neo4jGraphQLTranslationContext } from "../types/neo4j-graphql-translation-context";
2525
import { getEntityAdapterFromNode } from "../utils/get-entity-adapter-from-node";
2626
import { createAuthorizationBeforePredicate } from "./authorization/create-authorization-before-predicate";
@@ -34,13 +34,15 @@ export function translateTopLevelMatch({
3434
context,
3535
operation,
3636
where,
37+
ignoreOperationAuthorization = false,
3738
}: {
3839
matchNode: Cypher.Node;
3940
matchPattern: Cypher.Pattern;
4041
context: Neo4jGraphQLTranslationContext;
4142
node: Node;
4243
operation: AuthorizationOperation;
4344
where: GraphQLWhereArg | undefined;
45+
ignoreOperationAuthorization: boolean;
4446
}): Cypher.CypherResult {
4547
const { matchClause, preComputedWhereFieldSubqueries, whereClause } = createMatchClause({
4648
matchNode,
@@ -49,6 +51,7 @@ export function translateTopLevelMatch({
4951
context,
5052
operation,
5153
where,
54+
ignoreOperationAuthorization,
5255
});
5356

5457
return buildClause(Cypher.utils.concat(matchClause, preComputedWhereFieldSubqueries, whereClause), { context });
@@ -66,6 +69,7 @@ function createMatchClause({
6669
node,
6770
context,
6871
operation,
72+
ignoreOperationAuthorization,
6973
where,
7074
}: {
7175
matchNode: Cypher.Node;
@@ -74,26 +78,33 @@ function createMatchClause({
7478
node: Node;
7579
operation: AuthorizationOperation;
7680
where: GraphQLWhereArg | undefined;
81+
ignoreOperationAuthorization: boolean;
7782
}): CreateMatchClauseReturn {
7883
const matchClause: Cypher.Match | Cypher.Yield = new Cypher.Match(matchPattern);
7984
const whereOperators: Cypher.Predicate[] = [];
8085

8186
let whereClause: Cypher.Match | Cypher.Yield | Cypher.With | undefined;
8287

83-
const authorizationPredicateReturn = createAuthorizationBeforePredicate({
84-
context,
85-
nodes: [
86-
{
87-
variable: matchNode,
88-
node,
89-
},
90-
],
91-
operations: [operation],
92-
});
93-
if (authorizationPredicateReturn?.predicate) {
94-
whereClause = new Cypher.With("*");
95-
} else {
88+
let authorizationPredicateReturn: PredicateReturn | undefined;
89+
if (operation === "UPDATE" && ignoreOperationAuthorization) {
9690
whereClause = matchClause;
91+
authorizationPredicateReturn = undefined;
92+
} else {
93+
authorizationPredicateReturn = createAuthorizationBeforePredicate({
94+
context,
95+
nodes: [
96+
{
97+
variable: matchNode,
98+
node,
99+
},
100+
],
101+
operations: [operation],
102+
});
103+
if (authorizationPredicateReturn?.predicate) {
104+
whereClause = new Cypher.With("*");
105+
} else {
106+
whereClause = matchClause;
107+
}
97108
}
98109

99110
let preComputedWhereFieldSubqueries: Cypher.CompositeClause | undefined;

packages/graphql/src/translate/translate-update.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,18 @@ export default async function translateUpdate({
6767
const matchNode = new Cypher.NamedNode(varName);
6868
const where = resolveTree.args.where as GraphQLWhereArg | undefined;
6969
const matchPattern = new Cypher.Pattern(matchNode, { labels: node.getLabels(context) });
70+
// bypass auth rules if there's no actual update of properties (connect and disconnect have their own auth rules)
71+
const ignoreOperationAuthorization = updateInput
72+
? Object.keys(updateInput).every((x) => node.relationFields.some((field) => field.fieldName === x))
73+
: false;
7074
const topLevelMatch = translateTopLevelMatch({
7175
matchNode,
7276
matchPattern,
7377
node,
7478
context,
7579
operation: "UPDATE",
7680
where,
81+
ignoreOperationAuthorization,
7782
});
7883
matchAndWhereStr = topLevelMatch.cypher;
7984
let cypherParams = topLevelMatch.params;
@@ -173,6 +178,7 @@ export default async function translateUpdate({
173178
parentVar: varName,
174179
withVars,
175180
parameterPrefix: `${resolveTree.name}.args.update`,
181+
ignoreOperationAuthorization,
176182
});
177183
[updateStr] = updateAndParams;
178184
cypherParams = {

packages/graphql/tests/integration/directives/authorization/bind.int.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,7 @@ describe("auth/bind", () => {
497497
${User.operations.update}(
498498
where: { id_EQ: "${userId}" },
499499
update: {
500+
id_SET: "${userId}",
500501
posts: {
501502
update: {
502503
where: { node: { id_EQ: "${postId}" } },

0 commit comments

Comments
 (0)