From 7b18926434fd5c448ccf0a202ed73f73714be862 Mon Sep 17 00:00:00 2001 From: Olivia Ytterbrink Date: Tue, 6 Sep 2016 18:04:53 +0200 Subject: [PATCH] Reload auth and roles files on change --- .../security/auth/AbstractUserRepository.java | 14 ++ .../security/auth/FileUserRepository.java | 18 ++- .../server/security/auth/UserRepository.java | 12 ++ .../security/auth/UserSerialization.java | 1 + .../security/auth/InMemoryUserRepository.java | 12 ++ .../auth/AbstractRoleRepository.java | 18 +++ .../enterprise/auth/FileRoleRepository.java | 20 +++ .../auth/InternalFlatFileRealm.java | 52 +++++-- .../enterprise/auth/RoleRepository.java | 11 +- .../auth/InMemoryRoleRepository.java | 12 ++ .../auth/InternalFlatFileRealmIT.java | 131 ++++++++++++++++++ .../auth/RoleSerializationTest.java | 5 +- 12 files changed, 292 insertions(+), 14 deletions(-) create mode 100644 enterprise/security/src/test/java/org/neo4j/server/security/enterprise/auth/InternalFlatFileRealmIT.java diff --git a/community/security/src/main/java/org/neo4j/server/security/auth/AbstractUserRepository.java b/community/security/src/main/java/org/neo4j/server/security/auth/AbstractUserRepository.java index 15665cf438bfd..ac7504a23a194 100644 --- a/community/security/src/main/java/org/neo4j/server/security/auth/AbstractUserRepository.java +++ b/community/security/src/main/java/org/neo4j/server/security/auth/AbstractUserRepository.java @@ -42,6 +42,13 @@ public abstract class AbstractUserRepository extends LifecycleAdapter implements private final Pattern usernamePattern = Pattern.compile( "^[a-zA-Z0-9_]+$" ); + @Override + public void clear() + { + users.clear(); + usersByName.clear(); + } + @Override public User getUserByName( String username ) { @@ -82,6 +89,13 @@ public void create( User user ) throws InvalidArgumentsException, IOException */ protected abstract void saveUsers() throws IOException; + /** + * Override this in the implementing class to persist users + * + * @throws IOException + */ + abstract void loadUsers() throws IOException; + @Override public void update( User existingUser, User updatedUser ) throws ConcurrentModificationException, IOException, InvalidArgumentsException diff --git a/community/security/src/main/java/org/neo4j/server/security/auth/FileUserRepository.java b/community/security/src/main/java/org/neo4j/server/security/auth/FileUserRepository.java index 1d973b2785f21..a759af6fc00f3 100644 --- a/community/security/src/main/java/org/neo4j/server/security/auth/FileUserRepository.java +++ b/community/security/src/main/java/org/neo4j/server/security/auth/FileUserRepository.java @@ -54,8 +54,12 @@ public FileUserRepository( FileSystemAbstraction fileSystem, File file, LogProvi @Override public void start() throws Throwable { - users.clear(); - usersByName.clear(); + clear(); + loadUsers(); + } + + void loadUsers() throws IOException + { if ( fileSystem.fileExists( authFile ) ) { List loadedUsers; @@ -70,6 +74,7 @@ public void start() throws Throwable throw new IllegalStateException( "Failed to read authentication file: " + authFile ); } + clear(); users = loadedUsers; for ( User user : users ) { @@ -83,4 +88,13 @@ protected void saveUsers() throws IOException { serialization.saveRecordsToFile( fileSystem, authFile, users ); } + + @Override + public void reloadIfNeeded() throws IOException + { + if ( lastLoaded < fileSystem.lastModifiedTime( authFile ) ) + { + loadUsers(); + } + } } diff --git a/community/security/src/main/java/org/neo4j/server/security/auth/UserRepository.java b/community/security/src/main/java/org/neo4j/server/security/auth/UserRepository.java index fdba020ec2a46..70059f18b3250 100644 --- a/community/security/src/main/java/org/neo4j/server/security/auth/UserRepository.java +++ b/community/security/src/main/java/org/neo4j/server/security/auth/UserRepository.java @@ -31,6 +31,16 @@ */ public interface UserRepository extends Lifecycle { + /** + * Clears all cached user data. + */ + void clear(); + + /** + * Return the user associated with the given username. + * @param username the username + * @return the associated user, or null if no user exists + */ User getUserByName( String username ); /** @@ -66,4 +76,6 @@ void update( User existingUser, User updatedUser ) boolean isValidUsername( String username ); Set getAllUsernames(); + + void reloadIfNeeded() throws IOException; } diff --git a/community/security/src/main/java/org/neo4j/server/security/auth/UserSerialization.java b/community/security/src/main/java/org/neo4j/server/security/auth/UserSerialization.java index 5eb391a9fd7b8..3747cfbf79bd2 100644 --- a/community/security/src/main/java/org/neo4j/server/security/auth/UserSerialization.java +++ b/community/security/src/main/java/org/neo4j/server/security/auth/UserSerialization.java @@ -19,6 +19,7 @@ */ package org.neo4j.server.security.auth; +import org.neo4j.io.fs.FileSystemAbstraction; import org.neo4j.server.security.auth.exception.FormatException; import org.neo4j.string.HexString; diff --git a/community/security/src/test/java/org/neo4j/server/security/auth/InMemoryUserRepository.java b/community/security/src/test/java/org/neo4j/server/security/auth/InMemoryUserRepository.java index adb1fcb527834..03b6d0eb797d3 100644 --- a/community/security/src/test/java/org/neo4j/server/security/auth/InMemoryUserRepository.java +++ b/community/security/src/test/java/org/neo4j/server/security/auth/InMemoryUserRepository.java @@ -29,4 +29,16 @@ protected void saveUsers() throws IOException { // Nothing to do } + + @Override + void loadUsers() throws IOException + { + // Nothing to do + } + + @Override + public void reloadIfNeeded() + { + // Nothing to do + } } diff --git a/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/AbstractRoleRepository.java b/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/AbstractRoleRepository.java index 15cc8b5d39af1..0a736ee6db37a 100644 --- a/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/AbstractRoleRepository.java +++ b/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/AbstractRoleRepository.java @@ -32,6 +32,7 @@ import java.util.stream.Collectors; import org.neo4j.kernel.lifecycle.LifecycleAdapter; +import org.neo4j.server.security.auth.UserRepository; import org.neo4j.server.security.auth.exception.ConcurrentModificationException; public abstract class AbstractRoleRepository extends LifecycleAdapter implements RoleRepository @@ -47,6 +48,14 @@ public abstract class AbstractRoleRepository extends LifecycleAdapter implements private final Pattern roleNamePattern = Pattern.compile( "^[a-zA-Z0-9_]+$" ); + @Override + public void clear() + { + roles.clear(); + rolesByName.clear(); + rolesByUsername.clear(); + } + @Override public RoleRecord getRoleByName( String roleName ) { @@ -172,6 +181,8 @@ public synchronized boolean delete( RoleRecord role ) throws IOException */ protected abstract void saveRoles() throws IOException; + protected abstract void loadRoles() throws IOException; + @Override public synchronized int numberOfRoles() { @@ -208,6 +219,13 @@ public synchronized Set getAllRoleNames() return roles.stream().map( RoleRecord::name ).collect( Collectors.toSet() ); } + @Override + public boolean validateAgainst( UserRepository userRepository ) + { + return rolesByUsername.keySet().stream() + .allMatch( username -> userRepository.getUserByName( username ) != null ); + } + protected void populateUserMap( RoleRecord role ) { for ( String username : role.users() ) diff --git a/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/FileRoleRepository.java b/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/FileRoleRepository.java index 932f97dbacbe9..0f0dd90f53c3b 100644 --- a/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/FileRoleRepository.java +++ b/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/FileRoleRepository.java @@ -39,6 +39,8 @@ public class FileRoleRepository extends AbstractRoleRepository private final RoleSerialization serialization = new RoleSerialization(); private final FileSystemAbstraction fileSystem; + private long lastLoaded; + public FileRoleRepository( FileSystemAbstraction fileSystem, File file, LogProvider logProvider ) { this.roleFile = file; @@ -48,12 +50,20 @@ public FileRoleRepository( FileSystemAbstraction fileSystem, File file, LogProvi @Override public void start() throws Throwable + { + clear(); + loadRoles(); + } + + @Override + protected void loadRoles() throws IOException { if ( fileSystem.fileExists( roleFile ) ) { List loadedRoles; try { + lastLoaded = fileSystem.lastModifiedTime( roleFile ); loadedRoles = serialization.loadRecordsFromFile( fileSystem, roleFile ); } catch ( FormatException e ) @@ -62,6 +72,7 @@ public void start() throws Throwable throw new IllegalStateException( "Failed to read role file '" + roleFile + "'." ); } + clear(); roles = loadedRoles; for ( RoleRecord role : roles ) { @@ -77,4 +88,13 @@ protected void saveRoles() throws IOException { serialization.saveRecordsToFile( fileSystem, roleFile, roles ); } + + @Override + public void reloadIfNeeded() throws IOException + { + if ( lastLoaded < fileSystem.lastModifiedTime( roleFile ) ) + { + loadRoles(); + } + } } diff --git a/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/InternalFlatFileRealm.java b/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/InternalFlatFileRealm.java index 3c83540e8d751..86134f09ec082 100644 --- a/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/InternalFlatFileRealm.java +++ b/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/InternalFlatFileRealm.java @@ -45,6 +45,9 @@ import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; import org.neo4j.kernel.api.security.AuthToken; import org.neo4j.kernel.api.security.AuthenticationResult; @@ -70,6 +73,8 @@ public class InternalFlatFileRealm extends AuthorizingRealm implements RealmLife */ public static final String IS_SUSPENDED = "is_suspended"; + private int RELOAD_ATTEMPTS = 10; + private final RolePermissionResolver rolePermissionResolver = new RolePermissionResolver() { @Override @@ -96,8 +101,8 @@ public Collection resolvePermissionsInRole( String roleString ) private final Map roles; public InternalFlatFileRealm( UserRepository userRepository, RoleRepository roleRepository, - PasswordPolicy passwordPolicy, AuthenticationStrategy authenticationStrategy, - boolean authenticationEnabled, boolean authorizationEnabled ) + PasswordPolicy passwordPolicy, AuthenticationStrategy authenticationStrategy, boolean authenticationEnabled, + boolean authorizationEnabled, ScheduledExecutorService executorService ) { super(); @@ -113,6 +118,38 @@ public InternalFlatFileRealm( UserRepository userRepository, RoleRepository role setRolePermissionResolver( rolePermissionResolver ); roles = new PredefinedRolesBuilder().buildRoles(); + + executorService.scheduleAtFixedRate( () -> tryReload( RELOAD_ATTEMPTS ), 5, 5, TimeUnit.SECONDS ); + } + + public InternalFlatFileRealm( UserRepository userRepository, RoleRepository roleRepository, + PasswordPolicy passwordPolicy, AuthenticationStrategy authenticationStrategy, boolean authenticationEnabled, + boolean authorizationEnabled ) + { + this( userRepository, roleRepository, passwordPolicy, authenticationStrategy, authenticationEnabled, + authorizationEnabled, new ScheduledThreadPoolExecutor( 1 ) ); + } + + private void tryReload( int attemptLeft ) + { + if ( attemptLeft < 0 ) + { + throw new RuntimeException( "Unable to load valid flat file repositories!" ); + } + + try + { + userRepository.reloadIfNeeded(); + roleRepository.reloadIfNeeded(); + if ( !roleRepository.validateAgainst( userRepository ) ) + { + tryReload( attemptLeft - 1 ); + } + } + catch ( IOException ioe ) + { + tryReload( attemptLeft - 1 ); + } } public InternalFlatFileRealm( UserRepository userRepository, RoleRepository roleRepository, @@ -200,7 +237,7 @@ public boolean supports( AuthenticationToken token ) } return false; } - catch( InvalidAuthTokenException e ) + catch ( InvalidAuthTokenException e ) { return false; } @@ -473,7 +510,8 @@ public void setUserPassword( String username, String password, boolean requirePa .withRequiredPasswordChange( requirePasswordChange ) .build(); userRepository.update( existingUser, updatedUser ); - } catch ( ConcurrentModificationException e ) + } + catch ( ConcurrentModificationException e ) { // try again setUserPassword( username, password, requirePasswordChange ); @@ -578,8 +616,7 @@ private void assertValidUsername( String name ) throws InvalidArgumentsException if ( !userRepository.isValidUsername( name ) ) { throw new InvalidArgumentsException( - "User name '" + name + - "' contains illegal characters. Use simple ascii characters and numbers." ); + "User name '" + name + "' contains illegal characters. Use simple ascii characters and numbers." ); } } @@ -592,8 +629,7 @@ private void assertValidRoleName( String name ) throws InvalidArgumentsException if ( !roleRepository.isValidRoleName( name ) ) { throw new InvalidArgumentsException( - "Role name '" + name + - "' contains illegal characters. Use simple ascii characters and numbers." ); + "Role name '" + name + "' contains illegal characters. Use simple ascii characters and numbers." ); } } diff --git a/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/RoleRepository.java b/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/RoleRepository.java index 9dd9f4ac96075..a5e7189b3ca43 100644 --- a/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/RoleRepository.java +++ b/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/RoleRepository.java @@ -22,8 +22,8 @@ import java.io.IOException; import java.util.Set; -import org.neo4j.kernel.api.security.exception.InvalidArgumentsException; import org.neo4j.kernel.lifecycle.Lifecycle; +import org.neo4j.server.security.auth.UserRepository; import org.neo4j.server.security.auth.exception.ConcurrentModificationException; /** @@ -35,6 +35,11 @@ public interface RoleRepository extends Lifecycle Set getRoleNamesByUsername( String username ); + /** + * Clears all cached role data. + */ + void clear(); + /** * Create a role, given that the roles token is unique. * @@ -70,4 +75,8 @@ void removeUserFromAllRoles( String username ) throws ConcurrentModificationException, IOException; Set getAllRoleNames(); + + void reloadIfNeeded() throws IOException; + + boolean validateAgainst( UserRepository userRepository ); } diff --git a/enterprise/security/src/test/java/org/neo4j/server/security/enterprise/auth/InMemoryRoleRepository.java b/enterprise/security/src/test/java/org/neo4j/server/security/enterprise/auth/InMemoryRoleRepository.java index d2ff5305cd5b5..2c0136fec95bb 100644 --- a/enterprise/security/src/test/java/org/neo4j/server/security/enterprise/auth/InMemoryRoleRepository.java +++ b/enterprise/security/src/test/java/org/neo4j/server/security/enterprise/auth/InMemoryRoleRepository.java @@ -29,4 +29,16 @@ protected void saveRoles() throws IOException { // Nothing to do } + + @Override + protected void loadRoles() throws IOException + { + // Nothing to do + } + + @Override + public void reloadIfNeeded() + { + // Nothing to do + } } diff --git a/enterprise/security/src/test/java/org/neo4j/server/security/enterprise/auth/InternalFlatFileRealmIT.java b/enterprise/security/src/test/java/org/neo4j/server/security/enterprise/auth/InternalFlatFileRealmIT.java new file mode 100644 index 0000000000000..91461eef721be --- /dev/null +++ b/enterprise/security/src/test/java/org/neo4j/server/security/enterprise/auth/InternalFlatFileRealmIT.java @@ -0,0 +1,131 @@ +/* + * 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.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.io.Writer; +import java.nio.charset.Charset; +import java.nio.file.FileSystems; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import org.neo4j.io.fs.DelegateFileSystemAbstraction; +import org.neo4j.io.fs.FileSystemAbstraction; +import org.neo4j.logging.LogProvider; +import org.neo4j.logging.NullLogProvider; +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 org.neo4j.test.rule.fs.EphemeralFileSystemRule; +import org.neo4j.time.Clocks; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; + +public class InternalFlatFileRealmIT +{ + File userStoreFile; + File roleStoreFile; + + @Rule + public EphemeralFileSystemRule fsRule = new EphemeralFileSystemRule(); + + TestScheduledExecutorService executor = new TestScheduledExecutorService( 1 ); + LogProvider logProvider = NullLogProvider.getInstance(); + InternalFlatFileRealm realm; + + @Before + public void setup() throws Throwable + { + userStoreFile = new File( "dbms", "auth" ); + roleStoreFile = new File( "dbms", "roles" ); + final UserRepository userRepository = new FileUserRepository( fsRule.get(), userStoreFile, logProvider ); + final RoleRepository roleRepository = new FileRoleRepository( fsRule.get(), roleStoreFile, logProvider ); + final PasswordPolicy passwordPolicy = new BasicPasswordPolicy(); + AuthenticationStrategy authenticationStrategy = new RateLimitedAuthenticationStrategy( Clocks.systemClock(), 3 ); + + realm = new InternalFlatFileRealm( userRepository, roleRepository, passwordPolicy, authenticationStrategy, + true, true, executor ); + realm.init(); + realm.start(); + } + + @After + public void teardown() throws Throwable + { + realm.shutdown(); + } + + @Test + public void shouldReloadAuthFiles() throws Exception + { + overwrite( userStoreFile, + "Hanna:SHA-256,FE0056C37E,A543:\n" + + "Carol:SHA-256,FE0056C37E,A543:\n" + + "Mia:SHA-256,0E1FFFC23E,34A4:password_change_required\n" ); + + overwrite( roleStoreFile, + "admin:Mia\n" + + "publisher:Hanna,Carol\n" ); + + executor.scheduledRunnable.run(); + + assertThat( realm.getAllUsernames(), containsInAnyOrder( "Hanna", "Carol", "Mia" ) ); + assertThat( realm.getUsernamesForRole( "admin" ), containsInAnyOrder( "Mia" ) ); + assertThat( realm.getUsernamesForRole( "publisher" ), containsInAnyOrder( "Hanna", "Carol" ) ); + } + + protected void overwrite( File file, String newContents ) throws IOException + { + FileSystemAbstraction fs = fsRule.get(); + + fs.deleteFile( file ); + Writer w = fs.openAsWriter( file, Charset.defaultCharset(), false ); + w.write( newContents ); + w.close(); + } + + class TestScheduledExecutorService extends ScheduledThreadPoolExecutor + { + Runnable scheduledRunnable; + + public TestScheduledExecutorService( int corePoolSize ) + { + super( corePoolSize ); + } + + @Override + public ScheduledFuture scheduleAtFixedRate( Runnable r, long initialDelay, long delay, TimeUnit timeUnit ) + { + this.scheduledRunnable = r; + return null; + } + } +} diff --git a/enterprise/security/src/test/java/org/neo4j/server/security/enterprise/auth/RoleSerializationTest.java b/enterprise/security/src/test/java/org/neo4j/server/security/enterprise/auth/RoleSerializationTest.java index 3dde71c00c554..2d2ec4b688766 100644 --- a/enterprise/security/src/test/java/org/neo4j/server/security/enterprise/auth/RoleSerializationTest.java +++ b/enterprise/security/src/test/java/org/neo4j/server/security/enterprise/auth/RoleSerializationTest.java @@ -34,18 +34,17 @@ public class RoleSerializationTest { - private SortedSet steveBob; private SortedSet kellyMarie; @Before public void setUp() { - steveBob = new TreeSet(); + steveBob = new TreeSet<>(); steveBob.add( "Steve" ); steveBob.add( "Bob" ); - kellyMarie = new TreeSet(); + kellyMarie = new TreeSet<>(); kellyMarie.add( "Kelly" ); kellyMarie.add( "Marie" ); }