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
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
@@ -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",
Original file line number Diff line number Diff line change
@@ -86,6 +86,7 @@ function createAuthorizationFilterRule(
type: new GraphQLList(AUTHORIZATION_FILTER_OPERATION),
defaultValue: [
"READ",
"FILTER",
"AGGREGATE",
"UPDATE",
"DELETE",
Original file line number Diff line number Diff line change
@@ -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" },
@@ -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" },
Original file line number Diff line number Diff line change
@@ -22,6 +22,7 @@ import type { Annotation } from "./Annotation";

export type AuthenticationOperation =
| "READ"
| "FILTER"
| "AGGREGATE"
| "CREATE"
| "UPDATE"
Original file line number Diff line number Diff line change
@@ -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",
Original file line number Diff line number Diff line change
@@ -24,6 +24,7 @@ import { parseArgumentsFromUnknownDirective } from "../parse-arguments";

const authenticationDefaultOperations: AuthenticationOperation[] = [
"READ",
"FILTER",
"AGGREGATE",
"CREATE",
"UPDATE",
Original file line number Diff line number Diff line change
@@ -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";
@@ -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[] = [];
@@ -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,
@@ -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];
}
@@ -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);
}
@@ -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) {
@@ -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)
@@ -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);
@@ -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:
@@ -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