Clearing the realm caches on file updates

- Changed the behaviour of esusers realm so that whenever the `users` or the `users_roles` file are updated, the realm's cache expunges
- Changed LDAP realm such that when the `role_mapping.yml` file is updated, the realm's cache expunges

Also, cleaned up unused code (mainly around esusers and the different stores)

Original commit: elastic/x-pack-elasticsearch@3f093207da
This commit is contained in:
uboness 2014-10-28 17:25:20 +01:00
parent 3ab8f57f34
commit c5cbd58909
14 changed files with 192 additions and 401 deletions

View File

@ -7,13 +7,8 @@ package org.elasticsearch.shield.authc.esusers;
import org.elasticsearch.common.inject.util.Providers; import org.elasticsearch.common.inject.util.Providers;
import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.shield.authc.Realm;
import org.elasticsearch.shield.authc.support.UserPasswdStore;
import org.elasticsearch.shield.authc.support.UserRolesStore;
import org.elasticsearch.shield.support.AbstractShieldModule; import org.elasticsearch.shield.support.AbstractShieldModule;
import static org.elasticsearch.common.inject.name.Names.named;
/** /**
* *
*/ */
@ -30,8 +25,8 @@ public class ESUsersModule extends AbstractShieldModule.Node {
protected void configureNode() { protected void configureNode() {
if (enabled) { if (enabled) {
bind(ESUsersRealm.class).asEagerSingleton(); bind(ESUsersRealm.class).asEagerSingleton();
bind(UserPasswdStore.class).annotatedWith(named("file")).to(FileUserPasswdStore.class).asEagerSingleton(); bind(FileUserPasswdStore.class).asEagerSingleton();
bind(UserRolesStore.class).annotatedWith(named("file")).to(FileUserRolesStore.class).asEagerSingleton(); bind(FileUserRolesStore.class).asEagerSingleton();
} else { } else {
bind(ESUsersRealm.class).toProvider(Providers.<ESUsersRealm>of(null)); bind(ESUsersRealm.class).toProvider(Providers.<ESUsersRealm>of(null));
} }

View File

@ -5,17 +5,14 @@
*/ */
package org.elasticsearch.shield.authc.esusers; package org.elasticsearch.shield.authc.esusers;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.inject.name.Named;
import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.rest.RestController; import org.elasticsearch.rest.RestController;
import org.elasticsearch.rest.RestRequest; import org.elasticsearch.rest.RestRequest;
import org.elasticsearch.shield.User; import org.elasticsearch.shield.User;
import org.elasticsearch.shield.authc.AuthenticationToken; import org.elasticsearch.shield.authc.AuthenticationToken;
import org.elasticsearch.shield.authc.support.CachingUsernamePasswordRealm; import org.elasticsearch.shield.authc.support.CachingUsernamePasswordRealm;
import org.elasticsearch.shield.authc.support.UserPasswdStore; import org.elasticsearch.shield.authc.support.RefreshListener;
import org.elasticsearch.shield.authc.support.UserRolesStore;
import org.elasticsearch.shield.authc.support.UsernamePasswordToken; import org.elasticsearch.shield.authc.support.UsernamePasswordToken;
import org.elasticsearch.transport.TransportMessage; import org.elasticsearch.transport.TransportMessage;
@ -26,15 +23,18 @@ public class ESUsersRealm extends CachingUsernamePasswordRealm {
public static final String TYPE = "esusers"; public static final String TYPE = "esusers";
final UserPasswdStore userPasswdStore; final FileUserPasswdStore userPasswdStore;
final UserRolesStore userRolesStore; final FileUserRolesStore userRolesStore;
@Inject @Inject
public ESUsersRealm(Settings settings, @Named("file") UserPasswdStore userPasswdStore, public ESUsersRealm(Settings settings, FileUserPasswdStore userPasswdStore,
@Named("file") UserRolesStore userRolesStore, RestController restController) { FileUserRolesStore userRolesStore, RestController restController) {
super(settings); super(settings);
Listener listener = new Listener();
this.userPasswdStore = userPasswdStore; this.userPasswdStore = userPasswdStore;
userPasswdStore.addListener(listener);
this.userRolesStore = userRolesStore; this.userRolesStore = userRolesStore;
userRolesStore.addListener(listener);
restController.registerRelevantHeaders(UsernamePasswordToken.BASIC_AUTH_HEADER); restController.registerRelevantHeaders(UsernamePasswordToken.BASIC_AUTH_HEADER);
} }
@ -60,16 +60,17 @@ public class ESUsersRealm extends CachingUsernamePasswordRealm {
@Override @Override
protected User doAuthenticate(UsernamePasswordToken token) { protected User doAuthenticate(UsernamePasswordToken token) {
if (userPasswdStore == null) {
return null;
}
if (!userPasswdStore.verifyPassword(token.principal(), token.credentials())) { if (!userPasswdStore.verifyPassword(token.principal(), token.credentials())) {
return null; return null;
} }
String[] roles = Strings.EMPTY_ARRAY; String[] roles = userRolesStore.roles(token.principal());
if (userRolesStore != null) {
roles = userRolesStore.roles(token.principal());
}
return new User.Simple(token.principal(), roles); return new User.Simple(token.principal(), roles);
} }
class Listener implements RefreshListener {
@Override
public void onRefresh() {
expireAll();
}
}
} }

View File

@ -15,8 +15,8 @@ import org.elasticsearch.common.logging.ESLogger;
import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.env.Environment; import org.elasticsearch.env.Environment;
import org.elasticsearch.shield.authc.support.Hasher; import org.elasticsearch.shield.authc.support.Hasher;
import org.elasticsearch.shield.authc.support.RefreshListener;
import org.elasticsearch.shield.authc.support.SecuredString; import org.elasticsearch.shield.authc.support.SecuredString;
import org.elasticsearch.shield.authc.support.UserPasswdStore;
import org.elasticsearch.shield.plugin.ShieldPlugin; import org.elasticsearch.shield.plugin.ShieldPlugin;
import org.elasticsearch.watcher.FileChangesListener; import org.elasticsearch.watcher.FileChangesListener;
import org.elasticsearch.watcher.FileWatcher; import org.elasticsearch.watcher.FileWatcher;
@ -32,25 +32,26 @@ import java.nio.file.StandardOpenOption;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
/** /**
* *
*/ */
public class FileUserPasswdStore extends AbstractComponent implements UserPasswdStore { public class FileUserPasswdStore extends AbstractComponent {
private final Path file; private final Path file;
final Hasher hasher = Hasher.HTPASSWD; final Hasher hasher = Hasher.HTPASSWD;
private volatile ImmutableMap<String, char[]> esUsers; private volatile ImmutableMap<String, char[]> esUsers;
private final Listener listener; private CopyOnWriteArrayList<RefreshListener> listeners;
@Inject @Inject
public FileUserPasswdStore(Settings settings, Environment env, ResourceWatcherService watcherService) { public FileUserPasswdStore(Settings settings, Environment env, ResourceWatcherService watcherService) {
this(settings, env, watcherService, Listener.NOOP); this(settings, env, watcherService, null);
} }
FileUserPasswdStore(Settings settings, Environment env, ResourceWatcherService watcherService, Listener listener) { FileUserPasswdStore(Settings settings, Environment env, ResourceWatcherService watcherService, RefreshListener listener) {
super(settings); super(settings);
file = resolveFile(settings, env); file = resolveFile(settings, env);
esUsers = parseFile(file, logger); esUsers = parseFile(file, logger);
@ -60,10 +61,16 @@ public class FileUserPasswdStore extends AbstractComponent implements UserPasswd
FileWatcher watcher = new FileWatcher(file.getParent().toFile()); FileWatcher watcher = new FileWatcher(file.getParent().toFile());
watcher.addListener(new FileListener()); watcher.addListener(new FileListener());
watcherService.add(watcher, ResourceWatcherService.Frequency.HIGH); watcherService.add(watcher, ResourceWatcherService.Frequency.HIGH);
this.listener = listener; listeners = new CopyOnWriteArrayList<>();
if (listener != null) {
listeners.add(listener);
}
}
void addListener(RefreshListener listener) {
listeners.add(listener);
} }
@Override
public boolean verifyPassword(String username, SecuredString password) { public boolean verifyPassword(String username, SecuredString password) {
if (esUsers == null) { if (esUsers == null) {
return false; return false;
@ -139,20 +146,23 @@ public class FileUserPasswdStore extends AbstractComponent implements UserPasswd
} }
} }
protected void notifyRefresh() {
for (RefreshListener listener : listeners) {
listener.onRefresh();
}
}
private class FileListener extends FileChangesListener { private class FileListener extends FileChangesListener {
@Override @Override
public void onFileCreated(File file) { public void onFileCreated(File file) {
if (file.equals(FileUserPasswdStore.this.file.toFile())) { onFileChanged(file);
esUsers = parseFile(file.toPath(), logger);
listener.onRefresh();
}
} }
@Override @Override
public void onFileDeleted(File file) { public void onFileDeleted(File file) {
if (file.equals(FileUserPasswdStore.this.file.toFile())) { if (file.equals(FileUserPasswdStore.this.file.toFile())) {
esUsers = ImmutableMap.of(); esUsers = ImmutableMap.of();
listener.onRefresh(); notifyRefresh();
} }
} }
@ -160,19 +170,8 @@ public class FileUserPasswdStore extends AbstractComponent implements UserPasswd
public void onFileChanged(File file) { public void onFileChanged(File file) {
if (file.equals(FileUserPasswdStore.this.file.toFile())) { if (file.equals(FileUserPasswdStore.this.file.toFile())) {
esUsers = parseFile(file.toPath(), logger); esUsers = parseFile(file.toPath(), logger);
listener.onRefresh(); notifyRefresh();
} }
} }
} }
public static interface Listener {
final Listener NOOP = new Listener() {
@Override
public void onRefresh() {
}
};
void onRefresh();
}
} }

View File

@ -15,7 +15,7 @@ import org.elasticsearch.common.inject.internal.Nullable;
import org.elasticsearch.common.logging.ESLogger; import org.elasticsearch.common.logging.ESLogger;
import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.env.Environment; import org.elasticsearch.env.Environment;
import org.elasticsearch.shield.authc.support.UserRolesStore; import org.elasticsearch.shield.authc.support.RefreshListener;
import org.elasticsearch.shield.plugin.ShieldPlugin; import org.elasticsearch.shield.plugin.ShieldPlugin;
import org.elasticsearch.watcher.FileChangesListener; import org.elasticsearch.watcher.FileChangesListener;
import org.elasticsearch.watcher.FileWatcher; import org.elasticsearch.watcher.FileWatcher;
@ -29,12 +29,13 @@ import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.nio.file.StandardOpenOption; import java.nio.file.StandardOpenOption;
import java.util.*; import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.regex.Pattern; import java.util.regex.Pattern;
/** /**
* *
*/ */
public class FileUserRolesStore extends AbstractComponent implements UserRolesStore { public class FileUserRolesStore extends AbstractComponent {
private static final Pattern USERS_DELIM = Pattern.compile("\\s*,\\s*"); private static final Pattern USERS_DELIM = Pattern.compile("\\s*,\\s*");
@ -42,21 +43,28 @@ public class FileUserRolesStore extends AbstractComponent implements UserRolesSt
private volatile ImmutableMap<String, String[]> userRoles; private volatile ImmutableMap<String, String[]> userRoles;
private final Listener listener; private CopyOnWriteArrayList<RefreshListener> listeners;
@Inject @Inject
public FileUserRolesStore(Settings settings, Environment env, ResourceWatcherService watcherService) { public FileUserRolesStore(Settings settings, Environment env, ResourceWatcherService watcherService) {
this(settings, env, watcherService, Listener.NOOP); this(settings, env, watcherService, null);
} }
FileUserRolesStore(Settings settings, Environment env, ResourceWatcherService watcherService, Listener listener) { FileUserRolesStore(Settings settings, Environment env, ResourceWatcherService watcherService, RefreshListener listener) {
super(settings); super(settings);
file = resolveFile(settings, env); file = resolveFile(settings, env);
userRoles = parseFile(file, logger); userRoles = parseFile(file, logger);
FileWatcher watcher = new FileWatcher(file.getParent().toFile()); FileWatcher watcher = new FileWatcher(file.getParent().toFile());
watcher.addListener(new FileListener()); watcher.addListener(new FileListener());
watcherService.add(watcher, ResourceWatcherService.Frequency.HIGH); watcherService.add(watcher, ResourceWatcherService.Frequency.HIGH);
this.listener = listener; listeners = new CopyOnWriteArrayList<>();
if (listener != null) {
listeners.add(listener);
}
}
synchronized void addListener(RefreshListener listener) {
listeners.add(listener);
} }
public String[] roles(String username) { public String[] roles(String username) {
@ -175,20 +183,23 @@ public class FileUserRolesStore extends AbstractComponent implements UserRolesSt
} }
} }
public void notifyRefresh() {
for (RefreshListener listener : listeners) {
listener.onRefresh();
}
}
private class FileListener extends FileChangesListener { private class FileListener extends FileChangesListener {
@Override @Override
public void onFileCreated(File file) { public void onFileCreated(File file) {
if (file.equals(FileUserRolesStore.this.file.toFile())) { onFileChanged(file);
userRoles = parseFile(file.toPath(), logger);
listener.onRefresh();
}
} }
@Override @Override
public void onFileDeleted(File file) { public void onFileDeleted(File file) {
if (file.equals(FileUserRolesStore.this.file.toFile())) { if (file.equals(FileUserRolesStore.this.file.toFile())) {
userRoles = ImmutableMap.of(); userRoles = ImmutableMap.of();
listener.onRefresh(); notifyRefresh();
} }
} }
@ -196,19 +207,8 @@ public class FileUserRolesStore extends AbstractComponent implements UserRolesSt
public void onFileChanged(File file) { public void onFileChanged(File file) {
if (file.equals(FileUserRolesStore.this.file.toFile())) { if (file.equals(FileUserRolesStore.this.file.toFile())) {
userRoles = parseFile(file.toPath(), logger); userRoles = parseFile(file.toPath(), logger);
listener.onRefresh(); notifyRefresh();
} }
} }
} }
public static interface Listener {
static final Listener NOOP = new Listener() {
@Override
public void onRefresh() {
}
};
void onRefresh();
}
} }

View File

@ -13,6 +13,7 @@ import org.elasticsearch.common.logging.ESLogger;
import org.elasticsearch.common.settings.ImmutableSettings; import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.env.Environment; import org.elasticsearch.env.Environment;
import org.elasticsearch.shield.authc.support.RefreshListener;
import org.elasticsearch.shield.plugin.ShieldPlugin; import org.elasticsearch.shield.plugin.ShieldPlugin;
import org.elasticsearch.watcher.FileChangesListener; import org.elasticsearch.watcher.FileChangesListener;
import org.elasticsearch.watcher.FileWatcher; import org.elasticsearch.watcher.FileWatcher;
@ -27,6 +28,7 @@ import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.*; import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
/** /**
* This class loads and monitors the file defining the mappings of LDAP Group DNs to internal ES Roles. * This class loads and monitors the file defining the mappings of LDAP Group DNs to internal ES Roles.
@ -38,16 +40,17 @@ public class LdapGroupToRoleMapper extends AbstractComponent {
public static final String USE_UNMAPPED_GROUPS_AS_ROLES_SETTING = "unmapped_groups_as_roles"; public static final String USE_UNMAPPED_GROUPS_AS_ROLES_SETTING = "unmapped_groups_as_roles";
private final Path file; private final Path file;
private final Listener listener;
private final boolean useUnmappedGroupsAsRoles; private final boolean useUnmappedGroupsAsRoles;
private volatile ImmutableMap<LdapName, Set<String>> groupRoles; private volatile ImmutableMap<LdapName, Set<String>> groupRoles;
private CopyOnWriteArrayList<RefreshListener> listeners;
@Inject @Inject
public LdapGroupToRoleMapper(Settings settings, Environment env, ResourceWatcherService watcherService) { public LdapGroupToRoleMapper(Settings settings, Environment env, ResourceWatcherService watcherService) {
this(settings, env, watcherService, Listener.NOOP); this(settings, env, watcherService, null);
} }
LdapGroupToRoleMapper(Settings settings, Environment env, ResourceWatcherService watcherService, Listener listener) { LdapGroupToRoleMapper(Settings settings, Environment env, ResourceWatcherService watcherService, RefreshListener listener) {
super(settings); super(settings);
useUnmappedGroupsAsRoles = componentSettings.getAsBoolean(USE_UNMAPPED_GROUPS_AS_ROLES_SETTING, false); useUnmappedGroupsAsRoles = componentSettings.getAsBoolean(USE_UNMAPPED_GROUPS_AS_ROLES_SETTING, false);
file = resolveFile(componentSettings, env); file = resolveFile(componentSettings, env);
@ -55,7 +58,14 @@ public class LdapGroupToRoleMapper extends AbstractComponent {
FileWatcher watcher = new FileWatcher(file.getParent().toFile()); FileWatcher watcher = new FileWatcher(file.getParent().toFile());
watcher.addListener(new FileListener()); watcher.addListener(new FileListener());
watcherService.add(watcher, ResourceWatcherService.Frequency.HIGH); watcherService.add(watcher, ResourceWatcherService.Frequency.HIGH);
this.listener = listener; listeners = new CopyOnWriteArrayList<>();
if (listener != null) {
listeners.add(listener);
}
}
synchronized void addListener(RefreshListener listener) {
listeners.add(listener);
} }
public static Path resolveFile(Settings settings, Environment env) { public static Path resolveFile(Settings settings, Environment env) {
@ -121,6 +131,12 @@ public class LdapGroupToRoleMapper extends AbstractComponent {
return (String) groupLdapName.getRdn(groupLdapName.size() - 1).getValue(); return (String) groupLdapName.getRdn(groupLdapName.size() - 1).getValue();
} }
protected void notifyRefresh() {
for (RefreshListener listener : listeners) {
listener.onRefresh();
}
}
private class FileListener extends FileChangesListener { private class FileListener extends FileChangesListener {
@Override @Override
public void onFileCreated(File file) { public void onFileCreated(File file) {
@ -136,7 +152,7 @@ public class LdapGroupToRoleMapper extends AbstractComponent {
public void onFileChanged(File file) { public void onFileChanged(File file) {
if (file.equals(LdapGroupToRoleMapper.this.file.toFile())) { if (file.equals(LdapGroupToRoleMapper.this.file.toFile())) {
groupRoles = parseFile(file.toPath(), logger); groupRoles = parseFile(file.toPath(), logger);
listener.onRefresh(); notifyRefresh();
} }
} }
} }

View File

@ -13,6 +13,7 @@ import org.elasticsearch.shield.User;
import org.elasticsearch.shield.authc.AuthenticationToken; import org.elasticsearch.shield.authc.AuthenticationToken;
import org.elasticsearch.shield.authc.Realm; import org.elasticsearch.shield.authc.Realm;
import org.elasticsearch.shield.authc.support.CachingUsernamePasswordRealm; import org.elasticsearch.shield.authc.support.CachingUsernamePasswordRealm;
import org.elasticsearch.shield.authc.support.RefreshListener;
import org.elasticsearch.shield.authc.support.UsernamePasswordToken; import org.elasticsearch.shield.authc.support.UsernamePasswordToken;
import org.elasticsearch.transport.TransportMessage; import org.elasticsearch.transport.TransportMessage;
@ -34,6 +35,7 @@ public class LdapRealm extends CachingUsernamePasswordRealm implements Realm<Use
super(settings); super(settings);
this.connectionFactory = ldap; this.connectionFactory = ldap;
this.roleMapper = roleMapper; this.roleMapper = roleMapper;
roleMapper.addListener(new Listener());
restController.registerRelevantHeaders(UsernamePasswordToken.BASIC_AUTH_HEADER); restController.registerRelevantHeaders(UsernamePasswordToken.BASIC_AUTH_HEADER);
} }
@ -60,12 +62,17 @@ public class LdapRealm extends CachingUsernamePasswordRealm implements Realm<Use
try (LdapConnection session = connectionFactory.bind(token.principal(), token.credentials())) { try (LdapConnection session = connectionFactory.bind(token.principal(), token.credentials())) {
List<String> groupDNs = session.getGroups(); List<String> groupDNs = session.getGroups();
Set<String> roles = roleMapper.mapRoles(groupDNs); Set<String> roles = roleMapper.mapRoles(groupDNs);
User.Simple user = new User.Simple(token.principal(), roles.toArray(new String[roles.size()])); return new User.Simple(token.principal(), roles.toArray(new String[roles.size()]));
return user;
} catch (ShieldException e){ } catch (ShieldException e){
logger.info("Authentication Failed for user [{}]", e, token.principal()); logger.info("Authentication Failed for user [{}]", e, token.principal());
return null; return null;
} }
} }
class Listener implements RefreshListener {
@Override
public void onRefresh() {
expireAll();
}
}
} }

View File

@ -1,119 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.authc.support;
import org.elasticsearch.common.cache.CacheBuilder;
import org.elasticsearch.common.cache.CacheLoader;
import org.elasticsearch.common.cache.LoadingCache;
import org.elasticsearch.common.component.AbstractComponent;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.shield.authc.AuthenticationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
/**
* A base class for username/password stores that caches the users and their pwd hashes in-memory. The cache
* has an expiration time (defaults to 1hr, but it's configurable and can also be disabled by setting the cache
* ttl to 0).
*/
public abstract class CachingUserPasswdStore extends AbstractComponent implements UserPasswdStore {
private static final TimeValue DEFAULT_TTL = TimeValue.timeValueHours(1);
private final LoadingCache<String, PasswordHash> cache;
protected CachingUserPasswdStore(Settings settings) {
super(settings);
TimeValue ttl = componentSettings.getAsTime("cache.ttl", DEFAULT_TTL);
if (ttl.millis() > 0) {
cache = CacheBuilder.newBuilder()
.expireAfterWrite(ttl.getMillis(), TimeUnit.MILLISECONDS)
.build(new CacheLoader<String, PasswordHash>() {
@Override
public PasswordHash load(String username) throws Exception {
PasswordHash hash = passwordHash(username);
if (hash == null) {
throw new AuthenticationException("Authentication failed");
}
return hash;
}
});
} else {
cache = null;
}
}
protected final void expire(String username) {
if (cache != null) {
cache.invalidate(username);
}
}
protected final void expireAll() {
if (cache != null) {
cache.invalidateAll();
}
}
@Override
public final boolean verifyPassword(final String username, final SecuredString password) {
if (cache == null) {
return doVerifyPassword(username, password);
}
try {
PasswordHash hash = cache.get(username);
return hash.verify(password);
} catch (ExecutionException ee) {
return false;
}
}
/**
* Verifies the given password. Both the given username, and if the username is verified, then the
* given password. This method is used when the caching is disabled.
*/
protected abstract boolean doVerifyPassword(String username, SecuredString password);
protected abstract PasswordHash passwordHash(String username);
public static abstract class Writable extends CachingUserPasswdStore implements UserPasswdStore.Writable {
protected Writable(Settings settings) {
super(settings);
}
@Override
public final void store(String username, SecuredString password) {
doStore(username, password);
expire(username);
}
@Override
public void remove(String username) {
doRemove(username);
expire(username);
}
protected abstract void doStore(String username, SecuredString password);
protected abstract void doRemove(String username);
}
/**
* Represents a hash of a password.
*/
static interface PasswordHash {
boolean verify(SecuredString password);
}
}

View File

@ -1,113 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.authc.support;
import org.elasticsearch.common.cache.CacheBuilder;
import org.elasticsearch.common.cache.CacheLoader;
import org.elasticsearch.common.cache.LoadingCache;
import org.elasticsearch.common.component.AbstractComponent;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.shield.authc.AuthenticationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
/**
* A base class for user roles store that caches the roles per username in-memory. The cache
* has an expiration time (defaults to 1hr, but it's configurable and can also be disabled by setting the cache
* ttl to 0).
*/
public abstract class CachingUserRolesStore extends AbstractComponent implements UserRolesStore {
private static final TimeValue DEFAULT_TTL = TimeValue.timeValueHours(1);
private final LoadingCache<String, String[]> cache;
protected CachingUserRolesStore(Settings settings) {
super(settings);
TimeValue ttl = componentSettings.getAsTime("cache.ttl", DEFAULT_TTL);
if (ttl.millis() > 0) {
cache = CacheBuilder.newBuilder()
.expireAfterWrite(ttl.getMillis(), TimeUnit.MILLISECONDS)
.build(new CacheLoader<String, String[]>() {
@Override
public String[] load(String username) throws Exception {
return doLoadRoles(username);
}
});
} else {
cache = null;
}
}
protected final void expire(String username) {
if (cache != null) {
cache.invalidate(username);
}
}
protected final void expireAll() {
if (cache != null) {
cache.invalidateAll();
}
}
@Override
public String[] roles(final String username) {
if (cache == null) {
return doLoadRoles(username);
}
try {
return cache.get(username);
} catch (ExecutionException ee) {
throw new AuthenticationException("Could not load user roles", ee);
}
}
protected abstract String[] doLoadRoles(String username);
public static abstract class Writable extends CachingUserRolesStore implements UserRolesStore.Writable {
protected Writable(Settings settings) {
super(settings);
}
@Override
public void setRoles(String username, String... roles) {
doSetRoles(username, roles);
expire(username);
}
@Override
public void addRoles(String username, String... roles) {
doAddRoles(username, roles);
expire(username);
}
@Override
public void removeRoles(String username, String... roles) {
doRemoveRoles(username, roles);
expire(username);
}
@Override
public void removeUser(String username) {
doRemoveUser(username);
expire(username);
}
public abstract void doSetRoles(String username, String... roles);
public abstract void doAddRoles(String username, String... roles);
public abstract void doRemoveRoles(String username, String... roles);
public abstract void doRemoveUser(String username);
}
}

View File

@ -8,16 +8,14 @@ package org.elasticsearch.shield.authc.support;
/** /**
* *
*/ */
public interface UserPasswdStore { public interface RefreshListener {
boolean verifyPassword(String username, SecuredString password); static final RefreshListener NOOP = new RefreshListener() {
@Override
public void onRefresh() {
}
};
static interface Writable extends UserPasswdStore { void onRefresh();
void store(String username, SecuredString password);
void remove(String username);
}
} }

View File

@ -1,25 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.authc.support;
/**
*
*/
public interface UserRolesStore {
String[] roles(String username);
static interface Writable extends UserRolesStore {
void setRoles(String username, String... roles);
void addRoles(String username, String... roles);
void removeRoles(String username, String... roles);
void removeUser(String username);
}
}

View File

@ -17,14 +17,18 @@ import org.elasticsearch.client.IndicesAdminClient;
import org.elasticsearch.common.collect.ImmutableSet; import org.elasticsearch.common.collect.ImmutableSet;
import org.elasticsearch.common.settings.ImmutableSettings; import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.env.Environment;
import org.elasticsearch.rest.BaseRestHandler; import org.elasticsearch.rest.BaseRestHandler;
import org.elasticsearch.rest.RestChannel; import org.elasticsearch.rest.RestChannel;
import org.elasticsearch.rest.RestController; import org.elasticsearch.rest.RestController;
import org.elasticsearch.rest.RestRequest; import org.elasticsearch.rest.RestRequest;
import org.elasticsearch.shield.User; import org.elasticsearch.shield.User;
import org.elasticsearch.shield.authc.support.*; import org.elasticsearch.shield.authc.support.Hasher;
import org.elasticsearch.shield.authc.support.SecuredStringTests;
import org.elasticsearch.shield.authc.support.UsernamePasswordToken;
import org.elasticsearch.test.ElasticsearchTestCase; import org.elasticsearch.test.ElasticsearchTestCase;
import org.elasticsearch.transport.TransportRequest; import org.elasticsearch.transport.TransportRequest;
import org.elasticsearch.watcher.ResourceWatcherService;
import org.hamcrest.Matchers; import org.hamcrest.Matchers;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
@ -42,28 +46,31 @@ public class ESUsersRealmTests extends ElasticsearchTestCase {
private RestController restController; private RestController restController;
private Client client; private Client client;
private AdminClient adminClient; private AdminClient adminClient;
private FileUserPasswdStore userPasswdStore;
private FileUserRolesStore userRolesStore;
@Before @Before
public void init() throws Exception { public void init() throws Exception {
client = mock(Client.class); client = mock(Client.class);
adminClient = mock(AdminClient.class); adminClient = mock(AdminClient.class);
restController = mock(RestController.class); restController = mock(RestController.class);
userPasswdStore = mock(FileUserPasswdStore.class);
userRolesStore = mock(FileUserRolesStore.class);
} }
@Test @Test
public void testRestHeaderRegistration() { public void testRestHeaderRegistration() {
new ESUsersRealm(ImmutableSettings.EMPTY, mock(UserPasswdStore.class), mock(UserRolesStore.class), restController); new ESUsersRealm(ImmutableSettings.EMPTY, mock(FileUserPasswdStore.class), mock(FileUserRolesStore.class), restController);
verify(restController).registerRelevantHeaders(UsernamePasswordToken.BASIC_AUTH_HEADER); verify(restController).registerRelevantHeaders(UsernamePasswordToken.BASIC_AUTH_HEADER);
} }
@Test @Test
public void testAuthenticate() throws Exception { public void testAuthenticate() throws Exception {
Settings settings = ImmutableSettings.builder().build(); Settings settings = ImmutableSettings.builder().build();
MockUserPasswdStore userPasswdStore = new MockUserPasswdStore("user1", "test123"); when(userPasswdStore.verifyPassword("user1", SecuredStringTests.build("test123"))).thenReturn(true);
MockUserRolesStore userRolesStore = new MockUserRolesStore("user1", "role1", "role2"); when(userRolesStore.roles("user1")).thenReturn(new String[] { "role1", "role2" });
ESUsersRealm realm = new ESUsersRealm(settings, userPasswdStore, userRolesStore, restController); ESUsersRealm realm = new ESUsersRealm(settings, userPasswdStore, userRolesStore, restController);
User user = realm.authenticate(new UsernamePasswordToken("user1", SecuredStringTests.build("test123"))); User user = realm.authenticate(new UsernamePasswordToken("user1", SecuredStringTests.build("test123")));
assertTrue(userPasswdStore.called);
assertTrue(userRolesStore.called);
assertThat(user, notNullValue()); assertThat(user, notNullValue());
assertThat(user.principal(), equalTo("user1")); assertThat(user.principal(), equalTo("user1"));
assertThat(user.roles(), notNullValue()); assertThat(user.roles(), notNullValue());
@ -76,19 +83,40 @@ public class ESUsersRealmTests extends ElasticsearchTestCase {
Settings settings = ImmutableSettings.builder() Settings settings = ImmutableSettings.builder()
.put("shield.authc.esusers.cache.hash_algo", Hasher.values()[randomIntBetween(0, Hasher.values().length - 1)].name().toLowerCase(Locale.ROOT)) .put("shield.authc.esusers.cache.hash_algo", Hasher.values()[randomIntBetween(0, Hasher.values().length - 1)].name().toLowerCase(Locale.ROOT))
.build(); .build();
MockUserPasswdStore userPasswdStore = new MockUserPasswdStore("user1", "test123"); when(userPasswdStore.verifyPassword("user1", SecuredStringTests.build("test123"))).thenReturn(true);
MockUserRolesStore userRolesStore = new MockUserRolesStore("user1", "role1", "role2"); when(userRolesStore.roles("user1")).thenReturn(new String[] { "role1", "role2" });
ESUsersRealm realm = new ESUsersRealm(settings, userPasswdStore, userRolesStore, restController); ESUsersRealm realm = new ESUsersRealm(settings, userPasswdStore, userRolesStore, restController);
User user1 = realm.authenticate(new UsernamePasswordToken("user1", SecuredStringTests.build("test123"))); User user1 = realm.authenticate(new UsernamePasswordToken("user1", SecuredStringTests.build("test123")));
User user2 = realm.authenticate(new UsernamePasswordToken("user1", SecuredStringTests.build("test123"))); User user2 = realm.authenticate(new UsernamePasswordToken("user1", SecuredStringTests.build("test123")));
assertThat(user1, sameInstance(user2)); assertThat(user1, sameInstance(user2));
} }
public void testAuthenticate_Caching_Refresh() throws Exception {
userPasswdStore = spy(new UserPasswdStore());
userRolesStore = spy(new UserRolesStore());
doReturn(true).when(userPasswdStore).verifyPassword("user1", SecuredStringTests.build("test123"));
doReturn(new String[] { "role1", "role2" }).when(userRolesStore).roles("user1");
ESUsersRealm realm = new ESUsersRealm(ImmutableSettings.EMPTY, userPasswdStore, userRolesStore, restController);
User user1 = realm.authenticate(new UsernamePasswordToken("user1", SecuredStringTests.build("test123")));
User user2 = realm.authenticate(new UsernamePasswordToken("user1", SecuredStringTests.build("test123")));
assertThat(user1, sameInstance(user2));
userPasswdStore.notifyRefresh();
User user3 = realm.authenticate(new UsernamePasswordToken("user1", SecuredStringTests.build("test123")));
assertThat(user2, not(sameInstance(user3)));
User user4 = realm.authenticate(new UsernamePasswordToken("user1", SecuredStringTests.build("test123")));
assertThat(user3, sameInstance(user4));
userRolesStore.notifyRefresh();
User user5 = realm.authenticate(new UsernamePasswordToken("user1", SecuredStringTests.build("test123")));
assertThat(user4, not(sameInstance(user5)));
User user6 = realm.authenticate(new UsernamePasswordToken("user1", SecuredStringTests.build("test123")));
assertThat(user5, sameInstance(user6));
}
@Test @Test
public void testToken() throws Exception { public void testToken() throws Exception {
Settings settings = ImmutableSettings.builder().build(); Settings settings = ImmutableSettings.builder().build();
MockUserPasswdStore userPasswdStore = new MockUserPasswdStore("user1", "test123"); when(userPasswdStore.verifyPassword("user1", SecuredStringTests.build("test123"))).thenReturn(true);
MockUserRolesStore userRolesStore = new MockUserRolesStore("user1", "role1", "role2"); when(userRolesStore.roles("user1")).thenReturn(new String[] { "role1", "role2" });
ESUsersRealm realm = new ESUsersRealm(settings, userPasswdStore, userRolesStore, restController); ESUsersRealm realm = new ESUsersRealm(settings, userPasswdStore, userRolesStore, restController);
TransportRequest request = new TransportRequest() {}; TransportRequest request = new TransportRequest() {};
@ -101,51 +129,10 @@ public class ESUsersRealmTests extends ElasticsearchTestCase {
assertThat(new String(token.credentials().internalChars()), equalTo("test123")); assertThat(new String(token.credentials().internalChars()), equalTo("test123"));
} }
private static class MockUserPasswdStore implements UserPasswdStore {
final String username;
final String password;
boolean called = false;
private MockUserPasswdStore(String username, String password) {
this.username = username;
this.password = password;
}
@Override
public boolean verifyPassword(String username, SecuredString password) {
called = true;
assertThat(username, equalTo(this.username));
assertThat(new String(password.internalChars()), equalTo(this.password));
return true;
}
}
private static class MockUserRolesStore implements UserRolesStore {
final String username;
final String[] roles;
boolean called = false;
private MockUserRolesStore(String username, String... roles) {
this.username = username;
this.roles = roles;
}
@Override
public String[] roles(String username) {
called = true;
assertThat(username, equalTo(this.username));
return roles;
}
}
@Test @SuppressWarnings("unchecked") @Test @SuppressWarnings("unchecked")
public void testRestHeadersAreCopied() throws Exception { public void testRestHeadersAreCopied() throws Exception {
// the required header will be registered only if ESUsersRealm is actually used. // the required header will be registered only if ESUsersRealm is actually used.
new ESUsersRealm(ImmutableSettings.EMPTY, null, null, restController); new ESUsersRealm(ImmutableSettings.EMPTY, new UserPasswdStore(), new UserRolesStore(), restController);
when(restController.relevantHeaders()).thenReturn(ImmutableSet.of(UsernamePasswordToken.BASIC_AUTH_HEADER)); when(restController.relevantHeaders()).thenReturn(ImmutableSet.of(UsernamePasswordToken.BASIC_AUTH_HEADER));
when(client.admin()).thenReturn(adminClient); when(client.admin()).thenReturn(adminClient);
when(adminClient.cluster()).thenReturn(mock(ClusterAdminClient.class)); when(adminClient.cluster()).thenReturn(mock(ClusterAdminClient.class));
@ -171,4 +158,18 @@ public class ESUsersRealmTests extends ElasticsearchTestCase {
handler.handleRequest(restRequest, channel); handler.handleRequest(restRequest, channel);
assertThat((String) request.getHeader(UsernamePasswordToken.BASIC_AUTH_HEADER), Matchers.equalTo("foobar")); assertThat((String) request.getHeader(UsernamePasswordToken.BASIC_AUTH_HEADER), Matchers.equalTo("foobar"));
} }
static class UserPasswdStore extends FileUserPasswdStore {
public UserPasswdStore() {
super(ImmutableSettings.EMPTY, new Environment(ImmutableSettings.EMPTY), mock(ResourceWatcherService.class));
}
}
static class UserRolesStore extends FileUserRolesStore {
public UserRolesStore() {
super(ImmutableSettings.EMPTY, new Environment(ImmutableSettings.EMPTY), mock(ResourceWatcherService.class));
}
}
} }

View File

@ -12,6 +12,7 @@ import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.env.Environment; import org.elasticsearch.env.Environment;
import org.elasticsearch.shield.authc.support.Hasher; import org.elasticsearch.shield.authc.support.Hasher;
import org.elasticsearch.shield.authc.support.RefreshListener;
import org.elasticsearch.shield.authc.support.SecuredStringTests; import org.elasticsearch.shield.authc.support.SecuredStringTests;
import org.elasticsearch.test.ElasticsearchTestCase; import org.elasticsearch.test.ElasticsearchTestCase;
import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.threadpool.ThreadPool;
@ -31,6 +32,7 @@ import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static org.hamcrest.Matchers.*; import static org.hamcrest.Matchers.*;
import static org.mockito.Mockito.contains;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
/** /**
@ -79,7 +81,7 @@ public class FileUserPasswdStoreTests extends ElasticsearchTestCase {
threadPool = new ThreadPool("test"); threadPool = new ThreadPool("test");
watcherService = new ResourceWatcherService(settings, threadPool); watcherService = new ResourceWatcherService(settings, threadPool);
final CountDownLatch latch = new CountDownLatch(1); final CountDownLatch latch = new CountDownLatch(1);
FileUserPasswdStore store = new FileUserPasswdStore(settings, env, watcherService, new FileUserPasswdStore.Listener() { FileUserPasswdStore store = new FileUserPasswdStore(settings, env, watcherService, new RefreshListener() {
@Override @Override
public void onRefresh() { public void onRefresh() {
latch.countDown(); latch.countDown();

View File

@ -10,6 +10,7 @@ import org.elasticsearch.common.Strings;
import org.elasticsearch.common.settings.ImmutableSettings; import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.env.Environment; import org.elasticsearch.env.Environment;
import org.elasticsearch.shield.authc.support.RefreshListener;
import org.elasticsearch.test.ElasticsearchTestCase; import org.elasticsearch.test.ElasticsearchTestCase;
import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.watcher.ResourceWatcherService; import org.elasticsearch.watcher.ResourceWatcherService;
@ -73,7 +74,7 @@ public class FileUserRolesStoreTests extends ElasticsearchTestCase {
threadPool = new ThreadPool("test"); threadPool = new ThreadPool("test");
watcherService = new ResourceWatcherService(settings, threadPool); watcherService = new ResourceWatcherService(settings, threadPool);
final CountDownLatch latch = new CountDownLatch(1); final CountDownLatch latch = new CountDownLatch(1);
FileUserRolesStore store = new FileUserRolesStore(settings, env, watcherService, new FileUserRolesStore.Listener() { FileUserRolesStore store = new FileUserRolesStore(settings, env, watcherService, new RefreshListener() {
@Override @Override
public void onRefresh() { public void onRefresh() {
latch.countDown(); latch.countDown();

View File

@ -41,7 +41,7 @@ public class LdapRealmTest extends LdapTest {
} }
@Test @Test
public void testAuthenticate_subTreeGroupSearch(){ public void testAuthenticate_SubTreeGroupSearch(){
String groupSearchBase = "o=sevenSeas"; String groupSearchBase = "o=sevenSeas";
boolean isSubTreeSearch = true; boolean isSubTreeSearch = true;
String userTemplate = VALID_USER_TEMPLATE; String userTemplate = VALID_USER_TEMPLATE;
@ -55,7 +55,7 @@ public class LdapRealmTest extends LdapTest {
} }
@Test @Test
public void testAuthenticate_oneLevelGroupSearch(){ public void testAuthenticate_OneLevelGroupSearch(){
String groupSearchBase = "ou=crews,ou=groups,o=sevenSeas"; String groupSearchBase = "ou=crews,ou=groups,o=sevenSeas";
boolean isSubTreeSearch = false; boolean isSubTreeSearch = false;
String userTemplate = VALID_USER_TEMPLATE; String userTemplate = VALID_USER_TEMPLATE;
@ -70,7 +70,7 @@ public class LdapRealmTest extends LdapTest {
} }
@Test @Test
public void testAuthenticate_caching(){ public void testAuthenticate_Caching(){
String groupSearchBase = "o=sevenSeas"; String groupSearchBase = "o=sevenSeas";
boolean isSubTreeSearch = true; boolean isSubTreeSearch = true;
String userTemplate = VALID_USER_TEMPLATE; String userTemplate = VALID_USER_TEMPLATE;
@ -87,7 +87,33 @@ public class LdapRealmTest extends LdapTest {
} }
@Test @Test
public void testAuthenticate_noncaching(){ public void testAuthenticate_Caching_Refresh(){
String groupSearchBase = "o=sevenSeas";
boolean isSubTreeSearch = true;
String userTemplate = VALID_USER_TEMPLATE;
StandardLdapConnectionFactory ldapFactory = new StandardLdapConnectionFactory(
buildLdapSettings( apacheDsRule.getUrl(), userTemplate, groupSearchBase, isSubTreeSearch) );
LdapGroupToRoleMapper roleMapper = buildGroupAsRoleMapper();
ldapFactory = spy(ldapFactory);
LdapRealm ldap = new LdapRealm( buildCachingSettings(), ldapFactory, roleMapper, restController);
User user = ldap.authenticate( new UsernamePasswordToken(VALID_USERNAME, SecuredStringTests.build(PASSWORD)));
user = ldap.authenticate( new UsernamePasswordToken(VALID_USERNAME, SecuredStringTests.build(PASSWORD)));
//verify one and only one bind -> caching is working
verify(ldapFactory, times(1)).bind(anyString(), any(SecuredString.class));
roleMapper.notifyRefresh();
user = ldap.authenticate( new UsernamePasswordToken(VALID_USERNAME, SecuredStringTests.build(PASSWORD)));
//we need to bind again
verify(ldapFactory, times(2)).bind(anyString(), any(SecuredString.class));
}
@Test
public void testAuthenticate_Noncaching(){
String groupSearchBase = "o=sevenSeas"; String groupSearchBase = "o=sevenSeas";
boolean isSubTreeSearch = true; boolean isSubTreeSearch = true;
String userTemplate = VALID_USER_TEMPLATE; String userTemplate = VALID_USER_TEMPLATE;
@ -102,4 +128,6 @@ public class LdapRealmTest extends LdapTest {
//verify two and only two binds -> caching is disabled //verify two and only two binds -> caching is disabled
verify(ldapFactory, times(2)).bind(anyString(), any(SecuredString.class)); verify(ldapFactory, times(2)).bind(anyString(), any(SecuredString.class));
} }
} }