diff --git a/packages/graphql/src/translate/queryAST/ast/filters/authorization-filters/AuthorizationFilters.ts b/packages/graphql/src/translate/queryAST/ast/filters/authorization-filters/AuthorizationFilters.ts index d61786cfdd..1f35b1c1ae 100644 --- a/packages/graphql/src/translate/queryAST/ast/filters/authorization-filters/AuthorizationFilters.ts +++ b/packages/graphql/src/translate/queryAST/ast/filters/authorization-filters/AuthorizationFilters.ts @@ -62,10 +62,26 @@ export class AuthorizationFilters extends QueryASTNode { return; } + public getValidationPredicate( + context: QueryASTContext, + when: ValidateWhen = "BEFORE" + ): Cypher.Predicate | undefined { + const validationPredicate = Cypher.or( + ...this.getValidations(when).flatMap((validationRule) => validationRule.getPredicate(context)) + ); + return validationPredicate; + } + public getSubqueries(context: QueryASTContext): Cypher.Clause[] { return [...this.validations, ...this.filters].flatMap((c) => c.getSubqueries(context)); } + public getSubqueriesBefore(context: QueryASTContext): Cypher.Clause[] { + return [...this.validations.filter((v) => v.when === "BEFORE"), ...this.filters].flatMap((c) => + c.getSubqueries(context) + ); + } + public getSelection(context: QueryASTContext): Array { return [...this.validations, ...this.filters].flatMap((c) => c.getSelection(context)); } diff --git a/packages/graphql/src/translate/queryAST/ast/operations/ConnectOperation.ts b/packages/graphql/src/translate/queryAST/ast/operations/ConnectOperation.ts index 20b71653b5..1ec0885234 100644 --- a/packages/graphql/src/translate/queryAST/ast/operations/ConnectOperation.ts +++ b/packages/graphql/src/translate/queryAST/ast/operations/ConnectOperation.ts @@ -30,6 +30,9 @@ import type { AuthorizationFilters } from "../filters/authorization-filters/Auth import type { InputField } from "../input-fields/InputField"; import type { SelectionPattern } from "../selection/SelectionPattern/SelectionPattern"; import { MutationOperation, type OperationTranspileResult } from "./operations"; +import { checkEntityAuthentication } from "../../../authorization/check-authentication"; +import { ParamInputField } from "../input-fields/ParamInputField"; +import { isConcreteEntity } from "../../utils/is-concrete-entity"; export class ConnectOperation extends MutationOperation { public readonly target: ConcreteEntityAdapter; @@ -37,6 +40,7 @@ export class ConnectOperation extends MutationOperation { private selectionPattern: SelectionPattern; protected readonly authFilters: AuthorizationFilters[] = []; + protected readonly sourceAuthFilters: AuthorizationFilters[] = []; public readonly inputFields: Map = new Map(); private filters: Filter[] = []; @@ -74,6 +78,9 @@ export class ConnectOperation extends MutationOperation { public addAuthFilters(...filter: AuthorizationFilters[]) { this.authFilters.push(...filter); } + public addSourceAuthFilters(...filter: AuthorizationFilters[]) { + this.sourceAuthFilters.push(...filter); + } /** * Get and set field methods are utilities to remove duplicate fields between separate inputs @@ -115,6 +122,29 @@ export class ConnectOperation extends MutationOperation { const { nestedContext } = this.selectionPattern.apply(context); this.nestedContext = nestedContext; + checkEntityAuthentication({ + context: nestedContext.neo4jGraphQLContext, + entity: this.target.entity, + targetOperations: ["CREATE_RELATIONSHIP"], + }); + if (isConcreteEntity(this.relationship.source)) { + checkEntityAuthentication({ + context: nestedContext.neo4jGraphQLContext, + entity: this.relationship.source.entity, + targetOperations: ["CREATE_RELATIONSHIP"], + }); + } + this.inputFields.forEach((field) => { + if (field.attachedTo === "node" && field instanceof ParamInputField) { + checkEntityAuthentication({ + context: nestedContext.neo4jGraphQLContext, + entity: this.target.entity, + targetOperations: ["CREATE_RELATIONSHIP"], + field: field.name, + }); + } + }); + const matchPattern = new Cypher.Pattern(nestedContext.target, { labels: getEntityLabels(this.target, context.neo4jGraphQLContext), }); @@ -157,17 +187,36 @@ export class ConnectOperation extends MutationOperation { return input.getSubqueries(connectContext); }); + const authClausesBefore = this.getAuthorizationClauses(nestedContext); + const sourceAuthClausesBefore = this.getSourceAuthorizationClausesBefore(context); + const bothAuthClausesBefore: Cypher.Clause[] = []; + if (authClausesBefore.length === 0 && sourceAuthClausesBefore.length > 0) { + bothAuthClausesBefore.push(new Cypher.With("*"), ...sourceAuthClausesBefore); + } else { + bothAuthClausesBefore.push(Cypher.utils.concat(...authClausesBefore, ...sourceAuthClausesBefore)); + } + const clauses = Cypher.utils.concat( matchClause, - ...this.getAuthorizationClauses(nestedContext), // THESE ARE "BEFORE" AUTH + ...bothAuthClausesBefore, // THESE ARE "BEFORE" AUTH ...mutationSubqueries, - connectClause, - ...this.getAuthorizationClausesAfter(nestedContext) // THESE ARE "AFTER" AUTH + connectClause + // ...this.getAuthorizationClausesAfter(context) // THESE ARE "AFTER" AUTH ); + const authClausesAfter = this.getAuthorizationClausesAfter(nestedContext); + const sourceAuthClausesAfter = this.getSourceAuthorizationClausesAfter(context); + const callClause = new Cypher.Call(clauses, [context.target]); + const authClauses: Cypher.Clause[] = []; + if (authClausesAfter.length > 0 || sourceAuthClausesAfter.length > 0) { + authClauses.push(Cypher.utils.concat(...authClausesAfter, ...sourceAuthClausesAfter)); + } - return { projectionExpr: context.returnVariable, clauses: [callClause] }; + return { + projectionExpr: context.returnVariable, + clauses: [callClause, ...authClauses], + }; } private getAuthorizationClauses(context: QueryASTContext): Cypher.Clause[] { @@ -201,6 +250,36 @@ export class ConnectOperation extends MutationOperation { return []; } + private getSourceAuthorizationClausesAfter(context: QueryASTContext): Cypher.Clause[] { + const validationsAfter: Cypher.VoidProcedure[] = []; + for (const authFilter of this.sourceAuthFilters) { + const validationAfter = authFilter.getValidation(context, "AFTER"); + if (validationAfter) { + validationsAfter.push(validationAfter); + } + } + + if (validationsAfter.length > 0) { + return [new Cypher.With("*"), ...validationsAfter]; + } + return []; + } + + private getSourceAuthorizationClausesBefore(context: QueryASTContext): Cypher.Clause[] { + const validationsAfter: Cypher.VoidProcedure[] = []; + for (const authFilter of this.sourceAuthFilters) { + const validationAfter = authFilter.getValidation(context, "BEFORE"); + if (validationAfter) { + validationsAfter.push(validationAfter); + } + } + + if (validationsAfter.length > 0) { + return [new Cypher.With("*"), ...validationsAfter]; + } + return []; + } + private transpileAuthClauses(context: QueryASTContext): { selections: (Cypher.With | Cypher.Match)[]; subqueries: Cypher.Clause[]; diff --git a/packages/graphql/src/translate/queryAST/ast/operations/DisconnectOperation.ts b/packages/graphql/src/translate/queryAST/ast/operations/DisconnectOperation.ts index ff90813db9..1fb78bf95c 100644 --- a/packages/graphql/src/translate/queryAST/ast/operations/DisconnectOperation.ts +++ b/packages/graphql/src/translate/queryAST/ast/operations/DisconnectOperation.ts @@ -17,7 +17,7 @@ * limitations under the License. */ -import Cypher from "@neo4j/cypher-builder"; +import Cypher, { With } from "@neo4j/cypher-builder"; import type { ConcreteEntityAdapter } from "../../../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; import type { RelationshipAdapter } from "../../../../schema-model/relationship/model-adapters/RelationshipAdapter"; import { filterTruthy } from "../../../../utils/utils"; @@ -31,6 +31,9 @@ import type { InputField } from "../input-fields/InputField"; import type { SelectionPattern } from "../selection/SelectionPattern/SelectionPattern"; import type { ReadOperation } from "./ReadOperation"; import { MutationOperation, type OperationTranspileResult } from "./operations"; +import { checkEntityAuthentication } from "../../../authorization/check-authentication"; +import { ParamInputField } from "../input-fields/ParamInputField"; +import { isConcreteEntity } from "../../utils/is-concrete-entity"; export class DisconnectOperation extends MutationOperation { public readonly target: ConcreteEntityAdapter; @@ -38,6 +41,7 @@ export class DisconnectOperation extends MutationOperation { private selectionPattern: SelectionPattern; protected readonly authFilters: AuthorizationFilters[] = []; + protected readonly sourceAuthFilters: AuthorizationFilters[] = []; public readonly inputFields: Map = new Map(); private filters: Filter[] = []; @@ -75,6 +79,9 @@ export class DisconnectOperation extends MutationOperation { public addAuthFilters(...filter: AuthorizationFilters[]) { this.authFilters.push(...filter); } + public addSourceAuthFilters(...filter: AuthorizationFilters[]) { + this.sourceAuthFilters.push(...filter); + } /** * Get and set field methods are utilities to remove duplicate fields between separate inputs @@ -116,6 +123,29 @@ export class DisconnectOperation extends MutationOperation { const { nestedContext, pattern: matchPattern } = this.selectionPattern.apply(context); this.nestedContext = nestedContext; + checkEntityAuthentication({ + context: nestedContext.neo4jGraphQLContext, + entity: this.target.entity, + targetOperations: ["DELETE_RELATIONSHIP"], + }); + if (isConcreteEntity(this.relationship.source)) { + checkEntityAuthentication({ + context: nestedContext.neo4jGraphQLContext, + entity: this.relationship.source.entity, + targetOperations: ["DELETE_RELATIONSHIP"], + }); + } + this.inputFields.forEach((field) => { + if (field.attachedTo === "node" && field instanceof ParamInputField) { + checkEntityAuthentication({ + context: nestedContext.neo4jGraphQLContext, + entity: this.target.entity, + targetOperations: ["DELETE_RELATIONSHIP"], + field: field.name, + }); + } + }); + const allFilters = [...this.authFilters, ...this.filters]; const filterSubqueries = wrapSubqueriesInCypherCalls(nestedContext, allFilters, [nestedContext.target]); @@ -124,13 +154,13 @@ export class DisconnectOperation extends MutationOperation { if (filterSubqueries.length > 0) { const predicate = Cypher.and(...allFilters.map((f) => f.getPredicate(nestedContext))); matchClause = Cypher.utils.concat( - new Cypher.Match(matchPattern), + new Cypher.OptionalMatch(matchPattern), ...filterSubqueries, new Cypher.With("*").where(predicate) ); } else { const predicate = Cypher.and(...allFilters.map((f) => f.getPredicate(nestedContext))); - matchClause = new Cypher.Match(matchPattern).where(predicate); + matchClause = new Cypher.OptionalMatch(matchPattern).where(predicate); } const relVar = new Cypher.Relationship(); @@ -145,15 +175,40 @@ export class DisconnectOperation extends MutationOperation { const deleteClause = new Cypher.With(nestedContext.relationship!).delete(nestedContext.relationship!); + const authClausesBefore = this.getAuthorizationClauses(nestedContext); + const sourceAuthClausesBefore = this.getSourceAuthorizationClausesBefore(context); + + const bothAuthClausesBefore: Cypher.Clause[] = []; + if (authClausesBefore.length === 0 && sourceAuthClausesBefore.length > 0) { + bothAuthClausesBefore.push(new Cypher.With("*"), ...sourceAuthClausesBefore); + } else { + bothAuthClausesBefore.push(Cypher.utils.concat(...authClausesBefore, ...sourceAuthClausesBefore)); + } + const clauses = Cypher.utils.concat( matchClause, - ...this.getAuthorizationClauses(nestedContext), // THESE ARE "BEFORE" AUTH + ...bothAuthClausesBefore, ...mutationSubqueries, - deleteClause, - ...this.getAuthorizationClausesAfter(nestedContext) // THESE ARE "AFTER" AUTH + deleteClause + // ...this.getAuthorizationClausesAfter(nestedContext) // THESE ARE "AFTER" AUTH ); - return { projectionExpr: context.returnVariable, clauses: [clauses] }; + const authClausesAfter = this.getAuthorizationClausesAfter(nestedContext); + const sourceAuthClausesAfter = this.getSourceAuthorizationClausesAfter(context); + + const callClause = new Cypher.Call(clauses, [context.target]); + const authClauses: Cypher.Clause[] = []; + if (authClausesAfter.length > 0 || sourceAuthClausesAfter.length > 0) { + authClauses.push(Cypher.utils.concat(...authClausesAfter, ...sourceAuthClausesAfter)); + } + console.log("authClauses", authClauses); + + return { + projectionExpr: context.returnVariable, + clauses: [callClause, ...authClauses], + }; + + // return { projectionExpr: context.returnVariable, clauses: [clauses] }; } private getAuthorizationClauses(context: QueryASTContext): Cypher.Clause[] { @@ -187,6 +242,35 @@ export class DisconnectOperation extends MutationOperation { return []; } + private getSourceAuthorizationClausesAfter(context: QueryASTContext): Cypher.Clause[] { + const validationsAfter: Cypher.VoidProcedure[] = []; + for (const authFilter of this.sourceAuthFilters) { + const validationAfter = authFilter.getValidation(context, "AFTER"); + if (validationAfter) { + validationsAfter.push(validationAfter); + } + } + + if (validationsAfter.length > 0) { + return [new Cypher.With("*"), ...validationsAfter]; + } + return []; + } + private getSourceAuthorizationClausesBefore(context: QueryASTContext): Cypher.Clause[] { + const validationsAfter: Cypher.VoidProcedure[] = []; + for (const authFilter of this.sourceAuthFilters) { + const validationAfter = authFilter.getValidation(context, "BEFORE"); + if (validationAfter) { + validationsAfter.push(validationAfter); + } + } + + if (validationsAfter.length > 0) { + return [new Cypher.With("*"), ...validationsAfter]; + } + return []; + } + private transpileAuthClauses(context: QueryASTContext): { selections: (Cypher.With | Cypher.Match)[]; subqueries: Cypher.Clause[]; diff --git a/packages/graphql/src/translate/queryAST/ast/operations/UpdateOperation.ts b/packages/graphql/src/translate/queryAST/ast/operations/UpdateOperation.ts index e9aaab9a7e..44ae31489b 100644 --- a/packages/graphql/src/translate/queryAST/ast/operations/UpdateOperation.ts +++ b/packages/graphql/src/translate/queryAST/ast/operations/UpdateOperation.ts @@ -102,22 +102,6 @@ export class UpdateOperation extends Operation { const { nestedContext, pattern } = this.selectionPattern.apply(context); this.nestedContext = nestedContext; - // We need to call the filter subqueries before predicate to handle aggregate filters - const filterSubqueries = wrapSubqueriesInCypherCalls(nestedContext, this.filters, [nestedContext.target]); - - const predicate = this.getPredicate(nestedContext); - // const matchClause = new Cypher.Match(pattern).where(predicate); - - const matchClause = new Cypher.Match(pattern); - let filtersWith: Cypher.With | undefined; - - const hasFilterSubqueries = filterSubqueries.length > 0; - if (hasFilterSubqueries) { - filtersWith = new Cypher.With("*").where(predicate); - } else { - matchClause.where(predicate); - } - checkEntityAuthentication({ context: context.neo4jGraphQLContext, entity: this.target.entity, @@ -138,9 +122,16 @@ export class UpdateOperation extends Operation { return input.getSetParams(nestedContext); }); - const mutationSubqueries = Array.from(this.inputFields.values()).flatMap((input) => { - return input.getSubqueries(nestedContext); - }); + const mutationSubqueries = Array.from(this.inputFields.values()) + .flatMap((input) => { + const subqueries = input.getSubqueries(nestedContext); + const authSubqueries = input.getAuthorizationSubqueries(nestedContext); + if (authSubqueries.length > 0 || subqueries.length > 0) { + return Cypher.utils.concat(...subqueries, ...authSubqueries); + } + return undefined; + }) + .filter((s) => s !== undefined); // This is a small optimisation, to avoid subqueries with no changes // Top level should still be generated for projection @@ -150,8 +141,25 @@ export class UpdateOperation extends Operation { } } + const afterAuthFilters = this.authFilters.filter((af) => { + return af.getValidation(nestedContext!, "AFTER"); + }); + + // We need to call the filter subqueries before predicate to handle aggregate filters + const filterSubqueries = wrapSubqueriesInCypherCalls(nestedContext, this.filters, [nestedContext.target]); + const afterFilterSubqueries = wrapSubqueriesInCypherCalls(nestedContext, afterAuthFilters, [ + nestedContext.target, + ]); + + const predicate = this.getPredicate(nestedContext); + + const matchClause = new Cypher.Match(pattern); + const filtersWith = new Cypher.With("*").where(predicate); if (filtersWith) { filtersWith.set(...setParams); + if (mutationSubqueries.length || afterFilterSubqueries.length) { + filtersWith.with("*"); + } } else { matchClause.set(...setParams); } @@ -159,8 +167,11 @@ export class UpdateOperation extends Operation { const clauses = Cypher.utils.concat( matchClause, ...filterSubqueries, + ...this.getAuthorizationClauses(nestedContext), // THESE ARE "BEFORE" AUTH filtersWith, - ...mutationSubqueries.map((sq) => Cypher.utils.concat(new Cypher.With("*"), new Cypher.Call(sq, "*"))) + ...mutationSubqueries.map((sq) => Cypher.utils.concat(new Cypher.With("*"), new Cypher.Call(sq, "*"))), + ...afterFilterSubqueries, + ...this.getAuthorizationClausesAfter(nestedContext) // THESE ARE "AFTER" AUTH ); return { projectionExpr: nestedContext.target, clauses: [clauses] }; @@ -177,10 +188,10 @@ export class UpdateOperation extends Operation { } return [ - ...this.getAuthorizationClauses(nestedContext), - ...this.inputFields.flatMap((inputField) => { - return inputField.getAuthorizationSubqueries(nestedContext); - }), + // ...this.getAuthorizationClauses(nestedContext), + // ...this.inputFields.flatMap((inputField) => { + // return inputField.getAuthorizationSubqueries(nestedContext); + // }), ]; } @@ -189,15 +200,34 @@ export class UpdateOperation extends Operation { const predicate = Cypher.and(...predicates); const lastSelection = selections[selections.length - 1]; + const authSubqueries = subqueries.map((sq) => { + return new Cypher.Call(sq, "*"); + }); + // console.log("here", authSubqueries, validations); if (!predicates.length && !validations.length) { return []; } else { if (lastSelection) { lastSelection.where(predicate); - return [...subqueries, new Cypher.With("*"), ...selections, ...validations]; + return [...authSubqueries, new Cypher.With("*"), ...selections, ...validations]; } - return [...subqueries, new Cypher.With("*").where(predicate), ...selections, ...validations]; + return [...authSubqueries, new Cypher.With("*").where(predicate), ...selections, ...validations]; + } + } + + private getAuthorizationClausesAfter(context: QueryASTContext): Cypher.Clause[] { + const validationsAfter: Cypher.VoidProcedure[] = []; + for (const authFilter of this.authFilters) { + const validationAfter = authFilter.getValidation(context, "AFTER"); + if (validationAfter) { + validationsAfter.push(validationAfter); + } + } + + if (validationsAfter.length > 0) { + return [new Cypher.With("*"), ...validationsAfter]; } + return []; } private transpileAuthClauses(context: QueryASTContext): { @@ -212,9 +242,9 @@ export class UpdateOperation extends Operation { const validations: Cypher.VoidProcedure[] = []; for (const authFilter of this.authFilters) { const extraSelections = authFilter.getSelection(context); - const authSubqueries = authFilter.getSubqueries(context); + const authSubqueries = authFilter.getSubqueriesBefore(context); const authPredicate = authFilter.getPredicate(context); - const validation = authFilter.getValidation(context, "AFTER"); // CREATE only has AFTER auth + const validationBefore = authFilter.getValidation(context, "BEFORE"); if (extraSelections) { selections.push(...extraSelections); } @@ -224,8 +254,8 @@ export class UpdateOperation extends Operation { if (authPredicate) { predicates.push(authPredicate); } - if (validation) { - validations.push(validation); + if (validationBefore) { + validations.push(validationBefore); } } return { selections, subqueries, predicates, validations }; diff --git a/packages/graphql/src/translate/queryAST/factory/Operations/ConnectFactory.ts b/packages/graphql/src/translate/queryAST/factory/Operations/ConnectFactory.ts index d314df262c..602adeb7c9 100644 --- a/packages/graphql/src/translate/queryAST/factory/Operations/ConnectFactory.ts +++ b/packages/graphql/src/translate/queryAST/factory/Operations/ConnectFactory.ts @@ -141,6 +141,13 @@ export class ConnectFactory { context, operation: connect, }); + if (isConcreteEntity(relationship.source)) { + this.addSourceEntityAuthorization({ + entity: relationship.source, + context, + operation: connect, + }); + } asArray(input).forEach((inputItem) => { const { whereArg, connectArg } = this.parseConnectArgs(inputItem); @@ -269,6 +276,25 @@ export class ConnectFactory { operation.addAuthFilters(...authFilters); } + private addSourceEntityAuthorization({ + entity, + context, + operation, + }: { + entity: ConcreteEntityAdapter; + context: Neo4jGraphQLTranslationContext; + operation: ConnectOperation; + }): void { + const authFilters = this.queryASTFactory.authorizationFactory.getAuthFilters({ + entity, + operations: ["CREATE_RELATIONSHIP"], + context, + afterValidation: true, + }); + + operation.addSourceAuthFilters(...authFilters); + } + private getInputEdge(inputItem: Record, relationship: RelationshipAdapter): Record { const edge = inputItem.edge ?? {}; diff --git a/packages/graphql/src/translate/queryAST/factory/Operations/DisconnectFactory.ts b/packages/graphql/src/translate/queryAST/factory/Operations/DisconnectFactory.ts index 84c03ec030..d9a275420d 100644 --- a/packages/graphql/src/translate/queryAST/factory/Operations/DisconnectFactory.ts +++ b/packages/graphql/src/translate/queryAST/factory/Operations/DisconnectFactory.ts @@ -142,6 +142,14 @@ export class DisconnectFactory { operation: disconnect, }); + if (isConcreteEntity(relationship.source)) { + this.addSourceEntityAuthorization({ + entity: relationship.source, + context, + operation: disconnect, + }); + } + asArray(input).forEach((inputItem) => { const { whereArg, disconnectArg } = this.parseDisconnectArgs(inputItem); const nodeFilters: Filter[] = []; @@ -225,6 +233,24 @@ export class DisconnectFactory { operation.addAuthFilters(...authFilters); } + private addSourceEntityAuthorization({ + entity, + context, + operation, + }: { + entity: ConcreteEntityAdapter; + context: Neo4jGraphQLTranslationContext; + operation: DisconnectOperation; + }): void { + const authFilters = this.queryASTFactory.authorizationFactory.getAuthFilters({ + entity, + operations: ["DELETE_RELATIONSHIP"], + context, + afterValidation: true, + }); + + operation.addSourceAuthFilters(...authFilters); + } private getInputEdge(inputItem: Record, relationship: RelationshipAdapter): Record { const edge = inputItem.edge ?? {}; diff --git a/packages/graphql/src/translate/queryAST/factory/Operations/UpdateFactory.ts b/packages/graphql/src/translate/queryAST/factory/Operations/UpdateFactory.ts index 0bd1c68bb0..010b46e6d7 100644 --- a/packages/graphql/src/translate/queryAST/factory/Operations/UpdateFactory.ts +++ b/packages/graphql/src/translate/queryAST/factory/Operations/UpdateFactory.ts @@ -25,7 +25,7 @@ import type { ConcreteEntityAdapter } from "../../../../schema-model/entity/mode import type { InterfaceEntityAdapter } from "../../../../schema-model/entity/model-adapters/InterfaceEntityAdapter"; import type { RelationshipAdapter } from "../../../../schema-model/relationship/model-adapters/RelationshipAdapter"; import type { Neo4jGraphQLTranslationContext } from "../../../../types/neo4j-graphql-translation-context"; -import { asArray } from "../../../../utils/utils"; +import { asArray, filterTruthy } from "../../../../utils/utils"; import { OperationField } from "../../ast/fields/OperationField"; import { type InputField } from "../../ast/input-fields/InputField"; import { MutationOperationField } from "../../ast/input-fields/MutationOperationField"; @@ -63,7 +63,14 @@ export class UpdateFactory { const rawInput = resolveTree.args.update as Record[]; const input = asArray(rawInput) ?? []; - const updateOperations: UpdateOperation[] = input.map((inputItem) => { + let updateOperations: UpdateOperation[]; + + if (!input.length) { + // dummy input to translate top level match for the projection to work + input.push({}); + } + + updateOperations = input.map((inputItem) => { const updateOperation = new UpdateOperation({ target: entity, selectionPattern: new NodeSelectionPattern({ @@ -105,7 +112,6 @@ export class UpdateFactory { }); return fieldOperation; }); - const topLevelMutation = new TopLevelUpdateMutationOperation({ updateOperations, projectionOperations, @@ -153,6 +159,12 @@ export class UpdateFactory { raiseAttributeAmbiguityForUpdate(Object.keys(targetInput), target); raiseAttributeAmbiguityForUpdate(Object.keys(this.getInputEdge(inputItem)), relationship); + // this.addEntityAuthorization({ + // entity: target, + // context, + // operation: update, + // }); + const filters = this.queryASTFactory.filterFactory.createConnectionPredicates({ rel: relationship, entity: target, @@ -522,16 +534,37 @@ export class UpdateFactory { context: Neo4jGraphQLTranslationContext; operation: UpdateOperation; }): void { - const authFilters = this.queryASTFactory.authorizationFactory.createAuthValidateRule({ + // const authBeforeFilters = this.queryASTFactory.authorizationFactory.createAuthValidateRule({ + // entity, + // authAnnotation: entity.annotations.authorization, + // when: "BEFORE", + // operations: ["UPDATE"], + // context, + // }); + // console.log("authBeforeFilters", authBeforeFilters); + // if (authBeforeFilters) { + // operation.addAuthFilters(authBeforeFilters); + // } + // const authFilters = this.queryASTFactory.authorizationFactory.createAuthValidateRule({ + // entity, + // authAnnotation: entity.annotations.authorization, + // when: "AFTER", + // operations: ["UPDATE"], + // context, + // }); + // if (authFilters) { + // operation.addAuthFilters(authFilters); + // } + + console.log("add entity auth for", entity.name); + const authFilters = this.queryASTFactory.authorizationFactory.getAuthFilters({ entity, - authAnnotation: entity.annotations.authorization, - when: "AFTER", operations: ["UPDATE"], context, + afterValidation: true, }); - if (authFilters) { - operation.addAuthFilters(authFilters); - } + + operation.addAuthFilters(...authFilters); } private addAttributeAuthorization({ @@ -547,6 +580,17 @@ export class UpdateFactory { entity: ConcreteEntityAdapter; conditionForEvaluation?: Cypher.Predicate; }): void { + const authBeforeFilters = this.queryASTFactory.authorizationFactory.createAuthValidateRule({ + entity, + authAnnotation: attribute.annotations.authorization, + when: "BEFORE", + conditionForEvaluation, + operations: ["UPDATE"], + context, + }); + if (authBeforeFilters) { + update.addAuthFilters(authBeforeFilters); + } const attributeAuthorization = this.queryASTFactory.authorizationFactory.createAuthValidateRule({ entity, when: "AFTER",