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

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

Merged
merged 5 commits into from
Apr 26, 2023
Merged
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
4 changes: 2 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ subprojects {
// neo4jDockerImage system property is used in TestContainerUtil
systemProperties 'user.language' : 'en' ,
'user.country' : 'US',
'neo4jDockerImage' : System.getProperty("NEO4JVERSION") ? 'neo4j:' + System.getProperty("NEO4JVERSION") + '-enterprise' : 'neo4j:5.8.0-enterprise',
'neo4jDockerImage' : System.getProperty("NEO4JVERSION") ? 'neo4j:' + System.getProperty("NEO4JVERSION") + '-enterprise' : 'neo4j:5.6.0-enterprise',
'neo4jCommunityDockerImage': System.getProperty("NEO4JVERSION") ? 'neo4j:' + System.getProperty("NEO4JVERSION") : 'neo4j:5.8.0',
'coreDir': 'core'

Expand Down Expand Up @@ -128,6 +128,6 @@ subprojects {
}

ext {
neo4jVersionEffective = project.hasProperty("neo4jVersionOverride") ? project.getProperty("neo4jVersionOverride") : "5.8.0"
neo4jVersionEffective = project.hasProperty("neo4jVersionOverride") ? project.getProperty("neo4jVersionOverride") : "5.6.0"
testContainersVersion = '1.17.6'
}
2 changes: 2 additions & 0 deletions common/src/main/java/apoc/ApocExtensionFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,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 @@ -43,6 +44,7 @@ public interface Dependencies {
AvailabilityGuard availabilityGuard();
DatabaseManagementService databaseManagementService();
ApocConfig apocConfig();
DatabaseEventListeners databaseEventListeners();
@SuppressWarnings("unused") // used from extended
GlobalProcedures globalProceduresRegistry();
RegisterComponentFactory.RegisterComponentLifecycle registerComponentLifecycle();
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 @@ -33,6 +33,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())
);
}
}
55 changes: 54 additions & 1 deletion core/src/main/java/apoc/cypher/CypherInitializer.java
Original file line number Diff line number Diff line change
@@ -1,35 +1,52 @@
package apoc.cypher;

import apoc.ApocConfig;
import apoc.SystemLabels;
import apoc.util.Util;
import apoc.util.collection.Iterators;
import apoc.version.Version;
import org.apache.commons.configuration2.Configuration;
import org.apache.commons.lang3.StringUtils;
import org.neo4j.common.DependencyResolver;
import org.neo4j.dbms.api.DatabaseManagementService;
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.Map;
import java.util.TreeMap;
import java.util.function.BiConsumer;

import static apoc.SystemPropertyKeys.database;

public class CypherInitializer implements AvailabilityListener {

private final GraphDatabaseAPI db;
private final Log userLog;
private final DependencyResolver dependencyResolver;
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 volatile 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();
}

Expand Down Expand Up @@ -63,6 +80,9 @@ public void available() {
}
}

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

Configuration config = dependencyResolver.resolveDependency(ApocConfig.class).getConfig();
for (String query : collectInitializers(config)) {
try {
Expand Down Expand Up @@ -116,4 +136,37 @@ private void putIfNotBlank(Map<String,String> map, String key, String value) {
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();
}
}
}
57 changes: 54 additions & 3 deletions it/src/test/java/apoc/it/core/TriggerEnterpriseFeaturesTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,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 @@ -37,6 +39,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 @@ -46,10 +49,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(List.of(TestContainerUtil.ApocPackage.CORE), true)
.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 @@ -150,8 +158,51 @@ public void testTriggerInstallInNewDatabase() {
testCall(defaultDbSession, "MATCH (n:Something) RETURN n.created",
r -> assertNull(r.get("created")));
}
}

}

@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