High severityNVD Advisory· Published Apr 29, 2014· Updated May 6, 2026
CVE-2013-7259
CVE-2013-7259
Description
Multiple cross-site request forgery (CSRF) vulnerabilities in Neo4J 1.9.2 allow remote attackers to hijack the authentication of administrators for requests that execute arbitrary code, as demonstrated by a request to (1) db/data/ext/GremlinPlugin/graphdb/execute_script or (2) db/manage/server/console/.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.neo4j:neo4jMaven | < 2.2.0-M01 | 2.2.0-M01 |
Patches
140ad76078a25Introduce authentication and basic authorization in the server.
73 files changed · +4163 −220
community/kernel/src/main/java/org/neo4j/kernel/api/exceptions/KernelException.java+2 −1 modified@@ -22,7 +22,7 @@ import org.neo4j.kernel.api.TokenNameLookup; /** A super class of checked exceptions coming from the {@link org.neo4j.kernel.api.KernelAPI Kernel API}. */ -public abstract class KernelException extends Exception +public abstract class KernelException extends Exception implements Status.HasStatus { private final Status statusCode; @@ -40,6 +40,7 @@ protected KernelException( Status statusCode, String message, Object... paramete } /** The Neo4j status code associated with this exception type. */ + @Override public Status status() { return statusCode;
community/kernel/src/main/java/org/neo4j/kernel/api/exceptions/Status.java+26 −0 modified@@ -244,6 +244,27 @@ private LegacyIndex( Classification classification, String description ) } } + enum Security implements Status + { + // client + AuthenticationFailed( ClientError, "The client provided an incorrect username and/or password." ), + AuthorizationFailed( ClientError, "The client provided an invalid authorization token, or does not have privileges to perform the operation requested." ), + AuthenticationRateLimit( ClientError, "The client has provided incorrect authentication details too many times in a row. You will be allowed to try again in a few seconds." ); + + private final Code code; + + @Override + public Code code() + { + return code; + } + + private Security( Classification classification, String description ) + { + this.code = new Code( classification, this, description ); + } + } + enum General implements Status { ReadOnly( ClientError, "This is a read only database, writing or modifying the database is not allowed." ), @@ -405,4 +426,9 @@ public boolean rollbackTransaction() return rollbackTransaction; } } + + interface HasStatus + { + Status status(); + } }
community/kernel/src/main/java/org/neo4j/kernel/impl/util/BytePrinter.java+217 −0 added@@ -0,0 +1,217 @@ +/** + * Copyright (c) 2002-2014 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package org.neo4j.kernel.impl.util; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; + +import static java.nio.ByteBuffer.wrap; + +/** + * Utility to convert and print binary data in a human readable way. + */ +public class BytePrinter +{ + final protected static char[] hexArray = "0123456789ABCDEF".toCharArray(); + + /** + * Print a full byte array as nicely formatted groups of hex numbers. + * Output looks like: + * + * 01 02 03 04 05 06 07 08 01 02 03 04 05 06 07 08 01 02 03 04 05 06 07 08 01 02 03 04 05 06 07 08 + * 01 02 03 04 05 06 07 08 01 02 03 04 05 06 07 08 01 02 03 04 05 06 07 08 01 02 03 04 05 06 07 08 + * + */ + public static void print( byte [] bytes, PrintStream out ) + { + print( wrap( bytes ), out, 0, bytes.length ); + } + + /** + * Print a full byte buffer as nicely formatted groups of hex numbers. + * Output looks like: + * + * 01 02 03 04 05 06 07 08 01 02 03 04 05 06 07 08 01 02 03 04 05 06 07 08 01 02 03 04 05 06 07 08 + * 01 02 03 04 05 06 07 08 01 02 03 04 05 06 07 08 01 02 03 04 05 06 07 08 01 02 03 04 05 06 07 08 + * + * @param bytes + * @param out + */ + public static void print( ByteBuffer bytes, PrintStream out ) + { + print( bytes, out, 0, bytes.limit() ); + } + + /** + * Print a subsection of a byte buffer as nicely formatted groups of hex numbers. + * Output looks like: + * + * 01 02 03 04 05 06 07 08 01 02 03 04 05 06 07 08 01 02 03 04 05 06 07 08 01 02 03 04 05 06 07 08 + * 01 02 03 04 05 06 07 08 01 02 03 04 05 06 07 08 01 02 03 04 05 06 07 08 01 02 03 04 05 06 07 08 + * + * @param bytes + * @param out + */ + public static void print( ByteBuffer bytes, PrintStream out, int offset, int length ) + { + for(int i=offset;i<offset + length;i++) + { + print( bytes.get( i ), out ); + if((i - offset + 1) % 32 == 0) + { + out.println( ); + } else if((i - offset + 1) % 8 == 0) + { + out.print( " " ); + } else { + out.print( " " ); + } + } + } + + /** + * Print a single byte as a hex number. The number will always be two characters wide. + * + * @param b + * @param out + */ + public static void print( byte b, PrintStream out ) + { + out.print( hex( b ) ); + } + + /** + * This should not be in this class, move to a dedicated ascii-art class when appropriate. + * + * Use this to standardize the width of some text output to all be left-justified and space-padded + * on the right side to fill up the given column width. + * + * @param str + * @param columnWidth + * @return + */ + public static String ljust( String str, int columnWidth ) + { + return String.format( "%-" + columnWidth + "s", str); + } + + /** + * This should not be in this class, move to a dedicated ascii-art class when appropriate. + * + * Use this to standardize the width of some text output to all be right-justified and space-padded + * on the left side to fill up the given column width. + * + * @param str + * @param columnWidth + * @return + */ + public static String rjust( String str, int columnWidth ) + { + return String.format( "%" + columnWidth + "s", str); + } + + /** + * Convert a single byte to a human-readable hex number. The number will always be two characters wide. + * @param b + * @return + */ + public static String hex(byte b) + { + return String.format("%02x", b); + } + + /** + * Convert a subsection of a byte buffer to a human readable string of nicely formatted hex numbers. + * Output looks like: + * + * 01 02 03 04 05 06 07 08 01 02 03 04 05 06 07 08 01 02 03 04 05 06 07 08 01 02 03 04 05 06 07 08 + * 01 02 03 04 05 06 07 08 01 02 03 04 05 06 07 08 01 02 03 04 05 06 07 08 01 02 03 04 05 06 07 08 + * + * @param bytes + * @param offset + * @param length + * @return + */ + public static String hex( ByteBuffer bytes, int offset, int length ) + { + try + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + PrintStream ps = null; + ps = new PrintStream(baos, true, "UTF-8"); + print( bytes, ps, offset, length ); + return baos.toString("UTF-8"); + } + catch ( UnsupportedEncodingException e ) + { + throw new RuntimeException( e ); + } + } + + /** + * Convert a full byte buffer to a human readable string of nicely formatted hex numbers. + * Output looks like: + * + * 01 02 03 04 05 06 07 08 01 02 03 04 05 06 07 08 01 02 03 04 05 06 07 08 01 02 03 04 05 06 07 08 + * 01 02 03 04 05 06 07 08 01 02 03 04 05 06 07 08 01 02 03 04 05 06 07 08 01 02 03 04 05 06 07 08 + * + * @param bytes + * @return + */ + public static String hex(ByteBuffer bytes) + { + return hex( bytes, 0, bytes.capacity() ); + } + + /** + * Convert a full byte buffer to a human readable string of nicely formatted hex numbers. + * Output looks like: + * + * 01 02 03 04 05 06 07 08 01 02 03 04 05 06 07 08 01 02 03 04 05 06 07 08 01 02 03 04 05 06 07 08 + * 01 02 03 04 05 06 07 08 01 02 03 04 05 06 07 08 01 02 03 04 05 06 07 08 01 02 03 04 05 06 07 08 + * + * @param bytes + * @return + */ + public static String hex(byte[] bytes) + { + return hex( wrap( bytes ) ); + } + + /** + * Converts a byte array to a hexadecimal string. Unlike other methods in this utility class, it does not introduce + * newlines or spaces: + * + * 0102030405060708 + */ + public static String compactHex( byte[] bytes ) + { + char[] hexChars = new char[bytes.length * 2]; + for ( int j = 0; j < bytes.length; j++ ) { + int v = bytes[j] & 0xFF; + hexChars[j * 2] = hexArray[v >>> 4]; + hexChars[j * 2 + 1] = hexArray[v & 0x0F]; + } + return new String(hexChars); + } + +}
community/server-api/src/main/java/org/neo4j/server/rest/repr/RepresentationType.java+1 −0 modified@@ -83,6 +83,7 @@ public final class RepresentationType NOTHING = new RepresentationType( "void", null ), // System EXCEPTION = new RepresentationType( "exception" ), + AUTHORIZATION = new RepresentationType( "authorization" ), MAP = new RepresentationType( "map", "maps", Map.class ), NULL = new RepresentationType( "null", "nulls", Object.class );
community/server/CHANGES.txt+1 −0 modified@@ -4,6 +4,7 @@ o Expose node degree in REST API o Changed the DELETE verb on a transaction to also mark it as terminated even if there are concurrent requests that use the transaction. Consult community/kernel/CHANGES.txt and the manual for more details on the new transaction termination API. +o Introduce optional support for authentication and authorization using Authorization Tokens 2.1.5 -----
community/server/src/docs/dev/rest-api/index.asciidoc+2 −0 modified@@ -6,6 +6,8 @@ include::introduction.asciidoc[] include::transactional.asciidoc[] +include::security.asciidoc[] + include::service-root.asciidoc[] include::streaming.asciidoc[]
community/server/src/docs/dev/rest-api/security.asciidoc+50 −0 added@@ -0,0 +1,50 @@ +[[rest-api-security]] +== Neo4j Authentication and Authorization == + +Note: This is a new feature, your feedback is encouraged. + +In order to prevent unauthorized access to Neo4j, the REST API supports authorization and authentication. +When enabled, requests to the REST API must be authorized by an +Authorization Token+. +The token is acquired by authenticating, and can then be used to gain access to Neo4j. + +In general, it is recommended that you give the authorization token, rather than your username and password, to your Neo4j Driver. +This way the driver can access Neo4j, but is not allowed to change your password or invalidate the token. + +When Neo4j is first installed you can authenticate with the default user 'neo4j' and the default password 'neo4j'. +However, the default password must be <<rest-api-required-password-changes,changed>> before an authorization token is granted. +This can easily be done via the Neo4j Browser, or via direct HTTP calls. +For automated deployments, you may also <<rest-api-security-copy-config,copy security configuration from another neo4j instance>>. + +The authorization token is local to each Neo4j instance. +If you wish to have multiple instances, such as a set of cluster members, accept the same authorization token see <<rest-api-setting-authorization-token>>. +Or, alternatively, see <<rest-api-security-copy-config>>. + +To determine if security is enabled and to check if an authorization token is valid, see <<rest-api-get-current-authorization-status>>. + +include::authenticate-to-obtain-authorization-token.asciidoc[] + +include::using-the-authorization-token.asciidoc[] + +include::required-password-changes.asciidoc[] + +include::changing-your-password.asciidoc[] + +include::get-current-authorization-status.asciidoc[] + +include::get-authorization-status-when-auth-is-disabled.asciidoc[] + +include::invalidating-the-authorization-token.asciidoc[] + +include::setting-authorization-token.asciidoc[] + +include::attempting-to-get-authorization-status-while-unauthorized.asciidoc[] + +include::incorrect-username-or-password.asciidoc[] + +[[rest-api-security-copy-config]] +=== Copying security configuration from one instance to another === + +In many cases, such as automated deployments, you may want to start a Neo4j instance with pre-configured authentication and authorization. +This is possible by copying the authentication database file from a pre-existing Neo4j instance to your new instance. + +By default, this file is located at `data/dbms/auth.db`, and simply copying that file into a new neo4j instance will transfer your password and authorization token. \ No newline at end of file
community/server/src/docs/ops/security.asciidoc+17 −1 modified@@ -21,6 +21,22 @@ org.neo4j.server.webserver.port=7474 If you need to enable access from external hosts, configure the Web server in the _conf/neo4j-server.properties_ by setting the property +org.neo4j.server.webserver.address=0.0.0.0+ to enable access from any host. +== Authorization == + +Neo4j allows you to enable authorization to access the REST API. +This will disallow anyone without a valid authorization token access to the database. + +The authorization data is stored under 'data/dbms/authorization'. +This file can be copied over to other neo4j instances to ensure they share the same username/password and authorization token. + +Please refer to <<rest-api-security>> for details on this API. + +[source] +---- +# Enable authorization +dbms.security.authorization_enabled=true +---- + == Arbitrary code execution == By default, the Neo4j Server comes with some places where arbitrary code code execution can happen. These are the <<rest-api-traverse>> REST endpoints. @@ -63,7 +79,7 @@ org.neo4j.server.webserver.https.port=443 == Server Authorization Rules == -Administrators may require more fine-grained security policies in addition to IP-level restrictions on the Web server. +Administrators may require more fine-grained security policies in addition to the basic authorization and/or IP-level restrictions on the Web server. Neo4j server supports administrators in allowing or disallowing access the specific aspects of the database based on credentials that users or applications provide. To facilitate domain-specific authorization policies in Neo4j Server, security rules can be implemented and registered with the server.
community/server/src/main/java/org/neo4j/server/AbstractNeoServer.java+23 −30 modified@@ -31,7 +31,6 @@ import javax.servlet.Filter; import org.apache.commons.configuration.Configuration; - import org.neo4j.cypher.javacompat.ExecutionEngine; import org.neo4j.cypher.javacompat.internal.ServerExecutionEngine; import org.neo4j.graphdb.DependencyResolver; @@ -41,13 +40,15 @@ import org.neo4j.helpers.Provider; import org.neo4j.helpers.RunCarefully; import org.neo4j.helpers.Settings; +import org.neo4j.kernel.DefaultFileSystemAbstraction; import org.neo4j.kernel.InternalAbstractGraphDatabase; import org.neo4j.kernel.configuration.Config; import org.neo4j.kernel.guard.Guard; import org.neo4j.kernel.impl.util.Dependencies; import org.neo4j.kernel.impl.util.JobScheduler; import org.neo4j.kernel.impl.util.StringLogger; import org.neo4j.kernel.info.DiagnosticsManager; +import org.neo4j.kernel.lifecycle.LifeSupport; import org.neo4j.kernel.logging.ConsoleLogger; import org.neo4j.kernel.logging.Logging; import org.neo4j.server.configuration.ConfigWrappingConfiguration; @@ -82,9 +83,11 @@ import org.neo4j.server.rest.web.DatabaseActions; import org.neo4j.server.rrd.RrdDbProvider; import org.neo4j.server.rrd.RrdFactory; -import org.neo4j.server.security.KeyStoreFactory; -import org.neo4j.server.security.KeyStoreInformation; -import org.neo4j.server.security.SslCertificateFactory; +import org.neo4j.server.security.auth.FileUserRepository; +import org.neo4j.server.security.auth.SecurityCentral; +import org.neo4j.server.security.ssl.KeyStoreFactory; +import org.neo4j.server.security.ssl.KeyStoreInformation; +import org.neo4j.server.security.ssl.SslCertificateFactory; import org.neo4j.server.statistic.StatisticCollector; import org.neo4j.server.web.ServerInternalSettings; import org.neo4j.server.web.SimpleUriBuilder; @@ -95,7 +98,6 @@ import static java.lang.Math.round; import static java.lang.String.format; import static java.util.concurrent.TimeUnit.MILLISECONDS; - import static org.neo4j.helpers.Clock.SYSTEM_CLOCK; import static org.neo4j.helpers.collection.Iterables.map; import static org.neo4j.kernel.impl.util.JobScheduler.Group.serverTransactionTimeout; @@ -123,11 +125,14 @@ public abstract class AbstractNeoServer implements NeoServer protected WebServer webServer; protected final StatisticCollector statisticsCollector = new StatisticCollector(); + protected SecurityCentral security; + private final PreFlightTasks preFlight; private final List<ServerModule> serverModules = new ArrayList<>(); private final SimpleUriBuilder uriBuilder = new SimpleUriBuilder(); private final Config dbConfig; + private final LifeSupport life = new LifeSupport(); private InterruptThreadTimer interruptStartupTimer; private DatabaseActions databaseActions; @@ -161,9 +166,12 @@ public AbstractNeoServer( ConfigurationBuilder configurator, Database.Factory db this.dbConfig = new Config(); this.log = dependencies.logging().getConsoleLog( getClass() ); - this.database = dependencyResolver.satisfyDependency(dbFactory.newDatabase( dbConfig, - dependencies)); + this.database = life.add( dependencyResolver.satisfyDependency(dbFactory.newDatabase( dbConfig, dependencies)) ); + FileUserRepository users = life.add(new FileUserRepository( new DefaultFileSystemAbstraction(), + configurator.configuration().get( ServerInternalSettings.authorization_store ))); + + this.security = life.add(new SecurityCentral( Clock.SYSTEM_CLOCK, users )); this.preFlight = dependencyResolver.satisfyDependency(createPreflightTasks()); this.webServer = createWebServer(); @@ -195,7 +203,7 @@ public void start() throws ServerStartupException { reloadConfigFromDisk(); - database.start(); + life.start(); DiagnosticsManager diagnosticsManager = resolveDependency(DiagnosticsManager.class); @@ -233,17 +241,13 @@ public void start() throws ServerStartupException // Guard against poor operating systems that don't clear interrupt flags // after having handled interrupts (looking at you, Bill). Thread.interrupted(); - if ( interruptStartupTimer.wasTriggered() ) { // Make sure we don't leak rrd db files stopRrdDb(); // If the database has been started, attempt to cleanly shut it down to avoid unclean shutdowns. - if(database.isRunning()) - { - stopDatabase(); - } + life.shutdown(); throw new ServerStartupException( "Startup took longer than " + interruptStartupTimer.getTimeoutMillis() + "ms, " + @@ -573,6 +577,7 @@ protected KeyStoreInformation initHttpsKeyStore() @Override public void stop() { + // TODO: All components should be moved over to the LifeSupport instance, life, in here. new RunCarefully( new Runnable() { @Override @@ -599,7 +604,7 @@ public void run() @Override public void run() { - stopDatabase(); + life.stop(); } } ).run(); @@ -636,21 +641,6 @@ private void stopWebServer() } } - private void stopDatabase() - { - if ( database != null ) - { - try - { - database.stop(); - } - catch ( Throwable e ) - { - throw new RuntimeException( e ); - } - } - } - @Override public Database getDatabase() { @@ -731,8 +721,10 @@ protected Collection<InjectableProvider<?>> createDefaultInjectables() singletons.add( new ExecutionEngineProvider( cypherExecutor ) ); singletons.add( providerForSingleton( transactionFacade, TransactionFacade.class ) ); + singletons.add( providerForSingleton( security, SecurityCentral.class ) ); singletons.add( new TransactionFilter( database ) ); singletons.add( new LoggingProvider( dependencies.logging() ) ); + singletons.add( providerForSingleton( dependencies.logging().getConsoleLog( NeoServer.class ), ConsoleLogger.class ) ); return singletons; } @@ -773,7 +765,8 @@ protected <T> T resolveDependency( Class<T> type ) @Override public DependencyResolver instance() { - return dependencyResolver.resolveDependency( Database.class ).getGraph().getDependencyResolver(); + Database db = dependencyResolver.resolveDependency( Database.class ); + return db.getGraph().getDependencyResolver(); } }); }
community/server/src/main/java/org/neo4j/server/CommunityNeoServer.java+5 −5 modified@@ -29,7 +29,7 @@ import org.neo4j.server.configuration.ConfigurationBuilder; import org.neo4j.server.configuration.Configurator; import org.neo4j.server.database.Database; -import org.neo4j.server.modules.DiscoveryModule; +import org.neo4j.server.modules.DBMSModule; import org.neo4j.server.modules.ManagementApiModule; import org.neo4j.server.modules.Neo4jBrowserModule; import org.neo4j.server.modules.RESTApiModule; @@ -106,12 +106,12 @@ protected Iterable<ServerModule> createServerModules() { Logging logging = dependencies.logging(); return Arrays.asList( - new DiscoveryModule( webServer, logging ), + new DBMSModule(webServer, security, configurator.configuration() ), new RESTApiModule( webServer, database, configurator.configuration(), logging ), - new ManagementApiModule( webServer, configurator.configuration(), logging ), + new ManagementApiModule( webServer, configurator.configuration() ), new ThirdPartyJAXRSModule( webServer, configurator.configuration(), logging, this ), - new WebAdminModule( webServer, logging ), - new Neo4jBrowserModule( webServer, configurator.configuration(), logging, database ), + new WebAdminModule( webServer ), + new Neo4jBrowserModule( webServer ), new StatisticModule( webServer, statisticsCollector, configurator.configuration() ), new SecurityRulesModule( webServer, configurator.configuration(), logging ) ); }
community/server/src/main/java/org/neo4j/server/configuration/ServerSettings.java+3 −0 modified@@ -153,4 +153,7 @@ private ThirdPartyJaxRsPackage createThirdPartyJaxRsPackage( String packageAndMo @Description( "Timeout for idle transactions." ) public static final Setting<Long> transaction_timeout = setting( "org.neo4j.server.transaction.timeout", DURATION, "60s" ); + + @Description( "Enable authorization requirement to access Neo4j." ) + public static final Setting<Boolean> authorization_enabled = setting("dbms.security.authorization_enabled", BOOLEAN, FALSE); }
community/server/src/main/java/org/neo4j/server/database/LifecycleManagingDatabase.java+3 −1 modified@@ -19,6 +19,7 @@ */ package org.neo4j.server.database; +import java.io.File; import java.util.Map; import org.neo4j.cypher.javacompat.ExecutionEngine; @@ -88,7 +89,8 @@ public Logging getLogging() @Override public String getLocation() { - return dbConfig.get(GraphDatabaseSettings.store_dir).getAbsolutePath(); + File file = dbConfig.get( GraphDatabaseSettings.store_dir ); + return file.getAbsolutePath(); } @Override
community/server/src/main/java/org/neo4j/server/modules/DBMSModule.java+23 −8 renamed@@ -21,36 +21,51 @@ import java.util.List; -import org.neo4j.kernel.logging.ConsoleLogger; -import org.neo4j.kernel.logging.Logging; +import org.neo4j.kernel.configuration.Config; +import org.neo4j.server.configuration.ServerSettings; +import org.neo4j.server.rest.dbms.AuthenticationService; +import org.neo4j.server.rest.dbms.AuthorizationFilter; +import org.neo4j.server.rest.dbms.UserService; import org.neo4j.server.rest.discovery.DiscoveryService; +import org.neo4j.server.security.auth.SecurityCentral; import org.neo4j.server.web.WebServer; import static org.neo4j.server.JAXRSHelper.listFrom; -public class DiscoveryModule implements ServerModule +/** + * Mounts the DBMS REST API. + */ +public class DBMSModule implements ServerModule { private static final String ROOT_PATH = "/"; private final WebServer webServer; - private final ConsoleLogger log; + private final SecurityCentral security; + private final boolean enableAuthentication; - public DiscoveryModule( WebServer webServer, Logging logging ) + public DBMSModule( WebServer webServer, SecurityCentral security, Config config ) { this.webServer = webServer; - this.log = logging.getConsoleLog( getClass() ); + this.security = security; + this.enableAuthentication = config.get( ServerSettings.authorization_enabled ); } @Override public void start() { webServer.addJAXRSClasses( getClassNames(), ROOT_PATH, null ); - log.log( "Mounted discovery module at [%s]", ROOT_PATH ); + if(enableAuthentication) + { + webServer.addFilter( new AuthorizationFilter(security), "/*" ); + } } private List<String> getClassNames() { - return listFrom( DiscoveryService.class.getName() ); + return listFrom( + DiscoveryService.class.getName(), + AuthenticationService.class.getName(), + UserService.class.getName()); } @Override
community/server/src/main/java/org/neo4j/server/modules/ManagementApiModule.java+1 −6 modified@@ -23,8 +23,6 @@ import java.util.List; import org.neo4j.kernel.configuration.Config; -import org.neo4j.kernel.logging.ConsoleLogger; -import org.neo4j.kernel.logging.Logging; import org.neo4j.server.web.ServerInternalSettings; import org.neo4j.server.web.WebServer; import org.neo4j.server.webadmin.rest.JmxService; @@ -39,21 +37,18 @@ public class ManagementApiModule implements ServerModule { private final Config config; private final WebServer webServer; - private final ConsoleLogger log; - public ManagementApiModule(WebServer webServer, Config config, Logging logging) + public ManagementApiModule( WebServer webServer, Config config ) { this.webServer = webServer; this.config = config; - this.log = logging.getConsoleLog( getClass() ); } @Override public void start() { String serverMountPoint = managementApiUri().toString(); webServer.addJAXRSClasses( getClassNames(), serverMountPoint, null ); - log.log( "Mounted management API at [%s]", serverMountPoint ); } private List<String> getClassNames()
community/server/src/main/java/org/neo4j/server/modules/Neo4jBrowserModule.java+1 −12 modified@@ -19,35 +19,24 @@ */ package org.neo4j.server.modules; -import org.neo4j.kernel.configuration.Config; -import org.neo4j.kernel.logging.ConsoleLogger; -import org.neo4j.kernel.logging.Logging; -import org.neo4j.server.database.Database; import org.neo4j.server.web.WebServer; public class Neo4jBrowserModule implements ServerModule { private static final String DEFAULT_NEO4J_BROWSER_PATH = "/browser"; private static final String DEFAULT_NEO4J_BROWSER_STATIC_WEB_CONTENT_LOCATION = "browser"; - private final Config config; private final WebServer webServer; - private final Database database; - private final ConsoleLogger log; - public Neo4jBrowserModule(WebServer webServer, Config config, Logging logging, Database database) + public Neo4jBrowserModule( WebServer webServer ) { this.webServer = webServer; - this.config = config; - this.database = database; - this.log = logging.getConsoleLog( getClass() ); } @Override public void start() { webServer.addStaticContent( DEFAULT_NEO4J_BROWSER_STATIC_WEB_CONTENT_LOCATION, DEFAULT_NEO4J_BROWSER_PATH ); - log.log( "Mounted Neo4j Browser at [%s]", DEFAULT_NEO4J_BROWSER_PATH ); } @Override
community/server/src/main/java/org/neo4j/server/modules/RESTApiModule.java+3 −2 modified@@ -43,6 +43,9 @@ import static org.neo4j.server.JAXRSHelper.listFrom; +/** + * Mounts the database REST API. + */ public class RESTApiModule implements ServerModule { private PluginManager plugins; @@ -73,8 +76,6 @@ public void start() loadPlugins(); setupRequestTimeLimit(); - - log.log( "Mounted REST API at [%s]", restApiUri.toString() ); } catch ( URISyntaxException e ) {
community/server/src/main/java/org/neo4j/server/modules/ThirdPartyJAXRSModule.java+1 −1 modified@@ -60,7 +60,7 @@ public void start() List<String> packageNames = packagesFor( tpp ); Collection<Injectable<?>> injectables = extensionInitializer.initializePackages( packageNames ); webServer.addJAXRSPackages( packageNames, tpp.getMountPoint(), injectables ); - log.log( "Mounted third-party JAX-RS package [%s] at [%s]", tpp.getPackageName(), tpp.getMountPoint() ); + log.log( "Mounted unmanaged extension [%s] at [%s]", tpp.getPackageName(), tpp.getMountPoint() ); } }
community/server/src/main/java/org/neo4j/server/modules/WebAdminModule.java+1 −6 modified@@ -19,8 +19,6 @@ */ package org.neo4j.server.modules; -import org.neo4j.kernel.logging.ConsoleLogger; -import org.neo4j.kernel.logging.Logging; import org.neo4j.server.web.WebServer; public class WebAdminModule implements ServerModule @@ -29,19 +27,16 @@ public class WebAdminModule implements ServerModule private static final String DEFAULT_WEB_ADMIN_STATIC_WEB_CONTENT_LOCATION = "webadmin-html"; private final WebServer webServer; - private final ConsoleLogger log; - public WebAdminModule( WebServer webServer, Logging logging ) + public WebAdminModule( WebServer webServer ) { this.webServer = webServer; - this.log = logging.getConsoleLog( getClass() ); } @Override public void start() { webServer.addStaticContent( DEFAULT_WEB_ADMIN_STATIC_WEB_CONTENT_LOCATION, DEFAULT_WEB_ADMIN_PATH ); - log.log( "Mounted webadmin at [%s]", DEFAULT_WEB_ADMIN_PATH ); } @Override
community/server/src/main/java/org/neo4j/server/rest/dbms/AuthenticateHeaders.java+58 −0 added@@ -0,0 +1,58 @@ +/** + * Copyright (c) 2002-2014 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package org.neo4j.server.rest.dbms; + +import com.sun.jersey.core.util.Base64; + +public class AuthenticateHeaders +{ + /** + * Extract the encoded authorization token from a HTTP Authenticate header value. + */ + public static String extractToken(String authenticateHeader) + { + if(authenticateHeader == null) + { + return ""; + } + + String[] parts = authenticateHeader.trim().split( " " ); + String tokenSegment = parts[parts.length-1]; + + if(tokenSegment.trim().length() == 0) + { + return ""; + } + + String decoded = Base64.base64Decode( tokenSegment ); + if(decoded.length() < 1) + { + return ""; + } + + String[] blankAndToken = decoded.split( ":" ); + if(blankAndToken.length != 2) + { + return ""; + } + + return blankAndToken[1]; + } +}
community/server/src/main/java/org/neo4j/server/rest/dbms/AuthenticationService.java+159 −0 added@@ -0,0 +1,159 @@ +/** + * Copyright (c) 2002-2014 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package org.neo4j.server.rest.dbms; + +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.GET; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; + +import org.neo4j.kernel.api.exceptions.Status; +import org.neo4j.kernel.configuration.Config; +import org.neo4j.kernel.logging.ConsoleLogger; +import org.neo4j.server.configuration.ServerSettings; +import org.neo4j.server.rest.repr.AuthorizationRepresentation; +import org.neo4j.server.rest.repr.BadInputException; +import org.neo4j.server.rest.repr.ExceptionRepresentation; +import org.neo4j.server.rest.repr.InputFormat; +import org.neo4j.server.rest.repr.OutputFormat; +import org.neo4j.server.rest.transactional.error.Neo4jError; +import org.neo4j.server.security.auth.SecurityCentral; +import org.neo4j.server.security.auth.User; +import org.neo4j.server.security.auth.exception.TooManyAuthenticationAttemptsException; + +import static org.neo4j.kernel.api.exceptions.Status.Security.AuthorizationFailed; +import static org.neo4j.server.rest.dbms.UserService.PASSWORD; +import static org.neo4j.server.rest.dbms.UserService.USERNAME; +import static org.neo4j.server.rest.web.CustomStatusType.TOO_MANY; +import static org.neo4j.server.rest.web.CustomStatusType.UNPROCESSABLE; + +/** + * This serves as the HTTP API entry point into {@link org.neo4j.server.security.auth.SecurityCentral}, and is used + * by remote clients to authenticate, check auth status and acquire authorization tokens. + */ +@Path(AuthenticationService.AUTHENTICATION_PATH) +public class AuthenticationService +{ + public static final String AUTHENTICATION_PATH = "/authentication"; + + private final SecurityCentral security; + private final InputFormat input; + private final OutputFormat output; + private final ConsoleLogger log; + private final boolean authEnabled; + + public AuthenticationService(@Context SecurityCentral security, @Context InputFormat input, + @Context Config config, + @Context OutputFormat output, @Context ConsoleLogger log ) + { + this.security = security; + this.input = input; + this.output = output; + this.log = log; + this.authEnabled = config.get( ServerSettings.authorization_enabled ); + } + + @POST + public Response authenticate( @Context HttpServletRequest req, String payload ) + { + try + { + Map<String,Object> deserialized = input.readMap( payload ); + + String user = getString(deserialized, USERNAME ); + String password = getString( deserialized, PASSWORD ); + + if( security.authenticate( user, password )) + { + return output.ok( new AuthorizationRepresentation( security.userForName( user ) ) ); + } + else + { + log.warn( "Failed authentication attempt for '%s' from %s", user, req.getRemoteAddr() ); + } + + return output.response( UNPROCESSABLE, new ExceptionRepresentation( + new Neo4jError( Status.Security.AuthenticationFailed, "Invalid username and/or password." ) ) ); + } + catch ( BadInputException e ) + { + return output.badRequestWithoutLegacyStacktrace( e ); + } + catch ( IllegalArgumentException e ) + { + return output.response( UNPROCESSABLE, new ExceptionRepresentation( new Neo4jError( Status.Request.Invalid, e.getMessage() ) ) ); + } + catch ( TooManyAuthenticationAttemptsException e ) + { + return output.response( TOO_MANY, new ExceptionRepresentation( new Neo4jError( e.status(), e ) ) ); + } + } + + @GET + public Response metadata( @HeaderParam( HttpHeaders.AUTHORIZATION ) String authHeader ) + { + if(!authEnabled) + { + return output.ok(); + } + + if(authHeader == null) + { + return output.unauthorized( new ExceptionRepresentation ( new Neo4jError( AuthorizationFailed, "No authorization token supplied." ) ), "None" ); + } + + String token = AuthenticateHeaders.extractToken( authHeader ); + + if(token.length() == 0) + { + return output.response( Response.Status.BAD_REQUEST, new ExceptionRepresentation ( new Neo4jError( Status.Request.InvalidFormat, "Invalid Authorization header." ) )); + } + + User user = security.userForToken( token ); + if( user.privileges().APIAccess() ) + { + return output.ok( new AuthorizationRepresentation( user ) ); + } + + return output.unauthorized( new ExceptionRepresentation (new Neo4jError( AuthorizationFailed, "Invalid authorization token supplied." ) ), "None" ); + } + + private String getString( Map<String, Object> data, String key ) throws IllegalArgumentException + { + Object o = data.get( key ); + if( o == null ) + { + throw new IllegalArgumentException( String.format("Required parameter '%s' is missing.", key) ); + } + if(!(o instanceof String)) + { + throw new IllegalArgumentException( String.format("Expected '%s' to be a string.", key) ); + } + + return (String)o; + } + +}
community/server/src/main/java/org/neo4j/server/rest/dbms/AuthorizationFilter.java+222 −0 added@@ -0,0 +1,222 @@ +/** + * Copyright (c) 2002-2014 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package org.neo4j.server.rest.dbms; + +import java.io.IOException; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.core.HttpHeaders; + +import org.neo4j.kernel.api.exceptions.Status; +import org.neo4j.kernel.impl.util.Charsets; +import org.neo4j.server.rest.domain.JsonHelper; +import org.neo4j.server.rest.security.UriPathWildcardMatcher; +import org.neo4j.server.security.auth.SecurityCentral; + +import static java.util.Arrays.asList; +import static org.neo4j.helpers.collection.MapUtil.map; +import static org.neo4j.server.rest.dbms.AuthenticationService.AUTHENTICATION_PATH; + +public class AuthorizationFilter implements Filter +{ + enum ErrorType + { + // This is a pretty annoying duplication of work, where we're manually re-implementing the JSON serialization + // layer. Because we don't have access to jersey at this level, we can't use our regular serialization. This, + // obviously, implies a larger architectural issue, which is left as a future exercise. + + NO_HEADER(401, true ) + { + @Override + Object body(String authURL) + { + return map("errors", asList(map( + "code", Status.Security.AuthorizationFailed.code().serialize(), + "message", "No authorization token supplied.")), + "authentication", authURL); + } + }, + INVALID_TOKEN(401, true ) + { + @Override + Object body(String authURL) + { + return map("errors", asList(map( + "code", Status.Security.AuthorizationFailed.code().serialize(), + "message", "Invalid authorization token supplied.")), + "authentication", authURL); + } + }, + BAD_HEADER(400, false) + { + @Override + Object body(String authURL) + { + return map("errors", asList(map( + "code", Status.Request.InvalidFormat.code().serialize(), + "message", "Invalid Authorization header."))); + } + }; + + private final int statusCode; + private final boolean includeWWWAuthenticateHeader; + + private ErrorType( int statusCode, boolean includeWWWAuthenticateHeader ) + { + this.statusCode = statusCode; + this.includeWWWAuthenticateHeader = includeWWWAuthenticateHeader; + } + + synchronized void reply( HttpServletResponse response, HttpServletRequest req ) throws IOException + { + response.setStatus( statusCode ); + if(includeWWWAuthenticateHeader) + { + response.addHeader( HttpHeaders.WWW_AUTHENTICATE, "None" ); + } + + String authUrl = req.getScheme() + "://" + req.getHeader( HttpHeaders.HOST ) + AUTHENTICATION_PATH; + + response.getOutputStream().write( JsonHelper.createJsonFrom( body(authUrl) ).getBytes( Charsets.UTF_8 ) ); + } + + abstract Object body(String authenticateURL); + } + + private final UriPathWildcardMatcher[] whitelist = new UriPathWildcardMatcher[] + { + new UriPathWildcardMatcher("/authentication"), + new UriPathWildcardMatcher("/browser*"), + new UriPathWildcardMatcher("/webadmin*"), + new UriPathWildcardMatcher("/user/*/authorization_token"), + new UriPathWildcardMatcher("/user/*/password"), + new UriPathWildcardMatcher("/"), + }; + + private final SecurityCentral security; + + public AuthorizationFilter( SecurityCentral security ) + { + this.security = security; + } + + @Override + public void init( FilterConfig filterConfig ) throws ServletException + { + + } + + @Override + public void doFilter( ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain ) throws IOException, ServletException + { + validateRequestType( servletRequest ); + validateResponseType( servletResponse ); + + HttpServletRequest request = (HttpServletRequest) servletRequest; + HttpServletResponse response = (HttpServletResponse) servletResponse; + + if(authorized(request) || whitelisted(request)) + { + filterChain.doFilter( servletRequest, servletResponse ); + } + else + { + errorType(request).reply( response, request ); + } + } + + @Override + public void destroy() + { + + } + + private boolean whitelisted( HttpServletRequest request ) + { + String path = request.getContextPath() + (request.getPathInfo() == null ? "" : request.getPathInfo()); + for ( UriPathWildcardMatcher pattern : whitelist ) + { + if(pattern.matches( path )) + { + return true; + } + } + return false; + } + + private boolean authorized( HttpServletRequest request ) + { + String token = extractToken( request ); + return token != null && security.userForToken( token ).privileges().APIAccess(); + } + + private ErrorType errorType( HttpServletRequest request ) + { + String token = extractToken( request ); + if(token == null) + { + return ErrorType.NO_HEADER; + } + else if(token.length() == 0) + { + return ErrorType.BAD_HEADER; + } + return ErrorType.INVALID_TOKEN; + } + + private String extractToken( HttpServletRequest request ) + { + String value = request.getHeader( HttpHeaders.AUTHORIZATION ); + if(value == null) + { + return null; + } + else + { + return AuthenticateHeaders.extractToken( value ); + } + } + + private void validateRequestType( ServletRequest request ) throws ServletException + { + if ( !(request instanceof HttpServletRequest) ) + { + throw new ServletException( String.format( "Expected HttpServletRequest, received [%s]", request.getClass() + .getCanonicalName() ) ); + } + } + + private void validateResponseType( ServletResponse response ) throws ServletException + { + if ( !(response instanceof HttpServletResponse) ) + { + throw new ServletException( String.format( "Expected HttpServletResponse, received [%s]", + response.getClass() + .getCanonicalName() ) ); + } + } +}
community/server/src/main/java/org/neo4j/server/rest/dbms/UserService.java+179 −0 added@@ -0,0 +1,179 @@ +/** + * Copyright (c) 2002-2014 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package org.neo4j.server.rest.dbms; + +import java.io.IOException; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; + +import org.neo4j.kernel.api.exceptions.Status; +import org.neo4j.kernel.logging.ConsoleLogger; +import org.neo4j.server.rest.repr.AuthorizationRepresentation; +import org.neo4j.server.rest.repr.BadInputException; +import org.neo4j.server.rest.repr.ExceptionRepresentation; +import org.neo4j.server.rest.repr.InputFormat; +import org.neo4j.server.rest.repr.OutputFormat; +import org.neo4j.server.rest.transactional.error.Neo4jError; +import org.neo4j.server.security.auth.SecurityCentral; +import org.neo4j.server.security.auth.exception.IllegalTokenException; +import org.neo4j.server.security.auth.exception.TooManyAuthenticationAttemptsException; + +import static org.neo4j.server.rest.web.CustomStatusType.TOO_MANY; +import static org.neo4j.server.rest.web.CustomStatusType.UNPROCESSABLE; + +@Path("/user") +public class UserService +{ + public static final String USERNAME = "username"; + public static final String PASSWORD = "password"; + public static final String NEW_AUTHORIZATION_TOKEN = "new_authorization_token"; + public static final String NEW_PASSWORD = "new_password"; + + private final SecurityCentral security; + private final InputFormat input; + private final OutputFormat output; + private final ConsoleLogger log; + + public UserService(@Context SecurityCentral security, @Context InputFormat input, @Context OutputFormat output, + @Context ConsoleLogger log) + { + this.security = security; + this.input = input; + this.output = output; + this.log = log; + } + + @POST + @Path("/{user}/authorization_token") + public Response regenerateToken( @PathParam( "user" ) String user, @Context HttpServletRequest req, String payload) + { + try + { + Map<String,Object> deserialized = input.readMap( payload ); + + String password = getString( deserialized, PASSWORD ); + + if( security.authenticate( user, password )) + { + if( deserialized.containsKey( NEW_AUTHORIZATION_TOKEN ) ) + { + security.setToken( user, getString( deserialized, NEW_AUTHORIZATION_TOKEN ) ); + } + else + { + security.regenerateToken( user ); + } + return output.ok( new AuthorizationRepresentation( security.userForName( user ) ) ); + } + else + { + log.warn( "Failed authentication attempt for '%s' from %s", user, req.getRemoteAddr() ); + } + + return output.response( UNPROCESSABLE, new ExceptionRepresentation( + new Neo4jError( Status.Security.AuthenticationFailed, "Invalid username and/or password." ) ) ); + } + catch ( BadInputException | IllegalTokenException e ) + { + return output.badRequestWithoutLegacyStacktrace( e ); + } + catch ( IllegalArgumentException e ) + { + return output.response( UNPROCESSABLE, new ExceptionRepresentation( new Neo4jError( Status.Request.Invalid, e.getMessage() ) ) ); + } + catch ( TooManyAuthenticationAttemptsException e ) + { + return output.response( TOO_MANY, new ExceptionRepresentation( new Neo4jError( e.status(), e ) ) ); + } + catch ( IOException e ) + { + return output.serverErrorWithoutLegacyStacktrace( e ); + } + } + + @POST + @Path("/{user}/password") + public Response setPassword( @PathParam( "user" ) String user, @Context HttpServletRequest req, String payload ) + { + try + { + Map<String,Object> deserialized = input.readMap( payload ); + + String password = getString( deserialized, PASSWORD ); + String newPassword = getString( deserialized, NEW_PASSWORD ); + + if(password.equals( newPassword )) + { + return output.response( UNPROCESSABLE, new ExceptionRepresentation( + new Neo4jError( Status.Request.Invalid, "Old password and new password cannot be the same." ) ) ); + } + + if( security.authenticate( user, password )) + { + security.setPassword( user, newPassword ); + return output.ok( new AuthorizationRepresentation( security.userForName( user ) ) ); + } + else + { + log.warn( "Failed authentication attempt for '%s' from %s", user, req.getRemoteAddr() ); + } + + return output.response( UNPROCESSABLE, new ExceptionRepresentation( + new Neo4jError( Status.Security.AuthenticationFailed, "Invalid username and/or password." ) ) ); + } + catch ( BadInputException e ) + { + return output.badRequestWithoutLegacyStacktrace( e ); + } + catch ( IllegalArgumentException e ) + { + return output.response( UNPROCESSABLE, new ExceptionRepresentation( new Neo4jError( Status.Request.Invalid, e.getMessage() ) ) ); + } + catch ( TooManyAuthenticationAttemptsException e ) + { + return output.response( TOO_MANY, new ExceptionRepresentation( new Neo4jError( e.status(), e ) ) ); + } + catch ( IOException e ) + { + return output.serverErrorWithoutLegacyStacktrace( e ); + } + } + + private String getString( Map<String, Object> data, String key ) throws IllegalArgumentException + { + Object o = data.get( key ); + if( o == null ) + { + throw new IllegalArgumentException( String.format("Required parameter '%s' is missing.", key) ); + } + if(!(o instanceof String)) + { + throw new IllegalArgumentException( String.format("Expected '%s' to be a string.", key) ); + } + + return (String)o; + } +}
community/server/src/main/java/org/neo4j/server/rest/discovery/DiscoveryService.java+23 −11 modified@@ -22,47 +22,59 @@ import java.net.URISyntaxException; import javax.ws.rs.GET; +import javax.ws.rs.HeaderParam; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; -import org.eclipse.jetty.util.log.Log; -import org.eclipse.jetty.util.log.Logger; - import org.neo4j.kernel.configuration.Config; +import org.neo4j.server.configuration.ServerSettings; +import org.neo4j.server.rest.dbms.AuthenticationService; +import org.neo4j.server.rest.repr.AccessDeniedDiscoveryRepresentation; import org.neo4j.server.rest.repr.DiscoveryRepresentation; import org.neo4j.server.rest.repr.OutputFormat; +import org.neo4j.server.security.auth.SecurityCentral; import org.neo4j.server.web.ServerInternalSettings; +import static org.neo4j.server.rest.dbms.AuthenticateHeaders.extractToken; + /** * Used to discover the rest of the server URIs through a HTTP GET request to * the server root (/). */ @Path( "/" ) public class DiscoveryService { - - private static final Logger LOGGER = Log.getLogger(DiscoveryService.class); private final Config configuration; private final OutputFormat outputFormat; + private final SecurityCentral security; - public DiscoveryService( @Context Config configuration, @Context OutputFormat outputFormat ) + public DiscoveryService( @Context Config configuration, @Context OutputFormat outputFormat, + @Context SecurityCentral security ) { this.configuration = configuration; this.outputFormat = outputFormat; + this.security = security; } @GET @Produces( MediaType.APPLICATION_JSON ) - public Response getDiscoveryDocument() throws URISyntaxException + public Response getDiscoveryDocument( @HeaderParam( "Authorization" ) String auth ) throws URISyntaxException { - String webAdminManagementUri = configuration.get( ServerInternalSettings.management_api_path ).getPath(); - String dataUri = configuration.get( ServerInternalSettings.rest_api_path ).getPath(); + if( !configuration.get( ServerSettings.authorization_enabled ) + || security.userForToken(extractToken( auth )).privileges().APIAccess() ) + { + String webAdminManagementUri = configuration.get( ServerInternalSettings.management_api_path ).getPath(); + String dataUri = configuration.get( ServerInternalSettings.rest_api_path ).getPath(); - DiscoveryRepresentation dr = new DiscoveryRepresentation( webAdminManagementUri, dataUri ); - return outputFormat.ok( dr ); + return outputFormat.ok( new DiscoveryRepresentation( webAdminManagementUri, dataUri ) ); + } + else + { + return outputFormat.ok( new AccessDeniedDiscoveryRepresentation( AuthenticationService.AUTHENTICATION_PATH ) ); + } } @GET
community/server/src/main/java/org/neo4j/server/rest/repr/AccessDeniedDiscoveryRepresentation.java+40 −0 added@@ -0,0 +1,40 @@ +/** + * Copyright (c) 2002-2014 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package org.neo4j.server.rest.repr; + +public class AccessDeniedDiscoveryRepresentation extends MappingRepresentation +{ + + private static final String AUTH_URI_KEY = "authentication"; + private final String authURI; + + public AccessDeniedDiscoveryRepresentation( String authURI ) + { + super( "discovery" ); + this.authURI = authURI; + } + + @Override + protected void serialize( MappingSerializer serializer ) + { + serializer.putUri( AUTH_URI_KEY, authURI ); + } + +} \ No newline at end of file
community/server/src/main/java/org/neo4j/server/rest/repr/AuthorizationRepresentation.java+57 −0 added@@ -0,0 +1,57 @@ +/** + * Copyright (c) 2002-2014 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package org.neo4j.server.rest.repr; + +import org.neo4j.server.security.auth.User; + +import static java.lang.String.format; + +public class AuthorizationRepresentation extends ObjectRepresentation +{ + private final User user; + + public AuthorizationRepresentation( User user ) + { + super( RepresentationType.AUTHORIZATION ); + this.user = user; + } + + @Mapping("username") + public ValueRepresentation user() { return ValueRepresentation.string( user.name() ); } + + @Mapping("password_change_required") + public ValueRepresentation passwordChangeRequired() { return ValueRepresentation.bool( user.passwordChangeRequired() ); } + + @Mapping( "password_change" ) + public ValueRepresentation passwordChange() + { + return ValueRepresentation.uri( format( "/user/%s/password", user.name() ) ); + } + + @Override + void extraData( MappingSerializer serializer ) + { + if( user.token() != User.NO_TOKEN) // Yes, this is supposed to be instance equality + { + serializer.putString( "authorization_token", user.token() ); + serializer.putUri( "authorization_token_change", format( "/user/%s/authorization_token", user.name() )); + } + } +}
community/server/src/main/java/org/neo4j/server/rest/repr/ExceptionRepresentation.java+88 −2 modified@@ -21,19 +21,58 @@ import java.util.ArrayList; import java.util.Collection; +import java.util.LinkedList; +import java.util.List; + +import org.neo4j.helpers.collection.IterableWrapper; +import org.neo4j.kernel.api.exceptions.Status; +import org.neo4j.server.rest.transactional.error.Neo4jError; public class ExceptionRepresentation extends MappingRepresentation { - private final Throwable exception; + private final List<Neo4jError> errors = new LinkedList<>(); + private boolean includeLegacyRepresentation; public ExceptionRepresentation( Throwable exception ) + { + this( exception, true ); + } + public ExceptionRepresentation( Throwable exception, boolean includeLegacyRepresentation ) + { + super( RepresentationType.EXCEPTION ); + this.errors.add( new Neo4jError( statusCode( exception ), exception ) ); + this.includeLegacyRepresentation = includeLegacyRepresentation; + } + + public ExceptionRepresentation( Neo4jError ... errors ) { super( RepresentationType.EXCEPTION ); - this.exception = exception; + for ( Neo4jError exception : errors ) + { + this.errors.add( exception ); + } } @Override protected void serialize( MappingSerializer serializer ) + { + // For legacy reasons, this actually serializes into two separate formats - the old format, which simply + // serializes a single exception, and the new format which serializes multiple errors and provides simple + // status codes. + if(includeLegacyRepresentation) + { + renderWithLegacyFormat( errors.get( 0 ).cause(), serializer ); + } + + renderWithStatusCodeFormat( serializer ); + } + + private void renderWithStatusCodeFormat( MappingSerializer serializer ) + { + serializer.putList( "errors", ErrorEntryRepresentation.list( errors ) ); + } + + private void renderWithLegacyFormat( Throwable exception, MappingSerializer serializer ) { String message = exception.getMessage(); if ( message != null ) @@ -60,4 +99,51 @@ protected void serialize( MappingSerializer serializer ) serializer.putMapping( "cause", new ExceptionRepresentation( cause ) ); } } + + private static class ErrorEntryRepresentation extends MappingRepresentation + { + private final Neo4jError error; + + public ErrorEntryRepresentation( Neo4jError error ) + { + super( "error-entry" ); + this.error = error; + } + + @Override + protected void serialize( MappingSerializer serializer ) + { + serializer.putString( "code", error.status().code().serialize() ); + serializer.putString( "message", error.getMessage() ); + if(error.shouldSerializeStackTrace()) + { + serializer.putString( "stacktrace", error.getStackTraceAsString() ); + } + } + + public static ListRepresentation list( Collection<Neo4jError> errors ) + { + return new ListRepresentation( "error-list", new IterableWrapper<ErrorEntryRepresentation, Neo4jError>( errors ) + { + @Override + protected ErrorEntryRepresentation underlyingObjectToObject( Neo4jError error ) + { + return new ErrorEntryRepresentation( error ); + } + } ); + } + } + + private static Status statusCode( Throwable current ) + { + while(current != null) + { + if(current instanceof Status.HasStatus) + { + return ((Status.HasStatus)current).status(); + } + current = current.getCause(); + } + return Status.General.UnknownFailure; + } }
community/server/src/main/java/org/neo4j/server/rest/repr/OutputFormat.java+26 −1 modified@@ -23,6 +23,7 @@ import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.net.URI; + import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; @@ -36,6 +37,7 @@ import org.neo4j.server.web.HttpHeaderUtils; import static javax.ws.rs.core.Response.Status.BAD_REQUEST; +import static javax.ws.rs.core.Response.Status.UNAUTHORIZED; public class OutputFormat { @@ -86,11 +88,21 @@ public final <REPR extends Representation & EntityRepresentation> Response creat return response( Response.created( uri( representation ) ), representation ); } - public final Response response( Status status, Representation representation ) + public final Response response( Response.StatusType status, Representation representation ) { return response( Response.status( status ), representation ); } + /** + * Before the 'errors' response existed, we would just spit out stack traces. + * For new endpoints, we should return the new 'errors' response format, which will bundle stack traces only on + * unknown problems. + */ + public Response badRequestWithoutLegacyStacktrace( Throwable exception ) + { + return response( Response.status( BAD_REQUEST ), new ExceptionRepresentation( exception, false ) ); + } + public Response badRequest( Throwable exception ) { return response( Response.status( BAD_REQUEST ), new ExceptionRepresentation( exception ) ); @@ -124,6 +136,12 @@ public final <REPR extends Representation & EntityRepresentation> Response confl return response( Response.status( Status.CONFLICT ), representation ); } + /** @see {@link #badRequestWithoutLegacyStacktrace} */ + public Response serverErrorWithoutLegacyStacktrace( Throwable exception ) + { + return response( Response.status( Status.INTERNAL_SERVER_ERROR ), new ExceptionRepresentation( exception, false ) ); + } + public Response serverError( Throwable exception ) { return response( Response.status( Status.INTERNAL_SERVER_ERROR ), new ExceptionRepresentation( exception ) ); @@ -252,4 +270,11 @@ public Response badRequest( MediaType mediaType, String entity ) representationWriteHandler.onRepresentationFinal(); return Response.status( BAD_REQUEST ).type( mediaType ).entity( entity ).build(); } + + public Response unauthorized( Representation representation, String authChallenge ) + { + return formatRepresentation( Response.status( UNAUTHORIZED ), representation ) + .header( HttpHeaders.WWW_AUTHENTICATE, authChallenge ) + .build(); + } }
community/server/src/main/java/org/neo4j/server/rest/transactional/error/Neo4jError.java+7 −0 modified@@ -40,6 +40,11 @@ public class Neo4jError private final Status status; private final Throwable cause; + public Neo4jError( Status status, String message ) + { + this(status, new RuntimeException( message )); + } + public Neo4jError( Status status, Throwable cause ) { if ( status == null ) @@ -51,6 +56,8 @@ public Neo4jError( Status status, Throwable cause ) this.cause = cause; } + public Throwable cause() { return cause; } + public Status status() { return status;
community/server/src/main/java/org/neo4j/server/rest/web/CustomStatusType.java+66 −0 added@@ -0,0 +1,66 @@ +/** + * Copyright (c) 2002-2014 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package org.neo4j.server.rest.web; + +import javax.ws.rs.core.Response; + +import static javax.ws.rs.core.Response.Status.Family; + +public class CustomStatusType implements Response.StatusType +{ + public static Response.StatusType UNPROCESSABLE = new CustomStatusType( 422, "Unprocessable Entity" ); + public static Response.StatusType TOO_MANY = new CustomStatusType( 429, "Too Many Requests" ); + + private final int code; + private final String reason; + private final Family family; + + public CustomStatusType( int code, String reason ) + { + this.code = code; + this.reason = reason; + switch(code/100) { + case 1: this.family = Family.INFORMATIONAL; break; + case 2: this.family = Family.SUCCESSFUL; break; + case 3: this.family = Family.REDIRECTION; break; + case 4: this.family = Family.CLIENT_ERROR; break; + case 5: this.family = Family.SERVER_ERROR; break; + default: this.family = Family.OTHER; break; + } + } + + @Override + public int getStatusCode() + { + return code; + } + + @Override + public Family getFamily() + { + return family; + } + + @Override + public String getReasonPhrase() + { + return reason; + } +}
community/server/src/main/java/org/neo4j/server/security/auth/Authentication.java+240 −0 added@@ -0,0 +1,240 @@ +/** + * Copyright (c) 2002-2014 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package org.neo4j.server.security.auth; + +import java.io.IOException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicInteger; + +import org.neo4j.helpers.Clock; +import org.neo4j.helpers.ThisShouldNotHappenError; +import org.neo4j.kernel.impl.util.Charsets; +import org.neo4j.server.security.auth.exception.IllegalTokenException; +import org.neo4j.server.security.auth.exception.IllegalUsernameException; +import org.neo4j.server.security.auth.exception.TooManyAuthenticationAttemptsException; + +import static org.neo4j.kernel.impl.util.BytePrinter.compactHex; + +/** + * Controls authentication, you can use this class to verify if a client has valid user credentials for the user she + * claims to represent. This class also allows changing those credentials. + * + * Note that it crucially does not deal with authorization, including authorization tokens. These are handled by + * {@link org.neo4j.server.security.auth.SecurityCentral}. + */ +public class Authentication +{ + private final String DIGEST_ALGO = "SHA-256"; + + private class AuthenticationMetadata + { + private final AtomicInteger failedAuthAttempts = new AtomicInteger(); + private final String name; + private final int maxFailedAttempts; + private final long failedCooldownPeriod; + private final Clock clock; + + private long lastFailedAttemptTime = 0; + + AuthenticationMetadata( String name, int maxFailedAttempts, long failedCooldownPeriod, Clock clock ) + { + this.name = name; + this.maxFailedAttempts = maxFailedAttempts; + this.failedCooldownPeriod = failedCooldownPeriod; + this.clock = clock; + } + + public boolean authenticate( String password ) throws TooManyAuthenticationAttemptsException + { + if( tooManyAuthAttemtps() ) + { + throw new TooManyAuthenticationAttemptsException( "Too many failed authentication requests. Please try again in 5 seconds." ); + } + if( isCorrectPassword( password ) ) + { + failedAuthAttempts.set( 0 ); + return true; + } + else + { + failedAuthAttempts.incrementAndGet(); + lastFailedAttemptTime = clock.currentTimeMillis(); + return false; + } + } + + private boolean tooManyAuthAttemtps() + { + return failedAuthAttempts.get() >= maxFailedAttempts + && clock.currentTimeMillis() < (lastFailedAttemptTime + failedCooldownPeriod); + } + + protected boolean isCorrectPassword( String password ) + { + User user = users.get( name ); + if(user != null) + { + String hash = hash( user.credentials().salt(), password, user.credentials().digestAlgorithm() ); + return hash.equals( user.credentials().hash() ); + } + return false; + } + } + + private class UnknownUserMetadata extends AuthenticationMetadata + { + + UnknownUserMetadata( int maxFailedAttempts, long failedCooldownPeriod, Clock clock ) + { + super( "Unknown", maxFailedAttempts, failedCooldownPeriod, clock ); + } + + @Override + protected boolean isCorrectPassword( String password ) + { + return false; + } + } + + private final AuthenticationMetadata unknownUser; + private final int failedAuthCooldownPeriod = 5_000; + private final Clock clock; + private final int maxFailedAttempts; + private final SecureRandom rand = new SecureRandom(); + + /** Storage for data about principals */ + private final UserRepository users; + + /** Tracks authentication objects for each user, including tracking of authentication attempts. */ + private final ConcurrentMap<String, AuthenticationMetadata> authenticationData = new ConcurrentHashMap<>(); + + public Authentication( Clock clock, UserRepository users, int maxFailedAttempts ) + { + this.clock = clock; + this.users = users; + this.maxFailedAttempts = maxFailedAttempts; + this.unknownUser = new UnknownUserMetadata( maxFailedAttempts, failedAuthCooldownPeriod, clock ); + } + + /** Verify that a user name and password combo is valid. */ + public boolean authenticate( String name, String password ) throws TooManyAuthenticationAttemptsException + { + return authMetadataFor( name ).authenticate( password ); + } + + public void setPassword( String name, String password ) throws IOException + { + User user = users.get( name ); + if(user != null) + { + try + { + String salt = randomSalt(); + users.save( user.augment() + .withCredentials( new Credentials( salt, DIGEST_ALGO, hash( salt, password, DIGEST_ALGO ) ) ) + .withRequiredPasswordChange( false ) + .build()); + } + catch ( IllegalTokenException | IllegalUsernameException e ) + { + throw new ThisShouldNotHappenError( "Jake", "Token/username are not being modified.", e ); + } + } + else + { + throw new RuntimeException( "No such user: " + name ); + } + } + + /** Mark the user with the specified name as requiring a password change. All API access will be blocked until the password is changed. */ + public void requirePasswordChange( String name ) throws IOException + { + User user = users.get( name ); + if(user != null) + { + try + { + users.save(user.augment().withRequiredPasswordChange( true ).build()); + } + catch ( IllegalTokenException | IllegalUsernameException e ) + { + throw new ThisShouldNotHappenError( "Jake", "Token/username are not being modified.", e ); + } + } + else + { + throw new RuntimeException( "No such user: " + name ); + } + } + + private AuthenticationMetadata authMetadataFor( String name ) + { + if(name == null) + { + return unknownUser; + } + + AuthenticationMetadata authMeta = authenticationData.get( name ); + if(authMeta == null) + { + User user = users.get( name ); + if ( user != null ) + { + authMeta = new AuthenticationMetadata( name, maxFailedAttempts, failedAuthCooldownPeriod, clock ); + AuthenticationMetadata preExisting = authenticationData.putIfAbsent( name, authMeta ); + if(preExisting != null) + { + authMeta = preExisting; + } + } + else + { + authMeta = unknownUser; + } + } + return authMeta; + } + + private String randomSalt() + { + byte[] salt = new byte[16]; + rand.nextBytes( salt ); + return compactHex( salt ); + } + + private String hash( String salt, String password, String digestAlgo) + { + try + { + byte[] bytes = (salt+password).getBytes( Charsets.UTF_8 ); + MessageDigest m = MessageDigest.getInstance( digestAlgo ); + m.update( bytes, 0, bytes.length); + return compactHex( m.digest() ); + } + catch ( NoSuchAlgorithmException e ) + { + throw new RuntimeException( "Hash algorithm is not available on this platform: " + e.getMessage(),e ); + } + } +}
community/server/src/main/java/org/neo4j/server/security/auth/Credentials.java+94 −0 added@@ -0,0 +1,94 @@ +/** + * Copyright (c) 2002-2014 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package org.neo4j.server.security.auth; + +public class Credentials +{ + /* + Design note: Concurrent access to this class in common, and there are no access locks. Please do not add mutable + fields to this class. + */ + public static final Credentials INACCESSIBLE = new Credentials( "", "SHA-256", "" ); + + private final String salt; + private final String digestAlgo; + private final String hash; + + public Credentials( String salt, String digestAlgo, String hash ) + { + this.salt = salt; + this.digestAlgo = digestAlgo; + this.hash = hash; + } + + public String salt() + { + return salt; + } + + public String hash() + { + return hash; + } + + public String digestAlgorithm() + { + return digestAlgo; + } + + @Override + public boolean equals( Object o ) + { + if ( this == o ) + { + return true; + } + if ( o == null || getClass() != o.getClass() ) + { + return false; + } + + Credentials that = (Credentials) o; + + if ( digestAlgo != null ? !digestAlgo.equals( that.digestAlgo ) : that.digestAlgo != null ) + { + return false; + } + if ( hash != null ? !hash.equals( that.hash ) : that.hash != null ) + { + return false; + } + if ( salt != null ? !salt.equals( that.salt ) : that.salt != null ) + { + return false; + } + + return true; + } + + @Override + public int hashCode() + { + int result = salt != null ? salt.hashCode() : 0; + result = 31 * result + (digestAlgo != null ? digestAlgo.hashCode() : 0); + result = 31 * result + (hash != null ? hash.hashCode() : 0); + return result; + } +}
community/server/src/main/java/org/neo4j/server/security/auth/exception/IllegalTokenException.java+39 −0 added@@ -0,0 +1,39 @@ +/** + * Copyright (c) 2002-2014 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package org.neo4j.server.security.auth.exception; + +import org.neo4j.kernel.api.exceptions.Status; + +public class IllegalTokenException extends Exception implements Status.HasStatus +{ + private final Status status; + + public IllegalTokenException( String message ) + { + super(message); + this.status = Status.Request.Invalid; + } + + @Override + public Status status() + { + return status; + } +}
community/server/src/main/java/org/neo4j/server/security/auth/exception/IllegalUsernameException.java+39 −0 added@@ -0,0 +1,39 @@ +/** + * Copyright (c) 2002-2014 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package org.neo4j.server.security.auth.exception; + +import org.neo4j.kernel.api.exceptions.Status; + +public class IllegalUsernameException extends Exception implements Status.HasStatus +{ + private final Status status; + + public IllegalUsernameException( String message ) + { + super(message); + this.status = Status.Request.Invalid; + } + + @Override + public Status status() + { + return status; + } +}
community/server/src/main/java/org/neo4j/server/security/auth/exception/TooManyAuthenticationAttemptsException.java+39 −0 added@@ -0,0 +1,39 @@ +/** + * Copyright (c) 2002-2014 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package org.neo4j.server.security.auth.exception; + +import org.neo4j.kernel.api.exceptions.Status; + +public class TooManyAuthenticationAttemptsException extends Exception implements Status.HasStatus +{ + private final Status.Security status; + + public TooManyAuthenticationAttemptsException( String message ) + { + super(message); + this.status = Status.Security.AuthenticationRateLimit; + } + + @Override + public Status status() + { + return status; + } +}
community/server/src/main/java/org/neo4j/server/security/auth/FileUserRepository.java+212 −0 added@@ -0,0 +1,212 @@ +/** + * Copyright (c) 2002-2014 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package org.neo4j.server.security.auth; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.neo4j.io.fs.FileSystemAbstraction; +import org.neo4j.kernel.lifecycle.LifecycleAdapter; +import org.neo4j.server.security.auth.exception.IllegalTokenException; +import org.neo4j.server.security.auth.exception.IllegalUsernameException; + +/** + * Stores user auth data. In memory, but backed by persistent storage so changes to this repository will survive + * JVM restarts and crashes. + */ +public class FileUserRepository extends LifecycleAdapter implements UserRepository +{ + private final FileSystemAbstraction fs; + private final File dbFile; + + /** + * Used while writing to the dbfile, the whole file is first written to this file, so that we can recover + * if we crash. + */ + private final File tempFile; + + /** Quick lookup of users by name */ + private final Map<String, User> usersByName = new ConcurrentHashMap<>(); + + /** Master list of users */ + private volatile List<User> users = new ArrayList<>(); + + private final UserSerialization serialization = new UserSerialization(); + + public FileUserRepository( FileSystemAbstraction fs, File file ) + { + this.fs = fs; + this.dbFile = file; + this.tempFile = new File(file.getAbsolutePath() + ".tmp"); + } + + @Override + public User get( String name ) + { + return usersByName.get( name ); + } + + @Override + public void start() throws Throwable + { + if(fs.fileExists( dbFile )) + { + loadUsersFromFile(dbFile); + } + else if(fs.fileExists( tempFile )) + { + fs.renameFile( tempFile, dbFile ); + loadUsersFromFile( dbFile ); + fs.deleteFile( tempFile ); + } + } + + @Override + public synchronized void save( User user ) throws IllegalTokenException, IOException, IllegalUsernameException + { + // Assert input is ok + if(user.token() != User.NO_TOKEN && !isValidToken( user.token() )) + { + throw new IllegalTokenException( "Invalid token provided, cannot store user." ); + } + if(!isValidName(user.name())) + { + throw new IllegalUsernameException( "'" + user.name() + "' is not a valid user name." ); + } + + // Copy-on-write for the users list + List<User> newUsers = new ArrayList<>(users); + boolean replacedExisting = false; + for ( int i = 0; i < newUsers.size(); i++ ) + { + User other = newUsers.get( i ); + if( other.name().equals( user.name() )) + { + newUsers.set( i, user ); + replacedExisting = true; + } + else if ( user.token() != User.NO_TOKEN && other.tokenEquals( user.token() ) ) + { + throw new IllegalTokenException( "The specified token is already in use." ); + } + } + + if(!replacedExisting) + { + newUsers.add( user ); + } + + users = newUsers; + + commitToDisk(); + + usersByName.put( user.name(), user ); + } + + @Override + public int numberOfUsers() + { + return users.size(); + } + + @Override + public boolean isValidName( String name ) + { + return name.matches( "^[a-zA-Z0-9_]+$" ); + } + + @Override + public boolean isValidToken( String token ) + { + return token.matches( "^[a-fA-F0-9]+$" ); + } + + /* Assumes synchronization elsewhere */ + private void commitToDisk() throws IOException + { + writeUsersToFile( tempFile ); + writeUsersToFile( dbFile ); + fs.deleteFile( tempFile ); + } + + private void writeUsersToFile( File fileToWriteTo ) throws IOException + { + if(!fs.fileExists( fileToWriteTo.getParentFile() )) + { + fs.mkdirs( fileToWriteTo.getParentFile() ); + } + if(fs.fileExists( fileToWriteTo )) + { + fs.deleteFile( fileToWriteTo ); + } + fs.create( fileToWriteTo ); + try(OutputStream out = fs.openAsOutputStream( fileToWriteTo, false )) + { + out.write( serialization.serialize( users ) ); + out.flush(); + } + } + + private void loadUsersFromFile( File fileToLoadFrom ) throws IOException + { + if(fs.fileExists( fileToLoadFrom )) + { + List<User> loadedUsers; + try(InputStream in = fs.openAsInputStream( fileToLoadFrom )) + { + byte[] bytes = new byte[(int)fs.getFileSize( fileToLoadFrom )]; + int offset = 0; + while(offset < bytes.length) + { + int read = in.read( bytes, offset, bytes.length - offset ); + if(read == -1) break; + offset += read; + } + loadedUsers = serialization.deserializeUsers( bytes ); + } + + if(loadedUsers == null) + { + throw new IllegalStateException( "Failed to read authentication file: " + fileToLoadFrom.getAbsolutePath() ); + } + + users = loadedUsers; + for ( User user : users ) + { + usersByName.put( user.name(), user ); + } + } + } + + @Override + public Iterator<User> iterator() + { + return users.iterator(); + } + + +}
community/server/src/main/java/org/neo4j/server/security/auth/Privileges.java+52 −0 added@@ -0,0 +1,52 @@ +/** + * Copyright (c) 2002-2014 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package org.neo4j.server.security.auth; + +/** + * An initial version of basic user privileges. The design here may need to be revised as more advanced privileges + * are added. + */ +public interface Privileges +{ + /* Design note: This is just a shell, because we needed to differentiate between an authorized and an unauthorized + user. These privileges are not currently persisted anywhere, so if you go about introducing new special privs, + you need to also implement storage for privileges. + */ + + public static final Privileges ADMIN = new Privileges() + { + @Override + public boolean APIAccess() + { + return true; + } + }; + + public static final Privileges NONE = new Privileges() { + @Override + public boolean APIAccess() + { + return false; + } + }; + + /** This signals the user has access to the Data API and can execute read and write transactions against the database. */ + boolean APIAccess(); +}
community/server/src/main/java/org/neo4j/server/security/auth/SecurityCentral.java+215 −0 added@@ -0,0 +1,215 @@ +/** + * Copyright (c) 2002-2014 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package org.neo4j.server.security.auth; + +import java.io.IOException; +import java.security.SecureRandom; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.neo4j.helpers.Clock; +import org.neo4j.helpers.ThisShouldNotHappenError; +import org.neo4j.kernel.impl.util.BytePrinter; +import org.neo4j.kernel.lifecycle.LifecycleAdapter; +import org.neo4j.server.security.auth.exception.IllegalTokenException; +import org.neo4j.server.security.auth.exception.IllegalUsernameException; +import org.neo4j.server.security.auth.exception.TooManyAuthenticationAttemptsException; + +/** + * Manages server authentication and authorization. This is mainly a coordinating component, it delegates to other + * internal components for the majority of its work. + * + * Through the SecurityCentral you can create and edit users, check their credentials and their privileges. + */ +public class SecurityCentral extends LifecycleAdapter +{ + public static final User UNAUTHENTICATED = new UnauthenticatedUser(); + + // Cache to speed up token lookups + private final ConcurrentMap<String, User> usersByToken = new ConcurrentHashMap<>(); + + private final Authentication authentication; + private final UserRepository users; + private final SecureRandom rand = new SecureRandom(); + + public SecurityCentral( Clock clock, UserRepository users ) + { + this.users = users; + this.authentication = new Authentication( clock, users, 3 ); + } + + @Override + public void start() throws Throwable + { + if(users.numberOfUsers() == 0) + { + newUser( "neo4j", Privileges.ADMIN ); + } + } + + /** Determine if a set of credentials are valid. */ + public boolean authenticate( String user, String password ) throws TooManyAuthenticationAttemptsException + { + return authentication.authenticate( user, password ); + } + + public void newUser( String name, Privileges privileges ) throws IOException, IllegalUsernameException + { + try + { + assertValidName( name ); + users.save( new User.Builder() + .withName( name ) + .withPrivileges( privileges ) + .build() ); + // All users, by default, have their name as their password, usable only in order to set the password in a + // subsequent request. + authentication.setPassword( name, name ); + authentication.requirePasswordChange( name ); + } catch(IllegalTokenException e) + { + throw new ThisShouldNotHappenError( "Jake", "There is no token set at this point.", e ); + } + } + + /** + * Get a user, given a token. If no user is associated with the token, return an + * {@link org.neo4j.server.security.auth.UnauthenticatedUser unauthenticated} user. + */ + public User userForToken( String token ) + { + if(token == null ) + { + return UNAUTHENTICATED; + } + + User user = usersByToken.get( token ); + if( user != null) + { + return user; + } + + for ( User candidate : users ) + { + if( candidate.tokenEquals( token ) ) + { + usersByToken.putIfAbsent( candidate.token(), candidate ); + return candidate; + } + } + + return UNAUTHENTICATED; + } + + /** + * Get a user, given a name. If no user is associated with the name, return an + * {@link org.neo4j.server.security.auth.UnauthenticatedUser unauthenticated} user. + */ + public User userForName( String name ) + { + if(name == null) + { + return UNAUTHENTICATED; + } + + User user = users.get( name ); + if( user != null) + { + return user; + } + return UNAUTHENTICATED; + } + + /** Set a new random token for a given user */ + public String regenerateToken( String name ) throws IOException + { + try + { + String token = newToken(); + setToken( name, token ); + return token; + } + catch ( IllegalTokenException e ) + { + // This is technically a possibly infinite recursive loop. However, hand-wavedly, given a user regenerates + // her token a million times per second, this branch will execute once + // every 539 514 153 540 301 000 000 000 years. As such, it is unlikely this will loop forever. + return regenerateToken( name ); + } + } + + /** This is synchronized to avoid odd races if someone requests multiple concurrent token changes. */ + public synchronized void setToken( String name, String token ) throws IllegalTokenException, IOException + { + assertValidToken( token ); + User user = users.get( name ); + if(user != null) + { + String oldToken = user.token(); + user = user.augment().withToken( token ).build(); + try + { + users.save( user ); + } + catch ( IllegalUsernameException e ) + { + throw new ThisShouldNotHappenError( "Jake", "Username has already been accepted, we are modifying the token only." ); + } + usersByToken.put( token, user ); + + if(oldToken != User.NO_TOKEN) + { + usersByToken.remove( oldToken ); + } + } + } + + public synchronized void setPassword( String name, String password ) throws IOException + { + if(userForName( name ).passwordChangeRequired()) + { + regenerateToken( name ); + } + authentication.setPassword( name, password ); + } + + private String newToken() + { + byte[] tokenData = new byte[16]; + rand.nextBytes( tokenData ); + return BytePrinter.compactHex(tokenData).toLowerCase(); + } + + private void assertValidName( String name ) + { + if(!users.isValidName( name )) + { + throw new IllegalArgumentException( "User name contains illegal characters. Please use simple ascii characters and numbers." ); + } + } + + private void assertValidToken( String token ) + { + if(!users.isValidToken( token )) + { + throw new IllegalArgumentException( "Token is not a valid Neo4j Authorization Token." ); + } + } +}
community/server/src/main/java/org/neo4j/server/security/auth/UnauthenticatedUser.java+28 −0 added@@ -0,0 +1,28 @@ +/** + * Copyright (c) 2002-2014 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package org.neo4j.server.security.auth; + +public class UnauthenticatedUser extends User +{ + UnauthenticatedUser() + { + super( "Unauthenticated", Privileges.NONE ); + } +}
community/server/src/main/java/org/neo4j/server/security/auth/User.java+190 −0 added@@ -0,0 +1,190 @@ +/** + * Copyright (c) 2002-2014 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package org.neo4j.server.security.auth; + +/** + * Controls authorization and authentication for an individual user. + */ +public class User +{ + /* + Design note: These instances are shared across threads doing disparate things with them, and there are no access + locks. Correctness depends on write-time assertions and this class remaining immutable. Please do not introduce + mutable fields here. + */ + + public static final String NO_TOKEN = null; + + /** User name */ + private final String name; + + /** Currently valid user authorization token */ + private final String token; + + /** Privileges this user has */ + private final Privileges privileges; + + /** Authentication credentials used by the built in username/password authentication scheme */ + private final Credentials credentials; + + /** Whether a password change is needed */ + private final boolean passwordChangeRequired; + + public User( String name, Privileges privileges ) + { + this(name, null, privileges, Credentials.INACCESSIBLE, true ); + } + + public User(String name, String token, Privileges privileges, Credentials credentials, boolean passwordChangeRequired) + { + this.name = name; + this.token = token; + this.privileges = privileges; + this.credentials = credentials; + this.passwordChangeRequired = passwordChangeRequired; + } + + public String name() + { + return name; + } + + public String token() + { + return token; + } + + public boolean hasToken() + { + return token != NO_TOKEN; // Instance equality on purpose. + } + + public Privileges privileges() + { + return privileges; + } + + public Credentials credentials() + { + return credentials; + } + + public boolean passwordChangeRequired() { return passwordChangeRequired; } + + public boolean tokenEquals( String token ) + { + return (token == NO_TOKEN && this.token == NO_TOKEN) || (token != NO_TOKEN && token.equals( this.token )); + } + + /** Use this user as a base for a new user object */ + public Builder augment() { return new Builder(this); } + + @Override + public boolean equals( Object o ) + { + if ( this == o ) + { + return true; + } + if ( o == null || getClass() != o.getClass() ) + { + return false; + } + + User user = (User) o; + + if ( passwordChangeRequired != user.passwordChangeRequired ) + { + return false; + } + if ( credentials != null ? !credentials.equals( user.credentials ) : user.credentials != null ) + { + return false; + } + if ( name != null ? !name.equals( user.name ) : user.name != null ) + { + return false; + } + if ( privileges != null ? !privileges.equals( user.privileges ) : user.privileges != null ) + { + return false; + } + if ( token != null ? !token.equals( user.token ) : user.token != null ) + { + return false; + } + + return true; + } + + @Override + public int hashCode() + { + int result = name != null ? name.hashCode() : 0; + result = 31 * result + (token != null ? token.hashCode() : 0); + result = 31 * result + (privileges != null ? privileges.hashCode() : 0); + result = 31 * result + (credentials != null ? credentials.hashCode() : 0); + result = 31 * result + (passwordChangeRequired ? 1 : 0); + return result; + } + + @Override + public String toString() + { + return "User{" + + "name='" + name + '\'' + + ", token='" + token + '\'' + + ", privileges=" + privileges + + ", credentials=" + credentials + + ", passwordChangeRequired=" + passwordChangeRequired + + '}'; + } + + public static class Builder + { + private String name; + private String token = NO_TOKEN; + private Privileges privileges = Privileges.NONE; + private Credentials credentials = Credentials.INACCESSIBLE; + private boolean pwdChangeRequired; + + public Builder() { } + + public Builder( User base ) + { + name = base.name; + token = base.token; + privileges = base.privileges; + credentials = base.credentials; + pwdChangeRequired = base.passwordChangeRequired; + } + + public Builder withName( String name ) { this.name = name; return this; } + public Builder withToken( String token ) { this.token = token; return this; } + public Builder withPrivileges( Privileges privileges ) { this.privileges = privileges; return this; } + public Builder withCredentials( Credentials creds ) { this.credentials = creds; return this; } + public Builder withRequiredPasswordChange( boolean change ) { this.pwdChangeRequired = change; return this; } + + public User build() + { + return new User(name, token, privileges, credentials, pwdChangeRequired ); + } + } +}
community/server/src/main/java/org/neo4j/server/security/auth/UserRepository.java+44 −0 added@@ -0,0 +1,44 @@ +/** + * Copyright (c) 2002-2014 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package org.neo4j.server.security.auth; + +import java.io.IOException; + +import org.neo4j.server.security.auth.exception.IllegalTokenException; +import org.neo4j.server.security.auth.exception.IllegalUsernameException; + +/** + * A component that can store and retrieve users. Implementations must be thread safe. + */ +public interface UserRepository extends Iterable<User> +{ + public User get( String name ); + + /** Saves a user, given that the users token is unique. */ + public void save( User user ) throws IllegalTokenException, IOException, IllegalUsernameException; + + int numberOfUsers(); + + /** Utility for API consumers to tell if #save() will accept a given username */ + boolean isValidName( String name ); + + /** Utility for API consumers to tell if #save() will accept a given token */ + boolean isValidToken( String token ); +}
community/server/src/main/java/org/neo4j/server/security/auth/UserSerialization.java+109 −0 added@@ -0,0 +1,109 @@ +/** + * Copyright (c) 2002-2014 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package org.neo4j.server.security.auth; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.neo4j.kernel.impl.util.Charsets; + +/** + * Serializes user authorization and authentication data to a format similar to unix passwd files. + */ +public class UserSerialization +{ + private static final String userSeparator = ":"; + private static final String credentialSeparator = ","; + + public byte[] serialize(Collection<User> users) + { + StringBuilder sb = new StringBuilder(); + for ( User user : users ) + { + sb.append( serialize(user) ).append( "\n" ); + } + return sb.toString().getBytes( Charsets.UTF_8 ); + } + + public List<User> deserializeUsers( byte[] bytes ) + { + List<User> out = new ArrayList<>(); + for ( String line : new String( bytes, Charsets.UTF_8 ).split( "\n" ) ) + { + if(line.trim().length() > 0) + { + out.add( deserializeUser( line ) ); + } + } + return out; + } + + private String serialize( User user ) + { + return join( userSeparator, new String[]{ + user.name(), + user.token(), + serialize( user.credentials() ), + user.passwordChangeRequired() ? "true" : "false"} ); + } + + private User deserializeUser( String line ) + { + String[] parts = line.split( userSeparator, -1 ); + if(parts.length != 4) + { + throw new IllegalStateException( "Cannot read user data from authorization file." ); + } + return new User.Builder() + .withName( parts[0] ) + .withToken( parts[1] ) + .withCredentials( deserializeCredentials(parts[2]) ) + .withRequiredPasswordChange( parts[3].equals( "true" ) ) + .withPrivileges( Privileges.ADMIN ) // Only "real" privilege available right now + .build(); + } + + private String serialize( Credentials cred ) + { + return join( credentialSeparator, new String[]{cred.digestAlgorithm(), cred.hash(), cred.salt()} ); + } + + private Credentials deserializeCredentials( String part ) + { + String[] split = part.split( credentialSeparator, -1 ); + if(split.length != 3) + { + throw new IllegalStateException( "Cannot read credential data from authorization file: " + part + " " + split.length + " '" + join(":", split) + "'"); + } + return new Credentials( split[2], split[0], split[1] ); + } + + private String join( String separator, String[] segments ) + { + StringBuilder sb = new StringBuilder(); + for ( int i = 0; i < segments.length; i++ ) + { + if(i > 0) { sb.append( separator ); } + sb.append( segments[i] ); + } + return sb.toString(); + } +}
community/server/src/main/java/org/neo4j/server/security/ssl/KeyStoreFactory.java+1 −1 renamed@@ -17,7 +17,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package org.neo4j.server.security; +package org.neo4j.server.security.ssl; import java.io.File; import java.io.FileOutputStream;
community/server/src/main/java/org/neo4j/server/security/ssl/KeyStoreInformation.java+1 −1 renamed@@ -17,7 +17,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package org.neo4j.server.security; +package org.neo4j.server.security.ssl; public class KeyStoreInformation {
community/server/src/main/java/org/neo4j/server/security/ssl/SslCertificateFactory.java+1 −1 renamed@@ -17,7 +17,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package org.neo4j.server.security; +package org.neo4j.server.security.ssl; import java.io.DataInputStream; import java.io.File;
community/server/src/main/java/org/neo4j/server/security/ssl/SslSocketConnectorFactory.java+1 −1 renamed@@ -17,7 +17,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package org.neo4j.server.security; +package org.neo4j.server.security.ssl; import org.eclipse.jetty.http.HttpScheme; import org.eclipse.jetty.http.HttpVersion;
community/server/src/main/java/org/neo4j/server/web/Jetty9WebServer.java+2 −2 modified@@ -64,8 +64,8 @@ import org.neo4j.kernel.logging.Logging; import org.neo4j.server.database.InjectableProvider; import org.neo4j.server.plugins.Injectable; -import org.neo4j.server.security.KeyStoreInformation; -import org.neo4j.server.security.SslSocketConnectorFactory; +import org.neo4j.server.security.ssl.KeyStoreInformation; +import org.neo4j.server.security.ssl.SslSocketConnectorFactory; import static java.lang.String.format;
community/server/src/main/java/org/neo4j/server/web/ServerInternalSettings.java+4 −0 modified@@ -19,6 +19,7 @@ */ package org.neo4j.server.web; +import java.io.File; import java.net.URI; import org.neo4j.graphdb.config.Setting; @@ -27,6 +28,7 @@ import static org.neo4j.helpers.Settings.DURATION; import static org.neo4j.helpers.Settings.FALSE; import static org.neo4j.helpers.Settings.NORMALIZED_RELATIVE_URI; +import static org.neo4j.helpers.Settings.PATH; import static org.neo4j.helpers.Settings.TRUE; import static org.neo4j.helpers.Settings.setting; @@ -72,4 +74,6 @@ public class ServerInternalSettings public static final Setting<Long> startup_timeout = setting( "org.neo4j.server.startup_timeout", DURATION, "120s" ); + public static final Setting<File> authorization_store = setting("dbms.security.authorization_location", PATH, "data/dbms/authorization"); + }
community/server/src/main/java/org/neo4j/server/web/WebServer.java+1 −1 modified@@ -30,7 +30,7 @@ import org.neo4j.server.database.InjectableProvider; import org.neo4j.server.plugins.Injectable; -import org.neo4j.server.security.KeyStoreInformation; +import org.neo4j.server.security.ssl.KeyStoreInformation; public interface WebServer {
community/server/src/test/java/org/neo4j/server/helpers/CommunityServerBuilder.java+2 −1 modified@@ -56,7 +56,6 @@ import static java.lang.Boolean.FALSE; import static java.lang.Boolean.TRUE; - import static org.neo4j.helpers.Clock.SYSTEM_CLOCK; import static org.neo4j.server.ServerTestUtils.asOneLine; import static org.neo4j.server.ServerTestUtils.createTempPropertyFile; @@ -217,6 +216,8 @@ private void createPropertiesFile( File temporaryConfigFile ) } } + properties.put( ServerInternalSettings.authorization_store.name(), "neo4j-home/data/dbms/authorization" ); + for ( Object key : arbitraryProperties.keySet() ) { properties.put( String.valueOf( key ), String.valueOf( arbitraryProperties.get( key ) ) );
community/server/src/test/java/org/neo4j/server/modules/DBMSModuleTest.java+6 −6 renamed@@ -24,19 +24,19 @@ import org.junit.Rule; import org.junit.Test; -import org.neo4j.kernel.logging.SystemOutLogging; +import org.neo4j.kernel.configuration.Config; import org.neo4j.server.CommunityNeoServer; +import org.neo4j.server.configuration.ServerSettings; import org.neo4j.server.web.WebServer; import org.neo4j.test.Mute; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyCollection; import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; +import static org.neo4j.helpers.collection.MapUtil.stringMap; -public class DiscoveryModuleTest +public class DBMSModuleTest { @Rule public Mute mute = Mute.mute( Mute.System.err, Mute.System.out ); @@ -51,7 +51,7 @@ public void shouldRegisterAtRootByDefault() throws Exception when( neoServer.baseUri() ).thenReturn( new URI( "http://localhost:7575" ) ); when( neoServer.getWebServer() ).thenReturn( webServer ); - DiscoveryModule module = new DiscoveryModule(webServer, new SystemOutLogging() ); + DBMSModule module = new DBMSModule(webServer, null, new Config(stringMap( ServerSettings.authorization_enabled.name(), "false" )) ); module.start();
community/server/src/test/java/org/neo4j/server/modules/ManagementApiModuleTest.java+1 −2 modified@@ -27,7 +27,6 @@ import org.junit.Test; import org.neo4j.kernel.configuration.Config; -import org.neo4j.kernel.logging.DevNullLoggingService; import org.neo4j.server.CommunityNeoServer; import org.neo4j.server.configuration.Configurator; import org.neo4j.server.web.WebServer; @@ -57,7 +56,7 @@ public void shouldRegisterASingleUri() throws Exception when( neoServer.getConfig() ).thenReturn( config ); - ManagementApiModule module = new ManagementApiModule(webServer, config, DevNullLoggingService.DEV_NULL); + ManagementApiModule module = new ManagementApiModule(webServer, config ); module.start(); verify( webServer ).addJAXRSClasses( any( List.class ), anyString(), anyCollection() );
community/server/src/test/java/org/neo4j/server/rest/CypherDocIT.java+1 −1 modified@@ -391,7 +391,7 @@ public void send_queries_with_errors() throws Exception { String response = cypherRestCall( script, Status.BAD_REQUEST, Pair.of( "startName", "I" ), Pair.of( "name", "you" ) ); Map<String, Object> responseMap = jsonToMap( response ); - assertEquals( 4, responseMap.size() ); + assertEquals( 5, responseMap.size() ); assertThat( response, containsString( "message" ) ); assertThat( ((String) responseMap.get( "message" )), containsString( "frien not defined" ) ); }
community/server/src/test/java/org/neo4j/server/rest/dbms/AuthenticateHeadersTest.java+63 −0 added@@ -0,0 +1,63 @@ +/** + * Copyright (c) 2002-2014 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package org.neo4j.server.rest.dbms; + +import java.nio.charset.Charset; + +import com.sun.jersey.core.util.Base64; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.neo4j.server.rest.dbms.AuthenticateHeaders.extractToken; + +public class AuthenticateHeadersTest +{ + @Test + public void shouldParseHappyPath() throws Exception + { + // Given + String token = "12345"; + String header = "Basic realm=\"Neo4j\" " + base64(":" + token); + + // When + String parsed = extractToken( header ); + + // Then + assertEquals(token, parsed); + } + + @Test + public void shouldHandleSadPaths() throws Exception + { + // When & then + assertEquals("", extractToken( "" )); + assertEquals("", extractToken( null )); + assertEquals("", extractToken( "Basic" )); + assertEquals("", extractToken( "Basic realm=\"Neo4j\" not valid value" )); + assertEquals("", extractToken( "Basic realm=\"Neo4j\" " + base64("") )); + assertEquals("", extractToken( "Basic realm=\"Neo4j\" " + base64(":") )); + } + + private String base64(String value) + { + return new String( Base64.encode( value ), Charset + .forName( "UTF-8" )); + } +}
community/server/src/test/java/org/neo4j/server/rest/discovery/DiscoveryServiceTest.java+14 −14 modified@@ -25,20 +25,21 @@ import org.junit.Ignore; import org.junit.Test; - +import org.neo4j.helpers.Clock; import org.neo4j.kernel.configuration.Config; +import org.neo4j.server.configuration.ServerSettings; import org.neo4j.server.rest.repr.formats.JsonFormat; +import org.neo4j.server.security.auth.InMemoryUserRepository; +import org.neo4j.server.security.auth.SecurityCentral; import org.neo4j.server.web.ServerInternalSettings; import org.neo4j.test.server.EntityOutputFormat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; public class DiscoveryServiceTest { @@ -47,16 +48,15 @@ public void shouldReturnValidJSONWithDataAndManagementUris() throws Exception { Config mockConfig = mock( Config.class ); URI managementUri = new URI( "/management" ); - when( - mockConfig.get( ServerInternalSettings.management_api_path ) ).thenReturn( managementUri ); + when( mockConfig.get( ServerInternalSettings.management_api_path ) ).thenReturn( managementUri ); URI dataUri = new URI( "/data" ); - when( mockConfig.get( ServerInternalSettings.rest_api_path ) ).thenReturn( - dataUri ); + when( mockConfig.get( ServerInternalSettings.rest_api_path ) ).thenReturn( dataUri ); + when(mockConfig.get( ServerSettings.authorization_enabled )).thenReturn( false ); String baseUri = "http://www.example.com"; DiscoveryService ds = new DiscoveryService( mockConfig, new EntityOutputFormat( new JsonFormat(), new URI( - baseUri ), null ) ); - Response response = ds.getDiscoveryDocument(); + baseUri ), null ), new SecurityCentral( Clock.SYSTEM_CLOCK, new InMemoryUserRepository() )); + Response response = ds.getDiscoveryDocument(""); String json = new String( (byte[]) response.getEntity() ); @@ -84,8 +84,8 @@ public void shouldReturnConfiguredUrlIfConfigIsAbsolute() throws Exception String baseUri = "http://www.example.com"; DiscoveryService ds = new DiscoveryService( mockConfig, new EntityOutputFormat( new JsonFormat(), new URI( - baseUri ), null ) ); - Response response = ds.getDiscoveryDocument(); + baseUri ), null ), new SecurityCentral( Clock.SYSTEM_CLOCK, new InMemoryUserRepository() )); + Response response = ds.getDiscoveryDocument(""); String json = new String( (byte[]) response.getEntity() ); @@ -109,7 +109,7 @@ public void shouldReturnRedirectToAbsoluteAPIUsingOutputFormat() throws Exceptio String baseUri = "http://www.example.com:5435"; DiscoveryService ds = new DiscoveryService( mockConfig, new EntityOutputFormat( new JsonFormat(), new URI( - baseUri ), null ) ); + baseUri ), null ), new SecurityCentral( Clock.SYSTEM_CLOCK, new InMemoryUserRepository() )); Response response = ds.redirectToBrowser();
community/server/src/test/java/org/neo4j/server/rest/repr/ExceptionRepresentationTest.java+96 −0 added@@ -0,0 +1,96 @@ +/** + * Copyright (c) 2002-2014 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package org.neo4j.server.rest.repr; + +import java.net.URI; +import java.util.HashMap; +import java.util.Map; + +import org.codehaus.jackson.JsonNode; +import org.junit.Test; +import org.neo4j.kernel.api.exceptions.KernelException; +import org.neo4j.server.rest.domain.JsonHelper; +import org.neo4j.server.rest.domain.JsonParseException; +import org.neo4j.server.rest.repr.formats.MapWrappingWriter; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; +import static org.neo4j.kernel.api.exceptions.Status.General.UnknownFailure; + +public class ExceptionRepresentationTest +{ + + @Test + public void shouldIncludeCause() throws Exception + { + // Given + ExceptionRepresentation rep = new ExceptionRepresentation( + new RuntimeException("Hoho", new RuntimeException("Haha", new RuntimeException( "HAHA!" )) )); + + // When + JsonNode out = serialize( rep ); + + // Then + assertThat( out.get("cause").get("message").asText(), is( "Haha" ) ); + assertThat( out.get( "cause" ).get("cause").get("message").asText(), is( "HAHA!") ); + } + + @Test + public void shouldRenderErrorsWithNeo4jStatusCode() throws Exception + { + // Given + ExceptionRepresentation rep = new ExceptionRepresentation( new KernelException( UnknownFailure, "Hello" ) { }); + + // When + JsonNode out = serialize( rep ); + + // Then + assertThat(out.get("errors").get(0).get("code").asText(), equalTo("Neo.DatabaseError.General.UnknownFailure")); + assertThat(out.get("errors").get(0).get("message").asText(), equalTo("Hello")); + } + + @Test + public void shoudExcludeLegacyFormatIfAsked() throws Exception + { + // Given + ExceptionRepresentation rep = new ExceptionRepresentation( new KernelException( UnknownFailure, "Hello" ) { }, /*legacy*/false); + + // When + JsonNode out = serialize( rep ); + + // Then + assertThat(out.get("errors").get(0).get("code").asText(), equalTo("Neo.DatabaseError.General.UnknownFailure")); + assertThat(out.get("errors").get(0).get("message").asText(), equalTo("Hello")); + assertThat(out.has( "message" ), equalTo(false)); + } + + private JsonNode serialize( ExceptionRepresentation rep ) throws JsonParseException + { + Map<String, Object> output = new HashMap<>(); + MappingSerializer serializer = new MappingSerializer( new MapWrappingWriter(output), URI.create( "" ), + mock(ExtensionInjector.class ) ); + + // When + rep.serialize( serializer ); + return JsonHelper.jsonNode( JsonHelper.createJsonFrom( output ) ); + } +}
community/server/src/test/java/org/neo4j/server/rest/repr/TestExceptionRepresentation.java+0 −66 removed@@ -1,66 +0,0 @@ -/** - * Copyright (c) 2002-2014 "Neo Technology," - * Network Engine for Objects in Lund AB [http://neotechnology.com] - * - * This file is part of Neo4j. - * - * Neo4j is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ -package org.neo4j.server.rest.repr; - -import static org.hamcrest.CoreMatchers.instanceOf; -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; -import static org.mockito.Mockito.mock; - -import java.net.URI; -import java.util.HashMap; -import java.util.Map; - -import org.junit.Test; -import org.neo4j.server.rest.repr.formats.MapWrappingWriter; - -public class TestExceptionRepresentation -{ - - @Test - public void shouldIncludeCause() throws Exception - { - // Given - ExceptionRepresentation rep = new ExceptionRepresentation( - new RuntimeException("Hoho", - new RuntimeException("Haha", - new RuntimeException( "HAHA!" )) )); - - Map<String, Object> output = new HashMap<String, Object>(); - MappingSerializer serializer = new MappingSerializer( new MapWrappingWriter(output), URI.create( "" ), - mock(ExtensionInjector.class ) ); - - // When - rep.serialize( serializer ); - - // Then - assertThat(output.containsKey( "cause" ), is( true )); - assertThat( output.get( "cause" ), is( instanceOf( Map.class ) ) ); - assertThat( (String) ((Map<String,Object>)output.get( "cause" )).get("message"), is( "Haha" ) ); - assertThat( - ( (Map<String, Object>) output.get( "cause" ) ).get( "cause" ), - is( instanceOf( Map.class ) ) ); - assertThat( (String) ((Map<String,Object>)((Map<String,Object>)output.get( "cause" )).get("cause")).get( "message" ), - is( "HAHA!") ); - } - - - -}
community/server/src/test/java/org/neo4j/server/rest/RESTDocsGenerator.java+41 −17 modified@@ -36,20 +36,20 @@ import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; +import com.sun.jersey.api.client.Client; +import com.sun.jersey.api.client.ClientRequest; +import com.sun.jersey.api.client.ClientRequest.Builder; +import com.sun.jersey.api.client.ClientResponse; import org.neo4j.graphdb.Transaction; +import org.neo4j.helpers.Pair; +import org.neo4j.helpers.Predicate; +import org.neo4j.helpers.Predicates; import org.neo4j.test.AsciiDocGenerator; import org.neo4j.test.GraphDefinition; import org.neo4j.test.TestData.Producer; import org.neo4j.visualization.asciidoc.AsciidocHelper; -import com.sun.jersey.api.client.Client; -import com.sun.jersey.api.client.ClientRequest; -import com.sun.jersey.api.client.ClientRequest.Builder; -import com.sun.jersey.api.client.ClientResponse; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; +import static org.junit.Assert.*; /** * Generate asciidoc-formatted documentation from HTTP requests and responses. @@ -91,7 +91,7 @@ public void destroy( RESTDocsGenerator product, boolean successful ) private int expectedResponseStatus = -1; private MediaType expectedMediaType = MediaType.valueOf( "application/json; charset=UTF-8" ); private MediaType payloadMediaType = MediaType.APPLICATION_JSON_TYPE; - private final List<String> expectedHeaderFields = new ArrayList<>(); + private final List<Pair<String, Predicate<String>>> expectedHeaderFields = new ArrayList<>(); private String payload; private final Map<String, String> addedRequestHeaders = new TreeMap<>( ); private boolean noDoc = false; @@ -230,7 +230,21 @@ public RESTDocsGenerator docHeadingLevel( final int headingLevel ) */ public RESTDocsGenerator expectedHeader( final String expectedHeaderField ) { - this.expectedHeaderFields.add( expectedHeaderField ); + this.expectedHeaderFields.add( Pair.of(expectedHeaderField, Predicates.<String>notNull()) ); + return this; + } + + /** + * Add an expected response header. If the heading is missing in the + * response the test will fail. The header and its value are also included + * in the documentation. + * + * @param expectedHeaderField the expected header + * @param expectedValue the expected header value + */ + public RESTDocsGenerator expectedHeader( final String expectedHeaderField, String expectedValue ) + { + this.expectedHeaderFields.add( Pair.of(expectedHeaderField, Predicates.equalTo( expectedValue )) ); return this; } @@ -300,7 +314,7 @@ public ResponseEntity delete( final String uri ) */ private ResponseEntity retrieveResponseFromRequest( final String title, final String description, final String method, final String uri, final int responseCode, final MediaType accept, - final List<String> headerFields ) + final List<Pair<String,Predicate<String>>> headerFields ) { ClientRequest request; try @@ -321,7 +335,7 @@ private ResponseEntity retrieveResponseFromRequest( final String title, final St */ private ResponseEntity retrieveResponseFromRequest( final String title, final String description, final String method, final String uri, final String payload, final MediaType payloadType, - final int responseCode, final MediaType accept, final List<String> headerFields ) + final int responseCode, final MediaType accept, final List<Pair<String,Predicate<String>>> headerFields ) { ClientRequest request; try @@ -358,7 +372,7 @@ private <T extends Builder> T withHeaders(T builder) { * Send the request and create the documentation. */ private ResponseEntity retrieveResponse( final String title, final String description, final String uri, - final int responseCode, final MediaType type, final List<String> headerFields, final ClientRequest request ) + final int responseCode, final MediaType type, final List<Pair<String,Predicate<String>>> headerFields, final ClientRequest request ) { DocumentationData data = new DocumentationData(); getRequestHeaders( data, request.getHeaders() ); @@ -392,10 +406,10 @@ private ResponseEntity retrieveResponse( final String title, final String descri { assertTrue( "wrong response type: "+ data.entity, response.getType().isCompatible( type ) ); } - for ( String headerField : headerFields ) + for ( Pair<String,Predicate<String>> headerField : headerFields ) { - assertNotNull( "wrong headers: "+ data.entity, response.getHeaders() - .get( headerField ) ); + assertTrue( "wrong headers: " + response.getHeaders(), headerField.other().accept( response.getHeaders() + .getFirst( headerField.first() ) ) ); } if ( noDoc ) { @@ -407,7 +421,7 @@ private ResponseEntity retrieveResponse( final String title, final String descri data.setUri( uri ); data.setStatus( responseCode ); assertEquals( "Wrong response status. response: " + data.entity, responseCode, response.getStatus() ); - getResponseHeaders( data, response.getHeaders(), headerFields ); + getResponseHeaders( data, response.getHeaders(), headerNames(headerFields) ); if ( graph == null ) { document( data ); @@ -422,6 +436,16 @@ private ResponseEntity retrieveResponse( final String title, final String descri return new ResponseEntity( response, data.entity ); } + private List<String> headerNames( List<Pair<String, Predicate<String>>> headerPredicates ) + { + List<String> names = new ArrayList<>(); + for ( Pair<String, Predicate<String>> headerPredicate : headerPredicates ) + { + names.add( headerPredicate.first() ); + } + return names; + } + private void getResponseHeaders( final DocumentationData data, final MultivaluedMap<String, String> headers, final List<String> additionalFilter ) {
community/server/src/test/java/org/neo4j/server/rest/security/AuthenticationDocIT.java+454 −0 added@@ -0,0 +1,454 @@ +/** + * Copyright (c) 2002-2014 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package org.neo4j.server.rest.security; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.Charset; + +import javax.ws.rs.core.HttpHeaders; + +import com.sun.jersey.core.util.Base64; +import org.codehaus.jackson.JsonNode; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.neo4j.kernel.impl.annotations.Documented; +import org.neo4j.server.CommunityNeoServer; +import org.neo4j.server.configuration.ServerSettings; +import org.neo4j.server.helpers.CommunityServerBuilder; +import org.neo4j.server.rest.RESTDocsGenerator; +import org.neo4j.server.rest.domain.JsonHelper; +import org.neo4j.server.rest.domain.JsonParseException; +import org.neo4j.server.rest.web.PropertyValueException; +import org.neo4j.test.TestData; +import org.neo4j.test.server.ExclusiveServerTestBase; +import org.neo4j.test.server.HTTP; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.Matchers.greaterThan; +import static org.junit.Assert.*; +import static org.neo4j.test.server.HTTP.RawPayload; + +public class AuthenticationDocIT extends ExclusiveServerTestBase +{ + public @Rule TestData<RESTDocsGenerator> gen = TestData.producedThrough( RESTDocsGenerator.PRODUCER ); + private CommunityNeoServer server; + + @Before + public void setUp() + { + gen.get().setSection( "dev/rest-api" ); + } + + /** + * Required password changes + * + * In some cases, like the very first time you access Neo4j with authorization enabled, you are required to choose + * a new password. The database will signal that a new password is required when you try to authenticate. + * + * See <<rest-api-changing-your-password>> for how to set a new password. + */ + @Test + @Documented + public void password_change_required() throws PropertyValueException, IOException + { + // Given + startServer( true ); + + // Document + RESTDocsGenerator.ResponseEntity response = gen.get() + .noGraph() + .expectedStatus( 200 ) + .payload( quotedJson( "{'username':'neo4j', 'password':'neo4j'}" ) ) + .post( authURL() ); + + // Then + JsonNode data = JsonHelper.jsonNode( response.entity() ); + assertThat(data.get("username").asText(), equalTo("neo4j")); + assertThat(data.has("authorization_token" ), is( false )); + assertThat(data.has("authorization_token_change" ), is( false )); + assertThat(data.get("password_change_required").asBoolean(), equalTo( true )); + assertThat(data.get("password_change").asText(), equalTo( server.baseUri().resolve("user/neo4j/password").toString() )); + } + + /** + * Authenticate to obtain authorization token + * + * You authenticate by sending a username and a password to Neo4j. The database will reply with an authorization + * token. The reply from this endpoint will also indicate if your password should be changed which will, + * for instance, be the case in a newly installed instance. + */ + @Test + @Documented + public void successful_authentication() throws PropertyValueException, IOException + { + // Given + startServerWithConfiguredUser(); + + // Document + RESTDocsGenerator.ResponseEntity response = gen.get() + .noGraph() + .expectedStatus( 200 ) + .payload( quotedJson( "{'username':'neo4j', 'password':'secret'}" ) ) + .post( authURL() ); + + // Then + JsonNode data = JsonHelper.jsonNode( response.entity() ); + assertThat(data.get("username").asText(), equalTo("neo4j")); + assertThat(data.get("password_change_required").asBoolean(), equalTo( false )); + assertThat(data.get("authorization_token").asText().length(), greaterThan(0)); + assertThat(data.get("authorization_token_change").asText(), equalTo( server.baseUri().resolve("user/neo4j/authorization_token").toString() )); + } + + /** + * Using the Authorization Token + * + * Given that you have acquired an authorization token, you may use it to get access to the rest of the API. + * To include the token in requests to the server, it should be encoded as the 'password' part of the HTTP Basic Auth scheme. + * This means you should include a +Authorization+ header, with a value of +Basic realm="Neo4j" <token payload>+ + * where "token payload" is a base64 encoded string of the token prepended by a colon. + * + * In pseudo-code: + * + * [source,javascript] + * ---- + * authorization = 'Basic realm="Neo4j" ' + base64( ':' + token ); + * ---- + */ + @Test + @Documented + public void using_the_token() throws PropertyValueException, IOException + { + // Given + startServerWithConfiguredUser(); + String token = HTTP.POST( authURL(), RawPayload.quotedJson( "{'username':'neo4j','password':'secret'}" ) ) + .get( "authorization_token" ).asText(); + + // Document + gen.get() + .noGraph() + .expectedStatus( 200 ) + .withHeader( HttpHeaders.AUTHORIZATION, challengeResponse( token ) ) + .get( server.baseUri().resolve( "" ).toString() ); + } + + /** + * Incorrect username or password + * + * If you provide incorrect authentication credentials, the server replies with a an error. + */ + @Test + @Documented + public void incorrect_authentication() throws PropertyValueException, IOException + { + // Given + startServerWithConfiguredUser(); + + // Document + RESTDocsGenerator.ResponseEntity response = gen.get() + .noGraph() + .expectedStatus( 422 ) + .payload( quotedJson( "{'username':'bob', 'password':'incorrect'}" ) ) + .post( authURL() ); + + // Then + JsonNode data = JsonHelper.jsonNode( response.entity() ); + assertThat(data.get("errors").get(0).get("code").asText(), equalTo("Neo.ClientError.Security.AuthenticationFailed")); + assertThat(data.get("errors").get(0).get( "message" ).asText(), equalTo("Invalid username and/or password.")); + } + + /** + * Get current authorization status + * + * You can use this endpoint to determine if security is enabled, and to check if your authorization token is valid. + * + * Given that you have a valid authorization token, you can retrieve metadata about the current user from the authentication endpoint. + * If neo4j security is disabled, this endpoint will also return 200 OK, see <<rest-api-get-authorization-status-when-auth-is-disabled>>. + * If security is enabled and your token is invalid, you will get an error reply, see <<rest-api-attempting-to-get-authorization-status-while-unauthorized>>. + * This way, you can use this endpoint to determine if you need to acquire an authorization token. + */ + @Test + @Documented + public void authorization_metadata() throws PropertyValueException, IOException + { + // Given + startServerWithConfiguredUser(); + String authToken = HTTP.POST( authURL(), RawPayload.quotedJson( "{'username':'neo4j','password':'secret'}" ) ) + .get( "authorization_token" ).asText(); + + // Document + RESTDocsGenerator.ResponseEntity response = gen.get() + .noGraph() + .expectedStatus( 200 ) + .withHeader( HttpHeaders.AUTHORIZATION, challengeResponse( authToken ) ) + .get( authURL() ); + + // Then + JsonNode data = JsonHelper.jsonNode( response.entity() ); + assertThat(data.get( "username" ).asText(), equalTo("neo4j")); + } + + /** + * Attempting to get authorization status while unauthorized + * + * Given that you have an invalid authorization token, or no token at all, asking for authorization status leads + * to an unauthorized HTTP reply. + */ + @Test + @Documented + public void disallowed_authorization_metadata() throws PropertyValueException, IOException + { + // Given + startServerWithConfiguredUser(); + + // Document + RESTDocsGenerator.ResponseEntity response = gen.get() + .noGraph() + .expectedStatus( 401 ) + .expectedHeader( "WWW-Authenticate", "None" ) + .withHeader( HttpHeaders.AUTHORIZATION, "Basic realm=\"Neo4j\" " + base64( ":helloworld" ) ) + .get( authURL() ); + + // Then + JsonNode data = JsonHelper.jsonNode( response.entity() ); + assertThat(data.get("errors").get(0).get("code").asText(), equalTo("Neo.ClientError.Security.AuthorizationFailed")); + assertThat( data.get( "errors" ).get( 0 ).get( "message" ).asText(), equalTo( "Invalid authorization token supplied." ) ); + } + + /** + * Get authorization status when auth is disabled + * + * Given that auth is disabled in the configuration, you can perform a GET to the authentication endpoint, and will + * get back an OK response. You will not receive a username or authorization token. + */ + @Test + @Documented + public void auth_disabled_get_metadata() throws PropertyValueException, IOException + { + // Given + startServer(false); + + // Document + gen.get() + .noGraph() + .expectedStatus( 200 ) + .get( authURL() ); + } + + @Test + public void shouldSayTokenMissingIfNoTokenProvided() throws Exception + { + // Given + startServerWithConfiguredUser(); + + // When + HTTP.Response response = HTTP.GET( authURL() ); + + // Then + assertThat(response.status(), equalTo(401)); + assertThat(response.get("errors").get(0).get("code").asText(), equalTo("Neo.ClientError.Security.AuthorizationFailed")); + assertThat(response.get("errors").get(0).get("message").asText(), equalTo("No authorization token supplied.")); + assertThat(response.header("WWW-Authenticate"), equalTo("None")); + } + + @Test + public void shouldSayMalformedTokenIfMalformedToken() throws Exception + { + // Given + startServerWithConfiguredUser(); + + // When + HTTP.Response response = HTTP.withHeaders( HttpHeaders.AUTHORIZATION, "This makes no sense" ).GET( authURL() ); + + // Then + assertThat(response.status(), equalTo(400)); + assertThat(response.get("errors").get(0).get("code").asText(), equalTo("Neo.ClientError.Request.InvalidFormat")); + assertThat(response.get("errors").get(0).get("message").asText(), equalTo("Invalid Authorization header.")); + } + + @Test + public void shouldHandleMissingParameters() throws Exception + { + // Given + startServerWithConfiguredUser(); + + // When & Then + assertEquals( 422, HTTP.POST( authURL() ).status() ); + assertEquals( 422, HTTP.POST( authURL(), RawPayload.quotedJson("{'password':'whatever'}") ).status() ); + assertEquals( 422, HTTP.POST( authURL(), RawPayload.quotedJson("{'password':1234, 'username':{}}") ).status() ); + } + + @Test + public void shouldNotAllowDataAccess() throws Exception + { + // Given + startServerWithConfiguredUser(); + + // When & then + assertAuthorizationRequired( "POST", "db/data/node", RawPayload.quotedJson( "{'name':'jake'}" ), 201 ); + assertAuthorizationRequired( "GET", "db/data/node/1234", 404 ); + assertAuthorizationRequired( "POST", "db/data/transaction/commit", RawPayload.quotedJson( + "{'statements':[{'statement':'MATCH (n) RETURN n'}]}" ), 200 ); + assertEquals(200, HTTP.GET( server.baseUri().resolve( "webadmin" ).toString() ).status()); + assertEquals(200, HTTP.GET( server.baseUri().resolve( "browser" ).toString() ).status()); + assertEquals(200, HTTP.GET( server.baseUri().resolve( "" ).toString() ).status() ); + } + + @Test + public void rootEndpointShouldOnlyShowAuthenticationDiscoverabilityUrl() throws Exception + { + // Given + startServerWithConfiguredUser(); + + // When + HTTP.Response res = HTTP.GET( server.baseUri().resolve( "" ).toString() ); + + // Then + assertThat( res.rawContent(), equalTo( + "{\n" + + " \"authentication\" : \""+server.baseUri().resolve( "authentication" )+"\"\n" + + "}" )); + } + + @Test + public void shouldAllowAllAccessIfAuthenticationIsDisabled() throws Exception + { + // Given + startServer( false ); + + // When & then + assertEquals( 201, HTTP.POST( server.baseUri().resolve( "db/data/node" ).toString(), + RawPayload.quotedJson( "{'name':'jake'}" ) ).status() ); + assertEquals( 404, HTTP.GET( server.baseUri().resolve( "db/data/node/1234" ).toString() ).status() ); + assertEquals( 200, HTTP.POST( server.baseUri().resolve( "db/data/transaction/commit" ).toString(), + RawPayload.quotedJson( "{'statements':[{'statement':'MATCH (n) RETURN n'}]}" ) ).status() ); + } + + @Test + public void shouldReplyNicelyToTooManyFailedAuthAttempts() throws Exception + { + // Given + startServerWithConfiguredUser(); + long timeout = System.currentTimeMillis() + 30_000; + + // When + HTTP.Response response = null; + while(System.currentTimeMillis() < timeout) + { + // Done in a loop because we're racing with the clock to get enough failed requests into 5 seconds + response = HTTP.POST( server.baseUri().resolve( "authentication" ).toString(), + HTTP.RawPayload.quotedJson( "{'username':'neo4j', 'password':'something that is wrong'}" ) ); + + if(response.status() == 429) + { + break; + } + } + + // Then + assertThat(response.status(), equalTo(429)); + assertThat(response.get("errors").get(0).get("code").asText(), equalTo("Neo.ClientError.Security.AuthenticationRateLimit")); + assertThat(response.get("errors").get(0).get("message").asText(), equalTo("Too many failed authentication requests. Please try again in 5 seconds.")); + } + + private void assertAuthorizationRequired( String method, String path, int expectedAuthorizedStatus ) throws JsonParseException + { + assertAuthorizationRequired( method, path, null, expectedAuthorizedStatus ); + } + + private void assertAuthorizationRequired( String method, String path, Object payload, int expectedAuthorizedStatus ) throws JsonParseException + { + // When no header + HTTP.Response response = HTTP.request( method, server.baseUri().resolve( path ).toString(), payload ); + assertThat(response.status(), equalTo(401)); + assertThat(response.get("errors").get(0).get("code").asText(), equalTo("Neo.ClientError.Security.AuthorizationFailed")); + assertThat(response.get("errors").get(0).get("message").asText(), equalTo("No authorization token supplied.")); + assertThat(response.get("authentication").asText(), equalTo("http://localhost:7474/authentication")); + assertThat(response.header( HttpHeaders.WWW_AUTHENTICATE ), equalTo("None")); + + // When malformed header + response = HTTP.withHeaders( HttpHeaders.AUTHORIZATION, "This makes no sense" ).request( method, server.baseUri().resolve( path ).toString(), payload ); + assertThat(response.status(), equalTo(400)); + assertThat(response.get("errors").get(0).get("code").asText(), equalTo("Neo.ClientError.Request.InvalidFormat")); + assertThat(response.get("errors").get(0).get( "message" ).asText(), equalTo("Invalid Authorization header.")); + + // When invalid token + response = HTTP.withHeaders( HttpHeaders.AUTHORIZATION, "Basic realm=\"Neo4j\" " + base64( ":helloworld" ) ).request( method, server.baseUri().resolve( path ).toString(), payload ); + assertThat(response.status(), equalTo(401)); + assertThat(response.get("errors").get(0).get("code").asText(), equalTo("Neo.ClientError.Security.AuthorizationFailed")); + assertThat(response.get("errors").get(0).get("message").asText(), equalTo("Invalid authorization token supplied.")); + assertThat(response.get("authentication").asText(), equalTo("http://localhost:7474/authentication")); + assertThat(response.header(HttpHeaders.WWW_AUTHENTICATE ), equalTo("None")); + + // When authorized + String token = HTTP.POST( authURL(), RawPayload.quotedJson( "{'username':'neo4j','password':'secret'}" ) ) + .get( "authorization_token" ).asText(); + response = HTTP.withHeaders( HttpHeaders.AUTHORIZATION, challengeResponse( token ) ).request( method, server.baseUri().resolve( path ).toString(), payload ); + assertThat(response.status(), equalTo(expectedAuthorizedStatus)); + } + + @After + public void cleanup() + { + if(server != null) {server.stop();} + } + + public void startServerWithConfiguredUser() throws IOException + { + startServer( true ); + // Set the password + HTTP.Response put = HTTP.POST( server.baseUri().resolve( "/user/neo4j/password" ).toString(), + RawPayload.quotedJson( "{'password':'neo4j', 'new_password':'secret'}" ) ); + assertEquals( 200, put.status() ); + } + + public void startServer(boolean authEnabled) throws IOException + { + new File( "neo4j-home/data/dbms/authorization" ).delete(); // TODO: Implement a common component for managing Neo4j file structure and use that here + server = CommunityServerBuilder.server() + .withProperty( ServerSettings.authorization_enabled.name(), Boolean.toString( authEnabled ) ) + .build(); + server.start(); + } + + private String challengeResponse( String token ) + { + return "Basic realm=\"Neo4j\" " + base64( ":" + token ); + } + + private String authURL() + { + return server.baseUri().resolve("authentication").toString(); + } + + private String base64(String value) + { + return new String( Base64.encode( value ), Charset + .forName( "UTF-8" )); + } + + private String quotedJson( String singleQuoted ) + { + return singleQuoted.replaceAll( "'", "\"" ); + } +} \ No newline at end of file
community/server/src/test/java/org/neo4j/server/rest/security/UsersDocIT.java+284 −0 added@@ -0,0 +1,284 @@ +/** + * Copyright (c) 2002-2014 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package org.neo4j.server.rest.security; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.nio.charset.Charset; + +import javax.ws.rs.core.HttpHeaders; + +import com.sun.jersey.core.util.Base64; +import org.codehaus.jackson.JsonNode; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.neo4j.kernel.impl.annotations.Documented; +import org.neo4j.server.CommunityNeoServer; +import org.neo4j.server.configuration.ServerSettings; +import org.neo4j.server.helpers.CommunityServerBuilder; +import org.neo4j.server.rest.RESTDocsGenerator; +import org.neo4j.server.rest.domain.JsonHelper; +import org.neo4j.server.rest.domain.JsonParseException; +import org.neo4j.server.rest.web.PropertyValueException; +import org.neo4j.test.TestData; +import org.neo4j.test.server.ExclusiveServerTestBase; +import org.neo4j.test.server.HTTP; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.not; +import static org.junit.Assert.*; + +public class UsersDocIT extends ExclusiveServerTestBase +{ + public @Rule TestData<RESTDocsGenerator> gen = TestData.producedThrough( RESTDocsGenerator.PRODUCER ); + private CommunityNeoServer server; + + @Before + public void setUp() + { + gen.get().setSection( "dev/rest-api" ); + } + + /** + * Invalidating the authorization token + * + * You can ask that the server generates a new authorization token. This will invalidate any existing token for the + * user. + */ + @Test + @Documented + public void regenerate_token() throws PropertyValueException, IOException + { + // Given + startServerWithConfiguredUser(); + String token = HTTP.POST( authURL(), HTTP.RawPayload.quotedJson( "{'username':'neo4j','password':'secret'}" ) ) + .get( "authorization_token" ).asText(); + + // Document + RESTDocsGenerator.ResponseEntity response = gen.get() + .noGraph() + .expectedStatus( 200 ) + .payload( quotedJson( "{'password':'secret'}" ) ) + .post( server.baseUri().resolve( "/user/neo4j/authorization_token" ).toString() ); + + // Then + JsonNode data = JsonHelper.jsonNode( response.entity() ); + String newToken = data.get( "authorization_token" ).asText(); + assertThat( newToken, not( equalTo( token ) )); + assertThat( newToken.length(), not(0)); + + // And then the token I got back should work + assertEquals(200, HTTP.withHeaders( HttpHeaders.AUTHORIZATION, challengeResponse( newToken ) ).GET( authURL() ).status()); + + // And then the old token should not be invalid + assertEquals(401, HTTP.withHeaders( HttpHeaders.AUTHORIZATION, challengeResponse( token ) ).GET( authURL() ).status()); + } + + /** + * Changing your password + * + * Given that you know the current password, you can ask the server to change a users password. You can choose any + * password you like, as long as it is different from the current password. + */ + @Test + @Documented + public void change_password() throws PropertyValueException, IOException + { + // Given + startServerWithConfiguredUser(); + String originalToken = HTTP.POST( authURL(), HTTP.RawPayload.quotedJson( "{'username':'neo4j','password':'secret'}" ) ) + .get( "authorization_token" ).asText(); + + // Document + RESTDocsGenerator.ResponseEntity response = gen.get() + .noGraph() + .expectedStatus( 200 ) + .payload( quotedJson( "{'password':'secret', 'new_password':'qwerty'}" ) ) + .post( server.baseUri().resolve( "/user/neo4j/password" ).toString() ); + + // Then + JsonNode data = JsonHelper.jsonNode( response.entity() ); + String newToken = data.get( "authorization_token" ).asText(); + assertThat( newToken, equalTo( originalToken )); + assertThat( newToken.length(), not( 0 ) ); + + // And then the new password should work + assertEquals(200, HTTP.POST( authURL(), HTTP.RawPayload.quotedJson( "{'username':'neo4j','password':'qwerty'}" ) ).status()); + + // And then the old password should not be invalid + assertEquals(422, HTTP.POST( authURL(), HTTP.RawPayload.quotedJson( "{'username':'neo4j','password':'secret'}" ) ).status()); + } + + /** + * Setting authorization token + * + * In some cases you may want to explicitly set an authorization token for a user, for instance if you want to be + * able to use the same authorization token for multiple Neo4j instances in a cluster. + * + * This can be done by taking an authorization token generated by Neo4j and asking other Neo4j instance to use that + * token. This is similar to invalidating an existing token, except the new token to be used is passed in explicitly. + */ + @Test + @Documented + public void set_token() throws PropertyValueException, IOException + { + // Given + startServerWithConfiguredUser(); + + String originalToken = HTTP.POST( authURL(), HTTP.RawPayload.quotedJson( "{'username':'neo4j','password':'secret'}" ) ) + .get( "authorization_token" ).asText(); + + // Document + RESTDocsGenerator.ResponseEntity response = gen.get() + .noGraph() + .expectedStatus( 200 ) + .payload( quotedJson( "{'password':'secret', 'new_authorization_token':'EEB9E6883A24CEF7899CF35AD49D5944'}" ) ) + .post( server.baseUri().resolve( "/user/neo4j/authorization_token" ).toString() ); + + // Then + JsonNode data = JsonHelper.jsonNode( response.entity() ); + String newToken = data.get( "authorization_token" ).asText(); + assertThat( newToken, equalTo( "EEB9E6883A24CEF7899CF35AD49D5944" )); + + // And then the token I got back should work + assertEquals(200, HTTP.withHeaders( HttpHeaders.AUTHORIZATION, challengeResponse( newToken ) ).GET( authURL() ).status()); + + // And then the old token should not be invalid + assertEquals( 401, HTTP.withHeaders( HttpHeaders.AUTHORIZATION, challengeResponse( originalToken ) ).GET( + authURL() ).status() ); + } + + @Test + public void cantChangeToCurrentPassword() throws Exception + { + // Given + startServer( true ); + + // When + HTTP.Response res = HTTP.POST( server.baseUri().resolve( "/user/neo4j/password" ).toString(), + HTTP.RawPayload.quotedJson( "{'password':'neo4j', 'new_password':'neo4j'}" ) ); + + // Then + assertThat(res.status(), equalTo(422)); + } + + @Test + public void shouldRateLimit() throws Exception + { + // Given + startServer( true ); + assertRateLimited( "POST", server.baseUri().resolve( "/user/neo4j/password" ), "{'password':'something that is wrong', 'new_password':'secret'}"); + assertRateLimited( "POST", server.baseUri().resolve( "/user/neo4j/authorization_token" ), "{'password':'something that is wrong'}" ); + assertRateLimited( "POST", server.baseUri().resolve( "/user/neo4j/authorization_token" ), "{'password':'something that is wrong','new_authorization_token':'asd'}" ); + } + + @Test + public void shouldRequireAuthorization() throws Exception + { + // Given + startServer( true ); + assertAuthorizationNeeded( "POST", server.baseUri().resolve( "/user/neo4j/password" ), "{'password':'something that is wrong', 'new_password':'secret'}" ); + assertAuthorizationNeeded( "POST", server.baseUri().resolve( "/user/neo4j/authorization_token" ), "{'password':'something that is wrong'}" ); + assertAuthorizationNeeded( "POST", server.baseUri().resolve( "/user/neo4j/authorization_token" ), "{'password':'something that is wrong','new_authorization_token':'asd'}" ); + } + + private void assertAuthorizationNeeded( String method, URI ur, String payload ) throws JsonParseException + { + // When + HTTP.Response response = HTTP.request( method, ur.toString(), + HTTP.RawPayload.quotedJson( payload ) ); + + // Then + assertThat(response.status(), equalTo(422)); + assertThat(response.get("errors").get(0).get("code").asText(), equalTo("Neo.ClientError.Security.AuthenticationFailed")); + assertThat( response.get( "errors" ).get( 0 ).get( "message" ).asText(), equalTo( "Invalid username and/or password." ) ); + } + + private void assertRateLimited( String method, URI uri, String payload ) throws JsonParseException + { + long timeout = System.currentTimeMillis() + 30_000; + + // When + HTTP.Response response = null; + while(System.currentTimeMillis() < timeout) + { + // Done in a loop because we're racing with the clock to get enough failed requests into 5 seconds + response = HTTP.request( method, uri.toString(), + HTTP.RawPayload.quotedJson( payload ) ); + + if(response.status() == 429) + { + break; + } + } + + // Then + assertThat(response.status(), equalTo(429)); + assertThat(response.get("errors").get(0).get("code").asText(), equalTo("Neo.ClientError.Security.AuthenticationRateLimit")); + assertThat(response.get("errors").get(0).get("message").asText(), equalTo("Too many failed authentication requests. Please try again in 5 seconds.")); + } + + @After + public void cleanup() + { + if(server != null) {server.stop();} + } + + public void startServer(boolean authEnabled) throws IOException + { + new File( "neo4j-home/data/dbms/authorization" ).delete(); // TODO: Implement a common component for managing Neo4j file structure and use that here + server = CommunityServerBuilder.server().withProperty( ServerSettings.authorization_enabled.name(), + Boolean.toString( authEnabled ) ).build(); + server.start(); + } + + public void startServerWithConfiguredUser() throws IOException + { + startServer( true ); + // Set the password + HTTP.Response put = HTTP.POST( server.baseUri().resolve( "/user/neo4j/password" ).toString(), + HTTP.RawPayload.quotedJson( "{'password':'neo4j', 'new_password':'secret'}" ) ); + assertEquals( 200, put.status() ); + } + + private String challengeResponse( String token ) + { + return "Basic realm=\"Neo4j\" " + base64( ":" + token ); + } + + private String authURL() + { + return server.baseUri().resolve("authentication").toString(); + } + + private String base64(String value) + { + return new String( Base64.encode( value ), Charset + .forName( "UTF-8" )); + } + + private String quotedJson( String singleQuoted ) + { + return singleQuoted.replaceAll( "'", "\"" ); + } +}
community/server/src/test/java/org/neo4j/server/security/auth/AuthenticationTest.java+100 −0 added@@ -0,0 +1,100 @@ +/** + * Copyright (c) 2002-2014 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package org.neo4j.server.security.auth; + +import java.util.concurrent.TimeUnit; + +import org.junit.Test; +import org.neo4j.helpers.FakeClock; +import org.neo4j.server.security.auth.exception.TooManyAuthenticationAttemptsException; + +import static junit.framework.Assert.assertFalse; +import static junit.framework.TestCase.fail; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.*; + +public class AuthenticationTest +{ + @Test + public void shouldSetPassword() throws Exception + { + // Given + InMemoryUserRepository users = new InMemoryUserRepository(); + Authentication auth = new Authentication( new FakeClock(), users, 1 ); + + users.save( new User( "jake", Privileges.ADMIN ) ); + + // When + auth.setPassword( "jake", "hello, world!" ); + + // Then + assertTrue( auth.authenticate( "jake", "hello, world!" ) ); + assertFalse( auth.authenticate( "jake", "goodbye, world!" ) ); + } + + @Test + public void shouldSlowRequestRateOnMultipleFailedAttempts() throws Exception + { + // Given + FakeClock clock = new FakeClock(); + Authentication auth = new Authentication( clock, new InMemoryUserRepository(), 3 ); + + // And given we've failed three times + auth.authenticate( "wrong", "wrong" ); + auth.authenticate( "wrong", "wrong" ); + auth.authenticate( "wrong", "wrong" ); + + // When we do another request within the cooldown timeframe + try + { + auth.authenticate( "wrong", "wrong" ); + + // Then + fail("Shouldn't have been allowed"); + } + catch(TooManyAuthenticationAttemptsException e) + { + assertThat(e.getMessage(), equalTo("Too many failed authentication requests. Please try again in 5 seconds.")); + } + + // But when time heals all wounds + clock.forward( 6, TimeUnit.SECONDS ); + + // Then things should be alright + assertFalse( auth.authenticate( "wrong", "wrong" )); + } + + @Test + public void handlesMalformedAuthentication() throws Exception + { + // Given + InMemoryUserRepository users = new InMemoryUserRepository(); + Authentication auth = new Authentication( new FakeClock(), users, 50 ); + users.save( new User( "jake", Privileges.ADMIN ) ); + auth.setPassword( "jake", "helo" ); + + // When & then + assertFalse(auth.authenticate( "jake", "hello, world!" )); + assertFalse(auth.authenticate( null, "hello, world!" )); + assertFalse(auth.authenticate( "jake", null )); + assertFalse(auth.authenticate( null, null )); + assertFalse(auth.authenticate( "no such user", null )); + } +}
community/server/src/test/java/org/neo4j/server/security/auth/AuthorizationDisabledIT.java+62 −0 added@@ -0,0 +1,62 @@ +/** + * Copyright (c) 2002-2014 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package org.neo4j.server.security.auth; + +import org.junit.After; +import org.junit.Test; +import org.neo4j.server.CommunityNeoServer; +import org.neo4j.server.configuration.ServerSettings; +import org.neo4j.server.helpers.CommunityServerBuilder; +import org.neo4j.test.server.ExclusiveServerTestBase; +import org.neo4j.test.server.HTTP; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.*; +import static org.neo4j.test.server.HTTP.RawPayload.quotedJson; + +public class AuthorizationDisabledIT extends ExclusiveServerTestBase +{ + + private CommunityNeoServer server; + + @Test + public void shouldAllowDisablingAuthorization() throws Exception + { + // Given + server = CommunityServerBuilder.server().withProperty( ServerSettings.authorization_enabled.name(), "false" ).build(); + + // When + server.start(); + + // Then I should have write access + HTTP.Response response = HTTP.POST( server.baseUri().resolve( "db/data/node" ).toString(), quotedJson( "{'name':'My Node'}" ) ); + assertThat(response.status(), equalTo(201)); + String node = response.location(); + + // Then I should have read access + assertThat( HTTP.GET( node ).status(), equalTo( 200 ) ); + } + + @After + public void cleanup() + { + if(server != null) { server.stop(); } + } +}
community/server/src/test/java/org/neo4j/server/security/auth/FileUserRepositoryTest.java+110 −0 added@@ -0,0 +1,110 @@ +/** + * Copyright (c) 2002-2014 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package org.neo4j.server.security.auth; + +import org.junit.Rule; +import org.junit.Test; + +import java.io.File; + +import org.neo4j.test.EphemeralFileSystemRule; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class FileUserRepositoryTest +{ + public @Rule EphemeralFileSystemRule fsRule = new EphemeralFileSystemRule(); + + @Test + public void shouldStoreAndRetriveUsers() throws Exception + { + // Given + FileUserRepository users = new FileUserRepository( fsRule.get(), new File( "dbms/auth.db" ) ); + User user = new User( "jake", "af123", Privileges.ADMIN, Credentials.INACCESSIBLE, true ); + users.save( user ); + + // When + User result = users.get( user.name() ); + + // Then + assertThat(result, equalTo(user)); + } + + @Test + public void shouldPersistUsers() throws Throwable + { + // Given + FileUserRepository users = new FileUserRepository( fsRule.get(), new File( "dbms/auth.db" ) ); + User user = new User( "jake", "af123", Privileges.ADMIN, Credentials.INACCESSIBLE, true ); + users.save( user ); + + users = new FileUserRepository( fsRule.get(), new File( "dbms/auth.db" ) ); + users.start(); + + // When + User result = users.get( user.name() ); + + // Then + assertThat(result, equalTo(user)); + } + + @Test + public void shouldNotAllowComplexNames() throws Exception + { + // Given + FileUserRepository users = new FileUserRepository( fsRule.get(), new File( "dbms/auth.db" ) ); + + // When + assertTrue( users.isValidName( "neo4j" ) ); + assertTrue( users.isValidName( "johnosbourne" ) ); + assertTrue( users.isValidName( "john_osbourne" ) ); + + assertFalse( users.isValidName( ":" ) ); + assertFalse( users.isValidName( "" ) ); + assertFalse( users.isValidName( "john osbourne" ) ); + assertFalse( users.isValidName( "john:osbourne" ) ); + } + + @Test + public void shouldRecoverIfCrashedDuringWrite() throws Throwable + { + // Given + File dbFile = new File( "dbms/auth.db" ); + FileUserRepository users = new FileUserRepository( fsRule.get(), dbFile ); + User user = new User( "jake", "af123", Privileges.ADMIN, Credentials.INACCESSIBLE, true ); + users.save( user ); + + // And given we emulate having crashed when writing + File tempFile = new File( dbFile.getAbsolutePath() + ".tmp" ); + fsRule.get().renameFile( dbFile, tempFile ); + + // When + users = new FileUserRepository( fsRule.get(), dbFile ); + users.start(); + + // Then + assertFalse(fsRule.get().fileExists( tempFile )); + assertTrue(fsRule.get().fileExists( dbFile )); + assertThat( users.get( user.name() ), equalTo(user)); + } +}
community/server/src/test/java/org/neo4j/server/security/auth/InMemoryUserRepository.java+79 −0 added@@ -0,0 +1,79 @@ +/** + * Copyright (c) 2002-2014 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package org.neo4j.server.security.auth; + +import java.util.Iterator; +import java.util.concurrent.ConcurrentHashMap; + +import org.neo4j.server.security.auth.exception.IllegalTokenException; + +/** A user repository implementation that just stores users in memory */ +public class InMemoryUserRepository implements UserRepository +{ + private final ConcurrentHashMap<String, User> users = new ConcurrentHashMap<>(); + + public User get( String name ) + { + return users.get( name ); + } + + /** This is synchronized to ensure we can't have users with duplicate tokens. */ + public synchronized void save( User user ) throws IllegalTokenException + { + if(user.hasToken()) + { + for ( User other : users.values() ) + { + if ( other.tokenEquals( user.token() ) && !other.name().equals( user.name() ) ) + { + throw new IllegalTokenException( "Unable to set token, because the chosen token is already in use." ); + } + } + } + + users.put( user.name(), user ); + } + + @Override + public Iterator<User> iterator() + { + return users.values().iterator(); + } + + @Override + public int numberOfUsers() + { + return users.size(); + } + + @Override + public boolean isValidName( String name ) + { + // This repo can store any name + return true; + } + + @Override + public boolean isValidToken( String token ) + { + // This repo can store any token + return true; + } +}
community/server/src/test/java/org/neo4j/server/security/auth/SecurityCentralTest.java+103 −0 added@@ -0,0 +1,103 @@ +/** + * Copyright (c) 2002-2014 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package org.neo4j.server.security.auth; + +import org.junit.Test; +import org.neo4j.helpers.FakeClock; +import org.neo4j.server.security.auth.exception.IllegalTokenException; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.junit.Assert.*; + +public class SecurityCentralTest +{ + @Test + public void shouldKeepPersistentToken() throws Exception + { + // Given + SecurityCentral security = new SecurityCentral( new FakeClock(), new InMemoryUserRepository() ); + security.newUser("neo4j", Privileges.ADMIN); + security.regenerateToken( "neo4j" ); + String token = security.userForName( "neo4j" ).token(); + + // When + User user = security.userForToken( token ); + + // Then + assertThat( user.name(), equalTo("neo4j") ); + } + + @Test + public void shouldRegenerateToken() throws Exception + { + // Given + SecurityCentral security = new SecurityCentral( new FakeClock(), new InMemoryUserRepository() ); + security.newUser("neo4j", Privileges.ADMIN); + String oldToken = security.userForName( "neo4j" ).token(); + + // And given the user has been loaded by token before + security.userForToken( oldToken ); + + // When + String newToken = security.regenerateToken( "neo4j" ); + + // Then + assertThat( security.userForName( "neo4j" ).token(), equalTo(newToken) ); + assertThat( security.userForToken( newToken ).name(), equalTo("neo4j")); + assertThat( security.userForToken( oldToken ).name(), equalTo(SecurityCentral.UNAUTHENTICATED.name())); + } + + @Test + public void shouldNotAllowSettingDuplicateTokens() throws Exception + { + // Given + SecurityCentral security = new SecurityCentral( new FakeClock(), new InMemoryUserRepository() ); + security.newUser("neo4j", Privileges.ADMIN); + security.newUser("other", Privileges.ADMIN); + security.regenerateToken( "neo4j" ); + String neo4jUserToken = security.userForName( "neo4j" ).token(); + + // When + try + { + security.setToken( "other", neo4jUserToken ); + fail("Should not have been allowed."); + } catch(IllegalTokenException e) + { + assertThat(e.getMessage(), equalTo("Unable to set token, because the chosen token is already in use.")); + } + } + + @Test + public void shouldRegenerateTokenOnRequiredPasswordChange() throws Exception + { + // Given + SecurityCentral security = new SecurityCentral( new FakeClock(), new InMemoryUserRepository() ); + security.newUser("neo4j", Privileges.ADMIN); + + // When + security.setPassword( "neo4j", "secret" ); + + // Then + assertThat(security.userForName( "neo4j" ).passwordChangeRequired(), equalTo(false)); + assertThat(security.userForName( "neo4j" ).token(), notNullValue()); + } +}
community/server/src/test/java/org/neo4j/server/security/auth/UserSerializationTest.java+75 −0 added@@ -0,0 +1,75 @@ +/** + * Copyright (c) 2002-2014 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package org.neo4j.server.security.auth; + +import org.junit.Test; + +import java.util.List; + +import org.neo4j.kernel.impl.util.Charsets; + +import static java.util.Arrays.asList; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +public class UserSerializationTest +{ + @Test + public void shouldSerializeAndDeserialize() throws Exception + { + // Given + UserSerialization serialization = new UserSerialization(); + + List<User> users = asList( + new User( "Steve", "12345", Privileges.ADMIN, new Credentials( "SomeSalt", "SomeAlgo", "1234321" ), + false ), + new User( "Bob", "54321", Privileges.ADMIN, new Credentials( "OtherSalt", "OtherAlgo", "0987654" ), + false ) ); + + // When + byte[] serialized = serialization.serialize( users ); + + // Then + assertThat( serialization.deserializeUsers( serialized ), equalTo( users ) ); + } + + /** + * This is a future-proofing test. If you come here because you've made changes to the serialization format, + * this is your reminder to make sure to build this is in a backwards compatible way. + */ + @Test + public void shouldReadV1SerializationFormat() throws Exception + { + // Given + UserSerialization serialization = new UserSerialization(); + + // When + List<User> deserialized = serialization.deserializeUsers( + ("Steve:12345:SomeAlgo,1234321,SomeSalt:false\n" + + "Bob:54321:OtherAlgo,0987654,OtherSalt:false") .getBytes( Charsets.UTF_8 ) ); + + // Then + assertThat( deserialized, equalTo( asList( + new User( "Steve", "12345", Privileges.ADMIN, new Credentials( "SomeSalt", "SomeAlgo", "1234321" ), + false ), + new User( "Bob", "54321", Privileges.ADMIN, new Credentials( "OtherSalt", "OtherAlgo", "0987654" ), + false ) ) ) ); + } +} \ No newline at end of file
community/server/src/test/java/org/neo4j/server/security/ssl/KeyStoreFactoryTest.java+4 −1 renamed@@ -17,7 +17,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package org.neo4j.server.security; +package org.neo4j.server.security.ssl; import java.io.File; import java.io.FileInputStream; @@ -26,6 +26,9 @@ import java.security.cert.Certificate; import org.junit.Test; +import org.neo4j.server.security.ssl.KeyStoreFactory; +import org.neo4j.server.security.ssl.KeyStoreInformation; +import org.neo4j.server.security.ssl.SslCertificateFactory; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertEquals;
community/server/src/test/java/org/neo4j/server/security/ssl/TestSslCertificateFactory.java+2 −1 renamed@@ -17,7 +17,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package org.neo4j.server.security; +package org.neo4j.server.security.ssl; import java.io.File; import java.security.PrivateKey; @@ -26,6 +26,7 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.neo4j.server.security.ssl.SslCertificateFactory; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.is;
community/server/src/test/java/org/neo4j/server/webadmin/rest/RootServiceDocTest.java+8 −6 modified@@ -26,8 +26,8 @@ import javax.ws.rs.core.UriInfo; import org.junit.Test; - import org.neo4j.kernel.GraphDatabaseDependencies; +import org.neo4j.kernel.configuration.Config; import org.neo4j.kernel.logging.DevNullLoggingService; import org.neo4j.kernel.monitoring.Monitors; import org.neo4j.server.CommunityNeoServer; @@ -36,10 +36,8 @@ import org.neo4j.test.server.EntityOutputFormat; import static org.hamcrest.Matchers.containsString; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; public class RootServiceDocTest { @@ -50,8 +48,12 @@ public void shouldAdvertiseServicesWhenAsked() throws Exception URI uri = new URI( "http://example.org:7474/" ); when( uriInfo.getBaseUri() ).thenReturn( uri ); - RootService svc = new RootService( new CommunityNeoServer( mock( ConfigurationBuilder.class ), + ConfigurationBuilder configBuilder = mock( ConfigurationBuilder.class ); + when(configBuilder.configuration()).thenReturn( new Config() ); + + RootService svc = new RootService( new CommunityNeoServer( configBuilder, GraphDatabaseDependencies.newDependencies().logging(DevNullLoggingService.DEV_NULL).monitors(new Monitors())) ); + EntityOutputFormat output = new EntityOutputFormat( new JsonFormat(), null, null ); Response serviceDefinition = svc.getServiceDefinition( uriInfo, output );
community/server/src/test/java/org/neo4j/test/server/HTTP.java+29 −10 modified@@ -110,6 +110,16 @@ public static Response GET( String uri ) return BUILDER.GET( uri ); } + public static Response request( String method, String uri ) + { + return BUILDER.request( method, uri ); + } + + public static Response request( String method, String uri, Object payload ) + { + return BUILDER.request( method, uri, payload ); + } + public static class Builder { private final Map<String, String> headers; @@ -146,51 +156,55 @@ public Builder withBaseUri( String baseUri ) public Response POST( String uri ) { - return exec( "POST", uri ); + return request( "POST", uri ); } public Response POST( String uri, Object payload ) { - return exec( "POST", uri, payload ); + return request( "POST", uri, payload ); } public Response POST( String uri, RawPayload payload ) { - return exec( "POST", uri, payload ); + return request( "POST", uri, payload ); } public Response PUT( String uri ) { - return exec( "PUT", uri ); + return request( "PUT", uri ); } public Response PUT( String uri, Object payload ) { - return exec( "PUT", uri, payload ); + return request( "PUT", uri, payload ); } public Response PUT( String uri, RawPayload payload ) { - return exec( "PUT", uri, payload ); + return request( "PUT", uri, payload ); } public Response DELETE( String uri ) { - return exec( "DELETE", uri ); + return request( "DELETE", uri ); } public Response GET( String uri ) { - return exec( "GET", uri ); + return request( "GET", uri ); } - public Response exec( String method, String uri ) + public Response request( String method, String uri ) { return new Response( CLIENT.handle( build().build( buildUri( uri ), method ) ) ); } - public Response exec( String method, String uri, Object payload ) + public Response request( String method, String uri, Object payload ) { + if(payload == null) + { + return request(method, uri); + } String jsonPayload = payload instanceof RawPayload ? ((RawPayload) payload).get() : createJsonFrom( payload ); ClientRequest.Builder lastBuilder = build().entity( jsonPayload, MediaType.APPLICATION_JSON_TYPE ); @@ -297,6 +311,11 @@ public JsonNode get(String fieldName) throws JsonParseException return JsonHelper.jsonNode( entity ).get( fieldName ); } + public String header( String name ) + { + return response.getHeaders().getFirst( name ); + } + @Override public String toString() {
packaging/standalone/pom.xml+3 −1 modified@@ -50,11 +50,13 @@ <org.neo4j.server.webserver.https.enabled>true</org.neo4j.server.webserver.https.enabled> <org.neo4j.webserver.https.port>7473</org.neo4j.webserver.https.port> + <org.neo4j.server.webserver.https.cert.location>conf/ssl/snakeoil.cert</org.neo4j.server.webserver.https.cert.location> <org.neo4j.server.webserver.https.key.location>conf/ssl/snakeoil.key</org.neo4j.server.webserver.https.key.location> <org.neo4j.server.webserver.https.keystore.location>data/keystore</org.neo4j.server.webserver.https.keystore.location> - <org.neo4j.webservice.packages>org.neo4j.rest.web,org.neo4j.webadmin,org.neo4j.webadmin.backup,org.neo4j.webadmin.console,org.neo4j.webadmin.domain,org.neo4j.webadmin.parser,org.neo4j.webadmin.properties,org.neo4j.webadmin.resources,org.neo4j.webadmin.rest,org.neo4j.webadmin.rrd,org.neo4j.webadmin.task,org.neo4j.webadmin.utils</org.neo4j.webservice.packages> + <org.neo4j.webservice.packages>org.neo4j.rest.web,org.neo4j.webadmin,org.neo4j.webadmin.backup,org.neo4j.webadmin.console,org.neo4j.webadmin.domain,org.neo4j.webadmin.parser,org.neo4j.webadmin.properties,org.neo4j.webadmin.resources,org.neo4j.webadmin.rest,org.neo4j.webadmin.rrd,org.neo4j.webadmin.task,org.neo4j.webadmin.utils</org.neo4j.webservice.packages> <org.neo4j.server.bundledir>system/lib</org.neo4j.server.bundledir> + <dbms.security.require_authorization>false</dbms.security.require_authorization> <!-- Runtime properties. These are used to bootstrap the server. All other configuration should happen through a configuration file. Each of these should have a sensible default, so that the server can operate without them defined. --> <neo4j.home>${project.build.directory}/neo4j</neo4j.home>
packaging/standalone/standalone-advanced/src/main/distribution/text/advanced/conf/neo4j-server.properties+3 −0 modified@@ -15,6 +15,9 @@ org.neo4j.server.database.location=${org.neo4j.database.location} # security section in the neo4j manual before modifying this. #org.neo4j.server.webserver.address=0.0.0.0 +# Require (or disable the requirement of) authorization to access Neo4j +dbms.security.authorization_enabled=${dbms.security.require_authorization} + # # HTTP Connector #
packaging/standalone/standalone-community/src/main/distribution/text/community/conf/neo4j-server.properties+3 −0 modified@@ -15,6 +15,9 @@ org.neo4j.server.database.location=${org.neo4j.database.location} # security section in the neo4j manual before modifying this. #org.neo4j.server.webserver.address=0.0.0.0 +# Require (or disable the requirement of) authorization to access Neo4j +dbms.security.authorization_enabled=${dbms.security.require_authorization} + # # HTTP Connector #
packaging/standalone/standalone-enterprise/src/main/distribution/text/enterprise/conf/neo4j-server.properties+3 −0 modified@@ -15,6 +15,9 @@ org.neo4j.server.database.location=${org.neo4j.database.location} # security section in the neo4j manual before modifying this. #org.neo4j.server.webserver.address=0.0.0.0 +# Require (or disable the requirement of) authorization to access Neo4j +dbms.security.authorization_enabled=${dbms.security.require_authorization} + # # HTTP Connector #
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
9- github.com/advisories/GHSA-x626-q4v7-7xc6ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2013-7259ghsaADVISORY
- www.openwall.com/lists/oss-security/2014/01/03/3nvdWEB
- www.openwall.com/lists/oss-security/2014/01/03/8nvdWEB
- github.com/neo4j/neo4j/commit/40ad76078a25666d8b218772b6491fb443020df9ghsaWEB
- github.com/neo4j/neo4j/issues/2826ghsaWEB
- github.com/o2platform/DefCon_RESTing/tree/master/Live-Demos/Neo4jnvdWEB
- web.archive.org/web/20131017043717/http://blog.diniscruz.com/2013/08/neo4j-csrf-payload-to-start-processes.htmlghsaWEB
- blog.diniscruz.com/2013/08/neo4j-csrf-payload-to-start-processes.htmlnvd
News mentions
0No linked articles in our index yet.