Skip to content

Commit

Permalink
Add authorization using static groups
Browse files Browse the repository at this point in the history
- Add a single group per user to the file user repository
- Authorize using a static mapping from group to role
- Add user access mode tests
- Add a separate shiro auth manager test file
  • Loading branch information
henriknyman committed May 25, 2016
1 parent 6068b6e commit 57def64
Show file tree
Hide file tree
Showing 18 changed files with 774 additions and 90 deletions.
@@ -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 AccountBuilder
{
UserAccount buildAccount(User user, String realmName);
}
Expand Up @@ -26,6 +26,10 @@
*/
public interface AuthenticationStrategy
{
boolean isAuthenticationPermitted( String username );

void updateWithAuthenticationResult( AuthenticationResult result, String username );

/**
* Verify a user by password
*/
Expand Down
Expand Up @@ -40,7 +40,7 @@
*/
public class BasicAuthManager implements AuthManager, UserManager
{
private final AuthenticationStrategy authStrategy;
protected final AuthenticationStrategy authStrategy;
private final UserRepository users;
protected final boolean authEnabled;

Expand Down
Expand Up @@ -23,7 +23,7 @@
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.ExpiredCredentialsException;
import org.apache.shiro.authc.SimpleAccount;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.authz.AuthorizationInfo;
Expand All @@ -39,35 +39,37 @@
*/
public class FileUserRealm extends AuthorizingRealm
{
private final FileUserRepository userRepository;
private final UserRepository userRepository;

private final CredentialsMatcher credentialsMatcher = new CredentialsMatcher()
{
@Override
public boolean doCredentialsMatch( AuthenticationToken token, AuthenticationInfo info )
{
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
String infoUserName = (String) info.getPrincipals().getPrimaryPrincipal();
Credential infoCredential = (Credential) info.getCredentials();
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() ) );
boolean userNameMatches = infoUserName.equals( usernamePasswordToken.getUsername() );
boolean credentialsMatches =
infoCredential.matchesPassword( new String( usernamePasswordToken.getPassword() ) );

return userNameMatches && credentialsMatches;
}
};
return userNameMatches && credentialsMatches;
};

public FileUserRealm( FileUserRepository userRepository )
private AccountBuilder accountBuilder;

public FileUserRealm( UserRepository userRepository )
{
this.userRepository = userRepository;
setCredentialsMatcher( credentialsMatcher );
accountBuilder = new PredefinedGroupsAccountBuilder();
}

@Override
protected AuthorizationInfo doGetAuthorizationInfo( PrincipalCollection principals )
{
return null;
User user = userRepository.findByName( (String) principals.getPrimaryPrincipal() );

return accountBuilder.buildAccount(user, getName());
}

@Override
Expand All @@ -77,27 +79,30 @@ protected AuthenticationInfo doGetAuthenticationInfo( AuthenticationToken token

User user = userRepository.findByName( usernamePasswordToken.getUsername() );

// 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)
// For REST we may need to connect HttpSessionListener.sessionDestroyed with logout
if (user.passwordChangeRequired())
{
throw new ExpiredCredentialsException("Password change required");
}

return new SimpleAccount( user.name(), user.credentials(), getName() );
return new SimpleAuthenticationInfo( user.name(), user.credentials(), getName() );
}

int numberOfUsers()
{
return userRepository.numberOfUsers();
}

User newUser( String username, String initialPassword, boolean requirePasswordChange ) throws
User newUser( String username, String group, String initialPassword, boolean requirePasswordChange ) throws
IOException, IllegalCredentialsException
{
assertValidName( username );

User user = new User.Builder()
.withName( username )
.withGroup( group )
.withCredentials( Credential.forPassword( initialPassword ) )
.withRequiredPasswordChange( requirePasswordChange )
.build();
Expand Down
Expand Up @@ -213,11 +213,6 @@ private void loadUsersFromFile() throws IOException
} catch ( UserSerialization.FormatException e )
{
log.error( "Ignoring authorization file \"%s\" (%s)", authFile.toAbsolutePath(), e.getMessage() );
loadedUsers = new ArrayList<>();
}

if ( loadedUsers == null )
{
throw new IllegalStateException( "Failed to read authentication file: " + authFile );
}

Expand Down
@@ -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 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;

import java.util.Collections;
import java.util.Set;

public class PredefinedGroupsAccountBuilder implements AccountBuilder
{
@Override
public UserAccount buildAccount( User user, String realmName )
{
Set<String> roleNames = getRolesForGroup( user.group() );
return new UserAccount( user.name(), user.credentials(), realmName, roleNames );
}

private Set<String> getRolesForGroup( String group )
{
switch ( group )
{
case "admin":
case "architect":
case ShiroAuthManager.DEFAULT_GROUP:
return Collections.singleton( "schema" );
case "publisher":
return Collections.singleton( "write" );
case "reader":
return Collections.singleton( "read" );
default:
return Collections.emptySet();
}
}
}
Expand Up @@ -23,6 +23,7 @@
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;

import org.neo4j.kernel.api.security.AuthenticationResult;

Expand Down Expand Up @@ -66,11 +67,34 @@ public RateLimitedAuthenticationStrategy( Clock clock, int maxFailedAttempts )
this.maxFailedAttempts = maxFailedAttempts;
}

public AuthenticationResult authenticate( User user, String password )
@Override
public boolean isAuthenticationPermitted( String username )
{
AuthenticationMetadata authMetadata = authMetadataFor( user );
AuthenticationMetadata authMetadata = authMetadataFor( username );

if ( !authMetadata.authenticationPermitted() )
return authMetadata.authenticationPermitted();
}

@Override
public void updateWithAuthenticationResult( AuthenticationResult result, String username )
{
AuthenticationMetadata authMetadata = authMetadataFor( username );
if ( result == AuthenticationResult.FAILURE )
{
authMetadata.authFailed();
}
else
{
authMetadata.authSuccess();
}
}

@Override
public AuthenticationResult authenticate( User user, String password)
{
AuthenticationMetadata authMetadata = authMetadataFor( user.name() );

if ( !isAuthenticationPermitted( user.name() ) )
{
return AuthenticationResult.TOO_MANY_ATTEMPTS;
}
Expand All @@ -86,9 +110,8 @@ public AuthenticationResult authenticate( User user, String password )
}
}

private AuthenticationMetadata authMetadataFor( User user )
private AuthenticationMetadata authMetadataFor( String username )
{
String username = user.name();
AuthenticationMetadata authMeta = authenticationData.get( username );

if ( authMeta == null )
Expand Down
Expand Up @@ -25,58 +25,101 @@
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ThreadContext;

import java.io.IOException;
import java.time.Clock;

import org.neo4j.kernel.api.security.AuthSubject;
import org.neo4j.kernel.api.security.AuthenticationResult;
import org.neo4j.kernel.api.security.exception.IllegalCredentialsException;

public class ShiroAuthManager extends BasicAuthManager
{
private final SecurityManager securityManager;
private final FileUserRealm realm;
static final String DEFAULT_GROUP = "neo4j";

public ShiroAuthManager( FileUserRepository userRepository, Clock clock, boolean authEnabled )
public ShiroAuthManager( UserRepository userRepository, AuthenticationStrategy authStrategy, boolean authEnabled )
{
super( userRepository, clock, authEnabled );
super( userRepository, authStrategy, authEnabled );

realm = new FileUserRealm( userRepository );
// : Do not forget realm.setCacheManager(...) before going into production...
securityManager = new DefaultSecurityManager( realm );
}

public ShiroAuthManager( UserRepository users, AuthenticationStrategy authStrategy )
{
this( users, authStrategy, true );
}

public ShiroAuthManager( UserRepository users, Clock clock, boolean authEnabled )
{
this( users, new RateLimitedAuthenticationStrategy( clock, 3 ), authEnabled );
}

@Override
public void start() throws Throwable
{
if ( authEnabled && realm.numberOfUsers() == 0 )
{
realm.newUser( "neo4j", "neo4j", true );
realm.newUser( "neo4j", DEFAULT_GROUP, "neo4j", true );
}
}

@Override
public AuthenticationResult authenticate( String username, String password )
{
AuthSubject subject = login( username, password );

return subject.getAuthenticationResult();
}

@Override
public User newUser( String username, String initialPassword, boolean requirePasswordChange ) throws IOException,
IllegalCredentialsException
{
assertAuthEnabled();
return realm.newUser( username, DEFAULT_GROUP, initialPassword, requirePasswordChange );
}

public User newUser( String username, String group, String initialPassword, boolean requirePasswordChange ) throws
IOException,
IllegalCredentialsException
{
assertAuthEnabled();
return realm.newUser( username, group, initialPassword, requirePasswordChange );
}

ThreadContext.bind(securityManager);
public AuthSubject login( String username, String password )
{
assertAuthEnabled();

Subject subject = new Subject.Builder(securityManager).buildSubject();
ThreadContext.bind(subject);

UsernamePasswordToken token = new UsernamePasswordToken( username, password );
try
{
subject.login( token );
}
catch ( ExpiredCredentialsException e )
AuthenticationResult result = AuthenticationResult.SUCCESS;

if ( !authStrategy.isAuthenticationPermitted( username ) )
{
return AuthenticationResult.PASSWORD_CHANGE_REQUIRED;
result = AuthenticationResult.TOO_MANY_ATTEMPTS;
}
catch ( AuthenticationException e )
else
{
return AuthenticationResult.FAILURE;
try
{
subject.login( token );
}
catch ( ExpiredCredentialsException e )
{
result = AuthenticationResult.PASSWORD_CHANGE_REQUIRED;
}
catch ( AuthenticationException e )
{
result = AuthenticationResult.FAILURE;
}
authStrategy.updateWithAuthenticationResult( result, username );
}
return AuthenticationResult.SUCCESS;
return new ShiroAuthSubject(this, subject, result);
}
}

0 comments on commit 57def64

Please sign in to comment.