Skip to content

Commit

Permalink
Change the Bolt result for credentials expired
Browse files Browse the repository at this point in the history
- On password change required the Bolt server now responds with
<SUCCESS { "credentials_expired": true}>
- Changed AuthSubject.setPassword to void and throwing exceptions on failure
- Changed the changePassword built-in procedure to return an empty result
  • Loading branch information
henriknyman committed Mar 10, 2016
1 parent 07f60d3 commit e16ac52
Show file tree
Hide file tree
Showing 24 changed files with 248 additions and 48 deletions.
47 changes: 47 additions & 0 deletions community/bolt/src/docs/dev/examples.asciidoc
Expand Up @@ -3,6 +3,53 @@


This section contains concrete examples showing how to perform tasks using the full Bolt protocol stack. This section contains concrete examples showing how to perform tasks using the full Bolt protocol stack.


=== Authentication

The first time you connect to neo4j with the default credentials you will be asked to update the password.

.Run query
[source,bolt_auth]
----
# Handshake
Client: <connect>
Client: 60 60 B0 17
Client: 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00
Server: 00 00 00 01
Client: INIT "MyClient/1.0" { "scheme": "basic", "principal": "neo4j", "credentials": "neo4j"}
00 3F B1 01 8C 4D 79 43 6C 69 65 6E 74 2F 31 2E
30 A3 86 73 63 68 65 6D 65 85 62 61 73 69 63 89
70 72 69 6E 63 69 70 61 6C 85 6E 65 6F 34 6A 8B
63 72 65 64 65 6E 74 69 61 6C 73 85 6E 65 6F 34
6A 00 00
Server: SUCCESS { "credentials_expired": true}
00 19 B1 70 A1 D0 13 63 72 65 64 65 6E 74 69 61
6C 73 5F 65 78 70 69 72 65 64 C3 00 00
Server: <disconnect>
Client: <connect>
Client: 60 60 B0 17
Client: 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00
Server: 00 00 00 01
Client: INIT "MyClient/1.0" { "scheme": "basic", "principal": "neo4j", "credentials": "neo4j", "new_credentials": "secret"}
00 40 B1 01 8C 4D 79 43 6C 69 65 6E 74 2F 31 2E
30 A4 86 73 63 68 65 6D 65 85 62 61 73 69 63 89
70 72 69 6E 63 69 70 61 6C 85 6E 65 6F 34 6A 8B
63 72 65 64 65 6E 74 69 61 6C 73 85 6E 65 6F 34
6A 8F 00 16 6E 65 77 5F 63 72 65 64 65 6E 74 69
61 6C 73 86 73 65 63 72 65 74 00 00
Server: SUCCESS { }
00 03 b1 70 a0 00 00
----

=== Running a Cypher query === Running a Cypher query


This illustrates running a simple Cypher query without parameters, and retrieving the results. This illustrates running a simple Cypher query without parameters, and retrieving the results.
Expand Down
Expand Up @@ -45,12 +45,25 @@ public interface Authentication
* @param authToken The token to be authenticated. * @param authToken The token to be authenticated.
* @throws AuthenticationException If authentication failed. * @throws AuthenticationException If authentication failed.
*/ */
AccessMode authenticate( Map<String,Object> authToken ) throws AuthenticationException; AuthenticationResult authenticate( Map<String,Object> authToken ) throws AuthenticationException;


/** /**
* Allows all tokens to authenticate. * Allows all tokens to authenticate.
*/ */
Authentication NONE = authToken -> AccessMode.Static.FULL; Authentication NONE = authToken -> new AuthenticationResult()
{
@Override
public AccessMode getAccessMode()
{
return AccessMode.Static.FULL;
}

@Override
public boolean credentialsExpired()
{
return false;
}
};


String SCHEME_KEY = "scheme"; String SCHEME_KEY = "scheme";
String PRINCIPAL = "principal"; String PRINCIPAL = "principal";
Expand Down
@@ -0,0 +1,29 @@
/*
* Copyright (c) 2002-2016 "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.bolt.security.auth;

import org.neo4j.kernel.api.security.AccessMode;

public interface AuthenticationResult
{
AccessMode getAccessMode();

boolean credentialsExpired();
}
Expand Up @@ -25,7 +25,6 @@


import org.neo4j.graphdb.security.AuthorizationViolationException; import org.neo4j.graphdb.security.AuthorizationViolationException;
import org.neo4j.kernel.api.exceptions.Status; import org.neo4j.kernel.api.exceptions.Status;
import org.neo4j.kernel.api.security.AccessMode;
import org.neo4j.logging.Log; import org.neo4j.logging.Log;
import org.neo4j.logging.LogProvider; import org.neo4j.logging.LogProvider;
import org.neo4j.server.security.auth.AuthSubject; import org.neo4j.server.security.auth.AuthSubject;
Expand All @@ -51,7 +50,7 @@ public BasicAuthentication( BasicAuthManager authManager, LogProvider logProvide
} }


@Override @Override
public AccessMode authenticate( Map<String,Object> authToken ) throws AuthenticationException public AuthenticationResult authenticate( Map<String,Object> authToken ) throws AuthenticationException
{ {
if ( !SCHEME.equals( authToken.get( SCHEME_KEY ) ) ) if ( !SCHEME.equals( authToken.get( SCHEME_KEY ) ) )
{ {
Expand All @@ -71,27 +70,27 @@ public AccessMode authenticate( Map<String,Object> authToken ) throws Authentica
} }
} }


private AccessMode authenticate( String user, String password ) throws AuthenticationException private AuthenticationResult authenticate( String user, String password ) throws AuthenticationException
{ {
authSubject = authManager.login( user, password ); authSubject = authManager.login( user, password );
boolean credentialsExpired = false;
switch ( authSubject.getAuthenticationResult() ) switch ( authSubject.getAuthenticationResult() )
{ {
case SUCCESS: case SUCCESS:
break; break;
case PASSWORD_CHANGE_REQUIRED: case PASSWORD_CHANGE_REQUIRED:
// TODO: We just return OK for now, but we should notify the client with an appropriate message credentialsExpired = true;
//throw new AuthenticationException( Status.Security.CredentialsExpired, identifier.get() );
break; break;
case TOO_MANY_ATTEMPTS: case TOO_MANY_ATTEMPTS:
throw new AuthenticationException( Status.Security.AuthenticationRateLimit, identifier.get() ); throw new AuthenticationException( Status.Security.AuthenticationRateLimit, identifier.get() );
default: default:
log.warn( "Failed authentication attempt for '%s'", user); log.warn( "Failed authentication attempt for '%s'", user);
throw new AuthenticationException( Status.Security.Unauthorized, identifier.get() ); throw new AuthenticationException( Status.Security.Unauthorized, identifier.get() );
} }
return authSubject; return new BasicAuthenticationResult( authSubject, credentialsExpired );
} }


private AccessMode update( String user, String password, String newPassword ) throws AuthenticationException private AuthenticationResult update( String user, String password, String newPassword ) throws AuthenticationException
{ {
authSubject = authManager.login( user, password ); authSubject = authManager.login( user, password );
switch ( authSubject.getAuthenticationResult() ) switch ( authSubject.getAuthenticationResult() )
Expand All @@ -114,7 +113,7 @@ private AccessMode update( String user, String password, String newPassword ) th
default: default:
throw new AuthenticationException( Status.Security.Unauthorized, identifier.get() ); throw new AuthenticationException( Status.Security.Unauthorized, identifier.get() );
} }
return authSubject; return new BasicAuthenticationResult( authSubject, false );
} }


private String safeCast( String key, Map<String,Object> authToken ) throws AuthenticationException private String safeCast( String key, Map<String,Object> authToken ) throws AuthenticationException
Expand Down
@@ -0,0 +1,46 @@
/*
* Copyright (c) 2002-2016 "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.bolt.security.auth;

import org.neo4j.kernel.api.security.AccessMode;

public class BasicAuthenticationResult implements AuthenticationResult
{
private AccessMode accessMode;
private boolean credentialsExpired;

public BasicAuthenticationResult( AccessMode accessMode, boolean credentialsExpired )
{
this.accessMode = accessMode;
this.credentialsExpired = credentialsExpired;
}

@Override
public AccessMode getAccessMode()
{
return accessMode;
}

@Override
public boolean credentialsExpired()
{
return credentialsExpired;
}
}
@@ -0,0 +1,57 @@
/*
* Copyright (c) 2002-2016 "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.bolt.v1.messaging.msgprocess;

import java.util.HashMap;
import java.util.Map;

import org.neo4j.logging.Log;

public class InitCallback extends MessageProcessingCallback<Boolean>
{
private final Map<String,Object> successMetadata = new HashMap<>();

public InitCallback( Log log )
{
super( log );
}

@Override
public void result( Boolean credentialsExpired, Void none ) throws Exception
{
if ( credentialsExpired )
{
successMetadata.put( "credentials_expired", credentialsExpired );
}
}

@Override
protected Map<String,Object> successMetadata()
{
return successMetadata;
}

@Override
protected void clearState()
{
super.clearState();
successMetadata.clear();
}
}
Expand Up @@ -33,6 +33,7 @@ public class TransportBridge extends MessageHandler.Adapter<RuntimeException>
{ {
// Note that these callbacks can be used for multiple in-flight requests simultaneously, you cannot reset them // Note that these callbacks can be used for multiple in-flight requests simultaneously, you cannot reset them
// while there are in-flight requests. // while there are in-flight requests.
private final MessageProcessingCallback<Boolean> initCallback;
private final MessageProcessingCallback<StatementMetadata> runCallback; private final MessageProcessingCallback<StatementMetadata> runCallback;
private final MessageProcessingCallback<RecordStream> resultStreamCallback; private final MessageProcessingCallback<RecordStream> resultStreamCallback;
private final MessageProcessingCallback<Void> simpleCallback; private final MessageProcessingCallback<Void> simpleCallback;
Expand All @@ -45,7 +46,9 @@ public TransportBridge( Log log, Session session, MessageHandler<IOException> ou
this.resultStreamCallback = new RecordStreamCallback( log ); this.resultStreamCallback = new RecordStreamCallback( log );
this.simpleCallback = new MessageProcessingCallback<>( log ); this.simpleCallback = new MessageProcessingCallback<>( log );
this.runCallback = new RunCallback( log ); this.runCallback = new RunCallback( log );
this.initCallback = new InitCallback( log );
this.session = session; this.session = session;
this.initCallback.reset( output, onEachCompletedRequest );
this.simpleCallback.reset( output, onEachCompletedRequest ); this.simpleCallback.reset( output, onEachCompletedRequest );
this.resultStreamCallback.reset( output, onEachCompletedRequest ); this.resultStreamCallback.reset( output, onEachCompletedRequest );
this.runCallback.reset( output, onEachCompletedRequest ); this.runCallback.reset( output, onEachCompletedRequest );
Expand All @@ -54,7 +57,7 @@ public TransportBridge( Log log, Session session, MessageHandler<IOException> ou
@Override @Override
public void handleInitMessage( String clientName, Map<String,Object> authToken ) throws RuntimeException public void handleInitMessage( String clientName, Map<String,Object> authToken ) throws RuntimeException
{ {
session.init( clientName, authToken, null, simpleCallback ); session.init( clientName, authToken, null, initCallback );


} }


Expand Down
Expand Up @@ -77,7 +77,7 @@ public String key()
} }


@Override @Override
public <A> void init( String clientName, Map<String,Object> authToken, A attachment, Callback<Void,A> callback ) public <A> void init( String clientName, Map<String,Object> authToken, A attachment, Callback<Boolean,A> callback )
{ {
monitor.messageReceived(); monitor.messageReceived();
delegate.init( clientName, authToken, attachment, withMonitor( callback ) ); delegate.init( clientName, authToken, attachment, withMonitor( callback ) );
Expand Down
Expand Up @@ -125,7 +125,7 @@ public static <V, A> Callback<V,A> noop()
/** /**
* Initialize the session. * Initialize the session.
*/ */
<A> void init( String clientName, Map<String,Object> authToken, A attachment, Callback<Void,A> callback ); <A> void init( String clientName, Map<String,Object> authToken, A attachment, Callback<Boolean,A> callback );


/** /**
* Run a statement, yielding a result stream which can be retrieved through pulling it in a subsequent call. * Run a statement, yielding a result stream which can be retrieved through pulling it in a subsequent call.
Expand Down
Expand Up @@ -53,7 +53,7 @@ private <V, A> void reportError( A attachment, Callback<V,A> callback )
} }


@Override @Override
public <A> void init( String clientName, Map<String,Object> authToken, A attachment, Callback<Void,A> callback ) public <A> void init( String clientName, Map<String,Object> authToken, A attachment, Callback<Boolean,A> callback )
{ {
reportError( attachment, callback ); reportError( attachment, callback );
} }
Expand Down
Expand Up @@ -25,6 +25,7 @@


import org.neo4j.bolt.security.auth.Authentication; import org.neo4j.bolt.security.auth.Authentication;
import org.neo4j.bolt.security.auth.AuthenticationException; import org.neo4j.bolt.security.auth.AuthenticationException;
import org.neo4j.bolt.security.auth.AuthenticationResult;
import org.neo4j.bolt.v1.runtime.Session; import org.neo4j.bolt.v1.runtime.Session;
import org.neo4j.bolt.v1.runtime.StatementMetadata; import org.neo4j.bolt.v1.runtime.StatementMetadata;
import org.neo4j.bolt.v1.runtime.spi.RecordStream; import org.neo4j.bolt.v1.runtime.spi.RecordStream;
Expand Down Expand Up @@ -70,14 +71,20 @@ public State init( SessionStateMachine ctx, String clientName, Map<String,Object
{ {
try try
{ {
ctx.accessMode = ctx.spi.authenticate( authToken ); AuthenticationResult authResult = ctx.spi.authenticate( authToken );
ctx.accessMode = authResult.getAccessMode();
ctx.result( authResult.credentialsExpired() );
ctx.spi.udcRegisterClient( clientName ); ctx.spi.udcRegisterClient( clientName );
return IDLE; return IDLE;
} }
catch ( AuthenticationException e ) catch ( AuthenticationException e )
{ {
return error( ctx, new Neo4jError( e.status(), e.getMessage(), e ) ); return error( ctx, new Neo4jError( e.status(), e.getMessage(), e ) );
} }
catch ( Throwable e )
{
return error( ctx, e );
}
} }


@Override @Override
Expand Down Expand Up @@ -493,7 +500,6 @@ State error( SessionStateMachine ctx, Neo4jError err )
} }


private final String id = UUID.randomUUID().toString(); private final String id = UUID.randomUUID().toString();
private AccessMode accessMode;


/** A re-usable statement metadata instance that always represents the currently running statement */ /** A re-usable statement metadata instance that always represents the currently running statement */
private final StatementMetadata currentStatementMetadata = new StatementMetadata() private final StatementMetadata currentStatementMetadata = new StatementMetadata()
Expand Down Expand Up @@ -529,6 +535,9 @@ public String[] fieldNames()
/** Callback attachment */ /** Callback attachment */
private Object currentAttachment; private Object currentAttachment;


/** The current session auth state to be used for starting transactions */
private AccessMode accessMode;

/** These are the "external" actions the state machine can take */ /** These are the "external" actions the state machine can take */
private final SPI spi; private final SPI spi;


Expand All @@ -555,7 +564,7 @@ interface SPI
void unbindTransactionFromCurrentThread(); void unbindTransactionFromCurrentThread();
RecordStream run( SessionStateMachine ctx, String statement, Map<String, Object> params ) RecordStream run( SessionStateMachine ctx, String statement, Map<String, Object> params )
throws KernelException; throws KernelException;
AccessMode authenticate( Map<String, Object> authToken ) throws AuthenticationException; AuthenticationResult authenticate( Map<String, Object> authToken ) throws AuthenticationException;
void udcRegisterClient( String clientName ); void udcRegisterClient( String clientName );
Statement currentStatement(); Statement currentStatement();
} }
Expand All @@ -579,7 +588,7 @@ public String key()
} }


@Override @Override
public <A> void init( String clientName, Map<String,Object> authToken, A attachment, Callback<Void,A> callback ) public <A> void init( String clientName, Map<String,Object> authToken, A attachment, Callback<Boolean,A> callback )
{ {
before( attachment, callback ); before( attachment, callback );
try try
Expand Down

0 comments on commit e16ac52

Please sign in to comment.