Skip to content

Commit

Permalink
Recreate login logic in internal FileUserRealm
Browse files Browse the repository at this point in the history
The logic for the internal realm for handling password change required and
rate limiting had to move into the internal realm for the multi-realm
auth manager.

- Moved LDAP integration test to its own test class
- Added UserManagerSupplier interface to support the different ways that
auth managers can provide user management
- Changed multi-realm auth to use FirstSuccessfulStrategy

Note that rate limiting only works with a single (internal) realm
  • Loading branch information
henriknyman committed Jun 27, 2016
1 parent 8b47da6 commit 38286c9
Show file tree
Hide file tree
Showing 20 changed files with 391 additions and 141 deletions.
Expand Up @@ -41,7 +41,7 @@
* so the given UserRepository should not be added to another LifeSupport. * so the given UserRepository should not be added to another LifeSupport.
* </p> * </p>
*/ */
public class BasicAuthManager implements AuthManager, UserManager public class BasicAuthManager implements AuthManager, UserManager, UserManagerSupplier
{ {
protected final AuthenticationStrategy authStrategy; protected final AuthenticationStrategy authStrategy;
protected final UserRepository users; protected final UserRepository users;
Expand Down Expand Up @@ -206,4 +206,10 @@ private void assertValidName( String name )
throw new IllegalArgumentException( "User name contains illegal characters. Please use simple ascii characters and numbers." ); throw new IllegalArgumentException( "User name contains illegal characters. Please use simple ascii characters and numbers." );
} }
} }

@Override
public UserManager getUserManager()
{
return this;
}
} }
Expand Up @@ -94,7 +94,7 @@ public AuthenticationResult authenticate( User user, String password)
{ {
AuthenticationMetadata authMetadata = authMetadataFor( user.name() ); AuthenticationMetadata authMetadata = authMetadataFor( user.name() );


if ( !isAuthenticationPermitted( user.name() ) ) if ( !authMetadata.authenticationPermitted() )
{ {
return AuthenticationResult.TOO_MANY_ATTEMPTS; return AuthenticationResult.TOO_MANY_ATTEMPTS;
} }
Expand Down
@@ -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 <http://www.gnu.org/licenses/>.
*/
package org.neo4j.server.security.auth;

public interface UserManagerSupplier
{
UserManager getUserManager();
}
Expand Up @@ -22,6 +22,7 @@
import java.io.IOException; import java.io.IOException;
import java.security.Principal; import java.security.Principal;
import java.util.Map; import java.util.Map;
import java.util.function.Supplier;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.POST; import javax.ws.rs.POST;
Expand All @@ -41,6 +42,7 @@
import org.neo4j.server.rest.transactional.error.Neo4jError; import org.neo4j.server.rest.transactional.error.Neo4jError;
import org.neo4j.server.security.auth.User; import org.neo4j.server.security.auth.User;
import org.neo4j.server.security.auth.UserManager; 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 javax.ws.rs.core.Response.Status.BAD_REQUEST;
import static org.neo4j.server.rest.web.CustomStatusType.UNPROCESSABLE; import static org.neo4j.server.rest.web.CustomStatusType.UNPROCESSABLE;
Expand All @@ -56,11 +58,11 @@ public class UserService


public UserService( @Context AuthManager authManager, @Context InputFormat input, @Context OutputFormat output ) 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" ); 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.input = input;
this.output = output; this.output = output;
} }
Expand Down
Expand Up @@ -155,7 +155,7 @@ public Stream<UserResult> listUsers() throws IllegalCredentialsException, IOExce
throw new AuthorizationViolationException( PERMISSION_DENIED ); throw new AuthorizationViolationException( PERMISSION_DENIED );
} }
EnterpriseUserManager userManager = shiroSubject.getUserManager(); EnterpriseUserManager userManager = shiroSubject.getUserManager();
return shiroSubject.getUserManager().getAllUsernames().stream() return userManager.getAllUsernames().stream()
.map( u -> new UserResult( u, userManager.getRoleNamesForUser( u ) ) ); .map( u -> new UserResult( u, userManager.getRoleNamesForUser( u ) ) );
} }


Expand Down
Expand Up @@ -63,13 +63,12 @@ public AuthManager newInstance( Config config, LogProvider logProvider )


realms.add( internalRealm ); 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.external_auth_enabled ) )
{ {
if ( config.get( SecuritySettings.ldap_auth_enabled ) )
{
realms.add( new LdapRealm( config ) );
}


// TODO: Load pluggable realms // TODO: Load pluggable realms
} }
Expand Down
Expand Up @@ -22,9 +22,11 @@
import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authc.DisabledAccountException;
import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.authc.ExcessiveAttemptsException;
import org.apache.shiro.authc.credential.CredentialsMatcher; 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.authc.pam.UnsupportedTokenException;
import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.Permission; import org.apache.shiro.authz.Permission;
Expand All @@ -47,6 +49,7 @@
import org.neo4j.graphdb.security.AuthorizationViolationException; import org.neo4j.graphdb.security.AuthorizationViolationException;
import org.neo4j.kernel.api.security.AuthSubject; import org.neo4j.kernel.api.security.AuthSubject;
import org.neo4j.kernel.api.security.AuthToken; 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.IllegalCredentialsException;
import org.neo4j.kernel.api.security.exception.InvalidAuthTokenException; import org.neo4j.kernel.api.security.exception.InvalidAuthTokenException;
import org.neo4j.server.security.auth.AuthenticationStrategy; import org.neo4j.server.security.auth.AuthenticationStrategy;
Expand All @@ -59,28 +62,14 @@
/** /**
* Shiro realm wrapping FileUserRepository * 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 * 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 * placed here because of user suspension not being a part of community edition
*/ */
public static final String IS_SUSPENDED = "is_suspended"; 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() private final RolePermissionResolver rolePermissionResolver = new RolePermissionResolver()
{ {
@Override @Override
Expand Down Expand Up @@ -115,7 +104,7 @@ public FileUserRealm( UserRepository userRepository, RoleRepository roleReposito
this.passwordPolicy = passwordPolicy; this.passwordPolicy = passwordPolicy;
this.authenticationStrategy = authenticationStrategy; this.authenticationStrategy = authenticationStrategy;
this.authenticationEnabled = authenticationEnabled; this.authenticationEnabled = authenticationEnabled;
setCredentialsMatcher( credentialsMatcher ); setCredentialsMatcher( new AllowAllCredentialsMatcher() );
setRolePermissionResolver( rolePermissionResolver ); setRolePermissionResolver( rolePermissionResolver );


roles = new PredefinedRolesBuilder().buildRoles(); roles = new PredefinedRolesBuilder().buildRoles();
Expand Down Expand Up @@ -210,9 +199,11 @@ protected AuthenticationInfo doGetAuthenticationInfo( AuthenticationToken token
ShiroAuthToken shiroAuthToken = (ShiroAuthToken) token; ShiroAuthToken shiroAuthToken = (ShiroAuthToken) token;


String username; String username;
String password;
try try
{ {
username = AuthToken.safeCast( AuthToken.PRINCIPAL, shiroAuthToken.getMap() ); username = AuthToken.safeCast( AuthToken.PRINCIPAL, shiroAuthToken.getMap() );
password = AuthToken.safeCast( AuthToken.CREDENTIALS, shiroAuthToken.getMap() );
} }
catch ( InvalidAuthTokenException e ) catch ( InvalidAuthTokenException e )
{ {
Expand All @@ -222,24 +213,33 @@ protected AuthenticationInfo doGetAuthenticationInfo( AuthenticationToken token
User user = userRepository.getUserByName( username ); User user = userRepository.getUserByName( username );
if ( user == null ) if ( user == null )
{ {
throw new AuthenticationException( "User " + username + " does not exist" ); throw new UnknownAccountException();
} }


SimpleAuthenticationInfo authenticationInfo = AuthenticationResult result = authenticationStrategy.authenticate( user, password );
new SimpleAuthenticationInfo( user.name(), user.credentials(), getName() );
switch (result)
{
case FAILURE:
throw new IncorrectCredentialsException();
case TOO_MANY_ATTEMPTS:
throw new ExcessiveAttemptsException();
}


// TODO: This will not work if AuthenticationInfo is cached, // TODO: This will not work if AuthenticationInfo is cached,
// unless you always do SecurityManager.logout properly (which will invalidate the cache) // unless you always do SecurityManager.logout properly (which will invalidate the cache)
// For REST we may need to connect HttpSessionListener.sessionDestroyed with logout // For REST we may need to connect HttpSessionListener.sessionDestroyed with logout
if ( user.hasFlag( FileUserRealm.IS_SUSPENDED ) ) if ( user.hasFlag( FileUserRealm.IS_SUSPENDED ) )
{ {
// We don' want un-authenticated users to learn anything about user suspension state throw new DisabledAccountException( "User " + user.name() + " is suspended" );
// (normally this assertion is done by Shiro after we return from this method)
assertCredentialsMatch( token, authenticationInfo );
throw new AuthenticationException( "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() int numberOfUsers()
Expand Down Expand Up @@ -393,7 +393,7 @@ public boolean deleteUser( String username ) throws IOException
@Override @Override
public User getUser( String username ) public User getUser( String username )
{ {
return null; return userRepository.getUserByName( username );
} }


@Override @Override
Expand Down
Expand Up @@ -45,6 +45,7 @@ public LdapRealm( Config config )
super(); super();
setRolePermissionResolver( rolePermissionResolver ); setRolePermissionResolver( rolePermissionResolver );
configureRealm( config ); configureRealm( config );
// TODO: Set NeoSubjectFactory on the SecurityManager
} }


@Override @Override
Expand Down
Expand Up @@ -20,13 +20,14 @@
package org.neo4j.server.security.enterprise.auth; package org.neo4j.server.security.enterprise.auth;


import org.apache.shiro.authc.AuthenticationException; 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.authc.pam.UnsupportedTokenException;
import org.apache.shiro.cache.ehcache.EhCacheManager; import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.mgt.DefaultSecurityManager; import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.CachingRealm; import org.apache.shiro.realm.CachingRealm;
import org.apache.shiro.realm.Realm; import org.apache.shiro.realm.Realm;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Initializable; import org.apache.shiro.util.Initializable;


import java.util.Collection; import java.util.Collection;
Expand All @@ -35,49 +36,55 @@
import org.neo4j.kernel.api.security.AuthSubject; import org.neo4j.kernel.api.security.AuthSubject;
import org.neo4j.kernel.api.security.AuthenticationResult; import org.neo4j.kernel.api.security.AuthenticationResult;
import org.neo4j.kernel.api.security.exception.InvalidAuthTokenException; 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 EnterpriseUserManager userManager;
private final Collection<Realm> realms; private final Collection<Realm> realms;
private final SecurityManager securityManager; private final DefaultSecurityManager securityManager;
private final EhCacheManager cacheManager; private final EhCacheManager cacheManager;


public MultiRealmAuthManager( EnterpriseUserManager userManager, Collection<Realm> realms ) public MultiRealmAuthManager( EnterpriseUserManager userManager, Collection<Realm> realms )
{ {
this.userManager = userManager; this.userManager = userManager;
this.realms = realms; this.realms = realms;
securityManager = new DefaultSecurityManager( realms ); securityManager = new DefaultSecurityManager( realms );
securityManager.setSubjectFactory( new ShiroSubjectFactory() );
((ModularRealmAuthenticator) securityManager.getAuthenticator())
.setAuthenticationStrategy( new FirstSuccessfulStrategy() );


// TODO: This is a bit big for our current needs. // TODO: This is a bit big dependency for our current needs.
// Maybe MemoryConstrainedCacheManager is good enough if we do not need timeToLiveSeconds? // Maybe MemoryConstrainedCacheManager is good enough if we do not need timeToLiveSeconds?
cacheManager = new EhCacheManager(); cacheManager = new EhCacheManager();
} }


@Override @Override
public AuthSubject login( Map<String,Object> authToken ) throws InvalidAuthTokenException public AuthSubject login( Map<String,Object> authToken ) throws InvalidAuthTokenException
{ {
Subject subject = new Subject.Builder( securityManager ).buildSubject(); ShiroSubject subject;


ShiroAuthToken token = new ShiroAuthToken( authToken ); ShiroAuthToken token = new ShiroAuthToken( authToken );


AuthenticationResult result = AuthenticationResult.FAILURE;

try try
{ {
subject.login( token ); subject = (ShiroSubject) securityManager.login( null, token );
result = AuthenticationResult.SUCCESS;
} }
catch ( UnsupportedTokenException e ) catch ( UnsupportedTokenException e )
{ {
throw new InvalidAuthTokenException( e.getCause().getMessage() ); 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 ) catch ( AuthenticationException e )
{ {
result = AuthenticationResult.FAILURE; subject = new ShiroSubject( securityManager, AuthenticationResult.FAILURE );
} }


return new ShiroAuthSubject( this, subject, result ); return new ShiroAuthSubject( this, subject );
} }


@Override @Override
Expand All @@ -95,9 +102,9 @@ public void init() throws Throwable
{ {
((CachingRealm) realm).setCacheManager( cacheManager ); ((CachingRealm) realm).setCacheManager( cacheManager );
} }
if ( realm instanceof NeoLifecycleRealm ) if ( realm instanceof ShiroRealmLifecycle )
{ {
((NeoLifecycleRealm) realm).initialize(); ((ShiroRealmLifecycle) realm).initialize();
} }
} }
} }
Expand All @@ -107,9 +114,9 @@ public void start() throws Throwable
{ {
for ( Realm realm : realms ) for ( Realm realm : realms )
{ {
if ( realm instanceof NeoLifecycleRealm ) if ( realm instanceof ShiroRealmLifecycle )
{ {
((NeoLifecycleRealm) realm).start(); ((ShiroRealmLifecycle) realm).start();
} }
} }
} }
Expand All @@ -119,9 +126,9 @@ public void stop() throws Throwable
{ {
for ( Realm realm : realms ) for ( Realm realm : realms )
{ {
if ( realm instanceof NeoLifecycleRealm ) if ( realm instanceof ShiroRealmLifecycle )
{ {
((NeoLifecycleRealm) realm).stop(); ((ShiroRealmLifecycle) realm).stop();
} }
} }
} }
Expand All @@ -135,9 +142,9 @@ public void shutdown() throws Throwable
{ {
((CachingRealm) realm).setCacheManager( null ); ((CachingRealm) realm).setCacheManager( null );
} }
if ( realm instanceof NeoLifecycleRealm ) if ( realm instanceof ShiroRealmLifecycle )
{ {
((NeoLifecycleRealm) realm).shutdown(); ((ShiroRealmLifecycle) realm).shutdown();
} }
} }
cacheManager.destroy(); cacheManager.destroy();
Expand Down
Expand Up @@ -39,11 +39,11 @@ public class SecuritySettings


@Description( "Enable auth via external authentication providers." ) @Description( "Enable auth via external authentication providers." )
public static final Setting<Boolean> external_auth_enabled = public static final Setting<Boolean> 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<Boolean> ldap_auth_enabled = public static final Setting<Boolean> 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." ) @Description( "Hostname and port of LDAP server to use for authentication and authorization." )
public static final Setting<HostnamePort> ldap_server = public static final Setting<HostnamePort> ldap_server =
Expand Down

0 comments on commit 38286c9

Please sign in to comment.