diff --git a/community/security/src/main/java/org/neo4j/server/security/auth/BasicAuthManager.java b/community/security/src/main/java/org/neo4j/server/security/auth/BasicAuthManager.java index 4b751129bfbe..25b9e150dea4 100644 --- a/community/security/src/main/java/org/neo4j/server/security/auth/BasicAuthManager.java +++ b/community/security/src/main/java/org/neo4j/server/security/auth/BasicAuthManager.java @@ -41,7 +41,7 @@ * so the given UserRepository should not be added to another LifeSupport. *

*/ -public class BasicAuthManager implements AuthManager, UserManager +public class BasicAuthManager implements AuthManager, UserManager, UserManagerSupplier { protected final AuthenticationStrategy authStrategy; protected final UserRepository users; @@ -206,4 +206,10 @@ private void assertValidName( String name ) throw new IllegalArgumentException( "User name contains illegal characters. Please use simple ascii characters and numbers." ); } } + + @Override + public UserManager getUserManager() + { + return this; + } } diff --git a/community/security/src/main/java/org/neo4j/server/security/auth/RateLimitedAuthenticationStrategy.java b/community/security/src/main/java/org/neo4j/server/security/auth/RateLimitedAuthenticationStrategy.java index d8909e5619f7..8d0f163a620b 100644 --- a/community/security/src/main/java/org/neo4j/server/security/auth/RateLimitedAuthenticationStrategy.java +++ b/community/security/src/main/java/org/neo4j/server/security/auth/RateLimitedAuthenticationStrategy.java @@ -94,7 +94,7 @@ public AuthenticationResult authenticate( User user, String password) { AuthenticationMetadata authMetadata = authMetadataFor( user.name() ); - if ( !isAuthenticationPermitted( user.name() ) ) + if ( !authMetadata.authenticationPermitted() ) { return AuthenticationResult.TOO_MANY_ATTEMPTS; } diff --git a/community/security/src/main/java/org/neo4j/server/security/auth/UserManagerSupplier.java b/community/security/src/main/java/org/neo4j/server/security/auth/UserManagerSupplier.java new file mode 100644 index 000000000000..f374c235e276 --- /dev/null +++ b/community/security/src/main/java/org/neo4j/server/security/auth/UserManagerSupplier.java @@ -0,0 +1,25 @@ +/* + * 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 . + */ +package org.neo4j.server.security.auth; + +public interface UserManagerSupplier +{ + UserManager getUserManager(); +} diff --git a/community/server/src/main/java/org/neo4j/server/rest/dbms/UserService.java b/community/server/src/main/java/org/neo4j/server/rest/dbms/UserService.java index c8282363f7e7..4756059b72aa 100644 --- a/community/server/src/main/java/org/neo4j/server/rest/dbms/UserService.java +++ b/community/server/src/main/java/org/neo4j/server/rest/dbms/UserService.java @@ -22,6 +22,7 @@ import java.io.IOException; import java.security.Principal; import java.util.Map; +import java.util.function.Supplier; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.GET; import javax.ws.rs.POST; @@ -41,6 +42,7 @@ import org.neo4j.server.rest.transactional.error.Neo4jError; import org.neo4j.server.security.auth.User; import org.neo4j.server.security.auth.UserManager; +import org.neo4j.server.security.auth.UserManagerSupplier; import static javax.ws.rs.core.Response.Status.BAD_REQUEST; import static org.neo4j.server.rest.web.CustomStatusType.UNPROCESSABLE; @@ -56,11 +58,11 @@ public class UserService public UserService( @Context AuthManager authManager, @Context InputFormat input, @Context OutputFormat output ) { - if ( !(authManager instanceof UserManager) ) + if ( !(authManager instanceof UserManagerSupplier) ) { throw new IllegalArgumentException( "The provided auth manager is not capable of user management" ); } - this.userManager = (UserManager) authManager; + this.userManager = ((UserManagerSupplier) authManager).getUserManager(); this.input = input; this.output = output; } 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 777617f639cd..0962fa474cd1 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 @@ -155,7 +155,7 @@ public Stream listUsers() throws IllegalCredentialsException, IOExce throw new AuthorizationViolationException( PERMISSION_DENIED ); } EnterpriseUserManager userManager = shiroSubject.getUserManager(); - return shiroSubject.getUserManager().getAllUsernames().stream() + return userManager.getAllUsernames().stream() .map( u -> new UserResult( u, userManager.getRoleNamesForUser( u ) ) ); } 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 427ba96fbfd9..369bf461925e 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 @@ -63,13 +63,12 @@ public AuthManager newInstance( Config config, LogProvider logProvider ) realms.add( internalRealm ); - if ( config.get( SecuritySettings.ldap_auth_enabled ) ) - { - realms.add( new LdapRealm( config ) ); - } - if ( config.get( SecuritySettings.external_auth_enabled ) ) { + if ( config.get( SecuritySettings.ldap_auth_enabled ) ) + { + realms.add( new LdapRealm( config ) ); + } // TODO: Load pluggable realms } 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 540c6dde3461..f729f548fd2c 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 @@ -22,9 +22,11 @@ import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; -import org.apache.shiro.authc.SimpleAuthenticationInfo; -import org.apache.shiro.authc.UsernamePasswordToken; -import org.apache.shiro.authc.credential.CredentialsMatcher; +import org.apache.shiro.authc.DisabledAccountException; +import org.apache.shiro.authc.ExcessiveAttemptsException; +import org.apache.shiro.authc.IncorrectCredentialsException; +import org.apache.shiro.authc.UnknownAccountException; +import org.apache.shiro.authc.credential.AllowAllCredentialsMatcher; import org.apache.shiro.authc.pam.UnsupportedTokenException; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.Permission; @@ -47,6 +49,7 @@ 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; import org.neo4j.kernel.api.security.exception.IllegalCredentialsException; import org.neo4j.kernel.api.security.exception.InvalidAuthTokenException; import org.neo4j.server.security.auth.AuthenticationStrategy; @@ -59,7 +62,7 @@ /** * Shiro realm wrapping FileUserRepository */ -public class FileUserRealm extends AuthorizingRealm implements NeoLifecycleRealm, EnterpriseUserManager +public class FileUserRealm extends AuthorizingRealm implements ShiroRealmLifecycle, EnterpriseUserManager { /** * This flag is used in the same way as User.PASSWORD_CHANGE_REQUIRED, but it's @@ -67,20 +70,6 @@ public class FileUserRealm extends AuthorizingRealm implements NeoLifecycleRealm */ public static final String IS_SUSPENDED = "is_suspended"; - private final CredentialsMatcher credentialsMatcher = - ( AuthenticationToken token, AuthenticationInfo info ) -> - { - UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token; - String infoUserName = (String) info.getPrincipals().getPrimaryPrincipal(); - Credential infoCredential = (Credential) info.getCredentials(); - - boolean userNameMatches = infoUserName.equals( usernamePasswordToken.getUsername() ); - boolean credentialsMatches = - infoCredential.matchesPassword( new String( usernamePasswordToken.getPassword() ) ); - - return userNameMatches && credentialsMatches; - }; - private final RolePermissionResolver rolePermissionResolver = new RolePermissionResolver() { @Override @@ -115,7 +104,7 @@ public FileUserRealm( UserRepository userRepository, RoleRepository roleReposito this.passwordPolicy = passwordPolicy; this.authenticationStrategy = authenticationStrategy; this.authenticationEnabled = authenticationEnabled; - setCredentialsMatcher( credentialsMatcher ); + setCredentialsMatcher( new AllowAllCredentialsMatcher() ); setRolePermissionResolver( rolePermissionResolver ); roles = new PredefinedRolesBuilder().buildRoles(); @@ -210,9 +199,11 @@ protected AuthenticationInfo doGetAuthenticationInfo( AuthenticationToken token ShiroAuthToken shiroAuthToken = (ShiroAuthToken) token; String username; + String password; try { username = AuthToken.safeCast( AuthToken.PRINCIPAL, shiroAuthToken.getMap() ); + password = AuthToken.safeCast( AuthToken.CREDENTIALS, shiroAuthToken.getMap() ); } catch ( InvalidAuthTokenException e ) { @@ -222,24 +213,33 @@ protected AuthenticationInfo doGetAuthenticationInfo( AuthenticationToken token User user = userRepository.getUserByName( username ); if ( user == null ) { - throw new AuthenticationException( "User " + username + " does not exist" ); + throw new UnknownAccountException(); } - SimpleAuthenticationInfo authenticationInfo = - new SimpleAuthenticationInfo( user.name(), user.credentials(), getName() ); + AuthenticationResult result = authenticationStrategy.authenticate( user, password ); + + switch (result) + { + case FAILURE: + throw new IncorrectCredentialsException(); + case TOO_MANY_ATTEMPTS: + throw new ExcessiveAttemptsException(); + } // TODO: This will not work if AuthenticationInfo is cached, // unless you always do SecurityManager.logout properly (which will invalidate the cache) // For REST we may need to connect HttpSessionListener.sessionDestroyed with logout if ( user.hasFlag( FileUserRealm.IS_SUSPENDED ) ) { - // We don' want un-authenticated users to learn anything about user suspension state - // (normally this assertion is done by Shiro after we return from this method) - assertCredentialsMatch( token, authenticationInfo ); - throw new AuthenticationException( "User " + user.name() + " is suspended" ); + throw new DisabledAccountException( "User " + user.name() + " is suspended" ); } - return authenticationInfo; + if ( user.passwordChangeRequired() ) + { + result = AuthenticationResult.PASSWORD_CHANGE_REQUIRED; + } + + return new ShiroAuthenticationInfo( user.name(), user.credentials(), getName(), result ); } int numberOfUsers() @@ -393,7 +393,7 @@ public boolean deleteUser( String username ) throws IOException @Override public User getUser( String username ) { - return null; + return userRepository.getUserByName( username ); } @Override 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 index dcb44a6f80e6..ba8d20764685 100644 --- 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 @@ -45,6 +45,7 @@ public LdapRealm( Config config ) super(); setRolePermissionResolver( rolePermissionResolver ); configureRealm( config ); + // TODO: Set NeoSubjectFactory on the SecurityManager } @Override 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 index 31903c2414ee..f948d9d5c3cc 100644 --- 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 @@ -20,13 +20,14 @@ package org.neo4j.server.security.enterprise.auth; import org.apache.shiro.authc.AuthenticationException; +import org.apache.shiro.authc.ExcessiveAttemptsException; +import org.apache.shiro.authc.pam.FirstSuccessfulStrategy; +import org.apache.shiro.authc.pam.ModularRealmAuthenticator; 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; @@ -35,12 +36,13 @@ import org.neo4j.kernel.api.security.AuthSubject; import org.neo4j.kernel.api.security.AuthenticationResult; import org.neo4j.kernel.api.security.exception.InvalidAuthTokenException; +import org.neo4j.server.security.auth.UserManagerSupplier; -public class MultiRealmAuthManager implements EnterpriseAuthManager +public class MultiRealmAuthManager implements EnterpriseAuthManager, UserManagerSupplier { private final EnterpriseUserManager userManager; private final Collection realms; - private final SecurityManager securityManager; + private final DefaultSecurityManager securityManager; private final EhCacheManager cacheManager; public MultiRealmAuthManager( EnterpriseUserManager userManager, Collection realms ) @@ -48,8 +50,11 @@ public MultiRealmAuthManager( EnterpriseUserManager userManager, Collection authToken ) throws InvalidAuthTokenException { - Subject subject = new Subject.Builder( securityManager ).buildSubject(); + ShiroSubject subject; ShiroAuthToken token = new ShiroAuthToken( authToken ); - AuthenticationResult result = AuthenticationResult.FAILURE; - try { - subject.login( token ); - result = AuthenticationResult.SUCCESS; + subject = (ShiroSubject) securityManager.login( null, token ); } catch ( UnsupportedTokenException e ) { throw new InvalidAuthTokenException( e.getCause().getMessage() ); } + catch ( ExcessiveAttemptsException e ) + { + // NOTE: We only get this with single (internal) realm authentication + subject = new ShiroSubject( securityManager, AuthenticationResult.TOO_MANY_ATTEMPTS ); + } catch ( AuthenticationException e ) { - result = AuthenticationResult.FAILURE; + subject = new ShiroSubject( securityManager, AuthenticationResult.FAILURE ); } - return new ShiroAuthSubject( this, subject, result ); + return new ShiroAuthSubject( this, subject ); } @Override @@ -95,9 +102,9 @@ public void init() throws Throwable { ((CachingRealm) realm).setCacheManager( cacheManager ); } - if ( realm instanceof NeoLifecycleRealm ) + if ( realm instanceof ShiroRealmLifecycle ) { - ((NeoLifecycleRealm) realm).initialize(); + ((ShiroRealmLifecycle) realm).initialize(); } } } @@ -107,9 +114,9 @@ public void start() throws Throwable { for ( Realm realm : realms ) { - if ( realm instanceof NeoLifecycleRealm ) + if ( realm instanceof ShiroRealmLifecycle ) { - ((NeoLifecycleRealm) realm).start(); + ((ShiroRealmLifecycle) realm).start(); } } } @@ -119,9 +126,9 @@ public void stop() throws Throwable { for ( Realm realm : realms ) { - if ( realm instanceof NeoLifecycleRealm ) + if ( realm instanceof ShiroRealmLifecycle ) { - ((NeoLifecycleRealm) realm).stop(); + ((ShiroRealmLifecycle) realm).stop(); } } } @@ -135,9 +142,9 @@ public void shutdown() throws Throwable { ((CachingRealm) realm).setCacheManager( null ); } - if ( realm instanceof NeoLifecycleRealm ) + if ( realm instanceof ShiroRealmLifecycle ) { - ((NeoLifecycleRealm) realm).shutdown(); + ((ShiroRealmLifecycle) realm).shutdown(); } } cacheManager.destroy(); 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 72fd888f5152..313fce3d2a6f 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 @@ -39,11 +39,11 @@ public class SecuritySettings @Description( "Enable auth via external authentication providers." ) public static final Setting external_auth_enabled = - setting( "dbms.security.external_auth_enabled", BOOLEAN, "true" ); + setting( "dbms.security.external_auth_enabled", BOOLEAN, "false" ); - @Description( "Enable auth via a configurable LDAP authentication provider." ) + @Description( "Enable auth via a settings configurable LDAP authentication realm." ) public static final Setting ldap_auth_enabled = - setting( "dbms.security.ldap.enabled", BOOLEAN, "true" ); + setting( "dbms.security.ldap.enabled", BOOLEAN, "false" ); @Description( "Hostname and port of LDAP server to use for authentication and authorization." ) public static final Setting ldap_server = 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 64642e31f485..ef42ded69d65 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 @@ -23,11 +23,6 @@ import org.apache.shiro.authc.UsernamePasswordToken; 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; import java.io.IOException; import java.time.Clock; @@ -48,7 +43,7 @@ public class ShiroAuthManager extends BasicAuthManager implements EnterpriseAuthManager, EnterpriseUserManager { - protected SecurityManager securityManager; + protected DefaultSecurityManager securityManager; private final EhCacheManager cacheManager; private final FileUserRealm realm; private final RoleRepository roleRepository; @@ -67,7 +62,7 @@ public ShiroAuthManager( UserRepository userRepository, RoleRepository roleRepos this.roleRepository = roleRepository; } - protected SecurityManager createSecurityManager() + protected DefaultSecurityManager createSecurityManager() { return new DefaultSecurityManager( realm ); } @@ -90,6 +85,7 @@ public void init() throws Throwable realm.initialize(); securityManager = createSecurityManager(); + securityManager.setSubjectFactory( new ShiroSubjectFactory() ); } @Override @@ -161,7 +157,7 @@ public ShiroAuthSubject login( Map authToken ) throws InvalidAuth String password = AuthToken.safeCast( AuthToken.CREDENTIALS, authToken ); // Start with an anonymous subject - Subject subject = buildSubject( null ); + ShiroSubject subject = new ShiroSubject( securityManager, AuthenticationResult.FAILURE ); AuthenticationResult result = AuthenticationResult.FAILURE; @@ -174,7 +170,7 @@ public ShiroAuthSubject login( Map authToken ) throws InvalidAuth UsernamePasswordToken token = new UsernamePasswordToken( username, password ); try { - subject.login( token ); + subject = (ShiroSubject) securityManager.login( null, token ); if ( realm.findUser( username ).passwordChangeRequired() ) { result = AuthenticationResult.PASSWORD_CHANGE_REQUIRED; @@ -194,7 +190,7 @@ public ShiroAuthSubject login( Map authToken ) throws InvalidAuth } authStrategy.updateWithAuthenticationResult( result, username ); } - return new ShiroAuthSubject( this, subject, result ); + return new ShiroAuthSubject( this, subject ); } @Override @@ -260,24 +256,6 @@ public Set getAllUsernames() return realm.getAllUsernames(); } - protected Realm getInternalRealm() - { - return realm; - } - - private Subject buildSubject( String username ) - { - Subject.Builder subjectBuilder = new Subject.Builder( securityManager ); - - if ( username != null ) - { - PrincipalCollection identity = new SimplePrincipalCollection( username, realm.getName() ); - subjectBuilder = subjectBuilder.principals( identity ); - } - - return subjectBuilder.buildSubject(); - } - @Override public EnterpriseUserManager getUserManager() { 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 80cf5f2b103f..7e4866845473 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,11 +24,9 @@ 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; -import org.neo4j.server.security.auth.UserManager; public class ShiroAuthSubject implements AuthSubject { @@ -37,8 +35,7 @@ public class ShiroAuthSubject implements AuthSubject static final String READ = "data:read"; private final EnterpriseAuthManager authManager; - private final Subject subject; - private final AuthenticationResult authenticationResult; + private final ShiroSubject subject; public static ShiroAuthSubject castOrFail( AuthSubject authSubject ) { @@ -49,11 +46,10 @@ public static ShiroAuthSubject castOrFail( AuthSubject authSubject ) return (ShiroAuthSubject) authSubject; } - public ShiroAuthSubject( EnterpriseAuthManager authManager, Subject subject, AuthenticationResult authenticationResult ) + public ShiroAuthSubject( EnterpriseAuthManager authManager, ShiroSubject subject ) { this.authManager = authManager; this.subject = subject; - this.authenticationResult = authenticationResult; } @Override @@ -65,7 +61,7 @@ public void logout() @Override public AuthenticationResult getAuthenticationResult() { - return authenticationResult; + return subject.getAuthenticationResult(); } @Override diff --git a/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/ShiroAuthenticationInfo.java b/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/ShiroAuthenticationInfo.java new file mode 100644 index 000000000000..bcdfeb7cefa8 --- /dev/null +++ b/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/ShiroAuthenticationInfo.java @@ -0,0 +1,54 @@ +/* + * 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.AuthenticationInfo; +import org.apache.shiro.authc.SimpleAuthenticationInfo; + +import org.neo4j.kernel.api.security.AuthenticationResult; + +public class ShiroAuthenticationInfo extends SimpleAuthenticationInfo +{ + AuthenticationResult authenticationResult; + + public ShiroAuthenticationInfo( Object principal, Object credentials, String realmName, + AuthenticationResult authenticationResult ) + { + super( principal, credentials, realmName ); + this.authenticationResult = authenticationResult; + } + + public AuthenticationResult getAuthenticationResult() + { + return authenticationResult; + } + + @Override + public void merge( AuthenticationInfo info ) + { + super.merge( info ); + if ( info instanceof ShiroAuthenticationInfo ) + { + AuthenticationResult result = ((ShiroAuthenticationInfo) info).authenticationResult; + // TODO: Merge AuthenticationResult + authenticationResult = result; + } + } +} 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/ShiroRealmLifecycle.java similarity index 96% rename from enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/NeoLifecycleRealm.java rename to enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/ShiroRealmLifecycle.java index 6005c265bfc8..25ebbffdded6 100644 --- 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/ShiroRealmLifecycle.java @@ -19,7 +19,7 @@ */ package org.neo4j.server.security.enterprise.auth; -public interface NeoLifecycleRealm +public interface ShiroRealmLifecycle { void initialize() throws Throwable; void start() throws Throwable; diff --git a/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/ShiroSubject.java b/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/ShiroSubject.java new file mode 100644 index 000000000000..2e1a84d5b5fc --- /dev/null +++ b/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/ShiroSubject.java @@ -0,0 +1,50 @@ +/* + * 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.mgt.SecurityManager; +import org.apache.shiro.session.Session; +import org.apache.shiro.subject.PrincipalCollection; +import org.apache.shiro.subject.support.DelegatingSubject; + +import org.neo4j.kernel.api.security.AuthenticationResult; + +public class ShiroSubject extends DelegatingSubject +{ + private final AuthenticationResult authenticationResult; + + public ShiroSubject( SecurityManager securityManager, AuthenticationResult authenticationResult ) + { + super( securityManager ); + this.authenticationResult = authenticationResult; + } + + public ShiroSubject( PrincipalCollection principals, boolean authenticated, String host, Session session, + boolean sessionCreationEnabled, SecurityManager securityManager, AuthenticationResult authenticationResult ) + { + super( principals, authenticated, host, session, sessionCreationEnabled, securityManager ); + this.authenticationResult = authenticationResult; + } + + public AuthenticationResult getAuthenticationResult() + { + return authenticationResult; + } +} diff --git a/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/ShiroSubjectFactory.java b/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/ShiroSubjectFactory.java new file mode 100644 index 000000000000..9a6d679013ad --- /dev/null +++ b/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/ShiroSubjectFactory.java @@ -0,0 +1,45 @@ +/* + * 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.mgt.SecurityManager; +import org.apache.shiro.mgt.SubjectFactory; +import org.apache.shiro.session.Session; +import org.apache.shiro.subject.PrincipalCollection; +import org.apache.shiro.subject.Subject; +import org.apache.shiro.subject.SubjectContext; + +public class ShiroSubjectFactory implements SubjectFactory +{ + @Override + public Subject createSubject( SubjectContext context ) + { + SecurityManager securityManager = context.resolveSecurityManager(); + Session session = context.resolveSession(); + boolean sessionCreationEnabled = context.isSessionCreationEnabled(); + PrincipalCollection principals = context.resolvePrincipals(); + boolean authenticated = context.resolveAuthenticated(); + String host = context.resolveHost(); + ShiroAuthenticationInfo authcInfo = (ShiroAuthenticationInfo) context.getAuthenticationInfo(); + + return new ShiroSubject( principals, authenticated, host, session, sessionCreationEnabled, securityManager, + authcInfo.authenticationResult ); + } +} 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 a7a6cb0d32fe..bab10bf301af 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,27 +19,16 @@ */ 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 @@ -54,37 +43,7 @@ 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" ); + settings.put( SecuritySettings.external_auth_enabled, "false" ); }; } - - @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/security/src/test/java/org/neo4j/server/security/enterprise/auth/integration/bolt/LdapAuthenticationIT.java b/enterprise/security/src/test/java/org/neo4j/server/security/enterprise/auth/integration/bolt/LdapAuthenticationIT.java new file mode 100644 index 000000000000..8854ddd6db21 --- /dev/null +++ b/enterprise/security/src/test/java/org/neo4j/server/security/enterprise/auth/integration/bolt/LdapAuthenticationIT.java @@ -0,0 +1,128 @@ +/* + * 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.integration.bolt; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import java.util.Map; +import java.util.function.Consumer; + +import org.neo4j.bolt.v1.transport.integration.Neo4jWithSocket; +import org.neo4j.bolt.v1.transport.integration.TransportTestUtil; +import org.neo4j.bolt.v1.transport.socket.client.Connection; +import org.neo4j.bolt.v1.transport.socket.client.SecureSocketConnection; +import org.neo4j.function.Factory; +import org.neo4j.graphdb.config.Setting; +import org.neo4j.graphdb.factory.GraphDatabaseSettings; +import org.neo4j.helpers.HostnamePort; +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 LdapAuthenticationIT +{ + @Rule + public Neo4jWithSocket server = new Neo4jWithSocket( getTestGraphDatabaseFactory(), getSettingsFunction() ); + + protected TestGraphDatabaseFactory getTestGraphDatabaseFactory() + { + return new TestEnterpriseGraphDatabaseFactory(); + } + + protected Consumer, String>> getSettingsFunction() + { + return settings -> { + settings.put( GraphDatabaseSettings.auth_enabled, "true" ); + settings.put( GraphDatabaseSettings.auth_manager, "enterprise-auth-manager" ); + settings.put( SecuritySettings.external_auth_enabled, "true" ); + settings.put( SecuritySettings.ldap_auth_enabled, "true" ); + // 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" ); + }; + } + + public Factory cf = (Factory) SecureSocketConnection::new; + + public HostnamePort address = new HostnamePort( "localhost:7687" ); + + protected Connection client; + + + @Test + public void shouldBeAbleToLoginWithLdap() throws Throwable + { + // 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() ) ); + } + + @Before + public void setup() + { + this.client = cf.newInstance(); + } + + @After + public void teardown() throws Exception + { + if ( client != null ) + { + client.disconnect(); + } + } + + private void reconnect() throws Exception + { + if ( client != null ) + { + client.disconnect(); + } + this.client = cf.newInstance(); + } +} diff --git a/enterprise/server-enterprise/pom.xml b/enterprise/server-enterprise/pom.xml index e25fe6d83f60..3d17104c8437 100644 --- a/enterprise/server-enterprise/pom.xml +++ b/enterprise/server-enterprise/pom.xml @@ -78,7 +78,6 @@ org.neo4j neo4j-security-enterprise ${project.version} - test diff --git a/enterprise/server-enterprise/src/test/java/org/neo4j/server/rest/security/EnterpriseAuthenticationDocIT.java b/enterprise/server-enterprise/src/test/java/org/neo4j/server/rest/security/EnterpriseAuthenticationDocIT.java index 83347fa3535c..bccda823832a 100644 --- a/enterprise/server-enterprise/src/test/java/org/neo4j/server/rest/security/EnterpriseAuthenticationDocIT.java +++ b/enterprise/server-enterprise/src/test/java/org/neo4j/server/rest/security/EnterpriseAuthenticationDocIT.java @@ -25,11 +25,11 @@ import java.io.IOException; import java.util.stream.Collectors; import java.util.stream.Stream; - import javax.ws.rs.core.HttpHeaders; import org.neo4j.graphdb.factory.GraphDatabaseSettings; import org.neo4j.server.enterprise.helpers.EnterpriseServerBuilder; +import org.neo4j.server.security.enterprise.auth.SecuritySettings; import org.neo4j.test.server.HTTP; import static org.hamcrest.CoreMatchers.equalTo; @@ -44,6 +44,7 @@ public void startServer( boolean authEnabled ) throws IOException server = EnterpriseServerBuilder.server() .withProperty( GraphDatabaseSettings.auth_enabled.name(), Boolean.toString( authEnabled ) ) .withProperty( GraphDatabaseSettings.auth_manager.name(), "enterprise-auth-manager" ) + .withProperty( SecuritySettings.ldap_auth_enabled.name(), "false" ) .build(); server.start(); }