Skip to content

Commit

Permalink
[madSHaLz] APOC triggers aren't updated after a user deletes a databa…
Browse files Browse the repository at this point in the history
…se (neo4j/apoc#348)

* [madSHaLz] APOC triggers aren't updated after a user deletes a database

* [madSHaLz] changes review

* [madSHaLz] removed no longer valid test

* [madSHaLz] split test

---------

Co-authored-by: Nacho Cordón <[email protected]>
  • Loading branch information
vga91 and ncordon committed May 5, 2023
1 parent a909f15 commit 4b0c3f0
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 3 deletions.
2 changes: 2 additions & 0 deletions core/src/main/java/apoc/ApocExtensionFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import org.neo4j.kernel.internal.GraphDatabaseAPI;
import org.neo4j.kernel.lifecycle.Lifecycle;
import org.neo4j.kernel.lifecycle.LifecycleAdapter;
import org.neo4j.kernel.monitoring.DatabaseEventListeners;
import org.neo4j.logging.Log;
import org.neo4j.logging.internal.LogService;
import org.neo4j.scheduler.JobScheduler;
Expand Down Expand Up @@ -62,6 +63,7 @@ public interface Dependencies {
LogService log();
AvailabilityGuard availabilityGuard();
DatabaseManagementService databaseManagementService();
DatabaseEventListeners databaseEventListeners();
ApocConfig apocConfig();
TTLConfig ttlConfig();
GlobalProcedures globalProceduresRegistry();
Expand Down
6 changes: 5 additions & 1 deletion core/src/main/java/apoc/CoreApocGlobalComponents.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ public Collection<Class> getContextClasses() {

@Override
public Iterable<AvailabilityListener> getListeners(GraphDatabaseAPI db, ApocExtensionFactory.Dependencies dependencies) {
return Collections.singleton(new CypherInitializer(db, dependencies.log().getUserLog(CypherInitializer.class)));
return Collections.singleton(new CypherInitializer(db,
dependencies.log().getUserLog(CypherInitializer.class),
dependencies.databaseManagementService(),
dependencies.databaseEventListeners())
);
}
}
56 changes: 55 additions & 1 deletion core/src/main/java/apoc/cypher/CypherInitializer.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,26 +19,37 @@
package apoc.cypher;

import apoc.ApocConfig;
import apoc.SystemLabels;
import apoc.util.Util;
import apoc.version.Version;
import org.apache.commons.configuration2.Configuration;
import org.apache.commons.lang3.StringUtils;
import org.neo4j.common.DependencyResolver;
import org.neo4j.configuration.Config;
import org.neo4j.configuration.GraphDatabaseSettings;
import org.neo4j.dbms.api.DatabaseManagementService;
import org.neo4j.dbms.database.SystemGraphComponent.Status;
import org.neo4j.dbms.database.SystemGraphComponents;
import org.neo4j.internal.helpers.collection.Iterators;
import org.neo4j.kernel.api.procedure.GlobalProcedures;
import org.neo4j.graphdb.Label;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.Transaction;
import org.neo4j.graphdb.event.DatabaseEventContext;
import org.neo4j.graphdb.event.DatabaseEventListener;
import org.neo4j.kernel.availability.AvailabilityListener;
import org.neo4j.kernel.internal.GraphDatabaseAPI;
import org.neo4j.kernel.monitoring.DatabaseEventListeners;
import org.neo4j.logging.Log;

import java.util.Collection;

import java.util.ConcurrentModificationException;
import java.util.Map;
import java.util.TreeMap;
import java.util.function.BiConsumer;

import static apoc.SystemPropertyKeys.database;
import java.util.function.BooleanSupplier;


Expand All @@ -49,15 +60,22 @@ public class CypherInitializer implements AvailabilityListener {
private final GlobalProcedures procs;
private final DependencyResolver dependencyResolver;
private final String defaultDb;
private final DatabaseManagementService databaseManagementService;
private final DatabaseEventListeners databaseEventListeners;


/**
* indicates the status of the initializer, to be used for tests to ensure initializer operations are already done
*/
private boolean finished = false;

public CypherInitializer(GraphDatabaseAPI db, Log userLog) {
public CypherInitializer(GraphDatabaseAPI db, Log userLog,
DatabaseManagementService databaseManagementService,
DatabaseEventListeners databaseEventListeners) {
this.db = db;
this.userLog = userLog;
this.databaseManagementService = databaseManagementService;
this.databaseEventListeners = databaseEventListeners;
this.dependencyResolver = db.getDependencyResolver();
this.procs = dependencyResolver.resolveDependency(GlobalProcedures.class);
this.defaultDb = dependencyResolver.resolveDependency(Config.class).get(GraphDatabaseSettings.default_database);
Expand Down Expand Up @@ -142,6 +160,9 @@ public void available() {
}
}

// create listener for each database
databaseEventListeners.registerDatabaseEventListener(new SystemFunctionalityListener());

for (String query : initializers) {
try {
// we need to apply a retry strategy here since in systemdb we potentially conflict with
Expand Down Expand Up @@ -216,4 +237,37 @@ private boolean areProceduresRegistered(String procStart) {
public void unavailable() {
// intentionally empty
}

private class SystemFunctionalityListener implements DatabaseEventListener {

@Override
public void databaseDrop(DatabaseEventContext eventContext) {

forEachSystemLabel((tx, label) -> {
tx.findNodes(label, database.name(), eventContext.getDatabaseName())
.forEachRemaining(Node::delete);
});
}

@Override
public void databaseStart(DatabaseEventContext eventContext) {}

@Override
public void databaseShutdown(DatabaseEventContext eventContext) {}

@Override
public void databasePanic(DatabaseEventContext eventContext) {}

@Override
public void databaseCreate(DatabaseEventContext eventContext) {}
}

private void forEachSystemLabel(BiConsumer<Transaction, Label> consumer) {
try (Transaction tx = db.beginTx()) {
for (Label label: SystemLabels.values()) {
consumer.accept(tx, label);
}
tx.commit();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,14 @@
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;

import static apoc.ApocConfig.APOC_CONFIG_INITIALIZER;
import static apoc.ApocConfig.APOC_TRIGGER_ENABLED;
import static apoc.trigger.TriggerHandler.TRIGGER_REFRESH;
import static apoc.trigger.TriggerTestUtil.TIMEOUT;
import static apoc.trigger.TriggerTestUtil.TRIGGER_DEFAULT_REFRESH;
import static apoc.util.TestContainerUtil.createEnterpriseDB;
import static apoc.util.TestContainerUtil.testCall;
import static apoc.util.TestContainerUtil.testCallEmpty;
import static apoc.util.TestContainerUtil.testResult;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
Expand All @@ -55,6 +57,7 @@

public class TriggerEnterpriseFeaturesTest {
private static final String FOO_DB = "foo";
private static final String INIT_DB = "initdb";

private static final String NO_ADMIN_USER = "nonadmin";
private static final String NO_ADMIN_PWD = "test1234";
Expand All @@ -64,10 +67,15 @@ public class TriggerEnterpriseFeaturesTest {

@BeforeClass
public static void beforeAll() {
final String cypherInitializer = String.format("%s.%s.0",
APOC_CONFIG_INITIALIZER, SYSTEM_DATABASE_NAME);
final String createInitDb = String.format("CREATE DATABASE %s IF NOT EXISTS", INIT_DB);

// We build the project, the artifact will be placed into ./build/libs
neo4jContainer = createEnterpriseDB(!TestUtil.isRunningInCI())
.withEnv(APOC_TRIGGER_ENABLED, "true")
.withEnv(TRIGGER_REFRESH, String.valueOf(TRIGGER_DEFAULT_REFRESH));
.withEnv(TRIGGER_REFRESH, String.valueOf(TRIGGER_DEFAULT_REFRESH))
.withEnv(cypherInitializer, createInitDb);
neo4jContainer.start();
session = neo4jContainer.getSession();

Expand Down Expand Up @@ -170,6 +178,49 @@ public void testTriggerInstallInNewDatabase() {
}
}

@Test
public void testDeleteTriggerAfterDatabaseDeletion() {
try (Session sysSession = neo4jContainer.getDriver().session(forDatabase(SYSTEM_DATABASE_NAME))) {
final String dbToDelete = "todelete";

// create database with name `todelete`
sysSession.writeTransaction(tx -> tx.run(String.format("CREATE DATABASE %s WAIT;", dbToDelete)));

testDeleteTriggerAfterDropDb(dbToDelete, sysSession);
}
}

@Test
public void testDeleteTriggerAfterDatabaseDeletionCreatedViaCypherInit() {
try (Session sysSession = neo4jContainer.getDriver().session(forDatabase(SYSTEM_DATABASE_NAME))) {
// the database `initDb` is created via `apoc.initializer.*`
testDeleteTriggerAfterDropDb(INIT_DB, sysSession);
}
}

private static void testDeleteTriggerAfterDropDb(String dbToDelete, Session sysSession) {
final String defaultTriggerName = UUID.randomUUID().toString();

// install and show a trigger in the database and check existence
testCall(sysSession, "CALL apoc.trigger.install($dbName, $name, 'return 1', {})",
Map.of("dbName", dbToDelete, "name", defaultTriggerName),
r -> assertEquals(defaultTriggerName, r.get("name"))
);

testCall(sysSession, "CALL apoc.trigger.show($dbName)",
Map.of("dbName", dbToDelete),
r -> assertEquals(defaultTriggerName, r.get("name"))
);

// drop database
sysSession.writeTransaction(tx -> tx.run(String.format("DROP DATABASE %s WAIT;", dbToDelete)));

// check that the trigger has been removed
testCallEmpty(sysSession, "CALL apoc.trigger.show($dbName)",
Map.of("dbName", dbToDelete)
);
}

@Test
public void testTriggersAllowedOnlyWithAdmin() {

Expand Down

0 comments on commit 4b0c3f0

Please sign in to comment.