configs() {
*
* For example, georchestra's default baseDn is dc=georchestra,dc=org
*/
- @NotBlank
private String baseDn;
- @NotNull
- private Users users = new Users();
-
- @NotNull
- private Roles roles = new Roles();
+ /**
+ * How to extract user information. Only searchFilter is used if activeDirectory
+ * is true
+ */
+ private Users users;
- private Organizations orgs = null;
+ /**
+ * How to extract role information, un-used for Active Directory
+ */
+ private Roles roles;
/**
- * Configured the LDAP authentication source to use georchestra specific
- * extensions. For example, when using the default OpenLDAP database with
- * additional information like pending users and organizations
+ * How to extract Organization information, only used for OpenLDAP if extended =
+ * true
*/
- public boolean hasGeorchestraExtensions() {
- if (this.isExtended()) {// forced use of extensions
- return true;
- }
- // heuristically determining whether it's a georchestra extended db
- Users users = getUsers();
- if (StringUtils.hasText(users.getPendingUsersSearchBaseDN())) {
- return true;
- }
- Roles roles = getRoles();
- if (roles.getProtectedRoles() != null && !roles.getProtectedRoles().isEmpty()) {
- return true;
- }
- if (null != getOrgs()) {
- return true;
- }
- return false;
- }
+ private Organizations orgs;
}
- public static @Data class Users {
+ @Generated
+ public static @Data @Accessors(chain = true) class Users {
/**
* Users RDN Relative distinguished name of the "users" LDAP organization unit.
* E.g. if the complete name (or DN) is ou=users,dc=georchestra,dc=org, the RDN
* is ou=users.
*/
- @NotBlank
- private String rdn = "ou=users";
-
- @NotBlank
- private String searchFilter = "(uid={0})";
+ private String rdn;
/**
- * E.g. ou=pendingusers
+ * Users search filter, e.g. (uid={0}) for OpenLDAP, and
+ * (&(objectClass=user)(userPrincipalName={0})) for ActiveDirectory
*/
- private String pendingUsersSearchBaseDN;
-
- private List protectedUsers = List.of();
+ private String searchFilter;
}
- public static @Data class Roles {
+ @Generated
+ public static @Data @Accessors(chain = true) class Roles {
/**
* Roles RDN Relative distinguished name of the "roles" LDAP organization unit.
* E.g. if the complete name (or DN) is ou=roles,dc=georchestra,dc=org, the RDN
* is ou=roles.
*/
- @NotBlank
- private String rdn = "ou=roles";
+ private String rdn;
- @NotBlank
- private String searchFilter = "(member={0})";
+ /**
+ * Roles search filter. e.g. (member={0})
+ */
+ private String searchFilter;
- @NotBlank
private String prefix = "ROLE_";
private boolean upperCase = true;
-
- private List protectedRoles = List.of();
}
- public static @Data class Organizations {
+ @Generated
+ public static @Data @Accessors(chain = true) class Organizations {
- @NotBlank
+ /**
+ * Organizations search base. Default: ou=orgs
+ */
private String rdn = "ou=orgs";
+ }
+
+ public @Override boolean supports(Class> clazz) {
+ return LdapConfigProperties.class.equals(clazz);
+ }
- @NotBlank
- private String orgTypes = "Association,Company,NGO,Individual,Other";
+ @Override
+ public void validate(Object target, Errors errors) {
+ LdapConfigProperties config = (LdapConfigProperties) target;
+ Map ldap = config.getLdap();
+ if (ldap == null || ldap.isEmpty()) {
+ return;
+ }
+ LdapConfigPropertiesValidations validations = new LdapConfigPropertiesValidations();
+ ldap.forEach((name, serverConfig) -> validations.validate(name, serverConfig, errors));
+ }
+
+ public List simpleEnabled() {
+ LdapConfigBuilder builder = new LdapConfigBuilder();
+ return entries()//
+ .filter(e -> e.getValue().isEnabled())//
+ .filter(e -> !e.getValue().isActiveDirectory())//
+ .filter(e -> !e.getValue().isExtended())//
+ .map(e -> builder.asBasicLdapConfig(e.getKey(), e.getValue()))//
+ .collect(Collectors.toList());
+ }
+
+ public List extendedEnabled() {
+ LdapConfigBuilder builder = new LdapConfigBuilder();
+ return entries()//
+ .filter(e -> e.getValue().isEnabled())//
+ .filter(e -> !e.getValue().isActiveDirectory())//
+ .filter(e -> e.getValue().isExtended())//
+ .map(e -> builder.asExtendedLdapConfig(e.getKey(), e.getValue()))//
+ .collect(Collectors.toList());
+ }
+
+ public List activeDirectoryEnabled() {
+ LdapConfigBuilder builder = new LdapConfigBuilder();
+ return entries()//
+ .filter(e -> e.getValue().isEnabled())//
+ .filter(e -> e.getValue().isActiveDirectory())//
+ .map(e -> builder.asActiveDirectoryConfig(e.getKey(), e.getValue()))//
+ .collect(Collectors.toList());
+ }
- @NotBlank
- private String pendingOrgSearchBaseDN = "ou=pendingorgs";
+ private Stream> entries() {
+ return ldap == null ? Stream.empty() : ldap.entrySet().stream();
}
+
}
diff --git a/gateway/src/main/java/org/georchestra/gateway/security/ldap/LdapConfigPropertiesValidations.java b/gateway/src/main/java/org/georchestra/gateway/security/ldap/LdapConfigPropertiesValidations.java
new file mode 100644
index 00000000..2274383d
--- /dev/null
+++ b/gateway/src/main/java/org/georchestra/gateway/security/ldap/LdapConfigPropertiesValidations.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2022 by the geOrchestra PSC
+ *
+ * This file is part of geOrchestra.
+ *
+ * geOrchestra is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free
+ * Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * geOrchestra is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * geOrchestra. If not, see .
+ */
+package org.georchestra.gateway.security.ldap;
+
+import static java.lang.String.format;
+import static org.springframework.validation.ValidationUtils.rejectIfEmptyOrWhitespace;
+
+import org.georchestra.gateway.security.ldap.LdapConfigProperties.Server;
+import org.georchestra.gateway.security.ldap.LdapConfigProperties.Users;
+import org.springframework.util.StringUtils;
+import org.springframework.validation.Errors;
+
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j(topic = "org.georchestra.gateway.security.ldap")
+class LdapConfigPropertiesValidations {
+
+ public void validate(String name, Server config, Errors errors) {
+ if (!config.isEnabled()) {
+ log.debug("ignoring validation of LDAP config {}, enabled = false", name);
+ return;
+ }
+ final String url = format("ldap.[%s].url", name);
+ rejectIfEmptyOrWhitespace(errors, url, "", "LDAP url is required (e.g.: ldap://my.ldap.com:389)");
+
+ validateIsNotExtendedAndActiveDirectory(name, config, errors);
+
+ if (config.isActiveDirectory()) {
+ validateActiveDirectory(name, config, errors);
+ } else {
+ validateSimpleLdap(name, config, errors);
+ if (config.isExtended()) {
+ validateGeorchestraExtensions(name, config, errors);
+ }
+ }
+ }
+
+ private void validateSimpleLdap(String name, Server config, Errors errors) {
+ rejectIfEmptyOrWhitespace(errors, format("ldap.[%s].baseDn", name), "",
+ "LDAP base DN is required. e.g.: dc=georchestra,dc=org");
+
+ rejectIfEmptyOrWhitespace(errors, format("ldap.[%s].users.rdn", name), "",
+ "LDAP users RDN (Relative Distinguished Name) is required. e.g.: ou=users,dc=georchestra,dc=org");
+
+ rejectIfEmptyOrWhitespace(errors, format("ldap.[%s].users.searchFilter", name), "",
+ "LDAP users searchFilter is required for regular LDAP configs. e.g.: (uid={0}), and optional for Active Directory. e.g.: (&(objectClass=user)(userPrincipalName={0}))");
+
+ rejectIfEmptyOrWhitespace(errors, format("ldap.[%s].roles.rdn", name), "",
+ "Roles Relative distinguished name is required. e.g.: ou=roles");
+
+ rejectIfEmptyOrWhitespace(errors, format("ldap.[%s].roles.searchFilter", name), "",
+ "Roles searchFilter is required. e.g.: (member={0})");
+ }
+
+ private void validateGeorchestraExtensions(String name, Server config, Errors errors) {
+ rejectIfEmptyOrWhitespace(errors, format("ldap.[%s].orgs.rdn", name), "",
+ "Organizations search base RDN is required if extended is true. e.g.: ou=orgs");
+ }
+
+ private void validateActiveDirectory(String name, Server config, Errors errors) {
+ if (!StringUtils.hasText(config.getDomain())) {
+ log.warn("ldap.{}.domain is null, it is recommended to set up a domain name (e.g. my.company.com)", name);
+ }
+
+ if (!StringUtils.hasText(config.getBaseDn())) {
+ log.debug("ldap.{}.baseDn is null, will derive Active Directory rootDn from domain", name);
+ }
+
+ Users users = config.getUsers();
+ String searchFilter = users == null ? null : users.getSearchFilter();
+ if (!StringUtils.hasText(searchFilter)) {
+ log.debug(
+ "ldap.{}.users.searchFilter is null, will use default Active Directory value: (&(objectClass=user)(userPrincipalName={0}))");
+ }
+ if (users != null) {
+ String rdn = users.getRdn();
+ warnUnusedByActiveDirectory(name, "users.rdn", rdn);
+ }
+ warnUnusedByActiveDirectory(name, "roles", config.getRoles());
+ warnUnusedByActiveDirectory(name, "orgs", config.getOrgs());
+ }
+
+ private void warnUnusedByActiveDirectory(String name, String property, Object value) {
+ if (value != null) {
+ log.warn(
+ "Found config property org.georchestra.gateway.security.ldap.{}.{} but it's not used by Active Directory",
+ name, property);
+ }
+ }
+
+ private void validateIsNotExtendedAndActiveDirectory(String name, Server config, Errors errors) {
+ final boolean activeDirectory = config.isActiveDirectory();
+ final boolean extended = config.isExtended();
+ if (activeDirectory && extended) {
+ errors.rejectValue(format("ldap.[%s].extended", name), "",
+ "extended and activeDirectory are mutually exclusive");
+ errors.rejectValue(format("ldap.[%s].activeDirectory", name), "",
+ "extended and activeDirectory are mutually exclusive");
+ }
+ }
+}
diff --git a/gateway/src/main/java/org/georchestra/gateway/security/ldap/LdapSecurityConfiguration.java b/gateway/src/main/java/org/georchestra/gateway/security/ldap/LdapSecurityConfiguration.java
new file mode 100644
index 00000000..9f5afdb1
--- /dev/null
+++ b/gateway/src/main/java/org/georchestra/gateway/security/ldap/LdapSecurityConfiguration.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2022 by the geOrchestra PSC
+ *
+ * This file is part of geOrchestra.
+ *
+ * geOrchestra is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free
+ * Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * geOrchestra is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * geOrchestra. If not, see .
+ */
+package org.georchestra.gateway.security.ldap;
+
+import java.util.List;
+
+import org.georchestra.gateway.security.ServerHttpSecurityCustomizer;
+import org.georchestra.gateway.security.ldap.activedirectory.ActiveDirectoryAuthenticationConfiguration;
+import org.georchestra.gateway.security.ldap.basic.BasicLdapAuthenticationConfiguration;
+import org.georchestra.gateway.security.ldap.extended.ExtendedLdapAuthenticationConfiguration;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.context.annotation.Primary;
+import org.springframework.security.authentication.DelegatingReactiveAuthenticationManager;
+import org.springframework.security.authentication.ReactiveAuthenticationManager;
+import org.springframework.security.config.web.server.ServerHttpSecurity;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.ldap.userdetails.LdapUserDetails;
+import org.springframework.security.web.server.authentication.AuthenticationWebFilter;
+import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
+
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * {@link ServerHttpSecurityCustomizer} to enable LDAP based authentication and
+ * authorization across multiple LDAP databases.
+ *
+ * This configuration sets up the required beans for spring-based LDAP
+ * authentication and authorization, using {@link LdapConfigProperties} to get
+ * the {@link LdapConfigProperties#getUrl() connection URL} and the
+ * {@link LdapConfigProperties#getBaseDn() base DN}.
+ *
+ * As a result, the {@link ServerHttpSecurity} will have HTTP-Basic
+ * authentication enabled and {@link ServerHttpSecurity#formLogin() form login}
+ * set up.
+ *
+ * Upon successful authentication, the corresponding {@link Authentication} with
+ * an {@link LdapUserDetails} as {@link Authentication#getPrincipal() principal}
+ * and the roles extracted from LDAP as {@link Authentication#getAuthorities()
+ * authorities}, will be set as the security context's
+ * {@link SecurityContext#getAuthentication() authentication} property.
+ *
+ * Note however, this may not be enough information to convey
+ * geOrchestra-specific HTTP request headers to backend services, depending on
+ * the matching gateway-route configuration. See
+ * {@link ExtendedLdapAuthenticationConfiguration} for further details.
+ *
+ * @see LdapConfigProperties
+ * @see BasicLdapAuthenticationConfiguration
+ * @see ExtendedLdapAuthenticationConfiguration
+ * @see ActiveDirectoryAuthenticationConfiguration
+ */
+@Configuration(proxyBeanMethods = true)
+@EnableConfigurationProperties(LdapConfigProperties.class)
+@Import({ //
+ BasicLdapAuthenticationConfiguration.class, //
+ ExtendedLdapAuthenticationConfiguration.class, //
+ ActiveDirectoryAuthenticationConfiguration.class //
+})
+@Slf4j(topic = "org.georchestra.gateway.security.ldap")
+public class LdapSecurityConfiguration {
+
+ public static final class LDAPAuthenticationCustomizer implements ServerHttpSecurityCustomizer {
+ public @Override void customize(ServerHttpSecurity http) {
+ log.info("Enabling HTTP Basic authentication support for LDAP");
+ http.httpBasic().and().formLogin();
+ }
+ }
+
+ @Bean
+ public ServerHttpSecurityCustomizer ldapHttpBasicLoginFormEnablerExtension() {
+ return new LDAPAuthenticationCustomizer();
+ }
+
+ @Bean
+ public AuthenticationWebFilter ldapAuthenticationWebFilter(
+ ReactiveAuthenticationManager ldapAuthenticationManager) {
+
+ AuthenticationWebFilter ldapAuthFilter = new AuthenticationWebFilter(ldapAuthenticationManager);
+ ldapAuthFilter.setRequiresAuthenticationMatcher(ServerWebExchangeMatchers.pathMatchers("/auth/login"));
+ return ldapAuthFilter;
+ }
+
+ @Primary
+ @Bean
+ public ReactiveAuthenticationManager primaryAuthenticationManager(List delegates) {
+ return new DelegatingReactiveAuthenticationManager(delegates);
+ }
+}
diff --git a/gateway/src/main/java/org/georchestra/gateway/security/ldap/MultipleLdapSecurityConfiguration.java b/gateway/src/main/java/org/georchestra/gateway/security/ldap/MultipleLdapSecurityConfiguration.java
deleted file mode 100644
index b89cec50..00000000
--- a/gateway/src/main/java/org/georchestra/gateway/security/ldap/MultipleLdapSecurityConfiguration.java
+++ /dev/null
@@ -1,213 +0,0 @@
-/*
- * Copyright (C) 2022 by the geOrchestra PSC
- *
- * This file is part of geOrchestra.
- *
- * geOrchestra is free software: you can redistribute it and/or modify it under
- * the terms of the GNU General Public License as published by the Free
- * Software Foundation, either version 3 of the License, or (at your option)
- * any later version.
- *
- * geOrchestra is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
- * more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * geOrchestra. If not, see .
- */
-package org.georchestra.gateway.security.ldap;
-
-import java.util.List;
-import java.util.stream.Collectors;
-
-import org.georchestra.gateway.security.ServerHttpSecurityCustomizer;
-import org.georchestra.gateway.security.ldap.LdapConfigProperties.LdapServerConfig;
-import org.georchestra.gateway.security.ldap.LdapConfigProperties.Server;
-import org.springframework.boot.context.properties.EnableConfigurationProperties;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.ldap.core.support.BaseLdapPathContextSource;
-import org.springframework.ldap.core.support.LdapContextSource;
-import org.springframework.security.authentication.AuthenticationManager;
-import org.springframework.security.authentication.AuthenticationProvider;
-import org.springframework.security.authentication.ProviderManager;
-import org.springframework.security.authentication.ReactiveAuthenticationManager;
-import org.springframework.security.authentication.ReactiveAuthenticationManagerAdapter;
-import org.springframework.security.config.web.server.ServerHttpSecurity;
-import org.springframework.security.core.Authentication;
-import org.springframework.security.core.AuthenticationException;
-import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
-import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;
-import org.springframework.security.core.context.SecurityContext;
-import org.springframework.security.ldap.authentication.BindAuthenticator;
-import org.springframework.security.ldap.authentication.LdapAuthenticationProvider;
-import org.springframework.security.ldap.authentication.LdapAuthenticator;
-import org.springframework.security.ldap.search.FilterBasedLdapUserSearch;
-import org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator;
-import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator;
-import org.springframework.security.ldap.userdetails.LdapUserDetails;
-import org.springframework.security.web.server.authentication.AuthenticationWebFilter;
-import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
-
-import lombok.extern.slf4j.Slf4j;
-
-/**
- * {@link ServerHttpSecurityCustomizer} to enable LDAP based authentication and
- * authorization across multiple LDAP databases.
- *
- * This configuration sets up the required beans for spring-based LDAP
- * authentication and authorization, using {@link LdapConfigProperties} to get
- * the {@link LdapConfigProperties#getUrl() connection URL} and the
- * {@link LdapConfigProperties#getBaseDn() base DN}.
- *
- * As a result, the {@link ServerHttpSecurity} will have HTTP-Basic
- * authentication enabled and {@link ServerHttpSecurity#formLogin() form login}
- * set up.
- *
- * Upon successful authentication, the corresponding {@link Authentication} with
- * an {@link LdapUserDetails} as {@link Authentication#getPrincipal() principal}
- * and the roles extracted from LDAP as {@link Authentication#getAuthorities()
- * authorities}, will be set as the security context's
- * {@link SecurityContext#getAuthentication() authentication} property.
- *
- * Note however, this may not be enough information to convey
- * geOrchestra-specific HTTP request headers to backend services, depending on
- * the matching gateway-route configuration. See
- * {@link GeorchestraLdapAccountManagementConfiguration} for further details.
- *
- * @see GeorchestraLdapAccountManagementConfiguration
- * @see LdapConfigProperties
- */
-@Configuration(proxyBeanMethods = true)
-@EnableConfigurationProperties(LdapConfigProperties.class)
-@Slf4j(topic = "org.georchestra.gateway.security.ldap")
-public class MultipleLdapSecurityConfiguration {
-
- public static final class LDAPAuthenticationCustomizer implements ServerHttpSecurityCustomizer {
- public @Override void customize(ServerHttpSecurity http) {
- log.info("Enabling HTTP Basic authentication support for LDAP");
- http.httpBasic().and().formLogin();
- }
- }
-
- @Bean
- public ServerHttpSecurityCustomizer ldapHttpBasicLoginFormEnablerExtension() {
- return new LDAPAuthenticationCustomizer();
- }
-
- @Bean
- public AuthenticationWebFilter ldapAuthenticationWebFilter(
- ReactiveAuthenticationManager ldapAuthenticationManager) {
- AuthenticationWebFilter ldapAuthFilter = new AuthenticationWebFilter(ldapAuthenticationManager);
- ldapAuthFilter.setRequiresAuthenticationMatcher(ServerWebExchangeMatchers.pathMatchers("/auth/login"));
- return ldapAuthFilter;
- }
-
- @Bean
- public LdapAuthenticatedUserMapper ldapAuthenticatedUserMapper() {
- return new LdapAuthenticatedUserMapper();
- }
-
- /**
- * @return a {@link ReactiveAuthenticationManager} that will probe
- * username/password authentication over all configured and enabled LDAP
- * databases in {@link LdapConfigProperties}, returning the first
- * successful authorization.
- */
- @Bean
- public ReactiveAuthenticationManager ldapAuthenticationManager(LdapConfigProperties config) {
- List enabledConfigs = config.configs().stream().filter(LdapServerConfig::isEnabled)
- .collect(Collectors.toList());
- List providers = enabledConfigs.stream().map(this::createLdapProvider)
- .collect(Collectors.toList());
- AuthenticationManager manager = new ProviderManager(providers);
- return new ReactiveAuthenticationManagerAdapter(manager);
- }
-
- private AuthenticationProvider createLdapProvider(LdapServerConfig ldapConfig) {
- log.info("Creating LDAP AuthenticationProvider for {}", ldapConfig.getUrl());
- final BaseLdapPathContextSource source = contextSource(ldapConfig);
- final BindAuthenticator authenticator = ldapAuthenticator(ldapConfig, source);
- final DefaultLdapAuthoritiesPopulator rolesPopulator = ldapAuthoritiesPopulator(ldapConfig, source);
-
- LdapAuthenticationProvider provider;
- if (ldapConfig.hasGeorchestraExtensions()) {
- String configName = ldapConfig.getName();
- provider = new GeorchestraLdapAuthenticationProvider(configName, authenticator, rolesPopulator);
- } else {
- provider = new LdapAuthenticationProvider(authenticator, rolesPopulator);
- }
-
- final GrantedAuthoritiesMapper rolesMapper = ldapAuthoritiesMapper(ldapConfig);
- provider.setAuthoritiesMapper(rolesMapper);
- return provider;
- }
-
- private static class GeorchestraLdapAuthenticationProvider extends LdapAuthenticationProvider {
-
- private String configName;
-
- GeorchestraLdapAuthenticationProvider(//
- String configName, //
- LdapAuthenticator authenticator, //
- LdapAuthoritiesPopulator authoritiesPopulator) {
-
- super(authenticator, authoritiesPopulator);
- this.configName = configName;
- }
-
- @Override
- public Authentication authenticate(Authentication authentication) throws AuthenticationException {
- Authentication auth = super.authenticate(authentication);
- log.debug("Authenticated {} with roles {}", auth.getName(), auth.getAuthorities());
- return new GeorchestraUserNamePasswordAuthenticationToken(configName, auth);
- }
- }
-
- private BindAuthenticator ldapAuthenticator(Server server, final BaseLdapPathContextSource contextSource) {
- final String ldapUserSearchBase = server.getUsers().getRdn();
- final String ldapUserSearchFilter = server.getUsers().getSearchFilter();
-
- FilterBasedLdapUserSearch search = new FilterBasedLdapUserSearch(ldapUserSearchBase, ldapUserSearchFilter,
- contextSource);
-
- BindAuthenticator authenticator = new BindAuthenticator(contextSource);
- authenticator.setUserSearch(search);
- authenticator.afterPropertiesSet();
- return authenticator;
- }
-
- private BaseLdapPathContextSource contextSource(LdapServerConfig server) {
- LdapContextSource context = new LdapContextSource();
- context.setUrl(server.getUrl());
- context.setBase(server.getBaseDn());
- context.afterPropertiesSet();
- return context;
- }
-
- private GrantedAuthoritiesMapper ldapAuthoritiesMapper(LdapServerConfig server) {
- boolean upperCase = server.getRoles().isUpperCase();
- SimpleAuthorityMapper authorityMapper = new SimpleAuthorityMapper();
- authorityMapper.setConvertToUpperCase(upperCase);
- return authorityMapper;
- }
-
- private DefaultLdapAuthoritiesPopulator ldapAuthoritiesPopulator(LdapServerConfig server,
- BaseLdapPathContextSource contextSource) {
-
- String ldapGroupSearchBase = server.getRoles().getRdn();
- String ldapGroupSearchFilter = server.getRoles().getSearchFilter();
-
- DefaultLdapAuthoritiesPopulator authoritiesPopulator = new DefaultLdapAuthoritiesPopulator(contextSource,
- ldapGroupSearchBase);
- authoritiesPopulator.setGroupSearchFilter(ldapGroupSearchFilter);
-
- String prefix = server.getRoles().getPrefix();
- if (null != prefix) {
- authoritiesPopulator.setRolePrefix(prefix);
- }
-
- return authoritiesPopulator;
- }
-}
diff --git a/gateway/src/main/java/org/georchestra/gateway/security/ldap/activedirectory/ActiveDirectoryAuthenticationConfiguration.java b/gateway/src/main/java/org/georchestra/gateway/security/ldap/activedirectory/ActiveDirectoryAuthenticationConfiguration.java
new file mode 100644
index 00000000..28b515b0
--- /dev/null
+++ b/gateway/src/main/java/org/georchestra/gateway/security/ldap/activedirectory/ActiveDirectoryAuthenticationConfiguration.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2022 by the geOrchestra PSC
+ *
+ * This file is part of geOrchestra.
+ *
+ * geOrchestra is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free
+ * Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * geOrchestra is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * geOrchestra. If not, see .
+ */
+package org.georchestra.gateway.security.ldap.activedirectory;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import org.georchestra.gateway.security.ldap.LdapConfigProperties;
+import org.georchestra.gateway.security.ldap.basic.LdapAuthenticatedUserMapper;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.authentication.ProviderManager;
+import org.springframework.security.authentication.ReactiveAuthenticationManager;
+import org.springframework.security.authentication.ReactiveAuthenticationManagerAdapter;
+import org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider;
+
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ */
+@Configuration(proxyBeanMethods = false)
+@EnableConfigurationProperties(LdapConfigProperties.class)
+@Slf4j(topic = "org.georchestra.gateway.security.ldap.activedirectory")
+public class ActiveDirectoryAuthenticationConfiguration {
+
+ /**
+ * @return a {@link ReactiveAuthenticationManager} that will probe
+ * username/password authentication over all configured and enabled
+ * {@link LdapConfigProperties#activeDirectory() ActiveDirectory}
+ * services in {@link LdapConfigProperties}, returning the first
+ * successful authorization.
+ */
+ @Bean
+ public ReactiveAuthenticationManager activeDirectoryAuthenticationManager(
+ List adProviders) {
+ if (adProviders.isEmpty())
+ return null;
+ List providers = adProviders.stream().map(AuthenticationProvider.class::cast)
+ .collect(Collectors.toList());
+
+ return new ReactiveAuthenticationManagerAdapter(new ProviderManager(providers));
+ }
+
+ @Bean
+ public LdapAuthenticatedUserMapper activeDirectoryAuthenticatedUserMapper(
+ List enabledConfigs) {
+ return enabledConfigs.isEmpty() ? null : new LdapAuthenticatedUserMapper();
+ }
+
+ @Bean
+ List enabledActiveDirectoryLdapConfigs(LdapConfigProperties config) {
+ return config.activeDirectoryEnabled();
+ }
+
+ @Bean
+ List activeDirectoryLdapAuthenticationProviders(
+ List configs) {
+ return configs.stream().map(this::activeDirectoryAuthenticationProvider).collect(Collectors.toList());
+ }
+
+ private ActiveDirectoryLdapAuthenticationProvider activeDirectoryAuthenticationProvider(
+ ActiveDirectoryLdapServerConfig config) {
+
+ final String url = config.getUrl();
+ final String domain = config.getDomain().orElse(null);
+ final String rootDn = config.getRootDn().orElse(null);
+
+ // defaults to (&(objectClass=user)(userPrincipalName={0})) in
+ // ActiveDirectoryLdapAuthenticationProvider
+ final Optional searchFilter = config.getSearchFilter();
+
+ ActiveDirectoryLdapAuthenticationProvider adAuth = new ActiveDirectoryLdapAuthenticationProvider(domain, url,
+ rootDn);
+ // throw AccountStatusException subclasses, prevents the ProviderManager to
+ // continue trying other providers if the account is found and
+ // expired/disabled/locked
+ adAuth.setConvertSubErrorCodesToExceptions(true);
+ searchFilter.ifPresent(filter -> {
+ log.info("Using custom search filter for Active Directory config {}: {}", config.getName(), filter);
+ adAuth.setSearchFilter(filter);
+ });
+ return adAuth;
+ }
+}
diff --git a/gateway/src/main/java/org/georchestra/gateway/security/ldap/activedirectory/ActiveDirectoryLdapServerConfig.java b/gateway/src/main/java/org/georchestra/gateway/security/ldap/activedirectory/ActiveDirectoryLdapServerConfig.java
new file mode 100644
index 00000000..e042a613
--- /dev/null
+++ b/gateway/src/main/java/org/georchestra/gateway/security/ldap/activedirectory/ActiveDirectoryLdapServerConfig.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2022 by the geOrchestra PSC
+ *
+ * This file is part of geOrchestra.
+ *
+ * geOrchestra is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free
+ * Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * geOrchestra is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * geOrchestra. If not, see .
+ */
+
+package org.georchestra.gateway.security.ldap.activedirectory;
+
+import java.util.Optional;
+
+import lombok.Builder;
+import lombok.Generated;
+import lombok.NonNull;
+import lombok.Value;
+
+@Value
+@Builder
+@Generated
+public class ActiveDirectoryLdapServerConfig {
+ private @NonNull String name;
+ private boolean enabled;
+ private @NonNull String url;
+ private @NonNull Optional domain;
+ private @NonNull Optional rootDn;
+ private @NonNull Optional searchFilter;
+}
\ No newline at end of file
diff --git a/gateway/src/main/java/org/georchestra/gateway/security/ldap/basic/BasicLdapAuthenticationConfiguration.java b/gateway/src/main/java/org/georchestra/gateway/security/ldap/basic/BasicLdapAuthenticationConfiguration.java
new file mode 100644
index 00000000..f7af2ebe
--- /dev/null
+++ b/gateway/src/main/java/org/georchestra/gateway/security/ldap/basic/BasicLdapAuthenticationConfiguration.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2022 by the geOrchestra PSC
+ *
+ * This file is part of geOrchestra.
+ *
+ * geOrchestra is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free
+ * Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * geOrchestra is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * geOrchestra. If not, see .
+ */
+package org.georchestra.gateway.security.ldap.basic;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.georchestra.gateway.security.ServerHttpSecurityCustomizer;
+import org.georchestra.gateway.security.ldap.LdapConfigProperties;
+import org.georchestra.gateway.security.ldap.extended.ExtendedLdapAuthenticationConfiguration;
+import org.springframework.beans.factory.BeanCreationException;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.authentication.ProviderManager;
+import org.springframework.security.authentication.ReactiveAuthenticationManager;
+import org.springframework.security.authentication.ReactiveAuthenticationManagerAdapter;
+import org.springframework.security.config.web.server.ServerHttpSecurity;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.ldap.authentication.LdapAuthenticationProvider;
+import org.springframework.security.ldap.userdetails.LdapUserDetails;
+
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * {@link ServerHttpSecurityCustomizer} to enable LDAP based authentication and
+ * authorization across multiple LDAP databases.
+ *
+ * This configuration sets up the required beans for spring-based LDAP
+ * authentication and authorization, using {@link LdapConfigProperties} to get
+ * the {@link LdapConfigProperties#getUrl() connection URL} and the
+ * {@link LdapConfigProperties#getBaseDn() base DN}.
+ *
+ * As a result, the {@link ServerHttpSecurity} will have HTTP-Basic
+ * authentication enabled and {@link ServerHttpSecurity#formLogin() form login}
+ * set up.
+ *
+ * Upon successful authentication, the corresponding {@link Authentication} with
+ * an {@link LdapUserDetails} as {@link Authentication#getPrincipal() principal}
+ * and the roles extracted from LDAP as {@link Authentication#getAuthorities()
+ * authorities}, will be set as the security context's
+ * {@link SecurityContext#getAuthentication() authentication} property.
+ *
+ * Note however, this may not be enough information to convey
+ * geOrchestra-specific HTTP request headers to backend services, depending on
+ * the matching gateway-route configuration. See
+ * {@link ExtendedLdapAuthenticationConfiguration} for further details.
+ *
+ * @see ExtendedLdapAuthenticationConfiguration
+ * @see LdapConfigProperties
+ */
+@Configuration(proxyBeanMethods = false)
+@EnableConfigurationProperties(LdapConfigProperties.class)
+@Slf4j(topic = "org.georchestra.gateway.security.ldap.basic")
+public class BasicLdapAuthenticationConfiguration {
+
+ /**
+ * @return a {@link ReactiveAuthenticationManager} that will probe
+ * username/password authentication over all
+ * {@link LdapConfigProperties#simple() simple} configured and enabled
+ * LDAP databases in {@link LdapConfigProperties}, returning the first
+ * successful authorization.
+ *
+ * @see #ldapAuthenticationProviders
+ */
+ @Bean
+ public ReactiveAuthenticationManager ldapAuthenticationManager(List ldapProviders) {
+ return ldapProviders.isEmpty() ? null
+ : new ReactiveAuthenticationManagerAdapter(new ProviderManager(
+ ldapProviders.stream().map(AuthenticationProvider.class::cast).collect(Collectors.toList())));
+ }
+
+ @Bean
+ public LdapAuthenticatedUserMapper ldapAuthenticatedUserMapper(List enabledConfigs) {
+ return enabledConfigs.isEmpty() ? null : new LdapAuthenticatedUserMapper();
+ }
+
+ @Bean
+ List enabledSimpleLdapConfigs(LdapConfigProperties config) {
+ return config.simpleEnabled();
+ }
+
+ @Bean
+ List ldapAuthenticationProviders(List configs) {
+ return configs.stream().map(this::createLdapProvider).collect(Collectors.toList());
+ }
+
+ private LdapAuthenticationProvider createLdapProvider(LdapServerConfig config) {
+ log.info("Creating LDAP AuthenticationProvider {} with URL {}", config.getName(), config.getUrl());
+
+ try {
+ return new LdapAuthenticatorProviderBuilder()//
+ .url(config.getUrl())//
+ .baseDn(config.getBaseDn())//
+ .userSearchBase(config.getUsersRdn())//
+ .userSearchFilter(config.getUsersSearchFilter())//
+ .rolesSearchBase(config.getRolesRdn())//
+ .rolesSearchFilter(config.getRolesSearchFilter())//
+ .rolesPrefix(config.getRolesPrefix().orElse(null))//
+ .rolesUpperCase(config.isRolesUpperCase())//
+ .build();
+ } catch (RuntimeException e) {
+ throw new BeanCreationException(
+ "Error creating LDAP Authentication Provider for config " + config + ": " + e.getMessage(), e);
+ }
+ }
+}
diff --git a/gateway/src/main/java/org/georchestra/gateway/security/ldap/LdapAuthenticatedUserMapper.java b/gateway/src/main/java/org/georchestra/gateway/security/ldap/basic/LdapAuthenticatedUserMapper.java
similarity index 98%
rename from gateway/src/main/java/org/georchestra/gateway/security/ldap/LdapAuthenticatedUserMapper.java
rename to gateway/src/main/java/org/georchestra/gateway/security/ldap/basic/LdapAuthenticatedUserMapper.java
index a04d7c99..50bad1d9 100644
--- a/gateway/src/main/java/org/georchestra/gateway/security/ldap/LdapAuthenticatedUserMapper.java
+++ b/gateway/src/main/java/org/georchestra/gateway/security/ldap/basic/LdapAuthenticatedUserMapper.java
@@ -17,7 +17,7 @@
* geOrchestra. If not, see .
*/
-package org.georchestra.gateway.security.ldap;
+package org.georchestra.gateway.security.ldap.basic;
import java.util.Collection;
import java.util.List;
diff --git a/gateway/src/main/java/org/georchestra/gateway/security/ldap/basic/LdapAuthenticatorProviderBuilder.java b/gateway/src/main/java/org/georchestra/gateway/security/ldap/basic/LdapAuthenticatorProviderBuilder.java
new file mode 100644
index 00000000..23e87de1
--- /dev/null
+++ b/gateway/src/main/java/org/georchestra/gateway/security/ldap/basic/LdapAuthenticatorProviderBuilder.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2022 by the geOrchestra PSC
+ *
+ * This file is part of geOrchestra.
+ *
+ * geOrchestra is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free
+ * Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * geOrchestra is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * geOrchestra. If not, see .
+ */
+package org.georchestra.gateway.security.ldap.basic;
+
+import static java.util.Objects.requireNonNull;
+
+import javax.annotation.Nullable;
+
+import org.springframework.ldap.core.support.BaseLdapPathContextSource;
+import org.springframework.ldap.core.support.LdapContextSource;
+import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
+import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;
+import org.springframework.security.ldap.authentication.BindAuthenticator;
+import org.springframework.security.ldap.authentication.LdapAuthenticationProvider;
+import org.springframework.security.ldap.search.FilterBasedLdapUserSearch;
+import org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator;
+
+import lombok.Setter;
+import lombok.experimental.Accessors;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ */
+@Slf4j(topic = "org.georchestra.gateway.security.ldap.basic")
+@Accessors(chain = true, fluent = true)
+public class LdapAuthenticatorProviderBuilder {
+
+ private @Setter String url;
+ private @Setter String baseDn;
+
+ private @Setter String userSearchBase;
+ private @Setter String userSearchFilter;
+
+ private @Setter String rolesSearchBase;
+ private @Setter String rolesSearchFilter;
+
+ private @Setter @Nullable String rolesPrefix;
+ private @Setter @Nullable Boolean rolesUpperCase;
+
+ public LdapAuthenticationProvider build() {
+ requireNonNull(url, "url is not set");
+ requireNonNull(baseDn, "baseDn is not set");
+ requireNonNull(userSearchBase, "userSearchBase is not set");
+ requireNonNull(userSearchFilter, "userSearchFilter is not set");
+ requireNonNull(rolesSearchBase, "rolesSearchBase is not set");
+ requireNonNull(rolesSearchFilter, "rolesSearchFilter is not set");
+
+ log.info("Creating LDAP AuthenticationProvider for {}", url);
+ final BaseLdapPathContextSource source = contextSource();
+ final BindAuthenticator authenticator = ldapAuthenticator(source);
+ final DefaultLdapAuthoritiesPopulator rolesPopulator = ldapAuthoritiesPopulator(source);
+
+ LdapAuthenticationProvider provider = new LdapAuthenticationProvider(authenticator, rolesPopulator);
+
+ final GrantedAuthoritiesMapper rolesMapper = ldapAuthoritiesMapper();
+ provider.setAuthoritiesMapper(rolesMapper);
+ return provider;
+ }
+
+ private BindAuthenticator ldapAuthenticator(BaseLdapPathContextSource contextSource) {
+ FilterBasedLdapUserSearch search = new FilterBasedLdapUserSearch(userSearchBase, userSearchFilter,
+ contextSource);
+
+ BindAuthenticator authenticator = new BindAuthenticator(contextSource);
+ authenticator.setUserSearch(search);
+ authenticator.afterPropertiesSet();
+ return authenticator;
+ }
+
+ private BaseLdapPathContextSource contextSource() {
+ LdapContextSource context = new LdapContextSource();
+ context.setUrl(url);
+ context.setBase(baseDn);
+ context.afterPropertiesSet();
+ return context;
+ }
+
+ private GrantedAuthoritiesMapper ldapAuthoritiesMapper() {
+ SimpleAuthorityMapper authorityMapper = new SimpleAuthorityMapper();
+ if (null != rolesUpperCase) {
+ authorityMapper.setConvertToUpperCase(rolesUpperCase);
+ }
+ return authorityMapper;
+ }
+
+ private DefaultLdapAuthoritiesPopulator ldapAuthoritiesPopulator(BaseLdapPathContextSource contextSource) {
+ DefaultLdapAuthoritiesPopulator authoritiesPopulator = new DefaultLdapAuthoritiesPopulator(contextSource,
+ rolesSearchBase);
+ authoritiesPopulator.setGroupSearchFilter(rolesSearchFilter);
+
+ if (null != rolesPrefix) {
+ authoritiesPopulator.setRolePrefix(rolesPrefix);
+ }
+
+ return authoritiesPopulator;
+ }
+}
diff --git a/gateway/src/main/java/org/georchestra/gateway/security/ldap/basic/LdapServerConfig.java b/gateway/src/main/java/org/georchestra/gateway/security/ldap/basic/LdapServerConfig.java
new file mode 100644
index 00000000..3288a981
--- /dev/null
+++ b/gateway/src/main/java/org/georchestra/gateway/security/ldap/basic/LdapServerConfig.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2022 by the geOrchestra PSC
+ *
+ * This file is part of geOrchestra.
+ *
+ * geOrchestra is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free
+ * Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * geOrchestra is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * geOrchestra. If not, see .
+ */
+
+package org.georchestra.gateway.security.ldap.basic;
+
+import java.util.Optional;
+
+import lombok.Builder;
+import lombok.Generated;
+import lombok.NonNull;
+import lombok.Value;
+
+@Value
+@Builder
+@Generated
+public class LdapServerConfig {
+ private @NonNull String name;
+ private boolean enabled;
+ private @NonNull String url;
+ private @NonNull String baseDn;
+
+ private @NonNull String usersRdn;
+ private @NonNull String usersSearchFilter;
+ private @NonNull String rolesRdn;
+ private @NonNull String rolesSearchFilter;
+ private @NonNull Optional rolesPrefix;
+ private boolean rolesUpperCase;
+}
\ No newline at end of file
diff --git a/gateway/src/main/java/org/georchestra/gateway/security/ldap/DemultiplexingUsersApi.java b/gateway/src/main/java/org/georchestra/gateway/security/ldap/extended/DemultiplexingUsersApi.java
similarity index 96%
rename from gateway/src/main/java/org/georchestra/gateway/security/ldap/DemultiplexingUsersApi.java
rename to gateway/src/main/java/org/georchestra/gateway/security/ldap/extended/DemultiplexingUsersApi.java
index e2c90448..dfb40533 100644
--- a/gateway/src/main/java/org/georchestra/gateway/security/ldap/DemultiplexingUsersApi.java
+++ b/gateway/src/main/java/org/georchestra/gateway/security/ldap/extended/DemultiplexingUsersApi.java
@@ -17,7 +17,7 @@
* geOrchestra. If not, see .
*/
-package org.georchestra.gateway.security.ldap;
+package org.georchestra.gateway.security.ldap.extended;
import java.util.HashSet;
import java.util.Map;
@@ -46,7 +46,7 @@
* {@literal username}.
*/
@RequiredArgsConstructor
-public class DemultiplexingUsersApi {
+class DemultiplexingUsersApi {
private final @NonNull Map targets;
diff --git a/gateway/src/main/java/org/georchestra/gateway/security/ldap/extended/ExtendedLdapAuthenticationConfiguration.java b/gateway/src/main/java/org/georchestra/gateway/security/ldap/extended/ExtendedLdapAuthenticationConfiguration.java
new file mode 100644
index 00000000..2f68cdfe
--- /dev/null
+++ b/gateway/src/main/java/org/georchestra/gateway/security/ldap/extended/ExtendedLdapAuthenticationConfiguration.java
@@ -0,0 +1,255 @@
+package org.georchestra.gateway.security.ldap.extended;
+
+/*
+ * Copyright (C) 2022 by the geOrchestra PSC
+ *
+ * This file is part of geOrchestra.
+ *
+ * geOrchestra is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free
+ * Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * geOrchestra is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * geOrchestra. If not, see .
+ */
+
+import static java.util.Objects.requireNonNull;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import org.georchestra.ds.orgs.OrgsDao;
+import org.georchestra.ds.orgs.OrgsDaoImpl;
+import org.georchestra.ds.roles.RoleDao;
+import org.georchestra.ds.roles.RoleDaoImpl;
+import org.georchestra.ds.roles.RoleProtected;
+import org.georchestra.ds.security.UserMapper;
+import org.georchestra.ds.security.UserMapperImpl;
+import org.georchestra.ds.security.UsersApiImpl;
+import org.georchestra.ds.users.AccountDao;
+import org.georchestra.ds.users.AccountDaoImpl;
+import org.georchestra.ds.users.UserRule;
+import org.georchestra.gateway.security.GeorchestraUserMapperExtension;
+import org.georchestra.gateway.security.ldap.LdapConfigProperties;
+import org.georchestra.gateway.security.ldap.LdapConfigProperties.Server;
+import org.georchestra.gateway.security.ldap.basic.LdapAuthenticatorProviderBuilder;
+import org.georchestra.security.api.UsersApi;
+import org.georchestra.security.model.GeorchestraUser;
+import org.springframework.beans.factory.BeanInitializationException;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.ldap.core.LdapTemplate;
+import org.springframework.ldap.core.support.LdapContextSource;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.authentication.ProviderManager;
+import org.springframework.security.authentication.ReactiveAuthenticationManager;
+import org.springframework.security.authentication.ReactiveAuthenticationManagerAdapter;
+import org.springframework.security.ldap.authentication.LdapAuthenticationProvider;
+import org.springframework.security.ldap.userdetails.LdapUserDetails;
+
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * Sets up a {@link GeorchestraUserMapperExtension} that knows how to map an
+ * authentication credentials given by a
+ * {@link GeorchestraUserNamePasswordAuthenticationToken} with an
+ * {@link LdapUserDetails} (i.e., if the user authenticated with LDAP), to a
+ * {@link GeorchestraUser}, making use of geOrchestra's
+ * {@literal georchestra-ldap-account-management} module's {@link UsersApi}.
+ */
+@Configuration(proxyBeanMethods = false)
+@EnableConfigurationProperties(LdapConfigProperties.class)
+@Slf4j(topic = "org.georchestra.gateway.security.ldap.extended")
+public class ExtendedLdapAuthenticationConfiguration {
+
+ @Bean
+ public GeorchestraLdapAuthenticatedUserMapper georchestraLdapAuthenticatedUserMapper(DemultiplexingUsersApi users) {
+ return users.getTargetNames().isEmpty() ? null : new GeorchestraLdapAuthenticatedUserMapper(users);
+ }
+
+ /**
+ * @return a {@link ReactiveAuthenticationManager} that will probe
+ * username/password authentication over all
+ * {@link LdapConfigProperties#extended() extended} configured and
+ * enabled LDAP databases in {@link LdapConfigProperties}, returning the
+ * first successful authorization.
+ *
+ * @see #extendedLdapAuthenticationProviders
+ */
+ @Bean
+ public ReactiveAuthenticationManager extendedLdapAuthenticationManager(
+ List georExtendedProviders) {
+ if (georExtendedProviders.isEmpty())
+ return null;
+
+ List providers = georExtendedProviders.stream().map(AuthenticationProvider.class::cast)
+ .collect(Collectors.toList());
+
+ return new ReactiveAuthenticationManagerAdapter(new ProviderManager(providers));
+ }
+
+ @Bean
+ List enabledExtendedLdapConfigs(LdapConfigProperties config) {
+ return config.extendedEnabled();
+ }
+
+ @Bean
+ List extendedLdapAuthenticationProviders(List configs) {
+ return configs.stream().map(this::createLdapProvider).collect(Collectors.toList());
+ }
+
+ private GeorchestraLdapAuthenticationProvider createLdapProvider(ExtendedLdapConfig config) {
+ log.info("Creating extended LDAP AuthenticationProvider {} at {}", config.getName(), config.getUrl());
+
+ LdapAuthenticationProvider delegate = new LdapAuthenticatorProviderBuilder()//
+ .url(config.getUrl())//
+ .baseDn(config.getBaseDn())//
+ .userSearchBase(config.getUsersRdn())//
+ .userSearchFilter(config.getUsersSearchFilter())//
+ .rolesSearchBase(config.getRolesRdn())//
+ .rolesSearchFilter(config.getRolesSearchFilter())//
+ .rolesPrefix(config.getRolesPrefix().orElse(null))//
+ .rolesUpperCase(config.isRolesUpperCase())//
+ .build();
+
+ return new GeorchestraLdapAuthenticationProvider(config.getName(), delegate);
+ }
+
+ @Bean
+ DemultiplexingUsersApi demultiplexingUsersApi(List configs) {
+ Map targets = new HashMap<>();
+ for (ExtendedLdapConfig config : configs) {
+ try {
+ targets.put(config.getName(), createUsersApi(config));
+ } catch (Exception ex) {
+ throw new BeanInitializationException(
+ "Error creating georchestra users api for ldap config " + config.getName(), ex);
+ }
+ }
+ return new DemultiplexingUsersApi(targets);
+ }
+
+ //////////////////////////////////////////////
+ /// Low level LDAP account management beans
+ //////////////////////////////////////////////
+
+ private UsersApi createUsersApi(ExtendedLdapConfig ldapConfig) throws Exception {
+ final LdapTemplate ldapTemplate = ldapTemplate(ldapConfig);
+ final AccountDao accountsDao = accountsDao(ldapTemplate, ldapConfig);
+ final RoleDao roleDao = roleDao(ldapTemplate, ldapConfig, accountsDao);
+
+ final UserMapper ldapUserMapper = createUserMapper(roleDao);
+ UserRule userRule = ldapUserRule(ldapConfig);
+
+ UsersApiImpl impl = new UsersApiImpl();
+ impl.setAccountsDao(accountsDao);
+ impl.setMapper(ldapUserMapper);
+ impl.setUserRule(userRule);
+ return impl;
+ }
+
+ private UserMapper createUserMapper(RoleDao roleDao) {
+ UserMapperImpl impl = new UserMapperImpl();
+ impl.setRoleDao(roleDao);
+ return impl;
+ }
+
+ private LdapTemplate ldapTemplate(ExtendedLdapConfig server) throws Exception {
+ LdapContextSource contextSource = new LdapContextSource();
+ contextSource.setUrl(server.getUrl());
+ contextSource.setBase(server.getBaseDn());
+ contextSource.afterPropertiesSet();
+
+ LdapTemplate ldapTemplate = new LdapTemplate(contextSource);
+ ldapTemplate.afterPropertiesSet();
+ return ldapTemplate;
+ }
+
+ private AccountDao accountsDao(LdapTemplate ldapTemplate, ExtendedLdapConfig ldapConfig) {
+ String baseDn = ldapConfig.getBaseDn();
+ String userSearchBaseDN = ldapConfig.getUsersRdn();
+ String roleSearchBaseDN = ldapConfig.getRolesRdn();
+
+ // we don't need a configuration property for this,
+ // we don't allow pending users to log in. The LdapAuthenticationProvider won't
+ // even look them up.
+ final String pendingUsersSearchBaseDN = "ou=pendingusers";
+
+ AccountDaoImpl impl = new AccountDaoImpl(ldapTemplate);
+ impl.setBasePath(baseDn);
+ impl.setUserSearchBaseDN(userSearchBaseDN);
+ impl.setRoleSearchBaseDN(roleSearchBaseDN);
+ if (pendingUsersSearchBaseDN != null) {
+ impl.setPendingUserSearchBaseDN(pendingUsersSearchBaseDN);
+ }
+
+ String orgSearchBaseDN = ldapConfig.getOrgsRdn();
+ requireNonNull(orgSearchBaseDN);
+ impl.setOrgSearchBaseDN(orgSearchBaseDN);
+
+ // not needed here, only console cares, we shouldn't allow to authenticate
+ // pending users, should we?
+ final String pendingOrgSearchBaseDN = "ou=pendingorgs";
+ impl.setPendingOrgSearchBaseDN(pendingOrgSearchBaseDN);
+
+ impl.init();
+ return impl;
+ }
+
+ private RoleDao roleDao(LdapTemplate ldapTemplate, ExtendedLdapConfig ldapConfig, AccountDao accountDao) {
+ final String rolesRdn = ldapConfig.getRolesRdn();
+ RoleDaoImpl impl = new RoleDaoImpl();
+ impl.setLdapTemplate(ldapTemplate);
+ impl.setRoleSearchBaseDN(rolesRdn);
+ impl.setAccountDao(accountDao);
+ impl.setRoles(ldapProtectedRoles(ldapConfig));
+ return impl;
+ }
+
+ @SuppressWarnings("unused")
+ private OrgsDao orgsDao(LdapTemplate ldapTemplate, Server ldapConfig) {
+ OrgsDaoImpl impl = new OrgsDaoImpl();
+ impl.setLdapTemplate(ldapTemplate);
+ impl.setBasePath(ldapConfig.getBaseDn());
+ impl.setOrgSearchBaseDN(ldapConfig.getOrgs().getRdn());
+
+ final String pendingOrgSearchBaseDN = "ou=pendingorgs";
+
+ // not needed here, only console cares, we shouldn't allow to authenticate
+ // pending users, should we?
+ impl.setPendingOrgSearchBaseDN(pendingOrgSearchBaseDN);
+ // not needed here, only console's OrgsController cares about this, right?
+ // final String orgTypes = "Association,Company,NGO,Individual,Other";
+ // impl.setOrgTypeValues(orgTypes);
+ return impl;
+ }
+
+ private UserRule ldapUserRule(ExtendedLdapConfig ldapConfig) {
+ // we can't possibly try to delete a protected user, so no need to configure
+ // them
+ List protectedUsers = Collections.emptyList();
+ UserRule rule = new UserRule();
+ rule.setListOfprotectedUsers(protectedUsers.toArray(String[]::new));
+ return rule;
+ }
+
+ private RoleProtected ldapProtectedRoles(ExtendedLdapConfig ldapConfig) {
+ // protected roles are used by the console service to avoid deleting them. This
+ // application will never try to do so, so we don't care about configuring them
+ List protectedRoles = List.of();
+ RoleProtected bean = new RoleProtected();
+ bean.setListOfprotectedRoles(protectedRoles.toArray(String[]::new));
+ return bean;
+ }
+
+}
diff --git a/gateway/src/main/java/org/georchestra/gateway/autoconfigure/security/GeorchestraLdapAccountManagementAutoConfiguration.java b/gateway/src/main/java/org/georchestra/gateway/security/ldap/extended/ExtendedLdapConfig.java
similarity index 51%
rename from gateway/src/main/java/org/georchestra/gateway/autoconfigure/security/GeorchestraLdapAccountManagementAutoConfiguration.java
rename to gateway/src/main/java/org/georchestra/gateway/security/ldap/extended/ExtendedLdapConfig.java
index 85ddd8ae..203f2a1d 100644
--- a/gateway/src/main/java/org/georchestra/gateway/autoconfigure/security/GeorchestraLdapAccountManagementAutoConfiguration.java
+++ b/gateway/src/main/java/org/georchestra/gateway/security/ldap/extended/ExtendedLdapConfig.java
@@ -16,23 +16,31 @@
* You should have received a copy of the GNU General Public License along with
* geOrchestra. If not, see .
*/
-package org.georchestra.gateway.autoconfigure.security;
-import javax.annotation.PostConstruct;
+package org.georchestra.gateway.security.ldap.extended;
-import org.georchestra.gateway.security.ldap.GeorchestraLdapAccountManagementConfiguration;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.context.annotation.Import;
+import java.util.Optional;
-import lombok.extern.slf4j.Slf4j;
+import lombok.Builder;
+import lombok.Generated;
+import lombok.NonNull;
+import lombok.Value;
-@Configuration(proxyBeanMethods = false)
-@ConditionalOnLdapEnabled
-@Import(GeorchestraLdapAccountManagementConfiguration.class)
-@Slf4j(topic = "org.georchestra.gateway.autoconfigure.security")
-public class GeorchestraLdapAccountManagementAutoConfiguration {
+@Value
+@Builder
+@Generated
+public class ExtendedLdapConfig {
+ private @NonNull String name;
+ private boolean enabled;
+ private @NonNull String url;
+ private @NonNull String baseDn;
- public @PostConstruct void log() {
- log.info("georchestra LDAP security extensions enabled");
- }
-}
+ private @NonNull String usersRdn;
+ private @NonNull String usersSearchFilter;
+ private @NonNull String rolesRdn;
+ private @NonNull String rolesSearchFilter;
+ private @NonNull Optional rolesPrefix;
+ private boolean rolesUpperCase;
+
+ private @NonNull String orgsRdn;
+}
\ No newline at end of file
diff --git a/gateway/src/main/java/org/georchestra/gateway/security/ldap/GeorchestraLdapAuthenticatedUserMapper.java b/gateway/src/main/java/org/georchestra/gateway/security/ldap/extended/GeorchestraLdapAuthenticatedUserMapper.java
similarity index 94%
rename from gateway/src/main/java/org/georchestra/gateway/security/ldap/GeorchestraLdapAuthenticatedUserMapper.java
rename to gateway/src/main/java/org/georchestra/gateway/security/ldap/extended/GeorchestraLdapAuthenticatedUserMapper.java
index 6657ab64..4ebc78fd 100644
--- a/gateway/src/main/java/org/georchestra/gateway/security/ldap/GeorchestraLdapAuthenticatedUserMapper.java
+++ b/gateway/src/main/java/org/georchestra/gateway/security/ldap/extended/GeorchestraLdapAuthenticatedUserMapper.java
@@ -17,7 +17,7 @@
* geOrchestra. If not, see .
*/
-package org.georchestra.gateway.security.ldap;
+package org.georchestra.gateway.security.ldap.extended;
import java.util.Optional;
@@ -42,7 +42,7 @@
* @see DemultiplexingUsersApi
*/
@RequiredArgsConstructor
-public class GeorchestraLdapAuthenticatedUserMapper implements GeorchestraUserMapperExtension {
+class GeorchestraLdapAuthenticatedUserMapper implements GeorchestraUserMapperExtension {
private final @NonNull DemultiplexingUsersApi users;
diff --git a/gateway/src/main/java/org/georchestra/gateway/security/ldap/extended/GeorchestraLdapAuthenticationProvider.java b/gateway/src/main/java/org/georchestra/gateway/security/ldap/extended/GeorchestraLdapAuthenticationProvider.java
new file mode 100644
index 00000000..80881175
--- /dev/null
+++ b/gateway/src/main/java/org/georchestra/gateway/security/ldap/extended/GeorchestraLdapAuthenticationProvider.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2022 by the geOrchestra PSC
+ *
+ * This file is part of geOrchestra.
+ *
+ * geOrchestra is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free
+ * Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * geOrchestra is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * geOrchestra. If not, see .
+ */
+
+package org.georchestra.gateway.security.ldap.extended;
+
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+
+import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+@RequiredArgsConstructor
+@Slf4j(topic = "org.georchestra.gateway.security.ldap.extended")
+class GeorchestraLdapAuthenticationProvider implements AuthenticationProvider {
+
+ private final @NonNull String configName;
+ private final @NonNull AuthenticationProvider delegate;
+
+ @Override
+ public boolean supports(Class> authentication) {
+ return delegate.supports(authentication);
+ }
+
+ @Override
+ public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+ Authentication auth = delegate.authenticate(authentication);
+ log.debug("Authenticated {} with roles {}", auth.getName(), auth.getAuthorities());
+ return new GeorchestraUserNamePasswordAuthenticationToken(configName, auth);
+ }
+
+}
\ No newline at end of file
diff --git a/gateway/src/main/java/org/georchestra/gateway/security/ldap/GeorchestraUserNamePasswordAuthenticationToken.java b/gateway/src/main/java/org/georchestra/gateway/security/ldap/extended/GeorchestraUserNamePasswordAuthenticationToken.java
similarity index 94%
rename from gateway/src/main/java/org/georchestra/gateway/security/ldap/GeorchestraUserNamePasswordAuthenticationToken.java
rename to gateway/src/main/java/org/georchestra/gateway/security/ldap/extended/GeorchestraUserNamePasswordAuthenticationToken.java
index 926b0f28..79d6a68b 100644
--- a/gateway/src/main/java/org/georchestra/gateway/security/ldap/GeorchestraUserNamePasswordAuthenticationToken.java
+++ b/gateway/src/main/java/org/georchestra/gateway/security/ldap/extended/GeorchestraUserNamePasswordAuthenticationToken.java
@@ -17,7 +17,7 @@
* geOrchestra. If not, see .
*/
-package org.georchestra.gateway.security.ldap;
+package org.georchestra.gateway.security.ldap.extended;
import java.util.Collection;
@@ -35,7 +35,7 @@
* can be used to fetch additional user identity information.
*/
@RequiredArgsConstructor
-public class GeorchestraUserNamePasswordAuthenticationToken implements Authentication {
+class GeorchestraUserNamePasswordAuthenticationToken implements Authentication {
private static final long serialVersionUID = 1L;
diff --git a/gateway/src/main/resources/META-INF/spring.factories b/gateway/src/main/resources/META-INF/spring.factories
index 3ec80577..95c56c86 100644
--- a/gateway/src/main/resources/META-INF/spring.factories
+++ b/gateway/src/main/resources/META-INF/spring.factories
@@ -2,7 +2,6 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.georchestra.gateway.autoconfigure.security.WebSecurityAutoConfiguration,\
org.georchestra.gateway.autoconfigure.security.LdapSecurityAutoConfiguration,\
-org.georchestra.gateway.autoconfigure.security.GeorchestraLdapAccountManagementAutoConfiguration,\
org.georchestra.gateway.autoconfigure.security.OAuth2SecurityAutoConfiguration,\
org.georchestra.gateway.autoconfigure.app.FiltersAutoConfiguration,\
org.georchestra.gateway.autoconfigure.app.RoutePredicateFactoriesAutoConfiguration
\ No newline at end of file
diff --git a/gateway/src/test/java/org/georchestra/gateway/autoconfigure/security/LdapSecurityAutoConfigurationTest.java b/gateway/src/test/java/org/georchestra/gateway/autoconfigure/security/LdapSecurityAutoConfigurationTest.java
index f77928e6..470ca9d5 100644
--- a/gateway/src/test/java/org/georchestra/gateway/autoconfigure/security/LdapSecurityAutoConfigurationTest.java
+++ b/gateway/src/test/java/org/georchestra/gateway/autoconfigure/security/LdapSecurityAutoConfigurationTest.java
@@ -21,117 +21,107 @@
import static org.assertj.core.api.Assertions.assertThat;
-import org.georchestra.gateway.security.ldap.DemultiplexingUsersApi;
-import org.georchestra.gateway.security.ldap.GeorchestraLdapAuthenticatedUserMapper;
-import org.georchestra.gateway.security.ldap.LdapAuthenticatedUserMapper;
import org.georchestra.gateway.security.ldap.LdapConfigProperties;
-import org.georchestra.gateway.security.ldap.MultipleLdapSecurityConfiguration;
-import org.georchestra.gateway.security.ldap.MultipleLdapSecurityConfiguration.LDAPAuthenticationCustomizer;
+import org.georchestra.gateway.security.ldap.LdapSecurityConfiguration;
+import org.georchestra.gateway.security.ldap.LdapSecurityConfiguration.LDAPAuthenticationCustomizer;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
-import org.springframework.ldap.core.support.BaseLdapPathContextSource;
-import org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator;
+import org.springframework.security.authentication.DelegatingReactiveAuthenticationManager;
+import org.springframework.security.authentication.ReactiveAuthenticationManager;
+import org.springframework.security.web.server.authentication.AuthenticationWebFilter;
/**
* Assert context contributions of {@link LdapSecurityAutoConfiguration} /
- * {@link MultipleLdapSecurityConfiguration}
+ * {@link LdapSecurityConfiguration}
*
*/
class LdapSecurityAutoConfigurationTest {
- private ApplicationContextRunner runner = new ApplicationContextRunner().withConfiguration(AutoConfigurations
- .of(LdapSecurityAutoConfiguration.class, GeorchestraLdapAccountManagementAutoConfiguration.class));
+ private ApplicationContextRunner runner = new ApplicationContextRunner()
+ .withConfiguration(AutoConfigurations.of(LdapSecurityAutoConfiguration.class));
@Test
- void testDisabledByDefault() {
+ void testConditionalOnLdapEnabled_No_LdapConfigs() {
testDisabled(runner);
}
@Test
- void testDisabledExplicitly() {
- testDisabled(runner.withPropertyValues("georchestra.gateway.security.ldap.default.enabled=false"));
+ void testConditionalOnLdapEnabled_No_LdapConfigs_enabled() {
+ runner = runner.withPropertyValues(""//
+ // one disabled basic ldap config
+ , "georchestra.gateway.security.ldap.default.enabled: false" //
+ // one disabled extended ldap config
+ , "georchestra.gateway.security.ldap.extended1.enabled: false" //
+ , "georchestra.gateway.security.ldap.extended1.extended: true" //
+ // one disabled active directory config
+ , "georchestra.gateway.security.ldap.ad1.enabled: false" //
+ , "georchestra.gateway.security.ldap.ad1.activeDirectory: true" //
+ );
+
+ testDisabled(runner);
}
private void testDisabled(ApplicationContextRunner runner) {
runner.run(context -> {
assertThat(context).doesNotHaveBean(LdapConfigProperties.class);
assertThat(context).doesNotHaveBean(LDAPAuthenticationCustomizer.class);
- assertThat(context).doesNotHaveBean(LdapAuthenticatedUserMapper.class);
- assertThat(context).doesNotHaveBean(BaseLdapPathContextSource.class);
- assertThat(context).doesNotHaveBean(DefaultLdapAuthoritiesPopulator.class);
- assertThat(context).doesNotHaveBean("ldapAuthenticationWebFilter");
- assertThat(context).doesNotHaveBean("ldapAuthenticationManager");
- assertThat(context).doesNotHaveBean("ldapAuthoritiesMapper");
+ assertThat(context).doesNotHaveBean(AuthenticationWebFilter.class);
+ assertThat(context).doesNotHaveBean(ReactiveAuthenticationManager.class);
});
}
@Test
- void testDefaultLDAPEnabled() {
- runner.withPropertyValues(//
- "georchestra.gateway.security.ldap.default.enabled: true" //
- , "georchestra.gateway.security.ldap.default.extended: false"//
- , "georchestra.gateway.security.ldap.default.url: ldap://localhost:3891"//
- , "georchestra.gateway.security.ldap.default.baseDn: dc=georchestra,dc=org"//
- , "georchestra.gateway.security.ldap.default.users.rdn: ou=users"//
- , "georchestra.gateway.security.ldap.default.users.searchFilter: (uid={0})"//
- , "georchestra.gateway.security.ldap.default.users.pendingUsersSearchBaseDN: ou=pendingusers"//
- , "georchestra.gateway.security.ldap.default.users.protectedUsers: geoserver_privileged_user"//
- , "georchestra.gateway.security.ldap.default.roles.rdn: ou=roles"//
- , "georchestra.gateway.security.ldap.default.roles.searchFilter: (member={0})"//
- , "georchestra.gateway.security.ldap.default.roles.protectedRoles: ADMINISTRATOR, EXTRACTORAPP"//
- , "georchestra.gateway.security.ldap.default.orgs.rdn: ou=orgs"//
- , "georchestra.gateway.security.ldap.default.orgs.orgTypes: Association,Company"//
- , "georchestra.gateway.security.ldap.default.orgs.pendingOrgSearchBaseDN: ou=pendingorgs"//
- )//
- .run(context -> {
- assertThat(context).hasSingleBean(LdapAuthenticatedUserMapper.class);
- assertThat(context).hasSingleBean(GeorchestraLdapAuthenticatedUserMapper.class);
- assertThat(context).hasSingleBean(DemultiplexingUsersApi.class);
- assertThat(context).hasBean("ldapHttpBasicLoginFormEnablerExtension");
- assertThat(context).hasBean("ldapAuthenticatedUserMapper");
- assertThat(context).hasBean("ldapAuthenticationManager");
- DemultiplexingUsersApi usersApi = context.getBean(DemultiplexingUsersApi.class);
- assertThat(usersApi.getTargetNames()).containsExactlyInAnyOrder("default");
- });
+ void testConditionalOnLdapEnabled_triggers_with_basic_ldap_config() {
+ runner = runner.withPropertyValues(""//
+ , "georchestra.gateway.security.ldap.ldap1.enabled: true" //
+ , "georchestra.gateway.security.ldap.ldap1.url: ldap://ldap1.test.com:839" //
+ , "georchestra.gateway.security.ldap.ldap1.baseDn: dc=georchestra,dc=org" //
+ , "georchestra.gateway.security.ldap.ldap1.users.rdn: ou=users,dc=georchestra,dc=org" //
+ , "georchestra.gateway.security.ldap.ldap1.users.searchFilter: (uid={0})" //
+ , "georchestra.gateway.security.ldap.ldap1.roles.rdn: ou=roles" //
+ , "georchestra.gateway.security.ldap.ldap1.roles.searchFilter: (member={0})" //
+ );
+
+ testEnabled(runner);
+ }
+
+ @Test
+ void testConditionalOnLdapEnabled_triggers_with_extended_ldap_config() {
+ runner = runner.withPropertyValues(""//
+ , "georchestra.gateway.security.ldap.ldap1.enabled: true" //
+ , "georchestra.gateway.security.ldap.ldap1.extended: true" //
+ , "georchestra.gateway.security.ldap.ldap1.url: ldap://ldap1.test.com:839" //
+ , "georchestra.gateway.security.ldap.ldap1.baseDn: dc=georchestra,dc=org" //
+ , "georchestra.gateway.security.ldap.ldap1.users.rdn: ou=users,dc=georchestra,dc=org" //
+ , "georchestra.gateway.security.ldap.ldap1.users.searchFilter: (uid={0})" //
+ , "georchestra.gateway.security.ldap.ldap1.roles.rdn: ou=roles" //
+ , "georchestra.gateway.security.ldap.ldap1.roles.searchFilter: (member={0})" //
+ , "georchestra.gateway.security.ldap.ldap1.orgs.rdn: ou=orgs" //
+ );
+
+ testEnabled(runner);
}
@Test
- void testMultipleLDAPEnabled() {
- runner.withPropertyValues(//
- "georchestra.gateway.security.ldap.default.enabled: true" //
- , "georchestra.gateway.security.ldap.default.extended: true"//
- , "georchestra.gateway.security.ldap.default.url: ldap://localhost:3891"//
- , "georchestra.gateway.security.ldap.default.baseDn: dc=georchestra,dc=org"//
- , "georchestra.gateway.security.ldap.default.users.rdn: ou=users"//
- , "georchestra.gateway.security.ldap.default.users.searchFilter: (uid={0})"//
- , "georchestra.gateway.security.ldap.default.users.pendingUsersSearchBaseDN: ou=pendingusers"//
- , "georchestra.gateway.security.ldap.default.users.protectedUsers: geoserver_privileged_user"//
- , "georchestra.gateway.security.ldap.default.roles.rdn: ou=roles"//
- , "georchestra.gateway.security.ldap.default.roles.searchFilter: (member={0})"//
- , "georchestra.gateway.security.ldap.default.roles.protectedRoles: ADMINISTRATOR, EXTRACTORAPP"//
- , "georchestra.gateway.security.ldap.default.orgs.rdn: ou=orgs"//
- , "georchestra.gateway.security.ldap.default.orgs.orgTypes: Association,Company"//
- , "georchestra.gateway.security.ldap.default.orgs.pendingOrgSearchBaseDN: ou=pendingorgs"//
- ///
- , "georchestra.gateway.security.ldap.second.enabled: true" //
- , "georchestra.gateway.security.ldap.second.extended: true" //
- , "georchestra.gateway.security.ldap.second.url: ldap://localhost:3892"//
- , "georchestra.gateway.security.ldap.second.baseDn: dc=externals,dc=org"//
- , "georchestra.gateway.security.ldap.second.users.rdn: ou=users"//
- , "georchestra.gateway.security.ldap.second.users.searchFilter: (uid={0})"//
- , "georchestra.gateway.security.ldap.second.roles.rdn: ou=roles"//
- , "georchestra.gateway.security.ldap.second.roles.searchFilter: (member={0})"//
- )//
- .run(context -> {
- assertThat(context).hasSingleBean(LdapAuthenticatedUserMapper.class);
- assertThat(context).hasSingleBean(GeorchestraLdapAuthenticatedUserMapper.class);
- assertThat(context).hasSingleBean(DemultiplexingUsersApi.class);
- assertThat(context).hasBean("ldapHttpBasicLoginFormEnablerExtension");
- assertThat(context).hasBean("ldapAuthenticatedUserMapper");
- assertThat(context).hasBean("ldapAuthenticationManager");
- DemultiplexingUsersApi usersApi = context.getBean(DemultiplexingUsersApi.class);
- assertThat(usersApi.getTargetNames()).containsExactlyInAnyOrder("default", "second");
- });
- ;
+ void testConditionalOnLdapEnabled_triggers_with_activedirectory_ldap_config() {
+ runner = runner.withPropertyValues(""//
+ , "georchestra.gateway.security.ldap.ad.enabled: true" //
+ , "georchestra.gateway.security.ldap.ad.activeDirectory: true" //
+ , "georchestra.gateway.security.ldap.ad.url: ldap://test.ldap2:839" //
+ );
+
+ testEnabled(runner);
+ }
+
+ private void testEnabled(ApplicationContextRunner runner) {
+ runner.run(context -> {
+ assertThat(context).hasSingleBean(LdapConfigProperties.class);
+ assertThat(context).hasSingleBean(LDAPAuthenticationCustomizer.class);
+ assertThat(context).hasSingleBean(AuthenticationWebFilter.class);
+
+ assertThat(context).hasBean("primaryAuthenticationManager");
+ assertThat(context.getBean("primaryAuthenticationManager"))
+ .isInstanceOf(DelegatingReactiveAuthenticationManager.class);
+ });
}
}
diff --git a/gateway/src/test/java/org/georchestra/gateway/security/ldap/LdapConfigPropertiesValidationsTest.java b/gateway/src/test/java/org/georchestra/gateway/security/ldap/LdapConfigPropertiesValidationsTest.java
new file mode 100644
index 00000000..2ed9dbfb
--- /dev/null
+++ b/gateway/src/test/java/org/georchestra/gateway/security/ldap/LdapConfigPropertiesValidationsTest.java
@@ -0,0 +1,470 @@
+/*
+ * Copyright (C) 2022 by the geOrchestra PSC
+ *
+ * This file is part of geOrchestra.
+ *
+ * geOrchestra is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free
+ * Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * geOrchestra is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * geOrchestra. If not, see .
+ */
+
+package org.georchestra.gateway.security.ldap;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.List;
+import java.util.Optional;
+
+import org.georchestra.gateway.security.ldap.activedirectory.ActiveDirectoryLdapServerConfig;
+import org.georchestra.gateway.security.ldap.basic.LdapServerConfig;
+import org.georchestra.gateway.security.ldap.extended.ExtendedLdapConfig;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * Test cases for {@link LdapConfigPropertiesValidations}.
+ *
+ * {@link LdapConfigPropertiesValidations} will forbid application startup if
+ * {@link LdapConfigProperties} is invalid.
+ *
+ * {@link LdapConfigProperties} is loaded from application properties, usually
+ * from georchestra datadirectory's {@literal gateway/gateway.yaml}
+ */
+class LdapConfigPropertiesValidationsTest {
+
+ @EnableConfigurationProperties(LdapConfigProperties.class)
+ static @Configuration class EnableConfigProps {
+
+ }
+
+ private ApplicationContextRunner runner = new ApplicationContextRunner()
+ .withUserConfiguration(EnableConfigProps.class);
+
+ public @Test void no_ldap_configs() {
+ runner.run(context -> {
+ assertThat(context).hasSingleBean(LdapConfigProperties.class);
+ assertThat(context.getBean(LdapConfigProperties.class).getLdap()).isEmpty();
+ });
+ }
+
+ public @Test void no_ldap_enabled() {
+ runner.withPropertyValues(//
+ "georchestra.gateway.security.ldap.ldap1.enabled: false" //
+ , "georchestra.gateway.security.ldap.ldap2.enabled: false" //
+ ).run(context -> {
+ assertThat(context).hasSingleBean(LdapConfigProperties.class);
+ LdapConfigProperties config = context.getBean(LdapConfigProperties.class);
+ assertThat(config.getLdap()).hasSize(2);
+ assertThat(config.simpleEnabled()).isEmpty();
+ assertThat(config.extendedEnabled()).isEmpty();
+ assertThat(config.activeDirectoryEnabled()).isEmpty();
+ });
+ }
+
+ public @Test void validates_extended_and_activedirectory_are_mutually_exclusive() {
+ runner.withPropertyValues(""//
+ , "georchestra.gateway.security.ldap.ldap1.enabled: true" //
+ , "georchestra.gateway.security.ldap.ldap1.extended: true" //
+ , "georchestra.gateway.security.ldap.ldap1.activeDirectory: true" //
+ ).run(context -> {
+ assertThat(context).getFailure()//
+ .hasStackTraceContaining("extended and activeDirectory are mutually exclusive")
+ .hasStackTraceContaining("ldap.[ldap1].extended")//
+ .hasStackTraceContaining("ldap.[ldap1].activeDirectory");
+ });
+ }
+
+ public @Test void validates_common_url_is_mandatory_if_enabled() {
+ runner.withPropertyValues(""//
+ , "georchestra.gateway.security.ldap.basic1.enabled: true" //
+ , "georchestra.gateway.security.ldap.basic1.url:" //
+ , "georchestra.gateway.security.ldap.extended1.enabled: true" //
+ , "georchestra.gateway.security.ldap.extended1.extended: true" //
+ , "georchestra.gateway.security.ldap.extended1.url: " //
+ , "georchestra.gateway.security.ldap.ad1.enabled: true" //
+ , "georchestra.gateway.security.ldap.ad1.activeDirectory: true" //
+ , "georchestra.gateway.security.ldap.ad1.url:" //
+ , "georchestra.gateway.security.ldap.basic2.enabled: false" //
+ , "georchestra.gateway.security.ldap.basic2.url:" //
+ ).run(context -> {
+ assertThat(context).getFailure().hasStackTraceContaining("LDAP url is required")
+ .hasStackTraceContaining("ldap.[basic1].url").hasStackTraceContaining("ldap.[extended1].url")
+ .hasStackTraceContaining("ldap.[ad1].url");
+ });
+ }
+
+ public @Test void validates_common_url_not_set_does_not_fail_if_not_enabled() {
+ runner.withPropertyValues(""//
+ , "georchestra.gateway.security.ldap.basic1.enabled: false" //
+ , "georchestra.gateway.security.ldap.basic1.url:" //
+ ).run(context -> {
+ assertThat(context).hasNotFailed();
+ });
+ }
+
+ public @Test void validates_activeDirectory_baseDn_is_optional() {
+ runner.withPropertyValues(""//
+ // first AD config
+ , "georchestra.gateway.security.ldap.ad1.enabled: true" //
+ , "georchestra.gateway.security.ldap.ad1.activeDirectory: true" //
+ , "georchestra.gateway.security.ldap.ad1.url: ldap://test.ldap:839" //
+ , "georchestra.gateway.security.ldap.ad1.baseDn: " //
+ // second AD config
+ , "georchestra.gateway.security.ldap.ad2.enabled: true" //
+ , "georchestra.gateway.security.ldap.ad2.activeDirectory: true" //
+ , "georchestra.gateway.security.ldap.ad2.url: ldap://test.ldap2:839" //
+ , "georchestra.gateway.security.ldap.ad2.baseDn: dc=my,dc=company,dc=com" //
+ ).run(context -> {
+ LdapConfigProperties config = context.getBean(LdapConfigProperties.class);
+ List adConfigs = config.activeDirectoryEnabled();
+ assertThat(adConfigs).hasSize(2);
+ assertThat(adConfigs.get(0)).hasFieldOrPropertyWithValue("rootDn", Optional.empty());
+ assertThat(adConfigs.get(1)).hasFieldOrPropertyWithValue("rootDn", Optional.of("dc=my,dc=company,dc=com"));
+ });
+ }
+
+ public @Test void validates_activeDirectory_domain_is_optional() {
+ runner.withPropertyValues(""//
+ // first AD config
+ , "georchestra.gateway.security.ldap.ad1.enabled: true" //
+ , "georchestra.gateway.security.ldap.ad1.activeDirectory: true" //
+ , "georchestra.gateway.security.ldap.ad1.url: ldap://test.ldap:839" //
+ , "georchestra.gateway.security.ldap.ad1.domain: " //
+ // second AD config
+ , "georchestra.gateway.security.ldap.ad2.enabled: true" //
+ , "georchestra.gateway.security.ldap.ad2.activeDirectory: true" //
+ , "georchestra.gateway.security.ldap.ad2.url: ldap://test.ldap2:839" //
+ , "georchestra.gateway.security.ldap.ad2.domain: my.company.com" //
+ ).run(context -> {
+ LdapConfigProperties config = context.getBean(LdapConfigProperties.class);
+ List adConfigs = config.activeDirectoryEnabled();
+ assertThat(adConfigs).hasSize(2);
+ assertThat(adConfigs.get(0)).hasFieldOrPropertyWithValue("domain", Optional.empty());
+ assertThat(adConfigs.get(1)).hasFieldOrPropertyWithValue("domain", Optional.of("my.company.com"));
+ });
+ }
+
+ public @Test void validates_activeDirectory_searchFilter_is_optional() {
+ runner.withPropertyValues(""//
+ // first AD config
+ , "georchestra.gateway.security.ldap.ad1.enabled: true" //
+ , "georchestra.gateway.security.ldap.ad1.activeDirectory: true" //
+ , "georchestra.gateway.security.ldap.ad1.url: ldap://test.ldap:839" //
+ , "georchestra.gateway.security.ldap.ad1.users.searchFilter: "//
+ // second AD config
+ , "georchestra.gateway.security.ldap.ad2.enabled: true" //
+ , "georchestra.gateway.security.ldap.ad2.activeDirectory: true" //
+ , "georchestra.gateway.security.ldap.ad2.url: ldap://test.ldap2:839" //
+ ,
+ "georchestra.gateway.security.ldap.ad2.users.searchFilter: (&(objectClass=user)(userPrincipalName={0}))" //
+ ).run(context -> {
+ LdapConfigProperties config = context.getBean(LdapConfigProperties.class);
+ List adConfigs = config.activeDirectoryEnabled();
+ assertThat(adConfigs).hasSize(2);
+ assertThat(adConfigs.get(0)).hasFieldOrPropertyWithValue("searchFilter", Optional.empty());
+ assertThat(adConfigs.get(1)).hasFieldOrPropertyWithValue("searchFilter",
+ Optional.of("(&(objectClass=user)(userPrincipalName={0}))"));
+ });
+ }
+
+ public @Test void validates_basic_and_extended_baseDn_is_mandatory() {
+ runner.withPropertyValues(""//
+ // Basic LDAP config
+ , "georchestra.gateway.security.ldap.ldap1.enabled: true" //
+ , "georchestra.gateway.security.ldap.ldap1.url: ldap://ldap1.test.com:839" //
+ , "georchestra.gateway.security.ldap.ldap1.baseDn: " //
+ // Georchestra extended LDAP config
+ , "georchestra.gateway.security.ldap.extended1.enabled: true" //
+ , "georchestra.gateway.security.ldap.extended1.extended: true" //
+ , "georchestra.gateway.security.ldap.extended1.url: ldap://ldap2.test.com:839" //
+ , "georchestra.gateway.security.ldap.extended1.baseDn: " //
+ ).run(context -> {
+ assertThat(context).getFailure()//
+ .hasStackTraceContaining("LDAP base DN is required")//
+ .hasStackTraceContaining("ldap.[ldap1].baseDn").hasStackTraceContaining("ldap.[extended1].baseDn");
+ });
+ }
+
+ public @Test void validates_basic_and_extended_users_rdn_is_mandatory() {
+ runner.withPropertyValues(""//
+ // Basic LDAP config
+ , "georchestra.gateway.security.ldap.ldap1.enabled: true" //
+ , "georchestra.gateway.security.ldap.ldap1.url: ldap://ldap1.test.com:839" //
+ , "georchestra.gateway.security.ldap.ldap1.baseDn: dc=georchestra,dc=org" //
+ , "georchestra.gateway.security.ldap.ldap1.users.rdn: " //
+ // Georchestra extended LDAP config
+ , "georchestra.gateway.security.ldap.extended1.enabled: true" //
+ , "georchestra.gateway.security.ldap.extended1.extended: true" //
+ , "georchestra.gateway.security.ldap.extended1.url: ldap://ldap2.test.com:839" //
+ , "georchestra.gateway.security.ldap.extended1.baseDn: dc=test,dc=com" //
+ , "georchestra.gateway.security.ldap.extended1.users.rdn: " //
+ ).run(context -> {
+ assertThat(context).getFailure()//
+ .hasStackTraceContaining("LDAP users RDN (Relative Distinguished Name) is required")//
+ .hasStackTraceContaining("ldap.[ldap1].users.rdn")//
+ .hasStackTraceContaining("ldap.[extended1].users.rdn");
+ });
+ }
+
+ public @Test void validates_basic_and_extended_users_searchFilter_is_mandatory() {
+ runner.withPropertyValues(""//
+ // Basic LDAP config
+ , "georchestra.gateway.security.ldap.ldap1.enabled: true" //
+ , "georchestra.gateway.security.ldap.ldap1.url: ldap://ldap1.test.com:839" //
+ , "georchestra.gateway.security.ldap.ldap1.baseDn: dc=georchestra,dc=org" //
+ , "georchestra.gateway.security.ldap.ldap1.users.rdn: ou=users,dc=georchestra,dc=org" //
+ , "georchestra.gateway.security.ldap.ldap1.users.searchFilter: " //
+ // Georchestra extended LDAP config
+ , "georchestra.gateway.security.ldap.extended1.enabled: true" //
+ , "georchestra.gateway.security.ldap.extended1.extended: true" //
+ , "georchestra.gateway.security.ldap.extended1.url: ldap://ldap2.test.com:839" //
+ , "georchestra.gateway.security.ldap.extended1.baseDn: dc=test,dc=com" //
+ , "georchestra.gateway.security.ldap.extended1.users.rdn: ou=users,dc=tes,dc=com" //
+ , "georchestra.gateway.security.ldap.extended1.users.searchFilter: " //
+ ).run(context -> {
+ assertThat(context).getFailure()//
+ .hasStackTraceContaining("LDAP users searchFilter is required for regular LDAP configs")//
+ .hasStackTraceContaining("ldap.[ldap1].users.searchFilter")//
+ .hasStackTraceContaining("ldap.[extended1].users.searchFilter");
+ });
+ }
+
+ public @Test void validates_basic_and_extended_roles_rdn_is_mandatory() {
+ runner.withPropertyValues(""//
+ // Basic LDAP config
+ , "georchestra.gateway.security.ldap.ldap1.enabled: true" //
+ , "georchestra.gateway.security.ldap.ldap1.url: ldap://ldap1.test.com:839" //
+ , "georchestra.gateway.security.ldap.ldap1.baseDn: dc=georchestra,dc=org" //
+ , "georchestra.gateway.security.ldap.ldap1.users.rdn: ou=users,dc=georchestra,dc=org" //
+ , "georchestra.gateway.security.ldap.ldap1.users.searchFilter: (uid={0})" //
+ , "georchestra.gateway.security.ldap.ldap1.roles.rdn: " //
+ // Georchestra extended LDAP config
+ , "georchestra.gateway.security.ldap.extended1.enabled: true" //
+ , "georchestra.gateway.security.ldap.extended1.extended: true" //
+ , "georchestra.gateway.security.ldap.extended1.url: ldap://ldap2.test.com:839" //
+ , "georchestra.gateway.security.ldap.extended1.baseDn: dc=test,dc=com" //
+ , "georchestra.gateway.security.ldap.extended1.users.rdn: ou=users,dc=tes,dc=com" //
+ , "georchestra.gateway.security.ldap.extended1.users.searchFilter: (uid={0})" //
+ , "georchestra.gateway.security.ldap.extended1.roles.rdn: " //
+ ).run(context -> {
+ assertThat(context).getFailure()//
+ .hasStackTraceContaining("Roles Relative distinguished name is required")//
+ .hasStackTraceContaining("ldap.[ldap1].roles.rdn")//
+ .hasStackTraceContaining("ldap.[extended1].roles.rdn");
+ });
+ }
+
+ public @Test void validates_basic_and_extended_roles_searchFilter_is_mandatory() {
+ runner.withPropertyValues(""//
+ // Basic LDAP config
+ , "georchestra.gateway.security.ldap.ldap1.enabled: true" //
+ , "georchestra.gateway.security.ldap.ldap1.url: ldap://ldap1.test.com:839" //
+ , "georchestra.gateway.security.ldap.ldap1.baseDn: dc=georchestra,dc=org" //
+ , "georchestra.gateway.security.ldap.ldap1.users.rdn: ou=users,dc=georchestra,dc=org" //
+ , "georchestra.gateway.security.ldap.ldap1.users.searchFilter: (uid={0})" //
+ , "georchestra.gateway.security.ldap.ldap1.roles.rdn: ou=roles" //
+ , "georchestra.gateway.security.ldap.ldap1.roles.searchFilter: " //
+ // Georchestra extended LDAP config
+ , "georchestra.gateway.security.ldap.extended1.enabled: true" //
+ , "georchestra.gateway.security.ldap.extended1.extended: true" //
+ , "georchestra.gateway.security.ldap.extended1.url: ldap://ldap2.test.com:839" //
+ , "georchestra.gateway.security.ldap.extended1.baseDn: dc=test,dc=com" //
+ , "georchestra.gateway.security.ldap.extended1.users.rdn: ou=users,dc=tes,dc=com" //
+ , "georchestra.gateway.security.ldap.extended1.users.searchFilter: (uid={0})" //
+ , "georchestra.gateway.security.ldap.extended1.roles.rdn: ou=roles" //
+ , "georchestra.gateway.security.ldap.extended1.roles.searchFilter: "//
+ ).run(context -> {
+ assertThat(context).getFailure()//
+ .hasStackTraceContaining("Roles searchFilter is required")//
+ .hasStackTraceContaining("ldap.[ldap1].roles.searchFilter")//
+ .hasStackTraceContaining("ldap.[extended1].roles.searchFilter");
+ });
+ }
+
+ public @Test void validates_extended_orgs_rdn_is_mandatory() {
+ runner.withPropertyValues(""//
+ , "georchestra.gateway.security.ldap.extended1.enabled: true" //
+ , "georchestra.gateway.security.ldap.extended1.extended: true" //
+ , "georchestra.gateway.security.ldap.extended1.url: ldap://ldap2.test.com:839" //
+ , "georchestra.gateway.security.ldap.extended1.baseDn: dc=test,dc=com" //
+ , "georchestra.gateway.security.ldap.extended1.users.rdn: ou=users,dc=tes,dc=com" //
+ , "georchestra.gateway.security.ldap.extended1.users.searchFilter: (uid={0})" //
+ , "georchestra.gateway.security.ldap.extended1.roles.rdn: ou=roles" //
+ , "georchestra.gateway.security.ldap.extended1.roles.searchFilter: " //
+ , "georchestra.gateway.security.ldap.extended1.orgs.rdn: " //
+ ).run(context -> {
+ assertThat(context).getFailure()//
+ .hasStackTraceContaining("Organizations search base RDN is required if extended is true")//
+ .hasStackTraceContaining("ldap.[extended1].orgs.rdn");
+ });
+ }
+
+ public @Test void valid_single_config_basic() {
+ runner.withPropertyValues(""//
+ // Basic LDAP config
+ , "georchestra.gateway.security.ldap.ldap1.enabled: true" //
+ , "georchestra.gateway.security.ldap.ldap1.url: ldap://ldap1.test.com:839" //
+ , "georchestra.gateway.security.ldap.ldap1.baseDn: dc=georchestra,dc=org" //
+ , "georchestra.gateway.security.ldap.ldap1.users.rdn: ou=users,dc=georchestra,dc=org" //
+ , "georchestra.gateway.security.ldap.ldap1.users.searchFilter: (uid={0})" //
+ , "georchestra.gateway.security.ldap.ldap1.roles.rdn: ou=roles" //
+ , "georchestra.gateway.security.ldap.ldap1.roles.searchFilter: (member={0})" //
+ , "georchestra.gateway.security.ldap.ldap1.roles.prefix: GROUP_" //
+ , "georchestra.gateway.security.ldap.ldap1.roles.upperCase: false" //
+ ).run(context -> {
+ assertThat(context).hasNotFailed();
+ LdapConfigProperties config = context.getBean(LdapConfigProperties.class);
+ assertThat(config.simpleEnabled()).hasSize(1);
+ assertThat(config.extendedEnabled()).isEmpty();
+ assertThat(config.activeDirectoryEnabled()).isEmpty();
+
+ LdapServerConfig basic = config.simpleEnabled().get(0);
+ assertThat(basic).hasFieldOrPropertyWithValue("name", "ldap1");
+ assertThat(basic).hasFieldOrPropertyWithValue("enabled", true);
+ assertThat(basic).hasFieldOrPropertyWithValue("url", "ldap://ldap1.test.com:839");
+ assertThat(basic).hasFieldOrPropertyWithValue("baseDn", "dc=georchestra,dc=org");
+ assertThat(basic).hasFieldOrPropertyWithValue("usersRdn", "ou=users,dc=georchestra,dc=org");
+ assertThat(basic).hasFieldOrPropertyWithValue("usersSearchFilter", "(uid={0})");
+ assertThat(basic).hasFieldOrPropertyWithValue("rolesRdn", "ou=roles");
+ assertThat(basic).hasFieldOrPropertyWithValue("rolesSearchFilter", "(member={0})");
+ assertThat(basic).hasFieldOrPropertyWithValue("rolesPrefix", Optional.of("GROUP_"));
+ assertThat(basic).hasFieldOrPropertyWithValue("rolesUpperCase", false);
+ });
+ }
+
+ public @Test void valid_single_config_extended() {
+ runner.withPropertyValues(""//
+ // Basic LDAP config
+ , "georchestra.gateway.security.ldap.ldap1.enabled: true" //
+ , "georchestra.gateway.security.ldap.ldap1.extended: true" //
+ , "georchestra.gateway.security.ldap.ldap1.url: ldap://ldap1.test.com:839" //
+ , "georchestra.gateway.security.ldap.ldap1.baseDn: dc=georchestra,dc=org" //
+ , "georchestra.gateway.security.ldap.ldap1.users.rdn: ou=users,dc=georchestra,dc=org" //
+ , "georchestra.gateway.security.ldap.ldap1.users.searchFilter: (uid={0})" //
+ , "georchestra.gateway.security.ldap.ldap1.roles.rdn: ou=roles" //
+ , "georchestra.gateway.security.ldap.ldap1.roles.searchFilter: (member={0})" //
+ , "georchestra.gateway.security.ldap.ldap1.roles.prefix: GROUP_" //
+ , "georchestra.gateway.security.ldap.ldap1.roles.upperCase: false" //
+ , "georchestra.gateway.security.ldap.ldap1.orgs.rdn: ou=orgs" //
+ ).run(context -> {
+ assertThat(context).hasNotFailed();
+ LdapConfigProperties config = context.getBean(LdapConfigProperties.class);
+ assertThat(config.simpleEnabled()).isEmpty();
+ assertThat(config.extendedEnabled()).hasSize(1);
+ assertThat(config.activeDirectoryEnabled()).isEmpty();
+
+ ExtendedLdapConfig extended = config.extendedEnabled().get(0);
+ assertThat(extended).hasFieldOrPropertyWithValue("name", "ldap1");
+ assertThat(extended).hasFieldOrPropertyWithValue("enabled", true);
+ assertThat(extended).hasFieldOrPropertyWithValue("url", "ldap://ldap1.test.com:839");
+ assertThat(extended).hasFieldOrPropertyWithValue("baseDn", "dc=georchestra,dc=org");
+ assertThat(extended).hasFieldOrPropertyWithValue("usersRdn", "ou=users,dc=georchestra,dc=org");
+ assertThat(extended).hasFieldOrPropertyWithValue("usersSearchFilter", "(uid={0})");
+ assertThat(extended).hasFieldOrPropertyWithValue("rolesRdn", "ou=roles");
+ assertThat(extended).hasFieldOrPropertyWithValue("rolesSearchFilter", "(member={0})");
+ assertThat(extended).hasFieldOrPropertyWithValue("rolesPrefix", Optional.of("GROUP_"));
+ assertThat(extended).hasFieldOrPropertyWithValue("rolesUpperCase", false);
+ assertThat(extended).hasFieldOrPropertyWithValue("orgsRdn", "ou=orgs");
+ });
+ }
+
+ public @Test void valid_single_config_activeDirectory_minimal() {
+ runner.withPropertyValues(""//
+ , "georchestra.gateway.security.ldap.ad.enabled: true" //
+ , "georchestra.gateway.security.ldap.ad.activeDirectory: true" //
+ , "georchestra.gateway.security.ldap.ad.url: ldap://test.ldap2:839" //
+ ).run(context -> {
+ LdapConfigProperties config = context.getBean(LdapConfigProperties.class);
+ assertThat(config.simpleEnabled()).isEmpty();
+ assertThat(config.extendedEnabled()).isEmpty();
+ assertThat(config.activeDirectoryEnabled()).hasSize(1);
+
+ ActiveDirectoryLdapServerConfig ad = config.activeDirectoryEnabled().get(0);
+ assertThat(ad).hasFieldOrPropertyWithValue("name", "ad");
+ assertThat(ad).hasFieldOrPropertyWithValue("enabled", true);
+ assertThat(ad).hasFieldOrPropertyWithValue("url", "ldap://test.ldap2:839");
+ assertThat(ad).hasFieldOrPropertyWithValue("domain", Optional.empty());
+ assertThat(ad).hasFieldOrPropertyWithValue("rootDn", Optional.empty());
+ assertThat(ad).hasFieldOrPropertyWithValue("searchFilter", Optional.empty());
+ });
+ }
+
+ public @Test void valid_single_config_activeDirectory_full() {
+ runner.withPropertyValues(""//
+ , "georchestra.gateway.security.ldap.ad.enabled: true" //
+ , "georchestra.gateway.security.ldap.ad.activeDirectory: true" //
+ , "georchestra.gateway.security.ldap.ad.url: ldap://test.ldap2:839" //
+ , "georchestra.gateway.security.ldap.ad.domain: my.domain.com" //
+ , "georchestra.gateway.security.ldap.ad.baseDn: dc=my,dc=domain,dc=com" //
+ ,
+ "georchestra.gateway.security.ldap.ad.users.searchFilter: (&(objectClass=user)(userPrincipalName={0}))" //
+ ).run(context -> {
+ LdapConfigProperties config = context.getBean(LdapConfigProperties.class);
+ assertThat(config.simpleEnabled()).isEmpty();
+ assertThat(config.extendedEnabled()).isEmpty();
+ assertThat(config.activeDirectoryEnabled()).hasSize(1);
+
+ ActiveDirectoryLdapServerConfig ad = config.activeDirectoryEnabled().get(0);
+ assertThat(ad).hasFieldOrPropertyWithValue("name", "ad");
+ assertThat(ad).hasFieldOrPropertyWithValue("enabled", true);
+ assertThat(ad).hasFieldOrPropertyWithValue("url", "ldap://test.ldap2:839");
+ assertThat(ad).hasFieldOrPropertyWithValue("domain", Optional.of("my.domain.com"));
+ assertThat(ad).hasFieldOrPropertyWithValue("rootDn", Optional.of("dc=my,dc=domain,dc=com"));
+ assertThat(ad).hasFieldOrPropertyWithValue("searchFilter",
+ Optional.of("(&(objectClass=user)(userPrincipalName={0}))"));
+ });
+ }
+
+ public @Test void valid_multiple_configs() {
+ runner.withPropertyValues(""//
+ // Basic LDAP config
+ , "georchestra.gateway.security.ldap.ldap1.enabled: true" //
+ , "georchestra.gateway.security.ldap.ldap1.url: ldap://ldap1.test.com:839" //
+ , "georchestra.gateway.security.ldap.ldap1.baseDn: dc=georchestra,dc=org" //
+ , "georchestra.gateway.security.ldap.ldap1.users.rdn: ou=users,dc=georchestra,dc=org" //
+ , "georchestra.gateway.security.ldap.ldap1.users.searchFilter: (uid={0})" //
+ , "georchestra.gateway.security.ldap.ldap1.roles.rdn: ou=roles" //
+ , "georchestra.gateway.security.ldap.ldap1.roles.searchFilter: (member={0})" //
+ // Georchestra extended LDAP config
+ , "georchestra.gateway.security.ldap.extended1.enabled: true" //
+ , "georchestra.gateway.security.ldap.extended1.extended: true" //
+ , "georchestra.gateway.security.ldap.extended1.url: ldap://ldap2.test.com:839" //
+ , "georchestra.gateway.security.ldap.extended1.baseDn: dc=test,dc=com" //
+ , "georchestra.gateway.security.ldap.extended1.users.rdn: ou=users,dc=tes,dc=com" //
+ , "georchestra.gateway.security.ldap.extended1.users.searchFilter: (uid={0})" //
+ , "georchestra.gateway.security.ldap.extended1.roles.rdn: ou=roles" //
+ , "georchestra.gateway.security.ldap.extended1.roles.searchFilter: (member={0})" //
+ , "georchestra.gateway.security.ldap.extended1.orgs.rdn: ou=orgs" //
+ // minimal AD config
+ , "georchestra.gateway.security.ldap.ad1.enabled: true" //
+ , "georchestra.gateway.security.ldap.ad1.activeDirectory: true" //
+ , "georchestra.gateway.security.ldap.ad1.url: ldap://test.ldap:839" //
+ // full AD config
+ , "georchestra.gateway.security.ldap.ad2.enabled: true" //
+ , "georchestra.gateway.security.ldap.ad2.activeDirectory: true" //
+ , "georchestra.gateway.security.ldap.ad2.url: ldap://test.ldap2:839" //
+ , "georchestra.gateway.security.ldap.ad2.domain: my.domain.com" //
+ , "georchestra.gateway.security.ldap.ad2.baseDn: dc=my,dc=domain,dc=com" //
+ ,
+ "georchestra.gateway.security.ldap.ad2.users.searchFilter: (&(objectClass=user)(userPrincipalName={0}))" //
+ ).run(context -> {
+ assertThat(context).hasNotFailed();
+
+ LdapConfigProperties config = context.getBean(LdapConfigProperties.class);
+ assertThat(config.simpleEnabled()).hasSize(1);
+ assertThat(config.extendedEnabled()).hasSize(1);
+ assertThat(config.activeDirectoryEnabled()).hasSize(2);
+ });
+ }
+}
diff --git a/gateway/src/test/java/org/georchestra/gateway/security/ldap/activedirectory/ActiveDirectoryAuthenticationConfigurationTest.java b/gateway/src/test/java/org/georchestra/gateway/security/ldap/activedirectory/ActiveDirectoryAuthenticationConfigurationTest.java
new file mode 100644
index 00000000..2f6e80ef
--- /dev/null
+++ b/gateway/src/test/java/org/georchestra/gateway/security/ldap/activedirectory/ActiveDirectoryAuthenticationConfigurationTest.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2022 by the geOrchestra PSC
+ *
+ * This file is part of geOrchestra.
+ *
+ * geOrchestra is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free
+ * Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * geOrchestra is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * geOrchestra. If not, see .
+ */
+
+package org.georchestra.gateway.security.ldap.activedirectory;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.List;
+
+import org.georchestra.gateway.security.ldap.basic.LdapAuthenticatedUserMapper;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.context.annotation.UserConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.context.ApplicationContext;
+import org.springframework.security.authentication.ReactiveAuthenticationManagerAdapter;
+import org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider;
+
+/**
+ * Tests {@link ApplicationContext} contributions of
+ * {@link ActiveDirectoryAuthenticationConfiguration}
+ */
+class ActiveDirectoryAuthenticationConfigurationTest {
+
+ private ApplicationContextRunner runner = new ApplicationContextRunner()
+ .withConfiguration(UserConfigurations.of(ActiveDirectoryAuthenticationConfiguration.class));
+
+ @SuppressWarnings("unchecked")
+ public @Test void contextContributions_empty_config() {
+ runner.run(context -> {
+ assertThat(context).hasNotFailed();
+ assertThat(context).getBean("enabledActiveDirectoryLdapConfigs").isInstanceOf(List.class);
+ assertThat(context.getBean("enabledActiveDirectoryLdapConfigs", List.class)).isEmpty();
+ assertThat(context.getBean("activeDirectoryLdapAuthenticationProviders", List.class)).isEmpty();
+
+ assertThat(context.getBean("activeDirectoryAuthenticatedUserMapper").getClass().getName())
+ .isEqualTo("org.springframework.beans.factory.support.NullBean");
+
+ assertThat(context.getBean("activeDirectoryAuthenticationManager").getClass().getName())
+ .isEqualTo("org.springframework.beans.factory.support.NullBean");
+ });
+ }
+
+ @SuppressWarnings("unchecked")
+ public @Test void contextContributions_single_config() {
+ runner.withPropertyValues(""//
+ , "georchestra.gateway.security.ldap.ad.enabled: true" //
+ , "georchestra.gateway.security.ldap.ad.activeDirectory: true" //
+ , "georchestra.gateway.security.ldap.ad.url: ldap://test.ldap2:839" //
+ , "georchestra.gateway.security.ldap.ad.domain: my.domain.com" //
+ , "georchestra.gateway.security.ldap.ad.baseDn: dc=my,dc=domain,dc=com" //
+ ,
+ "georchestra.gateway.security.ldap.ad.users.searchFilter: (&(objectClass=user)(userPrincipalName={0}))" //
+ ).run(context -> {
+ assertThat(context).hasNotFailed();
+ assertThat(context).getBean("enabledActiveDirectoryLdapConfigs").isInstanceOf(List.class);
+ assertThat(context.getBean("enabledActiveDirectoryLdapConfigs", List.class)).hasSize(1);
+ assertThat(context.getBean("activeDirectoryLdapAuthenticationProviders", List.class)).hasSize(1);
+ assertThat(context.getBean("activeDirectoryLdapAuthenticationProviders", List.class)).singleElement()
+ .isInstanceOf(ActiveDirectoryLdapAuthenticationProvider.class);
+
+ assertThat(context.getBean("activeDirectoryAuthenticatedUserMapper"))
+ .isInstanceOf(LdapAuthenticatedUserMapper.class);
+
+ assertThat(context.getBean("activeDirectoryAuthenticationManager"))
+ .isInstanceOf(ReactiveAuthenticationManagerAdapter.class);
+ });
+ }
+
+ @SuppressWarnings("unchecked")
+ public @Test void contextContributions_multiple_configs() {
+ runner.withPropertyValues(""//
+ , "georchestra.gateway.security.ldap.ad1.enabled: true" //
+ , "georchestra.gateway.security.ldap.ad1.activeDirectory: true" //
+ , "georchestra.gateway.security.ldap.ad1.url: ldap://test.ldap2:839" //
+ , "georchestra.gateway.security.ldap.ad1.domain: my.domain.com" //
+ , "georchestra.gateway.security.ldap.ad1.baseDn: dc=my,dc=domain,dc=com" //
+ ,
+ "georchestra.gateway.security.ldap.ad.users.searchFilter: (&(objectClass=user)(userPrincipalName={0}))" //
+ //
+ , "georchestra.gateway.security.ldap.ad2.enabled: true" //
+ , "georchestra.gateway.security.ldap.ad2.activeDirectory: true" //
+ , "georchestra.gateway.security.ldap.ad2.url: ldap://test.ldap2:839" //
+ ).run(context -> {
+
+ assertThat(context).hasNotFailed();
+ assertThat(context.getBean("enabledActiveDirectoryLdapConfigs", List.class)).hasSize(2);
+ assertThat(context.getBean("activeDirectoryLdapAuthenticationProviders", List.class)).hasSize(2);
+ assertThat(context.getBean("activeDirectoryLdapAuthenticationProviders", List.class)).hasSize(2)
+ .allMatch(ActiveDirectoryLdapAuthenticationProvider.class::isInstance);
+
+ assertThat(context.getBean("activeDirectoryAuthenticatedUserMapper"))
+ .isInstanceOf(LdapAuthenticatedUserMapper.class);
+
+ assertThat(context.getBean("activeDirectoryAuthenticationManager"))
+ .isInstanceOf(ReactiveAuthenticationManagerAdapter.class);
+ });
+ }
+}
diff --git a/gateway/src/test/java/org/georchestra/gateway/security/ldap/basic/BasicLdapAuthenticationConfigurationTest.java b/gateway/src/test/java/org/georchestra/gateway/security/ldap/basic/BasicLdapAuthenticationConfigurationTest.java
new file mode 100644
index 00000000..4b5b87b4
--- /dev/null
+++ b/gateway/src/test/java/org/georchestra/gateway/security/ldap/basic/BasicLdapAuthenticationConfigurationTest.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2022 by the geOrchestra PSC
+ *
+ * This file is part of geOrchestra.
+ *
+ * geOrchestra is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free
+ * Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * geOrchestra is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * geOrchestra. If not, see .
+ */
+
+package org.georchestra.gateway.security.ldap.basic;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.context.annotation.UserConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.context.ApplicationContext;
+import org.springframework.security.authentication.ReactiveAuthenticationManagerAdapter;
+import org.springframework.security.ldap.authentication.LdapAuthenticationProvider;
+
+/**
+ * Tests {@link ApplicationContext} contributions of
+ * {@link BasicLdapAuthenticationConfiguration}
+ */
+class BasicLdapAuthenticationConfigurationTest {
+
+ private ApplicationContextRunner runner = new ApplicationContextRunner()
+ .withConfiguration(UserConfigurations.of(BasicLdapAuthenticationConfiguration.class));
+
+ @SuppressWarnings("unchecked")
+ public @Test void contextContributions_empty_config() {
+ runner.run(context -> {
+ assertThat(context).hasNotFailed();
+ assertThat(context).getBean("enabledSimpleLdapConfigs").isInstanceOf(List.class);
+ assertThat(context.getBean("enabledSimpleLdapConfigs", List.class)).isEmpty();
+
+ assertThat(context.getBean("ldapAuthenticatedUserMapper").getClass().getName())
+ .isEqualTo("org.springframework.beans.factory.support.NullBean");
+
+ assertThat(context.getBean("ldapAuthenticationManager").getClass().getName())
+ .isEqualTo("org.springframework.beans.factory.support.NullBean");
+ });
+ }
+
+ @SuppressWarnings("unchecked")
+ public @Test void contextContributions_single_config() {
+ runner.withPropertyValues(""//
+ // Basic LDAP config
+ , "georchestra.gateway.security.ldap.ldap1.enabled: true" //
+ , "georchestra.gateway.security.ldap.ldap1.url: ldap://ldap1.test.com:839" //
+ , "georchestra.gateway.security.ldap.ldap1.baseDn: dc=georchestra,dc=org" //
+ , "georchestra.gateway.security.ldap.ldap1.users.rdn: ou=users,dc=georchestra,dc=org" //
+ , "georchestra.gateway.security.ldap.ldap1.users.searchFilter: (uid={0})" //
+ , "georchestra.gateway.security.ldap.ldap1.roles.rdn: ou=roles" //
+ , "georchestra.gateway.security.ldap.ldap1.roles.searchFilter: (member={0})" //
+ , "georchestra.gateway.security.ldap.ldap1.roles.prefix: GROUP_" //
+ , "georchestra.gateway.security.ldap.ldap1.roles.upperCase: false" //
+ , "georchestra.gateway.security.ldap.ldap2.enabled: false" //
+ ).run(context -> {
+ assertThat(context).hasNotFailed();
+ assertThat(context).getBean("enabledSimpleLdapConfigs").isInstanceOf(List.class);
+ assertThat(context.getBean("enabledSimpleLdapConfigs", List.class)).hasSize(1);
+ assertThat(context.getBean("ldapAuthenticationProviders", List.class)).hasSize(1);
+ assertThat(context.getBean("ldapAuthenticationProviders", List.class)).singleElement()
+ .isInstanceOf(LdapAuthenticationProvider.class);
+
+ assertThat(context.getBean("ldapAuthenticatedUserMapper")).isInstanceOf(LdapAuthenticatedUserMapper.class);
+
+ assertThat(context.getBean("ldapAuthenticationManager"))
+ .isInstanceOf(ReactiveAuthenticationManagerAdapter.class);
+
+ });
+ }
+
+ @SuppressWarnings("unchecked")
+ public @Test void contextContributions_multiple_configs() {
+ runner.withPropertyValues(""//
+ // Basic LDAP config
+ , "georchestra.gateway.security.ldap.ldap1.enabled: true" //
+ , "georchestra.gateway.security.ldap.ldap1.url: ldap://ldap1.test.com:839" //
+ , "georchestra.gateway.security.ldap.ldap1.baseDn: dc=georchestra,dc=org" //
+ , "georchestra.gateway.security.ldap.ldap1.users.rdn: ou=users,dc=georchestra,dc=org" //
+ , "georchestra.gateway.security.ldap.ldap1.users.searchFilter: (uid={0})" //
+ , "georchestra.gateway.security.ldap.ldap1.roles.rdn: ou=roles" //
+ , "georchestra.gateway.security.ldap.ldap1.roles.searchFilter: (member={0})" //
+ , "georchestra.gateway.security.ldap.ldap1.roles.prefix: GROUP_" //
+ , "georchestra.gateway.security.ldap.ldap1.roles.upperCase: false" //
+ //
+ , "georchestra.gateway.security.ldap.ldap2.enabled: true" //
+ , "georchestra.gateway.security.ldap.ldap2.url: ldap://ldap1.test.com:839" //
+ , "georchestra.gateway.security.ldap.ldap2.baseDn: dc=georchestra,dc=org" //
+ , "georchestra.gateway.security.ldap.ldap2.users.rdn: ou=users,dc=georchestra,dc=org" //
+ , "georchestra.gateway.security.ldap.ldap2.users.searchFilter: (uid={0})" //
+ , "georchestra.gateway.security.ldap.ldap2.roles.rdn: ou=roles" //
+ , "georchestra.gateway.security.ldap.ldap2.roles.searchFilter: (member={0})" //
+ ).run(context -> {
+
+ assertThat(context).hasNotFailed();
+ assertThat(context.getBean("enabledSimpleLdapConfigs", List.class)).hasSize(2);
+ assertThat(context.getBean("ldapAuthenticationProviders", List.class)).hasSize(2);
+ assertThat(context.getBean("ldapAuthenticationProviders", List.class)).hasSize(2)
+ .allMatch(LdapAuthenticationProvider.class::isInstance);
+
+ assertThat(context.getBean("ldapAuthenticatedUserMapper")).isInstanceOf(LdapAuthenticatedUserMapper.class);
+
+ assertThat(context.getBean("ldapAuthenticationManager"))
+ .isInstanceOf(ReactiveAuthenticationManagerAdapter.class);
+
+ });
+ }
+}
diff --git a/gateway/src/test/java/org/georchestra/gateway/security/ldap/extended/ExtendedLdapAuthenticationConfigurationTest.java b/gateway/src/test/java/org/georchestra/gateway/security/ldap/extended/ExtendedLdapAuthenticationConfigurationTest.java
new file mode 100644
index 00000000..f281227f
--- /dev/null
+++ b/gateway/src/test/java/org/georchestra/gateway/security/ldap/extended/ExtendedLdapAuthenticationConfigurationTest.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2022 by the geOrchestra PSC
+ *
+ * This file is part of geOrchestra.
+ *
+ * geOrchestra is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free
+ * Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * geOrchestra is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * geOrchestra. If not, see .
+ */
+
+package org.georchestra.gateway.security.ldap.extended;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.List;
+import java.util.Set;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.context.annotation.UserConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.context.ApplicationContext;
+import org.springframework.security.authentication.ReactiveAuthenticationManagerAdapter;
+
+/**
+ * Tests {@link ApplicationContext} contributions of
+ * {@link ExtendedLdapAuthenticationConfiguration}
+ */
+class ExtendedLdapAuthenticationConfigurationTest {
+
+ private ApplicationContextRunner runner = new ApplicationContextRunner()
+ .withConfiguration(UserConfigurations.of(ExtendedLdapAuthenticationConfiguration.class));
+
+ @SuppressWarnings("unchecked")
+ public @Test void contextContributions_empty_config() {
+ runner.run(context -> {
+ assertThat(context).hasNotFailed();
+ assertThat(context).getBean("enabledExtendedLdapConfigs").isInstanceOf(List.class);
+ assertThat(context.getBean("enabledExtendedLdapConfigs", List.class)).isEmpty();
+
+ assertThat(context.getBean("georchestraLdapAuthenticatedUserMapper").getClass().getName())
+ .isEqualTo("org.springframework.beans.factory.support.NullBean");
+
+ assertThat(context.getBean("extendedLdapAuthenticationManager").getClass().getName())
+ .isEqualTo("org.springframework.beans.factory.support.NullBean");
+
+ assertThat(context.getBean(DemultiplexingUsersApi.class)).hasFieldOrPropertyWithValue("targetNames",
+ Set.of());
+ });
+ }
+
+ @SuppressWarnings("unchecked")
+ public @Test void contextContributions_single_config() {
+ runner.withPropertyValues(""//
+ , "georchestra.gateway.security.ldap.ldap1.enabled: true" //
+ , "georchestra.gateway.security.ldap.ldap1.extended: true" //
+ , "georchestra.gateway.security.ldap.ldap1.url: ldap://ldap1.test.com:839" //
+ , "georchestra.gateway.security.ldap.ldap1.baseDn: dc=georchestra,dc=org" //
+ , "georchestra.gateway.security.ldap.ldap1.users.rdn: ou=users,dc=georchestra,dc=org" //
+ , "georchestra.gateway.security.ldap.ldap1.users.searchFilter: (uid={0})" //
+ , "georchestra.gateway.security.ldap.ldap1.roles.rdn: ou=roles" //
+ , "georchestra.gateway.security.ldap.ldap1.roles.searchFilter: (member={0})" //
+ , "georchestra.gateway.security.ldap.ldap1.roles.prefix: GROUP_" //
+ , "georchestra.gateway.security.ldap.ldap1.roles.upperCase: false" //
+ , "georchestra.gateway.security.ldap.ldap1.orgs.rdn: ou=orgs" //
+ ).run(context -> {
+ assertThat(context).hasNotFailed();
+ assertThat(context).getBean("enabledExtendedLdapConfigs").isInstanceOf(List.class);
+ assertThat(context.getBean("enabledExtendedLdapConfigs", List.class)).hasSize(1);
+ assertThat(context.getBean("extendedLdapAuthenticationProviders", List.class)).hasSize(1);
+ assertThat(context.getBean("extendedLdapAuthenticationProviders", List.class)).singleElement()
+ .isInstanceOf(GeorchestraLdapAuthenticationProvider.class);
+
+ assertThat(context.getBean("georchestraLdapAuthenticatedUserMapper"))
+ .isInstanceOf(GeorchestraLdapAuthenticatedUserMapper.class);
+
+ assertThat(context.getBean("extendedLdapAuthenticationManager"))
+ .isInstanceOf(ReactiveAuthenticationManagerAdapter.class);
+
+ assertThat(context.getBean(DemultiplexingUsersApi.class)).hasFieldOrPropertyWithValue("targetNames",
+ Set.of("ldap1"));
+ });
+ }
+
+ @SuppressWarnings("unchecked")
+ public @Test void contextContributions_multiple_configs() {
+ runner.withPropertyValues(""//
+ , "georchestra.gateway.security.ldap.ldap1.enabled: true" //
+ , "georchestra.gateway.security.ldap.ldap1.extended: true" //
+ , "georchestra.gateway.security.ldap.ldap1.url: ldap://ldap1.test.com:839" //
+ , "georchestra.gateway.security.ldap.ldap1.baseDn: dc=georchestra,dc=org" //
+ , "georchestra.gateway.security.ldap.ldap1.users.rdn: ou=users,dc=georchestra,dc=org" //
+ , "georchestra.gateway.security.ldap.ldap1.users.searchFilter: (uid={0})" //
+ , "georchestra.gateway.security.ldap.ldap1.roles.rdn: ou=roles" //
+ , "georchestra.gateway.security.ldap.ldap1.roles.searchFilter: (member={0})" //
+ , "georchestra.gateway.security.ldap.ldap1.roles.prefix: GROUP_" //
+ , "georchestra.gateway.security.ldap.ldap1.roles.upperCase: false" //
+ , "georchestra.gateway.security.ldap.ldap1.orgs.rdn: ou=orgs" //
+ //
+ , "georchestra.gateway.security.ldap.ldap2.enabled: true" //
+ , "georchestra.gateway.security.ldap.ldap2.extended: true" //
+ , "georchestra.gateway.security.ldap.ldap2.url: ldap://ldap1.test.com:839" //
+ , "georchestra.gateway.security.ldap.ldap2.baseDn: dc=georchestra,dc=org" //
+ , "georchestra.gateway.security.ldap.ldap2.users.rdn: ou=users,dc=georchestra,dc=org" //
+ , "georchestra.gateway.security.ldap.ldap2.users.searchFilter: (uid={0})" //
+ , "georchestra.gateway.security.ldap.ldap2.roles.rdn: ou=roles" //
+ , "georchestra.gateway.security.ldap.ldap2.roles.searchFilter: (member={0})" //
+ , "georchestra.gateway.security.ldap.ldap2.orgs.rdn: ou=orgs" //
+ ).run(context -> {
+
+ assertThat(context).hasNotFailed();
+ assertThat(context.getBean("enabledExtendedLdapConfigs", List.class)).hasSize(2);
+ assertThat(context.getBean("extendedLdapAuthenticationProviders", List.class)).hasSize(2);
+ assertThat(context.getBean("extendedLdapAuthenticationProviders", List.class)).hasSize(2)
+ .allMatch(GeorchestraLdapAuthenticationProvider.class::isInstance);
+
+ assertThat(context.getBean("georchestraLdapAuthenticatedUserMapper"))
+ .isInstanceOf(GeorchestraLdapAuthenticatedUserMapper.class);
+
+ assertThat(context.getBean("extendedLdapAuthenticationManager"))
+ .isInstanceOf(ReactiveAuthenticationManagerAdapter.class);
+
+ assertThat(context.getBean(DemultiplexingUsersApi.class)).hasFieldOrPropertyWithValue("targetNames",
+ Set.of("ldap1", "ldap2"));
+ });
+ }
+}
diff --git a/gateway/src/test/java/org/georchestra/gateway/security/ldap/GeorchestraLdapAuthenticatedUserMapperTest.java b/gateway/src/test/java/org/georchestra/gateway/security/ldap/extended/GeorchestraLdapAuthenticatedUserMapperTest.java
similarity index 98%
rename from gateway/src/test/java/org/georchestra/gateway/security/ldap/GeorchestraLdapAuthenticatedUserMapperTest.java
rename to gateway/src/test/java/org/georchestra/gateway/security/ldap/extended/GeorchestraLdapAuthenticatedUserMapperTest.java
index 2f51e9ae..161295b6 100644
--- a/gateway/src/test/java/org/georchestra/gateway/security/ldap/GeorchestraLdapAuthenticatedUserMapperTest.java
+++ b/gateway/src/test/java/org/georchestra/gateway/security/ldap/extended/GeorchestraLdapAuthenticatedUserMapperTest.java
@@ -17,7 +17,7 @@
* geOrchestra. If not, see .
*/
-package org.georchestra.gateway.security.ldap;
+package org.georchestra.gateway.security.ldap.extended;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertSame;