Skip to content

Commit 22058f5

Browse files
a-alleangrykoala
andauthored
implement authentication and authorization (#6778)
* implement authentication and authorization * refactor auth in nested update * improve after subqueries for auth in update --------- Co-authored-by: angrykoala <[email protected]>
1 parent d4a2627 commit 22058f5

File tree

7 files changed

+328
-54
lines changed

7 files changed

+328
-54
lines changed

packages/graphql/src/translate/queryAST/ast/filters/authorization-filters/AuthorizationFilters.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,30 @@ export class AuthorizationFilters extends QueryASTNode {
6262
return;
6363
}
6464

65+
public getValidationPredicate(
66+
context: QueryASTContext,
67+
when: ValidateWhen = "BEFORE"
68+
): Cypher.Predicate | undefined {
69+
const validationPredicate = Cypher.or(
70+
...this.getValidations(when).flatMap((validationRule) => validationRule.getPredicate(context))
71+
);
72+
return validationPredicate;
73+
}
74+
6575
public getSubqueries(context: QueryASTContext): Cypher.Clause[] {
6676
return [...this.validations, ...this.filters].flatMap((c) => c.getSubqueries(context));
6777
}
6878

79+
public getSubqueriesBefore(context: QueryASTContext): Cypher.Clause[] {
80+
return [...this.validations.filter((v) => v.when === "BEFORE"), ...this.filters].flatMap((c) =>
81+
c.getSubqueries(context)
82+
);
83+
}
84+
85+
public getSubqueriesAfter(context: QueryASTContext): Cypher.Clause[] {
86+
return [...this.validations.filter((v) => v.when === "AFTER")].flatMap((c) => c.getSubqueries(context));
87+
}
88+
6989
public getSelection(context: QueryASTContext): Array<Cypher.Match | Cypher.With> {
7090
return [...this.validations, ...this.filters].flatMap((c) => c.getSelection(context));
7191
}

packages/graphql/src/translate/queryAST/ast/operations/ConnectOperation.ts

Lines changed: 82 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,16 @@ import Cypher from "@neo4j/cypher-builder";
2121
import type { ConcreteEntityAdapter } from "../../../../schema-model/entity/model-adapters/ConcreteEntityAdapter";
2222
import type { RelationshipAdapter } from "../../../../schema-model/relationship/model-adapters/RelationshipAdapter";
2323
import { filterTruthy } from "../../../../utils/utils";
24+
import { checkEntityAuthentication } from "../../../authorization/check-authentication";
2425
import { getEntityLabels } from "../../utils/create-node-from-entity";
26+
import { isConcreteEntity } from "../../utils/is-concrete-entity";
2527
import { wrapSubqueriesInCypherCalls } from "../../utils/wrap-subquery-in-calls";
2628
import type { QueryASTContext } from "../QueryASTContext";
2729
import type { QueryASTNode } from "../QueryASTNode";
2830
import type { Filter } from "../filters/Filter";
2931
import type { AuthorizationFilters } from "../filters/authorization-filters/AuthorizationFilters";
3032
import type { InputField } from "../input-fields/InputField";
33+
import { ParamInputField } from "../input-fields/ParamInputField";
3134
import type { SelectionPattern } from "../selection/SelectionPattern/SelectionPattern";
3235
import { MutationOperation, type OperationTranspileResult } from "./operations";
3336

@@ -37,6 +40,7 @@ export class ConnectOperation extends MutationOperation {
3740

3841
private selectionPattern: SelectionPattern;
3942
protected readonly authFilters: AuthorizationFilters[] = [];
43+
protected readonly sourceAuthFilters: AuthorizationFilters[] = [];
4044

4145
public readonly inputFields: Map<string, InputField> = new Map();
4246
private filters: Filter[] = [];
@@ -74,6 +78,9 @@ export class ConnectOperation extends MutationOperation {
7478
public addAuthFilters(...filter: AuthorizationFilters[]) {
7579
this.authFilters.push(...filter);
7680
}
81+
public addSourceAuthFilters(...filter: AuthorizationFilters[]) {
82+
this.sourceAuthFilters.push(...filter);
83+
}
7784

7885
/**
7986
* Get and set field methods are utilities to remove duplicate fields between separate inputs
@@ -115,6 +122,29 @@ export class ConnectOperation extends MutationOperation {
115122
const { nestedContext } = this.selectionPattern.apply(context);
116123
this.nestedContext = nestedContext;
117124

125+
checkEntityAuthentication({
126+
context: nestedContext.neo4jGraphQLContext,
127+
entity: this.target.entity,
128+
targetOperations: ["CREATE_RELATIONSHIP"],
129+
});
130+
if (isConcreteEntity(this.relationship.source)) {
131+
checkEntityAuthentication({
132+
context: nestedContext.neo4jGraphQLContext,
133+
entity: this.relationship.source.entity,
134+
targetOperations: ["CREATE_RELATIONSHIP"],
135+
});
136+
}
137+
this.inputFields.forEach((field) => {
138+
if (field.attachedTo === "node" && field instanceof ParamInputField) {
139+
checkEntityAuthentication({
140+
context: nestedContext.neo4jGraphQLContext,
141+
entity: this.target.entity,
142+
targetOperations: ["CREATE_RELATIONSHIP"],
143+
field: field.name,
144+
});
145+
}
146+
});
147+
118148
const matchPattern = new Cypher.Pattern(nestedContext.target, {
119149
labels: getEntityLabels(this.target, context.neo4jGraphQLContext),
120150
});
@@ -157,17 +187,35 @@ export class ConnectOperation extends MutationOperation {
157187
return input.getSubqueries(connectContext);
158188
});
159189

190+
const authClausesBefore = this.getAuthorizationClauses(nestedContext);
191+
const sourceAuthClausesBefore = this.getSourceAuthorizationClausesBefore(context);
192+
const bothAuthClausesBefore: Cypher.Clause[] = [];
193+
if (authClausesBefore.length === 0 && sourceAuthClausesBefore.length > 0) {
194+
bothAuthClausesBefore.push(new Cypher.With("*"), ...sourceAuthClausesBefore);
195+
} else {
196+
bothAuthClausesBefore.push(Cypher.utils.concat(...authClausesBefore, ...sourceAuthClausesBefore));
197+
}
198+
160199
const clauses = Cypher.utils.concat(
161200
matchClause,
162-
...this.getAuthorizationClauses(nestedContext), // THESE ARE "BEFORE" AUTH
201+
...bothAuthClausesBefore, // THESE ARE "BEFORE" AUTH
163202
...mutationSubqueries,
164-
connectClause,
165-
...this.getAuthorizationClausesAfter(nestedContext) // THESE ARE "AFTER" AUTH
203+
connectClause
166204
);
167205

206+
const authClausesAfter = this.getAuthorizationClausesAfter(nestedContext);
207+
const sourceAuthClausesAfter = this.getSourceAuthorizationClausesAfter(context);
208+
168209
const callClause = new Cypher.Call(clauses, [context.target]);
210+
const authClauses: Cypher.Clause[] = [];
211+
if (authClausesAfter.length > 0 || sourceAuthClausesAfter.length > 0) {
212+
authClauses.push(Cypher.utils.concat(...authClausesAfter, ...sourceAuthClausesAfter));
213+
}
169214

170-
return { projectionExpr: context.returnVariable, clauses: [callClause] };
215+
return {
216+
projectionExpr: context.returnVariable,
217+
clauses: [callClause, ...authClauses],
218+
};
171219
}
172220

173221
private getAuthorizationClauses(context: QueryASTContext): Cypher.Clause[] {
@@ -201,6 +249,36 @@ export class ConnectOperation extends MutationOperation {
201249
return [];
202250
}
203251

252+
private getSourceAuthorizationClausesAfter(context: QueryASTContext): Cypher.Clause[] {
253+
const validationsAfter: Cypher.VoidProcedure[] = [];
254+
for (const authFilter of this.sourceAuthFilters) {
255+
const validationAfter = authFilter.getValidation(context, "AFTER");
256+
if (validationAfter) {
257+
validationsAfter.push(validationAfter);
258+
}
259+
}
260+
261+
if (validationsAfter.length > 0) {
262+
return [new Cypher.With("*"), ...validationsAfter];
263+
}
264+
return [];
265+
}
266+
267+
private getSourceAuthorizationClausesBefore(context: QueryASTContext): Cypher.Clause[] {
268+
const validationsAfter: Cypher.VoidProcedure[] = [];
269+
for (const authFilter of this.sourceAuthFilters) {
270+
const validationAfter = authFilter.getValidation(context, "BEFORE");
271+
if (validationAfter) {
272+
validationsAfter.push(validationAfter);
273+
}
274+
}
275+
276+
if (validationsAfter.length > 0) {
277+
return [new Cypher.With("*"), ...validationsAfter];
278+
}
279+
return [];
280+
}
281+
204282
private transpileAuthClauses(context: QueryASTContext): {
205283
selections: (Cypher.With | Cypher.Match)[];
206284
subqueries: Cypher.Clause[];

packages/graphql/src/translate/queryAST/ast/operations/DisconnectOperation.ts

Lines changed: 87 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,16 @@ import Cypher from "@neo4j/cypher-builder";
2121
import type { ConcreteEntityAdapter } from "../../../../schema-model/entity/model-adapters/ConcreteEntityAdapter";
2222
import type { RelationshipAdapter } from "../../../../schema-model/relationship/model-adapters/RelationshipAdapter";
2323
import { filterTruthy } from "../../../../utils/utils";
24-
import { getEntityLabels } from "../../utils/create-node-from-entity";
24+
import { checkEntityAuthentication } from "../../../authorization/check-authentication";
25+
import { isConcreteEntity } from "../../utils/is-concrete-entity";
2526
import { wrapSubqueriesInCypherCalls } from "../../utils/wrap-subquery-in-calls";
2627
import type { QueryASTContext } from "../QueryASTContext";
2728
import type { QueryASTNode } from "../QueryASTNode";
2829
import type { Filter } from "../filters/Filter";
2930
import type { AuthorizationFilters } from "../filters/authorization-filters/AuthorizationFilters";
3031
import type { InputField } from "../input-fields/InputField";
32+
import { ParamInputField } from "../input-fields/ParamInputField";
3133
import type { SelectionPattern } from "../selection/SelectionPattern/SelectionPattern";
32-
import type { ReadOperation } from "./ReadOperation";
3334
import { MutationOperation, type OperationTranspileResult } from "./operations";
3435

3536
export class DisconnectOperation extends MutationOperation {
@@ -38,6 +39,7 @@ export class DisconnectOperation extends MutationOperation {
3839

3940
private selectionPattern: SelectionPattern;
4041
protected readonly authFilters: AuthorizationFilters[] = [];
42+
protected readonly sourceAuthFilters: AuthorizationFilters[] = [];
4143

4244
public readonly inputFields: Map<string, InputField> = new Map();
4345
private filters: Filter[] = [];
@@ -75,6 +77,9 @@ export class DisconnectOperation extends MutationOperation {
7577
public addAuthFilters(...filter: AuthorizationFilters[]) {
7678
this.authFilters.push(...filter);
7779
}
80+
public addSourceAuthFilters(...filter: AuthorizationFilters[]) {
81+
this.sourceAuthFilters.push(...filter);
82+
}
7883

7984
/**
8085
* Get and set field methods are utilities to remove duplicate fields between separate inputs
@@ -116,6 +121,29 @@ export class DisconnectOperation extends MutationOperation {
116121
const { nestedContext, pattern: matchPattern } = this.selectionPattern.apply(context);
117122
this.nestedContext = nestedContext;
118123

124+
checkEntityAuthentication({
125+
context: nestedContext.neo4jGraphQLContext,
126+
entity: this.target.entity,
127+
targetOperations: ["DELETE_RELATIONSHIP"],
128+
});
129+
if (isConcreteEntity(this.relationship.source)) {
130+
checkEntityAuthentication({
131+
context: nestedContext.neo4jGraphQLContext,
132+
entity: this.relationship.source.entity,
133+
targetOperations: ["DELETE_RELATIONSHIP"],
134+
});
135+
}
136+
this.inputFields.forEach((field) => {
137+
if (field.attachedTo === "node" && field instanceof ParamInputField) {
138+
checkEntityAuthentication({
139+
context: nestedContext.neo4jGraphQLContext,
140+
entity: this.target.entity,
141+
targetOperations: ["DELETE_RELATIONSHIP"],
142+
field: field.name,
143+
});
144+
}
145+
});
146+
119147
const allFilters = [...this.authFilters, ...this.filters];
120148

121149
const filterSubqueries = wrapSubqueriesInCypherCalls(nestedContext, allFilters, [nestedContext.target]);
@@ -124,13 +152,13 @@ export class DisconnectOperation extends MutationOperation {
124152
if (filterSubqueries.length > 0) {
125153
const predicate = Cypher.and(...allFilters.map((f) => f.getPredicate(nestedContext)));
126154
matchClause = Cypher.utils.concat(
127-
new Cypher.Match(matchPattern),
155+
new Cypher.OptionalMatch(matchPattern),
128156
...filterSubqueries,
129157
new Cypher.With("*").where(predicate)
130158
);
131159
} else {
132160
const predicate = Cypher.and(...allFilters.map((f) => f.getPredicate(nestedContext)));
133-
matchClause = new Cypher.Match(matchPattern).where(predicate);
161+
matchClause = new Cypher.OptionalMatch(matchPattern).where(predicate);
134162
}
135163

136164
const relVar = new Cypher.Relationship();
@@ -145,15 +173,33 @@ export class DisconnectOperation extends MutationOperation {
145173

146174
const deleteClause = new Cypher.With(nestedContext.relationship!).delete(nestedContext.relationship!);
147175

148-
const clauses = Cypher.utils.concat(
149-
matchClause,
150-
...this.getAuthorizationClauses(nestedContext), // THESE ARE "BEFORE" AUTH
151-
...mutationSubqueries,
152-
deleteClause,
153-
...this.getAuthorizationClausesAfter(nestedContext) // THESE ARE "AFTER" AUTH
154-
);
176+
const authClausesBefore = this.getAuthorizationClauses(nestedContext);
177+
const sourceAuthClausesBefore = this.getSourceAuthorizationClausesBefore(context);
178+
179+
const bothAuthClausesBefore: Cypher.Clause[] = [];
180+
if (authClausesBefore.length === 0 && sourceAuthClausesBefore.length > 0) {
181+
bothAuthClausesBefore.push(new Cypher.With("*"), ...sourceAuthClausesBefore);
182+
} else {
183+
bothAuthClausesBefore.push(Cypher.utils.concat(...authClausesBefore, ...sourceAuthClausesBefore));
184+
}
185+
186+
const clauses = Cypher.utils.concat(matchClause, ...bothAuthClausesBefore, ...mutationSubqueries, deleteClause);
155187

156-
return { projectionExpr: context.returnVariable, clauses: [clauses] };
188+
const authClausesAfter = this.getAuthorizationClausesAfter(nestedContext);
189+
const sourceAuthClausesAfter = this.getSourceAuthorizationClausesAfter(context);
190+
191+
const callClause = new Cypher.Call(clauses, [context.target]);
192+
const authClauses: Cypher.Clause[] = [];
193+
if (authClausesAfter.length > 0 || sourceAuthClausesAfter.length > 0) {
194+
authClauses.push(Cypher.utils.concat(...authClausesAfter, ...sourceAuthClausesAfter));
195+
}
196+
197+
return {
198+
projectionExpr: context.returnVariable,
199+
clauses: [callClause, ...authClauses],
200+
};
201+
202+
// return { projectionExpr: context.returnVariable, clauses: [clauses] };
157203
}
158204

159205
private getAuthorizationClauses(context: QueryASTContext): Cypher.Clause[] {
@@ -187,6 +233,35 @@ export class DisconnectOperation extends MutationOperation {
187233
return [];
188234
}
189235

236+
private getSourceAuthorizationClausesAfter(context: QueryASTContext): Cypher.Clause[] {
237+
const validationsAfter: Cypher.VoidProcedure[] = [];
238+
for (const authFilter of this.sourceAuthFilters) {
239+
const validationAfter = authFilter.getValidation(context, "AFTER");
240+
if (validationAfter) {
241+
validationsAfter.push(validationAfter);
242+
}
243+
}
244+
245+
if (validationsAfter.length > 0) {
246+
return [new Cypher.With("*"), ...validationsAfter];
247+
}
248+
return [];
249+
}
250+
private getSourceAuthorizationClausesBefore(context: QueryASTContext): Cypher.Clause[] {
251+
const validationsAfter: Cypher.VoidProcedure[] = [];
252+
for (const authFilter of this.sourceAuthFilters) {
253+
const validationAfter = authFilter.getValidation(context, "BEFORE");
254+
if (validationAfter) {
255+
validationsAfter.push(validationAfter);
256+
}
257+
}
258+
259+
if (validationsAfter.length > 0) {
260+
return [new Cypher.With("*"), ...validationsAfter];
261+
}
262+
return [];
263+
}
264+
190265
private transpileAuthClauses(context: QueryASTContext): {
191266
selections: (Cypher.With | Cypher.Match)[];
192267
subqueries: Cypher.Clause[];

0 commit comments

Comments
 (0)