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

Authorization checks for targets of relationship filters #5962

Draft
wants to merge 16 commits into
base: 7.x
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/strong-cameras-heal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@neo4j/graphql": major
---

Authorization for filters
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@

import { astFromDirective } from "@graphql-tools/utils";
import type { DirectiveDefinitionNode } from "graphql";
import { GraphQLString, GraphQLDirective, GraphQLInputObjectType, GraphQLList, DirectiveLocation } from "graphql";
import { DirectiveLocation, GraphQLDirective, GraphQLInputObjectType, GraphQLList, GraphQLString } from "graphql";
import { AUTHENTICATION_OPERATION } from "./static-definitions";

const authenticationDefaultOperations = [
"READ",
"FILTER",
"AGGREGATE",
"CREATE",
"UPDATE",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ function createAuthorizationFilterRule(
type: new GraphQLList(AUTHORIZATION_FILTER_OPERATION),
defaultValue: [
"READ",
"FILTER",
"AGGREGATE",
"UPDATE",
"DELETE",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export const AUTHORIZATION_FILTER_OPERATION = new GraphQLEnumType({
name: "AuthorizationFilterOperation",
values: {
READ: { value: "READ" },
FILTER: { value: "FILTER" },
AGGREGATE: { value: "AGGREGATE" },
UPDATE: { value: "UPDATE" },
DELETE: { value: "DELETE" },
Expand All @@ -54,6 +55,7 @@ export const AUTHENTICATION_OPERATION = new GraphQLEnumType({
values: {
CREATE: { value: "CREATE" },
READ: { value: "READ" },
FILTER: { value: "FILTER" },
AGGREGATE: { value: "AGGREGATE" },
UPDATE: { value: "UPDATE" },
DELETE: { value: "DELETE" },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import type { Annotation } from "./Annotation";

export type AuthenticationOperation =
| "READ"
| "FILTER"
| "AGGREGATE"
| "CREATE"
| "UPDATE"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,14 @@
*/

import type { GraphQLWhereArg } from "../../types";
import type { Annotation } from "./Annotation";
import type { ValueOf } from "../../utils/value-of";
import type { Annotation } from "./Annotation";

export const AuthorizationAnnotationArguments = ["filter", "validate"] as const;

export const AuthorizationFilterOperationRule = [
"READ",
"FILTER",
"AGGREGATE",
"UPDATE",
"DELETE",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { parseArgumentsFromUnknownDirective } from "../parse-arguments";

const authenticationDefaultOperations: AuthenticationOperation[] = [
"READ",
"FILTER",
"AGGREGATE",
"CREATE",
"UPDATE",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import Cypher from "@neo4j/cypher-builder";
import type { ConcreteEntityAdapter } from "../../../../schema-model/entity/model-adapters/ConcreteEntityAdapter";
import type { InterfaceEntityAdapter } from "../../../../schema-model/entity/model-adapters/InterfaceEntityAdapter";
import type { RelationshipAdapter } from "../../../../schema-model/relationship/model-adapters/RelationshipAdapter";
import { filterTruthy } from "../../../../utils/utils";
import { hasTarget } from "../../utils/context-has-target";
import { getEntityLabels } from "../../utils/create-node-from-entity";
import { isConcreteEntity } from "../../utils/is-concrete-entity";
Expand All @@ -29,6 +30,7 @@ import type { QueryASTContext } from "../QueryASTContext";
import type { QueryASTNode } from "../QueryASTNode";
import type { RelationshipWhereOperator } from "./Filter";
import { Filter } from "./Filter";
import type { AuthorizationFilters } from "./authorization-filters/AuthorizationFilters";

export class ConnectionFilter extends Filter {
protected innerFilters: Filter[] = [];
Expand All @@ -39,6 +41,8 @@ export class ConnectionFilter extends Filter {
// as subqueries and store them
protected subqueryPredicate: Cypher.Predicate | undefined;

private authFilters: Record<string, AuthorizationFilters[]> = {};

constructor({
relationship,
target,
Expand All @@ -58,6 +62,10 @@ export class ConnectionFilter extends Filter {
this.innerFilters.push(...filters);
}

public addAuthFilters(name: string, ...filter: AuthorizationFilters[]) {
this.authFilters[name] = filter;
}

public getChildren(): QueryASTNode[] {
return [...this.innerFilters];
}
Expand Down Expand Up @@ -132,15 +140,20 @@ export class ConnectionFilter extends Filter {
* }
* RETURN this { .name } AS this
**/
protected getLabelPredicate(context: QueryASTContext): Cypher.Predicate | undefined {
protected getLabelAndAuthorizationPredicate(context: QueryASTContext): Cypher.Predicate | undefined {
if (!hasTarget(context)) {
throw new Error("No parent node found!");
}
if (isConcreteEntity(this.target)) {
const authFilterPredicate = this.getAuthFilterPredicate(this.target.name, context);
if (authFilterPredicate.length) {
return Cypher.and(...authFilterPredicate);
}
return;
}
const labelPredicate = this.target.concreteEntities.map((e) => {
return context.target.hasLabels(...e.labels);
const authFilterPredicate = this.getAuthFilterPredicate(e.name, context);
return Cypher.and(context.target.hasLabels(...e.getLabels()), ...authFilterPredicate);
});
return Cypher.or(...labelPredicate);
}
Expand All @@ -150,7 +163,7 @@ export class ConnectionFilter extends Filter {
queryASTContext: QueryASTContext
): Cypher.Predicate | undefined {
const connectionFilter = this.innerFilters.map((c) => c.getPredicate(queryASTContext));
const labelPredicate = this.getLabelPredicate(queryASTContext);
const labelPredicate = this.getLabelAndAuthorizationPredicate(queryASTContext);
const innerPredicate = Cypher.and(...connectionFilter, labelPredicate);

if (!innerPredicate) {
Expand Down Expand Up @@ -203,6 +216,10 @@ export class ConnectionFilter extends Filter {
const returnVar = new Cypher.Variable();
const innerFiltersPredicates: Cypher.Predicate[] = [];

const authFilterSubqueries = this.getAuthFilterSubqueries(this.target.name, queryASTContext).map((sq) =>
new Cypher.Call(sq).importWith(queryASTContext.target)
);

const subqueries = this.innerFilters.flatMap((f) => {
const nestedSubqueries = f
.getSubqueries(queryASTContext)
Expand All @@ -218,7 +235,7 @@ export class ConnectionFilter extends Filter {
return clauses;
});

if (subqueries.length === 0) return []; // Hack logic to change predicates logic
// if (subqueries.length === 0) return []; // Hack logic to change predicates logic

const comparisonValue = this.operator === "NONE" ? Cypher.false : Cypher.true;
this.subqueryPredicate = Cypher.eq(returnVar, comparisonValue);
Expand All @@ -231,7 +248,7 @@ export class ConnectionFilter extends Filter {
const withPredicateReturn = new Cypher.With("*")
.where(Cypher.and(...innerFiltersPredicates))
.return([countComparisonPredicate, returnVar]);
return [Cypher.utils.concat(match, ...subqueries, withPredicateReturn)];
return [Cypher.utils.concat(match, ...authFilterSubqueries, ...subqueries, withPredicateReturn)];
}

// This method has a big deal of complexity due to a couple of factors:
Expand Down Expand Up @@ -291,4 +308,18 @@ export class ConnectionFilter extends Filter {

return [Cypher.utils.concat(match, ...subqueries), Cypher.utils.concat(match2, ...subqueries2)];
}

private getAuthFilterPredicate(name: string, context: QueryASTContext): Cypher.Predicate[] {
const authFilters = this.authFilters[name];
if (!authFilters) return [];

return filterTruthy(authFilters.map((f) => f.getPredicate(context)));
}

protected getAuthFilterSubqueries(name: string, context: QueryASTContext): Cypher.Clause[] {
const authFilters = this.authFilters[name];
if (!authFilters) return [];

return filterTruthy(authFilters.flatMap((f) => f.getSubqueries(context)));
}
}
Loading
Loading