From 1686add7ce5ede9c98d451a614d52eae207cd51e Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Fri, 14 Jul 2017 15:04:26 +1000 Subject: [PATCH] The configured role-mapping file must be valid. (elastic/x-pack-elasticsearch#1940) This adds a bootstrap-check that makes it an error to configure a role mapping file that doesn't exist or cannot be parsed. We are still lenient on dynamic reload because (a) killing a running node is quite drastic (b) file writes aren't atomic, so we might be picking up a file that is half way through being written (etc). If you rely on the default role mapping filename, then it doesn't need to exist (because you might be using the role mapping API instead) but if it does exist it has to parse successfully Original commit: elastic/x-pack-elasticsearch@5424dea4c4b9fab20a52d0cc19d627d79a98d2e3 --- .../xpack/security/Security.java | 53 ++++----- .../xpack/security/authc/InternalRealms.java | 38 +++++-- .../xpack/security/authc/RealmConfig.java | 6 + .../xpack/security/authc/RealmSettings.java | 21 +++- .../security/authc/support/DnRoleMapper.java | 91 +++++++++------- .../RoleMappingFileBootstrapCheck.java | 58 ++++++++++ .../support/mapper/CompositeRoleMapper.java | 2 +- .../authc/ldap/ActiveDirectoryRealmTests.java | 16 +-- .../security/authc/ldap/LdapRealmTests.java | 4 +- .../authc/ldap/support/LdapTestCase.java | 3 +- .../authc/support/DnRoleMapperTests.java | 63 ++++++----- .../RoleMappingFileBootstrapCheckTests.java | 103 ++++++++++++++++++ 12 files changed, 341 insertions(+), 117 deletions(-) create mode 100644 plugin/src/main/java/org/elasticsearch/xpack/security/authc/support/RoleMappingFileBootstrapCheck.java create mode 100644 plugin/src/test/java/org/elasticsearch/xpack/security/authc/support/RoleMappingFileBootstrapCheckTests.java diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/Security.java b/plugin/src/main/java/org/elasticsearch/xpack/security/Security.java index 8eee595120d..21b8479d155 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -5,6 +5,25 @@ */ package org.elasticsearch.xpack.security; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.time.Clock; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.function.UnaryOperator; +import java.util.stream.Collectors; + import org.apache.logging.log4j.Logger; import org.apache.lucene.util.SetOnce; import org.elasticsearch.action.ActionListener; @@ -35,6 +54,7 @@ import org.elasticsearch.common.settings.Setting.Property; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.SettingsFilter; import org.elasticsearch.common.util.BigArrays; +import org.elasticsearch.common.util.CollectionUtils; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.env.Environment; @@ -159,25 +179,6 @@ import org.elasticsearch.xpack.ssl.SSLService; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; -import java.io.IOException; -import java.security.GeneralSecurityException; -import java.time.Clock; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.function.BiConsumer; -import java.util.function.Function; -import java.util.function.Supplier; -import java.util.function.UnaryOperator; -import java.util.stream.Collectors; - import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; import static org.elasticsearch.xpack.XPackSettings.HTTP_SSL_ENABLED; @@ -496,13 +497,15 @@ public class Security implements ActionPlugin, IngestPlugin, NetworkPlugin { public List getBootstrapChecks() { if (enabled) { - return Arrays.asList( - new SSLBootstrapCheck(sslService, settings, env), - new TokenPassphraseBootstrapCheck(settings), - new TokenSSLBootstrapCheck(settings), - new PkiRealmBootstrapCheck(settings, sslService), - new ContainerPasswordBootstrapCheck() + final ArrayList checks = CollectionUtils.arrayAsArrayList( + new SSLBootstrapCheck(sslService, settings, env), + new TokenPassphraseBootstrapCheck(settings), + new TokenSSLBootstrapCheck(settings), + new PkiRealmBootstrapCheck(settings, sslService), + new ContainerPasswordBootstrapCheck() ); + checks.addAll(InternalRealms.getBootstrapChecks(settings)); + return checks; } else { return Collections.emptyList(); } diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/InternalRealms.java b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/InternalRealms.java index 4f110026270..0eded110335 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/InternalRealms.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/InternalRealms.java @@ -5,7 +5,18 @@ */ package org.elasticsearch.xpack.security.authc; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.elasticsearch.bootstrap.BootstrapCheck; import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.watcher.ResourceWatcherService; import org.elasticsearch.xpack.security.authc.esnative.NativeRealm; @@ -14,16 +25,10 @@ import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm; import org.elasticsearch.xpack.security.authc.file.FileRealm; import org.elasticsearch.xpack.security.authc.ldap.LdapRealm; import org.elasticsearch.xpack.security.authc.pki.PkiRealm; +import org.elasticsearch.xpack.security.authc.support.RoleMappingFileBootstrapCheck; import org.elasticsearch.xpack.security.authc.support.mapper.NativeRoleMappingStore; import org.elasticsearch.xpack.ssl.SSLService; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - /** * Provides a single entry point into dealing with all standard XPack security {@link Realm realms}. * This class does not handle extensions. @@ -78,7 +83,7 @@ public class InternalRealms { * This excludes the {@link ReservedRealm}, as it cannot be configured dynamically. * @return A map from realm-type to a collection of Setting objects. */ - public static Map>> getSettings() { + public static Map>> getSettings() { Map>> map = new HashMap<>(); map.put(FileRealm.TYPE, FileRealm.getSettings()); map.put(NativeRealm.TYPE, NativeRealm.getSettings()); @@ -91,4 +96,21 @@ public class InternalRealms { private InternalRealms() { } + public static List getBootstrapChecks(final Settings globalSettings) { + final List checks = new ArrayList<>(); + final Map settingsByRealm = RealmSettings.getRealmSettings(globalSettings); + settingsByRealm.forEach((name, settings) -> { + final RealmConfig realmConfig = new RealmConfig(name, settings, globalSettings, null); + switch (realmConfig.type()) { + case LdapRealm.AD_TYPE: + case LdapRealm.LDAP_TYPE: + case PkiRealm.TYPE: + final BootstrapCheck check = RoleMappingFileBootstrapCheck.create(realmConfig); + if (check != null) { + checks.add(check); + } + } + }); + return checks; + } } diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/RealmConfig.java b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/RealmConfig.java index 6ebc83b865b..2c4db222295 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/RealmConfig.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/RealmConfig.java @@ -16,6 +16,7 @@ public class RealmConfig { final String name; final boolean enabled; final int order; + private final String type; final Settings settings; private final Environment env; @@ -35,6 +36,7 @@ public class RealmConfig { this.env = env; enabled = RealmSettings.ENABLED_SETTING.get(settings); order = RealmSettings.ORDER_SETTING.get(settings); + type = RealmSettings.TYPE_SETTING.get(settings); this.threadContext = threadContext; } @@ -50,6 +52,10 @@ public class RealmConfig { return order; } + public String type() { + return type; + } + public Settings settings() { return settings; } diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/RealmSettings.java b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/RealmSettings.java index 6ed8caa40d4..22eee8052f9 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/RealmSettings.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/RealmSettings.java @@ -5,11 +5,6 @@ */ package org.elasticsearch.xpack.security.authc; -import org.elasticsearch.common.settings.AbstractScopedSettings; -import org.elasticsearch.common.settings.Setting; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.xpack.extensions.XPackExtension; - import java.util.Collection; import java.util.HashMap; import java.util.HashSet; @@ -17,8 +12,14 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Function; import java.util.stream.Collectors; +import org.elasticsearch.common.settings.AbstractScopedSettings; +import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.xpack.extensions.XPackExtension; + import static org.elasticsearch.common.Strings.isNullOrEmpty; import static org.elasticsearch.xpack.security.Security.setting; @@ -71,6 +72,16 @@ public class RealmSettings { return settings.getByPrefix(RealmSettings.PREFIX); } + /** + * Extracts the realm settings from a global settings object. + * Returns a Map of realm-name to realm-settings. + */ + public static Map getRealmSettings(Settings globalSettings) { + Settings realmsSettings = RealmSettings.get(globalSettings); + return realmsSettings.names().stream() + .collect(Collectors.toMap(Function.identity(), realmsSettings::getAsSettings)); + } + /** * Convert the child {@link Setting} for the provided realm into a fully scoped key for use in an error message. * @see #PREFIX diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/support/DnRoleMapper.java b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/support/DnRoleMapper.java index 02dd1f24f7d..24f1edc1325 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/support/DnRoleMapper.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/support/DnRoleMapper.java @@ -5,22 +5,6 @@ */ package org.elasticsearch.xpack.security.authc.support; -import com.unboundid.ldap.sdk.DN; -import com.unboundid.ldap.sdk.LDAPException; -import org.apache.logging.log4j.Logger; -import org.apache.logging.log4j.message.ParameterizedMessage; -import org.apache.logging.log4j.util.Supplier; -import org.elasticsearch.ElasticsearchException; -import org.elasticsearch.action.ActionListener; -import org.elasticsearch.common.settings.Setting; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.env.Environment; -import org.elasticsearch.watcher.FileChangesListener; -import org.elasticsearch.watcher.FileWatcher; -import org.elasticsearch.watcher.ResourceWatcherService; -import org.elasticsearch.xpack.XPackPlugin; -import org.elasticsearch.xpack.security.authc.RealmConfig; - import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; @@ -37,6 +21,25 @@ import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.Function; +import com.unboundid.ldap.sdk.DN; +import com.unboundid.ldap.sdk.LDAPException; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.message.ParameterizedMessage; +import org.apache.logging.log4j.util.Supplier; +import org.apache.lucene.util.SetOnce; +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.bootstrap.BootstrapCheck; +import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.env.Environment; +import org.elasticsearch.watcher.FileChangesListener; +import org.elasticsearch.watcher.FileWatcher; +import org.elasticsearch.watcher.ResourceWatcherService; +import org.elasticsearch.xpack.XPackPlugin; +import org.elasticsearch.xpack.security.authc.RealmConfig; +import org.yaml.snakeyaml.error.YAMLException; + import static java.util.Collections.emptyMap; import static java.util.Collections.unmodifiableMap; import static org.elasticsearch.xpack.security.authc.ldap.support.LdapUtils.dn; @@ -57,20 +60,18 @@ public class DnRoleMapper implements UserRoleMapper { protected final Logger logger; protected final RealmConfig config; - private final String realmType; private final Path file; private final boolean useUnmappedGroupsAsRoles; private final CopyOnWriteArrayList listeners = new CopyOnWriteArrayList<>(); private volatile Map> dnRoles; - public DnRoleMapper(String realmType, RealmConfig config, ResourceWatcherService watcherService) { - this.realmType = realmType; + public DnRoleMapper(RealmConfig config, ResourceWatcherService watcherService) { this.config = config; this.logger = config.logger(getClass()); useUnmappedGroupsAsRoles = USE_UNMAPPED_GROUPS_AS_ROLES_SETTING.get(config.settings()); file = resolveFile(config.settings(), config.env()); - dnRoles = parseFileLenient(file, logger, realmType, config.name()); + dnRoles = parseFileLenient(file, logger, config.type(), config.name()); FileWatcher watcher = new FileWatcher(file.getParent()); watcher.addListener(new FileListener()); try { @@ -101,7 +102,7 @@ public class DnRoleMapper implements UserRoleMapper { */ public static Map> parseFileLenient(Path path, Logger logger, String realmType, String realmName) { try { - return parseFile(path, logger, realmType, realmName); + return parseFile(path, logger, realmType, realmName, false); } catch (Exception e) { logger.error( (Supplier) () -> new ParameterizedMessage( @@ -110,14 +111,20 @@ public class DnRoleMapper implements UserRoleMapper { } } - public static Map> parseFile(Path path, Logger logger, String realmType, String realmName) { + public static Map> parseFile(Path path, Logger logger, String realmType, String realmName, boolean strict) { logger.trace("reading realm [{}/{}] role mappings file [{}]...", realmType, realmName, path.toAbsolutePath()); - if (!Files.exists(path)) { - logger.warn("Role mapping file [{}] for realm [{}] does not exist. Role mapping will be skipped.", + if (Files.exists(path) == false) { + final ParameterizedMessage message = new ParameterizedMessage( + "Role mapping file [{}] for realm [{}] does not exist.", path.toAbsolutePath(), realmName); - return emptyMap(); + if (strict) { + throw new ElasticsearchException(message.getFormattedMessage()); + } else { + logger.warn(message.getFormattedMessage() + " Role mapping will be skipped."); + return emptyMap(); + } } try (InputStream in = Files.newInputStream(path)) { @@ -136,14 +143,18 @@ public class DnRoleMapper implements UserRoleMapper { } dnRoles.add(role); } catch (LDAPException e) { - logger.error(new ParameterizedMessage( - "invalid DN [{}] found in [{}] role mappings [{}] for realm [{}/{}]. skipping... ", - providedDn, - realmType, - path.toAbsolutePath(), - realmType, - realmName), - e); + ParameterizedMessage message = new ParameterizedMessage( + "invalid DN [{}] found in [{}] role mappings [{}] for realm [{}/{}].", + providedDn, + realmType, + path.toAbsolutePath(), + realmType, + realmName); + if (strict) { + throw new ElasticsearchException(message.getFormattedMessage(), e); + } else { + logger.error(message.getFormattedMessage() + " skipping...", e); + } } } @@ -152,7 +163,7 @@ public class DnRoleMapper implements UserRoleMapper { logger.debug("[{}] role mappings found in file [{}] for realm [{}/{}]", dnToRoles.size(), path.toAbsolutePath(), realmType, realmName); return unmodifiableMap(dnToRoles); - } catch (IOException e) { + } catch (IOException | YAMLException e) { throw new ElasticsearchException("could not read realm [" + realmType + "/" + realmName + "] role mappings file [" + path.toAbsolutePath() + "]", e); } @@ -166,7 +177,7 @@ public class DnRoleMapper implements UserRoleMapper { public void resolveRoles(UserData user, ActionListener> listener) { try { listener.onResponse(resolveRoles(user.getDn(), user.getGroups())); - } catch( Exception e) { + } catch (Exception e) { listener.onFailure(e); } } @@ -185,8 +196,8 @@ public class DnRoleMapper implements UserRoleMapper { } } if (logger.isDebugEnabled()) { - logger.debug("the roles [{}], are mapped from these [{}] groups [{}] using file [{}] for realm [{}/{}]", roles, realmType, - groupDns, file.getFileName(), realmType, config.name()); + logger.debug("the roles [{}], are mapped from these [{}] groups [{}] using file [{}] for realm [{}/{}]", roles, config.type(), + groupDns, file.getFileName(), config.type(), config.name()); } DN userDn = dn(userDnString); @@ -197,7 +208,7 @@ public class DnRoleMapper implements UserRoleMapper { if (logger.isDebugEnabled()) { logger.debug("the roles [{}], are mapped from the user [{}] using file [{}] for realm [{}/{}]", (rolesMappedToUserDn == null) ? Collections.emptySet() : rolesMappedToUserDn, userDnString, file.getFileName(), - realmType, config.name()); + config.type(), config.name()); } return roles; } @@ -225,8 +236,8 @@ public class DnRoleMapper implements UserRoleMapper { public void onFileChanged(Path file) { if (file.equals(DnRoleMapper.this.file)) { logger.info("role mappings file [{}] changed for realm [{}/{}]. updating mappings...", file.toAbsolutePath(), - realmType, config.name()); - dnRoles = parseFileLenient(file, logger, realmType, config.name()); + config.type(), config.name()); + dnRoles = parseFileLenient(file, logger, config.type(), config.name()); notifyRefresh(); } } diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/support/RoleMappingFileBootstrapCheck.java b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/support/RoleMappingFileBootstrapCheck.java new file mode 100644 index 00000000000..04de860b1b2 --- /dev/null +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/support/RoleMappingFileBootstrapCheck.java @@ -0,0 +1,58 @@ +/* + * 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.xpack.security.authc.support; + +import java.nio.file.Path; + +import org.apache.lucene.util.SetOnce; +import org.elasticsearch.bootstrap.BootstrapCheck; +import org.elasticsearch.xpack.security.authc.RealmConfig; + +/** + * A BootstrapCheck that {@link DnRoleMapper} files exist and are valid (valid YAML and valid DNs) + */ +public class RoleMappingFileBootstrapCheck implements BootstrapCheck { + + private final RealmConfig realmConfig; + private final Path path; + + private final SetOnce error = new SetOnce<>(); + + public RoleMappingFileBootstrapCheck(RealmConfig config, Path path) { + this.realmConfig = config; + this.path = path; + } + + @Override + public boolean check() { + try { + DnRoleMapper.parseFile(path, realmConfig.logger(getClass()), realmConfig.type(), realmConfig.name(), true); + return false; + } catch (Exception e) { + error.set(e.getMessage()); + return true; + } + + } + + @Override + public String errorMessage() { + return error.get(); + } + + @Override + public boolean alwaysEnforce() { + return true; + } + + public static BootstrapCheck create(RealmConfig realmConfig) { + if (realmConfig.enabled() && DnRoleMapper.ROLE_MAPPING_FILE_SETTING.exists(realmConfig.settings())) { + Path file = DnRoleMapper.resolveFile(realmConfig.settings(), realmConfig.env()); + return new RoleMappingFileBootstrapCheck(realmConfig, file); + } + return null; + } +} diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/support/mapper/CompositeRoleMapper.java b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/support/mapper/CompositeRoleMapper.java index 5bf44f1a5ab..e981456ac6b 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/support/mapper/CompositeRoleMapper.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/support/mapper/CompositeRoleMapper.java @@ -34,7 +34,7 @@ public class CompositeRoleMapper implements UserRoleMapper { public CompositeRoleMapper(String realmType, RealmConfig realmConfig, ResourceWatcherService watcherService, NativeRoleMappingStore nativeRoleMappingStore) { - this(new DnRoleMapper(realmType, realmConfig, watcherService), nativeRoleMappingStore); + this(new DnRoleMapper(realmConfig, watcherService), nativeRoleMappingStore); } private CompositeRoleMapper(UserRoleMapper... delegates) { diff --git a/plugin/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectoryRealmTests.java b/plugin/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectoryRealmTests.java index 74e2f9a21c6..dce6fc075d9 100644 --- a/plugin/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectoryRealmTests.java +++ b/plugin/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectoryRealmTests.java @@ -136,7 +136,7 @@ public class ActiveDirectoryRealmTests extends ESTestCase { Settings settings = settings(); RealmConfig config = new RealmConfig("testAuthenticateUserPrincipleName", settings, globalSettings, new Environment(globalSettings), new ThreadContext(globalSettings)); ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, sslService); - DnRoleMapper roleMapper = new DnRoleMapper(LdapRealm.AD_TYPE, config, resourceWatcherService); + DnRoleMapper roleMapper = new DnRoleMapper(config, resourceWatcherService); LdapRealm realm = new LdapRealm(LdapRealm.AD_TYPE, config, sessionFactory, roleMapper, threadPool); PlainActionFuture future = new PlainActionFuture<>(); @@ -152,7 +152,7 @@ public class ActiveDirectoryRealmTests extends ESTestCase { Settings settings = settings(); RealmConfig config = new RealmConfig("testAuthenticateSAMAccountName", settings, globalSettings, new Environment(globalSettings), new ThreadContext(globalSettings)); ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, sslService); - DnRoleMapper roleMapper = new DnRoleMapper(LdapRealm.AD_TYPE, config, resourceWatcherService); + DnRoleMapper roleMapper = new DnRoleMapper(config, resourceWatcherService); LdapRealm realm = new LdapRealm(LdapRealm.AD_TYPE, config, sessionFactory, roleMapper, threadPool); // Thor does not have a UPN of form CN=Thor@ad.test.elasticsearch.com @@ -176,7 +176,7 @@ public class ActiveDirectoryRealmTests extends ESTestCase { Settings settings = settings(); RealmConfig config = new RealmConfig("testAuthenticateCachesSuccesfulAuthentications", settings, globalSettings, new Environment(globalSettings), new ThreadContext(globalSettings)); ActiveDirectorySessionFactory sessionFactory = spy(new ActiveDirectorySessionFactory(config, sslService)); - DnRoleMapper roleMapper = new DnRoleMapper(LdapRealm.AD_TYPE, config, resourceWatcherService); + DnRoleMapper roleMapper = new DnRoleMapper(config, resourceWatcherService); LdapRealm realm = new LdapRealm(LdapRealm.AD_TYPE, config, sessionFactory, roleMapper, threadPool); int count = randomIntBetween(2, 10); @@ -194,7 +194,7 @@ public class ActiveDirectoryRealmTests extends ESTestCase { Settings settings = settings(Settings.builder().put(CachingUsernamePasswordRealm.CACHE_TTL_SETTING.getKey(), -1).build()); RealmConfig config = new RealmConfig("testAuthenticateCachingCanBeDisabled", settings, globalSettings, new Environment(globalSettings), new ThreadContext(globalSettings)); ActiveDirectorySessionFactory sessionFactory = spy(new ActiveDirectorySessionFactory(config, sslService)); - DnRoleMapper roleMapper = new DnRoleMapper(LdapRealm.AD_TYPE, config, resourceWatcherService); + DnRoleMapper roleMapper = new DnRoleMapper(config, resourceWatcherService); LdapRealm realm = new LdapRealm(LdapRealm.AD_TYPE, config, sessionFactory, roleMapper, threadPool); int count = randomIntBetween(2, 10); @@ -212,7 +212,7 @@ public class ActiveDirectoryRealmTests extends ESTestCase { Settings settings = settings(); RealmConfig config = new RealmConfig("testAuthenticateCachingClearsCacheOnRoleMapperRefresh", settings, globalSettings, new Environment(globalSettings), new ThreadContext(globalSettings)); ActiveDirectorySessionFactory sessionFactory = spy(new ActiveDirectorySessionFactory(config, sslService)); - DnRoleMapper roleMapper = new DnRoleMapper(LdapRealm.AD_TYPE, config, resourceWatcherService); + DnRoleMapper roleMapper = new DnRoleMapper(config, resourceWatcherService); LdapRealm realm = new LdapRealm(LdapRealm.AD_TYPE, config, sessionFactory, roleMapper, threadPool); int count = randomIntBetween(2, 10); @@ -243,7 +243,7 @@ public class ActiveDirectoryRealmTests extends ESTestCase { .build()); RealmConfig config = new RealmConfig("testRealmMapsGroupsToRoles", settings, globalSettings, new Environment(globalSettings), new ThreadContext(globalSettings)); ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, sslService); - DnRoleMapper roleMapper = new DnRoleMapper(LdapRealm.AD_TYPE, config, resourceWatcherService); + DnRoleMapper roleMapper = new DnRoleMapper(config, resourceWatcherService); LdapRealm realm = new LdapRealm(LdapRealm.AD_TYPE, config, sessionFactory, roleMapper, threadPool); PlainActionFuture future = new PlainActionFuture<>(); @@ -259,7 +259,7 @@ public class ActiveDirectoryRealmTests extends ESTestCase { .build()); RealmConfig config = new RealmConfig("testRealmMapsGroupsToRoles", settings, globalSettings, new Environment(globalSettings), new ThreadContext(globalSettings)); ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, sslService); - DnRoleMapper roleMapper = new DnRoleMapper(LdapRealm.AD_TYPE, config, resourceWatcherService); + DnRoleMapper roleMapper = new DnRoleMapper(config, resourceWatcherService); LdapRealm realm = new LdapRealm(LdapRealm.AD_TYPE, config, sessionFactory, roleMapper, threadPool); PlainActionFuture future = new PlainActionFuture<>(); @@ -278,7 +278,7 @@ public class ActiveDirectoryRealmTests extends ESTestCase { RealmConfig config = new RealmConfig("testRealmUsageStats", settings, globalSettings, new Environment(globalSettings), new ThreadContext(globalSettings)); ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, sslService); - DnRoleMapper roleMapper = new DnRoleMapper(LdapRealm.AD_TYPE, config, resourceWatcherService); + DnRoleMapper roleMapper = new DnRoleMapper(config, resourceWatcherService); LdapRealm realm = new LdapRealm(LdapRealm.AD_TYPE, config, sessionFactory, roleMapper, threadPool); Map stats = realm.usageStats(); diff --git a/plugin/src/test/java/org/elasticsearch/xpack/security/authc/ldap/LdapRealmTests.java b/plugin/src/test/java/org/elasticsearch/xpack/security/authc/ldap/LdapRealmTests.java index d3e69a03a78..c1074d55f36 100644 --- a/plugin/src/test/java/org/elasticsearch/xpack/security/authc/ldap/LdapRealmTests.java +++ b/plugin/src/test/java/org/elasticsearch/xpack/security/authc/ldap/LdapRealmTests.java @@ -285,7 +285,7 @@ public class LdapRealmTests extends LdapTestCase { LdapSessionFactory ldapFactory = new LdapSessionFactory(config, sslService); LdapRealm ldap = new LdapRealm(LdapRealm.LDAP_TYPE, config, ldapFactory, - new DnRoleMapper(LdapRealm.LDAP_TYPE, config, resourceWatcherService), threadPool); + new DnRoleMapper(config, resourceWatcherService), threadPool); PlainActionFuture future = new PlainActionFuture<>(); ldap.authenticate(new UsernamePasswordToken("Horatio Hornblower", new SecureString(PASSWORD)), future); @@ -346,7 +346,7 @@ public class LdapRealmTests extends LdapTestCase { LdapSessionFactory ldapFactory = new LdapSessionFactory(config, sslService); LdapRealm realm = new LdapRealm(LdapRealm.LDAP_TYPE, config, ldapFactory, - new DnRoleMapper(LdapRealm.LDAP_TYPE, config, resourceWatcherService), threadPool); + new DnRoleMapper(config, resourceWatcherService), threadPool); Map stats = realm.usageStats(); assertThat(stats, is(notNullValue())); diff --git a/plugin/src/test/java/org/elasticsearch/xpack/security/authc/ldap/support/LdapTestCase.java b/plugin/src/test/java/org/elasticsearch/xpack/security/authc/ldap/support/LdapTestCase.java index 7cbfeac3933..04e09870e9f 100644 --- a/plugin/src/test/java/org/elasticsearch/xpack/security/authc/ldap/support/LdapTestCase.java +++ b/plugin/src/test/java/org/elasticsearch/xpack/security/authc/ldap/support/LdapTestCase.java @@ -16,7 +16,6 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.xpack.security.authc.RealmConfig; -import org.elasticsearch.xpack.security.authc.ldap.LdapRealm; import org.elasticsearch.xpack.security.authc.ldap.LdapSessionFactory; import org.elasticsearch.xpack.security.authc.support.DnRoleMapper; import org.elasticsearch.test.ESTestCase; @@ -138,7 +137,7 @@ public abstract class LdapTestCase extends ESTestCase { Settings global = Settings.builder().put("path.home", createTempDir()).build(); RealmConfig config = new RealmConfig("ldap1", settings, global, new ThreadContext(Settings.EMPTY)); - return new DnRoleMapper(LdapRealm.LDAP_TYPE, config, resourceWatcherService); + return new DnRoleMapper(config, resourceWatcherService); } protected LdapSession session(SessionFactory factory, String username, SecureString password) { diff --git a/plugin/src/test/java/org/elasticsearch/xpack/security/authc/support/DnRoleMapperTests.java b/plugin/src/test/java/org/elasticsearch/xpack/security/authc/support/DnRoleMapperTests.java index 03eef18dc03..7e385fffbc3 100644 --- a/plugin/src/test/java/org/elasticsearch/xpack/security/authc/support/DnRoleMapperTests.java +++ b/plugin/src/test/java/org/elasticsearch/xpack/security/authc/support/DnRoleMapperTests.java @@ -5,22 +5,6 @@ */ package org.elasticsearch.xpack.security.authc.support; -import com.unboundid.ldap.sdk.DN; -import org.apache.logging.log4j.Level; -import org.apache.logging.log4j.Logger; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.util.concurrent.ThreadContext; -import org.elasticsearch.env.Environment; -import org.elasticsearch.xpack.security.audit.logfile.CapturingLogger; -import org.elasticsearch.xpack.security.authc.RealmConfig; -import org.elasticsearch.xpack.security.authc.ldap.LdapRealm; -import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.threadpool.TestThreadPool; -import org.elasticsearch.threadpool.ThreadPool; -import org.elasticsearch.watcher.ResourceWatcherService; -import org.junit.After; -import org.junit.Before; - import java.io.BufferedWriter; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -36,14 +20,33 @@ import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import com.unboundid.ldap.sdk.DN; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.bootstrap.BootstrapCheck; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.env.Environment; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.threadpool.TestThreadPool; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.watcher.ResourceWatcherService; +import org.elasticsearch.xpack.security.audit.logfile.CapturingLogger; +import org.elasticsearch.xpack.security.authc.RealmConfig; +import org.junit.After; +import org.junit.Before; + import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.hasItems; import static org.hamcrest.Matchers.hasKey; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; public class DnRoleMapperTests extends ESTestCase { @@ -186,6 +189,7 @@ public class DnRoleMapperTests extends ESTestCase { public void testAddNullListener() throws Exception { Path file = env.configFile().resolve("test_role_mapping.yml"); + Files.write(file, Collections.singleton("")); ResourceWatcherService watcherService = new ResourceWatcherService(settings, threadPool); DnRoleMapper mapper = createMapper(file, watcherService); NullPointerException e = expectThrows(NullPointerException.class, () -> mapper.addListener(null)); @@ -195,7 +199,7 @@ public class DnRoleMapperTests extends ESTestCase { public void testParseFile() throws Exception { Path file = getDataPath("role_mapping.yml"); Logger logger = CapturingLogger.newCapturingLogger(Level.INFO); - Map> mappings = DnRoleMapper.parseFile(file, logger, "_type", "_name"); + Map> mappings = DnRoleMapper.parseFile(file, logger, "_type", "_name", false); assertThat(mappings, notNullValue()); assertThat(mappings.size(), is(3)); @@ -225,7 +229,7 @@ public class DnRoleMapperTests extends ESTestCase { Path file = createTempDir().resolve("foo.yaml"); Files.createFile(file); Logger logger = CapturingLogger.newCapturingLogger(Level.DEBUG); - Map> mappings = DnRoleMapper.parseFile(file, logger, "_type", "_name"); + Map> mappings = DnRoleMapper.parseFile(file, logger, "_type", "_name", false); assertThat(mappings, notNullValue()); assertThat(mappings.isEmpty(), is(true)); List events = CapturingLogger.output(logger.getName(), Level.DEBUG); @@ -236,9 +240,16 @@ public class DnRoleMapperTests extends ESTestCase { public void testParseFile_WhenFileDoesNotExist() throws Exception { Path file = createTempDir().resolve(randomAlphaOfLength(10)); Logger logger = CapturingLogger.newCapturingLogger(Level.INFO); - Map> mappings = DnRoleMapper.parseFile(file, logger, "_type", "_name"); + Map> mappings = DnRoleMapper.parseFile(file, logger, "_type", "_name", false); assertThat(mappings, notNullValue()); assertThat(mappings.isEmpty(), is(true)); + + final ElasticsearchException exception = expectThrows(ElasticsearchException.class, () -> { + DnRoleMapper.parseFile(file, logger, "_type", "_name", true); + }); + assertThat(exception.getMessage(), containsString(file.toString())); + assertThat(exception.getMessage(), containsString("does not exist")); + assertThat(exception.getMessage(), containsString("_name")); } public void testParseFile_WhenCannotReadFile() throws Exception { @@ -247,7 +258,7 @@ public class DnRoleMapperTests extends ESTestCase { Files.write(file, Collections.singletonList("aldlfkjldjdflkjd"), StandardCharsets.UTF_16); Logger logger = CapturingLogger.newCapturingLogger(Level.INFO); try { - DnRoleMapper.parseFile(file, logger, "_type", "_name"); + DnRoleMapper.parseFile(file, logger, "_type", "_name", false); fail("expected a parse failure"); } catch (Exception e) { this.logger.info("expected", e); @@ -274,7 +285,7 @@ public class DnRoleMapperTests extends ESTestCase { .build(); RealmConfig config = new RealmConfig("ldap1", ldapSettings, settings, new ThreadContext(Settings.EMPTY)); - DnRoleMapper mapper = new DnRoleMapper(LdapRealm.LDAP_TYPE, config, new ResourceWatcherService(settings, threadPool)); + DnRoleMapper mapper = new DnRoleMapper(config, new ResourceWatcherService(settings, threadPool)); Set roles = mapper.resolveRoles("", Arrays.asList(STARK_GROUP_DNS)); @@ -286,9 +297,9 @@ public class DnRoleMapperTests extends ESTestCase { Settings ldapSettings = Settings.builder() .put(USE_UNMAPPED_GROUPS_AS_ROLES_SETTING_KEY, true) .build(); - RealmConfig config = new RealmConfig("ldap1", ldapSettings, settings, new ThreadContext(Settings.EMPTY));; + RealmConfig config = new RealmConfig("ldap1", ldapSettings, settings, new ThreadContext(Settings.EMPTY)); - DnRoleMapper mapper = new DnRoleMapper(LdapRealm.LDAP_TYPE, config, new ResourceWatcherService(settings, threadPool)); + DnRoleMapper mapper = new DnRoleMapper(config, new ResourceWatcherService(settings, threadPool)); Set roles = mapper.resolveRoles("", Arrays.asList(STARK_GROUP_DNS)); assertThat(roles, hasItems("genius", "billionaire", "playboy", "philanthropist", "shield", "avengers")); @@ -300,9 +311,9 @@ public class DnRoleMapperTests extends ESTestCase { .put(ROLE_MAPPING_FILE_SETTING, file.toAbsolutePath()) .put(USE_UNMAPPED_GROUPS_AS_ROLES_SETTING_KEY, false) .build(); - RealmConfig config = new RealmConfig("ldap-userdn-role", ldapSettings, settings, new ThreadContext(Settings.EMPTY));; + RealmConfig config = new RealmConfig("ldap-userdn-role", ldapSettings, settings, new ThreadContext(Settings.EMPTY)); - DnRoleMapper mapper = new DnRoleMapper(LdapRealm.LDAP_TYPE, config, new ResourceWatcherService(settings, threadPool)); + DnRoleMapper mapper = new DnRoleMapper(config, new ResourceWatcherService(settings, threadPool)); Set roles = mapper.resolveRoles("cn=Horatio Hornblower,ou=people,o=sevenSeas", Collections.emptyList()); assertThat(roles, hasItem("avenger")); @@ -313,6 +324,6 @@ public class DnRoleMapperTests extends ESTestCase { .put("files.role_mapping", file.toAbsolutePath()) .build(); RealmConfig config = new RealmConfig("ad-group-mapper-test", realmSettings, settings, env, new ThreadContext(Settings.EMPTY)); - return new DnRoleMapper(randomBoolean() ? LdapRealm.AD_TYPE : LdapRealm.LDAP_TYPE, config, watcherService); + return new DnRoleMapper(config, watcherService); } } diff --git a/plugin/src/test/java/org/elasticsearch/xpack/security/authc/support/RoleMappingFileBootstrapCheckTests.java b/plugin/src/test/java/org/elasticsearch/xpack/security/authc/support/RoleMappingFileBootstrapCheckTests.java new file mode 100644 index 00000000000..5894076039b --- /dev/null +++ b/plugin/src/test/java/org/elasticsearch/xpack/security/authc/support/RoleMappingFileBootstrapCheckTests.java @@ -0,0 +1,103 @@ +/* + * 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.xpack.security.authc.support; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; + +import org.elasticsearch.bootstrap.BootstrapCheck; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.security.authc.RealmConfig; +import org.junit.Before; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; + +public class RoleMappingFileBootstrapCheckTests extends ESTestCase { + + private static final String ROLE_MAPPING_FILE_SETTING = DnRoleMapper.ROLE_MAPPING_FILE_SETTING.getKey(); + + protected Settings settings; + + @Before + public void init() throws IOException { + settings = Settings.builder() + .put("resource.reload.interval.high", "100ms") + .put("path.home", createTempDir()) + .build(); + } + + public void testBootstrapCheckOfValidFile() { + Path file = getDataPath("role_mapping.yml"); + Settings ldapSettings = Settings.builder() + .put(ROLE_MAPPING_FILE_SETTING, file.toAbsolutePath()) + .build(); + RealmConfig config = new RealmConfig("ldap1", ldapSettings, settings, new ThreadContext(Settings.EMPTY)); + final BootstrapCheck check = RoleMappingFileBootstrapCheck.create(config); + assertThat(check, notNullValue()); + assertThat(check.alwaysEnforce(), equalTo(true)); + assertThat(check.check(), equalTo(false)); + } + + public void testBootstrapCheckOfMissingFile() { + final String fileName = randomAlphaOfLength(10); + Path file = createTempDir().resolve(fileName); + Settings ldapSettings = Settings.builder() + .put(ROLE_MAPPING_FILE_SETTING, file.toAbsolutePath()) + .build(); + RealmConfig config = new RealmConfig("the-realm-name", ldapSettings, settings, new ThreadContext(Settings.EMPTY)); + final BootstrapCheck check = RoleMappingFileBootstrapCheck.create(config); + assertThat(check, notNullValue()); + assertThat(check.alwaysEnforce(), equalTo(true)); + assertThat(check.check(), equalTo(true)); + assertThat(check.errorMessage(), containsString("the-realm-name")); + assertThat(check.errorMessage(), containsString(fileName)); + assertThat(check.errorMessage(), containsString("does not exist")); + } + + public void testBootstrapCheckWithInvalidYaml() throws IOException { + Path file = createTempFile("", ".yml"); + // writing in utf_16 should cause a parsing error as we try to read the file in utf_8 + Files.write(file, Collections.singletonList("junk"), StandardCharsets.UTF_16); + + Settings ldapSettings = Settings.builder() + .put(ROLE_MAPPING_FILE_SETTING, file.toAbsolutePath()) + .build(); + RealmConfig config = new RealmConfig("the-realm-name", ldapSettings, settings, new ThreadContext(Settings.EMPTY)); + final BootstrapCheck check = RoleMappingFileBootstrapCheck.create(config); + assertThat(check, notNullValue()); + assertThat(check.alwaysEnforce(), equalTo(true)); + assertThat(check.check(), equalTo(true)); + assertThat(check.errorMessage(), containsString("the-realm-name")); + assertThat(check.errorMessage(), containsString(file.toString())); + assertThat(check.errorMessage(), containsString("could not read")); + } + + public void testBootstrapCheckWithInvalidDn() throws IOException { + Path file = createTempFile("", ".yml"); + // A DN must have at least 1 '=' symbol + Files.write(file, Collections.singletonList("role: not-a-dn")); + + Settings ldapSettings = Settings.builder() + .put(ROLE_MAPPING_FILE_SETTING, file.toAbsolutePath()) + .build(); + RealmConfig config = new RealmConfig("the-realm-name", ldapSettings, settings, new ThreadContext(Settings.EMPTY)); + final BootstrapCheck check = RoleMappingFileBootstrapCheck.create(config); + assertThat(check, notNullValue()); + assertThat(check.alwaysEnforce(), equalTo(true)); + assertThat(check.check(), equalTo(true)); + assertThat(check.errorMessage(), containsString("the-realm-name")); + assertThat(check.errorMessage(), containsString(file.toString())); + assertThat(check.errorMessage(), containsString("invalid DN")); + assertThat(check.errorMessage(), containsString("not-a-dn")); + } +}