diff --git a/community/bolt/src/test/java/org/neo4j/bolt/v1/transport/integration/AuthenticationIT.java b/community/bolt/src/test/java/org/neo4j/bolt/v1/transport/integration/AuthenticationIT.java index c801b86f08730..2dfe405c4fb47 100644 --- a/community/bolt/src/test/java/org/neo4j/bolt/v1/transport/integration/AuthenticationIT.java +++ b/community/bolt/src/test/java/org/neo4j/bolt/v1/transport/integration/AuthenticationIT.java @@ -76,7 +76,7 @@ protected Consumer, String>> getSettingsFunction() @Parameterized.Parameter( 1 ) public HostnamePort address; - private Connection client; + protected Connection client; @Parameterized.Parameters public static Collection transports() diff --git a/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/AuthProcedures.java b/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/AuthProcedures.java index 98005f3b8bb95..777617f639cdb 100644 --- a/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/AuthProcedures.java +++ b/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/AuthProcedures.java @@ -83,7 +83,7 @@ public void addUserToRole( @Name( "username" ) String username, @Name( "roleName { throw new AuthorizationViolationException( PERMISSION_DENIED ); } - shiroSubject.getRoleManager().addUserToRole( username, roleName ); + shiroSubject.getUserManager().addUserToRole( username, roleName ); } @PerformsDBMS @@ -96,7 +96,7 @@ public void removeUserFromRole( @Name( "username" ) String username, @Name( "rol { throw new AuthorizationViolationException( PERMISSION_DENIED ); } - shiroSubject.getRoleManager().removeUserFromRole( username, roleName ); + shiroSubject.getUserManager().removeUserFromRole( username, roleName ); } @PerformsDBMS @@ -141,8 +141,8 @@ public Stream showCurrentUser( ) throws IllegalCredentialsException, IOException { ShiroAuthSubject shiroSubject = ShiroAuthSubject.castOrFail( authSubject ); - RoleManager roleManager = shiroSubject.getRoleManager(); - return Stream.of( new UserResult( shiroSubject.name(), roleManager.getRoleNamesForUser( shiroSubject.name() ) ) ); + EnterpriseUserManager userManager = shiroSubject.getUserManager(); + return Stream.of( new UserResult( shiroSubject.name(), userManager.getRoleNamesForUser( shiroSubject.name() ) ) ); } @PerformsDBMS @@ -154,9 +154,9 @@ public Stream listUsers() throws IllegalCredentialsException, IOExce { throw new AuthorizationViolationException( PERMISSION_DENIED ); } - RoleManager roleManager = shiroSubject.getRoleManager(); + EnterpriseUserManager userManager = shiroSubject.getUserManager(); return shiroSubject.getUserManager().getAllUsernames().stream() - .map( u -> new UserResult( u, roleManager.getRoleNamesForUser( u ) ) ); + .map( u -> new UserResult( u, userManager.getRoleNamesForUser( u ) ) ); } @PerformsDBMS @@ -168,9 +168,9 @@ public Stream listRoles() throws IllegalCredentialsException, IOExce { throw new AuthorizationViolationException( PERMISSION_DENIED ); } - RoleManager roleManager = shiroSubject.getRoleManager(); - return roleManager.getAllRoleNames().stream() - .map( r -> new RoleResult( r, roleManager.getUsernamesForRole( r ) ) ); + EnterpriseUserManager userManager = shiroSubject.getUserManager(); + return userManager.getAllRoleNames().stream() + .map( r -> new RoleResult( r, userManager.getUsernamesForRole( r ) ) ); } @PerformsDBMS @@ -183,7 +183,7 @@ public Stream listRolesForUser( @Name( "username" ) String usernam { throw new AuthorizationViolationException( PERMISSION_DENIED ); } - return shiroSubject.getRoleManager().getRoleNamesForUser( username ).stream().map( StringResult::new ); + return shiroSubject.getUserManager().getRoleNamesForUser( username ).stream().map( StringResult::new ); } @PerformsDBMS @@ -196,7 +196,7 @@ public Stream listUsersForRole( @Name( "roleName" ) String roleNam { throw new AuthorizationViolationException( PERMISSION_DENIED ); } - return shiroSubject.getRoleManager().getUsernamesForRole( roleName ).stream().map( StringResult::new ); + return shiroSubject.getUserManager().getUsernamesForRole( roleName ).stream().map( StringResult::new ); } public class StringResult { diff --git a/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/EnterpriseAuthManager.java b/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/EnterpriseAuthManager.java index b0ea5f856ad21..ba0c6ec0a2b3b 100644 --- a/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/EnterpriseAuthManager.java +++ b/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/EnterpriseAuthManager.java @@ -20,21 +20,15 @@ package org.neo4j.server.security.enterprise.auth; import java.time.Clock; +import java.util.Map; +import org.neo4j.kernel.api.security.AuthManager; +import org.neo4j.kernel.api.security.AuthSubject; +import org.neo4j.kernel.api.security.exception.InvalidAuthTokenException; import org.neo4j.server.security.auth.PasswordPolicy; import org.neo4j.server.security.auth.UserRepository; -public class EnterpriseAuthManager extends ShiroAuthManager +public interface EnterpriseAuthManager extends AuthManager { - public EnterpriseAuthManager( UserRepository userRepository, RoleRepository roleRepository, - PasswordPolicy passwordPolicy, Clock clock, boolean authEnabled ) - { - super( userRepository, roleRepository, passwordPolicy, clock, authEnabled ); - } - - @Override - public void start() throws Throwable - { - super.start(); - } + EnterpriseUserManager getUserManager(); } diff --git a/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/EnterpriseAuthManagerFactory.java b/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/EnterpriseAuthManagerFactory.java index 157d307146ec6..427ba96fbfd90 100644 --- a/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/EnterpriseAuthManagerFactory.java +++ b/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/EnterpriseAuthManagerFactory.java @@ -19,7 +19,11 @@ */ package org.neo4j.server.security.enterprise.auth; +import org.apache.shiro.realm.Realm; + import java.io.File; +import java.util.ArrayList; +import java.util.List; import org.neo4j.dbms.DatabaseManagementSystemSettings; import org.neo4j.graphdb.factory.GraphDatabaseSettings; @@ -27,9 +31,11 @@ import org.neo4j.kernel.api.security.AuthManager; import org.neo4j.kernel.configuration.Config; import org.neo4j.logging.LogProvider; +import org.neo4j.server.security.auth.AuthenticationStrategy; import org.neo4j.server.security.auth.BasicPasswordPolicy; import org.neo4j.server.security.auth.FileUserRepository; import org.neo4j.server.security.auth.PasswordPolicy; +import org.neo4j.server.security.auth.RateLimitedAuthenticationStrategy; import org.neo4j.server.security.auth.UserRepository; import static java.time.Clock.systemUTC; @@ -50,6 +56,28 @@ public EnterpriseAuthManagerFactory() @Override public AuthManager newInstance( Config config, LogProvider logProvider ) + { + FileUserRealm internalRealm = createInternalRealm( config, logProvider ); + + List realms = new ArrayList<>( 2 ); + + realms.add( internalRealm ); + + if ( config.get( SecuritySettings.ldap_auth_enabled ) ) + { + realms.add( new LdapRealm( config ) ); + } + + if ( config.get( SecuritySettings.external_auth_enabled ) ) + { + + // TODO: Load pluggable realms + } + + return new MultiRealmAuthManager( internalRealm, realms ); + } + + private FileUserRealm createInternalRealm( Config config, LogProvider logProvider ) { // Resolve auth store file names File authStoreDir = config.get( DatabaseManagementSystemSettings.auth_store_directory ); @@ -71,7 +99,9 @@ public AuthManager newInstance( Config config, LogProvider logProvider ) final PasswordPolicy passwordPolicy = new BasicPasswordPolicy(); - return new EnterpriseAuthManager( userRepository, roleRepository, passwordPolicy, systemUTC(), - config.get( GraphDatabaseSettings.auth_enabled ) ); + AuthenticationStrategy authenticationStrategy = new RateLimitedAuthenticationStrategy( systemUTC(), 3 ); + + return new FileUserRealm( userRepository, roleRepository, passwordPolicy, authenticationStrategy, + true ); } } diff --git a/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/EnterpriseUserManager.java b/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/EnterpriseUserManager.java new file mode 100644 index 0000000000000..c594277220046 --- /dev/null +++ b/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/EnterpriseUserManager.java @@ -0,0 +1,39 @@ +/* + * 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 Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.server.security.enterprise.auth; + +import java.io.IOException; +import java.util.Set; + +import org.neo4j.kernel.api.security.AuthSubject; +import org.neo4j.kernel.api.security.exception.IllegalCredentialsException; +import org.neo4j.server.security.auth.UserManager; + +public interface EnterpriseUserManager extends UserManager, RoleManager +{ + void setPassword( AuthSubject authSubject, String username, String password ) throws IOException, + IllegalCredentialsException; + + void suspendUser( String username ) throws IOException; + + void activateUser( String username ) throws IOException; + + Set getAllUsernames(); +} diff --git a/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/FileUserRealm.java b/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/FileUserRealm.java index e24a9152eff85..540c6dde3461f 100644 --- a/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/FileUserRealm.java +++ b/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/FileUserRealm.java @@ -25,6 +25,7 @@ import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.authc.credential.CredentialsMatcher; +import org.apache.shiro.authc.pam.UnsupportedTokenException; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.Permission; import org.apache.shiro.authz.SimpleAuthorizationInfo; @@ -43,8 +44,14 @@ import java.util.SortedSet; import java.util.TreeSet; +import org.neo4j.graphdb.security.AuthorizationViolationException; +import org.neo4j.kernel.api.security.AuthSubject; +import org.neo4j.kernel.api.security.AuthToken; import org.neo4j.kernel.api.security.exception.IllegalCredentialsException; +import org.neo4j.kernel.api.security.exception.InvalidAuthTokenException; +import org.neo4j.server.security.auth.AuthenticationStrategy; import org.neo4j.server.security.auth.Credential; +import org.neo4j.server.security.auth.PasswordPolicy; import org.neo4j.server.security.auth.User; import org.neo4j.server.security.auth.UserRepository; import org.neo4j.server.security.auth.exception.ConcurrentModificationException; @@ -52,11 +59,8 @@ /** * Shiro realm wrapping FileUserRepository */ -public class FileUserRealm extends AuthorizingRealm +public class FileUserRealm extends AuthorizingRealm implements NeoLifecycleRealm, EnterpriseUserManager { - private final UserRepository userRepository; - private final RoleRepository roleRepository; - /** * This flag is used in the same way as User.PASSWORD_CHANGE_REQUIRED, but it's * placed here because of user suspension not being a part of community edition @@ -94,32 +98,102 @@ public Collection resolvePermissionsInRole( String roleString ) } }; + private final UserRepository userRepository; + private final RoleRepository roleRepository; + private final PasswordPolicy passwordPolicy; + private final AuthenticationStrategy authenticationStrategy; + private final boolean authenticationEnabled; private final Map roles; - public FileUserRealm( UserRepository userRepository, RoleRepository roleRepository ) + public FileUserRealm( UserRepository userRepository, RoleRepository roleRepository, PasswordPolicy passwordPolicy, + AuthenticationStrategy authenticationStrategy, boolean authenticationEnabled ) { super(); this.userRepository = userRepository; this.roleRepository = roleRepository; + this.passwordPolicy = passwordPolicy; + this.authenticationStrategy = authenticationStrategy; + this.authenticationEnabled = authenticationEnabled; setCredentialsMatcher( credentialsMatcher ); setRolePermissionResolver( rolePermissionResolver ); roles = new PredefinedRolesBuilder().buildRoles(); } + @Override + public void initialize() throws Throwable + { + userRepository.init(); + roleRepository.init(); + } + + @Override + public void start() throws Throwable + { + userRepository.start(); + roleRepository.start(); + + if ( numberOfUsers() == 0 ) + { + newUser( "neo4j", "neo4j", true ); + + if ( numberOfRoles() == 0 ) + { + // Make the default user admin for now + newRole( PredefinedRolesBuilder.ADMIN, "neo4j" ); + } + } + } + + @Override + public void stop() throws Throwable + { + userRepository.stop(); + roleRepository.stop(); + } + + @Override + public void shutdown() throws Throwable + { + userRepository.shutdown(); + roleRepository.shutdown(); + setCacheManager( null ); + } + + @Override + public boolean supports( AuthenticationToken token ) + { + try + { + if ( token instanceof ShiroAuthToken ) + { + return ((ShiroAuthToken) token).getScheme().equals( "basic" ); + } + return false; + } + catch( InvalidAuthTokenException e ) + { + return false; + } + } + @Override protected AuthorizationInfo doGetAuthorizationInfo( PrincipalCollection principals ) throws AuthenticationException { - User user = userRepository.getUserByName( (String) principals.getPrimaryPrincipal() ); + String username = (String) getAvailablePrincipal( principals ); + if ( username == null ) + { + return null; + } - //TODO: perhaps a more informative message here - this happens if the user has been deleted + User user = userRepository.getUserByName( username ); if ( user == null ) { - throw new AuthenticationException( "User " + principals.getPrimaryPrincipal() + " does not exist" ); + return null; } - if ( user.passwordChangeRequired() || user.hasFlag( IS_SUSPENDED )) + if ( user.passwordChangeRequired() || user.hasFlag( IS_SUSPENDED ) ) { return new SimpleAuthorizationInfo(); } @@ -133,13 +207,22 @@ protected AuthorizationInfo doGetAuthorizationInfo( PrincipalCollection principa @Override protected AuthenticationInfo doGetAuthenticationInfo( AuthenticationToken token ) throws AuthenticationException { - UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token; + ShiroAuthToken shiroAuthToken = (ShiroAuthToken) token; - User user = userRepository.getUserByName( usernamePasswordToken.getUsername() ); + String username; + try + { + username = AuthToken.safeCast( AuthToken.PRINCIPAL, shiroAuthToken.getMap() ); + } + catch ( InvalidAuthTokenException e ) + { + throw new UnsupportedTokenException( e ); + } + User user = userRepository.getUserByName( username ); if ( user == null ) { - throw new AuthenticationException( "User " + usernamePasswordToken.getUsername() + " does not exist" ); + throw new AuthenticationException( "User " + username + " does not exist" ); } SimpleAuthenticationInfo authenticationInfo = @@ -169,7 +252,25 @@ int numberOfRoles() return roleRepository.numberOfRoles(); } - User newUser( String username, String initialPassword, boolean requirePasswordChange ) + @Override + public void setPassword( AuthSubject authSubject, String username, String password ) throws IOException, + IllegalCredentialsException + { + ShiroAuthSubject shiroAuthSubject = ShiroAuthSubject.castOrFail( authSubject ); + + if ( !shiroAuthSubject.doesUsernameMatch( username ) ) + { + throw new AuthorizationViolationException( "Invalid attempt to change the password for user " + username ); + } + + setUserPassword( username, password ); + + // This will invalidate the auth cache + authSubject.logout(); + } + + @Override + public User newUser( String username, String initialPassword, boolean requirePasswordChange ) throws IOException, IllegalCredentialsException { assertValidUsername( username ); @@ -200,7 +301,8 @@ RoleRecord newRole( String roleName, String... usernames ) throws IOException return role; } - void addUserToRole( String username, String roleName ) throws IOException + @Override + public void addUserToRole( String username, String roleName ) throws IOException { checkValidityOfUsernameAndRoleName( username, roleName ); @@ -233,7 +335,8 @@ void addUserToRole( String username, String roleName ) throws IOException clearCachedAuthorizationInfoForUser( username ); } - void removeUserFromRole( String username, String roleName ) throws IOException + @Override + public void removeUserFromRole( String username, String roleName ) throws IOException { checkValidityOfUsernameAndRoleName( username, roleName ); @@ -266,7 +369,8 @@ void removeUserFromRole( String username, String roleName ) throws IOException clearCachedAuthorizationInfoForUser( username ); } - boolean deleteUser( String username ) throws IOException + @Override + public boolean deleteUser( String username ) throws IOException { boolean result = false; synchronized ( this ) @@ -286,10 +390,47 @@ boolean deleteUser( String username ) throws IOException return result; } - void suspendUser( String username ) throws IOException + @Override + public User getUser( String username ) + { + return null; + } + + @Override + public void setUserPassword( String username, String password ) throws IOException, IllegalCredentialsException + { + User existingUser = userRepository.getUserByName( username ); + if ( existingUser == null ) + { + throw new IllegalCredentialsException( "User " + username + " does not exist" ); + } + + passwordPolicy.validatePassword( password ); + + if ( existingUser.credentials().matchesPassword( password ) ) + { + throw new IllegalCredentialsException( "Old password and new password cannot be the same." ); + } + + try + { + User updatedUser = existingUser.augment() + .withCredentials( Credential.forPassword( password ) ) + .withRequiredPasswordChange( false ) + .build(); + userRepository.update( existingUser, updatedUser ); + } catch ( ConcurrentModificationException e ) + { + // try again + setUserPassword( username, password ); + } + } + + @Override + public void suspendUser( String username ) throws IOException { // This method is not synchronized as it only modifies the UserRepository, which is synchronized in itself - // If user is modified between findByName and update, we get ConcurrentModificationException and try again + // If user is modified between getUserByName and update, we get ConcurrentModificationException and try again User user = userRepository.getUserByName( username ); if ( user == null ) { @@ -311,10 +452,11 @@ void suspendUser( String username ) throws IOException clearCacheForUser( username ); } - void activateUser( String username ) throws IOException + @Override + public void activateUser( String username ) throws IOException { // This method is not synchronized as it only modifies the UserRepository, which is synchronized in itself - // If user is modified between findByName and update, we get ConcurrentModificationException and try again + // If user is modified between getUserByName and update, we get ConcurrentModificationException and try again User user = userRepository.getUserByName( username ); if ( user == null ) { @@ -336,6 +478,39 @@ void activateUser( String username ) throws IOException clearCacheForUser( username ); } + @Override + public Set getAllRoleNames() + { + return roleRepository.getAllRoleNames(); + } + + @Override + public Set getRoleNamesForUser( String username ) + { + if ( userRepository.getUserByName( username ) == null ) + { + throw new IllegalArgumentException( "User " + username + " does not exist." ); + } + return roleRepository.getRoleNamesByUsername( username ); + } + + @Override + public Set getUsernamesForRole( String roleName ) + { + RoleRecord role = roleRepository.getRoleByName( roleName ); + if ( role == null ) + { + throw new IllegalArgumentException( "Role " + roleName + " does not exist." ); + } + return role.users(); + } + + @Override + public Set getAllUsernames() + { + return userRepository.getAllUsernames(); + } + User findUser( String username ) { return userRepository.getUserByName( username ); @@ -354,11 +529,6 @@ private void removeUserFromAllRoles( String username ) throws IOException } } - public Set getAllUsernames() - { - return userRepository.getAllUsernames(); - } - private void checkValidityOfUsernameAndRoleName( String username, String roleName ) throws IllegalArgumentException { assertValidUsername( username ); diff --git a/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/LdapRealm.java b/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/LdapRealm.java new file mode 100644 index 0000000000000..dcb44a6f80e6f --- /dev/null +++ b/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/LdapRealm.java @@ -0,0 +1,87 @@ +/* + * 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 Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.server.security.enterprise.auth; + +import org.apache.shiro.authz.AuthorizationInfo; +import org.apache.shiro.authz.Permission; +import org.apache.shiro.authz.SimpleAuthorizationInfo; +import org.apache.shiro.authz.SimpleRole; +import org.apache.shiro.authz.permission.RolePermissionResolver; +import org.apache.shiro.realm.ldap.JndiLdapContextFactory; +import org.apache.shiro.realm.ldap.JndiLdapRealm; +import org.apache.shiro.realm.ldap.LdapContextFactory; +import org.apache.shiro.subject.PrincipalCollection; + +import java.util.Collection; +import java.util.Collections; +import javax.naming.NamingException; + +import org.neo4j.kernel.configuration.Config; + +/** + * Shiro realm for LDAP based on configuration settings + */ +public class LdapRealm extends JndiLdapRealm +{ + public LdapRealm( Config config ) + { + super(); + setRolePermissionResolver( rolePermissionResolver ); + configureRealm( config ); + } + + @Override + protected AuthorizationInfo queryForAuthorizationInfo(PrincipalCollection principals, + LdapContextFactory ldapContextFactory) throws NamingException + { + // TODO: This is just temporary + return new SimpleAuthorizationInfo( Collections.singleton( PredefinedRolesBuilder.READER ) ); + } + + private final RolePermissionResolver rolePermissionResolver = new RolePermissionResolver() + { + @Override + public Collection resolvePermissionsInRole( String roleString ) + { + SimpleRole role = PredefinedRolesBuilder.roles.get( roleString ); + if ( role != null ) + { + return role.getPermissions(); + } + else + { + return Collections.emptyList(); + } + } + }; + + private void configureRealm( Config config ) + { + JndiLdapContextFactory contextFactory = new JndiLdapContextFactory(); + contextFactory.setUrl( "ldap://" + config.get( SecuritySettings.ldap_server ) ); + contextFactory.setAuthenticationMechanism( config.get( SecuritySettings.ldap_auth_mechanism ) ); + contextFactory.setReferral( config.get( SecuritySettings.ldap_referral ) ); + contextFactory.setSystemUsername( config.get( SecuritySettings.ldap_system_username ) ); + contextFactory.setSystemPassword( config.get( SecuritySettings.ldap_system_password ) ); + + setContextFactory( contextFactory ); + setUserDnTemplate( config.get( SecuritySettings.ldap_user_dn_template ) ); + } +} diff --git a/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/MultiRealmAuthManager.java b/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/MultiRealmAuthManager.java new file mode 100644 index 0000000000000..31903c2414ee9 --- /dev/null +++ b/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/MultiRealmAuthManager.java @@ -0,0 +1,151 @@ +/* + * 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 Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.server.security.enterprise.auth; + +import org.apache.shiro.authc.AuthenticationException; +import org.apache.shiro.authc.pam.UnsupportedTokenException; +import org.apache.shiro.cache.ehcache.EhCacheManager; +import org.apache.shiro.mgt.DefaultSecurityManager; +import org.apache.shiro.mgt.SecurityManager; +import org.apache.shiro.realm.CachingRealm; +import org.apache.shiro.realm.Realm; +import org.apache.shiro.subject.Subject; +import org.apache.shiro.util.Initializable; + +import java.util.Collection; +import java.util.Map; + +import org.neo4j.kernel.api.security.AuthSubject; +import org.neo4j.kernel.api.security.AuthenticationResult; +import org.neo4j.kernel.api.security.exception.InvalidAuthTokenException; + +public class MultiRealmAuthManager implements EnterpriseAuthManager +{ + private final EnterpriseUserManager userManager; + private final Collection realms; + private final SecurityManager securityManager; + private final EhCacheManager cacheManager; + + public MultiRealmAuthManager( EnterpriseUserManager userManager, Collection realms ) + { + this.userManager = userManager; + this.realms = realms; + securityManager = new DefaultSecurityManager( realms ); + + // TODO: This is a bit big for our current needs. + // Maybe MemoryConstrainedCacheManager is good enough if we do not need timeToLiveSeconds? + cacheManager = new EhCacheManager(); + } + + @Override + public AuthSubject login( Map authToken ) throws InvalidAuthTokenException + { + Subject subject = new Subject.Builder( securityManager ).buildSubject(); + + ShiroAuthToken token = new ShiroAuthToken( authToken ); + + AuthenticationResult result = AuthenticationResult.FAILURE; + + try + { + subject.login( token ); + result = AuthenticationResult.SUCCESS; + } + catch ( UnsupportedTokenException e ) + { + throw new InvalidAuthTokenException( e.getCause().getMessage() ); + } + catch ( AuthenticationException e ) + { + result = AuthenticationResult.FAILURE; + } + + return new ShiroAuthSubject( this, subject, result ); + } + + @Override + public void init() throws Throwable + { + cacheManager.init(); + + for ( Realm realm : realms ) + { + if ( realm instanceof Initializable ) + { + ((Initializable) realm).init(); + } + if ( realm instanceof CachingRealm ) + { + ((CachingRealm) realm).setCacheManager( cacheManager ); + } + if ( realm instanceof NeoLifecycleRealm ) + { + ((NeoLifecycleRealm) realm).initialize(); + } + } + } + + @Override + public void start() throws Throwable + { + for ( Realm realm : realms ) + { + if ( realm instanceof NeoLifecycleRealm ) + { + ((NeoLifecycleRealm) realm).start(); + } + } + } + + @Override + public void stop() throws Throwable + { + for ( Realm realm : realms ) + { + if ( realm instanceof NeoLifecycleRealm ) + { + ((NeoLifecycleRealm) realm).stop(); + } + } + } + + @Override + public void shutdown() throws Throwable + { + for ( Realm realm : realms ) + { + if ( realm instanceof CachingRealm ) + { + ((CachingRealm) realm).setCacheManager( null ); + } + if ( realm instanceof NeoLifecycleRealm ) + { + ((NeoLifecycleRealm) realm).shutdown(); + } + } + cacheManager.destroy(); + } + + @Override + public EnterpriseUserManager getUserManager() + { + return userManager; + } +} diff --git a/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/NeoLifecycleRealm.java b/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/NeoLifecycleRealm.java new file mode 100644 index 0000000000000..6005c265bfc8f --- /dev/null +++ b/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/NeoLifecycleRealm.java @@ -0,0 +1,28 @@ +/* + * 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 Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.server.security.enterprise.auth; + +public interface NeoLifecycleRealm +{ + void initialize() throws Throwable; + void start() throws Throwable; + void stop() throws Throwable; + void shutdown() throws Throwable; +} diff --git a/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/PredefinedRolesBuilder.java b/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/PredefinedRolesBuilder.java index ffc8736ed70b7..63578e6780554 100644 --- a/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/PredefinedRolesBuilder.java +++ b/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/PredefinedRolesBuilder.java @@ -32,8 +32,9 @@ public class PredefinedRolesBuilder implements RolesBuilder public static final String PUBLISHER = "publisher"; public static final String READER = "reader"; - @Override - public Map buildRoles() + public static final Map roles = staticBuildRoles(); + + public static Map staticBuildRoles() { Map roles = new LinkedHashMap<>( 4 ); @@ -56,4 +57,11 @@ public Map buildRoles() return roles; } + + @Override + public Map buildRoles() + { + return staticBuildRoles(); + } + } diff --git a/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/SecuritySettings.java b/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/SecuritySettings.java index 200633a99351b..72fd888f51520 100644 --- a/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/SecuritySettings.java +++ b/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/SecuritySettings.java @@ -23,6 +23,7 @@ import org.neo4j.graphdb.factory.Description; import org.neo4j.helpers.HostnamePort; +import static org.neo4j.kernel.configuration.Settings.BOOLEAN; import static org.neo4j.kernel.configuration.Settings.HOSTNAME_PORT; import static org.neo4j.kernel.configuration.Settings.NO_DEFAULT; import static org.neo4j.kernel.configuration.Settings.STRING; @@ -36,6 +37,14 @@ public class SecuritySettings { @SuppressWarnings( "unused" ) // accessed by reflection + @Description( "Enable auth via external authentication providers." ) + public static final Setting external_auth_enabled = + setting( "dbms.security.external_auth_enabled", BOOLEAN, "true" ); + + @Description( "Enable auth via a configurable LDAP authentication provider." ) + public static final Setting ldap_auth_enabled = + setting( "dbms.security.ldap.enabled", BOOLEAN, "true" ); + @Description( "Hostname and port of LDAP server to use for authentication and authorization." ) public static final Setting ldap_server = setting( "dbms.security.ldap.host", HOSTNAME_PORT, "0.0.0.0:389" ); @@ -48,11 +57,15 @@ public class SecuritySettings public static final Setting ldap_referral = setting( "dbms.security.ldap.referral", STRING, "follow" ); - @Description( "User DN template." ) public static final Setting ldap_user_dn_template = setting( "dbms.security.ldap.user_dn_template", STRING, "uid={0},ou=users,dc=example,dc=com" ); - // Or use "sAMAccountName={0}" as default? + @Description( "System username" ) + public static final Setting ldap_system_username = + setting( "dbms.security.ldap.system_username", STRING, NO_DEFAULT ); + @Description( "System password" ) + public static final Setting ldap_system_password = + setting( "dbms.security.ldap.system_password", STRING, NO_DEFAULT ); } diff --git a/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/ShiroAuthManager.java b/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/ShiroAuthManager.java index a53f62f01ac7e..64642e31f4857 100644 --- a/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/ShiroAuthManager.java +++ b/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/ShiroAuthManager.java @@ -24,6 +24,7 @@ import org.apache.shiro.cache.ehcache.EhCacheManager; import org.apache.shiro.mgt.DefaultSecurityManager; import org.apache.shiro.mgt.SecurityManager; +import org.apache.shiro.realm.Realm; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.subject.SimplePrincipalCollection; import org.apache.shiro.subject.Subject; @@ -33,7 +34,6 @@ import java.util.Map; import java.util.Set; -import org.neo4j.graphdb.security.AuthorizationViolationException; import org.neo4j.kernel.api.security.AuthSubject; import org.neo4j.kernel.api.security.AuthToken; import org.neo4j.kernel.api.security.AuthenticationResult; @@ -46,50 +46,50 @@ import org.neo4j.server.security.auth.User; import org.neo4j.server.security.auth.UserRepository; -public class ShiroAuthManager extends BasicAuthManager implements RoleManager +public class ShiroAuthManager extends BasicAuthManager implements EnterpriseAuthManager, EnterpriseUserManager { - private final SecurityManager securityManager; + protected SecurityManager securityManager; private final EhCacheManager cacheManager; private final FileUserRealm realm; private final RoleRepository roleRepository; public ShiroAuthManager( UserRepository userRepository, RoleRepository roleRepository, - PasswordPolicy passwordPolicy, AuthenticationStrategy authStrategy, boolean authEnabled ) + PasswordPolicy passwordPolicy, AuthenticationStrategy authStrategy ) { - super( userRepository, passwordPolicy, authStrategy, authEnabled ); + super( userRepository, passwordPolicy, authStrategy, true /* auth always enabled */ ); - realm = new FileUserRealm( userRepository, roleRepository ); + realm = new FileUserRealm( userRepository, roleRepository, passwordPolicy, authStrategy, true ); // TODO: Maybe MemoryConstrainedCacheManager is good enough if we do not need timeToLiveSeconds? // It would be one less dependency. // Or we could try to reuse Hazelcast which is already a dependency, but we would need to write some // glue code or use the HazelcastCacheManager from the Shiro Support repository. cacheManager = new EhCacheManager(); - securityManager = new DefaultSecurityManager( realm ); this.roleRepository = roleRepository; } - public ShiroAuthManager( UserRepository userRepository, RoleRepository roleRepository, - PasswordPolicy passwordPolicy, AuthenticationStrategy authStrategy ) + protected SecurityManager createSecurityManager() { - this( userRepository, roleRepository, passwordPolicy, authStrategy, true ); + return new DefaultSecurityManager( realm ); } public ShiroAuthManager( UserRepository userRepository, RoleRepository roleRepository, - PasswordPolicy passwordPolicy, Clock clock, boolean authEnabled ) + PasswordPolicy passwordPolicy, Clock clock ) { - this( userRepository, roleRepository, passwordPolicy, new RateLimitedAuthenticationStrategy( clock, 3 ), - authEnabled ); + this( userRepository, roleRepository, passwordPolicy, new RateLimitedAuthenticationStrategy( clock, 3 ) ); } @Override public void init() throws Throwable { + super.init(); roleRepository.init(); cacheManager.init(); realm.setCacheManager( cacheManager ); - realm.init(); + realm.initialize(); + + securityManager = createSecurityManager(); } @Override @@ -163,8 +163,7 @@ public ShiroAuthSubject login( Map authToken ) throws InvalidAuth // Start with an anonymous subject Subject subject = buildSubject( null ); - UsernamePasswordToken token = new UsernamePasswordToken( username, password ); - AuthenticationResult result = AuthenticationResult.SUCCESS; + AuthenticationResult result = AuthenticationResult.FAILURE; if ( !authStrategy.isAuthenticationPermitted( username ) ) { @@ -172,6 +171,7 @@ public ShiroAuthSubject login( Map authToken ) throws InvalidAuth } else { + UsernamePasswordToken token = new UsernamePasswordToken( username, password ); try { subject.login( token ); @@ -179,33 +179,24 @@ public ShiroAuthSubject login( Map authToken ) throws InvalidAuth { result = AuthenticationResult.PASSWORD_CHANGE_REQUIRED; } + else + { + result = AuthenticationResult.SUCCESS; + } } catch ( AuthenticationException e ) { result = AuthenticationResult.FAILURE; } + finally + { + token.clear(); + } authStrategy.updateWithAuthenticationResult( result, username ); } return new ShiroAuthSubject( this, subject, result ); } - @Override - public void setPassword( AuthSubject authSubject, String username, String password ) throws IOException, - IllegalCredentialsException - { - ShiroAuthSubject shiroAuthSubject = ShiroAuthSubject.castOrFail( authSubject ); - - if ( !shiroAuthSubject.doesUsernameMatch( username ) ) - { - throw new AuthorizationViolationException( "Invalid attempt to change the password for user " + username ); - } - - setUserPassword( username, password ); - - // This will invalidate the auth cache - authSubject.logout(); - } - @Override public void addUserToRole( String username, String roleName ) throws IOException { @@ -227,13 +218,15 @@ public boolean deleteUser( String username ) throws IOException return realm.deleteUser( username ); } - void suspendUser( String username ) throws IOException + @Override + public void suspendUser( String username ) throws IOException { assertAuthEnabled(); realm.suspendUser( username ); } - void activateUser( String username ) throws IOException + @Override + public void activateUser( String username ) throws IOException { assertAuthEnabled(); realm.activateUser( username ); @@ -243,38 +236,35 @@ void activateUser( String username ) throws IOException public Set getAllRoleNames() { assertAuthEnabled(); - return roleRepository.getAllRoleNames(); + return realm.getAllRoleNames(); } @Override public Set getRoleNamesForUser( String username ) { assertAuthEnabled(); - if (users.getUserByName( username ) == null) - { - throw new IllegalArgumentException( "User " + username + " does not exist." ); - } - return roleRepository.getRoleNamesByUsername( username ); + return realm.getRoleNamesForUser( username ); } @Override public Set getUsernamesForRole( String roleName ) { assertAuthEnabled(); - RoleRecord role = roleRepository.getRoleByName( roleName ); - if (role == null) - { - throw new IllegalArgumentException( "Role " + roleName + " does not exist." ); - } - return role.users(); + return realm.getUsernamesForRole( roleName ); } + @Override public Set getAllUsernames() { assertAuthEnabled(); return realm.getAllUsernames(); } + protected Realm getInternalRealm() + { + return realm; + } + private Subject buildSubject( String username ) { Subject.Builder subjectBuilder = new Subject.Builder( securityManager ); @@ -287,4 +277,10 @@ private Subject buildSubject( String username ) return subjectBuilder.buildSubject(); } + + @Override + public EnterpriseUserManager getUserManager() + { + return this; + } } diff --git a/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/ShiroAuthSubject.java b/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/ShiroAuthSubject.java index 0f6598efe89fc..80cf5f2b103fc 100644 --- a/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/ShiroAuthSubject.java +++ b/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/ShiroAuthSubject.java @@ -24,6 +24,7 @@ import java.io.IOException; import org.neo4j.kernel.api.security.AccessMode; +import org.neo4j.kernel.api.security.AuthManager; import org.neo4j.kernel.api.security.AuthSubject; import org.neo4j.kernel.api.security.AuthenticationResult; import org.neo4j.kernel.api.security.exception.IllegalCredentialsException; @@ -35,7 +36,7 @@ public class ShiroAuthSubject implements AuthSubject static final String READ_WRITE = "data:read,write"; static final String READ = "data:read"; - private final ShiroAuthManager authManager; + private final EnterpriseAuthManager authManager; private final Subject subject; private final AuthenticationResult authenticationResult; @@ -48,7 +49,7 @@ public static ShiroAuthSubject castOrFail( AuthSubject authSubject ) return (ShiroAuthSubject) authSubject; } - public ShiroAuthSubject( ShiroAuthManager authManager, Subject subject, AuthenticationResult authenticationResult ) + public ShiroAuthSubject( EnterpriseAuthManager authManager, Subject subject, AuthenticationResult authenticationResult ) { this.authManager = authManager; this.subject = subject; @@ -70,17 +71,12 @@ public AuthenticationResult getAuthenticationResult() @Override public void setPassword( String password ) throws IOException, IllegalCredentialsException { - authManager.setPassword( this, (String) subject.getPrincipal(), password ); + authManager.getUserManager().setPassword( this, (String) subject.getPrincipal(), password ); } - public RoleManager getRoleManager() + public EnterpriseUserManager getUserManager() { - return authManager; - } - - public ShiroAuthManager getUserManager() - { - return authManager; + return authManager.getUserManager(); } public boolean isAdmin() diff --git a/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/ShiroAuthToken.java b/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/ShiroAuthToken.java new file mode 100644 index 0000000000000..2643d51b005b5 --- /dev/null +++ b/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/ShiroAuthToken.java @@ -0,0 +1,59 @@ +/* + * 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 Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.server.security.enterprise.auth; + +import org.apache.shiro.authc.AuthenticationToken; + +import java.util.Map; + +import org.neo4j.kernel.api.security.AuthToken; +import org.neo4j.kernel.api.security.exception.InvalidAuthTokenException; + +public class ShiroAuthToken implements AuthenticationToken +{ + private Map authToken; + + public ShiroAuthToken( Map authToken ) + { + this.authToken = authToken; + } + + @Override + public Object getPrincipal() + { + return authToken.get( AuthToken.PRINCIPAL ); + } + + @Override + public Object getCredentials() + { + return authToken.get( AuthToken.CREDENTIALS ); + } + + public String getScheme() throws InvalidAuthTokenException + { + return AuthToken.safeCast( AuthToken.SCHEME_KEY, authToken ); + } + + public Map getMap() + { + return authToken; + } +} diff --git a/enterprise/security/src/test/java/org/neo4j/server/security/enterprise/auth/AuthProcedureTestBase.java b/enterprise/security/src/test/java/org/neo4j/server/security/enterprise/auth/AuthProcedureTestBase.java index 2fa29af9f51b8..db13afe243ec1 100644 --- a/enterprise/security/src/test/java/org/neo4j/server/security/enterprise/auth/AuthProcedureTestBase.java +++ b/enterprise/security/src/test/java/org/neo4j/server/security/enterprise/auth/AuthProcedureTestBase.java @@ -76,8 +76,8 @@ public class AuthProcedureTestBase public void setUp() throws Throwable { db = (GraphDatabaseAPI) new TestEnterpriseGraphDatabaseFactory().newImpermanentDatabase(); - manager = new EnterpriseAuthManager( new InMemoryUserRepository(), new InMemoryRoleRepository(), - new BasicPasswordPolicy(), systemUTC(), true ); + manager = new ShiroAuthManager( new InMemoryUserRepository(), new InMemoryRoleRepository(), + new BasicPasswordPolicy(), systemUTC() ); manager.init(); manager.start(); manager.newUser( "noneSubject", "abc", false ); @@ -86,7 +86,7 @@ public void setUp() throws Throwable manager.newUser( "schemaSubject", "abc", false ); manager.newUser( "readWriteSubject", "abc", false ); manager.newUser( "readSubject", "123", false ); - // Currently admin role is created by default + // Currently admin, architect, publisher and reader roles are created by default manager.addUserToRole( "adminSubject", ADMIN ); manager.addUserToRole( "schemaSubject", ARCHITECT ); manager.addUserToRole( "readWriteSubject", PUBLISHER ); diff --git a/enterprise/security/src/test/java/org/neo4j/server/security/enterprise/auth/AuthProceduresTest.java b/enterprise/security/src/test/java/org/neo4j/server/security/enterprise/auth/AuthProceduresTest.java index c38e5265d3454..08c24739a9f21 100644 --- a/enterprise/security/src/test/java/org/neo4j/server/security/enterprise/auth/AuthProceduresTest.java +++ b/enterprise/security/src/test/java/org/neo4j/server/security/enterprise/auth/AuthProceduresTest.java @@ -42,7 +42,6 @@ // TODO: homogenize "'' does not exist" type error messages. In short, add quotes in the right places public class AuthProceduresTest extends AuthProcedureTestBase { - //---------- Change own password ----------- // Enterprise version of test in BuiltInProceduresIT.callChangePasswordWithAccessModeInDbmsMode. diff --git a/enterprise/security/src/test/java/org/neo4j/server/security/enterprise/auth/FileUserRealmTest.java b/enterprise/security/src/test/java/org/neo4j/server/security/enterprise/auth/FileUserRealmTest.java index 27022f985a134..323029380abbc 100644 --- a/enterprise/security/src/test/java/org/neo4j/server/security/enterprise/auth/FileUserRealmTest.java +++ b/enterprise/security/src/test/java/org/neo4j/server/security/enterprise/auth/FileUserRealmTest.java @@ -32,7 +32,11 @@ import java.util.Arrays; +import org.neo4j.kernel.api.security.AuthenticationResult; +import org.neo4j.server.security.auth.AuthenticationStrategy; +import org.neo4j.server.security.auth.BasicPasswordPolicy; import org.neo4j.server.security.auth.InMemoryUserRepository; +import org.neo4j.server.security.auth.PasswordPolicy; import org.neo4j.server.security.auth.User; import org.neo4j.server.security.auth.UserRepository; @@ -44,6 +48,8 @@ public class FileUserRealmTest { RoleRepository roleRepository; UserRepository userRepository; + PasswordPolicy passwordPolicy; + AuthenticationStrategy authenticationStrategy; private static final String USERNAME = "neo4j"; private static final String ROLE = "admin"; @@ -60,6 +66,27 @@ private void setup() throws Exception userRepository = new InMemoryUserRepository(); userRepository.create( new User.Builder().withName( USERNAME ).build() ); roleRepository.create( new RoleRecord.Builder().withName( ROLE ).build() ); + passwordPolicy = new BasicPasswordPolicy(); + authenticationStrategy = new AuthenticationStrategy() + { + @Override + public boolean isAuthenticationPermitted( String username ) + { + return true; + } + + @Override + public void updateWithAuthenticationResult( AuthenticationResult result, String username ) + { + } + + @Override + public AuthenticationResult authenticate( User user, String password ) + { + return AuthenticationResult.SUCCESS; + } + }; + } @Test @@ -92,7 +119,8 @@ public void addUserToRoleShouldBeAtomic() throws Exception // Create a code position for where we want to break in the main thread CodePosition codePosition = getCodePositionAfterCall( "addUserToRole", "getUserByName" ); - FileUserRealm realm = new FileUserRealm( userRepository, roleRepository ); + FileUserRealm realm = new FileUserRealm( userRepository, roleRepository, passwordPolicy, authenticationStrategy, + true ); // When RunResult result = InterleavedRunner.interleave( @@ -114,7 +142,8 @@ public void deleteUserShouldBeAtomic() throws Exception // Create a code position for where we want to break in the main thread CodePosition codePosition = getCodePositionAfterCall( "deleteUser", "getUserByName" ); - FileUserRealm realm = new FileUserRealm( userRepository, roleRepository ); + FileUserRealm realm = new FileUserRealm( userRepository, roleRepository, passwordPolicy, authenticationStrategy, + true ); // When RunResult result = InterleavedRunner.interleave( diff --git a/enterprise/security/src/test/java/org/neo4j/server/security/enterprise/auth/ShiroAuthManagerTest.java b/enterprise/security/src/test/java/org/neo4j/server/security/enterprise/auth/ShiroAuthManagerTest.java index a6bcce6f41ac0..5e739fde8c776 100644 --- a/enterprise/security/src/test/java/org/neo4j/server/security/enterprise/auth/ShiroAuthManagerTest.java +++ b/enterprise/security/src/test/java/org/neo4j/server/security/enterprise/auth/ShiroAuthManagerTest.java @@ -62,7 +62,7 @@ public void setUp() throws Throwable authStrategy = mock( AuthenticationStrategy.class ); when( authStrategy.isAuthenticationPermitted( anyString() ) ).thenReturn( true ); passwordPolicy = mock( PasswordPolicy.class ); - manager = new ShiroAuthManager( users, roles, passwordPolicy, authStrategy, true ); + manager = new ShiroAuthManager( users, roles, passwordPolicy, authStrategy ); manager.init(); } @@ -518,63 +518,6 @@ public void shouldHaveNoPermissionsAfterLogout() throws Throwable assertFalse( subject.allowsSchemaWrites() ); } - @Test - public void shouldThrowWhenAuthIsDisabled() throws Throwable - { - // Restart with auth disabled - manager.stop(); - manager.shutdown(); - manager = new ShiroAuthManager( users, roles, passwordPolicy, authStrategy, false ); - manager.start(); - - try - { - manager.login( authToken( "foo", "bar" ) ); - fail( "exception expected" ); - } catch ( IllegalStateException e ) - { - // expected - } - - try - { - manager.newUser( "foo", "bar", true ); - fail( "exception expected" ); - } catch ( IllegalStateException e ) - { - // expected - } - - try - { - manager.deleteUser( "foo" ); - fail( "exception expected" ); - } catch ( IllegalStateException e ) - { - // expected - } - - try - { - manager.getUser( "foo" ); - fail( "exception expected" ); - } catch ( IllegalStateException e ) - { - // expected - } - - try - { - manager.setUserPassword( "foo", "bar" ); - fail( "exception expected" ); - } catch ( IllegalStateException e ) - { - // expected - } - - assertTrue( users.numberOfUsers() == 0 ); - } - private User newUser( String userName, String password, boolean pwdChange ) { return new User.Builder( userName, Credential.forPassword( password )) diff --git a/enterprise/security/src/test/java/org/neo4j/server/security/enterprise/auth/integration/bolt/EnterpriseAuthenticationIT.java b/enterprise/security/src/test/java/org/neo4j/server/security/enterprise/auth/integration/bolt/EnterpriseAuthenticationIT.java index 3b6018a8c4086..a7a6cb0d32fed 100644 --- a/enterprise/security/src/test/java/org/neo4j/server/security/enterprise/auth/integration/bolt/EnterpriseAuthenticationIT.java +++ b/enterprise/security/src/test/java/org/neo4j/server/security/enterprise/auth/integration/bolt/EnterpriseAuthenticationIT.java @@ -19,15 +19,27 @@ */ package org.neo4j.server.security.enterprise.auth.integration.bolt; +import org.junit.Test; + import java.util.Map; import java.util.function.Consumer; import org.neo4j.bolt.v1.transport.integration.AuthenticationIT; +import org.neo4j.bolt.v1.transport.integration.TransportTestUtil; import org.neo4j.graphdb.config.Setting; import org.neo4j.graphdb.factory.GraphDatabaseSettings; +import org.neo4j.server.security.enterprise.auth.SecuritySettings; import org.neo4j.test.TestEnterpriseGraphDatabaseFactory; import org.neo4j.test.TestGraphDatabaseFactory; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.neo4j.bolt.v1.messaging.message.Messages.init; +import static org.neo4j.bolt.v1.messaging.message.Messages.pullAll; +import static org.neo4j.bolt.v1.messaging.message.Messages.run; +import static org.neo4j.bolt.v1.messaging.util.MessageMatchers.msgSuccess; +import static org.neo4j.bolt.v1.transport.integration.TransportTestUtil.eventuallyRecieves; +import static org.neo4j.helpers.collection.MapUtil.map; + public class EnterpriseAuthenticationIT extends AuthenticationIT { @Override @@ -42,6 +54,37 @@ protected Consumer, String>> getSettingsFunction() return settings -> { settings.put( GraphDatabaseSettings.auth_enabled, "true" ); settings.put( GraphDatabaseSettings.auth_manager, "enterprise-auth-manager" ); + // TODO: This is configuration for a temporary external ldap test server + settings.put( SecuritySettings.ldap_server, "ldap.forumsys.com:389" ); + settings.put( SecuritySettings.ldap_user_dn_template, "uid={0},dc=example,dc=com" ); + settings.put( SecuritySettings.ldap_system_username, "xcn=read-only-admin,dc=example,dc=com" ); + settings.put( SecuritySettings.ldap_system_password, "password" ); }; } + + @Test + public void shouldBeAbleToLoginWithLdap() throws Throwable + { + // TODO: Move this to separate external auth test class + + // When + client.connect( address ) + .send( TransportTestUtil.acceptedVersions( 1, 0, 0, 0 ) ) + .send( TransportTestUtil.chunk( + init( "TestClient/1.1", map( "principal", "einstein", + "credentials", "password", "scheme", "basic" ) ) ) ); + + // Then + assertThat( client, eventuallyRecieves( new byte[]{0, 0, 0, 1} ) ); + assertThat( client, eventuallyRecieves( msgSuccess() ) ); + + // When + client.send( TransportTestUtil.chunk( + run( "MATCH (n) RETURN n" ), + pullAll() ) ); + + // Then + assertThat( client, eventuallyRecieves( msgSuccess() ) ); + } + } diff --git a/enterprise/server-enterprise/pom.xml b/enterprise/server-enterprise/pom.xml index b2850f0efcc5b..e25fe6d83f603 100644 --- a/enterprise/server-enterprise/pom.xml +++ b/enterprise/server-enterprise/pom.xml @@ -74,6 +74,12 @@ neo4j-enterprise ${project.version} + + org.neo4j + neo4j-security-enterprise + ${project.version} + test + @@ -109,12 +115,6 @@ test-jar test - - org.neo4j - neo4j-security-enterprise - ${project.version} - test - org.neo4j neo4j-security-enterprise