From c82dd699967fac047e0fef0b4b5a58a4d6b2822f Mon Sep 17 00:00:00 2001 From: Gabriel Roldan Date: Sat, 4 Jun 2022 07:31:14 -0300 Subject: [PATCH] Add support for LDAP Active Directory authentication. Multiple LDAP authorization services can be configured, in which case, when doing HTTP Basic auth and Form login, each **enabled** LDAP service will be probed for the authentication credentials in the order they appear in the configuration, and the first successful authentication will be used. If no `georchestra.security.ldap.[name].enabled` is `true`, the log-in page won't even show the username/password form inputs, and HTTP Basic authentication won't be enabled. At application startup, the enabled configurations are validated. The application will fail to start if there's a validation error. Each LDAP authentication provider can be one of: * A **standard** LDAP provider, which provides provides basic authorization credentials in the form of a list of role names. * An **extended** LDAP provider, as traditionally used by geOrchestra's internal OpenLDAP database, which enriches the authentication principal object with additional user identity properties. * An **Active Directory** LDAP provider, which provides basicauthorization credentials in the form of a list of role names. --- Makefile | 2 +- datadir | 2 +- docs/authzn.adoc | 144 +++++- gateway/pom.xml | 2 +- .../app/GeorchestraGatewayApplication.java | 1 - .../LdapSecurityAutoConfiguration.java | 16 +- .../ResolveGeorchestraUserGlobalFilter.java | 1 - ...traLdapAccountManagementConfiguration.java | 198 -------- .../security/ldap/LdapConfigBuilder.java | 77 +++ .../security/ldap/LdapConfigProperties.java | 188 +++---- .../ldap/LdapConfigPropertiesValidations.java | 118 +++++ .../ldap/LdapSecurityConfiguration.java | 108 ++++ .../MultipleLdapSecurityConfiguration.java | 213 -------- ...eDirectoryAuthenticationConfiguration.java | 103 ++++ .../ActiveDirectoryLdapServerConfig.java | 39 ++ .../BasicLdapAuthenticationConfiguration.java | 123 +++++ .../LdapAuthenticatedUserMapper.java | 2 +- .../LdapAuthenticatorProviderBuilder.java | 99 ++++ .../security/ldap/basic/LdapServerConfig.java | 40 ++ .../DemultiplexingUsersApi.java | 4 +- ...tendedLdapAuthenticationConfiguration.java | 253 ++++++++++ .../ldap/extended/ExtendedLdapConfig.java} | 34 +- ...eorchestraLdapAuthenticatedUserMapper.java | 4 +- ...GeorchestraLdapAuthenticationProvider.java | 49 ++ ...raUserNamePasswordAuthenticationToken.java | 4 +- .../main/resources/META-INF/spring.factories | 1 - .../LdapSecurityAutoConfigurationTest.java | 156 +++--- .../LdapConfigPropertiesValidationsTest.java | 462 ++++++++++++++++++ ...ectoryAuthenticationConfigurationTest.java | 114 +++++ ...icLdapAuthenticationConfigurationTest.java | 119 +++++ ...edLdapAuthenticationConfigurationTest.java | 131 +++++ ...hestraLdapAuthenticatedUserMapperTest.java | 2 +- 32 files changed, 2186 insertions(+), 623 deletions(-) delete mode 100644 gateway/src/main/java/org/georchestra/gateway/security/ldap/GeorchestraLdapAccountManagementConfiguration.java create mode 100644 gateway/src/main/java/org/georchestra/gateway/security/ldap/LdapConfigBuilder.java create mode 100644 gateway/src/main/java/org/georchestra/gateway/security/ldap/LdapConfigPropertiesValidations.java create mode 100644 gateway/src/main/java/org/georchestra/gateway/security/ldap/LdapSecurityConfiguration.java delete mode 100644 gateway/src/main/java/org/georchestra/gateway/security/ldap/MultipleLdapSecurityConfiguration.java create mode 100644 gateway/src/main/java/org/georchestra/gateway/security/ldap/activedirectory/ActiveDirectoryAuthenticationConfiguration.java create mode 100644 gateway/src/main/java/org/georchestra/gateway/security/ldap/activedirectory/ActiveDirectoryLdapServerConfig.java create mode 100644 gateway/src/main/java/org/georchestra/gateway/security/ldap/basic/BasicLdapAuthenticationConfiguration.java rename gateway/src/main/java/org/georchestra/gateway/security/ldap/{ => basic}/LdapAuthenticatedUserMapper.java (98%) create mode 100644 gateway/src/main/java/org/georchestra/gateway/security/ldap/basic/LdapAuthenticatorProviderBuilder.java create mode 100644 gateway/src/main/java/org/georchestra/gateway/security/ldap/basic/LdapServerConfig.java rename gateway/src/main/java/org/georchestra/gateway/security/ldap/{ => extended}/DemultiplexingUsersApi.java (96%) create mode 100644 gateway/src/main/java/org/georchestra/gateway/security/ldap/extended/ExtendedLdapAuthenticationConfiguration.java rename gateway/src/main/java/org/georchestra/gateway/{autoconfigure/security/GeorchestraLdapAccountManagementAutoConfiguration.java => security/ldap/extended/ExtendedLdapConfig.java} (51%) rename gateway/src/main/java/org/georchestra/gateway/security/ldap/{ => extended}/GeorchestraLdapAuthenticatedUserMapper.java (94%) create mode 100644 gateway/src/main/java/org/georchestra/gateway/security/ldap/extended/GeorchestraLdapAuthenticationProvider.java rename gateway/src/main/java/org/georchestra/gateway/security/ldap/{ => extended}/GeorchestraUserNamePasswordAuthenticationToken.java (94%) create mode 100644 gateway/src/test/java/org/georchestra/gateway/security/ldap/LdapConfigPropertiesValidationsTest.java create mode 100644 gateway/src/test/java/org/georchestra/gateway/security/ldap/activedirectory/ActiveDirectoryAuthenticationConfigurationTest.java create mode 100644 gateway/src/test/java/org/georchestra/gateway/security/ldap/basic/BasicLdapAuthenticationConfigurationTest.java create mode 100644 gateway/src/test/java/org/georchestra/gateway/security/ldap/extended/ExtendedLdapAuthenticationConfigurationTest.java rename gateway/src/test/java/org/georchestra/gateway/security/ldap/{ => extended}/GeorchestraLdapAuthenticatedUserMapperTest.java (98%) diff --git a/Makefile b/Makefile index 58f3b509..ca1bc1de 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ install: ./mvnw clean install -pl :georchestra-gateway -ntp -DskipTests test: - ./mvnw verify -pl :georchestra-gateway -ntp -DskipTests + ./mvnw verify -pl :georchestra-gateway -ntp docker: @TAG=`./mvnw -f gateway/ help:evaluate -q -DforceStdout -Dexpression=imageTag` && \ diff --git a/datadir b/datadir index be422c14..53619539 160000 --- a/datadir +++ b/datadir @@ -1 +1 @@ -Subproject commit be422c14e674d85911cb76ae4172ab558ed7bd9e +Subproject commit 53619539681a65906a60f3e6047da17f74fb2809 diff --git a/docs/authzn.adoc b/docs/authzn.adoc index 0b052e5b..b117a7cd 100644 --- a/docs/authzn.adoc +++ b/docs/authzn.adoc @@ -8,33 +8,147 @@ toc::[] == LDAP (HTTP Basic and Form Login) +Georchestra Gateway supports authentication and authorization against LDAP, +including Microsoft Active Directory. + +Multiple LDAP authorization services can be configured, in which case, when +doing HTTP Basic auth and Form login, each **enabled** LDAP service will be +probed for the authentication credentials in the order they appear in the +configuration, and the first successful authentication will be used. + +If no `georchestra.security.ldap.[name].enabled` is `true`, the log-in page won't +even show the username/password form inputs, and HTTP Basic authentication won't be +enabled. + +At application startup, the enabled configurations are validated. The application +will fail to start if there's a validation error. + +Each LDAP authentication provider can be one of: + +* A **standard** LDAP provider, which provides provides basic authorization +credentials in the form of a list of role names. +* An **extended** LDAP provider, as traditionally used by geOrchestra's +internal OpenLDAP database, which enriches the authentication principal +object with additional user identity properties. +* An **Active Directory** LDAP provider, which provides basicauthorization +credentials in the form of a list of role names. + +=== Configuration properties + LDAP Authentication is enabled and set up through the following -configuration properties in `application.yml`: +externalized configuration properties (usually from georchestra data +directory's `gateway/security.yaml`): + +|=== +|Property name | Default value | Description +|`georchestra.gateway.security.ldap.[name]` +| +|Name assigned to the configuration, under which to set up each specific LDAP provider. + +|`georchestra.gateway.security.ldap.[name].enabled` +|`false` +|Whether the LDAP authentication provider is enabled. If `false` (default) it won't be taken into account at startup. If `true` and the configuration is invalid, the application won't be able to +start. The configuration can be invalid because a mandatory property is not set. Some properties +are optional or mandatory depending on whether the authentication provider is "standard", "extended", +or "Active Directory". + +|`georchestra.gateway.security.ldap.[name].extended` +|`false` +|If `true`, then geOrchestra's extended LDAP properties will be extracted as part of the authentication +user principal, besides the authorization role names. These properties are usually configured to be sent back to backend services in `georchestra.gateway.default-headers.*` and/or +`georchestra.gateway.services.[service].headers.*` This property and `activeDirectory` are mutually +exclusive, both can't be `true`. + +|`georchestra.gateway.security.ldap.[name].activeDirectory` +|`false` +|If `true`, the authentication provider is configured as an Active Directory service . This property and `extended` are mutually exclusive, both can't be `true`. + +|`georchestra.gateway.security.ldap.[name].url` +| +|Mandatory. The LDAP URL, for example: `ldap://localhost:389`. + +|`georchestra.gateway.security.ldap.[name].domain` +| +|The Active Directory domain, maybe empty, though most of the time you'll need to set the configured domain. Only relevant if `activeDirectory` is `true`. + +|`georchestra.gateway.security.ldap.[name].baseDn` +| +|Base Distinguished Name of the LDAP directory. +Also named root or suffix, see http://www.zytrax.com/books/ldap/apd/index.html#base +For example, georchestra's default baseDn is `dc=georchestra,dc=org` + +|`georchestra.gateway.security.ldap.[name].users.rdn` +| +|Mandatory except if `activeDirectory` is `true`, in which case it's ignored. 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`. + +|`georchestra.gateway.security.ldap.[name].users.searchFilter` +|No default value for basic and extended LDAP. Defaults to `(&(objectClass=user)(userPrincipalName={0}))` for Active Directory. +|Optional if `activeDirectory` is `true`, mandatory otherwise. Users search filter, +e.g. `(uid={0})`. + +|`georchestra.gateway.security.ldap.[name].roles.rdn` +| +|Ignored for Active Directory, mandatory otherwise. 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 shall be `ou=roles`. + +|`georchestra.gateway.security.ldap.[name].roles.searchFilter` +| +|Ignored for Active Directory, mandatory otherwise. Roles search filter. e.g. `(member={0})`. + +|`georchestra.gateway.security.ldap.[name].orgs.rdn` +| +| Mandatory if `[name].extended` is `true`, ignored otherwise. Organizations search base. +For example: `ou=orgs`. +|=== + +=== Sample configuration + +The usual geOrchestra OpenLDAP configuration is embedded in the application's +default configuration file, but disabled, to make it really easy to get started +with default settings, by just setting `georchestra.gateway.security.ldap.default.enabled=true`. + +The following is a sample configuration encompassing three LDAP services, the `default` one, +another extended config named `ldap2`, and an Active Directory service named `activeDirSample`: [source,yaml] ---- -georchestra.security.ldap: - enabled: true - url: ${ldapScheme}://${ldapHost}:${ldapPort} - baseDn: ${ldapBaseDn:dc=georchestra,dc=org} - usersRdn: ${ldapUsersRdn:ou=users} - userSearchFilter: ${ldapUserSearchFilter:(uid={0})} - rolesRdn: ${ldapRolesRdn:ou=roles} - rolesSearchFilter: ${ldapRolesSearchFilter:(member={0})} +georchestra: + gateway: + security: + ldap: + default: + enabled: true + ldap2: + enabled: false + extended: true + url: ${ldapScheme}://${ldapHost}:${ldapPort} + baseDn: ${ldapBaseDn:dc=georchestra,dc=org} + users: + rdn: ${ldapUsersRdn:ou=users} + searchFilter: ${ldapUserSearchFilter:(uid={0})} + roles: + rdn: ${ldapRolesRdn:ou=roles} + searchFilter: ${ldapRolesSearchFilter:(member={0})} + orgs: + rdn: ${ldapOrgsRdn:ou=orgs} + activeDirSample: + enabled: false + url: ldap://test.activedirectory.com:389 + domain: test.georchestra.org + baseDn: dc=georchestra,dc=org + users.searchFilter: (&(objectClass=user)(userPrincipalName={0})) ---- -If `georchestra.security.ldap.enabled` is `false`,the log-in page won't show the -username/password form inputs. - -=== Externalized Configuration == OAuth2 -=== OAuth2 Externalized Configuration +=== OAuth2 Configuration == OpenID Connect -=== Externalized Configuration +=== Configuration Both standard and non-standard claims can be used to set the `GeorchestraUser`'s `organization` short name and `roles` properties using JSONPath expressions with diff --git a/gateway/pom.xml b/gateway/pom.xml index cd59cd5d..74323050 100644 --- a/gateway/pom.xml +++ b/gateway/pom.xml @@ -156,7 +156,7 @@ - [11,) + 11 [3.6.3,) diff --git a/gateway/src/main/java/org/georchestra/gateway/app/GeorchestraGatewayApplication.java b/gateway/src/main/java/org/georchestra/gateway/app/GeorchestraGatewayApplication.java index 770bed75..44f26f34 100644 --- a/gateway/src/main/java/org/georchestra/gateway/app/GeorchestraGatewayApplication.java +++ b/gateway/src/main/java/org/georchestra/gateway/app/GeorchestraGatewayApplication.java @@ -19,7 +19,6 @@ package org.georchestra.gateway.app; import java.io.File; -import java.net.URI; import java.util.LinkedHashMap; import java.util.Map; diff --git a/gateway/src/main/java/org/georchestra/gateway/autoconfigure/security/LdapSecurityAutoConfiguration.java b/gateway/src/main/java/org/georchestra/gateway/autoconfigure/security/LdapSecurityAutoConfiguration.java index 6094e588..579be676 100644 --- a/gateway/src/main/java/org/georchestra/gateway/autoconfigure/security/LdapSecurityAutoConfiguration.java +++ b/gateway/src/main/java/org/georchestra/gateway/autoconfigure/security/LdapSecurityAutoConfiguration.java @@ -20,15 +20,27 @@ import javax.annotation.PostConstruct; -import org.georchestra.gateway.security.ldap.MultipleLdapSecurityConfiguration; +import org.georchestra.gateway.security.ldap.LdapSecurityConfiguration; +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.autoconfigure.EnableAutoConfiguration; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import lombok.extern.slf4j.Slf4j; +/** + * {@link EnableAutoConfiguration AutoConfiguration} to set up LDAP security + * + * @see LdapSecurityConfiguration + * @see BasicLdapAuthenticationConfiguration + * @see ExtendedLdapAuthenticationConfiguration + * @see ActiveDirectoryAuthenticationConfiguration + */ @Configuration(proxyBeanMethods = false) @ConditionalOnLdapEnabled -@Import(MultipleLdapSecurityConfiguration.class) +@Import(LdapSecurityConfiguration.class) @Slf4j(topic = "org.georchestra.gateway.autoconfigure.security") public class LdapSecurityAutoConfiguration { diff --git a/gateway/src/main/java/org/georchestra/gateway/security/ResolveGeorchestraUserGlobalFilter.java b/gateway/src/main/java/org/georchestra/gateway/security/ResolveGeorchestraUserGlobalFilter.java index 5ed74bc9..8138db60 100644 --- a/gateway/src/main/java/org/georchestra/gateway/security/ResolveGeorchestraUserGlobalFilter.java +++ b/gateway/src/main/java/org/georchestra/gateway/security/ResolveGeorchestraUserGlobalFilter.java @@ -18,7 +18,6 @@ */ package org.georchestra.gateway.security; -import org.georchestra.gateway.filter.global.ResolveTargetGlobalFilter; import org.georchestra.gateway.model.GeorchestraTargetConfig; import org.georchestra.gateway.model.GeorchestraUsers; import org.georchestra.security.model.GeorchestraUser; diff --git a/gateway/src/main/java/org/georchestra/gateway/security/ldap/GeorchestraLdapAccountManagementConfiguration.java b/gateway/src/main/java/org/georchestra/gateway/security/ldap/GeorchestraLdapAccountManagementConfiguration.java deleted file mode 100644 index 9894ae2e..00000000 --- a/gateway/src/main/java/org/georchestra/gateway/security/ldap/GeorchestraLdapAccountManagementConfiguration.java +++ /dev/null @@ -1,198 +0,0 @@ -package org.georchestra.gateway.security.ldap; - -/* - * 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.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.Organizations; -import org.georchestra.gateway.security.ldap.LdapConfigProperties.Roles; -import org.georchestra.gateway.security.ldap.LdapConfigProperties.Server; -import org.georchestra.gateway.security.ldap.LdapConfigProperties.Users; -import org.georchestra.security.api.UsersApi; -import org.georchestra.security.model.GeorchestraUser; -import org.springframework.beans.factory.BeanInitializationException; -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.ldap.userdetails.LdapUserDetails; - -/** - * 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) -public class GeorchestraLdapAccountManagementConfiguration { - - @Bean - public GeorchestraLdapAuthenticatedUserMapper georchestraLdapAuthenticatedUserMapper(DemultiplexingUsersApi users) { - return new GeorchestraLdapAuthenticatedUserMapper(users); - } - - @Bean - public DemultiplexingUsersApi demultiplexingUsersApi(LdapConfigProperties config) { - - Map ldapExtendedConfigs = config.getLdap().entrySet().stream() - .filter(e -> e.getValue().isEnabled())// - .filter(e -> e.getValue().hasGeorchestraExtensions())// - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - - Map targets = new HashMap<>(); - ldapExtendedConfigs.forEach((name, ldapConfig) -> { - UsersApi target; - try { - target = createUsersApi(ldapConfig); - } catch (Exception ex) { - throw new BeanInitializationException("Error creating georchestra users api for ldap config " + name, - ex); - } - targets.put(name, target); - }); - return new DemultiplexingUsersApi(targets); - } - - ////////////////////////////////////////////// - /// Low level LDAP account management beans - ////////////////////////////////////////////// - - private UsersApi createUsersApi(Server 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(Server 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, Server ldapConfig) { - Users usersConfig = ldapConfig.getUsers(); - requireNonNull(usersConfig); - - Roles rolesConfig = ldapConfig.getRoles(); - requireNonNull(rolesConfig); - - String baseDn = ldapConfig.getBaseDn(); - String userSearchBaseDN = usersConfig.getRdn(); - String roleSearchBaseDN = rolesConfig.getRdn(); - String pendingUsersSearchBaseDN = usersConfig.getPendingUsersSearchBaseDN(); - - AccountDaoImpl impl = new AccountDaoImpl(ldapTemplate); - impl.setBasePath(baseDn); - impl.setUserSearchBaseDN(userSearchBaseDN); - impl.setRoleSearchBaseDN(roleSearchBaseDN); - if (pendingUsersSearchBaseDN != null) { - impl.setPendingUserSearchBaseDN(pendingUsersSearchBaseDN); - } - - Organizations orgsConfig = ldapConfig.getOrgs(); - if (orgsConfig != null) { - String orgSearchBaseDN = orgsConfig.getRdn(); - String pendingOrgSearchBaseDN = orgsConfig.getPendingOrgSearchBaseDN(); - impl.setOrgSearchBaseDN(orgSearchBaseDN); - impl.setPendingOrgSearchBaseDN(pendingOrgSearchBaseDN); - } - - impl.init(); - return impl; - } - - private RoleDao roleDao(LdapTemplate ldapTemplate, Server ldapConfig, AccountDao accountDao) { - String rolesRdn = ldapConfig.getRoles().getRdn(); - - 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()); - impl.setPendingOrgSearchBaseDN(ldapConfig.getOrgs().getPendingOrgSearchBaseDN()); - impl.setOrgTypeValues(ldapConfig.getOrgs().getOrgTypes()); - return impl; - } - - private UserRule ldapUserRule(Server ldapConfig) { - Users users = ldapConfig.getUsers(); - List protectedUsers = users.getProtectedUsers(); - UserRule rule = new UserRule(); - rule.setListOfprotectedUsers(protectedUsers.toArray(String[]::new)); - return rule; - } - - private RoleProtected ldapProtectedRoles(Server ldapConfig) { - Roles roles = ldapConfig.getRoles(); - List protectedRoles = roles.getProtectedRoles(); - RoleProtected bean = new RoleProtected(); - bean.setListOfprotectedRoles(protectedRoles.toArray(String[]::new)); - return bean; - } - -} diff --git a/gateway/src/main/java/org/georchestra/gateway/security/ldap/LdapConfigBuilder.java b/gateway/src/main/java/org/georchestra/gateway/security/ldap/LdapConfigBuilder.java new file mode 100644 index 00000000..80487b69 --- /dev/null +++ b/gateway/src/main/java/org/georchestra/gateway/security/ldap/LdapConfigBuilder.java @@ -0,0 +1,77 @@ +/* + * 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.util.Optional.ofNullable; + +import java.util.Optional; + +import org.georchestra.gateway.security.ldap.LdapConfigProperties.Server; +import org.georchestra.gateway.security.ldap.LdapConfigProperties.Users; +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.springframework.util.StringUtils; + +/** + */ +class LdapConfigBuilder { + + public LdapServerConfig asBasicLdapConfig(String name, Server config) { + return LdapServerConfig.builder()// + .name(name)// + .enabled(config.isEnabled())// + .url(config.getUrl())// + .baseDn(config.getBaseDn())// + .usersRdn(config.getUsers().getRdn())// + .usersSearchFilter(config.getUsers().getSearchFilter())// + .rolesRdn(config.getRoles().getRdn())// + .rolesSearchFilter(config.getRoles().getSearchFilter())// + .build(); + } + + public ExtendedLdapConfig asExtendedLdapConfig(String name, Server config) { + return ExtendedLdapConfig.builder()// + .name(name)// + .enabled(config.isEnabled())// + .url(config.getUrl())// + .baseDn(config.getBaseDn())// + .usersRdn(config.getUsers().getRdn())// + .usersSearchFilter(config.getUsers().getSearchFilter())// + .rolesRdn(config.getRoles().getRdn())// + .rolesSearchFilter(config.getRoles().getSearchFilter())// + .orgsRdn(config.getOrgs().getRdn())// + .build(); + } + + public ActiveDirectoryLdapServerConfig asActiveDirectoryConfig(String name, Server config) { + return ActiveDirectoryLdapServerConfig.builder()// + .name(name)// + .enabled(config.isEnabled())// + .url(config.getUrl())// + .domain(toOptional(config.getDomain()))// + .rootDn(toOptional(config.getBaseDn()))// + .searchFilter(ofNullable(config.getUsers()).map(Users::getSearchFilter).flatMap(this::toOptional))// + .build(); + } + + private Optional toOptional(String value) { + return ofNullable(StringUtils.hasText(value) ? value : null); + } +} diff --git a/gateway/src/main/java/org/georchestra/gateway/security/ldap/LdapConfigProperties.java b/gateway/src/main/java/org/georchestra/gateway/security/ldap/LdapConfigProperties.java index 2a0268e8..e2392161 100644 --- a/gateway/src/main/java/org/georchestra/gateway/security/ldap/LdapConfigProperties.java +++ b/gateway/src/main/java/org/georchestra/gateway/security/ldap/LdapConfigProperties.java @@ -20,18 +20,23 @@ import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.stream.Collectors; +import java.util.stream.Stream; -import javax.validation.constraints.NotBlank; -import javax.validation.constraints.NotNull; +import javax.validation.Valid; +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.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.util.StringUtils; +import org.springframework.validation.Errors; +import org.springframework.validation.Validator; import org.springframework.validation.annotation.Validated; import lombok.Data; import lombok.Generated; -import lombok.Getter; +import lombok.experimental.Accessors; /** * Config properties, usually loaded from georchestra datadir's @@ -53,43 +58,39 @@ */ @Data @Generated -@ConfigurationProperties(prefix = "georchestra.gateway.security") @Validated -public class LdapConfigProperties { +@Accessors(chain = true) +@ConfigurationProperties(prefix = "georchestra.gateway.security") +public class LdapConfigProperties implements Validator { + @Valid private Map ldap = Map.of(); - public static class LdapServerConfig extends Server { - - private @Getter String name; - - public LdapServerConfig(String name, Server value) { - this.name = name; - super.setUrl(value.getUrl()); - super.setBaseDn(value.getBaseDn()); - super.setEnabled(value.isEnabled()); - super.setExtended(value.isExtended()); - super.setUsers(value.getUsers()); - super.setRoles(value.getRoles()); - super.setOrgs(value.getOrgs()); - } - - } - - public List configs() { - return ldap.entrySet().stream().map(e -> new LdapServerConfig(e.getKey(), e.getValue())) - .collect(Collectors.toList()); - } - + @Generated public static @Data class Server { boolean enabled; + /** + * Whether the LDAP authentication source shall use georchestra-specific + * extensions. For example, when using the default OpenLDAP database with + * additional user identity information + */ boolean extended; - @NotBlank private String url; + /** + * Flag indicating the LDAP authentication end point is an Active Directory + * service + */ + private boolean activeDirectory; + + /** + * The active directory domain, maybe null or empty. + */ + private String domain; + /** * Base DN of the LDAP directory Base Distinguished Name of the LDAP directory. * Also named root or suffix, see @@ -97,92 +98,113 @@ public List 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_"; + @Generated + public static @Data @Accessors(chain = true) class Organizations { - private boolean upperCase = true; + /** + * Organizations search base. Default: ou=orgs + */ + private String rdn = "ou=orgs"; + } - private List protectedRoles = List.of(); + public @Override boolean supports(Class clazz) { + return LdapConfigProperties.class.equals(clazz); } - public static @Data class Organizations { + @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)); + } - @NotBlank - private String rdn = "ou=orgs"; + 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()); + } - @NotBlank - private String orgTypes = "Association,Company,NGO,Individual,Other"; + 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()); + } - @NotBlank - private String pendingOrgSearchBaseDN = "ou=pendingorgs"; + 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()); } + + 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..e1c32cc1 --- /dev/null +++ b/gateway/src/main/java/org/georchestra/gateway/security/ldap/LdapConfigPropertiesValidations.java @@ -0,0 +1,118 @@ +/* + * 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}))", + name); + } + 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..56d7a4dc --- /dev/null +++ b/gateway/src/main/java/org/georchestra/gateway/security/ldap/basic/BasicLdapAuthenticationConfiguration.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 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())// + .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..e68b6472 --- /dev/null +++ b/gateway/src/main/java/org/georchestra/gateway/security/ldap/basic/LdapAuthenticatorProviderBuilder.java @@ -0,0 +1,99 @@ +/* + * 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 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; + + 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() { + return new SimpleAuthorityMapper(); + } + + private DefaultLdapAuthoritiesPopulator ldapAuthoritiesPopulator(BaseLdapPathContextSource contextSource) { + DefaultLdapAuthoritiesPopulator authoritiesPopulator = new DefaultLdapAuthoritiesPopulator(contextSource, + rolesSearchBase); + authoritiesPopulator.setGroupSearchFilter(rolesSearchFilter); + 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..a010be8d --- /dev/null +++ b/gateway/src/main/java/org/georchestra/gateway/security/ldap/basic/LdapServerConfig.java @@ -0,0 +1,40 @@ +/* + * 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 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; +} \ 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..ceff6be9 --- /dev/null +++ b/gateway/src/main/java/org/georchestra/gateway/security/ldap/extended/ExtendedLdapAuthenticationConfiguration.java @@ -0,0 +1,253 @@ +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())// + .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..8a281e25 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,27 @@ * 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 lombok.Builder; +import lombok.Generated; +import lombok.NonNull; +import lombok.Value; -import lombok.extern.slf4j.Slf4j; +@Value +@Builder +@Generated +public class ExtendedLdapConfig { + private @NonNull String name; + private boolean enabled; + private @NonNull String url; + private @NonNull String baseDn; -@Configuration(proxyBeanMethods = false) -@ConditionalOnLdapEnabled -@Import(GeorchestraLdapAccountManagementConfiguration.class) -@Slf4j(topic = "org.georchestra.gateway.autoconfigure.security") -public class GeorchestraLdapAccountManagementAutoConfiguration { + private @NonNull String usersRdn; + private @NonNull String usersSearchFilter; + private @NonNull String rolesRdn; + private @NonNull String rolesSearchFilter; - public @PostConstruct void log() { - log.info("georchestra LDAP security extensions enabled"); - } -} + 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..bbc2ba65 --- /dev/null +++ b/gateway/src/test/java/org/georchestra/gateway/security/ldap/LdapConfigPropertiesValidationsTest.java @@ -0,0 +1,462 @@ +/* + * 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})" // + ).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})"); + }); + } + + 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.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("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..98b9163e --- /dev/null +++ b/gateway/src/test/java/org/georchestra/gateway/security/ldap/basic/BasicLdapAuthenticationConfigurationTest.java @@ -0,0 +1,119 @@ +/* + * 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.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.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..2b8f891c --- /dev/null +++ b/gateway/src/test/java/org/georchestra/gateway/security/ldap/extended/ExtendedLdapAuthenticationConfigurationTest.java @@ -0,0 +1,131 @@ +/* + * 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.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.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;