Skip to content

Change test setup to use user with restricted permission #6306

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

Draft
wants to merge 6 commits into
base: dev
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
88 changes: 81 additions & 7 deletions packages/graphql/jest.global-setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,29 @@ const neo4j = require("neo4j-driver");

const TZ = "Etc/UTC";
const INT_TEST_DB_NAME = "neo4jgraphqlinttestdatabase";
const INT_TEST_ROLE_NAME = "neo4jgraphqlinttestrole";
const INT_TEST_USER_NAME = "neo4jgraphqlinttestuser";

const cypherDropData = `MATCH (n) DETACH DELETE n`;
const cypherDropIndexes = `CALL apoc.schema.assert({},{},true) YIELD label, key RETURN *`;

const cypherCreateUser = `CREATE USER ${INT_TEST_USER_NAME} IF NOT EXISTS SET PASSWORD 'password' CHANGE NOT REQUIRED`;
const cypherCreateRole = `CREATE ROLE ${INT_TEST_ROLE_NAME} IF NOT EXISTS`;
const cypherGrantRole = `GRANT ROLE ${INT_TEST_ROLE_NAME} TO ${INT_TEST_USER_NAME}`;


const cypherDropUser = `DROP USER ${INT_TEST_USER_NAME}`;
const cypherDropRole = `DROP ROLE ${INT_TEST_ROLE_NAME} IF EXISTS`;


module.exports = async function globalSetup() {
process.env.NODE_ENV = "test";

setTZ(TZ);

// INFO: The 'global' object can only be accessed in globalSetup and globalTeardown.
global.INT_TEST_DB_NAME = INT_TEST_DB_NAME;

const { NEO_USER = "neo4j", NEO_PASSWORD = "password", NEO_URL = "neo4j://localhost:7687/neo4j" } = process.env;
const auth = neo4j.auth.basic(NEO_USER, NEO_PASSWORD);
const driver = neo4j.driver(NEO_URL, auth);
Expand All @@ -32,11 +43,11 @@ module.exports = async function globalSetup() {
await session.run(cypherCreateDb);
} catch (error) {
if (
error.message.includes(
"This is an administration command and it should be executed against the system database"
) ||
error.message.includes("This is an administration command and it should be executed against the system database") ||
error.message.includes("Unsupported administration command") ||
error.message.includes("Unable to route write operation to leader for database 'system'")
error.message.includes("Unable to route write operation to leader for database 'system'") ||
error.message.includes("CREATE DATABASE is not supported") ||
error.message.includes("DROP DATABASE is not supported")
) {
console.log(
`\nJest /packages/graphql setup: Will NOT create a separate integration test database as the command is not supported in the current environment.`
Expand All @@ -48,12 +59,75 @@ module.exports = async function globalSetup() {
await dropDataAndIndexes(session);
}
} finally {
if (session) await session.close();
if (driver) await driver.close();
if (session) {
await session.close();
}
}

// Some tests use different DBs, so using "*" for now
const dbName = "*"
// GRANTS READ/WRITE SERVICE ACCOUNT
const readWriteGrants = [
`GRANT ACCESS ON DATABASE * TO ${INT_TEST_ROLE_NAME}`,
`GRANT SHOW CONSTRAINT ON DATABASE ${dbName} TO ${INT_TEST_ROLE_NAME}`,
`GRANT SHOW INDEX ON DATABASE ${dbName} TO ${INT_TEST_ROLE_NAME}`,
`GRANT MATCH {*} ON GRAPH ${dbName} TO ${INT_TEST_ROLE_NAME}`,
`GRANT EXECUTE PROCEDURE * ON DBMS TO ${INT_TEST_ROLE_NAME}`,
`GRANT EXECUTE FUNCTION * ON DBMS TO ${INT_TEST_ROLE_NAME}`,
`GRANT WRITE ON GRAPH ${dbName} TO ${INT_TEST_ROLE_NAME}`,
`GRANT NAME MANAGEMENT ON DATABASE ${dbName} TO ${INT_TEST_ROLE_NAME}`,
];

try {
session = driver.session();
await dropUserAndRole(session);
} catch (error) {
if (error.gqlStatus === "50N42") {
console.log(`\nJest /packages/graphql setup: Failure to drop test user/role, this is expected if the user/role does not exist. Error: ${error.message}`);
}
} finally {
if (session) {
await session.close();
}
}

try {
session = driver.session();
await createUserAndRole(session);

for (const cypherGrant of readWriteGrants) {
await session.run(cypherGrant);
}
} catch (error) {
if (error.gqlStatus === "42NFF") {
console.log(`\nJest /packages/graphql setup: Will NOT create a separate integration test user and role as the command is not supported in the current environment.`);
} else {
throw error;
}

} finally {
if (session) {
await session.close();
}
if (driver) {
await driver.close();
}
}
};


async function dropDataAndIndexes(session) {
await session.run(cypherDropData);
await session.run(cypherDropIndexes);
}

async function dropUserAndRole(session) {
await session.run(cypherDropUser);
await session.run(cypherDropRole);
}

async function createUserAndRole(session) {
await session.run(cypherCreateUser);
await session.run(cypherCreateRole);
await session.run(cypherGrantRole);
}
48 changes: 39 additions & 9 deletions packages/graphql/tests/utils/tests-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,21 @@ import * as neo4j from "neo4j-driver";
import type { Neo4jGraphQLConstructor, Neo4jGraphQLContext } from "../../src";
import { Neo4jGraphQL, Neo4jGraphQLSubscriptionsCDCEngine } from "../../src";
import { Neo4jDatabaseInfo } from "../../src/classes";
import { Neo4jGraphQLSessionConfig } from "../../src/classes/Executor";
import type { Neo4jEdition } from "../../src/classes/Neo4jDatabaseInfo";
import { createBearerToken } from "./create-bearer-token";
import { UniqueType } from "./graphql-types";

const INT_TEST_DB_NAME = "neo4jgraphqlinttestdatabase";
const DEFAULT_DB = "neo4j";
const readWriteUser = "neo4jgraphqlinttestuser";

export class TestHelper {
private _database: string = DEFAULT_DB;
private neo4jGraphQL: Neo4jGraphQL | undefined;
private uniqueTypes: UniqueType[] = [];
private driver: neo4j.Driver | undefined;
private _useRestrictedUser: boolean = false;

private lock: boolean = false; // Lock to avoid race condition between initNeo4jGraphQL

Expand Down Expand Up @@ -91,7 +94,9 @@ export class TestHelper {
throw new Error("Neo4jGraphQL already initialized. Did you forget calling .close()?");
}
this.lock = true;

const driver = await this.getDriver();

this.neo4jGraphQL = new Neo4jGraphQL({
...options,
driver,
Expand All @@ -116,7 +121,7 @@ export class TestHelper {
SHOW DATABASES YIELD name, options
WHERE name = "${this.database}"
RETURN coalesce(options.txLogEnrichment = "FULL", false) AS cdcEnabled
`);
`);

return result.records[0]?.get("cdcEnabled");
}
Expand Down Expand Up @@ -148,7 +153,10 @@ export class TestHelper {
schema,
...args,
source: query,
contextValue: await this.getContextValue(args.contextValue as Partial<Neo4jGraphQLContext> | undefined),
contextValue: await this.getContextValue(
args.contextValue as Partial<Neo4jGraphQLContext> | undefined,
true
),
});
}

Expand Down Expand Up @@ -183,11 +191,21 @@ export class TestHelper {
}

/** Use this if using graphql() directly. If possible, use .runGraphQL */
public async getContextValue(options?: Record<string, unknown>): Promise<Neo4jGraphQLContext> {
public async getContextValue(
options?: Record<string, unknown>,
useRestrictedUser?: boolean
): Promise<Neo4jGraphQLContext> {
const driver = await this.getDriver();

const sessionConfig: Neo4jGraphQLSessionConfig = {
database: this.database,
};
if (useRestrictedUser && this._useRestrictedUser) {
sessionConfig.impersonatedUser = readWriteUser;
}
return {
executionContext: driver,
sessionConfig: { database: this.database },
sessionConfig,
...(options || {}),
};
}
Expand All @@ -198,14 +216,12 @@ export class TestHelper {
}
const { NEO_USER = "neo4j", NEO_PASSWORD = "password", NEO_URL = "neo4j://localhost:7687/neo4j" } = process.env;

// if (process.env.NEO_WAIT) {
// await util.promisify(setTimeout)(Number(process.env.NEO_WAIT));
// }

const auth = neo4j.auth.basic(NEO_USER, NEO_PASSWORD);
const driver = neo4j.driver(NEO_URL, auth);

try {
this._database = await this.checkConnectivity(driver);
this._useRestrictedUser = await this.checkIfUseRestrictedUser(driver);
} catch (error: any) {
await driver.close();
throw new Error(`Could not connect to neo4j @ ${NEO_URL}, Error: ${error.message}`);
Expand All @@ -221,7 +237,6 @@ export class TestHelper {
* */
public async getSession(options?: Record<string, unknown>): Promise<neo4j.Session> {
const driver = await this.getDriver();

const appliedOptions = { ...options, database: this.database };
return driver.session(appliedOptions);
}
Expand Down Expand Up @@ -341,4 +356,19 @@ export class TestHelper {

await driver.executeQuery(cypher, {}, { database: this.database });
}

/** Check if it is possible to impersonate a restricted user, so that executeGraphQL can be executed with limited grants */
private async checkIfUseRestrictedUser(driver: neo4j.Driver): Promise<boolean> {
// Should we do add a warning when we're not using a restricted user?
if (!(await driver.supportsUserImpersonation())) {
return false;
}
try {
await driver.session({ database: this.database, impersonatedUser: readWriteUser }).run("RETURN 1");
} catch (error: any) {
return false;
}

return true;
}
}
Loading