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")); + } +}