Aligned users, users_roles and role_mapping file stores to behave like roles.yml
All three files are auto loaded by shield when modified. The behaviour that we agreed on is that when there's a parse failure in any of these files, we don't prevent the node from starting. Instead we skip the records that we failed to parse as if they don't exist. This is how `roles.yml` is handled today, and this commit makes sure that `users`, `users_roles` and `role_mapping.yml` are aligned with this behaviour. Also, the same behaviour is applied when the file is modified at runtime (so it's consistent with node start up). This commit also adds a lot of missing tests for both `LdapGroupToRoleMapper` and `ActiveDirectoryGroupToRoleMapper` classes. Original commit: elastic/x-pack-elasticsearch@7fdd6bb5cc
This commit is contained in:
parent
d7d96d866e
commit
910d7c6372
|
@ -18,6 +18,7 @@ import org.elasticsearch.shield.authc.RealmConfig;
|
|||
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.support.NoOpLogger;
|
||||
import org.elasticsearch.shield.support.Validation;
|
||||
import org.elasticsearch.watcher.FileChangesListener;
|
||||
import org.elasticsearch.watcher.FileWatcher;
|
||||
|
@ -46,7 +47,7 @@ public class FileUserPasswdStore {
|
|||
private final Path file;
|
||||
final Hasher hasher = Hasher.HTPASSWD;
|
||||
|
||||
private volatile ImmutableMap<String, char[]> esUsers;
|
||||
private volatile ImmutableMap<String, char[]> users;
|
||||
|
||||
private CopyOnWriteArrayList<RefreshListener> listeners;
|
||||
|
||||
|
@ -57,8 +58,8 @@ public class FileUserPasswdStore {
|
|||
FileUserPasswdStore(RealmConfig config, ResourceWatcherService watcherService, RefreshListener listener) {
|
||||
logger = config.logger(FileUserPasswdStore.class);
|
||||
file = resolveFile(config.settings(), config.env());
|
||||
esUsers = parseFile(file, logger);
|
||||
if (esUsers.isEmpty() && logger.isDebugEnabled()) {
|
||||
users = parseFileLenient(file, logger);
|
||||
if (users.isEmpty() && logger.isDebugEnabled()) {
|
||||
logger.debug("realm [esusers] has no users");
|
||||
}
|
||||
FileWatcher watcher = new FileWatcher(file.getParent().toFile());
|
||||
|
@ -74,15 +75,16 @@ public class FileUserPasswdStore {
|
|||
listeners.add(listener);
|
||||
}
|
||||
|
||||
int usersCount() {
|
||||
return users.size();
|
||||
}
|
||||
|
||||
public boolean verifyPassword(String username, SecuredString password) {
|
||||
if (esUsers == null) {
|
||||
if (users == null) {
|
||||
return false;
|
||||
}
|
||||
char[] hash = esUsers.get(username);
|
||||
if (hash == null) {
|
||||
return false;
|
||||
}
|
||||
return hasher.verify(password, hash);
|
||||
char[] hash = users.get(username);
|
||||
return hash != null && hasher.verify(password, hash);
|
||||
}
|
||||
|
||||
public static Path resolveFile(Settings settings, Environment env) {
|
||||
|
@ -93,14 +95,29 @@ public class FileUserPasswdStore {
|
|||
return Paths.get(location);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internally in this class, we try to load the file, but if for some reason we can't, we're being more lenient by
|
||||
* logging the error and skipping all users. This is aligned with how we handle other auto-loaded files in shield.
|
||||
*/
|
||||
static ImmutableMap<String, char[]> parseFileLenient(Path path, ESLogger logger) {
|
||||
try {
|
||||
return parseFile(path, logger);
|
||||
} catch (Throwable t) {
|
||||
logger.error("failed to parse users file [{}]. skipping/removing all users...", t, path.toAbsolutePath());
|
||||
return ImmutableMap.of();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* parses the esusers file. Should never return {@code null}, if the file doesn't exist an
|
||||
* empty map is returned
|
||||
*/
|
||||
public static ImmutableMap<String, char[]> parseFile(Path path, @Nullable ESLogger logger) {
|
||||
if (logger != null) {
|
||||
logger.trace("reading users file located at [{}]", path.toAbsolutePath());
|
||||
if (logger == null) {
|
||||
logger = NoOpLogger.INSTANCE;
|
||||
}
|
||||
logger.trace("reading users file [{}]...", path.toAbsolutePath());
|
||||
|
||||
if (!Files.exists(path)) {
|
||||
return ImmutableMap.of();
|
||||
}
|
||||
|
@ -122,17 +139,13 @@ public class FileUserPasswdStore {
|
|||
}
|
||||
int i = line.indexOf(":");
|
||||
if (i <= 0 || i == line.length() - 1) {
|
||||
if (logger != null) {
|
||||
logger.error("invalid entry in users file [{}], line [{}]. skipping...", path.toAbsolutePath(), lineNr);
|
||||
}
|
||||
logger.error("invalid entry in users file [{}], line [{}]. skipping...", path.toAbsolutePath(), lineNr);
|
||||
continue;
|
||||
}
|
||||
String username = line.substring(0, i).trim();
|
||||
Validation.Error validationError = Validation.ESUsers.validateUsername(username);
|
||||
if (validationError != null) {
|
||||
if (logger != null) {
|
||||
logger.error("invalid username [{}] in users file [{}], skipping... ({})", username, path.toAbsolutePath(), validationError);
|
||||
}
|
||||
logger.error("invalid username [{}] in users file [{}], skipping... ({})", username, path.toAbsolutePath(), validationError);
|
||||
continue;
|
||||
}
|
||||
String hash = line.substring(i + 1).trim();
|
||||
|
@ -140,7 +153,7 @@ public class FileUserPasswdStore {
|
|||
}
|
||||
|
||||
ImmutableMap<String, char[]> usersMap = users.build();
|
||||
if (logger != null && usersMap.isEmpty()){
|
||||
if (usersMap.isEmpty()){
|
||||
logger.warn("no users found in users file [{}]. use bin/shield/esusers to add users and role mappings", path.toAbsolutePath());
|
||||
}
|
||||
return usersMap;
|
||||
|
@ -176,13 +189,8 @@ public class FileUserPasswdStore {
|
|||
@Override
|
||||
public void onFileChanged(File file) {
|
||||
if (file.equals(FileUserPasswdStore.this.file.toFile())) {
|
||||
try {
|
||||
esUsers = parseFile(file.toPath(), logger);
|
||||
logger.info("updated users (users file [{}] changed)", file.getAbsolutePath());
|
||||
} catch (Throwable t) {
|
||||
logger.error("failed to parse users file [{}]. current users remain unmodified", t, file.getAbsolutePath());
|
||||
return;
|
||||
}
|
||||
logger.info("users file [{}] changed. updating users... )", file.getAbsolutePath());
|
||||
users = parseFileLenient(file.toPath(), logger);
|
||||
notifyRefresh();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ import org.elasticsearch.env.Environment;
|
|||
import org.elasticsearch.shield.ShieldPlugin;
|
||||
import org.elasticsearch.shield.authc.RealmConfig;
|
||||
import org.elasticsearch.shield.authc.support.RefreshListener;
|
||||
import org.elasticsearch.shield.support.NoOpLogger;
|
||||
import org.elasticsearch.shield.support.Validation;
|
||||
import org.elasticsearch.watcher.FileChangesListener;
|
||||
import org.elasticsearch.watcher.FileWatcher;
|
||||
|
@ -53,7 +54,7 @@ public class FileUserRolesStore {
|
|||
FileUserRolesStore(RealmConfig config, ResourceWatcherService watcherService, RefreshListener listener) {
|
||||
logger = config.logger(FileUserRolesStore.class);
|
||||
file = resolveFile(config.settings(), config.env());
|
||||
userRoles = parseFile(file, logger);
|
||||
userRoles = parseFileLenient(file, logger);
|
||||
FileWatcher watcher = new FileWatcher(file.getParent().toFile());
|
||||
watcher.addListener(new FileListener());
|
||||
watcherService.add(watcher, ResourceWatcherService.Frequency.HIGH);
|
||||
|
@ -67,6 +68,10 @@ public class FileUserRolesStore {
|
|||
listeners.add(listener);
|
||||
}
|
||||
|
||||
int entriesCount() {
|
||||
return userRoles.size();
|
||||
}
|
||||
|
||||
public String[] roles(String username) {
|
||||
if (userRoles == null) {
|
||||
return Strings.EMPTY_ARRAY;
|
||||
|
@ -83,15 +88,30 @@ public class FileUserRolesStore {
|
|||
return Paths.get(location);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internally in this class, we try to load the file, but if for some reason we can't, we're being more lenient by
|
||||
* logging the error and skipping all enries. This is aligned with how we handle other auto-loaded files in shield.
|
||||
*/
|
||||
static ImmutableMap<String, String[]> parseFileLenient(Path path, ESLogger logger) {
|
||||
try {
|
||||
return parseFile(path, logger);
|
||||
} catch (Throwable t) {
|
||||
logger.error("failed to parse users_roles file [{}]. skipping/removing all entries...", t, path.toAbsolutePath());
|
||||
return ImmutableMap.of();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* parses the users_roles file. Should never return return {@code null}, if the file doesn't exist
|
||||
* an empty map is returned. The read file holds a mapping per line of the form "role -> users" while the returned
|
||||
* map holds entries of the form "user -> roles".
|
||||
*/
|
||||
public static ImmutableMap<String, String[]> parseFile(Path path, @Nullable ESLogger logger) {
|
||||
if (logger != null) {
|
||||
logger.trace("reading users roles file located at [{}]", path.toAbsolutePath());
|
||||
if (logger == null) {
|
||||
logger = NoOpLogger.INSTANCE;
|
||||
}
|
||||
logger.trace("reading users_roles file [{}]...", path.toAbsolutePath());
|
||||
|
||||
|
||||
if (!Files.exists(path)) {
|
||||
return ImmutableMap.of();
|
||||
|
@ -114,31 +134,23 @@ public class FileUserRolesStore {
|
|||
}
|
||||
int i = line.indexOf(":");
|
||||
if (i <= 0 || i == line.length() - 1) {
|
||||
if (logger != null) {
|
||||
logger.error("invalid entry in users_roles file [{}], line [{}]. skipping...", path.toAbsolutePath(), lineNr);
|
||||
}
|
||||
logger.error("invalid entry in users_roles file [{}], line [{}]. skipping...", path.toAbsolutePath(), lineNr);
|
||||
continue;
|
||||
}
|
||||
String role = line.substring(0, i).trim();
|
||||
Validation.Error validationError = Validation.Roles.validateRoleName(role);
|
||||
if (validationError != null) {
|
||||
if (logger != null) {
|
||||
logger.error("invalid role entry in users_roles file [{}], line [{}] - {}. skipping...", path.toAbsolutePath(), lineNr, validationError);
|
||||
}
|
||||
logger.error("invalid role entry in users_roles file [{}], line [{}] - {}. skipping...", path.toAbsolutePath(), lineNr, validationError);
|
||||
continue;
|
||||
}
|
||||
String usersStr = line.substring(i + 1).trim();
|
||||
if (Strings.isEmpty(usersStr)) {
|
||||
if (logger != null) {
|
||||
logger.error("invalid entry for role [{}] in users_roles file [{}], line [{}]. no users found. skipping...", role, path.toAbsolutePath(), lineNr);
|
||||
}
|
||||
logger.error("invalid entry for role [{}] in users_roles file [{}], line [{}]. no users found. skipping...", role, path.toAbsolutePath(), lineNr);
|
||||
continue;
|
||||
}
|
||||
String[] roleUsers = USERS_DELIM.split(usersStr);
|
||||
if (roleUsers.length == 0) {
|
||||
if (logger != null) {
|
||||
logger.error("invalid entry for role [{}] in users_roles file [{}], line [{}]. no users found. skipping...", role, path.toAbsolutePath(), lineNr);
|
||||
}
|
||||
logger.error("invalid entry for role [{}] in users_roles file [{}], line [{}]. no users found. skipping...", role, path.toAbsolutePath(), lineNr);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -152,12 +164,17 @@ public class FileUserRolesStore {
|
|||
}
|
||||
}
|
||||
|
||||
ImmutableMap.Builder<String, String[]> usersRoles = ImmutableMap.builder();
|
||||
ImmutableMap.Builder<String, String[]> builder = ImmutableMap.builder();
|
||||
for (Map.Entry<String, List<String>> entry : userToRoles.entrySet()) {
|
||||
usersRoles.put(entry.getKey(), entry.getValue().toArray(new String[entry.getValue().size()]));
|
||||
builder.put(entry.getKey(), entry.getValue().toArray(new String[entry.getValue().size()]));
|
||||
}
|
||||
|
||||
return usersRoles.build();
|
||||
ImmutableMap<String, String[]> usersRoles = builder.build();
|
||||
if (usersRoles.isEmpty()){
|
||||
logger.warn("no entries found in users_roles file [{}]. use bin/shield/esusers to add users and role mappings", path.toAbsolutePath());
|
||||
}
|
||||
|
||||
return usersRoles;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -205,13 +222,8 @@ public class FileUserRolesStore {
|
|||
@Override
|
||||
public void onFileChanged(File file) {
|
||||
if (file.equals(FileUserRolesStore.this.file.toFile())) {
|
||||
try {
|
||||
userRoles = parseFile(file.toPath(), logger);
|
||||
logger.info("updated users (users_roles file [{}] changed)", file.getAbsolutePath());
|
||||
} catch (Throwable t) {
|
||||
logger.error("failed to parse users_roles file [{}]. current users_roles remain unmodified", t, file.getAbsolutePath());
|
||||
return;
|
||||
}
|
||||
logger.info("users_roles file [{}] changed. updating users roles...", file.getAbsolutePath());
|
||||
userRoles = parseFileLenient(file.toPath(), logger);
|
||||
notifyRefresh();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
package org.elasticsearch.shield.authc.ldap;
|
||||
|
||||
import org.elasticsearch.shield.authc.RealmConfig;
|
||||
import org.elasticsearch.shield.authc.support.RefreshListener;
|
||||
import org.elasticsearch.shield.authc.support.ldap.AbstractGroupToRoleMapper;
|
||||
import org.elasticsearch.watcher.ResourceWatcherService;
|
||||
|
||||
|
@ -14,7 +15,12 @@ import org.elasticsearch.watcher.ResourceWatcherService;
|
|||
*/
|
||||
public class LdapGroupToRoleMapper extends AbstractGroupToRoleMapper {
|
||||
|
||||
public LdapGroupToRoleMapper(RealmConfig config, ResourceWatcherService watcherService, RefreshListener listener) {
|
||||
super(LdapRealm.TYPE, config, watcherService, listener);
|
||||
}
|
||||
|
||||
public LdapGroupToRoleMapper(RealmConfig config, ResourceWatcherService watcherService) {
|
||||
super(LdapRealm.TYPE, config, watcherService, null);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
*/
|
||||
package org.elasticsearch.shield.authc.support.ldap;
|
||||
|
||||
import org.elasticsearch.ElasticsearchException;
|
||||
import org.elasticsearch.common.Nullable;
|
||||
import org.elasticsearch.common.collect.ImmutableMap;
|
||||
import org.elasticsearch.common.logging.ESLogger;
|
||||
|
@ -13,6 +12,7 @@ import org.elasticsearch.common.settings.ImmutableSettings;
|
|||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.env.Environment;
|
||||
import org.elasticsearch.shield.ShieldPlugin;
|
||||
import org.elasticsearch.shield.ShieldSettingsException;
|
||||
import org.elasticsearch.shield.authc.RealmConfig;
|
||||
import org.elasticsearch.shield.authc.support.RefreshListener;
|
||||
import org.elasticsearch.watcher.FileChangesListener;
|
||||
|
@ -45,7 +45,7 @@ public abstract class AbstractGroupToRoleMapper {
|
|||
private final String realmType;
|
||||
private final Path file;
|
||||
private final boolean useUnmappedGroupsAsRoles;
|
||||
private volatile ImmutableMap<LdapName, Set<String>> groupRoles;
|
||||
protected volatile ImmutableMap<LdapName, Set<String>> groupRoles;
|
||||
|
||||
private CopyOnWriteArrayList<RefreshListener> listeners;
|
||||
|
||||
|
@ -56,7 +56,7 @@ public abstract class AbstractGroupToRoleMapper {
|
|||
|
||||
useUnmappedGroupsAsRoles = config.settings().getAsBoolean(USE_UNMAPPED_GROUPS_AS_ROLES_SETTING, false);
|
||||
file = resolveFile(config.settings(), config.env());
|
||||
groupRoles = parseFile(file, logger, realmType, config.name());
|
||||
groupRoles = parseFileLenient(file, logger, realmType, config.name());
|
||||
FileWatcher watcher = new FileWatcher(file.getParent().toFile());
|
||||
watcher.addListener(new FileListener());
|
||||
watcherService.add(watcher, ResourceWatcherService.Frequency.HIGH);
|
||||
|
@ -78,7 +78,24 @@ public abstract class AbstractGroupToRoleMapper {
|
|||
return Paths.get(location);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internally in this class, we try to load the file, but if for some reason we can't, we're being more lenient by
|
||||
* logging the error and skipping/removing all mappings. This is aligned with how we handle other auto-loaded files
|
||||
* in shield.
|
||||
*/
|
||||
public static ImmutableMap<LdapName, Set<String>> parseFileLenient(Path path, ESLogger logger, String realmType, String realmName) {
|
||||
try {
|
||||
return parseFile(path, logger, realmType, realmName);
|
||||
} catch (Throwable t) {
|
||||
logger.error("failed to parse role mappings file [{}]. skipping/removing all mappings...", t, path.toAbsolutePath());
|
||||
return ImmutableMap.of();
|
||||
}
|
||||
}
|
||||
|
||||
public static ImmutableMap<LdapName, Set<String>> parseFile(Path path, ESLogger logger, String realmType, String realmName) {
|
||||
|
||||
logger.trace("reading realm [{}/{}] role mappings file [{}]...", realmType, realmName, path.toAbsolutePath());
|
||||
|
||||
if (!Files.exists(path)) {
|
||||
return ImmutableMap.of();
|
||||
}
|
||||
|
@ -101,18 +118,27 @@ public abstract class AbstractGroupToRoleMapper {
|
|||
}
|
||||
groupRoles.add(role);
|
||||
} catch (InvalidNameException e) {
|
||||
logger.error("invalid group DN [{}] found in [{}] group to role mappings [{}] for realm [{}]. skipping... ", e, ldapDN, realmType, path.toAbsolutePath(), realmName);
|
||||
logger.error("invalid group DN [{}] found in [{}] group to role mappings [{}] for realm [{}/{}]. skipping... ", e, ldapDN, realmType, path.toAbsolutePath(), realmType, realmName);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (groupToRoles.isEmpty()){
|
||||
logger.warn("no mappings found in role mappings file [{}] for realm [{}/{}]", path.toAbsolutePath(), realmType, realmName);
|
||||
}
|
||||
|
||||
return ImmutableMap.copyOf(groupToRoles);
|
||||
|
||||
} catch (IOException e) {
|
||||
throw new ElasticsearchException("unable to load [" + realmName + "] role mapper file [" + path.toAbsolutePath() + "]", e);
|
||||
throw new ShieldSettingsException("could not read realm [" + realmType + "/" + realmName + "] role mappings file [" + path.toAbsolutePath() + "]", e);
|
||||
}
|
||||
}
|
||||
|
||||
int mappingsCount() {
|
||||
return groupRoles.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* This will map the groupDN's to ES Roles
|
||||
*/
|
||||
|
@ -127,7 +153,7 @@ public abstract class AbstractGroupToRoleMapper {
|
|||
}
|
||||
}
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("the roles [{}], are mapped from these [{}] groups [{}] for realm [{}]", roles, realmType, groupDns, config.name());
|
||||
logger.debug("the roles [{}], are mapped from these [{}] groups [{}] for realm [{}/{}]", roles, realmType, groupDns, realmType, config.name());
|
||||
}
|
||||
return roles;
|
||||
}
|
||||
|
@ -156,13 +182,8 @@ public abstract class AbstractGroupToRoleMapper {
|
|||
@Override
|
||||
public void onFileChanged(File file) {
|
||||
if (file.equals(AbstractGroupToRoleMapper.this.file.toFile())) {
|
||||
try {
|
||||
groupRoles = parseFile(file.toPath(), logger, realmType, config.name());
|
||||
logger.info("updated role mappings (role mappings file [{}] changed) for realm [{}]", file.getAbsolutePath(), config.name());
|
||||
} catch (Throwable t) {
|
||||
logger.error("could not reload role mappings file [{}] for realm [{}]. current role mappings remain unmodified", t, file.getAbsolutePath(), config.name());
|
||||
return;
|
||||
}
|
||||
logger.info("role mappings file [{}] changed for realm [{}/{}]. updating mappings...", file.getAbsolutePath(), realmType, config.name());
|
||||
groupRoles = parseFileLenient(file.toPath(), logger, realmType, config.name());
|
||||
notifyRefresh();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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.active_directory;
|
||||
|
||||
import org.elasticsearch.common.settings.ImmutableSettings;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.shield.authc.RealmConfig;
|
||||
import org.elasticsearch.shield.authc.support.ldap.AbstractGroupToRoleMapper;
|
||||
import org.elasticsearch.shield.authc.support.ldap.AbstractGroupToRoleMapperTests;
|
||||
import org.elasticsearch.watcher.ResourceWatcherService;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class ActiveDirectoryGroupToRoleMapperTests extends AbstractGroupToRoleMapperTests {
|
||||
|
||||
@Override
|
||||
protected AbstractGroupToRoleMapper createMapper(Path file, ResourceWatcherService watcherService) {
|
||||
Settings adSettings = ImmutableSettings.builder()
|
||||
.put("files.role_mapping", file.toAbsolutePath())
|
||||
.build();
|
||||
RealmConfig config = new RealmConfig("ad-group-mapper-test", adSettings, settings, env);
|
||||
return new ActiveDirectoryGroupToRoleMapper(config, watcherService);
|
||||
}
|
||||
|
||||
}
|
|
@ -5,12 +5,16 @@
|
|||
*/
|
||||
package org.elasticsearch.shield.authc.esusers;
|
||||
|
||||
import com.carrotsearch.ant.tasks.junit4.dependencies.com.google.common.base.Charsets;
|
||||
import org.elasticsearch.common.base.Charsets;
|
||||
import org.elasticsearch.common.collect.ImmutableList;
|
||||
import org.elasticsearch.common.collect.ImmutableMap;
|
||||
import org.elasticsearch.common.logging.ESLogger;
|
||||
import org.elasticsearch.common.logging.ESLoggerFactory;
|
||||
import org.elasticsearch.common.settings.ImmutableSettings;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.env.Environment;
|
||||
import org.elasticsearch.shield.ShieldException;
|
||||
import org.elasticsearch.shield.audit.logfile.CapturingLogger;
|
||||
import org.elasticsearch.shield.authc.RealmConfig;
|
||||
import org.elasticsearch.shield.authc.support.Hasher;
|
||||
import org.elasticsearch.shield.authc.support.RefreshListener;
|
||||
|
@ -18,6 +22,8 @@ import org.elasticsearch.shield.authc.support.SecuredStringTests;
|
|||
import org.elasticsearch.test.ElasticsearchTestCase;
|
||||
import org.elasticsearch.threadpool.ThreadPool;
|
||||
import org.elasticsearch.watcher.ResourceWatcherService;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
|
@ -26,6 +32,7 @@ import java.nio.file.Files;
|
|||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
@ -39,6 +46,117 @@ import static org.mockito.Mockito.*;
|
|||
*/
|
||||
public class FileUserPasswdStoreTests extends ElasticsearchTestCase {
|
||||
|
||||
private Settings settings;
|
||||
private Environment env;
|
||||
private ThreadPool threadPool;
|
||||
|
||||
@Before
|
||||
public void init() {
|
||||
settings = ImmutableSettings.builder()
|
||||
.put("watcher.interval.high", "2s")
|
||||
.build();
|
||||
env = new Environment(settings);
|
||||
threadPool = new ThreadPool("test");
|
||||
}
|
||||
|
||||
@After
|
||||
public void shutdown() {
|
||||
threadPool.shutdownNow();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStore_ConfiguredWithUnreadableFile() throws Exception {
|
||||
|
||||
Path file = newTempFile().toPath();
|
||||
|
||||
// writing in utf_16 should cause a parsing error as we try to read the file in utf_8
|
||||
Files.write(file, ImmutableList.of("aldlfkjldjdflkjd"), Charsets.UTF_16);
|
||||
|
||||
Settings esusersSettings = ImmutableSettings.builder()
|
||||
.put("files.users", file.toAbsolutePath())
|
||||
.build();
|
||||
|
||||
RealmConfig config = new RealmConfig("esusers-test", esusersSettings, settings, env);
|
||||
ResourceWatcherService watcherService = new ResourceWatcherService(settings, threadPool);
|
||||
FileUserPasswdStore store = new FileUserPasswdStore(config, watcherService);
|
||||
assertThat(store.usersCount(), is(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStore_AutoReload() throws Exception {
|
||||
Path users = Paths.get(getClass().getResource("users").toURI());
|
||||
Path tmp = Files.createTempFile(null, null);
|
||||
Files.copy(users, Files.newOutputStream(tmp));
|
||||
|
||||
Settings esusersSettings = ImmutableSettings.builder()
|
||||
.put("files.users", tmp.toAbsolutePath())
|
||||
.build();
|
||||
|
||||
|
||||
RealmConfig config = new RealmConfig("esusers-test", esusersSettings, settings, env);
|
||||
ResourceWatcherService watcherService = new ResourceWatcherService(settings, threadPool);
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
FileUserPasswdStore store = new FileUserPasswdStore(config, watcherService, new RefreshListener() {
|
||||
@Override
|
||||
public void onRefresh() {
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
assertThat(store.verifyPassword("bcrypt", SecuredStringTests.build("test123")), is(true));
|
||||
|
||||
watcherService.start();
|
||||
|
||||
try (BufferedWriter writer = Files.newBufferedWriter(tmp, Charsets.UTF_8, StandardOpenOption.APPEND)) {
|
||||
writer.newLine();
|
||||
writer.append("foobar:").append(new String(Hasher.HTPASSWD.hash(SecuredStringTests.build("barfoo"))));
|
||||
}
|
||||
|
||||
if (!latch.await(5, TimeUnit.SECONDS)) {
|
||||
fail("Waited too long for the updated file to be picked up");
|
||||
}
|
||||
|
||||
assertThat(store.verifyPassword("foobar", SecuredStringTests.build("barfoo")), is(true));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStore_AutoReload_WithParseFailures() throws Exception {
|
||||
Path users = Paths.get(getClass().getResource("users").toURI());
|
||||
Path tmp = Files.createTempFile(null, null);
|
||||
Files.copy(users, Files.newOutputStream(tmp));
|
||||
|
||||
Settings esusersSettings = ImmutableSettings.builder()
|
||||
.put("files.users", tmp.toAbsolutePath())
|
||||
.build();
|
||||
|
||||
|
||||
RealmConfig config = new RealmConfig("esusers-test", esusersSettings, settings, env);
|
||||
ResourceWatcherService watcherService = new ResourceWatcherService(settings, threadPool);
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
FileUserPasswdStore store = new FileUserPasswdStore(config, watcherService, new RefreshListener() {
|
||||
@Override
|
||||
public void onRefresh() {
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
assertTrue(store.verifyPassword("bcrypt", SecuredStringTests.build("test123")));
|
||||
|
||||
watcherService.start();
|
||||
|
||||
// now replacing the content of the users file with something that cannot be read
|
||||
Files.write(tmp, ImmutableList.of("aldlfkjldjdflkjd"), Charsets.UTF_16);
|
||||
|
||||
if (!latch.await(5, TimeUnit.SECONDS)) {
|
||||
fail("Waited too long for the updated file to be picked up");
|
||||
}
|
||||
|
||||
assertThat(store.usersCount(), is(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseFile() throws Exception {
|
||||
Path path = Paths.get(getClass().getResource("users").toURI());
|
||||
|
@ -60,63 +178,40 @@ public class FileUserPasswdStoreTests extends ElasticsearchTestCase {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testAutoReload() throws Exception {
|
||||
ThreadPool threadPool = null;
|
||||
ResourceWatcherService watcherService = null;
|
||||
public void testParseFile_Empty() throws Exception {
|
||||
File empty = newTempFile();
|
||||
ESLogger log = ESLoggerFactory.getLogger("test");
|
||||
log = spy(log);
|
||||
ImmutableMap<String, char[]> users = FileUserPasswdStore.parseFile(empty.toPath(), log);
|
||||
assertThat(users.isEmpty(), is(true));
|
||||
verify(log, times(1)).warn(contains("no users found"), eq(empty.toPath().toAbsolutePath()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseFile_WhenFileDoesNotExist() throws Exception {
|
||||
File file = new File(randomAsciiOfLength(10));
|
||||
CapturingLogger logger = new CapturingLogger(CapturingLogger.Level.INFO);
|
||||
Map<String, char[]> users = FileUserPasswdStore.parseFile(file.toPath(), logger);
|
||||
assertThat(users, notNullValue());
|
||||
assertThat(users.isEmpty(), is(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseFile_WhenCannotReadFile() throws Exception {
|
||||
File file = newTempFile();
|
||||
// writing in utf_16 should cause a parsing error as we try to read the file in utf_8
|
||||
Files.write(file.toPath(), ImmutableList.of("aldlfkjldjdflkjd"), Charsets.UTF_16);
|
||||
CapturingLogger logger = new CapturingLogger(CapturingLogger.Level.INFO);
|
||||
try {
|
||||
Path users = Paths.get(getClass().getResource("users").toURI());
|
||||
Path tmp = Files.createTempFile(null, null);
|
||||
Files.copy(users, Files.newOutputStream(tmp));
|
||||
|
||||
Settings settings = ImmutableSettings.builder()
|
||||
.put("watcher.interval.high", "2s")
|
||||
.build();
|
||||
|
||||
Settings esusersSettings = ImmutableSettings.builder()
|
||||
.put("files.users", tmp.toAbsolutePath())
|
||||
.build();
|
||||
|
||||
|
||||
Environment env = new Environment(settings);
|
||||
RealmConfig config = new RealmConfig("esusers-test", esusersSettings, settings, env);
|
||||
threadPool = new ThreadPool("test");
|
||||
watcherService = new ResourceWatcherService(settings, threadPool);
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
FileUserPasswdStore store = new FileUserPasswdStore(config, watcherService, new RefreshListener() {
|
||||
@Override
|
||||
public void onRefresh() {
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
assertTrue(store.verifyPassword("bcrypt", SecuredStringTests.build("test123")));
|
||||
|
||||
watcherService.start();
|
||||
|
||||
try (BufferedWriter writer = Files.newBufferedWriter(tmp, Charsets.UTF_8, StandardOpenOption.APPEND)) {
|
||||
writer.newLine();
|
||||
writer.append("foobar:" + new String(Hasher.HTPASSWD.hash(SecuredStringTests.build("barfoo"))));
|
||||
}
|
||||
|
||||
if (!latch.await(5, TimeUnit.SECONDS)) {
|
||||
fail("Waited too long for the updated file to be picked up");
|
||||
}
|
||||
|
||||
assertTrue(store.verifyPassword("foobar", SecuredStringTests.build("barfoo")));
|
||||
|
||||
} finally {
|
||||
if (watcherService != null) {
|
||||
watcherService.stop();
|
||||
}
|
||||
if (threadPool != null) {
|
||||
threadPool.shutdownNow();
|
||||
}
|
||||
FileUserPasswdStore.parseFile(file.toPath(), logger);
|
||||
fail("expected a parse failure");
|
||||
} catch (ShieldException se) {
|
||||
this.logger.info("expected", se);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testThatInvalidLineDoesNotResultInLoggerNPE() throws Exception {
|
||||
public void testParseFile_InvalidLineDoesNotResultInLoggerNPE() throws Exception {
|
||||
File file = newTempFile();
|
||||
com.google.common.io.Files.write("NotValidUsername=Password\nuser:pass".getBytes(org.elasticsearch.common.base.Charsets.UTF_8), file);
|
||||
Map<String, char[]> users = FileUserPasswdStore.parseFile(file.toPath(), null);
|
||||
|
@ -125,12 +220,17 @@ public class FileUserPasswdStoreTests extends ElasticsearchTestCase {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testParseEmptyFile() throws Exception {
|
||||
File empty = newTempFile();
|
||||
ESLogger log = ESLoggerFactory.getLogger("test");
|
||||
log = spy(log);
|
||||
Map<String, char[]> users = FileUserPasswdStore.parseFile(empty.toPath(), log);
|
||||
|
||||
verify(log, times(1)).warn(contains("no users found"), eq(empty.toPath().toAbsolutePath()));
|
||||
public void testParseFileLenient_WhenCannotReadFile() throws Exception {
|
||||
File file = newTempFile();
|
||||
// writing in utf_16 should cause a parsing error as we try to read the file in utf_8
|
||||
Files.write(file.toPath(), ImmutableList.of("aldlfkjldjdflkjd"), Charsets.UTF_16);
|
||||
CapturingLogger logger = new CapturingLogger(CapturingLogger.Level.INFO);
|
||||
Map<String, char[]> users = FileUserPasswdStore.parseFileLenient(file.toPath(), logger);
|
||||
assertThat(users, notNullValue());
|
||||
assertThat(users.isEmpty(), is(true));
|
||||
List<CapturingLogger.Msg> msgs = logger.output(CapturingLogger.Level.ERROR);
|
||||
assertThat(msgs.size(), is(1));
|
||||
assertThat(msgs.get(0).text, containsString("failed to parse users file"));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -5,16 +5,22 @@
|
|||
*/
|
||||
package org.elasticsearch.shield.authc.esusers;
|
||||
|
||||
import com.carrotsearch.ant.tasks.junit4.dependencies.com.google.common.base.Charsets;
|
||||
import org.elasticsearch.common.Strings;
|
||||
import org.elasticsearch.common.base.Charsets;
|
||||
import org.elasticsearch.common.collect.ImmutableList;
|
||||
import org.elasticsearch.common.logging.ESLogger;
|
||||
import org.elasticsearch.common.logging.ESLoggerFactory;
|
||||
import org.elasticsearch.common.settings.ImmutableSettings;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.env.Environment;
|
||||
import org.elasticsearch.shield.audit.logfile.CapturingLogger;
|
||||
import org.elasticsearch.shield.authc.RealmConfig;
|
||||
import org.elasticsearch.shield.authc.support.RefreshListener;
|
||||
import org.elasticsearch.test.ElasticsearchTestCase;
|
||||
import org.elasticsearch.threadpool.ThreadPool;
|
||||
import org.elasticsearch.watcher.ResourceWatcherService;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
|
@ -23,18 +29,143 @@ import java.nio.file.Files;
|
|||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.mockito.Mockito.contains;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class FileUserRolesStoreTests extends ElasticsearchTestCase {
|
||||
|
||||
private Settings settings;
|
||||
private Environment env;
|
||||
private ThreadPool threadPool;
|
||||
|
||||
@Before
|
||||
public void init() {
|
||||
settings = ImmutableSettings.builder()
|
||||
.put("watcher.interval.high", "2s")
|
||||
.build();
|
||||
env = new Environment(settings);
|
||||
threadPool = new ThreadPool("test");
|
||||
}
|
||||
|
||||
@After
|
||||
public void shutdown() {
|
||||
threadPool.shutdownNow();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStore_ConfiguredWithUnreadableFile() throws Exception {
|
||||
|
||||
File file = newTempFile();
|
||||
List<String> lines = new ArrayList<>();
|
||||
lines.add("aldlfkjldjdflkjd");
|
||||
|
||||
// writing in utf_16 should cause a parsing error as we try to read the file in utf_8
|
||||
Files.write(file.toPath(), lines, Charsets.UTF_16);
|
||||
|
||||
Settings esusersSettings = ImmutableSettings.builder()
|
||||
.put("files.users_roles", file.toPath().toAbsolutePath())
|
||||
.build();
|
||||
|
||||
RealmConfig config = new RealmConfig("esusers-test", esusersSettings, settings, env);
|
||||
ResourceWatcherService watcherService = new ResourceWatcherService(settings, threadPool);
|
||||
FileUserRolesStore store = new FileUserRolesStore(config, watcherService);
|
||||
assertThat(store.entriesCount(), is(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStore_AutoReload() throws Exception {
|
||||
Path users = Paths.get(getClass().getResource("users_roles").toURI());
|
||||
Path tmp = Files.createTempFile(null, null);
|
||||
Files.copy(users, Files.newOutputStream(tmp));
|
||||
|
||||
Settings esusersSettings = ImmutableSettings.builder()
|
||||
.put("files.users_roles", tmp.toAbsolutePath())
|
||||
.build();
|
||||
|
||||
RealmConfig config = new RealmConfig("esusers-test", esusersSettings, settings, env);
|
||||
ResourceWatcherService watcherService = new ResourceWatcherService(settings, threadPool);
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
FileUserRolesStore store = new FileUserRolesStore(config, watcherService, new RefreshListener() {
|
||||
@Override
|
||||
public void onRefresh() {
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
String[] roles = store.roles("user1");
|
||||
assertThat(roles, notNullValue());
|
||||
assertThat(roles.length, is(3));
|
||||
assertThat(roles, arrayContaining("role1", "role2", "role3"));
|
||||
assertThat(store.roles("user4"), equalTo(Strings.EMPTY_ARRAY));
|
||||
|
||||
watcherService.start();
|
||||
|
||||
try (BufferedWriter writer = Files.newBufferedWriter(tmp, Charsets.UTF_8, StandardOpenOption.APPEND)) {
|
||||
writer.newLine();
|
||||
writer.append("role4:user4\nrole5:user4\n");
|
||||
}
|
||||
|
||||
if (!latch.await(5, TimeUnit.SECONDS)) {
|
||||
fail("Waited too long for the updated file to be picked up");
|
||||
}
|
||||
|
||||
roles = store.roles("user4");
|
||||
assertThat(roles, notNullValue());
|
||||
assertThat(roles.length, is(2));
|
||||
assertThat(roles, arrayContaining("role4", "role5"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStore_AutoReload_WithParseFailure() throws Exception {
|
||||
Path users = Paths.get(getClass().getResource("users_roles").toURI());
|
||||
Path tmp = Files.createTempFile(null, null);
|
||||
Files.copy(users, Files.newOutputStream(tmp));
|
||||
|
||||
Settings esusersSettings = ImmutableSettings.builder()
|
||||
.put("files.users_roles", tmp.toAbsolutePath())
|
||||
.build();
|
||||
|
||||
RealmConfig config = new RealmConfig("esusers-test", esusersSettings, settings, env);
|
||||
ResourceWatcherService watcherService = new ResourceWatcherService(settings, threadPool);
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
FileUserRolesStore store = new FileUserRolesStore(config, watcherService, new RefreshListener() {
|
||||
@Override
|
||||
public void onRefresh() {
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
String[] roles = store.roles("user1");
|
||||
assertThat(roles, notNullValue());
|
||||
assertThat(roles.length, is(3));
|
||||
assertThat(roles, arrayContaining("role1", "role2", "role3"));
|
||||
assertThat(store.roles("user4"), equalTo(Strings.EMPTY_ARRAY));
|
||||
|
||||
watcherService.start();
|
||||
|
||||
// now replacing the content of the users file with something that cannot be read
|
||||
Files.write(tmp, ImmutableList.of("aldlfkjldjdflkjd"), Charsets.UTF_16);
|
||||
|
||||
if (!latch.await(5, TimeUnit.SECONDS)) {
|
||||
fail("Waited too long for the updated file to be picked up");
|
||||
}
|
||||
|
||||
assertThat(store.entriesCount(), is(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseFile() throws Exception {
|
||||
Path path = Paths.get(getClass().getResource("users_roles").toURI());
|
||||
|
@ -53,69 +184,42 @@ public class FileUserRolesStoreTests extends ElasticsearchTestCase {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testAutoReload() throws Exception {
|
||||
ThreadPool threadPool = null;
|
||||
ResourceWatcherService watcherService = null;
|
||||
public void testParseFile_Empty() throws Exception {
|
||||
File empty = newTempFile();
|
||||
ESLogger log = ESLoggerFactory.getLogger("test");
|
||||
log = spy(log);
|
||||
FileUserRolesStore.parseFile(empty.toPath(), log);
|
||||
verify(log, times(1)).warn(contains("no entries found"), eq(empty.toPath().toAbsolutePath()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseFile_WhenFileDoesNotExist() throws Exception {
|
||||
File file = new File(randomAsciiOfLength(10));
|
||||
CapturingLogger logger = new CapturingLogger(CapturingLogger.Level.INFO);
|
||||
Map<String, String[]> usersRoles = FileUserRolesStore.parseFile(file.toPath(), logger);
|
||||
assertThat(usersRoles, notNullValue());
|
||||
assertThat(usersRoles.isEmpty(), is(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseFile_WhenCannotReadFile() throws Exception {
|
||||
File file = newTempFile();
|
||||
List<String> lines = new ArrayList<>();
|
||||
lines.add("aldlfkjldjdflkjd");
|
||||
|
||||
// writing in utf_16 should cause a parsing error as we try to read the file in utf_8
|
||||
Files.write(file.toPath(), lines, Charsets.UTF_16);
|
||||
CapturingLogger logger = new CapturingLogger(CapturingLogger.Level.INFO);
|
||||
try {
|
||||
Path users = Paths.get(getClass().getResource("users_roles").toURI());
|
||||
Path tmp = Files.createTempFile(null, null);
|
||||
Files.copy(users, Files.newOutputStream(tmp));
|
||||
|
||||
Settings settings = ImmutableSettings.builder()
|
||||
.put("watcher.interval.high", "2s")
|
||||
.build();
|
||||
|
||||
Settings esusersSettings = ImmutableSettings.builder()
|
||||
.put("files.users_roles", tmp.toAbsolutePath())
|
||||
.build();
|
||||
|
||||
Environment env = new Environment(settings);
|
||||
RealmConfig config = new RealmConfig("esusers-test", esusersSettings, settings, env);
|
||||
threadPool = new ThreadPool("test");
|
||||
watcherService = new ResourceWatcherService(settings, threadPool);
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
FileUserRolesStore store = new FileUserRolesStore(config, watcherService, new RefreshListener() {
|
||||
@Override
|
||||
public void onRefresh() {
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
String[] roles = store.roles("user1");
|
||||
assertThat(roles, notNullValue());
|
||||
assertThat(roles.length, is(3));
|
||||
assertThat(roles, arrayContaining("role1", "role2", "role3"));
|
||||
assertThat(store.roles("user4"), equalTo(Strings.EMPTY_ARRAY));
|
||||
|
||||
watcherService.start();
|
||||
|
||||
try (BufferedWriter writer = Files.newBufferedWriter(tmp, Charsets.UTF_8, StandardOpenOption.APPEND)) {
|
||||
writer.newLine();
|
||||
writer.append("role4:user4\nrole5:user4\n");
|
||||
}
|
||||
|
||||
if (!latch.await(5, TimeUnit.SECONDS)) {
|
||||
fail("Waited too long for the updated file to be picked up");
|
||||
}
|
||||
|
||||
roles = store.roles("user4");
|
||||
assertThat(roles, notNullValue());
|
||||
assertThat(roles.length, is(2));
|
||||
assertThat(roles, arrayContaining("role4", "role5"));
|
||||
|
||||
} finally {
|
||||
if (watcherService != null) {
|
||||
watcherService.stop();
|
||||
}
|
||||
if (threadPool != null) {
|
||||
threadPool.shutdownNow();
|
||||
}
|
||||
FileUserRolesStore.parseFile(file.toPath(), logger);
|
||||
fail("expected a parse failure");
|
||||
} catch (Throwable t) {
|
||||
this.logger.info("expected", t);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testThatEmptyRolesDoesNotCauseNPE() throws Exception {
|
||||
public void testParseFile_EmptyRolesDoesNotCauseNPE() throws Exception {
|
||||
ThreadPool threadPool = null;
|
||||
try {
|
||||
threadPool = new ThreadPool("test");
|
||||
|
@ -130,7 +234,7 @@ public class FileUserRolesStoreTests extends ElasticsearchTestCase {
|
|||
.build();
|
||||
|
||||
Environment env = new Environment(settings);
|
||||
RealmConfig config = new RealmConfig("esusers-test", ImmutableSettings.EMPTY, settings, env);
|
||||
RealmConfig config = new RealmConfig("esusers-test", esusersSettings, settings, env);
|
||||
ResourceWatcherService watcherService = new ResourceWatcherService(settings, threadPool);
|
||||
FileUserRolesStore store = new FileUserRolesStore(config, watcherService);
|
||||
assertThat(store.roles("user"), equalTo(Strings.EMPTY_ARRAY));
|
||||
|
@ -142,24 +246,41 @@ public class FileUserRolesStoreTests extends ElasticsearchTestCase {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testThatEmptyFileIsParsed() throws Exception {
|
||||
public void testParseFile_EmptyFileIsParsed() throws Exception {
|
||||
assertInvalidInputIsSilentlyIgnored("");
|
||||
assertInvalidInputIsSilentlyIgnored("#");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testThatEmptyRoleNameDoesNotThrowException() throws Exception {
|
||||
public void testParseFile_EmptyRoleNameDoesNotThrowException() throws Exception {
|
||||
assertInvalidInputIsSilentlyIgnored(":user1,user2");
|
||||
assertInvalidInputIsSilentlyIgnored(" :user1,user2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testThatEmptyRoleDoesNotThrowException() throws Exception {
|
||||
public void testParseFile_EmptyRoleDoesNotThrowException() throws Exception {
|
||||
assertInvalidInputIsSilentlyIgnored("role:");
|
||||
assertInvalidInputIsSilentlyIgnored("role: ");
|
||||
assertInvalidInputIsSilentlyIgnored("role: , ");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseFileLenient_WhenCannotReadFile() throws Exception {
|
||||
File file = newTempFile();
|
||||
List<String> lines = new ArrayList<>();
|
||||
lines.add("aldlfkjldjdflkjd");
|
||||
|
||||
// writing in utf_16 should cause a parsing error as we try to read the file in utf_8
|
||||
Files.write(file.toPath(), lines, Charsets.UTF_16);
|
||||
CapturingLogger logger = new CapturingLogger(CapturingLogger.Level.INFO);
|
||||
Map<String, String[]> usersRoles = FileUserRolesStore.parseFileLenient(file.toPath(), logger);
|
||||
assertThat(usersRoles, notNullValue());
|
||||
assertThat(usersRoles.isEmpty(), is(true));
|
||||
List<CapturingLogger.Msg> msgs = logger.output(CapturingLogger.Level.ERROR);
|
||||
assertThat(msgs.size(), is(1));
|
||||
assertThat(msgs.get(0).text, containsString("failed to parse users_roles file"));
|
||||
}
|
||||
|
||||
private File writeUsersRoles(String input) throws Exception {
|
||||
File file = newTempFile();
|
||||
com.google.common.io.Files.write(input.getBytes(Charsets.UTF_8), file);
|
||||
|
|
|
@ -9,23 +9,20 @@ import org.elasticsearch.common.settings.ImmutableSettings;
|
|||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.shield.authc.RealmConfig;
|
||||
import org.elasticsearch.shield.authc.support.ldap.AbstractGroupToRoleMapper;
|
||||
import org.elasticsearch.test.ElasticsearchTestCase;
|
||||
import org.elasticsearch.threadpool.ThreadPool;
|
||||
import org.elasticsearch.shield.authc.support.ldap.AbstractGroupToRoleMapperTests;
|
||||
import org.elasticsearch.watcher.ResourceWatcherService;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.hamcrest.Matchers.hasItems;
|
||||
|
||||
public class LdapGroupToRoleMapperTest extends ElasticsearchTestCase {
|
||||
public class LdapGroupToRoleMapperTests extends AbstractGroupToRoleMapperTests {
|
||||
|
||||
private final String tonyStarkDN = "cn=tstark,ou=marvel,o=superheros";
|
||||
private final String[] starkGroupDns = new String[] {
|
||||
//groups can be named by different attributes, depending on the directory,
|
||||
//we don't care what it is named by
|
||||
|
@ -36,47 +33,43 @@ public class LdapGroupToRoleMapperTest extends ElasticsearchTestCase {
|
|||
"gid = playboy , dc = example , dc = com",
|
||||
"groupid=philanthropist,ou=groups,dc=unitedway,dc=org"
|
||||
};
|
||||
private final String roleShield = "shield";
|
||||
private final String roleAvenger = "avenger";
|
||||
private ThreadPool threadPool;
|
||||
|
||||
@Before
|
||||
public void init() {
|
||||
threadPool = new ThreadPool("test");
|
||||
@Override
|
||||
protected AbstractGroupToRoleMapper createMapper(Path file, ResourceWatcherService watcherService) {
|
||||
Settings ldapSettings = ImmutableSettings.builder()
|
||||
.put("files.role_mapping", file.toAbsolutePath())
|
||||
.build();
|
||||
RealmConfig config = new RealmConfig("ldap-group-mapper-test", ldapSettings, settings, env);
|
||||
return new LdapGroupToRoleMapper(config, watcherService);
|
||||
}
|
||||
|
||||
@After
|
||||
public void shutdown() {
|
||||
threadPool.shutdownNow();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testYaml() throws IOException {
|
||||
File file = this.getResource("../support/ldap/role_mapping.yml");
|
||||
Settings settings = ImmutableSettings.settingsBuilder()
|
||||
Settings ldapSettings = ImmutableSettings.settingsBuilder()
|
||||
.put(LdapGroupToRoleMapper.ROLE_MAPPING_FILE_SETTING, file.getCanonicalPath())
|
||||
.build();
|
||||
RealmConfig config = new RealmConfig("ldap1", settings);
|
||||
RealmConfig config = new RealmConfig("ldap1", ldapSettings);
|
||||
|
||||
AbstractGroupToRoleMapper mapper = new LdapGroupToRoleMapper(config, new ResourceWatcherService(settings, threadPool));
|
||||
|
||||
Set<String> roles = mapper.mapRoles( Arrays.asList(starkGroupDns) );
|
||||
|
||||
//verify
|
||||
assertThat(roles, hasItems(roleShield, roleAvenger));
|
||||
assertThat(roles, hasItems("shield", "avenger"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRelativeDN() {
|
||||
Settings settings = ImmutableSettings.builder()
|
||||
Settings ldapSettings = ImmutableSettings.builder()
|
||||
.put(AbstractGroupToRoleMapper.USE_UNMAPPED_GROUPS_AS_ROLES_SETTING, true)
|
||||
.build();
|
||||
RealmConfig config = new RealmConfig("ldap1", settings);
|
||||
RealmConfig config = new RealmConfig("ldap1", ldapSettings);
|
||||
|
||||
AbstractGroupToRoleMapper mapper = new LdapGroupToRoleMapper(config, new ResourceWatcherService(settings, threadPool));
|
||||
|
||||
Set<String> roles = mapper.mapRoles(Arrays.asList(starkGroupDns));
|
||||
assertThat(roles, hasItems("genius", "billionaire", "playboy", "philanthropist", "shield", "avengers"));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,216 @@
|
|||
/*
|
||||
* 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.ldap;
|
||||
|
||||
import org.elasticsearch.common.base.Charsets;
|
||||
import org.elasticsearch.common.collect.ImmutableList;
|
||||
import org.elasticsearch.common.collect.ImmutableMap;
|
||||
import org.elasticsearch.common.settings.ImmutableSettings;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.env.Environment;
|
||||
import org.elasticsearch.shield.audit.logfile.CapturingLogger;
|
||||
import org.elasticsearch.shield.authc.ldap.LdapGroupToRoleMapper;
|
||||
import org.elasticsearch.shield.authc.support.RefreshListener;
|
||||
import org.elasticsearch.test.ElasticsearchTestCase;
|
||||
import org.elasticsearch.threadpool.ThreadPool;
|
||||
import org.elasticsearch.watcher.ResourceWatcherService;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import javax.naming.ldap.LdapName;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.File;
|
||||
import java.nio.file.*;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.hamcrest.Matchers.*;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public abstract class AbstractGroupToRoleMapperTests extends ElasticsearchTestCase {
|
||||
|
||||
protected Settings settings;
|
||||
protected Environment env;
|
||||
protected ThreadPool threadPool;
|
||||
|
||||
@Before
|
||||
public void init() {
|
||||
settings = ImmutableSettings.builder()
|
||||
.put("watcher.interval.high", "2s")
|
||||
.build();
|
||||
env = new Environment(settings);
|
||||
threadPool = new ThreadPool("test");
|
||||
}
|
||||
|
||||
@After
|
||||
public void shutdown() {
|
||||
threadPool.shutdownNow();
|
||||
}
|
||||
|
||||
protected abstract AbstractGroupToRoleMapper createMapper(Path file, ResourceWatcherService watcherService);
|
||||
|
||||
@Test
|
||||
public void testMapper_ConfiguredWithUnreadableFile() throws Exception {
|
||||
Path file = newTempFile().toPath();
|
||||
// writing in utf_16 should cause a parsing error as we try to read the file in utf_8
|
||||
Files.write(file, ImmutableList.of("aldlfkjldjdflkjd"), Charsets.UTF_16);
|
||||
|
||||
ResourceWatcherService watcherService = new ResourceWatcherService(settings, threadPool);
|
||||
AbstractGroupToRoleMapper mapper = createMapper(file, watcherService);
|
||||
assertThat(mapper.mappingsCount(), is(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMapper_AutoReload() throws Exception {
|
||||
Path roleMappingFile = Paths.get(AbstractGroupToRoleMapperTests.class.getResource("role_mapping.yml").toURI());
|
||||
Path file = Files.createTempFile(null, ".yml");
|
||||
Files.copy(roleMappingFile, file, StandardCopyOption.REPLACE_EXISTING);
|
||||
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
ResourceWatcherService watcherService = new ResourceWatcherService(settings, threadPool);
|
||||
AbstractGroupToRoleMapper mapper = createMapper(file, watcherService);
|
||||
mapper.addListener(new RefreshListener() {
|
||||
@Override
|
||||
public void onRefresh() {
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
Set<String> roles = mapper.mapRoles(ImmutableList.of("cn=shield,ou=marvel,o=superheros"));
|
||||
assertThat(roles, notNullValue());
|
||||
assertThat(roles.size(), is(1));
|
||||
assertThat(roles, contains("shield"));
|
||||
|
||||
watcherService.start();
|
||||
|
||||
try (BufferedWriter writer = Files.newBufferedWriter(file, Charsets.UTF_8, StandardOpenOption.APPEND)) {
|
||||
writer.newLine();
|
||||
writer.append("fantastic_four:\n")
|
||||
.append(" - \"cn=fantastic_four,ou=marvel,o=superheros\"");
|
||||
}
|
||||
|
||||
if (!latch.await(5, TimeUnit.SECONDS)) {
|
||||
fail("Waited too long for the updated file to be picked up");
|
||||
}
|
||||
|
||||
roles = mapper.mapRoles(ImmutableList.of("cn=fantastic_four,ou=marvel,o=superheros"));
|
||||
assertThat(roles, notNullValue());
|
||||
assertThat(roles.size(), is(1));
|
||||
assertThat(roles, contains("fantastic_four"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMapper_AutoReload_WithParseFailures() throws Exception {
|
||||
Path roleMappingFile = Paths.get(AbstractGroupToRoleMapperTests.class.getResource("role_mapping.yml").toURI());
|
||||
Path file = Files.createTempFile(null, ".yml");
|
||||
Files.copy(roleMappingFile, file, StandardCopyOption.REPLACE_EXISTING);
|
||||
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
ResourceWatcherService watcherService = new ResourceWatcherService(settings, threadPool);
|
||||
AbstractGroupToRoleMapper mapper = createMapper(file, watcherService);
|
||||
mapper.addListener(new RefreshListener() {
|
||||
@Override
|
||||
public void onRefresh() {
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
Set<String> roles = mapper.mapRoles(ImmutableList.of("cn=shield,ou=marvel,o=superheros"));
|
||||
assertThat(roles, notNullValue());
|
||||
assertThat(roles.size(), is(1));
|
||||
assertThat(roles, contains("shield"));
|
||||
|
||||
watcherService.start();
|
||||
|
||||
// now replacing the content of the users file with something that cannot be read
|
||||
Files.write(file, ImmutableList.of("aldlfkjldjdflkjd"), Charsets.UTF_16);
|
||||
|
||||
if (!latch.await(5, TimeUnit.SECONDS)) {
|
||||
fail("Waited too long for the updated file to be picked up");
|
||||
}
|
||||
|
||||
assertThat(mapper.mappingsCount(), is(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseFile() throws Exception {
|
||||
Path file = Paths.get(AbstractGroupToRoleMapperTests.class.getResource("role_mapping.yml").toURI());
|
||||
CapturingLogger logger = new CapturingLogger(CapturingLogger.Level.INFO);
|
||||
ImmutableMap<LdapName, Set<String>> mappings = LdapGroupToRoleMapper.parseFile(file, logger, "_type", "_name");
|
||||
assertThat(mappings, notNullValue());
|
||||
assertThat(mappings.size(), is(2));
|
||||
|
||||
LdapName ldapName = new LdapName("cn=avengers,ou=marvel,o=superheros");
|
||||
assertThat(mappings, hasKey(ldapName));
|
||||
Set<String> roles = mappings.get(ldapName);
|
||||
assertThat(roles, notNullValue());
|
||||
assertThat(roles, hasSize(2));
|
||||
assertThat(roles, containsInAnyOrder("shield", "avenger"));
|
||||
|
||||
ldapName = new LdapName("cn=shield,ou=marvel,o=superheros");
|
||||
assertThat(mappings, hasKey(ldapName));
|
||||
roles = mappings.get(ldapName);
|
||||
assertThat(roles, notNullValue());
|
||||
assertThat(roles, hasSize(1));
|
||||
assertThat(roles, containsInAnyOrder("shield"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseFile_Empty() throws Exception {
|
||||
Path file = newTempFile().toPath();
|
||||
CapturingLogger logger = new CapturingLogger(CapturingLogger.Level.INFO);
|
||||
ImmutableMap<LdapName, Set<String>> mappings = LdapGroupToRoleMapper.parseFile(file, logger, "_type", "_name");
|
||||
assertThat(mappings, notNullValue());
|
||||
assertThat(mappings.isEmpty(), is(true));
|
||||
List<CapturingLogger.Msg> msgs = logger.output(CapturingLogger.Level.WARN);
|
||||
assertThat(msgs.size(), is(1));
|
||||
assertThat(msgs.get(0).text, containsString("no mappings found"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseFile_WhenFileDoesNotExist() throws Exception {
|
||||
Path file = new File(randomAsciiOfLength(10)).toPath();
|
||||
CapturingLogger logger = new CapturingLogger(CapturingLogger.Level.INFO);
|
||||
ImmutableMap<LdapName, Set<String>> mappings = LdapGroupToRoleMapper.parseFile(file, logger, "_type", "_name");
|
||||
assertThat(mappings, notNullValue());
|
||||
assertThat(mappings.isEmpty(), is(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseFile_WhenCannotReadFile() throws Exception {
|
||||
Path file = newTempFile().toPath();
|
||||
// writing in utf_16 should cause a parsing error as we try to read the file in utf_8
|
||||
Files.write(file, ImmutableList.of("aldlfkjldjdflkjd"), Charsets.UTF_16);
|
||||
CapturingLogger logger = new CapturingLogger(CapturingLogger.Level.INFO);
|
||||
try {
|
||||
LdapGroupToRoleMapper.parseFile(file, logger, "_type", "_name");
|
||||
fail("expected a parse failure");
|
||||
} catch (Exception e) {
|
||||
this.logger.info("expected", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseFileLenient_WhenCannotReadFile() throws Exception {
|
||||
Path file = newTempFile().toPath();
|
||||
// writing in utf_16 should cause a parsing error as we try to read the file in utf_8
|
||||
Files.write(file, ImmutableList.of("aldlfkjldjdflkjd"), Charsets.UTF_16);
|
||||
CapturingLogger logger = new CapturingLogger(CapturingLogger.Level.INFO);
|
||||
ImmutableMap<LdapName, Set<String>> mappings = LdapGroupToRoleMapper.parseFileLenient(file, logger, "_type", "_name");
|
||||
assertThat(mappings, notNullValue());
|
||||
assertThat(mappings.isEmpty(), is(true));
|
||||
List<CapturingLogger.Msg> msgs = logger.output(CapturingLogger.Level.ERROR);
|
||||
assertThat(msgs.size(), is(1));
|
||||
assertThat(msgs.get(0).text, containsString("failed to parse role mappings file"));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue