#
tokens: 44204/50000 10/140 files (page 3/6)
lines: off (toggle) GitHub
raw markdown copy
This is page 3 of 6. Use http://codebase.md/chillbruhhh/crawl4ai-mcp?page={x} to view the full context.

# Directory Structure

```
├── .dockerignore
├── .env.example
├── .gitattributes
├── .gitignore
├── crawled_pages.sql
├── Dockerfile
├── knowledge_graphs
│   ├── ai_hallucination_detector.py
│   ├── ai_script_analyzer.py
│   ├── hallucination_reporter.py
│   ├── knowledge_graph_validator.py
│   ├── parse_repo_into_neo4j.py
│   ├── query_knowledge_graph.py
│   └── test_script.py
├── LICENSE
├── neo4j
│   └── docker-neo4j
│       ├── .github
│       │   └── ISSUE_TEMPLATE
│       │       └── bug_report.md
│       ├── .gitignore
│       ├── build-docker-image.sh
│       ├── build-utils-common-functions.sh
│       ├── COPYRIGHT
│       ├── DEVELOPMENT.md
│       ├── devenv
│       ├── devenv.local.template
│       ├── docker-image-src
│       │   ├── 2.3
│       │   │   ├── docker-entrypoint.sh
│       │   │   └── Dockerfile
│       │   ├── 3.0
│       │   │   ├── docker-entrypoint.sh
│       │   │   └── Dockerfile
│       │   ├── 3.1
│       │   │   ├── docker-entrypoint.sh
│       │   │   └── Dockerfile
│       │   ├── 3.2
│       │   │   ├── docker-entrypoint.sh
│       │   │   └── Dockerfile
│       │   ├── 3.3
│       │   │   ├── docker-entrypoint.sh
│       │   │   └── Dockerfile
│       │   ├── 3.4
│       │   │   ├── docker-entrypoint.sh
│       │   │   └── Dockerfile
│       │   ├── 3.5
│       │   │   ├── coredb
│       │   │   │   ├── docker-entrypoint.sh
│       │   │   │   ├── Dockerfile
│       │   │   │   └── neo4j-plugins.json
│       │   │   └── neo4j-admin
│       │   │       ├── docker-entrypoint.sh
│       │   │       └── Dockerfile
│       │   ├── 4.0
│       │   │   ├── coredb
│       │   │   │   ├── docker-entrypoint.sh
│       │   │   │   └── Dockerfile
│       │   │   └── neo4j-admin
│       │   │       ├── docker-entrypoint.sh
│       │   │       └── Dockerfile
│       │   ├── 4.1
│       │   │   ├── coredb
│       │   │   │   ├── docker-entrypoint.sh
│       │   │   │   └── Dockerfile
│       │   │   └── neo4j-admin
│       │   │       ├── docker-entrypoint.sh
│       │   │       └── Dockerfile
│       │   ├── 4.2
│       │   │   ├── coredb
│       │   │   │   ├── docker-entrypoint.sh
│       │   │   │   ├── Dockerfile
│       │   │   │   └── neo4j-plugins.json
│       │   │   └── neo4j-admin
│       │   │       ├── docker-entrypoint.sh
│       │   │       └── Dockerfile
│       │   ├── 4.3
│       │   │   ├── coredb
│       │   │   │   ├── docker-entrypoint.sh
│       │   │   │   ├── Dockerfile
│       │   │   │   └── neo4j-plugins.json
│       │   │   └── neo4j-admin
│       │   │       ├── docker-entrypoint.sh
│       │   │       └── Dockerfile
│       │   ├── 4.4
│       │   │   ├── coredb
│       │   │   │   ├── docker-entrypoint.sh
│       │   │   │   ├── Dockerfile-debian
│       │   │   │   ├── Dockerfile-ubi9
│       │   │   │   ├── neo4j-admin-report.sh
│       │   │   │   └── neo4j-plugins.json
│       │   │   └── neo4j-admin
│       │   │       ├── docker-entrypoint.sh
│       │   │       ├── Dockerfile-debian
│       │   │       └── Dockerfile-ubi9
│       │   ├── 5
│       │   │   ├── coredb
│       │   │   │   ├── docker-entrypoint.sh
│       │   │   │   ├── Dockerfile-debian
│       │   │   │   ├── Dockerfile-ubi8
│       │   │   │   ├── Dockerfile-ubi9
│       │   │   │   ├── neo4j-admin-report.sh
│       │   │   │   └── neo4j-plugins.json
│       │   │   └── neo4j-admin
│       │   │       ├── docker-entrypoint.sh
│       │   │       ├── Dockerfile-debian
│       │   │       ├── Dockerfile-ubi8
│       │   │       └── Dockerfile-ubi9
│       │   ├── calver
│       │   │   ├── coredb
│       │   │   │   ├── docker-entrypoint.sh
│       │   │   │   ├── Dockerfile-debian
│       │   │   │   ├── Dockerfile-ubi9
│       │   │   │   ├── neo4j-admin-report.sh
│       │   │   │   └── neo4j-plugins.json
│       │   │   └── neo4j-admin
│       │   │       ├── docker-entrypoint.sh
│       │   │       ├── Dockerfile-debian
│       │   │       └── Dockerfile-ubi9
│       │   └── common
│       │       ├── semver.jq
│       │       └── utilities.sh
│       ├── generate-stub-plugin
│       │   ├── build.gradle.kts
│       │   ├── Dockerfile
│       │   ├── ExampleNeo4jPlugin.java
│       │   ├── Makefile
│       │   ├── README.md
│       │   └── settings.gradle.kts
│       ├── LICENSE
│       ├── Makefile
│       ├── pom.xml
│       ├── publish-neo4j-admin-image.sh
│       ├── publish-neo4j-admin-images.sh
│       ├── README.md
│       └── src
│           ├── main
│           │   └── resources
│           │       └── log4j.properties
│           └── test
│               ├── java
│               │   └── com
│               │       └── neo4j
│               │           └── docker
│               │               ├── coredb
│               │               │   ├── configurations
│               │               │   │   ├── Configuration.java
│               │               │   │   ├── Setting.java
│               │               │   │   ├── TestConfSettings.java
│               │               │   │   ├── TestExtendedConf.java
│               │               │   │   └── TestJVMAdditionalConfig.java
│               │               │   ├── plugins
│               │               │   │   ├── Neo4jPluginEnv.java
│               │               │   │   ├── StubPluginHelper.java
│               │               │   │   ├── TestBundledPluginInstallation.java
│               │               │   │   ├── TestPluginInstallation.java
│               │               │   │   └── TestSemVerPluginMatching.java
│               │               │   ├── TestAdminReport.java
│               │               │   ├── TestAuthentication.java
│               │               │   ├── TestBasic.java
│               │               │   ├── TestCausalCluster.java
│               │               │   ├── TestMounting.java
│               │               │   └── TestUpgrade.java
│               │               ├── neo4jadmin
│               │               │   ├── TestAdminBasic.java
│               │               │   ├── TestBackupRestore.java
│               │               │   ├── TestBackupRestore44.java
│               │               │   ├── TestDumpLoad.java
│               │               │   ├── TestDumpLoad44.java
│               │               │   └── TestReport.java
│               │               ├── TestDeprecationWarning.java
│               │               ├── TestDockerComposeSecrets.java
│               │               └── utils
│               │                   ├── DatabaseIO.java
│               │                   ├── HostFileHttpHandler.java
│               │                   ├── HttpServerTestExtension.java
│               │                   ├── Neo4jVersion.java
│               │                   ├── Neo4jVersionTest.java
│               │                   ├── Network.java
│               │                   ├── SetContainerUser.java
│               │                   ├── TemporaryFolderManager.java
│               │                   ├── TemporaryFolderManagerTest.java
│               │                   ├── TestSettings.java
│               │                   └── WaitStrategies.java
│               └── resources
│                   ├── causal-cluster-compose.yml
│                   ├── confs
│                   │   ├── before50
│                   │   │   ├── ConfsNotOverridden.conf
│                   │   │   ├── ConfsReplaced.conf
│                   │   │   ├── EnterpriseOnlyNotOverwritten.conf
│                   │   │   ├── EnvVarsOverride.conf
│                   │   │   ├── ExtendedConf.conf
│                   │   │   ├── InvalidExtendedConf.conf
│                   │   │   ├── JvmAdditionalNotOverridden.conf
│                   │   │   ├── NoNewline.conf
│                   │   │   └── ReadConf.conf
│                   │   ├── ConfsNotOverridden.conf
│                   │   ├── ConfsReplaced.conf
│                   │   ├── EnterpriseOnlyNotOverwritten.conf
│                   │   ├── EnvVarsOverride.conf
│                   │   ├── ExtendedConf.conf
│                   │   ├── InvalidExtendedConf.conf
│                   │   ├── JvmAdditionalNotOverridden.conf
│                   │   ├── NoNewline.conf
│                   │   └── ReadConf.conf
│                   ├── dockersecrets
│                   │   ├── container-compose-with-incorrect-secrets.yml
│                   │   ├── container-compose-with-secrets-override.yml
│                   │   ├── container-compose-with-secrets.yml
│                   │   ├── simple-container-compose-with-external-file-var.yml
│                   │   └── simple-container-compose.yml
│                   ├── ha-cluster-compose.yml
│                   └── stubplugin
│                       └── myPlugin.jar
├── pyproject.toml
├── README.md
├── src
│   ├── crawl4ai_mcp.py
│   └── utils.py
└── uv.lock
```

# Files

--------------------------------------------------------------------------------
/neo4j/docker-neo4j/src/test/java/com/neo4j/docker/utils/TemporaryFolderManager.java:
--------------------------------------------------------------------------------

```java
package com.neo4j.docker.utils;

import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream;
import org.apache.commons.io.FileUtils;
import org.junit.jupiter.api.extension.AfterAllCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testcontainers.containers.BindMode;
import org.testcontainers.containers.Container;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.shaded.org.apache.commons.io.IOUtils;
import org.testcontainers.utility.DockerImageName;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.stream.Collectors;

/**JUnit extension to create temporary folders and compress them after each test class runs.
 * <p>
 * <h2>WHY</h2>
 * Starting a clean neo4j pre-allocates 500MB of space for the data folder. With all these docker tests
 * this ends up allocating a huge amount of empty space that fills the test machine memory.
 * There are enough unit tests now, that we frequently get test failures just because
 * the machine running the tests ran out of space.
 * This empty space can easily be freed by compressing the mounted folders once we are finished with them.
 * <p>
 *
 * <h2>HOW</h2>
 * To use this utility, create an object as a class field, and use @RegisterExtension annotation.
 *
 * On using TemporaryFolderManager to create a folder for the  first time in a test method, it will create a folder
 * with the method name (plus a little salt), and put all the method's temporary folders and data inside.
 * This means...
 * <ul>
 *     <li>when creating temporary folders, you don't need to worry about using the same folder name as another test.</li>
 *     <li>you can give folders generic names without having to worry too much about them being descriptive.</li>
 *     <li>Folder names can be automatically generated from the mountpoint if desired</li>
 * </ul>
 *
 * <h2>EXAMPLE USE CASES</h2>
 *
 * First, to get a TemporaryFolderManager instantiation do:
 * <pre>{@code
 @RegisterExtension
 public static TemporaryFolderManager temporaryFolderManager = new TemporaryFolderManager();
 * }</pre>
 *
 * <h3>SIMPLE: Just create one or two unrelated folders and mount them</h3>
 * Most of the time, this is all you'll need.
 * Assuming you already have a container...
 * <pre>{@code
Path confpath = temporaryFolderManager.createFolderAndMountAsVolume(container, "/conf");
Path logpath = temporaryFolderManager.createFolderAndMountAsVolume(container, "/logs");
 * }</pre>
 * This will create a folder in {@link TestSettings#TEST_TMP_FOLDER} with the name
 * <code>CLASSNAME_METHODNAME_RANDOMNUMBER</code> and inside it, there will be a folder called <code>conf</code> and
 * a folder called <code>logs</code>. These will be mounted to <code>container</code> at <code>/conf</code> and <code>/logs</code>.
 * <p>
 * For example if your test class is TestMounting.java and the method is called <code>shouldWriteToMount</code>
 * the folders created will be together inside <code>com.neo4j.docker.coredb.TestMounting_shouldWriteToMount_RANDOMNUMBER</code>.
 *
 * <h3>HARDER: Mount the same folder to two different (consecutive) containers</h3>
 * <pre>{@code
Path confpath;
try(container1 = makeAContainer())
{
    confpath = temporaryFolderManager.createFolderAndMountAsVolume(container1, "/conf");
}
try(container2 = makeAContainer())
{
    temporaryFolderManager.mountHostFolderAsVolume(container2, confpath, "/conf");
}
 * }</pre>
 *
 * <h3>HARDEST: Two containers, two different folders, same mount point each time</h3>
 * <pre>{@code
try(container1 = makeAContainer())
{
    Path confpath1 = temporaryFolderManager.createNamedFolderAndMountAsVolume( container1, "conf1", "/conf" );
}
try(container2 = makeAContainer())
{
    Path confpath2 = temporaryFolderManager.createNamedFolderAndMountAsVolume( container2, "conf2", "/conf" );
}
 * }</pre>
 * */
public class TemporaryFolderManager implements AfterAllCallback, BeforeEachCallback
{
    private final Logger log = LoggerFactory.getLogger( TemporaryFolderManager.class );
    // if we ever run parallel tests, random number generator and
    // list of folders to compress need to be made thread safe
    private Random rng = new Random(  );
    private final Path folderRoot;
    protected Path methodOutputFolder;    // protected scope for testing
    protected Set<Path> toCompressAfterAll = new HashSet<>();    // protected scope for testing

    public TemporaryFolderManager( )
    {
        this(TestSettings.TEST_TMP_FOLDER);
    }
    public TemporaryFolderManager( Path testOutputParentFolder )
    {
        this.folderRoot = testOutputParentFolder;
    }

    @Override
    public void beforeEach( ExtensionContext extensionContext ) throws Exception
    {
        String methodOutputFolderName = extensionContext.getTestClass().get().getName() + "_" +
                                 extensionContext.getTestMethod().get().getName();
        if(!extensionContext.getDisplayName().startsWith( extensionContext.getTestMethod().get().getName() ))
        {
            methodOutputFolderName += "_" + extensionContext.getDisplayName()
                                                            .replace( ' ', '_' );
        }
        // finally add some salt so  that we can run the same test method twice and not get naming clashes.
        methodOutputFolderName += String.format( "_%04d", rng.nextInt(10000 ) );
        log.info( "Recommended folder prefix is " + methodOutputFolderName );
        methodOutputFolder = folderRoot.resolve( methodOutputFolderName );
    }

    @Override
    public void afterAll( ExtensionContext extensionContext ) throws Exception
    {
        triggerCleanup();
    }

    public void triggerCleanup() throws Exception
    {
        if(TestSettings.SKIP_MOUNTED_FOLDER_TARBALLING)
        {
            log.info( "Cleanup of test artifacts skipped by request" );
            return;
        }
        log.info( "Performing cleanup of {}", folderRoot );
        // create tar archive of data
        for(Path p : toCompressAfterAll)
        {
            String tarOutName = p.getFileName().toString() + ".tar.gz";
            try ( OutputStream fo = Files.newOutputStream( p.getParent().resolve( tarOutName ) );
                  OutputStream gzo = new GzipCompressorOutputStream( fo );
                  TarArchiveOutputStream archiver = new TarArchiveOutputStream( gzo ) )
            {
                archiver.setLongFileMode( TarArchiveOutputStream.LONGFILE_POSIX );
                List<Path> files = Files.walk( p ).toList();
                for(Path fileToBeArchived : files)
                {
                    // don't archive directories...
                    if(fileToBeArchived.toFile().isDirectory()) continue;
                    try( InputStream fileStream = Files.newInputStream( fileToBeArchived ))
                    {
                        ArchiveEntry entry = archiver.createArchiveEntry( fileToBeArchived, folderRoot.relativize( fileToBeArchived ).toString() );
                        archiver.putArchiveEntry( entry );
                        IOUtils.copy( fileStream, archiver );
                        archiver.closeArchiveEntry();
                    } catch (IOException ioe)
                    {
                        // consume the error, because sometimes, file permissions won't let us copy
                        log.warn( "Could not archive "+ fileToBeArchived, ioe);
                    }
                }
                archiver.finish();
            }
        }
        // delete original folders
        log.debug( "Re owning folders: {}", toCompressAfterAll.stream()
                                                              .map( Path::toString )
                                                              .collect( Collectors.joining(", ")));
        setFolderOwnerTo( SetContainerUser.getNonRootUserString(),
                          toCompressAfterAll.toArray(new Path[toCompressAfterAll.size()]) );

        for(Path p : toCompressAfterAll)
        {
            log.debug( "Deleting test output folder {}", p.getFileName().toString() );
            FileUtils.deleteDirectory( p.toFile() );
        }
        toCompressAfterAll.clear();
    }

    public Path createNamedFolderAndMountAsVolume( GenericContainer container, String hostFolderName, String containerMountPoint ) throws IOException
    {
        Path tempFolder = createFolder( hostFolderName );
        mountHostFolderAsVolume( container, tempFolder, containerMountPoint );
        return tempFolder;
    }

    public Path createFolderAndMountAsVolume( GenericContainer container, String containerMountPoint ) throws IOException
    {
        Path tempFolder = createFolder( getFolderNameFromMountPoint( containerMountPoint ) );
        mountHostFolderAsVolume( container, tempFolder, containerMountPoint );
        return tempFolder;
    }

//    public Path createNamedFolderAndMountAsVolume( GenericContainer container, String hostFolderName,
//                                                   Path parentFolder, String containerMountPoint ) throws IOException
//    {
//        Path tempFolder = createFolder( hostFolderName, parentFolder );
//        mountHostFolderAsVolume( container, tempFolder, containerMountPoint );
//        return tempFolder;
//    }

//    public Path createFolderAndMountAsVolume( GenericContainer container, String containerMountPoint, Path parentFolder ) throws IOException
//    {
//        return null;
//        Path hostFolder = createTempFolder( hostFolderNamePrefix, parentFolder );
//        mountHostFolderAsVolume( container, hostFolder, containerMountPoint );
//        return hostFolder;
//    }

    public void mountHostFolderAsVolume(GenericContainer container, Path hostFolder, String containerMountPoint)
    {
        container.withFileSystemBind( hostFolder.toAbsolutePath().toString(),
                                      containerMountPoint,
                                      BindMode.READ_WRITE );
    }

    public Path createFolder( String folderName ) throws IOException
    {
    	return createFolder( folderName, methodOutputFolder );
    }

    public Path createFolder( String folderName, Path parentFolder ) throws IOException
    {
        if(!parentFolder.startsWith( folderRoot ))
        {
            throw new IOException("Requested to create temp folder outside of " + folderRoot +". " +
                                  "This is a problem with the test.");
        }
        Path hostFolder = parentFolder.resolve(folderName);
        try
        {
            Files.createDirectories( hostFolder );
        }
        catch ( IOException e )
        {
            log.error( "could not create directory: {}", hostFolder.toAbsolutePath() );
            e.printStackTrace();
            throw e;
        }
        log.info( "Created folder {}", hostFolder );
        // flag top level methodOutputFolder for cleanup
        toCompressAfterAll.add( methodOutputFolder ); // toCompressAfterAll is a set, so automatically removes duplicates.
        return hostFolder;
    }

    public void setFolderOwnerToCurrentUser(Path file) throws Exception
    {
        setFolderOwnerTo( SetContainerUser.getNonRootUserString(), file );
    }

    public void setFolderOwnerToNeo4j(Path file) throws Exception
    {
        setFolderOwnerTo( "7474:7474", file );
    }

    protected String getFolderNameFromMountPoint(String containerMountPoint)
    {
        return containerMountPoint.substring( 1 )
                                  .replace( '/', '_' )
                                  .replace( ' ', '_' );
    }

    private void setFolderOwnerTo(String userAndGroup, Path... files) throws Exception
    {
        // uses docker privileges to set file owner, since probably the current user is not a sudoer.

        // Using nginx because it's easy to verify that the image started.
        try(GenericContainer container = new GenericContainer( DockerImageName.parse( "nginx:latest")))
        {
            container.withExposedPorts( 80 )
                     .waitingFor( Wait.forHttp( "/" ).withStartupTimeout( Duration.ofSeconds( 20 ) ) );
            for(Path p : files)
            {
                mountHostFolderAsVolume( container, p, p.toAbsolutePath().toString() );
            }
            container.start();
            for(Path p : files)
            {
                Container.ExecResult x =
                        container.execInContainer( "chown", "-R", userAndGroup,
                                                   p.toAbsolutePath().toString() );
            }
            container.stop();
        }
    }
}

```

--------------------------------------------------------------------------------
/neo4j/docker-neo4j/src/test/java/com/neo4j/docker/coredb/TestBasic.java:
--------------------------------------------------------------------------------

```java
package com.neo4j.docker.coredb;

import com.github.dockerjava.api.command.KillContainerCmd;
import com.github.dockerjava.api.command.StopContainerCmd;
import com.neo4j.docker.utils.DatabaseIO;
import com.neo4j.docker.utils.Neo4jVersion;
import com.neo4j.docker.utils.TemporaryFolderManager;
import com.neo4j.docker.utils.TestSettings;
import com.neo4j.docker.utils.WaitStrategies;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testcontainers.containers.Container;
import org.testcontainers.containers.ContainerLaunchException;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.output.OutputFrame;
import org.testcontainers.containers.output.Slf4jLogConsumer;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.List;
import java.util.stream.Stream;

import static com.neo4j.docker.utils.Network.getUniqueHostPort;
import static com.neo4j.docker.utils.WaitStrategies.waitForBoltReady;
import static com.neo4j.docker.utils.WaitStrategies.waitForNeo4jReady;
import static org.testcontainers.shaded.org.awaitility.Awaitility.await;

public class TestBasic
{
    private static Logger log = LoggerFactory.getLogger( TestBasic.class );
    @RegisterExtension
    public static TemporaryFolderManager temporaryFolderManager = new TemporaryFolderManager();

    private GenericContainer createBasicContainer()
    {
        GenericContainer container = new GenericContainer( TestSettings.IMAGE_ID );
        container.withEnv( "NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes" )
                 .withExposedPorts( 7474, 7687 )
                 .withLogConsumer( new Slf4jLogConsumer( log ) );
        return container;
    }

    @Test
    void testListensOn7687()
    {
        try ( GenericContainer container = createBasicContainer() )
        {
            container.waitingFor( waitForNeo4jReady( "neo4j" ) );
            container.start();
            Assertions.assertTrue( container.isRunning() );
            String stdout = container.getLogs();
            Assertions.assertFalse( stdout.contains( "DEBUGGING ENABLED" ),
                                    "Debugging was enabled even though we did not set debugging" );
        }
    }

    @Test
    void testNoUnexpectedErrors()
    {
        try ( GenericContainer container = createBasicContainer() )
        {
            container.waitingFor( waitForNeo4jReady( "neo4j" ) );
            container.start();
            Assertions.assertTrue( container.isRunning() );

            String stderr = container.getLogs( OutputFrame.OutputType.STDERR );
            Assertions.assertEquals( "", stderr,
                                     "Unexpected errors in stderr from container!\n" +
                                     stderr );
        }
    }

    @Test
    void testLicenseAcceptanceRequired_Neo4jServer()
    {
        Assumptions.assumeTrue( TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 3, 3, 0 ) ),
                                "No license checks before version 3.3.0" );
        Assumptions.assumeTrue( TestSettings.EDITION == TestSettings.Edition.ENTERPRISE,
                                "No license checks for community edition" );

        String logsOut;
        try ( GenericContainer container = new GenericContainer( TestSettings.IMAGE_ID )
                .withLogConsumer( new Slf4jLogConsumer( log ) ) )
        {
            WaitStrategies.waitUntilContainerFinished( container, Duration.ofSeconds( 30 ) );
            // container start should fail due to licensing.
            Assertions.assertThrows( ContainerLaunchException.class, container::start,
                                     "Neo4j did not notify about accepting the license agreement" );
            logsOut = container.getLogs();
        }
        // double check the container didn't warn and start neo4j anyway
        Assertions.assertTrue( logsOut.contains( "must accept the license" ),
                               "Neo4j did not notify about accepting the license agreement" );
        Assertions.assertFalse( logsOut.contains( "Remote interface available" ),
                                "Neo4j was started even though the license was not accepted" );
    }

    @Test
    void testLicenseAcceptanceAvoidsWarning()
    {
        Assumptions.assumeTrue( TestSettings.EDITION == TestSettings.Edition.ENTERPRISE,
                                "No license checks for community edition" );
        Assumptions.assumeTrue( TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 5, 0, 0 ) ),
                                "No unified license acceptance method before 5.0.0" );
        try ( GenericContainer container = createBasicContainer() )
        {
            container.waitingFor( waitForNeo4jReady( "neo4j" ) );
            container.start();
            Assertions.assertTrue( container.isRunning() );

            String stdout = container.getLogs( OutputFrame.OutputType.STDOUT );
            Assertions.assertTrue( stdout.contains( "The license agreement was accepted with environment variable " +
                                                    "NEO4J_ACCEPT_LICENSE_AGREEMENT=yes when the Software was started." ),
                                   "Neo4j did not register that the license was agreed to." );
        }
    }

    @Test
    void testLicenseAcceptanceAvoidsWarning_evaluation()
    {
        Assumptions.assumeTrue( TestSettings.EDITION == TestSettings.Edition.ENTERPRISE,
                                "No license checks for community edition" );
        Assumptions.assumeTrue( TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 5, 0, 0 ) ),
                                "No unified license acceptance method before 5.0.0" );
        try ( GenericContainer container = createBasicContainer() )
        {
            container.withEnv( "NEO4J_ACCEPT_LICENSE_AGREEMENT", "eval" )
                     .waitingFor( waitForNeo4jReady( "neo4j" ) );
            container.start();
            Assertions.assertTrue( container.isRunning() );

            String stdout = container.getLogs( OutputFrame.OutputType.STDOUT );
            Assertions.assertTrue( stdout.contains( "The license agreement was accepted with environment variable " +
                                                    "NEO4J_ACCEPT_LICENSE_AGREEMENT=eval when the Software was started." ),
                                   "Neo4j did not register that the evaluation license was agreed to." );
        }
    }

    @Test
    void testCypherShellOnPath() throws Exception
    {
        String expectedCypherShellPath = "/var/lib/neo4j/bin/cypher-shell";
        try ( GenericContainer container = createBasicContainer() )
        {
            container.waitingFor( waitForNeo4jReady( "neo4j" ) );
            container.start();

            Container.ExecResult whichResult = container.execInContainer( "which", "cypher-shell" );
            Assertions.assertTrue( whichResult.getStdout().contains( expectedCypherShellPath ),
                                   "cypher-shell not on path" );
        }
    }

    @Test
    void testCanChangeWorkDir()
    {
        try ( GenericContainer container = createBasicContainer() )
        {
            container.waitingFor( waitForNeo4jReady( "neo4j" ) );
            container.setWorkingDirectory( "/tmp" );
            Assertions.assertDoesNotThrow( container::start,
                                           "Could not start neo4j from workdir other than NEO4J_HOME" );
        }
    }

    @Test
    void testPackagingInfoContainsDocker() throws Exception
    {
        Assumptions.assumeTrue( TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 5, 0, 0 ) ),
                "No packaging_info file before 5.0.0" );
        try ( GenericContainer container = createBasicContainer() )
        {
            container.waitingFor( waitForNeo4jReady( "neo4j" ) );
            container.start();
            String packagingInfo = container.execInContainer("cat", "/var/lib/neo4j/packaging_info").getStdout();
            List<String> actualPackageType = Stream.of(packagingInfo.split( "\n" ))
                    .filter(line -> line.startsWith("Package Type:"))
                    .toList();
            Assertions.assertEquals(1, actualPackageType.size(),
                    "There should only be 1 Package Type declarations in the packaging_info:\n"+actualPackageType);
            Assertions.assertEquals("Package Type: docker " + TestSettings.BASE_OS.name().toLowerCase(),
                    actualPackageType.get(0), "Docker packaging type is missing from packaging info file");
        }
    }

    @ParameterizedTest( name = "ShutsDownCorrectly_{0}" )
    @ValueSource( strings = {"SIGTERM", "SIGINT"} )
    void testShutsDownCleanly( String signal )
    {
        try ( GenericContainer container = createBasicContainer() )
        {
            container.withEnv( "NEO4J_AUTH", "none" )
                     .waitingFor( waitForNeo4jReady( "none" ) );
            container.start();
            DatabaseIO dbio = new DatabaseIO( container );
            dbio.putInitialDataIntoContainer( "neo4j", "none" );
            try(KillContainerCmd kill = container.getDockerClient().killContainerCmd(container.getContainerId());
                StopContainerCmd stop = container.getDockerClient().stopContainerCmd(container.getContainerId()))
            {
                log.info( "issuing container stop command " + signal );
                kill.withSignal(signal).exec();
                log.info("waiting for container to shut down.");
                stop.withTimeout(30).exec();
            }
            String stdout = container.getLogs();
            Assertions.assertTrue( stdout.contains( "Neo4j Server shutdown initiated by request" ),
                                   "clean shutdown not initiated by " + signal + "\n" + stdout);
            Assertions.assertTrue( stdout.contains( "Stopped." ),
                                   "clean shutdown not initiated by " + signal + "\n" + stdout);
        }
    }

    @Test
    void testStartsWhenDebuggingEnabled()
    {
        try ( GenericContainer container = createBasicContainer() )
        {
            container.withEnv( "NEO4J_DEBUG", "true" );
            container.start();
            Assertions.assertTrue( container.isRunning() );
        }
    }

    /*
        This test emulates a termination of the Docker Desktop or Docker Engine by the user. In these
        cases the container receives a SIGKILL signal and neo4j doesn't have time to clean up the PID
        file. In turn this causes the container to not be re-startable.
     */
    @Test
    void testContainerCanBeRestartedAfterUnexpectedTermination() throws IOException
    {
        try ( GenericContainer container = createBasicContainer() )
        {
            int boltHostPort = getUniqueHostPort();
            int browserHostPort = getUniqueHostPort();

            container.waitingFor( waitForBoltReady() );
            container.withEnv( "NEO4J_AUTH", "none" );

            // Ensuring host ports are constant with container restarts
            container.setPortBindings( List.of( browserHostPort + ":7474", boltHostPort + ":7687" ) );

            container.start();

            // Terminating container with a SIGKILL signal to emulate docker engine (docker desktop) being terminated by user.
            // This also keeps around the container unlike GenericContainer::stop(), which cleans up everything
            log.info( "Terminating container with SIGKILL signal" );
            container.getDockerClient().killContainerCmd( container.getContainerId() ).withSignal( "SIGKILL" ).exec();

            await().atMost( Duration.ofSeconds( 120 ) ).untilAsserted( () -> {
                Assertions.assertFalse( container.isRunning());
            } );

            // Restarting the container with DockerClient because the GenericContainer was not terminates and GenericContainer::start()
            // does not work
            log.info( "Starting container" );
            container.getDockerClient().startContainerCmd( container.getContainerId() ).exec();

            // Applying the Waiting strategy to ensure container is correctly running, because DockerClient does not check
            waitForBoltReady().waitUntilReady( container );
        }
    }

    @Test
    void testExtensionScriptIsExecuted() throws IOException
    {
        Path scriptFolder = temporaryFolderManager.createFolder("extension_script");
        Path script = scriptFolder.resolve("startscript.sh");
        Files.writeString(script, "#!/bin/bash\n\necho \"SCRIPT EXECUTED!\"");

        try ( GenericContainer container = createBasicContainer() )
        {
            temporaryFolderManager.mountHostFolderAsVolume(container, scriptFolder, "/extension");
            container.waitingFor(waitForBoltReady())
                    .withEnv("EXTENSION_SCRIPT", "/extension/startscript.sh");
            container.start();
            String logs = container.getLogs(OutputFrame.OutputType.STDOUT);
            Assertions.assertTrue(logs.contains("SCRIPT EXECUTED!"), "The extension script did not get executed");
        }
    }
}

```

--------------------------------------------------------------------------------
/neo4j/docker-neo4j/src/test/java/com/neo4j/docker/coredb/plugins/TestPluginInstallation.java:
--------------------------------------------------------------------------------

```java
package com.neo4j.docker.coredb.plugins;

import com.github.dockerjava.api.command.CreateContainerCmd;
import com.neo4j.docker.coredb.configurations.Configuration;
import com.neo4j.docker.coredb.configurations.Setting;
import com.neo4j.docker.utils.DatabaseIO;
import com.neo4j.docker.utils.HttpServerTestExtension;
import com.neo4j.docker.utils.Neo4jVersion;
import com.neo4j.docker.utils.SetContainerUser;
import com.neo4j.docker.utils.TemporaryFolderManager;
import com.neo4j.docker.utils.TestSettings;
import com.neo4j.docker.utils.WaitStrategies;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.neo4j.driver.Record;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testcontainers.Testcontainers;
import org.testcontainers.containers.ContainerLaunchException;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.Network;
import org.testcontainers.containers.output.OutputFrame;
import org.testcontainers.containers.output.Slf4jLogConsumer;

import java.nio.file.Path;
import java.time.Duration;
import java.util.List;
import java.util.function.Consumer;

import static com.neo4j.docker.utils.TestSettings.NEO4J_VERSION;


public class TestPluginInstallation
{
    private static final String DB_USER = "neo4j";
    private static final String DB_PASSWORD = "qualityPassword";

    private final Logger log = LoggerFactory.getLogger( TestPluginInstallation.class );
    @RegisterExtension
    public static TemporaryFolderManager temporaryFolderManager = new TemporaryFolderManager();
    @RegisterExtension
    public HttpServerTestExtension httpServer = new HttpServerTestExtension();
    StubPluginHelper stubPluginHelper = new StubPluginHelper(httpServer);


    private GenericContainer createContainerWithTestingPlugin(boolean asCurrentUser)
    {
        Testcontainers.exposeHostPorts( httpServer.PORT );
        GenericContainer container = new GenericContainer( TestSettings.IMAGE_ID );

        container.withEnv( "NEO4J_AUTH", DB_USER + "/" + DB_PASSWORD )
                 .withEnv( "NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes" )
                 .withEnv( "NEO4J_DEBUG", "yes" )
                 .withEnv( Neo4jPluginEnv.get(), "[\"" + stubPluginHelper.PLUGIN_ENV_NAME + "\"]" )
                 .withExposedPorts( 7474, 7687 )
                 .withLogConsumer( new Slf4jLogConsumer( log ) )
                 .waitingFor( WaitStrategies.waitForNeo4jReady( DB_PASSWORD));
        if(asCurrentUser) SetContainerUser.nonRootUser( container );
        return container;
    }

    @ParameterizedTest(name = "as_current_user_{0}")
    @ValueSource( booleans = {true, false} )
    public void testPluginLoads(boolean asCurrentUser) throws Exception
    {
        Path pluginsDir = temporaryFolderManager.createFolder("plugins");
        stubPluginHelper.createStubPluginForVersion(pluginsDir, NEO4J_VERSION);
        try ( GenericContainer container = createContainerWithTestingPlugin(asCurrentUser) )
        {
            container.start();
            DatabaseIO db = new DatabaseIO( container );
            stubPluginHelper.verifyStubPluginLoaded( db, DB_USER, DB_PASSWORD );
        }
    }

    @Test
    public void test_NEO4JLABS_PLUGIN_envWorksIn5() throws Exception
    {
        Assumptions.assumeTrue( NEO4J_VERSION.isAtLeastVersion( Neo4jVersion.NEO4J_VERSION_500 ),
                                "NEO4JLABS_PLUGIN backwards compatibility does not need checking pre 5.x" );

        Path pluginsDir = temporaryFolderManager.createFolder("plugins");
        stubPluginHelper.createStubPluginForVersion(pluginsDir, NEO4J_VERSION);
        try ( GenericContainer container = createContainerWithTestingPlugin(false) )
        {
            container.withEnv( Neo4jPluginEnv.PLUGIN_ENV_5X, "" );
            container.withEnv( Neo4jPluginEnv.PLUGIN_ENV_4X, "[\"_testing\"]" );
            container.start();
            DatabaseIO db = new DatabaseIO( container );
            stubPluginHelper.verifyStubPluginLoaded( db, DB_USER, DB_PASSWORD );
        }
    }

    @Test
    public void test_NEO4J_PLUGIN_envWorksIn44() throws Exception
    {
        Assumptions.assumeTrue( NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 4, 4, 18 ) ),
                                "NEO4JLABS_PLUGIN did not work in 4.4 before 4.4.18" );
        Assumptions.assumeTrue( NEO4J_VERSION.isOlderThan( Neo4jVersion.NEO4J_VERSION_500 ),
                                "Only checking forwards compatibility in 4.4" );

        Path pluginsDir = temporaryFolderManager.createFolder("plugins");
        stubPluginHelper.createStubPluginForVersion(pluginsDir, NEO4J_VERSION);
        try ( GenericContainer container = createContainerWithTestingPlugin(false) )
        {
            container.withEnv( Neo4jPluginEnv.PLUGIN_ENV_5X, "[\"_testing\"]" );
            container.withEnv( Neo4jPluginEnv.PLUGIN_ENV_4X, "" );
            container.start();
            DatabaseIO db = new DatabaseIO( container );
            stubPluginHelper.verifyStubPluginLoaded( db, DB_USER, DB_PASSWORD );
        }
    }

    @Test
    public void testPluginConfigurationDoesNotOverrideUserSetValues() throws Exception
    {
        Path pluginsDir = temporaryFolderManager.createFolder("plugins");
        Configuration securityProcedures = Configuration.getConfigurationNameMap()
                .get( Setting.SECURITY_PROCEDURES_UNRESTRICTED );
        stubPluginHelper.createStubPluginForVersion(pluginsDir, NEO4J_VERSION);
        try ( GenericContainer container = createContainerWithTestingPlugin(false) )
        {
            container.withEnv( securityProcedures.envName, "foo" );
            container.start();
            // When we connect to the database with the plugin
            // Check that the config remains as set by our env var and is not overridden by the plugin defaults
            DatabaseIO db = new DatabaseIO( container );
            stubPluginHelper.verifyStubPluginLoaded( db, DB_USER, DB_PASSWORD );
            db.verifyConfigurationSetting( DB_USER, DB_PASSWORD,
                                           securityProcedures,
                                           "foo",
                                           "neo4j config should not be overridden by plugin" );
        }
    }

    @Test
    void invalidPluginNameShouldGiveOptionsAndError()
    {
        Assumptions.assumeTrue( NEO4J_VERSION.isAtLeastVersion( Neo4jVersion.NEO4J_VERSION_440 ) );
        try ( GenericContainer container = new GenericContainer( TestSettings.IMAGE_ID ) )
        {
            // if we try to set a plugin that doesn't exist
            container.withEnv( Neo4jPluginEnv.get(), "[\"notarealplugin\"]" )
                     .withEnv( "NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes" )
                     .withLogConsumer( new Slf4jLogConsumer( log ) );
            WaitStrategies.waitUntilContainerFinished( container, Duration.ofSeconds( 30 ) );
            Assertions.assertThrows( ContainerLaunchException.class, container::start );
            // the container should output a helpful message and quit
            String stdout = container.getLogs();
            Assertions.assertTrue( stdout.contains( "\"notarealplugin\" is not a known Neo4j plugin. Options are:" ) );
            Assertions.assertFalse( stdout.contains( "_testing" ), "Fake _testing plugin is exposed." );
        }
    }

    @Test
    void invalidPluginNameShouldGiveOptionsAndError_mulitpleplugins()
    {
        Assumptions.assumeTrue( NEO4J_VERSION.isAtLeastVersion( Neo4jVersion.NEO4J_VERSION_440 ) );
        try ( GenericContainer container = new GenericContainer( TestSettings.IMAGE_ID ) )
        {
            // if we try to set a plugin that doesn't exist
            container.withEnv( Neo4jPluginEnv.get(), "[\"apoc\", \"notarealplugin\"]" )
                     .withEnv( "NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes" )
                     .withLogConsumer( new Slf4jLogConsumer( log ) );
            WaitStrategies.waitUntilContainerFinished( container, Duration.ofSeconds( 30 ) );
            Assertions.assertThrows( ContainerLaunchException.class, container::start );
            // the container should output a helpful message and quit
            String stdout = container.getLogs();
            Assertions.assertTrue( stdout.contains( "\"notarealplugin\" is not a known Neo4j plugin. Options are:" ) );
            Assertions.assertFalse( stdout.contains( StubPluginHelper.PLUGIN_ENV_NAME), "Fake _testing plugin is exposed." );
        }
    }

    @Test
    public void testBrokenVersionsJsonGivesWarning() throws Exception
    {
        Assumptions.assumeTrue( NEO4J_VERSION.isAtLeastVersion( Neo4jVersion.NEO4J_VERSION_440 ) );
        Path pluginsDir = temporaryFolderManager.createFolder("plugins");
        // create a versions.json that DOES NOT contain the current neo4j version in its mapping
        stubPluginHelper.createStubPluginForVersion(pluginsDir, new Neo4jVersion(50,0,0));
        try ( GenericContainer container = createContainerWithTestingPlugin(true) )
        {
            container.start();
            String startupErrors = container.getLogs( OutputFrame.OutputType.STDERR );
            Assertions.assertTrue( startupErrors.contains( "No compatible \"_testing\" plugin found for Neo4j " + NEO4J_VERSION ),
                                   "Did not error about plugin compatibility." );
            DatabaseIO db = new DatabaseIO( container );
            // make sure plugin did not load
            List<Record> procedures = db.runCypherQuery( DB_USER, DB_PASSWORD,
                                                         "SHOW PROCEDURES YIELD name, signature RETURN name, signature" );
            Assertions.assertFalse( procedures.stream()
                                              .anyMatch( x -> x.get( "name" ).asString()
                                                               .equals( "com.neo4j.docker.test.myplugin.defaultValues" ) ),
                                    "Incompatible test plugin was loaded." );
        }
    }

    //@Disabled("Test is flaky for unknown reasons. Needs further investigation.")
    @Test
    void testMissingVersionsJsonGivesWarning()
    {
        Configuration securityProcedures = Configuration.getConfigurationNameMap().get(Setting.SECURITY_PROCEDURES_UNRESTRICTED);
        // make double sure there are no versions.json files being served.
        httpServer.unregisterEndpoint("/versions.json");
        try ( Network net = Network.newNetwork();
              GenericContainer container = createContainerWithTestingPlugin(false)
                      .withNetwork(net) ) {
            container.start();
            String startupErrors = container.getLogs( OutputFrame.OutputType.STDERR );
            Assertions.assertTrue(
                    startupErrors.contains( "could not query http://host.testcontainers.internal:3000/versions.json for plugin compatibility information" ),
                    "Did not error about missing versions.json. Actual errors:\n\"" + startupErrors + "\"" );
            Assertions.assertFalse( startupErrors.contains( "No compatible \"_testing\" plugin found for Neo4j " + NEO4J_VERSION ),
                                    "Should not have errored about incompatible versions in versions.json" );
            // make sure plugin did not load
            DatabaseIO db = new DatabaseIO( container );
            List<Record> procedures = db.runCypherQuery( DB_USER, DB_PASSWORD,
                                                         "SHOW PROCEDURES YIELD name, signature RETURN name, signature" );
            Assertions.assertFalse( procedures.stream()
                                              .anyMatch( x -> x.get( "name" ).asString()
                                                               .equals( "com.neo4j.docker.test.myplugin.defaultValues" ) ),
                                    "Incompatible test plugin was loaded." );
            // make sure configuration did not set
            String securityConf = db.getConfigurationSettingAsString(DB_USER, DB_PASSWORD, securityProcedures);
            Assertions.assertFalse(securityConf.contains("com.neo4j.docker.neo4jserver.plugins.*"),
                    "Test plugin configuration setting was set, even though the plugin did not load.");
        }
    }

    @ParameterizedTest(name = "as_current_user_{0}")
    @ValueSource( booleans = {true, false} )
    public void testPlugin_originalEntrypointLocation(boolean asCurrentUser) throws Exception
    {
        // Older versions of Neo4j had docker-entrypoint.sh in / rather than /startup and sometimes
        // users use the old entrypoint location. This apparently caused problems loading plugins.
        Assumptions.assumeTrue( NEO4J_VERSION.isOlderThan( Neo4jVersion.NEO4J_VERSION_500 ),
                                "/docker-entrypoint.sh is permanently moved from 5.0 onwards" );
        Path pluginsDir = temporaryFolderManager.createFolder("plugins");
        stubPluginHelper.createStubPluginForVersion(pluginsDir, NEO4J_VERSION);
        try ( GenericContainer container = createContainerWithTestingPlugin(asCurrentUser) )
        {
            container.withCreateContainerCmdModifier(
                    (Consumer<CreateContainerCmd>) cmd -> cmd.withEntrypoint( "/docker-entrypoint.sh", "neo4j" ) );
            container.start();
            DatabaseIO db = new DatabaseIO( container );
            stubPluginHelper.verifyStubPluginLoaded( db, DB_USER, DB_PASSWORD );
        }
    }

    @ParameterizedTest( name = "as_current_user_{0}" )
    @ValueSource( booleans = {true, false} )
    void testPluginIsMovedToMountedFolderAndIsLoadedCorrectly( boolean asCurrentUser ) throws Exception
    {
        try ( GenericContainer container = createContainerWithTestingPlugin(asCurrentUser))
        {
            Path pluginsFolder = temporaryFolderManager.createFolderAndMountAsVolume(container, "/plugins");
            stubPluginHelper.createStubPluginForVersion(pluginsFolder, NEO4J_VERSION);
            container.start();
            Assertions.assertTrue( pluginsFolder.resolve( StubPluginHelper.PLUGIN_ENV_NAME +".jar" ).toFile().exists(),
                    "Did not find _testing.jar in plugins folder");

            DatabaseIO databaseIO = new DatabaseIO( container );
            stubPluginHelper.verifyStubPluginLoaded(databaseIO, DB_USER, DB_PASSWORD);
        }
    }
}

```

--------------------------------------------------------------------------------
/neo4j/docker-neo4j/src/test/java/com/neo4j/docker/coredb/TestMounting.java:
--------------------------------------------------------------------------------

```java
package com.neo4j.docker.coredb;

import com.github.dockerjava.api.command.CreateContainerCmd;
import com.github.dockerjava.api.model.Bind;
import com.neo4j.docker.utils.DatabaseIO;
import com.neo4j.docker.utils.Neo4jVersion;
import com.neo4j.docker.utils.SetContainerUser;
import com.neo4j.docker.utils.TemporaryFolderManager;
import com.neo4j.docker.utils.TestSettings;
import com.neo4j.docker.utils.WaitStrategies;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testcontainers.containers.ContainerLaunchException;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.output.OutputFrame;
import org.testcontainers.containers.output.Slf4jLogConsumer;
import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy;
import org.testcontainers.containers.wait.strategy.Wait;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.Random;
import java.util.function.Consumer;
import java.util.stream.Stream;

public class TestMounting
{
    private static Logger log = LoggerFactory.getLogger( TestMounting.class );

    @RegisterExtension
    public static TemporaryFolderManager temporaryFolderManager = new TemporaryFolderManager();

    @AfterEach
    void archiveTestArtifacts() throws Exception
    {
        temporaryFolderManager.triggerCleanup();
    }

    static Stream<Arguments> defaultUserFlagSecurePermissionsFlag()
    {
        // "asUser={0}, secureFlag={1}"
        // expected behaviour is that if you set --user flag, your data should be read/writable
        // if you don't set --user flag then read/writability should be controlled by the secure file permissions flag
        // the asCurrentUser=false, secureflag=true combination is tested separately because the container should fail to start.
        return Stream.of(
                Arguments.arguments( false, false ),
                Arguments.arguments( true, false ),
                Arguments.arguments( true, true ) );
    }

    private GenericContainer setupBasicContainer( boolean asCurrentUser, boolean isSecurityFlagSet )
    {
        log.info( "Running as user {}, {}",
                  asCurrentUser ? "non-root" : "root",
                  isSecurityFlagSet ? "with secure file permissions" : "with unsecured file permissions" );

        GenericContainer container = new GenericContainer( TestSettings.IMAGE_ID );
        container.withExposedPorts( 7474, 7687 )
                 .withLogConsumer( new Slf4jLogConsumer( log ) )
                 .withEnv( "NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes" )
                 .withEnv( "NEO4J_AUTH", "none" )
                 .waitingFor( WaitStrategies.waitForNeo4jReady( "none" ) );
        if ( asCurrentUser )
        {
            SetContainerUser.nonRootUser( container );
        }
        if ( isSecurityFlagSet )
        {
            container.withEnv( "SECURE_FILE_PERMISSIONS", "yes" );
        }
        return container;
    }

    private void verifySingleFolder( Path folderToCheck, boolean shouldBeWritable )
    {
        String folderForDiagnostics = folderToCheck.toAbsolutePath().toString();

        Assertions.assertTrue( folderToCheck.toFile().exists(), "did not create " + folderForDiagnostics + " folder on host" );
        if ( shouldBeWritable )
        {
            Assertions.assertTrue( folderToCheck.toFile().canRead(), "cannot read host " + folderForDiagnostics + " folder" );
            Assertions.assertTrue( folderToCheck.toFile().canWrite(), "cannot write to host " + folderForDiagnostics + " folder" );
        }
    }

    private void verifyDataFolderContentsArePresentOnHost( Path dataMount, boolean shouldBeWritable )
    {
        verifySingleFolder( dataMount.resolve( "databases" ), shouldBeWritable );

        if ( TestSettings.NEO4J_VERSION.isAtLeastVersion( Neo4jVersion.NEO4J_VERSION_400 ) )
        {
            verifySingleFolder( dataMount.resolve( "transactions" ), shouldBeWritable );
        }
    }

    private void verifyLogsFolderContentsArePresentOnHost( Path logsMount, boolean shouldBeWritable )
    {
        verifySingleFolder( logsMount, shouldBeWritable );
        Assertions.assertTrue( logsMount.resolve( "debug.log" ).toFile().exists(),
                               "Neo4j did not write a debug.log file to " + logsMount.toString() );
        Assertions.assertEquals( shouldBeWritable,
                                 logsMount.resolve( "debug.log" ).toFile().canWrite(),
                                 String.format( "The debug.log file should %sbe writable", shouldBeWritable ? "" : "not " ) );
    }

    @ParameterizedTest(name = "as_current_user_{0}")
    @ValueSource( booleans = {true, false} )
    void canDumpConfig( boolean asCurrentUser ) throws Exception
    {
        File confFile;
        Path confMount;
        String assertMsg = "Conf file was not successfully dumped when running container as "
                           + (asCurrentUser? "current user" : "root");

        try ( GenericContainer container = setupBasicContainer( asCurrentUser, false ) )
        {
            //Mount /conf
            confMount = temporaryFolderManager.createFolderAndMountAsVolume(container, "/conf");
            confFile = confMount.resolve( "neo4j.conf" ).toFile();

            //Start the container
            container.setWaitStrategy(
                    Wait.forLogMessage( ".*Config Dumped.*", 1 )
                        .withStartupTimeout( Duration.ofSeconds( 30 ) ) );
            container.setStartupCheckStrategy( new OneShotStartupCheckStrategy() );
            container.setCommand( "dump-config" );
            container.start();
        }

        // verify conf file was written
        Assertions.assertTrue( confFile.exists(), assertMsg );
        // verify conf folder does not have new owner if not running as root
        if ( asCurrentUser )
        {
            int fileUID = (Integer) Files.getAttribute( confFile.toPath(), "unix:uid" );
            int expectedUID = Integer.parseInt( SetContainerUser.getNonRootUserString().split( ":" )[0] );
            Assertions.assertEquals( expectedUID, fileUID, "Owner of dumped conf file is not the currently running user" );
        }
    }

    @Test
    void canDumpConfig_errorsWithoutConfMount() throws Exception
    {
        try ( GenericContainer container = setupBasicContainer( false, false ) )
        {
            container.setWaitStrategy(
                    Wait.forLogMessage( ".*Config Dumped.*", 1 )
                        .withStartupTimeout( Duration.ofSeconds( 30 ) ) );
            container.setStartupCheckStrategy( new OneShotStartupCheckStrategy() );
            container.setCommand( "dump-config" );
            Assertions.assertThrows( ContainerLaunchException.class,
                                     () -> container.start(),
                                     "Did not error when dump config requested without mounted /conf folder" );
            String stderr = container.getLogs( OutputFrame.OutputType.STDERR );
            Assertions.assertTrue( stderr.endsWith( "You must mount a folder to /conf so that the configuration file(s) can be dumped to there.\n" ) );
        }
    }

    @ParameterizedTest( name = "asUser={0}, secureFlag={1}" )
    @MethodSource( "defaultUserFlagSecurePermissionsFlag" )
    void testCanMountJustDataFolder( boolean asCurrentUser, boolean isSecurityFlagSet ) throws IOException
    {
        Assumptions.assumeTrue( TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 3, 1, 0 ) ),
                                "User checks not valid before 3.1" );

        try ( GenericContainer container = setupBasicContainer( asCurrentUser, isSecurityFlagSet ) )
        {
            Path dataMount = temporaryFolderManager.createFolderAndMountAsVolume(container, "/data");
            container.start();

            // neo4j should now have started, so there'll be stuff in the data folder
            // we need to check that stuff is readable and owned by the correct user
            verifyDataFolderContentsArePresentOnHost( dataMount, asCurrentUser );
        }
    }

    @ParameterizedTest( name = "asUser={0}, secureFlag={1}" )
    @MethodSource( "defaultUserFlagSecurePermissionsFlag" )
    void testCanMountJustLogsFolder( boolean asCurrentUser, boolean isSecurityFlagSet ) throws IOException
    {
        Assumptions.assumeTrue( TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 3, 1, 0 ) ),
                                "User checks not valid before 3.1" );

        try ( GenericContainer container = setupBasicContainer( asCurrentUser, isSecurityFlagSet ) )
        {
            Path logsMount = temporaryFolderManager.createFolderAndMountAsVolume(container, "/logs");
            container.start();

            verifyLogsFolderContentsArePresentOnHost( logsMount, asCurrentUser );
        }
    }

    @ParameterizedTest( name = "asUser={0}, secureFlag={1}" )
    @MethodSource( "defaultUserFlagSecurePermissionsFlag" )
    void testCanMountDataAndLogsFolder( boolean asCurrentUser, boolean isSecurityFlagSet ) throws IOException
    {
        Assumptions.assumeTrue( TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 3, 1, 0 ) ),
                                "User checks not valid before 3.1" );

        try ( GenericContainer container = setupBasicContainer( asCurrentUser, isSecurityFlagSet ) )
        {
            Path dataMount = temporaryFolderManager.createFolderAndMountAsVolume(container, "/data");
            Path logsMount = temporaryFolderManager.createFolderAndMountAsVolume(container, "/logs");
            container.start();

            verifyDataFolderContentsArePresentOnHost( dataMount, asCurrentUser );
            verifyLogsFolderContentsArePresentOnHost( logsMount, asCurrentUser );
        }
    }

    @Test
    void testCantWriteIfSecureEnabledAndNoPermissions_data() throws IOException
    {
        Assumptions.assumeTrue( TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 3, 1, 0 ) ),
                                "User checks not valid before 3.1" );

        try ( GenericContainer container = setupBasicContainer( false, true ) )
        {
            temporaryFolderManager.createFolderAndMountAsVolume(container, "/data");

            // currently Neo4j will try to start and fail. It should be fixed to throw an error and not try starting
            container.setWaitStrategy( Wait.forLogMessage( "[fF]older /data is not accessible for user", 1 )
                                           .withStartupTimeout( Duration.ofSeconds( 20 ) ) );
            Assertions.assertThrows( ContainerLaunchException.class,
                                     () -> container.start(),
                                     "Neo4j should not start in secure mode if data folder is unwritable" );
        }
    }

    @Test
    void testCantWriteIfSecureEnabledAndNoPermissions_logs() throws IOException
    {
        Assumptions.assumeTrue( TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 3, 1, 0 ) ),
                                "User checks not valid before 3.1" );

        try ( GenericContainer container = setupBasicContainer( false, true ) )
        {
            temporaryFolderManager.createFolderAndMountAsVolume(container, "/logs");

            // currently Neo4j will try to start and fail. It should be fixed to throw an error and not try starting
            container.setWaitStrategy( Wait.forLogMessage( "[fF]older /logs is not accessible for user", 1 )
                                           .withStartupTimeout( Duration.ofSeconds( 20 ) ) );
            Assertions.assertThrows( ContainerLaunchException.class,
                                     () -> container.start(),
                                     "Neo4j should not start in secure mode if logs folder is unwritable" );
        }
    }

    @ParameterizedTest(name = "as_current_user_{0}")
    @ValueSource( booleans = {true, false} )
    void canMountAllTheThings_fileMounts( boolean asCurrentUser ) throws Exception
    {
        try ( GenericContainer container = setupBasicContainer( asCurrentUser, false ) )
        {
            temporaryFolderManager.createFolderAndMountAsVolume(container, "/conf");
            temporaryFolderManager.createFolderAndMountAsVolume(container, "/data");
            temporaryFolderManager.createFolderAndMountAsVolume(container, "/import");
            temporaryFolderManager.createFolderAndMountAsVolume(container, "/logs");
            temporaryFolderManager.createFolderAndMountAsVolume(container, "/metrics");
            temporaryFolderManager.createFolderAndMountAsVolume(container, "/plugins");
            container.start();
            DatabaseIO databaseIO = new DatabaseIO( container );
            // do some database writes so that we try writing to writable folders.
            databaseIO.putInitialDataIntoContainer( "neo4j", "none" );
            databaseIO.verifyInitialDataInContainer( "neo4j", "none" );
        }
    }

    @ParameterizedTest(name = "as_current_user_{0}")
    @ValueSource( booleans = {true, false} )
    void canMountAllTheThings_namedVolumes( boolean asCurrentUser ) throws Exception
    {
        String id = String.format( "%04d", new Random().nextInt( 10000 ) );
        try ( GenericContainer container = setupBasicContainer( asCurrentUser, false ) )
        {
            container.withCreateContainerCmdModifier(
                    (Consumer<CreateContainerCmd>) cmd -> cmd.getHostConfig().withBinds(
                            Bind.parse( "conf-" + id + ":/conf" ),
                            Bind.parse( "data-" + id + ":/data" ),
                            Bind.parse( "import-" + id + ":/import" ),
                            Bind.parse( "logs-" + id + ":/logs" ),
                            //Bind.parse("metrics-"+id+":/metrics"), 	//todo metrics needs to be writable but we aren't chowning in the dockerfile, so a named volume for metrics will fail
                            Bind.parse( "plugins-" + id + ":/plugins" )
                    ) );
            container.start();
            DatabaseIO databaseIO = new DatabaseIO( container );
            // do some database writes so that we try writing to writable folders.
            databaseIO.putInitialDataIntoContainer( "neo4j", "none" );
            databaseIO.verifyInitialDataInContainer( "neo4j", "none" );
        }
    }

    @Test
    void shouldReownSubfilesToNeo4j() throws Exception
    {
        Assumptions.assumeTrue(
                TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 4, 0, 0 ) ),
                "User checks not valid before 4.0" );

        Path logMount = temporaryFolderManager.createFolder( "subfileownership" );
        Path debugLog = logMount.resolve( "debug.log" );
        // put file in logMount
        Files.write( debugLog, "some log words".getBytes() );
        // make neo4j own the conf folder but NOT the neo4j.conf
        temporaryFolderManager.setFolderOwnerToNeo4j( logMount );
        temporaryFolderManager.setFolderOwnerToCurrentUser( debugLog );

        try ( GenericContainer container = setupBasicContainer( false, false ) )
        {
            temporaryFolderManager.mountHostFolderAsVolume( container, logMount, "/logs" );
            container.start();
            // if debug.log doesn't get re-owned, neo4j will not start and this test will fail here
        }
    }
}

```

--------------------------------------------------------------------------------
/knowledge_graphs/query_knowledge_graph.py:
--------------------------------------------------------------------------------

```python
#!/usr/bin/env python3
"""
Knowledge Graph Query Tool

Interactive script to explore what's actually stored in your Neo4j knowledge graph.
Useful for debugging hallucination detection and understanding graph contents.
"""

import asyncio
import os
from dotenv import load_dotenv
from neo4j import AsyncGraphDatabase
from typing import List, Dict, Any
import argparse


class KnowledgeGraphQuerier:
    """Interactive tool to query the knowledge graph"""
    
    def __init__(self, neo4j_uri: str, neo4j_user: str, neo4j_password: str):
        self.neo4j_uri = neo4j_uri
        self.neo4j_user = neo4j_user
        self.neo4j_password = neo4j_password
        self.driver = None
    
    async def initialize(self):
        """Initialize Neo4j connection"""
        self.driver = AsyncGraphDatabase.driver(
            self.neo4j_uri, 
            auth=(self.neo4j_user, self.neo4j_password)
        )
        print("🔗 Connected to Neo4j knowledge graph")
    
    async def close(self):
        """Close Neo4j connection"""
        if self.driver:
            await self.driver.close()
    
    async def list_repositories(self):
        """List all repositories in the knowledge graph"""
        print("\n📚 Repositories in Knowledge Graph:")
        print("=" * 50)
        
        async with self.driver.session() as session:
            query = "MATCH (r:Repository) RETURN r.name as name ORDER BY r.name"
            result = await session.run(query)
            
            repos = []
            async for record in result:
                repos.append(record['name'])
            
            if repos:
                for i, repo in enumerate(repos, 1):
                    print(f"{i}. {repo}")
            else:
                print("No repositories found in knowledge graph.")
        
        return repos
    
    async def explore_repository(self, repo_name: str):
        """Get overview of a specific repository"""
        print(f"\n🔍 Exploring Repository: {repo_name}")
        print("=" * 60)
        
        async with self.driver.session() as session:
            # Get file count
            files_query = """
            MATCH (r:Repository {name: $repo_name})-[:CONTAINS]->(f:File)
            RETURN count(f) as file_count
            """
            result = await session.run(files_query, repo_name=repo_name)
            file_count = (await result.single())['file_count']
            
            # Get class count
            classes_query = """
            MATCH (r:Repository {name: $repo_name})-[:CONTAINS]->(f:File)-[:DEFINES]->(c:Class)
            RETURN count(DISTINCT c) as class_count
            """
            result = await session.run(classes_query, repo_name=repo_name)
            class_count = (await result.single())['class_count']
            
            # Get function count
            functions_query = """
            MATCH (r:Repository {name: $repo_name})-[:CONTAINS]->(f:File)-[:DEFINES]->(func:Function)
            RETURN count(DISTINCT func) as function_count
            """
            result = await session.run(functions_query, repo_name=repo_name)
            function_count = (await result.single())['function_count']
            
            print(f"📄 Files: {file_count}")
            print(f"🏗️  Classes: {class_count}")
            print(f"⚙️  Functions: {function_count}")
    
    async def list_classes(self, repo_name: str = None, limit: int = 20):
        """List classes in the knowledge graph"""
        title = f"Classes in {repo_name}" if repo_name else "All Classes"
        print(f"\n🏗️  {title} (limit {limit}):")
        print("=" * 50)
        
        async with self.driver.session() as session:
            if repo_name:
                query = """
                MATCH (r:Repository {name: $repo_name})-[:CONTAINS]->(f:File)-[:DEFINES]->(c:Class)
                RETURN c.name as name, c.full_name as full_name
                ORDER BY c.name
                LIMIT $limit
                """
                result = await session.run(query, repo_name=repo_name, limit=limit)
            else:
                query = """
                MATCH (c:Class)
                RETURN c.name as name, c.full_name as full_name
                ORDER BY c.name
                LIMIT $limit
                """
                result = await session.run(query, limit=limit)
            
            classes = []
            async for record in result:
                classes.append({
                    'name': record['name'],
                    'full_name': record['full_name']
                })
            
            if classes:
                for i, cls in enumerate(classes, 1):
                    print(f"{i:2d}. {cls['name']} ({cls['full_name']})")
            else:
                print("No classes found.")
        
        return classes
    
    async def explore_class(self, class_name: str):
        """Get detailed information about a specific class"""
        print(f"\n🔍 Exploring Class: {class_name}")
        print("=" * 60)
        
        async with self.driver.session() as session:
            # Find the class
            class_query = """
            MATCH (c:Class)
            WHERE c.name = $class_name OR c.full_name = $class_name
            RETURN c.name as name, c.full_name as full_name
            LIMIT 1
            """
            result = await session.run(class_query, class_name=class_name)
            class_record = await result.single()
            
            if not class_record:
                print(f"❌ Class '{class_name}' not found in knowledge graph.")
                return None
            
            actual_name = class_record['name']
            full_name = class_record['full_name']
            
            print(f"📋 Name: {actual_name}")
            print(f"📋 Full Name: {full_name}")
            
            # Get methods
            methods_query = """
            MATCH (c:Class)-[:HAS_METHOD]->(m:Method)
            WHERE c.name = $class_name OR c.full_name = $class_name
            RETURN m.name as name, m.params_list as params_list, m.params_detailed as params_detailed, m.return_type as return_type
            ORDER BY m.name
            """
            result = await session.run(methods_query, class_name=class_name)
            
            methods = []
            async for record in result:
                methods.append({
                    'name': record['name'],
                    'params_list': record['params_list'] or [],
                    'params_detailed': record['params_detailed'] or [],
                    'return_type': record['return_type'] or 'Any'
                })
            
            if methods:
                print(f"\n⚙️  Methods ({len(methods)}):")
                for i, method in enumerate(methods, 1):
                    # Use detailed params if available, fall back to simple params
                    params_to_show = method['params_detailed'] or method['params_list']
                    params = ', '.join(params_to_show) if params_to_show else ''
                    print(f"{i:2d}. {method['name']}({params}) -> {method['return_type']}")
            else:
                print("\n⚙️  No methods found.")
            
            # Get attributes
            attributes_query = """
            MATCH (c:Class)-[:HAS_ATTRIBUTE]->(a:Attribute)
            WHERE c.name = $class_name OR c.full_name = $class_name
            RETURN a.name as name, a.type as type
            ORDER BY a.name
            """
            result = await session.run(attributes_query, class_name=class_name)
            
            attributes = []
            async for record in result:
                attributes.append({
                    'name': record['name'],
                    'type': record['type'] or 'Any'
                })
            
            if attributes:
                print(f"\n📋 Attributes ({len(attributes)}):")
                for i, attr in enumerate(attributes, 1):
                    print(f"{i:2d}. {attr['name']}: {attr['type']}")
            else:
                print("\n📋 No attributes found.")
        
        return {'methods': methods, 'attributes': attributes}
    
    async def search_method(self, method_name: str, class_name: str = None):
        """Search for methods by name"""
        title = f"Method '{method_name}'"
        if class_name:
            title += f" in class '{class_name}'"
        
        print(f"\n🔍 Searching for {title}:")
        print("=" * 60)
        
        async with self.driver.session() as session:
            if class_name:
                query = """
                MATCH (c:Class)-[:HAS_METHOD]->(m:Method)
                WHERE (c.name = $class_name OR c.full_name = $class_name)
                  AND m.name = $method_name
                RETURN c.name as class_name, c.full_name as class_full_name,
                       m.name as method_name, m.params_list as params_list, 
                       m.return_type as return_type, m.args as args
                """
                result = await session.run(query, class_name=class_name, method_name=method_name)
            else:
                query = """
                MATCH (c:Class)-[:HAS_METHOD]->(m:Method)
                WHERE m.name = $method_name
                RETURN c.name as class_name, c.full_name as class_full_name,
                       m.name as method_name, m.params_list as params_list, 
                       m.return_type as return_type, m.args as args
                ORDER BY c.name
                """
                result = await session.run(query, method_name=method_name)
            
            methods = []
            async for record in result:
                methods.append({
                    'class_name': record['class_name'],
                    'class_full_name': record['class_full_name'],
                    'method_name': record['method_name'],
                    'params_list': record['params_list'] or [],
                    'return_type': record['return_type'] or 'Any',
                    'args': record['args'] or []
                })
            
            if methods:
                for i, method in enumerate(methods, 1):
                    params = ', '.join(method['params_list']) if method['params_list'] else ''
                    print(f"{i}. {method['class_full_name']}.{method['method_name']}({params}) -> {method['return_type']}")
                    if method['args']:
                        print(f"   Legacy args: {method['args']}")
            else:
                print(f"❌ Method '{method_name}' not found.")
        
        return methods
    
    async def run_custom_query(self, query: str):
        """Run a custom Cypher query"""
        print(f"\n🔍 Running Custom Query:")
        print("=" * 60)
        print(f"Query: {query}")
        print("-" * 60)
        
        async with self.driver.session() as session:
            try:
                result = await session.run(query)
                
                records = []
                async for record in result:
                    records.append(dict(record))
                
                if records:
                    for i, record in enumerate(records, 1):
                        print(f"{i:2d}. {record}")
                        if i >= 20:  # Limit output
                            print(f"... and {len(records) - 20} more records")
                            break
                else:
                    print("No results found.")
                
                return records
                
            except Exception as e:
                print(f"❌ Query error: {str(e)}")
                return None


async def interactive_mode(querier: KnowledgeGraphQuerier):
    """Interactive exploration mode"""
    print("\n🚀 Welcome to Knowledge Graph Explorer!")
    print("Available commands:")
    print("  repos          - List all repositories")
    print("  explore <repo> - Explore a specific repository") 
    print("  classes [repo] - List classes (optionally in specific repo)")
    print("  class <name>   - Explore a specific class")
    print("  method <name> [class] - Search for method")
    print("  query <cypher> - Run custom Cypher query")
    print("  quit           - Exit")
    print()
    
    while True:
        try:
            command = input("🔍 > ").strip()
            
            if not command:
                continue
            elif command == "quit":
                break
            elif command == "repos":
                await querier.list_repositories()
            elif command.startswith("explore "):
                repo_name = command[8:].strip()
                await querier.explore_repository(repo_name)
            elif command == "classes":
                await querier.list_classes()
            elif command.startswith("classes "):
                repo_name = command[8:].strip()
                await querier.list_classes(repo_name)
            elif command.startswith("class "):
                class_name = command[6:].strip()
                await querier.explore_class(class_name)
            elif command.startswith("method "):
                parts = command[7:].strip().split()
                if len(parts) >= 2:
                    await querier.search_method(parts[0], parts[1])
                else:
                    await querier.search_method(parts[0])
            elif command.startswith("query "):
                query = command[6:].strip()
                await querier.run_custom_query(query)
            else:
                print("❌ Unknown command. Type 'quit' to exit.")
                
        except KeyboardInterrupt:
            print("\n👋 Goodbye!")
            break
        except Exception as e:
            print(f"❌ Error: {str(e)}")


async def main():
    """Main function with CLI argument support"""
    parser = argparse.ArgumentParser(description="Query the knowledge graph")
    parser.add_argument('--repos', action='store_true', help='List repositories')
    parser.add_argument('--classes', metavar='REPO', nargs='?', const='', help='List classes')
    parser.add_argument('--explore', metavar='REPO', help='Explore repository')
    parser.add_argument('--class', dest='class_name', metavar='NAME', help='Explore class')
    parser.add_argument('--method', nargs='+', metavar=('NAME', 'CLASS'), help='Search method')
    parser.add_argument('--query', metavar='CYPHER', help='Run custom query')
    parser.add_argument('--interactive', action='store_true', help='Interactive mode')
    
    args = parser.parse_args()
    
    # Load environment
    load_dotenv()
    neo4j_uri = os.environ.get('NEO4J_URI', 'bolt://localhost:7687')
    neo4j_user = os.environ.get('NEO4J_USER', 'neo4j')
    neo4j_password = os.environ.get('NEO4J_PASSWORD', 'password')
    
    querier = KnowledgeGraphQuerier(neo4j_uri, neo4j_user, neo4j_password)
    
    try:
        await querier.initialize()
        
        # Execute commands based on arguments
        if args.repos:
            await querier.list_repositories()
        elif args.classes is not None:
            await querier.list_classes(args.classes if args.classes else None)
        elif args.explore:
            await querier.explore_repository(args.explore)
        elif args.class_name:
            await querier.explore_class(args.class_name)
        elif args.method:
            if len(args.method) >= 2:
                await querier.search_method(args.method[0], args.method[1])
            else:
                await querier.search_method(args.method[0])
        elif args.query:
            await querier.run_custom_query(args.query)
        elif args.interactive or len(sys.argv) == 1:
            await interactive_mode(querier)
        else:
            parser.print_help()
    
    finally:
        await querier.close()


if __name__ == "__main__":
    import sys
    asyncio.run(main())
```

--------------------------------------------------------------------------------
/neo4j/docker-neo4j/docker-image-src/3.4/docker-entrypoint.sh:
--------------------------------------------------------------------------------

```bash
#!/bin/bash -eu

cmd="$1"

function running_as_root
{
    test "$(id -u)" = "0"
}

function secure_mode_enabled
{
    test "${SECURE_FILE_PERMISSIONS:=no}" = "yes"
}

function containsElement
{
  local e match="$1"
  shift
  for e; do [[ "$e" == "$match" ]] && return 0; done
  return 1
}

function is_readable
{
    # this code is fairly ugly but works no matter who this script is running as.
    # It would be nice if the writability tests could use this logic somehow.
    local _file=${1}
    perm=$(stat -c %a "${_file}")

    # everyone permission
    if [[ ${perm:2:1} -ge 4 ]]; then
        return 0
    fi
    # owner permissions
    if [[ ${perm:0:1} -ge 4 ]]; then
        if [[ "$(stat -c %U ${_file})" = "${userid}" ]] || [[ "$(stat -c %u ${_file})" = "${userid}" ]]; then
            return 0
        fi
    fi
    # group permissions
    if [[ ${perm:1:1} -ge 4 ]]; then
        if containsElement "$(stat -c %g ${_file})" "${groups[@]}" || containsElement "$(stat -c %G ${_file})" "${groups[@]}" ; then
            return 0
        fi
    fi
    return 1
}

function is_writable
{
    # It would be nice if this and the is_readable function could combine somehow
    local _file=${1}
    perm=$(stat -c %a "${_file}")

    # everyone permission
    if containsElement ${perm:2:1} 2 3 6 7; then
        return 0
    fi
    # owner permissions
    if containsElement ${perm:0:1} 2 3 6 7; then
        if [[ "$(stat -c %U ${_file})" = "${userid}" ]] || [[ "$(stat -c %u ${_file})" = "${userid}" ]]; then
            return 0
        fi
    fi
    # group permissions
    if containsElement ${perm:1:1} 2 3 6 7; then
        if containsElement "$(stat -c %g ${_file})" "${groups[@]}" || containsElement "$(stat -c %G ${_file})" "${groups[@]}" ; then
            return 0
        fi
    fi
    return 1
}


function print_permissions_advice_and_fail
{
    _directory=${1}
    echo >&2 "
Folder ${_directory} is not accessible for user: ${userid} or group ${groupid} or groups ${groups[@]}, this is commonly a file permissions issue on the mounted folder.

Hints to solve the issue:
1) Make sure the folder exists before mounting it. Docker will create the folder using root permissions before starting the Neo4j container. The root permissions disallow Neo4j from writing to the mounted folder.
2) Pass the folder owner's user ID and group ID to docker run, so that docker runs as that user.
If the folder is owned by the current user, this can be done by adding this flag to your docker run command:
  --user=\$(id -u):\$(id -g)
       "
    exit 1
}

function check_mounted_folder_readable
{
    local _directory=${1}
    if ! is_readable "${_directory}"; then
        print_permissions_advice_and_fail "${_directory}"
    fi
}

function check_mounted_folder_with_chown
{
# The /data and /log directory are a bit different because they are very likely to be mounted by the user but not
# necessarily writable.
# This depends on whether a user ID is passed to the container and which folders are mounted.
#
#   No user ID passed to container:
#   1) No folders are mounted.
#      The /data and /log folder are owned by neo4j by default, so should be writable already.
#   2) Both /log and /data are mounted.
#      This means on start up, /data and /logs are owned by an unknown user and we should chown them to neo4j for
#      backwards compatibility.
#
#   User ID passed to container:
#   1) Both /data and /logs are mounted
#      The /data and /logs folders are owned by an unknown user but we *should* have rw permission to them.
#      That should be verified and error (helpfully) if not.
#   2) User mounts /data or /logs *but not both*
#      The  unmounted folder is still owned by neo4j, which should already be writable. The mounted folder should
#      have rw permissions through user id. This should be verified.
#   3) No folders are mounted.
#      The /data and /log folder are owned by neo4j by default, and these are already writable by the user.
#      (This is a very unlikely use case).

    local mountFolder=${1}
    if running_as_root; then
        if ! is_writable "${mountFolder}" && ! secure_mode_enabled; then
            # warn that we're about to chown the folder and then chown it
            echo "Warning: Folder mounted to \"${mountFolder}\" is not writable from inside container. Changing folder owner to ${userid}."
            chown -R "${userid}":"${groupid}" "${mountFolder}"
        fi
    else
        if [[ ! -w "${mountFolder}" ]]  && [[ "$(stat -c %U ${mountFolder})" != "neo4j" ]]; then
            print_permissions_advice_and_fail "${mountFolder}"
        fi
    fi
}

function load_plugin_from_github
{
  # Load a plugin at runtime. The provided github repository must have a versions.json on the master branch with the
  # correct format.
  local _plugin_name="${1}" #e.g. apoc, graph-algorithms, graph-ql

  local _plugins_dir="${NEO4J_HOME}/plugins"
  if [ -d /plugins ]; then
    local _plugins_dir="/plugins"
  fi
  local _versions_json_url="$(jq --raw-output "with_entries( select(.key==\"${_plugin_name}\") ) | to_entries[] | .value.versions" /neo4j-plugins.json )"
  # Using the same name for the plugin irrespective of version ensures we don't end up with different versions of the same plugin
  local _destination="${_plugins_dir}/${_plugin_name}.jar"
  local _neo4j_version="$(neo4j --version | cut -d' ' -f2)"

  # Now we call out to github to get the versions.json for this plugin and we parse that to find the url for the correct plugin jar for our neo4j version
  echo "Fetching versions.json for Plugin '${_plugin_name}' from ${_versions_json_url}"
  local _versions_json="$(wget -q --timeout 300 --tries 30 -O - "${_versions_json_url}")"
  local _plugin_jar_url="$(echo "${_versions_json}" | jq --raw-output ".[] | select(.neo4j==\"${_neo4j_version}\") | .jar")"
  if [[ -z "${_plugin_jar_url}" ]]; then
    echo >&2 "Error: No jar URL found for version '${_neo4j_version}' in versions.json from '${_versions_json_url}'"
    echo >&2 "${_versions_json}"
    exit 1
  fi
  echo "Installing Plugin '${_plugin_name}' from ${_plugin_jar_url} to ${_destination} "
  wget -q --timeout 300 --tries 30 --output-document="${_destination}" "${_plugin_jar_url}"

  if ! is_readable "${_destination}"; then
    echo >&2 "Plugin at '${_destination}' is not readable"
    exit 1
  fi
}

function apply_plugin_default_configuration
{
  # Set the correct Load a plugin at runtime. The provided github repository must have a versions.json on the master branch with the
  # correct format.
  local _plugin_name="${1}" #e.g. apoc, graph-algorithms, graph-ql
  local _reference_conf="${2}" # used to determine if we can override properties
  local _neo4j_conf="${NEO4J_HOME}/conf/neo4j.conf"

  local _property _value
  echo "Applying default values for plugin ${_plugin_name} to neo4j.conf"
  for _entry in $(jq  --compact-output --raw-output "with_entries( select(.key==\"${_plugin_name}\") ) | to_entries[] | .value.properties | to_entries[]" /neo4j-plugins.json); do
    _property="$(jq --raw-output '.key' <<< "${_entry}")"
    _value="$(jq --raw-output '.value' <<< "${_entry}")"

    # the first grep strips out comments
    if grep -o "^[^#]*" "${_reference_conf}" | grep -q --fixed-strings "${_property}=" ; then
      # property is already set in the user provided config. In this case we don't override what has been set explicitly by the user.
      echo "Skipping ${_property} for plugin ${_plugin_name} because it is already set"
    else
      if grep -o "^[^#]*" "${_neo4j_conf}" | grep -q --fixed-strings "${_property}=" ; then
        sed --in-place "s/${_property}=/&${_value},/" "${_neo4j_conf}"
      else
        echo "${_property}=${_value}" >> "${_neo4j_conf}"
      fi
    fi
  done
}

function install_neo4j_labs_plugins
{
  # We store a copy of the config before we modify it for the plugins to allow us to see if there are user-set values in the input config that we shouldn't override
  local _old_config="$(mktemp)"
  cp "${NEO4J_HOME}"/conf/neo4j.conf "${_old_config}"
  for plugin_name in $(echo "${NEO4JLABS_PLUGINS}" | jq --raw-output '.[]'); do
    load_plugin_from_github "${plugin_name}"
    apply_plugin_default_configuration "${plugin_name}" "${_old_config}"
  done
  rm "${_old_config}"
}

# If we're running as root, then run as the neo4j user. Otherwise
# docker is running with --user and we simply use that user.  Note
# that su-exec, despite its name, does not replicate the functionality
# of exec, so we need to use both
if running_as_root; then
  userid="neo4j"
  groupid="neo4j"
  groups=($(id -G neo4j))
  exec_cmd="exec gosu neo4j:neo4j"
else
  userid="$(id -u)"
  groupid="$(id -g)"
  groups=($(id -G))
  exec_cmd="exec"
fi
readonly userid
readonly groupid
readonly groups
readonly exec_cmd


# Need to chown the home directory - but a user might have mounted a
# volume here (notably a conf volume). So take care not to chown
# volumes (stuff not owned by neo4j)
if running_as_root; then
    # Non-recursive chown for the base directory
    chown "${userid}":"${groupid}" "${NEO4J_HOME}"
    chmod 700 "${NEO4J_HOME}"
    find "${NEO4J_HOME}" -mindepth 1 -maxdepth 1 -type d -exec chown -R ${userid}:${groupid} {} \;
    find "${NEO4J_HOME}" -mindepth 1 -maxdepth 1 -type d -exec chmod -R 700 {} \;
fi

# Only prompt for license agreement if command contains "neo4j" in it
if [[ "${cmd}" == *"neo4j"* ]]; then
  if [ "${NEO4J_EDITION}" == "enterprise" ]; then
    if [ "${NEO4J_ACCEPT_LICENSE_AGREEMENT:=no}" != "yes" ]; then
      echo >&2 "
In order to use Neo4j Enterprise Edition you must accept the license agreement.

(c) Neo4j Sweden AB. 2021.  All Rights Reserved.
Use of this Software without a proper commercial license with Neo4j,
Inc. or its affiliates is prohibited.

Email inquiries can be directed to: [email protected]

More information is also available at: https://neo4j.com/licensing/


To accept the license agreement set the environment variable
NEO4J_ACCEPT_LICENSE_AGREEMENT=yes

To do this you can use the following docker argument:

        --env=NEO4J_ACCEPT_LICENSE_AGREEMENT=yes
"
      exit 1
    fi
  fi
fi

# Env variable naming convention:
# - prefix NEO4J_
# - double underscore char '__' instead of single underscore '_' char in the setting name
# - underscore char '_' instead of dot '.' char in the setting name
# Example:
# NEO4J_dbms_tx__log_rotation_retention__policy env variable to set
#       dbms.tx_log.rotation.retention_policy setting

# Backward compatibility - map old hardcoded env variables into new naming convention (if they aren't set already)
# Set some to default values if unset
: ${NEO4J_dbms_tx__log_rotation_retention__policy:=${NEO4J_dbms_txLog_rotation_retentionPolicy:-"100M size"}}
: ${NEO4J_wrapper_java_additional:=${NEO4J_UDC_SOURCE:-"-Dneo4j.ext.udc.source=docker"}}
: ${NEO4J_dbms_unmanaged__extension__classes:=${NEO4J_dbms_unmanagedExtensionClasses:-}}
: ${NEO4J_dbms_allow__format__migration:=${NEO4J_dbms_allowFormatMigration:-}}
: ${NEO4J_dbms_connectors_default__advertised__address:=${NEO4J_dbms_connectors_defaultAdvertisedAddress:-}}
: ${NEO4J_ha_server__id:=${NEO4J_ha_serverId:-}}
: ${NEO4J_ha_initial__hosts:=${NEO4J_ha_initialHosts:-}}

if [ "${NEO4J_EDITION}" == "enterprise" ];
  then
   : ${NEO4J_causal__clustering_expected__core__cluster__size:=${NEO4J_causalClustering_expectedCoreClusterSize:-}}
   : ${NEO4J_causal__clustering_initial__discovery__members:=${NEO4J_causalClustering_initialDiscoveryMembers:-}}
   : ${NEO4J_causal__clustering_discovery__advertised__address:=${NEO4J_causalClustering_discoveryAdvertisedAddress:-"$(hostname):5000"}}
   : ${NEO4J_causal__clustering_transaction__advertised__address:=${NEO4J_causalClustering_transactionAdvertisedAddress:-"$(hostname):6000"}}
   : ${NEO4J_causal__clustering_raft__advertised__address:=${NEO4J_causalClustering_raftAdvertisedAddress:-"$(hostname):7000"}}
   # Custom settings for dockerized neo4j
   : ${NEO4J_ha_host_coordination:=$(hostname):5001}
   : ${NEO4J_ha_host_data:=$(hostname):6001}
   : ${NEO4J_causal__clustering_discovery__advertised__address:=$(hostname):5000}
   : ${NEO4J_causal__clustering_transaction__advertised__address:=$(hostname):6000}
   : ${NEO4J_causal__clustering_raft__advertised__address:=$(hostname):7000}
fi

# unset old hardcoded unsupported env variables
unset NEO4J_dbms_txLog_rotation_retentionPolicy NEO4J_UDC_SOURCE \
    NEO4J_dbms_unmanagedExtensionClasses NEO4J_dbms_allowFormatMigration \
    NEO4J_dbms_connectors_defaultAdvertisedAddress NEO4J_ha_serverId \
    NEO4J_ha_initialHosts NEO4J_causalClustering_expectedCoreClusterSize \
    NEO4J_causalClustering_initialDiscoveryMembers \
    NEO4J_causalClustering_discoveryListenAddress \
    NEO4J_causalClustering_discoveryAdvertisedAddress \
    NEO4J_causalClustering_transactionListenAddress \
    NEO4J_causalClustering_transactionAdvertisedAddress \
    NEO4J_causalClustering_raftListenAddress \
    NEO4J_causalClustering_raftAdvertisedAddress

if [ -d /conf ]; then
    if secure_mode_enabled; then
	    check_mounted_folder_readable "/conf"
    fi
    find /conf -type f -exec cp {} "${NEO4J_HOME}"/conf \;
fi

if [ -d /ssl ]; then
    if secure_mode_enabled; then
    	check_mounted_folder_readable "/ssl"
    fi
    : ${NEO4J_dbms_directories_certificates:="/ssl"}
fi

if [ -d /plugins ]; then
    if secure_mode_enabled; then
        if [[ ! -z "${NEO4JLABS_PLUGINS:-}" ]]; then
            # We need write permissions
            check_mounted_folder_with_chown "/plugins"
        fi
        check_mounted_folder_readable "/plugins"
    fi
    : ${NEO4J_dbms_directories_plugins:="/plugins"}
fi

if [ -d /import ]; then
    if secure_mode_enabled; then
        check_mounted_folder_readable "/import"
    fi
    : ${NEO4J_dbms_directories_import:="/import"}
fi

if [ -d /metrics ]; then
    if secure_mode_enabled; then
        check_mounted_folder_readable "/metrics"
    fi
    : ${NEO4J_dbms_directories_metrics:="/metrics"}
fi

if [ -d /logs ]; then
    check_mounted_folder_with_chown "/logs"
    : ${NEO4J_dbms_directories_logs:="/logs"}
    if [ -d /data/databases ]; then
        check_mounted_folder_with_chown "/data/databases"
    fi
    if [ -d /data/dbms ]; then
        check_mounted_folder_with_chown "/data/dbms"
    fi
fi

if [ -d /data ]; then
    check_mounted_folder_with_chown "/data"
fi


# set the neo4j initial password only if you run the database server
if [ "${cmd}" == "neo4j" ]; then
    if [ "${NEO4J_AUTH:-}" == "none" ]; then
        NEO4J_dbms_security_auth__enabled=false
    elif [[ "${NEO4J_AUTH:-}" == neo4j/* ]]; then
        password="${NEO4J_AUTH#neo4j/}"
        if [ "${password}" == "neo4j" ]; then
            echo >&2 "Invalid value for password. It cannot be 'neo4j', which is the default."
            exit 1
        fi

        if running_as_root; then
            # running set-initial-password as root will create subfolders to /data as root, causing startup fail when neo4j can't read or write the /data/dbms folder
            # creating the folder first will avoid that
            mkdir -p /data/dbms
            chown "${userid}":"${groupid}" /data/dbms
        fi
        # Will exit with error if users already exist (and print a message explaining that)
        # we probably don't want the message though, since it throws an error message on restarting the container.
        neo4j-admin set-initial-password "${password}" 2>/dev/null || true
    elif [ -n "${NEO4J_AUTH:-}" ]; then
        echo >&2 "Invalid value for NEO4J_AUTH: '${NEO4J_AUTH}'"
        exit 1
    fi
fi

declare -A COMMUNITY
declare -A ENTERPRISE

COMMUNITY=(
     [dbms.tx_log.rotation.retention_policy]="100M size"
     [dbms.memory.pagecache.size]="512M"
     [dbms.connectors.default_listen_address]="0.0.0.0"
     [dbms.connector.https.listen_address]="0.0.0.0:7473"
     [dbms.connector.http.listen_address]="0.0.0.0:7474"
     [dbms.connector.bolt.listen_address]="0.0.0.0:7687"
)

ENTERPRISE=(
)

for conf in ${!COMMUNITY[@]} ; do
    if ! grep -q "^$conf" "${NEO4J_HOME}"/conf/neo4j.conf
    then
        echo -e "\n"$conf=${COMMUNITY[$conf]} >> "${NEO4J_HOME}"/conf/neo4j.conf
    fi
done

for conf in ${!ENTERPRISE[@]} ; do
    if [ "${NEO4J_EDITION}" == "enterprise" ];
    then
       if ! grep -q "^$conf" "${NEO4J_HOME}"/conf/neo4j.conf
       then
        echo -e "\n"$conf=${ENTERPRISE[$conf]} >> "${NEO4J_HOME}"/conf/neo4j.conf
       fi
    fi
done

#The udc.source=tarball should be replaced by udc.source=docker in both dbms.jvm.additional and wrapper.java.additional
#Using sed to replace only this part will allow the custom configs to be added after, separated by a ,.
if grep -q "udc.source=tarball" "${NEO4J_HOME}"/conf/neo4j.conf; then
     sed -i -e 's/udc.source=tarball/udc.source=docker/g' "${NEO4J_HOME}"/conf/neo4j.conf
fi
#The udc.source should always be set to docker by default and we have to allow also custom configs to be added after that.
#In this case, this piece of code helps to add the default value and a , to support custom configs after.
if ! grep -q "dbms.jvm.additional=-Dunsupported.dbms.udc.source=docker" "${NEO4J_HOME}"/conf/neo4j.conf; then
  sed -i -e 's/dbms.jvm.additional=/dbms.jvm.additional=-Dunsupported.dbms.udc.source=docker,/g' "${NEO4J_HOME}"/conf/neo4j.conf
fi

# list env variables with prefix NEO4J_ and create settings from them
unset NEO4J_AUTH NEO4J_SHA256 NEO4J_TARBALL
for i in $( set | grep ^NEO4J_ | awk -F'=' '{print $1}' | sort -rn ); do
    setting=$(echo ${i} | sed 's|^NEO4J_||' | sed 's|_|.|g' | sed 's|\.\.|_|g')
    value=$(echo ${!i})
    # Don't allow settings with no value or settings that start with a number (neo4j converts settings to env variables and you cannot have an env variable that starts with a number)
    if [[ -n ${value} ]]; then
        if [[ ! "${setting}" =~ ^[0-9]+.*$ ]]; then
            if grep -q -F "${setting}=" "${NEO4J_HOME}"/conf/neo4j.conf; then
                # Remove any lines containing the setting already
                sed --in-place "/^${setting}=.*/d" "${NEO4J_HOME}"/conf/neo4j.conf
            fi
            # Then always append setting to file
            echo "${setting}=${value}" >> "${NEO4J_HOME}"/conf/neo4j.conf
        else
            echo >&2 "WARNING: ${setting} not written to conf file because settings that start with a number are not permitted"
        fi
    fi
done

if [[ ! -z "${NEO4JLABS_PLUGINS:-}" ]]; then
  # NEO4JLABS_PLUGINS should be a json array of plugins like '["graph-algorithms", "apoc", "streams", "graphql"]'
  install_neo4j_labs_plugins
fi

[ -f "${EXTENSION_SCRIPT:-}" ] && . ${EXTENSION_SCRIPT}

if [ "${cmd}" == "dump-config" ]; then
    if ! is_writable "/conf"; then
        print_permissions_advice_and_fail "/conf"
    fi
    cp --recursive "${NEO4J_HOME}"/conf/* /conf
    echo "Config Dumped"
    exit 0
fi

# Use su-exec to drop privileges to neo4j user
# Note that su-exec, despite its name, does not replicate the
# functionality of exec, so we need to use both
if [ "${cmd}" == "neo4j" ]; then
  ${exec_cmd} neo4j console
else
  ${exec_cmd} "$@"
fi

```

--------------------------------------------------------------------------------
/neo4j/docker-neo4j/src/test/java/com/neo4j/docker/coredb/TestAuthentication.java:
--------------------------------------------------------------------------------

```java
package com.neo4j.docker.coredb;

import com.neo4j.docker.coredb.configurations.Configuration;
import com.neo4j.docker.coredb.configurations.Setting;
import com.neo4j.docker.utils.DatabaseIO;
import com.neo4j.docker.utils.Neo4jVersion;
import com.neo4j.docker.utils.SetContainerUser;
import com.neo4j.docker.utils.WaitStrategies;
import com.neo4j.docker.utils.TemporaryFolderManager;
import com.neo4j.docker.utils.TestSettings;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testcontainers.containers.ContainerLaunchException;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.output.OutputFrame;
import org.testcontainers.containers.output.Slf4jLogConsumer;
import org.testcontainers.containers.output.WaitingConsumer;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.concurrent.TimeUnit;

public class TestAuthentication
{
    private static Logger log = LoggerFactory.getLogger(TestAuthentication.class);
    private static final String NEO4J_AUTH_FILE_ENV = "NEO4J_AUTH_PATH";

    @RegisterExtension
    public static TemporaryFolderManager temporaryFolderManager = new TemporaryFolderManager();

    private GenericContainer createContainer( boolean asCurrentUser )
    {
        GenericContainer container = new GenericContainer( TestSettings.IMAGE_ID );
        container.withEnv( "NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes" )
                 .withExposedPorts( 7474, 7687 )
                 .withLogConsumer( new Slf4jLogConsumer( log ) )
                 .waitingFor(WaitStrategies.waitForBoltReady());
        if(asCurrentUser)
        {
            SetContainerUser.nonRootUser( container );
        }
        return container;
    }

    private Path setInitialPasswordWithSecretsFile(GenericContainer container, String password) throws IOException
    {
        Path secretsFolder = temporaryFolderManager.createFolderAndMountAsVolume( container, "/secrets" );
        Files.writeString( secretsFolder.resolve( "passwordfile" ), "neo4j/"+password );
        container.withEnv( NEO4J_AUTH_FILE_ENV, "/secrets/passwordfile" );
        return secretsFolder;
    }

	@Test
	void testNoPassword() throws IOException
    {
		// we test that setting NEO4J_AUTH to "none" lets the database start in TestBasic.java,
        // but that does not test that we can read/write the database
		try(GenericContainer container = createContainer( false ))
		{
			container.withEnv( "NEO4J_AUTH", "none");
            temporaryFolderManager.createFolderAndMountAsVolume( container, "/data" );
            temporaryFolderManager.createFolderAndMountAsVolume( container, "/logs" );

			container.start();
            DatabaseIO db = new DatabaseIO(container);
            db.putInitialDataIntoContainer( "neo4j", "none" );
			db.verifyInitialDataInContainer( "neo4j", "none" );
		}
	}

	@Test
    void testPasswordCantBeNeo4j() throws Exception
    {
        try(GenericContainer failContainer = new GenericContainer( TestSettings.IMAGE_ID ).withLogConsumer( new Slf4jLogConsumer( log ) ))
        {
            if ( TestSettings.EDITION == TestSettings.Edition.ENTERPRISE )
            {
                failContainer.withEnv( "NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes" );
            }
            failContainer.withEnv( "NEO4J_AUTH", "neo4j/neo4j" );
            failContainer.start();

            WaitingConsumer waitingConsumer = new WaitingConsumer();
            failContainer.followOutput( waitingConsumer );

            Assertions.assertDoesNotThrow( () -> waitingConsumer.waitUntil(
                    frame -> frame.getUtf8String().contains("Invalid value for password" ), 20, TimeUnit.SECONDS ),
                                           "did not error due to invalid password" );
        }
    }

	@Test
	void testDefaultPasswordAndPasswordResetIfNoNeo4jAuthSet()
	{
		try(GenericContainer container = createContainer( true ))
        {
            log.info( "Starting first container as current user and not specifying NEO4J_AUTH" );
            container.waitingFor( WaitStrategies.waitForNeo4jReady( "neo4j" ) );
            container.start();
            DatabaseIO db = new DatabaseIO(container);
            // try with no password, this should fail because the default password should be applied with no NEO4J_AUTH env variable
			Assertions.assertThrows( org.neo4j.driver.exceptions.AuthenticationException.class,
									 () -> db.putInitialDataIntoContainer( "neo4j", "none" ),
									 "Able to access database with no password, even though NEO4J_AUTH=none was not specified!");
			Assertions.assertThrows( org.neo4j.driver.exceptions.ClientException.class,
									 () -> db.putInitialDataIntoContainer( "neo4j", "neo4j" ),
									 "Was not prompted for a new password when using default");
			db.changePassword( "neo4j", "neo4j", "newpassword" );
            db.putInitialDataIntoContainer( "neo4j", "newpassword" );
        }
	}

	@ParameterizedTest(name = "as_current_user_{0}")
    @ValueSource(booleans = {true, false})
    void testCanSetPassword( boolean asCurrentUser ) throws Exception
    {
        // create container and mount /data folder so that data can persist between sessions
        String password = "some_valid_password";
        Path dataMount;

        try(GenericContainer firstContainer = createContainer( asCurrentUser ))
		{
			firstContainer.withEnv( "NEO4J_AUTH", "neo4j/"+password )
                          .waitingFor(WaitStrategies.waitForNeo4jReady(password));
			dataMount = temporaryFolderManager.createFolderAndMountAsVolume(firstContainer, "/data");
			log.info( String.format( "Starting first container as %s user and setting password",
									 asCurrentUser? "current" : "default" ) );
            // create a database with stuff in
            firstContainer.start();
            DatabaseIO db = new DatabaseIO(firstContainer);
            db.putInitialDataIntoContainer( "neo4j", password );
        }

        // with a new container, check the database data.
        try(GenericContainer secondContainer = createContainer( asCurrentUser )
                .waitingFor(WaitStrategies.waitForNeo4jReady(password)))
        {
            temporaryFolderManager.mountHostFolderAsVolume( secondContainer, dataMount, "/data" );
            log.info( "starting new container with same /data mount as same user without setting password" );
            secondContainer.start();
            DatabaseIO db = new DatabaseIO(secondContainer);
            db.verifyInitialDataInContainer( "neo4j", password );
        }
    }

    @ParameterizedTest(name = "as_current_user_{0}")
    @ValueSource(booleans = {true, false})
    void testCanSetPasswordFromSecretsFile( boolean asCurrentUser ) throws Exception
    {
        String password = "some_valid_password";

        try(GenericContainer container = createContainer( asCurrentUser )
                .waitingFor(WaitStrategies.waitForNeo4jReady(password)))
		{
			setInitialPasswordWithSecretsFile( container, password );
			log.info( String.format( "Starting first container as %s user and setting password",
									 asCurrentUser? "current" : "default" ) );
            container.start();
            DatabaseIO db = new DatabaseIO(container);
            db.putInitialDataIntoContainer( "neo4j", password );
            db.verifyInitialDataInContainer( "neo4j", password );
        }
    }

    @Test
    void testSecretsFileTakesPriorityOverEnvAuthentication() throws Exception
    {
        String password = "some_valid_password";
        String wrongPassword = "not_the_password";

        try(GenericContainer container = createContainer(false )
                .waitingFor(WaitStrategies.waitForNeo4jReady(password)))
		{
            container.withEnv( "NEO4J_AUTH", "neo4j/" + wrongPassword );
			setInitialPasswordWithSecretsFile( container, password );
            container.start();
            DatabaseIO db = new DatabaseIO(container);
			Assertions.assertThrows( org.neo4j.driver.exceptions.AuthenticationException.class,
									 () -> db.putInitialDataIntoContainer("neo4j", wrongPassword),
									 "Able to access database with password set in the environment rather than secrets");

            db.putInitialDataIntoContainer( "neo4j", password );
            db.verifyInitialDataInContainer( "neo4j", password );
        }
    }

    @Test
    void testFailsIfSecretsFileSetButMissing()
    {
        try(GenericContainer failContainer = createContainer( false ))
		{
            WaitStrategies.waitUntilContainerFinished( failContainer, Duration.ofSeconds( 30 ) );
            failContainer.withEnv( NEO4J_AUTH_FILE_ENV, "/secrets/doesnotexist.secret" );

            Assertions.assertThrows( ContainerLaunchException.class, failContainer::start,
                                     "Neo4j started even though the password file does not exist" );
            // an error message should be printed to stderr
            String errors = failContainer.getLogs( OutputFrame.OutputType.STDERR);
            Assertions.assertTrue( errors.contains( "The password file '/secrets/doesnotexist.secret' does not exist" ),
                                   "Did not error about missing password file. Actual error was: "+errors);
        }
    }

    @Test
    void testCanSetPasswordWithDebugging() throws Exception
    {
        String password = "some_valid_password";

        try ( GenericContainer container = createContainer( false  ) )
        {
            container.withEnv( "NEO4J_AUTH", "neo4j/" + password )
                     .withEnv( "NEO4J_DEBUG", "yes" )
                     .waitingFor(WaitStrategies.waitForNeo4jReady( password ));
            // create a database with stuff in
            container.start();
            DatabaseIO db = new DatabaseIO( container );
            db.putInitialDataIntoContainer( "neo4j", password );
        }
    }

    @ParameterizedTest(name = "as_current_user_{0}")
    @ValueSource(booleans = {true, false})
    void testSettingNeo4jAuthDoesntOverrideExistingPassword( boolean asCurrentUser ) throws Exception
    {
        String password = "some_valid_password";
        Path dataMount;

		try(GenericContainer firstContainer = createContainer( asCurrentUser ))
		{
			firstContainer.withEnv( "NEO4J_AUTH", "neo4j/"+password )
                          .waitingFor(WaitStrategies.waitForNeo4jReady( password));
			dataMount = temporaryFolderManager.createFolderAndMountAsVolume(firstContainer, "/data");

			// create a database with stuff in
			log.info( String.format( "Starting first container as %s user and setting password",
									 asCurrentUser? "current" : "default" ) );
			firstContainer.start();
			DatabaseIO db = new DatabaseIO(firstContainer);
			db.putInitialDataIntoContainer( "neo4j", password );
        }

        // with a new container, check the database data.
        try(GenericContainer secondContainer = createContainer( asCurrentUser ))
        {
            String wrongPassword = "not_the_password";
            secondContainer.withEnv( "NEO4J_AUTH", "neo4j/"+wrongPassword );
            temporaryFolderManager.mountHostFolderAsVolume( secondContainer, dataMount, "/data" );
            log.info( "starting new container with same /data mount as same user without setting password" );
            secondContainer.start();
            DatabaseIO db = new DatabaseIO(secondContainer);
            db.verifyInitialDataInContainer( "neo4j", password );
        Assertions.assertThrows( org.neo4j.driver.exceptions.AuthenticationException.class,
                () -> db.verifyConnectivity( "neo4j", wrongPassword) );
        }
    }

    @Test
    void testPromptsForPasswordReset()
    {
        Assumptions.assumeTrue( TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 3,6,0 ) ),
                                "Require password reset is only a feature in 3.6 onwards");
        try(GenericContainer container = createContainer( false ))
        {
            String user = "neo4j";
            String intialPass = "apassword";
            String resetPass = "new_password";
            container.withEnv("NEO4J_AUTH", user+"/"+intialPass+"/true" )
                     .waitingFor(   WaitStrategies.waitForNeo4jReady( intialPass ) );
            container.start();
            DatabaseIO db = new DatabaseIO(container);
            Assertions.assertThrows( org.neo4j.driver.exceptions.ClientException.class,
                                     () -> db.putInitialDataIntoContainer( user, intialPass ),
                                     "Neo4j did not error because of password reset requirement");

            db.changePassword( user, intialPass, resetPass );
            db.putInitialDataIntoContainer( user, resetPass );
            db.verifyInitialDataInContainer( user, resetPass );
        }
    }

    @ParameterizedTest(name = "use_secrets_file_{0}")
    @ValueSource(booleans = {true, false})
    void testWarnAndFailIfPasswordLessThan8Chars(boolean usePasswordFile) throws Exception
    {
        Assumptions.assumeTrue( TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 5,2,0 ) ),
                                "Minimum password length introduced in 5.2.0");
        String shortPassword = "123";
        try(GenericContainer failContainer = createContainer( false ))
        {
            if(usePasswordFile)
            {
                setInitialPasswordWithSecretsFile( failContainer, shortPassword );
            }
            else
            {
                failContainer.withEnv( "NEO4J_AUTH", "neo4j/"+shortPassword );
            }
            WaitStrategies.waitUntilContainerFinished( failContainer, Duration.ofSeconds( 30 ) );
            Assertions.assertThrows( ContainerLaunchException.class, failContainer::start,
                                     "Neo4j started even though initial password was too short" );
            String logsOut = failContainer.getLogs();
            Assertions.assertTrue( logsOut.contains( "Invalid value for password" ),
                                   "did not error due to too short password");
            Assertions.assertFalse( logsOut.contains( "Remote interface available at http://localhost:7474/" ),
                                    "Neo4j started even though an invalid password was set");
        }
    }

    @ParameterizedTest(name = "use_secrets_file_{0}")
    @ValueSource(booleans = {true, false})
    void testWarnAndFailIfPasswordLessThanOverride(boolean usePasswordFile) throws Exception
    {
        Assumptions.assumeTrue( TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 5,2,0 ) ),
                                "Minimum password length introduced in 5.2.0");
        String shortPassword = "123";
        try(GenericContainer failContainer = createContainer( false ))
        {
            if(usePasswordFile)
            {
                setInitialPasswordWithSecretsFile( failContainer, shortPassword );
            }
            else
            {
                failContainer.withEnv( "NEO4J_AUTH", "neo4j/"+shortPassword );
            }

            WaitStrategies.waitUntilContainerFinished( failContainer, Duration.ofSeconds( 30 ) )
                          .withEnv(Configuration.getConfigurationNameMap().get( Setting.MINIMUM_PASSWORD_LENGTH ).envName, "20");
            Assertions.assertThrows( ContainerLaunchException.class, failContainer::start,
                                     "Neo4j started even though initial password was too short" );
            String logsOut = failContainer.getLogs();
            Assertions.assertTrue( logsOut.contains( "Invalid value for password" ),
                                   "did not error due to too short password");
            Assertions.assertFalse( logsOut.contains( "Remote interface available at http://localhost:7474/" ),
                                    "Neo4j started even though an invalid password was set");
        }
    }

    @ParameterizedTest(name = "use_secrets_file_{0}")
    @ValueSource(booleans = {true, false})
    void shouldNotWarnAboutMinimumPasswordLengthIfSettingOverridden_env(boolean usePasswordFile) throws Exception
    {
        Assumptions.assumeTrue( TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 5,2,0 ) ),
                                "Minimum password length introduced in 5.2.0");
        String shortPassword = "123";
        try(GenericContainer container = createContainer( false ))
        {
            if(usePasswordFile)
            {
                setInitialPasswordWithSecretsFile( container, shortPassword );
            }
            else
            {
                container.withEnv( "NEO4J_AUTH", "neo4j/"+shortPassword );
            }
            container.withEnv(Configuration.getConfigurationNameMap().get( Setting.MINIMUM_PASSWORD_LENGTH ).envName, "2");
            verifyDoesNotWarnAboutMinimumPasswordLengthIfSettingOverridden( container, shortPassword );
        }
    }

    @ParameterizedTest(name = "use_secrets_file_{0}")
    @ValueSource(booleans = {true, false})
    void shouldNotWarnAboutMinimumPasswordLengthIfSettingOverridden_conf(boolean usePasswordFile) throws Exception
    {
        Assumptions.assumeTrue( TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 5,2,0 ) ),
                                "Minimum password length introduced in 5.2.0");
        String shortPassword = "123";
        try(GenericContainer container = createContainer( false ))
        {
            if(usePasswordFile)
            {
                setInitialPasswordWithSecretsFile( container, shortPassword );
            }
            else
            {
                container.withEnv( "NEO4J_AUTH", "neo4j/"+shortPassword );
            }
            Path confMount = temporaryFolderManager.createFolderAndMountAsVolume(container, "/conf");
            Files.writeString(confMount.resolve( "neo4j.conf" ),
                              Configuration.getConfigurationNameMap().get( Setting.MINIMUM_PASSWORD_LENGTH ).name+"=2");
            verifyDoesNotWarnAboutMinimumPasswordLengthIfSettingOverridden( container, shortPassword );
        }
    }

    private void verifyDoesNotWarnAboutMinimumPasswordLengthIfSettingOverridden(GenericContainer container, String password) throws Exception
    {
            container.start();
            String logs = container.getLogs();
            Assertions.assertFalse( logs.contains( "Invalid value for password. The minimum password length is 8 characters." ),
                                    "Should not error about minimum password length if overridden.");
            DatabaseIO db = new DatabaseIO( container );
            db.putInitialDataIntoContainer( "neo4j", password );
            db.verifyInitialDataInContainer( "neo4j", password );
    }
}

```

--------------------------------------------------------------------------------
/neo4j/docker-neo4j/docker-image-src/3.3/docker-entrypoint.sh:
--------------------------------------------------------------------------------

```bash
#!/bin/bash -eu

cmd="$1"

function running_as_root
{
    test "$(id -u)" = "0"
}

function secure_mode_enabled
{
    test "${SECURE_FILE_PERMISSIONS:=no}" = "yes"
}

function containsElement
{
  local e match="$1"
  shift
  for e; do [[ "$e" == "$match" ]] && return 0; done
  return 1
}

function is_readable
{
    # this code is fairly ugly but works no matter who this script is running as.
    # It would be nice if the writability tests could use this logic somehow.
    local _file=${1}
    perm=$(stat -c %a "${_file}")

    # everyone permission
    if [[ ${perm:2:1} -ge 4 ]]; then
        return 0
    fi
    # owner permissions
    if [[ ${perm:0:1} -ge 4 ]]; then
        if [[ "$(stat -c %U ${_file})" = "${userid}" ]] || [[ "$(stat -c %u ${_file})" = "${userid}" ]]; then
            return 0
        fi
    fi
    # group permissions
    if [[ ${perm:1:1} -ge 4 ]]; then
        if containsElement "$(stat -c %g ${_file})" "${groups[@]}" || containsElement "$(stat -c %G ${_file})" "${groups[@]}" ; then
            return 0
        fi
    fi
    return 1
}

function is_writable
{
    # It would be nice if this and the is_readable function could combine somehow
    local _file=${1}
    perm=$(stat -c %a "${_file}")

    # everyone permission
    if containsElement ${perm:2:1} 2 3 6 7; then
        return 0
    fi
    # owner permissions
    if containsElement ${perm:0:1} 2 3 6 7; then
        if [[ "$(stat -c %U ${_file})" = "${userid}" ]] || [[ "$(stat -c %u ${_file})" = "${userid}" ]]; then
            return 0
        fi
    fi
    # group permissions
    if containsElement ${perm:1:1} 2 3 6 7; then
        if containsElement "$(stat -c %g ${_file})" "${groups[@]}" || containsElement "$(stat -c %G ${_file})" "${groups[@]}" ; then
            return 0
        fi
    fi
    return 1
}


function print_permissions_advice_and_fail
{
    _directory=${1}
    echo >&2 "
Folder ${_directory} is not accessible for user: ${userid} or group ${groupid} or groups ${groups[@]}, this is commonly a file permissions issue on the mounted folder.

Hints to solve the issue:
1) Make sure the folder exists before mounting it. Docker will create the folder using root permissions before starting the Neo4j container. The root permissions disallow Neo4j from writing to the mounted folder.
2) Pass the folder owner's user ID and group ID to docker run, so that docker runs as that user.
If the folder is owned by the current user, this can be done by adding this flag to your docker run command:
  --user=\$(id -u):\$(id -g)
       "
    exit 1
}

function check_mounted_folder_readable
{
    local _directory=${1}
    if ! is_readable "${_directory}"; then
        print_permissions_advice_and_fail "${_directory}"
    fi
}

function check_mounted_folder_with_chown
{
# The /data and /log directory are a bit different because they are very likely to be mounted by the user but not
# necessarily writable.
# This depends on whether a user ID is passed to the container and which folders are mounted.
#
#   No user ID passed to container:
#   1) No folders are mounted.
#      The /data and /log folder are owned by neo4j by default, so should be writable already.
#   2) Both /log and /data are mounted.
#      This means on start up, /data and /logs are owned by an unknown user and we should chown them to neo4j for
#      backwards compatibility.
#
#   User ID passed to container:
#   1) Both /data and /logs are mounted
#      The /data and /logs folders are owned by an unknown user but we *should* have rw permission to them.
#      That should be verified and error (helpfully) if not.
#   2) User mounts /data or /logs *but not both*
#      The  unmounted folder is still owned by neo4j, which should already be writable. The mounted folder should
#      have rw permissions through user id. This should be verified.
#   3) No folders are mounted.
#      The /data and /log folder are owned by neo4j by default, and these are already writable by the user.
#      (This is a very unlikely use case).

    local mountFolder=${1}
    if running_as_root; then
        if ! is_writable "${mountFolder}" && ! secure_mode_enabled; then
            # warn that we're about to chown the folder and then chown it
            echo "Warning: Folder mounted to \"${mountFolder}\" is not writable from inside container. Changing folder owner to ${userid}."
            chown -R "${userid}":"${groupid}" "${mountFolder}"
        fi
    else
        if [[ ! -w "${mountFolder}" ]]  && [[ "$(stat -c %U ${mountFolder})" != "neo4j" ]]; then
            print_permissions_advice_and_fail "${mountFolder}"
        fi
    fi
}

function load_plugin_from_github
{
  # Load a plugin at runtime. The provided github repository must have a versions.json on the master branch with the
  # correct format.
  local _plugin_name="${1}" #e.g. apoc, graph-algorithms, graph-ql

  local _plugins_dir="${NEO4J_HOME}/plugins"
  if [ -d /plugins ]; then
    local _plugins_dir="/plugins"
  fi
  local _versions_json_url="$(jq --raw-output "with_entries( select(.key==\"${_plugin_name}\") ) | to_entries[] | .value.versions" /neo4j-plugins.json )"
  # Using the same name for the plugin irrespective of version ensures we don't end up with different versions of the same plugin
  local _destination="${_plugins_dir}/${_plugin_name}.jar"
  local _neo4j_version="$(neo4j --version | cut -d' ' -f2)"

  # Now we call out to github to get the versions.json for this plugin and we parse that to find the url for the correct plugin jar for our neo4j version
  echo "Fetching versions.json for Plugin '${_plugin_name}' from ${_versions_json_url}"
  local _versions_json="$(curl --silent --show-error --fail --retry 30 --retry-max-time 300 -L "${_versions_json_url}")"
  local _plugin_jar_url="$(echo "${_versions_json}" | jq --raw-output ".[] | select(.neo4j==\"${_neo4j_version}\") | .jar")"
  if [[ -z "${_plugin_jar_url}" ]]; then
    echo >&2 "Error: No jar URL found for version '${_neo4j_version}' in versions.json from '${_versions_json_url}'"
    echo >&2 "${_versions_json}"
    exit 1
  fi
  echo "Installing Plugin '${_plugin_name}' from ${_plugin_jar_url} to ${_destination} "
  curl --silent --show-error --fail --retry 30 --retry-max-time 300 -L -o "${_destination}" "${_plugin_jar_url}"

  if ! is_readable "${_destination}"; then
    echo >&2 "Plugin at '${_destination}' is not readable"
    exit 1
  fi
}

function apply_plugin_default_configuration
{
  # Set the correct Load a plugin at runtime. The provided github repository must have a versions.json on the master branch with the
  # correct format.
  local _plugin_name="${1}" #e.g. apoc, graph-algorithms, graph-ql
  local _reference_conf="${2}" # used to determine if we can override properties
  local _neo4j_conf="${NEO4J_HOME}/conf/neo4j.conf"

  local _property _value
  echo "Applying default values for plugin ${_plugin_name} to neo4j.conf"
  for _entry in $(jq  --compact-output --raw-output "with_entries( select(.key==\"${_plugin_name}\") ) | to_entries[] | .value.properties | to_entries[]" /neo4j-plugins.json); do
    _property="$(jq --raw-output '.key' <<< "${_entry}")"
    _value="$(jq --raw-output '.value' <<< "${_entry}")"

    # the first grep strips out comments
    if grep -o "^[^#]*" "${_reference_conf}" | grep -q --fixed-strings "${_property}=" ; then
      # property is already set in the user provided config. In this case we don't override what has been set explicitly by the user.
      echo "Skipping ${_property} for plugin ${_plugin_name} because it is already set"
    else
      if grep -o "^[^#]*" "${_neo4j_conf}" | grep -q --fixed-strings "${_property}=" ; then
        sed --in-place "s/${_property}=/&${_value},/" "${_neo4j_conf}"
      else
        echo "${_property}=${_value}" >> "${_neo4j_conf}"
      fi
    fi
  done
}

function install_neo4j_labs_plugins
{
  # We store a copy of the config before we modify it for the plugins to allow us to see if there are user-set values in the input config that we shouldn't override
  local _old_config="$(mktemp)"
  cp "${NEO4J_HOME}"/conf/neo4j.conf "${_old_config}"
  for plugin_name in $(echo "${NEO4JLABS_PLUGINS}" | jq --raw-output '.[]'); do
    load_plugin_from_github "${plugin_name}"
    apply_plugin_default_configuration "${plugin_name}" "${_old_config}"
  done
  rm "${_old_config}"
}

# If we're running as root, then run as the neo4j user. Otherwise
# docker is running with --user and we simply use that user.  Note
# that su-exec, despite its name, does not replicate the functionality
# of exec, so we need to use both
if running_as_root; then
  userid="neo4j"
  groupid="neo4j"
  groups=($(id -G neo4j))
  exec_cmd="exec gosu neo4j:neo4j"
else
  userid="$(id -u)"
  groupid="$(id -g)"
  groups=($(id -G))
  exec_cmd="exec"
fi
readonly userid
readonly groupid
readonly groups
readonly exec_cmd


# Need to chown the home directory - but a user might have mounted a
# volume here (notably a conf volume). So take care not to chown
# volumes (stuff not owned by neo4j)
if running_as_root; then
    # Non-recursive chown for the base directory
    chown "${userid}":"${groupid}" "${NEO4J_HOME}"
    chmod 700 "${NEO4J_HOME}"
    find "${NEO4J_HOME}" -mindepth 1 -maxdepth 1 -user root -type d -exec chown -R ${userid}:${groupid} {} \;
    find "${NEO4J_HOME}" -mindepth 1 -maxdepth 1 -user root -type d -exec chmod -R 700 {} \;
fi

# Only prompt for license agreement if command contains "neo4j" in it
if [[ "${cmd}" == *"neo4j"* ]]; then
  if [ "${NEO4J_EDITION}" == "enterprise" ]; then
    if [ "${NEO4J_ACCEPT_LICENSE_AGREEMENT:=no}" != "yes" ]; then
      echo >&2 "
In order to use Neo4j Enterprise Edition you must accept the license agreement.

(c) Neo4j Sweden AB. 2021.  All Rights Reserved.
Use of this Software without a proper commercial license with Neo4j,
Inc. or its affiliates is prohibited.

Email inquiries can be directed to: [email protected]

More information is also available at: https://neo4j.com/licensing/


To accept the license agreement set the environment variable
NEO4J_ACCEPT_LICENSE_AGREEMENT=yes

To do this you can use the following docker argument:

        --env=NEO4J_ACCEPT_LICENSE_AGREEMENT=yes
"
      exit 1
    fi
  fi
fi

# Env variable naming convention:
# - prefix NEO4J_
# - double underscore char '__' instead of single underscore '_' char in the setting name
# - underscore char '_' instead of dot '.' char in the setting name
# Example:
# NEO4J_dbms_tx__log_rotation_retention__policy env variable to set
#       dbms.tx_log.rotation.retention_policy setting

# Backward compatibility - map old hardcoded env variables into new naming convention (if they aren't set already)
# Set some to default values if unset
: ${NEO4J_dbms_tx__log_rotation_retention__policy:=${NEO4J_dbms_txLog_rotation_retentionPolicy:-"100M size"}}
: ${NEO4J_wrapper_java_additional:=${NEO4J_UDC_SOURCE:-"-Dneo4j.ext.udc.source=docker"}}
: ${NEO4J_dbms_unmanaged__extension__classes:=${NEO4J_dbms_unmanagedExtensionClasses:-}}
: ${NEO4J_dbms_allow__format__migration:=${NEO4J_dbms_allowFormatMigration:-}}
: ${NEO4J_dbms_connectors_default__advertised__address:=${NEO4J_dbms_connectors_defaultAdvertisedAddress:-}}
: ${NEO4J_ha_server__id:=${NEO4J_ha_serverId:-}}
: ${NEO4J_ha_initial__hosts:=${NEO4J_ha_initialHosts:-}}

if [ "${NEO4J_EDITION}" == "enterprise" ];
  then
   : ${NEO4J_causal__clustering_expected__core__cluster__size:=${NEO4J_causalClustering_expectedCoreClusterSize:-}}
   : ${NEO4J_causal__clustering_initial__discovery__members:=${NEO4J_causalClustering_initialDiscoveryMembers:-}}
   : ${NEO4J_causal__clustering_discovery__advertised__address:=${NEO4J_causalClustering_discoveryAdvertisedAddress:-"$(hostname):5000"}}
   : ${NEO4J_causal__clustering_transaction__advertised__address:=${NEO4J_causalClustering_transactionAdvertisedAddress:-"$(hostname):6000"}}
   : ${NEO4J_causal__clustering_raft__advertised__address:=${NEO4J_causalClustering_raftAdvertisedAddress:-"$(hostname):7000"}}
   # Custom settings for dockerized neo4j
   : ${NEO4J_ha_host_coordination:=$(hostname):5001}
   : ${NEO4J_ha_host_data:=$(hostname):6001}
   : ${NEO4J_causal__clustering_discovery__advertised__address:=$(hostname):5000}
   : ${NEO4J_causal__clustering_transaction__advertised__address:=$(hostname):6000}
   : ${NEO4J_causal__clustering_raft__advertised__address:=$(hostname):7000}
fi

# unset old hardcoded unsupported env variables
unset NEO4J_dbms_txLog_rotation_retentionPolicy NEO4J_UDC_SOURCE \
    NEO4J_dbms_unmanagedExtensionClasses NEO4J_dbms_allowFormatMigration \
    NEO4J_dbms_connectors_defaultAdvertisedAddress NEO4J_ha_serverId \
    NEO4J_ha_initialHosts NEO4J_causalClustering_expectedCoreClusterSize \
    NEO4J_causalClustering_initialDiscoveryMembers \
    NEO4J_causalClustering_discoveryListenAddress \
    NEO4J_causalClustering_discoveryAdvertisedAddress \
    NEO4J_causalClustering_transactionListenAddress \
    NEO4J_causalClustering_transactionAdvertisedAddress \
    NEO4J_causalClustering_raftListenAddress \
    NEO4J_causalClustering_raftAdvertisedAddress

if [ -d /conf ]; then
    if secure_mode_enabled; then
	    check_mounted_folder_readable "/conf"
    fi
    find /conf -type f -exec cp {} "${NEO4J_HOME}"/conf \;
fi

if [ -d /ssl ]; then
    if secure_mode_enabled; then
    	check_mounted_folder_readable "/ssl"
    fi
    : ${NEO4J_dbms_directories_certificates:="/ssl"}
fi

if [ -d /plugins ]; then
    if secure_mode_enabled; then
        if [[ ! -z "${NEO4JLABS_PLUGINS:-}" ]]; then
            # We need write permissions
            check_mounted_folder_with_chown "/plugins"
        fi
        check_mounted_folder_readable "/plugins"
    fi
    : ${NEO4J_dbms_directories_plugins:="/plugins"}
fi

if [ -d /import ]; then
    if secure_mode_enabled; then
        check_mounted_folder_readable "/import"
    fi
    : ${NEO4J_dbms_directories_import:="/import"}
fi

if [ -d /metrics ]; then
    if secure_mode_enabled; then
        check_mounted_folder_readable "/metrics"
    fi
    : ${NEO4J_dbms_directories_metrics:="/metrics"}
fi

if [ -d /logs ]; then
    check_mounted_folder_with_chown "/logs"
    : ${NEO4J_dbms_directories_logs:="/logs"}
    if [ -d /data/databases ]; then
        check_mounted_folder_with_chown "/data/databases"
    fi
    if [ -d /data/dbms ]; then
        check_mounted_folder_with_chown "/data/dbms"
    fi
fi

if [ -d /data ]; then
    check_mounted_folder_with_chown "/data"
fi


# set the neo4j initial password only if you run the database server
if [ "${cmd}" == "neo4j" ]; then
    if [ "${NEO4J_AUTH:-}" == "none" ]; then
        NEO4J_dbms_security_auth__enabled=false
    elif [[ "${NEO4J_AUTH:-}" == neo4j/* ]]; then
        password="${NEO4J_AUTH#neo4j/}"
        if [ "${password}" == "neo4j" ]; then
            echo >&2 "Invalid value for password. It cannot be 'neo4j', which is the default."
            exit 1
        fi

        if running_as_root; then
            # running set-initial-password as root will create subfolders to /data as root, causing startup fail when neo4j can't read or write the /data/dbms folder
            # creating the folder first will avoid that
            mkdir -p /data/dbms
            chown "${userid}":"${groupid}" /data/dbms
        fi
        # Will exit with error if users already exist (and print a message explaining that)
        # we probably don't want the message though, since it throws an error message on restarting the container.
        neo4j-admin set-initial-password "${password}" 2>/dev/null || true
    elif [ -n "${NEO4J_AUTH:-}" ]; then
        echo >&2 "Invalid value for NEO4J_AUTH: '${NEO4J_AUTH}'"
        exit 1
    fi
fi

declare -A COMMUNITY
declare -A ENTERPRISE

COMMUNITY=(
     [dbms.tx_log.rotation.retention_policy]="100M size"
     [dbms.memory.pagecache.size]="512M"
     [dbms.connectors.default_listen_address]="0.0.0.0"
     [dbms.connector.https.listen_address]="0.0.0.0:7473"
     [dbms.connector.http.listen_address]="0.0.0.0:7474"
     [dbms.connector.bolt.listen_address]="0.0.0.0:7687"
)

ENTERPRISE=(
     [causal_clustering.transaction_listen_address]="0.0.0.0:6000"
     [causal_clustering.raft_listen_address]="0.0.0.0:7000"
     [causal_clustering.discovery_listen_address]="0.0.0.0:5000"
)

for conf in ${!COMMUNITY[@]} ; do

    if ! grep -q "^$conf" "${NEO4J_HOME}"/conf/neo4j.conf
    then
        echo -e "\n"$conf=${COMMUNITY[$conf]} >> "${NEO4J_HOME}"/conf/neo4j.conf
    fi
done

for conf in ${!ENTERPRISE[@]} ; do

    if [ "${NEO4J_EDITION}" == "enterprise" ];
    then
       if ! grep -q "^$conf" "${NEO4J_HOME}"/conf/neo4j.conf
       then
        echo -e "\n"$conf=${ENTERPRISE[$conf]} >> "${NEO4J_HOME}"/conf/neo4j.conf
       fi
    fi
done

#The udc.source=tarball should be replaced by udc.source=docker in both dbms.jvm.additional and wrapper.java.additional
#Using sed to replace only this part will allow the custom configs to be added after, separated by a ,.
if grep -q "udc.source=tarball" "${NEO4J_HOME}"/conf/neo4j.conf; then
     sed -i -e 's/udc.source=tarball/udc.source=docker/g' "${NEO4J_HOME}"/conf/neo4j.conf
fi
#The udc.source should always be set to docker by default and we have to allow also custom configs to be added after that.
#In this case, this piece of code helps to add the default value and a , to support custom configs after.
if ! grep -q "dbms.jvm.additional=-Dunsupported.dbms.udc.source=docker" "${NEO4J_HOME}"/conf/neo4j.conf; then
  sed -i -e 's/dbms.jvm.additional=/dbms.jvm.additional=-Dunsupported.dbms.udc.source=docker,/g' "${NEO4J_HOME}"/conf/neo4j.conf
fi

# list env variables with prefix NEO4J_ and create settings from them
unset NEO4J_AUTH NEO4J_SHA256 NEO4J_TARBALL
for i in $( set | grep ^NEO4J_ | awk -F'=' '{print $1}' | sort -rn ); do
    setting=$(echo ${i} | sed 's|^NEO4J_||' | sed 's|_|.|g' | sed 's|\.\.|_|g')
    value=$(echo ${!i})
    # Don't allow settings with no value or settings that start with a number (neo4j converts settings to env variables and you cannot have an env variable that starts with a number)
    if [[ -n ${value} ]]; then
        if [[ ! "${setting}" =~ ^[0-9]+.*$ ]]; then
            if grep -q -F "${setting}=" "${NEO4J_HOME}"/conf/neo4j.conf; then
                # Remove any lines containing the setting already
                sed --in-place "/^${setting}=.*/d" "${NEO4J_HOME}"/conf/neo4j.conf
            fi
            # Then always append setting to file
            echo "${setting}=${value}" >> "${NEO4J_HOME}"/conf/neo4j.conf
        else
            echo >&2 "WARNING: ${setting} not written to conf file because settings that start with a number are not permitted"
        fi
    fi
done

if [[ ! -z "${NEO4JLABS_PLUGINS:-}" ]]; then
  # NEO4JLABS_PLUGINS should be a json array of plugins like '["graph-algorithms", "apoc", "streams", "graphql"]'
  install_neo4j_labs_plugins
fi

[ -f "${EXTENSION_SCRIPT:-}" ] && . ${EXTENSION_SCRIPT}

if [ "${cmd}" == "dump-config" ]; then
    if ! is_writable "/conf"; then
        print_permissions_advice_and_fail "/conf"
    fi
    cp --recursive "${NEO4J_HOME}"/conf/* /conf
    echo "Config Dumped"
    exit 0
fi

# Use su-exec to drop privileges to neo4j user
# Note that su-exec, despite its name, does not replicate the
# functionality of exec, so we need to use both
if [ "${cmd}" == "neo4j" ]; then
  ${exec_cmd} neo4j console
else
  ${exec_cmd} "$@"
fi

```

--------------------------------------------------------------------------------
/neo4j/docker-neo4j/docker-image-src/4.0/coredb/docker-entrypoint.sh:
--------------------------------------------------------------------------------

```bash
#!/bin/bash -eu

cmd="$1"

function running_as_root
{
    test "$(id -u)" = "0"
}

function secure_mode_enabled
{
    test "${SECURE_FILE_PERMISSIONS:=no}" = "yes"
}

function containsElement
{
  local e match="$1"
  shift
  for e; do [[ "$e" == "$match" ]] && return 0; done
  return 1
}

function is_readable
{
    # this code is fairly ugly but works no matter who this script is running as.
    # It would be nice if the writability tests could use this logic somehow.
    local _file=${1}
    perm=$(stat -c %a "${_file}")

    # everyone permission
    if [[ ${perm:2:1} -ge 4 ]]; then
        return 0
    fi
    # owner permissions
    if [[ ${perm:0:1} -ge 4 ]]; then
        if [[ "$(stat -c %U ${_file})" = "${userid}" ]] || [[ "$(stat -c %u ${_file})" = "${userid}" ]]; then
            return 0
        fi
    fi
    # group permissions
    if [[ ${perm:1:1} -ge 4 ]]; then
        if containsElement "$(stat -c %g ${_file})" "${groups[@]}" || containsElement "$(stat -c %G ${_file})" "${groups[@]}" ; then
            return 0
        fi
    fi
    return 1
}

function is_writable
{
    # It would be nice if this and the is_readable function could combine somehow
    local _file=${1}
    perm=$(stat -c %a "${_file}")

    # everyone permission
    if containsElement ${perm:2:1} 2 3 6 7; then
        return 0
    fi
    # owner permissions
    if containsElement ${perm:0:1} 2 3 6 7; then
        if [[ "$(stat -c %U ${_file})" = "${userid}" ]] || [[ "$(stat -c %u ${_file})" = "${userid}" ]]; then
            return 0
        fi
    fi
    # group permissions
    if containsElement ${perm:1:1} 2 3 6 7; then
        if containsElement "$(stat -c %g ${_file})" "${groups[@]}" || containsElement "$(stat -c %G ${_file})" "${groups[@]}" ; then
            return 0
        fi
    fi
    return 1
}


function print_permissions_advice_and_fail
{
    _directory=${1}
    echo >&2 "
Folder ${_directory} is not accessible for user: ${userid} or group ${groupid} or groups ${groups[@]}, this is commonly a file permissions issue on the mounted folder.

Hints to solve the issue:
1) Make sure the folder exists before mounting it. Docker will create the folder using root permissions before starting the Neo4j container. The root permissions disallow Neo4j from writing to the mounted folder.
2) Pass the folder owner's user ID and group ID to docker run, so that docker runs as that user.
If the folder is owned by the current user, this can be done by adding this flag to your docker run command:
  --user=\$(id -u):\$(id -g)
       "
    exit 1
}

function check_mounted_folder_readable
{
    local _directory=${1}
    if ! is_readable "${_directory}"; then
        print_permissions_advice_and_fail "${_directory}"
    fi
}

function check_mounted_folder_writable_with_chown
{
# The /data and /log directory are a bit different because they are very likely to be mounted by the user but not
# necessarily writable.
# This depends on whether a user ID is passed to the container and which folders are mounted.
#
#   No user ID passed to container:
#   1) No folders are mounted.
#      The /data and /log folder are owned by neo4j by default, so should be writable already.
#   2) Both /log and /data are mounted.
#      This means on start up, /data and /logs are owned by an unknown user and we should chown them to neo4j for
#      backwards compatibility.
#
#   User ID passed to container:
#   1) Both /data and /logs are mounted
#      The /data and /logs folders are owned by an unknown user but we *should* have rw permission to them.
#      That should be verified and error (helpfully) if not.
#   2) User mounts /data or /logs *but not both*
#      The  unmounted folder is still owned by neo4j, which should already be writable. The mounted folder should
#      have rw permissions through user id. This should be verified.
#   3) No folders are mounted.
#      The /data and /log folder are owned by neo4j by default, and these are already writable by the user.
#      (This is a very unlikely use case).

    local mountFolder=${1}
    if running_as_root; then
        if ! secure_mode_enabled; then
            # check folder permissions
            if ! is_writable "${mountFolder}" ;  then
                # warn that we're about to chown the folder and then chown it
                echo "Warning: Folder mounted to \"${mountFolder}\" is not writable from inside container. Changing folder owner to ${userid}."
                chown -R "${userid}":"${groupid}" "${mountFolder}"
            # check permissions on files in the folder
            elif [ $(gosu "${userid}":"${groupid}" find "${mountFolder}" -not -writable | wc -l) -gt 0 ]; then
                echo "Warning: Some files inside \"${mountFolder}\" are not writable from inside container. Changing folder owner to ${userid}."
                chown -R "${userid}":"${groupid}" "${mountFolder}"
            fi
        fi
    else
        if [[ ! -w "${mountFolder}" ]]  && [[ "$(stat -c %U ${mountFolder})" != "neo4j" ]]; then
            print_permissions_advice_and_fail "${mountFolder}"
        fi
    fi
}

function load_plugin_from_github
{
  # Load a plugin at runtime. The provided github repository must have a versions.json on the master branch with the
  # correct format.
  local _plugin_name="${1}" #e.g. apoc, graph-algorithms, graph-ql

  local _plugins_dir="${NEO4J_HOME}/plugins"
  if [ -d /plugins ]; then
    local _plugins_dir="/plugins"
  fi
  local _versions_json_url="$(jq --raw-output "with_entries( select(.key==\"${_plugin_name}\") ) | to_entries[] | .value.versions" /neo4j-plugins.json )"
  # Using the same name for the plugin irrespective of version ensures we don't end up with different versions of the same plugin
  local _destination="${_plugins_dir}/${_plugin_name}.jar"
  local _neo4j_version="$(neo4j --version | cut -d' ' -f2)"

  # Now we call out to github to get the versions.json for this plugin and we parse that to find the url for the correct plugin jar for our neo4j version
  echo "Fetching versions.json for Plugin '${_plugin_name}' from ${_versions_json_url}"
  local _versions_json="$(wget -q --timeout 300 --tries 30 -O - "${_versions_json_url}")"
  local _plugin_jar_url="$(echo "${_versions_json}" | jq --raw-output ".[] | select(.neo4j==\"${_neo4j_version}\") | .jar")"
  if [[ -z "${_plugin_jar_url}" ]]; then
    echo >&2 "Error: No jar URL found for version '${_neo4j_version}' in versions.json from '${_versions_json_url}'"
    echo >&2 "${_versions_json}"
    exit 1
  fi
  echo "Installing Plugin '${_plugin_name}' from ${_plugin_jar_url} to ${_destination} "
  wget -q --timeout 300 --tries 30 --output-document="${_destination}" "${_plugin_jar_url}"

  if ! is_readable "${_destination}"; then
    echo >&2 "Plugin at '${_destination}' is not readable"
    exit 1
  fi
}

function apply_plugin_default_configuration
{
  # Set the correct Load a plugin at runtime. The provided github repository must have a versions.json on the master branch with the
  # correct format.
  local _plugin_name="${1}" #e.g. apoc, graph-algorithms, graph-ql
  local _reference_conf="${2}" # used to determine if we can override properties
  local _neo4j_conf="${NEO4J_HOME}/conf/neo4j.conf"

  local _property _value
  echo "Applying default values for plugin ${_plugin_name} to neo4j.conf"
  for _entry in $(jq  --compact-output --raw-output "with_entries( select(.key==\"${_plugin_name}\") ) | to_entries[] | .value.properties | to_entries[]" /neo4j-plugins.json); do
    _property="$(jq --raw-output '.key' <<< "${_entry}")"
    _value="$(jq --raw-output '.value' <<< "${_entry}")"

    # the first grep strips out comments
    if grep -o "^[^#]*" "${_reference_conf}" | grep -q --fixed-strings "${_property}=" ; then
      # property is already set in the user provided config. In this case we don't override what has been set explicitly by the user.
      echo "Skipping ${_property} for plugin ${_plugin_name} because it is already set"
    else
      if grep -o "^[^#]*" "${_neo4j_conf}" | grep -q --fixed-strings "${_property}=" ; then
        sed --in-place "s/${_property}=/&${_value},/" "${_neo4j_conf}"
      else
        echo "${_property}=${_value}" >> "${_neo4j_conf}"
      fi
    fi
  done
}

function install_neo4j_labs_plugins
{
  # We store a copy of the config before we modify it for the plugins to allow us to see if there are user-set values in the input config that we shouldn't override
  local _old_config="$(mktemp)"
  cp "${NEO4J_HOME}"/conf/neo4j.conf "${_old_config}"
  for plugin_name in $(echo "${NEO4JLABS_PLUGINS}" | jq --raw-output '.[]'); do
    load_plugin_from_github "${plugin_name}"
    apply_plugin_default_configuration "${plugin_name}" "${_old_config}"
  done
  rm "${_old_config}"
}

function add_docker_default_to_conf
{
    # docker defaults should NOT overwrite values already in the conf file
    local _setting="${1}"
    local _value="${2}"
    local _neo4j_home="${3}"

    if ! grep -q "^${_setting}=" "${_neo4j_home}"/conf/neo4j.conf
    then
        echo -e "\n"${_setting}=${_value} >> "${_neo4j_home}"/conf/neo4j.conf
    fi
}

function add_env_setting_to_conf
{
    # settings from environment variables should overwrite values already in the conf
    local _setting=${1}
    local _value=${2}
    local _neo4j_home=${3}

    if grep -q -F "${_setting}=" "${_neo4j_home}"/conf/neo4j.conf; then
        # Remove any lines containing the setting already
        sed --in-place "/^${_setting}=.*/d" "${_neo4j_home}"/conf/neo4j.conf
    fi
    # Then always append setting to file
    echo "${_setting}=${_value}" >> "${_neo4j_home}"/conf/neo4j.conf
}

function set_initial_password
{
    local _neo4j_auth="${1}"

    # set the neo4j initial password only if you run the database server
    if [ "${cmd}" == "neo4j" ]; then
        if [ "${_neo4j_auth:-}" == "none" ]; then
            add_env_setting_to_conf "dbms.security.auth_enabled" "false" "${NEO4J_HOME}"
            # NEO4J_dbms_security_auth__enabled=false
        elif [[ "${_neo4j_auth:-}" =~ ^([^/]+)\/([^/]+)/?([tT][rR][uU][eE])?$ ]]; then
            admin_user="${BASH_REMATCH[1]}"
            password="${BASH_REMATCH[2]}"
            do_reset="${BASH_REMATCH[3]}"

            if [ "${password}" == "neo4j" ]; then
                echo >&2 "Invalid value for password. It cannot be 'neo4j', which is the default."
                exit 1
            fi
            if [ "${admin_user}" != "neo4j" ]; then
                echo >&2 "Invalid admin username, it must be neo4j"
                exit 1
            fi

            if running_as_root; then
                # running set-initial-password as root will create subfolders to /data as root, causing startup fail when neo4j can't read or write the /data/dbms folder
                # creating the folder first will avoid that
                mkdir -p /data/dbms
                chown "${userid}":"${groupid}" /data/dbms
            fi

            # Will exit with error if users already exist (and print a message explaining that)
            # we probably don't want the message though, since it throws an error message on restarting the container.
            if [ "${do_reset}" == "true" ]; then
                neo4j-admin set-initial-password "${password}" --require-password-change 2>/dev/null || true
            else
                neo4j-admin set-initial-password "${password}" 2>/dev/null || true
            fi
        elif [ -n "${_neo4j_auth:-}" ]; then
            echo "$_neo4j_auth is invalid"
            echo >&2 "Invalid value for NEO4J_AUTH: '${_neo4j_auth}'"
            exit 1
        fi
    fi
}

# If we're running as root, then run as the neo4j user. Otherwise
# docker is running with --user and we simply use that user.  Note
# that su-exec, despite its name, does not replicate the functionality
# of exec, so we need to use both
if running_as_root; then
  userid="neo4j"
  groupid="neo4j"
  groups=($(id -G neo4j))
  exec_cmd="exec gosu neo4j:neo4j"
else
  userid="$(id -u)"
  groupid="$(id -g)"
  groups=($(id -G))
  exec_cmd="exec"
fi
readonly userid
readonly groupid
readonly groups
readonly exec_cmd


# Need to chown the home directory
if running_as_root; then
    chown -R "${userid}":"${groupid}" "${NEO4J_HOME}"
    chmod 700 "${NEO4J_HOME}"
    find "${NEO4J_HOME}" -mindepth 1 -maxdepth 1 -type d -exec chmod -R 700 {} \;
fi

# Only prompt for license agreement if command contains "neo4j" in it
# ==== CHECK LICENSE AGREEMENT ====

if [[ "${cmd}" == *"neo4j"* ]]; then
  if [ "${NEO4J_EDITION}" == "enterprise" ]; then
    if [ "${NEO4J_ACCEPT_LICENSE_AGREEMENT:=no}" != "yes" ]; then
      echo >&2 "
In order to use Neo4j Enterprise Edition you must accept the license agreement.

(c) Neo4j Sweden AB. 2021.  All Rights Reserved.
Use of this Software without a proper commercial license with Neo4j,
Inc. or its affiliates is prohibited.

Email inquiries can be directed to: [email protected]

More information is also available at: https://neo4j.com/licensing/


To accept the license agreement set the environment variable
NEO4J_ACCEPT_LICENSE_AGREEMENT=yes

To do this you can use the following docker argument:

        --env=NEO4J_ACCEPT_LICENSE_AGREEMENT=yes
"
      exit 1
    fi
  fi
fi

# Env variable naming convention:
# ==== RENAME LEGACY ENVIRONMENT CONF VARIABLES ====

# - prefix NEO4J_
# - double underscore char '__' instead of single underscore '_' char in the setting name
# - underscore char '_' instead of dot '.' char in the setting name
# Example:
# NEO4J_dbms_tx__log_rotation_retention__policy env variable to set
#       dbms.tx_log.rotation.retention_policy setting

# Backward compatibility - map old hardcoded env variables into new naming convention (if they aren't set already)
# Set some to default values if unset
: ${NEO4J_dbms_tx__log_rotation_retention__policy:=${NEO4J_dbms_txLog_rotation_retentionPolicy:-}}
: ${NEO4J_dbms_unmanaged__extension__classes:=${NEO4J_dbms_unmanagedExtensionClasses:-}}
: ${NEO4J_dbms_allow__format__migration:=${NEO4J_dbms_allowFormatMigration:-}}
: ${NEO4J_dbms_connectors_default__advertised__address:=${NEO4J_dbms_connectors_defaultAdvertisedAddress:-}}

if [ "${NEO4J_EDITION}" == "enterprise" ];
  then
   : ${NEO4J_causal__clustering_expected__core__cluster__size:=${NEO4J_causalClustering_expectedCoreClusterSize:-}}
   : ${NEO4J_causal__clustering_initial__discovery__members:=${NEO4J_causalClustering_initialDiscoveryMembers:-}}
   : ${NEO4J_causal__clustering_discovery__advertised__address:=${NEO4J_causalClustering_discoveryAdvertisedAddress:-}}
   : ${NEO4J_causal__clustering_transaction__advertised__address:=${NEO4J_causalClustering_transactionAdvertisedAddress:-}}
   : ${NEO4J_causal__clustering_raft__advertised__address:=${NEO4J_causalClustering_raftAdvertisedAddress:-}}
fi

# unset old hardcoded unsupported env variables
unset NEO4J_dbms_txLog_rotation_retentionPolicy NEO4J_UDC_SOURCE \
    NEO4J_dbms_unmanagedExtensionClasses NEO4J_dbms_allowFormatMigration \
    NEO4J_dbms_connectors_defaultAdvertisedAddress NEO4J_ha_serverId \
    NEO4J_ha_initialHosts NEO4J_causalClustering_expectedCoreClusterSize \
    NEO4J_causalClustering_initialDiscoveryMembers \
    NEO4J_causalClustering_discoveryListenAddress \
    NEO4J_causalClustering_discoveryAdvertisedAddress \
    NEO4J_causalClustering_transactionListenAddress \
    NEO4J_causalClustering_transactionAdvertisedAddress \
    NEO4J_causalClustering_raftListenAddress \
    NEO4J_causalClustering_raftAdvertisedAddress


# ==== CHECK FILE PERMISSIONS ON MOUNTED FOLDERS ====

if [ -d /conf ]; then
    check_mounted_folder_readable "/conf"
    find /conf -type f -exec cp {} "${NEO4J_HOME}"/conf \;
fi

if [ -d /ssl ]; then
    check_mounted_folder_readable "/ssl"
    rm -rf "${NEO4J_HOME}"/certificates
    ln -s /ssl "${NEO4J_HOME}"/certificates
fi

if [ -d /plugins ]; then
    if [[ -n "${NEO4JLABS_PLUGINS:-}" ]]; then
        # We need write permissions
        check_mounted_folder_writable_with_chown "/plugins"
    fi
    check_mounted_folder_readable "/plugins"
    : ${NEO4J_dbms_directories_plugins:="/plugins"}
fi

if [ -d /import ]; then
    check_mounted_folder_readable "/import"
    : ${NEO4J_dbms_directories_import:="/import"}
fi

if [ -d /metrics ]; then
    check_mounted_folder_writable_with_chown "/metrics"
    : ${NEO4J_dbms_directories_metrics:="/metrics"}
fi

if [ -d /logs ]; then
    check_mounted_folder_writable_with_chown "/logs"
    : ${NEO4J_dbms_directories_logs:="/logs"}
fi

if [ -d /data ]; then
    check_mounted_folder_writable_with_chown "/data"
    if [ -d /data/databases ]; then
        check_mounted_folder_writable_with_chown "/data/databases"
    fi
    if [ -d /data/dbms ]; then
        check_mounted_folder_writable_with_chown "/data/dbms"
    fi
    if [ -d /data/transactions ]; then
        check_mounted_folder_writable_with_chown "/data/transactions"
    fi
fi

# ==== SET CONFIGURATIONS ====

## == DOCKER SPECIFIC DEFAULT CONFIGURATIONS ===
## these should not override *any* configurations set by the user

add_docker_default_to_conf "dbms.tx_log.rotation.retention_policy" "100M size" "${NEO4J_HOME}"
add_docker_default_to_conf "dbms.memory.pagecache.size" "512M" "${NEO4J_HOME}"
add_docker_default_to_conf "dbms.default_listen_address" "0.0.0.0" "${NEO4J_HOME}"
# set enterprise only docker defaults
if [ "${NEO4J_EDITION}" == "enterprise" ];
then
    add_docker_default_to_conf "causal_clustering.discovery_advertised_address" "$(hostname):5000" "${NEO4J_HOME}"
    add_docker_default_to_conf "causal_clustering.transaction_advertised_address" "$(hostname):6000" "${NEO4J_HOME}"
    add_docker_default_to_conf "causal_clustering.raft_advertised_address" "$(hostname):7000" "${NEO4J_HOME}"
fi

## == ENVIRONMENT VARIABLE CONFIGURATIONS ===
## these override BOTH defaults and any existing values in the neo4j.conf file

# save NEO4J_HOME and NEO4J_AUTH to temp variables that don't begin with NEO4J_ so they don't get added to the conf
temp_neo4j_home="${NEO4J_HOME}"
temp_neo4j_auth="${NEO4J_AUTH:-}"
# list env variables with prefix NEO4J_ and create settings from them
unset NEO4J_AUTH NEO4J_SHA256 NEO4J_TARBALL NEO4J_EDITION NEO4J_ACCEPT_LICENSE_AGREEMENT NEO4J_HOME
for i in $( set | grep ^NEO4J_ | awk -F'=' '{print $1}' | sort -rn ); do
    setting=$(echo "${i}" | sed 's|^NEO4J_||' | sed 's|_|.|g' | sed 's|\.\.|_|g')
    value=$(echo "${!i}")
    # Don't allow settings with no value or settings that start with a number (neo4j converts settings to env variables and you cannot have an env variable that starts with a number)
    if [[ -n ${value} ]]; then
        if [[ ! "${setting}" =~ ^[0-9]+.*$ ]]; then
            add_env_setting_to_conf "${setting}" "${value}" "${temp_neo4j_home}"
        else
            echo >&2 "WARNING: ${setting} not written to conf file because settings that start with a number are not permitted"
        fi
    fi
done
export NEO4J_HOME="${temp_neo4j_home}"
unset temp_neo4j_home

# ==== SET PASSWORD AND PLUGINS ====

set_initial_password "${temp_neo4j_auth}"


if [[ ! -z "${NEO4JLABS_PLUGINS:-}" ]]; then
  # NEO4JLABS_PLUGINS should be a json array of plugins like '["graph-algorithms", "apoc", "streams", "graphql"]'
  install_neo4j_labs_plugins
fi

# ==== INVOKE NEO4J STARTUP ====

[ -f "${EXTENSION_SCRIPT:-}" ] && . ${EXTENSION_SCRIPT}

if [ "${cmd}" == "dump-config" ]; then
    if ! is_writable "/conf"; then
        print_permissions_advice_and_fail "/conf"
    fi
    cp --recursive "${NEO4J_HOME}"/conf/* /conf
    echo "Config Dumped"
    exit 0
fi

# Use su-exec to drop privileges to neo4j user
# Note that su-exec, despite its name, does not replicate the
# functionality of exec, so we need to use both
if [ "${cmd}" == "neo4j" ]; then
  ${exec_cmd} neo4j console
else
  ${exec_cmd} "$@"
fi

```

--------------------------------------------------------------------------------
/neo4j/docker-neo4j/docker-image-src/4.1/coredb/docker-entrypoint.sh:
--------------------------------------------------------------------------------

```bash
#!/bin/bash -eu

cmd="$1"

function running_as_root
{
    test "$(id -u)" = "0"
}

function secure_mode_enabled
{
    test "${SECURE_FILE_PERMISSIONS:=no}" = "yes"
}

function containsElement
{
  local e match="$1"
  shift
  for e; do [[ "$e" == "$match" ]] && return 0; done
  return 1
}

function is_readable
{
    # this code is fairly ugly but works no matter who this script is running as.
    # It would be nice if the writability tests could use this logic somehow.
    local _file=${1}
    perm=$(stat -c %a "${_file}")

    # everyone permission
    if [[ ${perm:2:1} -ge 4 ]]; then
        return 0
    fi
    # owner permissions
    if [[ ${perm:0:1} -ge 4 ]]; then
        if [[ "$(stat -c %U ${_file})" = "${userid}" ]] || [[ "$(stat -c %u ${_file})" = "${userid}" ]]; then
            return 0
        fi
    fi
    # group permissions
    if [[ ${perm:1:1} -ge 4 ]]; then
        if containsElement "$(stat -c %g ${_file})" "${groups[@]}" || containsElement "$(stat -c %G ${_file})" "${groups[@]}" ; then
            return 0
        fi
    fi
    return 1
}

function is_writable
{
    # It would be nice if this and the is_readable function could combine somehow
    local _file=${1}
    perm=$(stat -c %a "${_file}")

    # everyone permission
    if containsElement ${perm:2:1} 2 3 6 7; then
        return 0
    fi
    # owner permissions
    if containsElement ${perm:0:1} 2 3 6 7; then
        if [[ "$(stat -c %U ${_file})" = "${userid}" ]] || [[ "$(stat -c %u ${_file})" = "${userid}" ]]; then
            return 0
        fi
    fi
    # group permissions
    if containsElement ${perm:1:1} 2 3 6 7; then
        if containsElement "$(stat -c %g ${_file})" "${groups[@]}" || containsElement "$(stat -c %G ${_file})" "${groups[@]}" ; then
            return 0
        fi
    fi
    return 1
}


function print_permissions_advice_and_fail
{
    _directory=${1}
    echo >&2 "
Folder ${_directory} is not accessible for user: ${userid} or group ${groupid} or groups ${groups[@]}, this is commonly a file permissions issue on the mounted folder.

Hints to solve the issue:
1) Make sure the folder exists before mounting it. Docker will create the folder using root permissions before starting the Neo4j container. The root permissions disallow Neo4j from writing to the mounted folder.
2) Pass the folder owner's user ID and group ID to docker run, so that docker runs as that user.
If the folder is owned by the current user, this can be done by adding this flag to your docker run command:
  --user=\$(id -u):\$(id -g)
       "
    exit 1
}

function check_mounted_folder_readable
{
    local _directory=${1}
    if ! is_readable "${_directory}"; then
        print_permissions_advice_and_fail "${_directory}"
    fi
}

function check_mounted_folder_writable_with_chown
{
# The /data and /log directory are a bit different because they are very likely to be mounted by the user but not
# necessarily writable.
# This depends on whether a user ID is passed to the container and which folders are mounted.
#
#   No user ID passed to container:
#   1) No folders are mounted.
#      The /data and /log folder are owned by neo4j by default, so should be writable already.
#   2) Both /log and /data are mounted.
#      This means on start up, /data and /logs are owned by an unknown user and we should chown them to neo4j for
#      backwards compatibility.
#
#   User ID passed to container:
#   1) Both /data and /logs are mounted
#      The /data and /logs folders are owned by an unknown user but we *should* have rw permission to them.
#      That should be verified and error (helpfully) if not.
#   2) User mounts /data or /logs *but not both*
#      The  unmounted folder is still owned by neo4j, which should already be writable. The mounted folder should
#      have rw permissions through user id. This should be verified.
#   3) No folders are mounted.
#      The /data and /log folder are owned by neo4j by default, and these are already writable by the user.
#      (This is a very unlikely use case).

    local mountFolder=${1}
    if running_as_root && ! secure_mode_enabled; then
        # check folder permissions
        if ! is_writable "${mountFolder}" ;  then
            # warn that we're about to chown the folder and then chown it
            echo "Warning: Folder mounted to \"${mountFolder}\" is not writable from inside container. Changing folder owner to ${userid}."
            chown -R "${userid}":"${groupid}" "${mountFolder}"
        # check permissions on files in the folder
        elif [ $(gosu "${userid}":"${groupid}" find "${mountFolder}" -not -writable | wc -l) -gt 0 ]; then
            echo "Warning: Some files inside \"${mountFolder}\" are not writable from inside container. Changing folder owner to ${userid}."
            chown -R "${userid}":"${groupid}" "${mountFolder}"
        fi
    else
        if ! is_writable "${mountFolder}"; then
        #if [[ ! -w "${mountFolder}" ]]  && [[ "$(stat -c %U ${mountFolder})" != "neo4j" ]]; then
            echo >&2 "Consider unsetting SECURE_FILE_PERMISSIONS environment variable, to enable docker to write to ${mountFolder}."
            print_permissions_advice_and_fail "${mountFolder}"
        fi
    fi
}

function load_plugin_from_github
{
  # Load a plugin at runtime. The provided github repository must have a versions.json on the master branch with the
  # correct format.
  local _plugin_name="${1}" #e.g. apoc, graph-algorithms, graph-ql

  local _plugins_dir="${NEO4J_HOME}/plugins"
  if [ -d /plugins ]; then
    local _plugins_dir="/plugins"
  fi
  local _versions_json_url="$(jq --raw-output "with_entries( select(.key==\"${_plugin_name}\") ) | to_entries[] | .value.versions" /startup/neo4j-plugins.json )"
  # Using the same name for the plugin irrespective of version ensures we don't end up with different versions of the same plugin
  local _destination="${_plugins_dir}/${_plugin_name}.jar"
  local _neo4j_version="$(neo4j --version | cut -d' ' -f2)"

  # Now we call out to github to get the versions.json for this plugin and we parse that to find the url for the correct plugin jar for our neo4j version
  echo "Fetching versions.json for Plugin '${_plugin_name}' from ${_versions_json_url}"
  local _versions_json="$(wget -q --timeout 300 --tries 30 -O - "${_versions_json_url}")"
  local _plugin_jar_url="$(echo "${_versions_json}" | jq -L/startup --raw-output "import \"semver\" as lib; [ .[] | select(.neo4j|lib::semver(\"${_neo4j_version}\")) ] | min_by(.neo4j) | .jar")"
  if [[ -z "${_plugin_jar_url}" ]]; then
    echo >&2 "Error: No jar URL found for version '${_neo4j_version}' in versions.json from '${_versions_json_url}'"
    exit 1
  fi
  echo "Installing Plugin '${_plugin_name}' from ${_plugin_jar_url} to ${_destination} "
  wget -q --timeout 300 --tries 30 --output-document="${_destination}" "${_plugin_jar_url}"

  if ! is_readable "${_destination}"; then
    echo >&2 "Plugin at '${_destination}' is not readable"
    exit 1
  fi
}

function apply_plugin_default_configuration
{
  # Set the correct Load a plugin at runtime. The provided github repository must have a versions.json on the master branch with the
  # correct format.
  local _plugin_name="${1}" #e.g. apoc, graph-algorithms, graph-ql
  local _reference_conf="${2}" # used to determine if we can override properties
  local _neo4j_conf="${NEO4J_HOME}/conf/neo4j.conf"

  local _property _value
  echo "Applying default values for plugin ${_plugin_name} to neo4j.conf"
  for _entry in $(jq  --compact-output --raw-output "with_entries( select(.key==\"${_plugin_name}\") ) | to_entries[] | .value.properties | to_entries[]" /startup/neo4j-plugins.json); do
    _property="$(jq --raw-output '.key' <<< "${_entry}")"
    _value="$(jq --raw-output '.value' <<< "${_entry}")"

    # the first grep strips out comments
    if grep -o "^[^#]*" "${_reference_conf}" | grep -q --fixed-strings "${_property}=" ; then
      # property is already set in the user provided config. In this case we don't override what has been set explicitly by the user.
      echo "Skipping ${_property} for plugin ${_plugin_name} because it is already set"
    else
      if grep -o "^[^#]*" "${_neo4j_conf}" | grep -q --fixed-strings "${_property}=" ; then
        sed --in-place "s/${_property}=/&${_value},/" "${_neo4j_conf}"
      else
        echo "${_property}=${_value}" >> "${_neo4j_conf}"
      fi
    fi
  done
}

function install_neo4j_labs_plugins
{
  # We store a copy of the config before we modify it for the plugins to allow us to see if there are user-set values in the input config that we shouldn't override
  local _old_config="$(mktemp)"
  cp "${NEO4J_HOME}"/conf/neo4j.conf "${_old_config}"
  for plugin_name in $(echo "${NEO4JLABS_PLUGINS}" | jq --raw-output '.[]'); do
    load_plugin_from_github "${plugin_name}"
    apply_plugin_default_configuration "${plugin_name}" "${_old_config}"
  done
  rm "${_old_config}"
}

function add_docker_default_to_conf
{
    # docker defaults should NOT overwrite values already in the conf file
    local _setting="${1}"
    local _value="${2}"
    local _neo4j_home="${3}"

    if ! grep -q "^${_setting}=" "${_neo4j_home}"/conf/neo4j.conf
    then
        echo -e "\n"${_setting}=${_value} >> "${_neo4j_home}"/conf/neo4j.conf
    fi
}

function add_env_setting_to_conf
{
    # settings from environment variables should overwrite values already in the conf
    local _setting=${1}
    local _value=${2}
    local _neo4j_home=${3}

    if grep -q -F "${_setting}=" "${_neo4j_home}"/conf/neo4j.conf; then
        # Remove any lines containing the setting already
        sed --in-place "/^${_setting}=.*/d" "${_neo4j_home}"/conf/neo4j.conf
    fi
    # Then always append setting to file
    echo "${_setting}=${_value}" >> "${_neo4j_home}"/conf/neo4j.conf
}

function set_initial_password
{
    local _neo4j_auth="${1}"

    # set the neo4j initial password only if you run the database server
    if [ "${cmd}" == "neo4j" ]; then
        if [ "${_neo4j_auth:-}" == "none" ]; then
            add_env_setting_to_conf "dbms.security.auth_enabled" "false" "${NEO4J_HOME}"
            # NEO4J_dbms_security_auth__enabled=false
        elif [[ "${_neo4j_auth:-}" =~ ^([^/]+)\/([^/]+)/?([tT][rR][uU][eE])?$ ]]; then
            admin_user="${BASH_REMATCH[1]}"
            password="${BASH_REMATCH[2]}"
            do_reset="${BASH_REMATCH[3]}"

            if [ "${password}" == "neo4j" ]; then
                echo >&2 "Invalid value for password. It cannot be 'neo4j', which is the default."
                exit 1
            fi
            if [ "${admin_user}" != "neo4j" ]; then
                echo >&2 "Invalid admin username, it must be neo4j"
                exit 1
            fi

            if running_as_root; then
                # running set-initial-password as root will create subfolders to /data as root, causing startup fail when neo4j can't read or write the /data/dbms folder
                # creating the folder first will avoid that
                mkdir -p /data/dbms
                chown "${userid}":"${groupid}" /data/dbms
            fi

            # Will exit with error if users already exist (and print a message explaining that)
            # we probably don't want the message though, since it throws an error message on restarting the container.
            if [ "${do_reset}" == "true" ]; then
                neo4j-admin set-initial-password "${password}" --require-password-change 2>/dev/null || true
            else
                neo4j-admin set-initial-password "${password}" 2>/dev/null || true
            fi
        elif [ -n "${_neo4j_auth:-}" ]; then
            echo "$_neo4j_auth is invalid"
            echo >&2 "Invalid value for NEO4J_AUTH: '${_neo4j_auth}'"
            exit 1
        fi
    fi
}

# If we're running as root, then run as the neo4j user. Otherwise
# docker is running with --user and we simply use that user.  Note
# that su-exec, despite its name, does not replicate the functionality
# of exec, so we need to use both
if running_as_root; then
  userid="neo4j"
  groupid="neo4j"
  groups=($(id -G neo4j))
  exec_cmd="exec gosu neo4j:neo4j"
else
  userid="$(id -u)"
  groupid="$(id -g)"
  groups=($(id -G))
  exec_cmd="exec"
fi
readonly userid
readonly groupid
readonly groups
readonly exec_cmd


# Need to chown the home directory
if running_as_root; then
    chown -R "${userid}":"${groupid}" "${NEO4J_HOME}"
    chmod 700 "${NEO4J_HOME}"
    find "${NEO4J_HOME}" -mindepth 1 -maxdepth 1 -type d -exec chmod -R 700 {} \;
fi

# Only prompt for license agreement if command contains "neo4j" in it
# ==== CHECK LICENSE AGREEMENT ====

if [[ "${cmd}" == *"neo4j"* ]]; then
  if [ "${NEO4J_EDITION}" == "enterprise" ]; then
    if [ "${NEO4J_ACCEPT_LICENSE_AGREEMENT:=no}" != "yes" ]; then
      echo >&2 "
In order to use Neo4j Enterprise Edition you must accept the license agreement.

(c) Neo4j Sweden AB. 2021.  All Rights Reserved.
Use of this Software without a proper commercial license with Neo4j,
Inc. or its affiliates is prohibited.

Email inquiries can be directed to: [email protected]

More information is also available at: https://neo4j.com/licensing/


To accept the license agreement set the environment variable
NEO4J_ACCEPT_LICENSE_AGREEMENT=yes

To do this you can use the following docker argument:

        --env=NEO4J_ACCEPT_LICENSE_AGREEMENT=yes
"
      exit 1
    fi
  fi
fi

# Env variable naming convention:
# ==== RENAME LEGACY ENVIRONMENT CONF VARIABLES ====

# - prefix NEO4J_
# - double underscore char '__' instead of single underscore '_' char in the setting name
# - underscore char '_' instead of dot '.' char in the setting name
# Example:
# NEO4J_dbms_tx__log_rotation_retention__policy env variable to set
#       dbms.tx_log.rotation.retention_policy setting

# Backward compatibility - map old hardcoded env variables into new naming convention (if they aren't set already)
# Set some to default values if unset
: ${NEO4J_dbms_tx__log_rotation_retention__policy:=${NEO4J_dbms_txLog_rotation_retentionPolicy:-}}
: ${NEO4J_dbms_unmanaged__extension__classes:=${NEO4J_dbms_unmanagedExtensionClasses:-}}
: ${NEO4J_dbms_allow__format__migration:=${NEO4J_dbms_allowFormatMigration:-}}
: ${NEO4J_dbms_connectors_default__advertised__address:=${NEO4J_dbms_connectors_defaultAdvertisedAddress:-}}

if [ "${NEO4J_EDITION}" == "enterprise" ];
  then
   : ${NEO4J_causal__clustering_expected__core__cluster__size:=${NEO4J_causalClustering_expectedCoreClusterSize:-}}
   : ${NEO4J_causal__clustering_initial__discovery__members:=${NEO4J_causalClustering_initialDiscoveryMembers:-}}
   : ${NEO4J_causal__clustering_discovery__advertised__address:=${NEO4J_causalClustering_discoveryAdvertisedAddress:-}}
   : ${NEO4J_causal__clustering_transaction__advertised__address:=${NEO4J_causalClustering_transactionAdvertisedAddress:-}}
   : ${NEO4J_causal__clustering_raft__advertised__address:=${NEO4J_causalClustering_raftAdvertisedAddress:-}}
fi

# unset old hardcoded unsupported env variables
unset NEO4J_dbms_txLog_rotation_retentionPolicy NEO4J_UDC_SOURCE \
    NEO4J_dbms_unmanagedExtensionClasses NEO4J_dbms_allowFormatMigration \
    NEO4J_dbms_connectors_defaultAdvertisedAddress NEO4J_ha_serverId \
    NEO4J_ha_initialHosts NEO4J_causalClustering_expectedCoreClusterSize \
    NEO4J_causalClustering_initialDiscoveryMembers \
    NEO4J_causalClustering_discoveryListenAddress \
    NEO4J_causalClustering_discoveryAdvertisedAddress \
    NEO4J_causalClustering_transactionListenAddress \
    NEO4J_causalClustering_transactionAdvertisedAddress \
    NEO4J_causalClustering_raftListenAddress \
    NEO4J_causalClustering_raftAdvertisedAddress


# ==== CHECK FILE PERMISSIONS ON MOUNTED FOLDERS ====

if [ -d /conf ]; then
    check_mounted_folder_readable "/conf"
    find /conf -type f -exec cp {} "${NEO4J_HOME}"/conf \;
fi

if [ -d /ssl ]; then
    check_mounted_folder_readable "/ssl"
    rm -rf "${NEO4J_HOME}"/certificates
    ln -s /ssl "${NEO4J_HOME}"/certificates
fi

if [ -d /plugins ]; then
    if [[ -n "${NEO4JLABS_PLUGINS:-}" ]]; then
        # We need write permissions
        check_mounted_folder_writable_with_chown "/plugins"
    fi
    check_mounted_folder_readable "/plugins"
    : ${NEO4J_dbms_directories_plugins:="/plugins"}
fi

if [ -d /import ]; then
    check_mounted_folder_readable "/import"
    : ${NEO4J_dbms_directories_import:="/import"}
fi

if [ -d /metrics ]; then
    # metrics is enterprise only
    if [ "${NEO4J_EDITION}" == "enterprise" ];
    then
        check_mounted_folder_writable_with_chown "/metrics"
        : ${NEO4J_dbms_directories_metrics:="/metrics"}
    fi
fi

if [ -d /logs ]; then
    check_mounted_folder_writable_with_chown "/logs"
    : ${NEO4J_dbms_directories_logs:="/logs"}
fi

if [ -d /data ]; then
    check_mounted_folder_writable_with_chown "/data"
    if [ -d /data/databases ]; then
        check_mounted_folder_writable_with_chown "/data/databases"
    fi
    if [ -d /data/dbms ]; then
        check_mounted_folder_writable_with_chown "/data/dbms"
    fi
    if [ -d /data/transactions ]; then
        check_mounted_folder_writable_with_chown "/data/transactions"
    fi
fi

# ==== SET CONFIGURATIONS ====

## == DOCKER SPECIFIC DEFAULT CONFIGURATIONS ===
## these should not override *any* configurations set by the user

add_docker_default_to_conf "dbms.tx_log.rotation.retention_policy" "100M size" "${NEO4J_HOME}"
add_docker_default_to_conf "dbms.memory.pagecache.size" "512M" "${NEO4J_HOME}"
add_docker_default_to_conf "dbms.default_listen_address" "0.0.0.0" "${NEO4J_HOME}"
# set enterprise only docker defaults
if [ "${NEO4J_EDITION}" == "enterprise" ];
then
    add_docker_default_to_conf "causal_clustering.discovery_advertised_address" "$(hostname):5000" "${NEO4J_HOME}"
    add_docker_default_to_conf "causal_clustering.transaction_advertised_address" "$(hostname):6000" "${NEO4J_HOME}"
    add_docker_default_to_conf "causal_clustering.raft_advertised_address" "$(hostname):7000" "${NEO4J_HOME}"
fi

## == ENVIRONMENT VARIABLE CONFIGURATIONS ===
## these override BOTH defaults and any existing values in the neo4j.conf file

# save NEO4J_HOME and NEO4J_AUTH to temp variables that don't begin with NEO4J_ so they don't get added to the conf
temp_neo4j_home="${NEO4J_HOME}"
temp_neo4j_auth="${NEO4J_AUTH:-}"
# list env variables with prefix NEO4J_ and create settings from them
unset NEO4J_AUTH NEO4J_SHA256 NEO4J_TARBALL NEO4J_EDITION NEO4J_ACCEPT_LICENSE_AGREEMENT NEO4J_HOME
for i in $( set | grep ^NEO4J_ | awk -F'=' '{print $1}' | sort -rn ); do
    setting=$(echo "${i}" | sed 's|^NEO4J_||' | sed 's|_|.|g' | sed 's|\.\.|_|g')
    value=$(echo "${!i}")
    # Don't allow settings with no value or settings that start with a number (neo4j converts settings to env variables and you cannot have an env variable that starts with a number)
    if [[ -n ${value} ]]; then
        if [[ ! "${setting}" =~ ^[0-9]+.*$ ]]; then
            add_env_setting_to_conf "${setting}" "${value}" "${temp_neo4j_home}"
        else
            echo >&2 "WARNING: ${setting} not written to conf file because settings that start with a number are not permitted"
        fi
    fi
done
export NEO4J_HOME="${temp_neo4j_home}"
unset temp_neo4j_home

# ==== SET PASSWORD AND PLUGINS ====

set_initial_password "${temp_neo4j_auth}"


if [[ ! -z "${NEO4JLABS_PLUGINS:-}" ]]; then
  # NEO4JLABS_PLUGINS should be a json array of plugins like '["graph-algorithms", "apoc", "streams", "graphql"]'
  install_neo4j_labs_plugins
fi

# ==== INVOKE NEO4J STARTUP ====

[ -f "${EXTENSION_SCRIPT:-}" ] && . ${EXTENSION_SCRIPT}

if [ "${cmd}" == "dump-config" ]; then
    if [ ! -d "/conf" ]; then
        echo >&2 "You must mount a folder to /conf so that the configuration file(s) can be dumped to there."
        exit 1
    fi
    check_mounted_folder_writable_with_chown "/conf"
    cp --recursive "${NEO4J_HOME}"/conf/* /conf
    echo "Config Dumped"
    exit 0
fi

# Use su-exec to drop privileges to neo4j user
# Note that su-exec, despite its name, does not replicate the
# functionality of exec, so we need to use both
if [ "${cmd}" == "neo4j" ]; then
  ${exec_cmd} neo4j console
else
  ${exec_cmd} "$@"
fi

```
Page 3/6FirstPrevNextLast