diff --git a/community/kernel/src/main/java/org/neo4j/kernel/api/security/SecurityContext.java b/community/kernel/src/main/java/org/neo4j/kernel/api/security/SecurityContext.java index 8e2e74b221a5d..090f9dd3bb424 100644 --- a/community/kernel/src/main/java/org/neo4j/kernel/api/security/SecurityContext.java +++ b/community/kernel/src/main/java/org/neo4j/kernel/api/security/SecurityContext.java @@ -19,6 +19,8 @@ */ package org.neo4j.kernel.api.security; +import static org.neo4j.graphdb.security.AuthorizationViolationException.PERMISSION_DENIED; + /** Controls the capabilities of a KernelTransaction. */ public interface SecurityContext { @@ -29,6 +31,14 @@ public interface SecurityContext SecurityContext freeze(); SecurityContext withMode( AccessMode mode ); + default void assertCredentialsNotExpired() + { + if ( subject().getAuthenticationResult().equals( AuthenticationResult.PASSWORD_CHANGE_REQUIRED ) ) + { + throw mode().onViolation( PERMISSION_DENIED ); + } + } + default String description() { return String.format( "user '%s' with %s", subject().username(), mode().name() ); diff --git a/community/kernel/src/main/java/org/neo4j/kernel/builtinprocs/BuiltInDbmsProcedures.java b/community/kernel/src/main/java/org/neo4j/kernel/builtinprocs/BuiltInDbmsProcedures.java index 58fe050517082..a518d0e9b50ed 100644 --- a/community/kernel/src/main/java/org/neo4j/kernel/builtinprocs/BuiltInDbmsProcedures.java +++ b/community/kernel/src/main/java/org/neo4j/kernel/builtinprocs/BuiltInDbmsProcedures.java @@ -50,6 +50,7 @@ public class BuiltInDbmsProcedures @Procedure( name = "dbms.listConfig", mode = DBMS ) public Stream listConfig( @Name( value = "searchString", defaultValue = "" ) String searchString ) { + securityContext.assertCredentialsNotExpired(); if ( !securityContext.isAdmin() ) { throw new AuthorizationViolationException( PERMISSION_DENIED ); @@ -66,6 +67,7 @@ public Stream listConfig( @Name( value = "searchString", defaultVa @Procedure( name = "dbms.procedures", mode = DBMS ) public Stream listProcedures() { + securityContext.assertCredentialsNotExpired(); return graph.getDependencyResolver().resolveDependency( Procedures.class ).getAllProcedures().stream() .sorted( Comparator.comparing( a -> a.name().toString() ) ) .map( ProcedureResult::new ); @@ -75,6 +77,7 @@ public Stream listProcedures() @Procedure(name = "dbms.functions", mode = DBMS) public Stream listFunctions() { + securityContext.assertCredentialsNotExpired(); return graph.getDependencyResolver().resolveDependency( Procedures.class ).getAllFunctions().stream() .sorted( Comparator.comparing( a -> a.name().toString() ) ) .map( FunctionResult::new ); diff --git a/community/security/src/main/java/org/neo4j/server/security/auth/AuthProcedures.java b/community/security/src/main/java/org/neo4j/server/security/auth/AuthProcedures.java index b1be4fc89940d..4a24d825d1f85 100644 --- a/community/security/src/main/java/org/neo4j/server/security/auth/AuthProcedures.java +++ b/community/security/src/main/java/org/neo4j/server/security/auth/AuthProcedures.java @@ -56,6 +56,7 @@ public void createUser( @Name( value = "requirePasswordChange", defaultValue = "true" ) boolean requirePasswordChange ) throws InvalidArgumentsException, IOException { + securityContext.assertCredentialsNotExpired(); userManager.newUser( username, password, requirePasswordChange ); } @@ -63,6 +64,7 @@ public void createUser( @Procedure( name = "dbms.security.deleteUser", mode = DBMS ) public void deleteUser( @Name( "username" ) String username ) throws InvalidArgumentsException, IOException { + securityContext.assertCredentialsNotExpired(); if ( securityContext.subject().hasUsername( username ) ) { throw new InvalidArgumentsException( "Deleting yourself (user '" + username + "') is not allowed." ); @@ -109,6 +111,7 @@ public Stream showCurrentUserDeprecated() throws InvalidArgumentsExc @Procedure( name = "dbms.security.listUsers", mode = DBMS ) public Stream listUsers() throws InvalidArgumentsException, IOException { + securityContext.assertCredentialsNotExpired(); Set usernames = userManager.getAllUsernames(); if ( usernames.isEmpty() ) diff --git a/community/security/src/test/java/org/neo4j/server/security/auth/AuthProceduresIT.java b/community/security/src/test/java/org/neo4j/server/security/auth/AuthProceduresIT.java index 6f96272d2811c..14196632cdc1d 100644 --- a/community/security/src/test/java/org/neo4j/server/security/auth/AuthProceduresIT.java +++ b/community/security/src/test/java/org/neo4j/server/security/auth/AuthProceduresIT.java @@ -104,6 +104,27 @@ public void shouldNotChangeOwnPasswordIfNewPasswordInvalid() throws Exception assertFail( admin, "CALL dbms.changePassword( 'neo4j' )", "Old password and new password cannot be the same." ); } + @Test + public void newUserShouldBeAbleToChangePassword() throws Throwable + { + // Given + authManager.newUser( "andres", "banana", true ); + + // Then + assertEmpty( login("andres", "banana"), "CALL dbms.changePassword('abc')" ); + } + + @Test + public void newUserShouldNotBeAbleToCallOtherProcedures() throws Throwable + { + // Given + authManager.newUser( "andres", "banana", true ); + BasicSecurityContext user = login("andres", "banana"); + + // Then + assertFail( user, "CALL dbms.procedures", "The credentials you provided were valid, but must be changed before you can use this instance." ); + } + //---------- create user ----------- @Test diff --git a/enterprise/kernel/src/main/java/org/neo4j/kernel/enterprise/builtinprocs/EnterpriseBuiltInDbmsProcedures.java b/enterprise/kernel/src/main/java/org/neo4j/kernel/enterprise/builtinprocs/EnterpriseBuiltInDbmsProcedures.java index 97cd080e4e795..5d6108d285dad 100644 --- a/enterprise/kernel/src/main/java/org/neo4j/kernel/enterprise/builtinprocs/EnterpriseBuiltInDbmsProcedures.java +++ b/enterprise/kernel/src/main/java/org/neo4j/kernel/enterprise/builtinprocs/EnterpriseBuiltInDbmsProcedures.java @@ -82,6 +82,7 @@ public class EnterpriseBuiltInDbmsProcedures @Procedure( name = "dbms.setTXMetaData", mode = DBMS ) public void setTXMetaData( @Name( value = "data" ) Map data ) { + securityContext.assertCredentialsNotExpired(); int totalCharSize = data.entrySet().stream() .mapToInt( e -> e.getKey().length() + e.getValue().toString().length() ) .sum(); @@ -164,6 +165,7 @@ public Stream terminateConnectionsForUser( @Name( "username" ) @Procedure(name = "dbms.functions", mode = DBMS) public Stream listFunctions() { + securityContext.assertCredentialsNotExpired(); return graph.getDependencyResolver().resolveDependency( Procedures.class ).getAllFunctions().stream() .sorted( ( a, b ) -> a.name().toString().compareTo( b.name().toString() ) ) .map( FunctionResult::new ); @@ -190,6 +192,7 @@ private FunctionResult( UserFunctionSignature signature ) @Procedure( name = "dbms.procedures", mode = DBMS ) public Stream listProcedures() { + securityContext.assertCredentialsNotExpired(); Procedures procedures = graph.getDependencyResolver().resolveDependency( Procedures.class ); return procedures.getAllProcedures().stream() .sorted( ( a, b ) -> a.name().toString().compareTo( b.name().toString() ) ) @@ -239,6 +242,7 @@ public ProcedureResult( ProcedureSignature signature ) @Procedure( name = "dbms.listQueries", mode = DBMS ) public Stream listQueries() throws InvalidArgumentsException, IOException { + securityContext.assertCredentialsNotExpired(); try { return getKernelTransactions().activeTransactions().stream() @@ -258,6 +262,7 @@ public Stream listQueries() throws InvalidArgumentsException, public Stream listActiveLocks( @Name( "queryId" ) String queryId ) throws InvalidArgumentsException { + securityContext.assertCredentialsNotExpired(); try { long id = fromExternalString( queryId ).kernelQueryId(); @@ -276,6 +281,7 @@ public Stream listActiveLocks( @Name( "queryId" ) String public Stream killQuery( @Name( "id" ) String idText ) throws InvalidArgumentsException, IOException { + securityContext.assertCredentialsNotExpired(); try { long queryId = fromExternalString( idText ).kernelQueryId(); @@ -295,6 +301,7 @@ public Stream killQuery( @Name( "id" ) String idText ) public Stream killQueries( @Name( "ids" ) List idTexts ) throws InvalidArgumentsException, IOException { + securityContext.assertCredentialsNotExpired(); try { Set queryIds = idTexts diff --git a/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/SecurityProcedures.java b/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/SecurityProcedures.java index ebbe1cd956cee..d847c27307378 100644 --- a/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/SecurityProcedures.java +++ b/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/SecurityProcedures.java @@ -49,6 +49,7 @@ public Stream showCurrentUser() throws Inva @Procedure( name = "dbms.security.clearAuthCache", mode = DBMS ) public void clearAuthenticationCache() { + securityContext.assertCredentialsNotExpired(); if ( !securityContext.isAdmin() ) { throw new AuthorizationViolationException( PERMISSION_DENIED ); diff --git a/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/UserManagementProcedures.java b/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/UserManagementProcedures.java index d954d9a4f061c..aa1bd46f32051 100644 --- a/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/UserManagementProcedures.java +++ b/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/UserManagementProcedures.java @@ -40,6 +40,7 @@ public void createUser( @Name( "username" ) String username, @Name( "password" ) @Name( value = "requirePasswordChange", defaultValue = "true" ) boolean requirePasswordChange ) throws InvalidArgumentsException, IOException { + securityContext.assertCredentialsNotExpired(); userManager.newUser( username, password, requirePasswordChange ); } @@ -58,7 +59,7 @@ public void changePassword( @Name( "password" ) String password, @Name( value = "requirePasswordChange", defaultValue = "false" ) boolean requirePasswordChange ) throws InvalidArgumentsException, IOException { - changeUserPassword( securityContext.subject().username(), password, requirePasswordChange ); + setUserPassword( securityContext.subject().username(), password, requirePasswordChange ); } @Description( "Change the given user's password." ) @@ -67,11 +68,8 @@ public void changeUserPassword( @Name( "username" ) String username, @Name( "new @Name( value = "requirePasswordChange", defaultValue = "true" ) boolean requirePasswordChange ) throws InvalidArgumentsException, IOException { - userManager.setUserPassword( username, newPassword, requirePasswordChange ); - if ( securityContext.subject().hasUsername( username ) ) - { - securityContext.subject().setPasswordChangeNoLongerRequired(); - } + securityContext.assertCredentialsNotExpired(); + setUserPassword( username, newPassword, requirePasswordChange ); } @Description( "Assign a role to the user." ) @@ -79,6 +77,7 @@ public void changeUserPassword( @Name( "username" ) String username, @Name( "new public void addRoleToUser( @Name( "roleName" ) String roleName, @Name( "username" ) String username ) throws IOException, InvalidArgumentsException { + securityContext.assertCredentialsNotExpired(); userManager.addRoleToUser( roleName, username ); } @@ -87,6 +86,7 @@ public void addRoleToUser( @Name( "roleName" ) String roleName, @Name( "username public void removeRoleFromUser( @Name( "roleName" ) String roleName, @Name( "username" ) String username ) throws InvalidArgumentsException, IOException { + securityContext.assertCredentialsNotExpired(); userManager.removeRoleFromUser( roleName, username ); } @@ -94,6 +94,7 @@ public void removeRoleFromUser( @Name( "roleName" ) String roleName, @Name( "use @Procedure( name = "dbms.security.deleteUser", mode = DBMS ) public void deleteUser( @Name( "username" ) String username ) throws InvalidArgumentsException, IOException { + securityContext.assertCredentialsNotExpired(); if ( userManager.deleteUser( username ) ) { kickoutUser( username, "deletion" ); @@ -104,6 +105,7 @@ public void deleteUser( @Name( "username" ) String username ) throws InvalidArgu @Procedure( name = "dbms.security.suspendUser", mode = DBMS ) public void suspendUser( @Name( "username" ) String username ) throws IOException, InvalidArgumentsException { + securityContext.assertCredentialsNotExpired(); userManager.suspendUser( username ); kickoutUser( username, "suspension" ); } @@ -114,6 +116,7 @@ public void activateUser( @Name( "username" ) String username, @Name( value = "requirePasswordChange", defaultValue = "true" ) boolean requirePasswordChange ) throws IOException, InvalidArgumentsException { + securityContext.assertCredentialsNotExpired(); userManager.activateUser( username, requirePasswordChange ); } @@ -121,6 +124,7 @@ public void activateUser( @Name( "username" ) String username, @Procedure( name = "dbms.security.listUsers", mode = DBMS ) public Stream listUsers() throws InvalidArgumentsException, IOException { + securityContext.assertCredentialsNotExpired(); Set users = userManager.getAllUsernames(); if ( users.isEmpty() ) { @@ -136,6 +140,7 @@ public Stream listUsers() throws InvalidArgumentsException, IOExcept @Procedure( name = "dbms.security.listRoles", mode = DBMS ) public Stream listRoles() throws InvalidArgumentsException, IOException { + securityContext.assertCredentialsNotExpired(); Set roles = userManager.getAllRoleNames(); return roles.stream().map( this::roleResultForName ); } @@ -145,6 +150,7 @@ public Stream listRoles() throws InvalidArgumentsException, IOExcept public Stream listRolesForUser( @Name( "username" ) String username ) throws InvalidArgumentsException, IOException { + securityContext.assertCredentialsNotExpired(); return userManager.getRoleNamesForUser( username ).stream().map( StringResult::new ); } @@ -153,6 +159,7 @@ public Stream listRolesForUser( @Name( "username" ) String usernam public Stream listUsersForRole( @Name( "roleName" ) String roleName ) throws InvalidArgumentsException, IOException { + securityContext.assertCredentialsNotExpired(); return userManager.getUsernamesForRole( roleName ).stream().map( StringResult::new ); } @@ -160,6 +167,7 @@ public Stream listUsersForRole( @Name( "roleName" ) String roleNam @Procedure( name = "dbms.security.createRole", mode = DBMS ) public void createRole( @Name( "roleName" ) String roleName ) throws InvalidArgumentsException, IOException { + securityContext.assertCredentialsNotExpired(); userManager.newRole( roleName ); } @@ -167,6 +175,17 @@ public void createRole( @Name( "roleName" ) String roleName ) throws InvalidArgu @Procedure( name = "dbms.security.deleteRole", mode = DBMS ) public void deleteRole( @Name( "roleName" ) String roleName ) throws InvalidArgumentsException, IOException { + securityContext.assertCredentialsNotExpired(); userManager.deleteRole( roleName ); } + + private void setUserPassword( String username, String newPassword, boolean requirePasswordChange ) + throws IOException, InvalidArgumentsException + { + userManager.setUserPassword( username, newPassword, requirePasswordChange ); + if ( securityContext.subject().hasUsername( username ) ) + { + securityContext.subject().setPasswordChangeNoLongerRequired(); + } + } } diff --git a/enterprise/security/src/test/java/org/neo4j/server/security/enterprise/auth/UserManagementProceduresLoggingTest.java b/enterprise/security/src/test/java/org/neo4j/server/security/enterprise/auth/UserManagementProceduresLoggingTest.java index 93548c306800d..fff12e3eb8f9b 100644 --- a/enterprise/security/src/test/java/org/neo4j/server/security/enterprise/auth/UserManagementProceduresLoggingTest.java +++ b/enterprise/security/src/test/java/org/neo4j/server/security/enterprise/auth/UserManagementProceduresLoggingTest.java @@ -29,7 +29,6 @@ import org.neo4j.graphdb.security.AuthorizationViolationException; import org.neo4j.kernel.api.exceptions.InvalidArgumentsException; import org.neo4j.kernel.api.security.AuthenticationResult; -import org.neo4j.kernel.api.security.SecurityContext; import org.neo4j.kernel.enterprise.api.security.EnterpriseSecurityContext; import org.neo4j.server.security.enterprise.log.SecurityLog; import org.neo4j.kernel.impl.util.JobScheduler; diff --git a/integrationtests/src/test/java/org/neo4j/bolt/DeleteUserStressIT.java b/integrationtests/src/test/java/org/neo4j/bolt/DeleteUserStressIT.java index 513ac5e9c1b50..c03a04ea05c24 100644 --- a/integrationtests/src/test/java/org/neo4j/bolt/DeleteUserStressIT.java +++ b/integrationtests/src/test/java/org/neo4j/bolt/DeleteUserStressIT.java @@ -57,6 +57,14 @@ public void setup() throws Exception File knownHosts = new File( System.getProperty( "user.home" ) + "/.neo4j/known_hosts" ); FileUtils.deleteFile( knownHosts ); adminDriver = GraphDatabase.driver( db.boltURI(), basic( "neo4j", "neo4j" ) ); + try ( Session session = adminDriver.session(); + Transaction tx = session.beginTransaction() ) + { + tx.run( "CALL dbms.changePassword('abc')" ).consume(); + tx.success(); + } + adminDriver.close(); + adminDriver = GraphDatabase.driver( db.boltURI(), basic( "neo4j", "abc" ) ); } @Test