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.
* </p>
*/
public class BasicAuthManager implements AuthManager, UserManager
public class BasicAuthManager implements AuthManager, UserManager, UserManagerSupplier
{
protected final AuthenticationStrategy authStrategy;
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." );
}
}

@Override
public UserManager getUserManager()
{
return this;
}
}
Expand Up @@ -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;
}
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.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;
Expand All @@ -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;
Expand All @@ -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;
}
Expand Down
Expand Up @@ -155,7 +155,7 @@ public Stream<UserResult> 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 ) ) );
}

Expand Down
Expand Up @@ -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
}
Expand Down
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -59,28 +62,14 @@
/**
* 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
* placed here because of user suspension not being a part of community edition
*/
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
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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 )
{
Expand All @@ -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()
Expand Down Expand Up @@ -393,7 +393,7 @@ public boolean deleteUser( String username ) throws IOException
@Override
public User getUser( String username )
{
return null;
return userRepository.getUserByName( username );
}

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

@Override
Expand Down
Expand Up @@ -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;
Expand All @@ -35,49 +36,55 @@
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<Realm> realms;
private final SecurityManager securityManager;
private final DefaultSecurityManager securityManager;
private final EhCacheManager cacheManager;

public MultiRealmAuthManager( EnterpriseUserManager userManager, Collection<Realm> realms )
{
this.userManager = userManager;
this.realms = 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?
cacheManager = new EhCacheManager();
}

@Override
public AuthSubject login( Map<String,Object> 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
Expand All @@ -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();
}
}
}
Expand All @@ -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();
}
}
}
Expand All @@ -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();
}
}
}
Expand All @@ -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();
Expand Down
Expand Up @@ -39,11 +39,11 @@ public class SecuritySettings

@Description( "Enable auth via external authentication providers." )
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 =
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<HostnamePort> ldap_server =
Expand Down

0 comments on commit 38286c9

Please sign in to comment.