diff --git a/datadir/gateway/security.yaml b/datadir/gateway/security.yaml index 0a4e5a6e..39f54490 100644 --- a/datadir/gateway/security.yaml +++ b/datadir/gateway/security.yaml @@ -4,6 +4,15 @@ georchestra: gateway: security: + createNonExistingUsersInLDAP: false + events: + rabbitmq: + # Note usually enableRabbitmqEvents, rabbitmqHost, etc. come from georchestra's default.properties + enabled: ${enableRabbitmqEvents:false} + host: ${rabbitmqHost} + port: ${rabbitmqPort} + user: ${rabbitmqUser} + password: ${rabbitmqPassword} oauth2: # if enabled, make sure to have at least one OAuth2 client # set up at spring.security.oauth2.client below @@ -19,7 +28,7 @@ georchestra: # Multiple LDAP data sources are supported. The first key defines a simple # name for them. The `default` one here, disabled by default, is pre-configured # to use Georchestra's default OpenLDAP database. - # You should usually just enable it in the georchestra dataidr's gateway.yml + # You should usually just enable it in the georchestra datadir's gateway.yml # with georchestra.gateway.security.ldap.default.enabled: true default: enabled: true diff --git a/docker-compose.yml b/docker-compose.yml index 1d29aae9..a8f4b112 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,12 +8,6 @@ volumes: o: bind device: $PWD/datadir -secrets: - slapd_password: - file: ./datadir/secrets/slapd_password.txt - geoserver_privileged_user_passwd: - file: ./datadir/secrets/geoserver_privileged_user_passwd.txt - services: database: image: georchestra/database:latest @@ -28,9 +22,6 @@ services: ldap: image: georchestra/ldap:latest - secrets: - - slapd_password - - geoserver_privileged_user_passwd environment: - SLAPD_ORGANISATION=georchestra - SLAPD_DOMAIN=georchestra.org diff --git a/gateway/src/main/java/org/georchestra/gateway/accounts/admin/AbstractAccountsManager.java b/gateway/src/main/java/org/georchestra/gateway/accounts/admin/AbstractAccountsManager.java new file mode 100644 index 00000000..5e384f38 --- /dev/null +++ b/gateway/src/main/java/org/georchestra/gateway/accounts/admin/AbstractAccountsManager.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2023 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.accounts.admin; + +import java.util.Optional; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Consumer; + +import org.georchestra.security.model.GeorchestraUser; + +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public abstract class AbstractAccountsManager implements AccountManager { + + private final @NonNull Consumer eventPublisher; + + protected final ReadWriteLock lock = new ReentrantReadWriteLock(); + + @Override + public GeorchestraUser getOrCreate(@NonNull GeorchestraUser mappedUser) { + return find(mappedUser).orElseGet(() -> createIfMissing(mappedUser)); + } + + protected Optional find(GeorchestraUser mappedUser) { + lock.readLock().lock(); + try { + return findInternal(mappedUser); + } finally { + lock.readLock().unlock(); + } + } + + protected Optional findInternal(GeorchestraUser mappedUser) { + if (null != mappedUser.getOAuth2ProviderId()) { + return findByOAuth2ProviderId(mappedUser.getOAuth2ProviderId()); + } + return findByUsername(mappedUser.getUsername()); + } + + GeorchestraUser createIfMissing(GeorchestraUser mapped) { + lock.writeLock().lock(); + try { + GeorchestraUser existing = findInternal(mapped).orElse(null); + if (null == existing) { + createInternal(mapped); + existing = findInternal(mapped).orElseThrow(() -> new IllegalStateException( + "User " + mapped.getUsername() + " not found right after creation")); + eventPublisher.accept(new AccountCreated(existing)); + } + return existing; + + } finally { + lock.writeLock().unlock(); + } + } + + protected abstract Optional findByOAuth2ProviderId(String oauth2ProviderId); + + protected abstract Optional findByUsername(String username); + + protected abstract void createInternal(GeorchestraUser mapped); + +} diff --git a/gateway/src/main/java/org/georchestra/gateway/accounts/admin/AccountCreated.java b/gateway/src/main/java/org/georchestra/gateway/accounts/admin/AccountCreated.java new file mode 100644 index 00000000..aa6775c1 --- /dev/null +++ b/gateway/src/main/java/org/georchestra/gateway/accounts/admin/AccountCreated.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2023 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.accounts.admin; + +import org.georchestra.security.model.GeorchestraUser; + +import lombok.NonNull; +import lombok.Value; + +/** + * Application event published when a new account was created + */ +@Value +public class AccountCreated { + + private @NonNull GeorchestraUser user; +} diff --git a/gateway/src/main/java/org/georchestra/gateway/accounts/admin/AccountManager.java b/gateway/src/main/java/org/georchestra/gateway/accounts/admin/AccountManager.java new file mode 100644 index 00000000..d5b51975 --- /dev/null +++ b/gateway/src/main/java/org/georchestra/gateway/accounts/admin/AccountManager.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2023 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.accounts.admin; + +import org.georchestra.gateway.security.GeorchestraUserMapper; +import org.georchestra.gateway.security.ResolveGeorchestraUserGlobalFilter; +import org.georchestra.security.model.GeorchestraUser; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.security.core.Authentication; + +/** + * @see CreateAccountUserCustomizer + * @see ResolveGeorchestraUserGlobalFilter + */ +public interface AccountManager { + + /** + * Finds the stored user that belongs to the {@code mappedUser} or creates it if + * it doesn't exist in the users repository. + *

+ * When a user is created, an {@link AccountCreated} event must be published to + * the {@link ApplicationEventPublisher}. + * + * @param mappedUser the user {@link ResolveGeorchestraUserGlobalFilter} + * resolved by calling + * {@link GeorchestraUserMapper#resolve(Authentication)} + * @return the stored version of the user, whether it existed or was created as + * the result of calling this method. + */ + GeorchestraUser getOrCreate(GeorchestraUser mappedUser); + +} diff --git a/gateway/src/main/java/org/georchestra/gateway/accounts/admin/CreateAccountUserCustomizer.java b/gateway/src/main/java/org/georchestra/gateway/accounts/admin/CreateAccountUserCustomizer.java new file mode 100644 index 00000000..c0279be4 --- /dev/null +++ b/gateway/src/main/java/org/georchestra/gateway/accounts/admin/CreateAccountUserCustomizer.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2023 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.accounts.admin; + +import java.util.Objects; + +import org.georchestra.gateway.security.GeorchestraUserCustomizerExtension; +import org.georchestra.security.model.GeorchestraUser; +import org.springframework.core.Ordered; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; + +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +/** + * {@link GeorchestraUserCustomizerExtension} that + * {@link AccountManager#getOrCreate creates an account} when authenticated + * through request headers (trusted proxy feature) or through OAuth2. + */ +@RequiredArgsConstructor +public class CreateAccountUserCustomizer implements GeorchestraUserCustomizerExtension, Ordered { + + private final @NonNull AccountManager accounts; + + /** + * @return {@link Ordered#LOWEST_PRECEDENCE} so it runs after all other + * authentication customizations have been performed, such as setting + * additional roles from externalized configuration, etc. + */ + public @Override int getOrder() { + return Ordered.LOWEST_PRECEDENCE; + } + + /** + * @return the stored version (either existing or created as result of calling + * this method) of the user account, if the {@code Authentication} + * object is either an {@link OAuth2AuthenticationToken} + */ + @Override + public @NonNull GeorchestraUser apply(@NonNull Authentication auth, @NonNull GeorchestraUser mappedUser) { + final boolean isOauth2 = auth instanceof OAuth2AuthenticationToken; + if (isOauth2) { + Objects.requireNonNull(mappedUser.getOAuth2ProviderId(), "GeorchestraUser.oAuth2ProviderId is null"); + } + if (isOauth2) { + return accounts.getOrCreate(mappedUser); + } + return mappedUser; + } + +} diff --git a/gateway/src/main/java/org/georchestra/gateway/accounts/admin/ldap/GeorchestraLdapAccountManagementConfiguration.java b/gateway/src/main/java/org/georchestra/gateway/accounts/admin/ldap/GeorchestraLdapAccountManagementConfiguration.java new file mode 100644 index 00000000..4072a549 --- /dev/null +++ b/gateway/src/main/java/org/georchestra/gateway/accounts/admin/ldap/GeorchestraLdapAccountManagementConfiguration.java @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2023 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.accounts.admin.ldap; + +import static java.util.Objects.requireNonNull; + +import java.util.Collections; +import java.util.List; + +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.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.accounts.admin.AccountManager; +import org.georchestra.gateway.accounts.admin.CreateAccountUserCustomizer; +import org.georchestra.gateway.security.ldap.LdapConfigProperties; +import org.georchestra.gateway.security.ldap.extended.ExtendedLdapConfig; +import org.georchestra.security.api.UsersApi; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.ApplicationEventPublisher; +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.ldap.pool.factory.PoolingContextSource; +import org.springframework.ldap.pool.validation.DefaultDirContextValidator; + +@Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties(LdapConfigProperties.class) +public class GeorchestraLdapAccountManagementConfiguration { + + @Bean + AccountManager ldapAccountsManager(// + ApplicationEventPublisher eventPublisher, // + AccountDao accountDao, RoleDao roleDao, OrgsDao orgsDao) { + + UsersApi usersApi = ldapUsersApi(accountDao, roleDao); + return new LdapAccountsManager(eventPublisher::publishEvent, accountDao, roleDao, orgsDao, usersApi); + } + + @Bean + CreateAccountUserCustomizer createAccountUserCustomizer(AccountManager accountManager) { + return new CreateAccountUserCustomizer(accountManager); + } + + private UsersApi ldapUsersApi(AccountDao accountDao, RoleDao roleDao) { + UserMapperImpl mapper = new UserMapperImpl(); + mapper.setRoleDao(roleDao); + List protectedUsers = Collections.emptyList(); + UserRule rule = new UserRule(); + rule.setListOfprotectedUsers(protectedUsers.toArray(String[]::new)); + UsersApiImpl usersApi = new UsersApiImpl(); + usersApi.setAccountsDao(accountDao); + usersApi.setMapper(mapper); + usersApi.setUserRule(rule); + return usersApi; + } + + @Bean + LdapContextSource singleContextSource(LdapConfigProperties config) { + ExtendedLdapConfig ldapConfig = config.extendedEnabled().get(0); + LdapContextSource singleContextSource = new LdapContextSource(); + singleContextSource.setUrl(ldapConfig.getUrl()); + singleContextSource.setBase(ldapConfig.getBaseDn()); + singleContextSource.setUserDn(ldapConfig.getAdminDn().get()); + singleContextSource.setPassword(ldapConfig.getAdminPassword().get()); + return singleContextSource; + } + + @Bean + PoolingContextSource contextSource(LdapContextSource singleContextSource) { + PoolingContextSource contextSource = new PoolingContextSource(); + contextSource.setContextSource(singleContextSource); + contextSource.setDirContextValidator(new DefaultDirContextValidator()); + contextSource.setTestOnBorrow(true); + contextSource.setMaxActive(8); + contextSource.setMinIdle(1); + contextSource.setMaxIdle(8); + contextSource.setMaxTotal(-1); + contextSource.setMaxWait(-1); + return contextSource; + } + + @Bean + LdapTemplate ldapTemplate(PoolingContextSource contextSource) throws Exception { + LdapTemplate ldapTemplate = new LdapTemplate(contextSource); + return ldapTemplate; + } + + @Bean + RoleDao roleDao(LdapTemplate ldapTemplate, LdapConfigProperties config) { + RoleDaoImpl impl = new RoleDaoImpl(); + impl.setLdapTemplate(ldapTemplate); + impl.setRoleSearchBaseDN(config.extendedEnabled().get(0).getRolesRdn()); + return impl; + } + + @Bean + OrgsDao orgsDao(LdapTemplate ldapTemplate, LdapConfigProperties config) { + OrgsDaoImpl impl = new OrgsDaoImpl(); + impl.setLdapTemplate(ldapTemplate); + ExtendedLdapConfig ldapConfig = config.extendedEnabled().get(0); + impl.setBasePath(ldapConfig.getBaseDn()); + impl.setOrgSearchBaseDN(ldapConfig.getOrgsRdn()); + impl.setPendingOrgSearchBaseDN(ldapConfig.getPendingOrgsRdn()); + return impl; + } + + @Bean + AccountDao accountDao(LdapTemplate ldapTemplate, LdapConfigProperties config) throws Exception { + ExtendedLdapConfig ldapConfig = config.extendedEnabled().get(0); + 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); + + final String pendingOrgSearchBaseDN = "ou=pendingorgs"; + impl.setPendingOrgSearchBaseDN(pendingOrgSearchBaseDN); + + impl.init(); + return impl; + } + + @Bean + RoleProtected roleProtected() { + RoleProtected roleProtected = new RoleProtected(); + roleProtected.setListOfprotectedRoles( + new String[] { "ADMINISTRATOR", "GN_.*", "ORGADMIN", "REFERENT", "USER", "SUPERUSER" }); + return roleProtected; + } +} diff --git a/gateway/src/main/java/org/georchestra/gateway/accounts/admin/ldap/LdapAccountsManager.java b/gateway/src/main/java/org/georchestra/gateway/accounts/admin/ldap/LdapAccountsManager.java new file mode 100644 index 00000000..50a33c77 --- /dev/null +++ b/gateway/src/main/java/org/georchestra/gateway/accounts/admin/ldap/LdapAccountsManager.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2023 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.accounts.admin.ldap; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import org.georchestra.ds.DataServiceException; +import org.georchestra.ds.DuplicatedCommonNameException; +import org.georchestra.ds.orgs.Org; +import org.georchestra.ds.orgs.OrgsDao; +import org.georchestra.ds.roles.RoleDao; +import org.georchestra.ds.roles.RoleFactory; +import org.georchestra.ds.users.Account; +import org.georchestra.ds.users.AccountDao; +import org.georchestra.ds.users.AccountFactory; +import org.georchestra.ds.users.DuplicatedEmailException; +import org.georchestra.ds.users.DuplicatedUidException; +import org.georchestra.gateway.accounts.admin.AbstractAccountsManager; +import org.georchestra.gateway.accounts.admin.AccountCreated; +import org.georchestra.gateway.accounts.admin.AccountManager; +import org.georchestra.security.api.UsersApi; +import org.georchestra.security.model.GeorchestraUser; +import org.springframework.ldap.NameNotFoundException; + +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; + +/** + * {@link AccountManager} that fetches and creates {@link GeorchestraUser}s from + * the Georchestra extended LDAP service provided by an {@link AccountDao} and + * {@link RoleDao}. + */ +@Slf4j(topic = "org.georchestra.gateway.accounts.admin.ldap") +class LdapAccountsManager extends AbstractAccountsManager { + + private final @NonNull AccountDao accountDao; + private final @NonNull RoleDao roleDao; + + private final @NonNull OrgsDao orgsDao; + private final @NonNull UsersApi usersApi; + + public LdapAccountsManager(Consumer eventPublisher, AccountDao accountDao, RoleDao roleDao, + OrgsDao orgsDao, UsersApi usersApi) { + super(eventPublisher); + this.accountDao = accountDao; + this.roleDao = roleDao; + this.orgsDao = orgsDao; + this.usersApi = usersApi; + } + + @Override + protected Optional findByOAuth2ProviderId(@NonNull String oauth2ProviderId) { + return usersApi.findByOAuth2ProviderId(oauth2ProviderId).map(this::ensureRolesPrefixed); + } + + @Override + protected Optional findByUsername(@NonNull String username) { + return usersApi.findByUsername(username).map(this::ensureRolesPrefixed); + } + + private GeorchestraUser ensureRolesPrefixed(GeorchestraUser user) { + List roles = user.getRoles().stream().filter(Objects::nonNull) + .map(r -> r.startsWith("ROLE_") ? r : "ROLE_" + r).collect(Collectors.toList()); + user.setRoles(roles); + return user; + } + + @Override + protected void createInternal(GeorchestraUser mapped) { + Account newAccount = mapToAccountBrief(mapped); + try { + accountDao.insert(newAccount); + } catch (DataServiceException | DuplicatedUidException | DuplicatedEmailException accountError) { + throw new IllegalStateException(accountError); + } + + ensureOrgExists(newAccount); + + ensureRolesExist(mapped, newAccount); + } + + private void ensureRolesExist(GeorchestraUser mapped, Account newAccount) { + try {// account created, add roles + if (!mapped.getRoles().contains("ROLE_USER")) { + roleDao.addUser("USER", newAccount); + } + for (String role : mapped.getRoles()) { + role = role.replaceFirst("^ROLE_", ""); + ensureRoleExists(role); + roleDao.addUser(role, newAccount); + } + } catch (NameNotFoundException | DataServiceException roleError) { + try {// roll-back account + accountDao.delete(newAccount); + } catch (NameNotFoundException | DataServiceException rollbackError) { + log.warn("Error reverting user creation after roleDao update failure", rollbackError); + } + throw new IllegalStateException(roleError); + } + } + + private void ensureRoleExists(String role) throws DataServiceException { + try { + roleDao.findByCommonName(role); + } catch (NameNotFoundException notFound) { + try { + roleDao.insert(RoleFactory.create(role, null, null)); + } catch (DuplicatedCommonNameException e) { + throw new IllegalStateException(e); + } + } + } + + private Account mapToAccountBrief(@NonNull GeorchestraUser preAuth) { + String username = preAuth.getUsername(); + String email = preAuth.getEmail(); + String firstName = preAuth.getFirstName(); + String lastName = preAuth.getLastName(); + String org = preAuth.getOrganization(); + String password = null; + String phone = ""; + String title = ""; + String description = ""; + final @javax.annotation.Nullable String oAuth2ProviderId = preAuth.getOAuth2ProviderId(); + + Account newAccount = AccountFactory.createBrief(username, password, firstName, lastName, email, phone, title, + description, oAuth2ProviderId); + newAccount.setPending(false); + newAccount.setOrg(org); + return newAccount; + } + + private void ensureOrgExists(@NonNull Account newAccount) { + String orgId = newAccount.getOrg(); + if (null == orgId) + return; + try { // account created, add org + Org org; + try { + org = orgsDao.findByCommonName(orgId); + // org already in the LDAP, add the newly + // created account to it + List currentMembers = org.getMembers(); + currentMembers.add(newAccount.getUid()); + org.setMembers(currentMembers); + orgsDao.update(org); + } catch (NameNotFoundException e) { + log.info("Org {} does not exist, trying to create it", orgId); + // org does not exist yet, create it + org = new Org(); + org.setId(orgId); + org.setName(orgId); + org.setMembers(Arrays.asList(newAccount.getUid())); + orgsDao.insert(org); + } + } catch (Exception orgError) { + log.error("Error when trying to create / update the organisation {}, reverting the account creation", orgId, + orgError); + try {// roll-back account + accountDao.delete(newAccount); + } catch (NameNotFoundException | DataServiceException rollbackError) { + log.warn("Error reverting user creation after orgsDao update failure", rollbackError); + } + throw new IllegalStateException(orgError); + } + } +} diff --git a/gateway/src/main/java/org/georchestra/gateway/accounts/events/rabbitmq/RabbitmqAccountCreatedEventSender.java b/gateway/src/main/java/org/georchestra/gateway/accounts/events/rabbitmq/RabbitmqAccountCreatedEventSender.java new file mode 100644 index 00000000..f64ae41f --- /dev/null +++ b/gateway/src/main/java/org/georchestra/gateway/accounts/events/rabbitmq/RabbitmqAccountCreatedEventSender.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2023 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.accounts.events.rabbitmq; + +import java.util.UUID; + +import org.georchestra.gateway.accounts.admin.AccountCreated; +import org.georchestra.security.model.GeorchestraUser; +import org.json.JSONObject; +import org.springframework.amqp.core.AmqpTemplate; +import org.springframework.context.event.EventListener; + +/** + * Service bean that listens to {@link AccountCreated} events and publish a + * distributed event through rabbitmq to the {@literal OAUTH2-ACCOUNT-CREATION} + * queue. + */ +public class RabbitmqAccountCreatedEventSender { + + public static final String OAUTH2_ACCOUNT_CREATION = "OAUTH2-ACCOUNT-CREATION"; + + private AmqpTemplate eventTemplate; + + public RabbitmqAccountCreatedEventSender(AmqpTemplate eventTemplate) { + this.eventTemplate = eventTemplate; + } + + @EventListener(AccountCreated.class) + public void on(AccountCreated event) { + GeorchestraUser user = event.getUser(); + final String oAuth2ProviderId = user.getOAuth2ProviderId(); + if (null != oAuth2ProviderId) { + String fullName = user.getFirstName() + " " + user.getLastName(); + String email = user.getEmail(); + String provider = oAuth2ProviderId; + sendNewOAuthAccountMessage(fullName, email, provider); + } + } + + public void sendNewOAuthAccountMessage(String fullName, String email, String provider) { + // beans getting a reference to the sender + JSONObject jsonObj = new JSONObject(); + jsonObj.put("uid", UUID.randomUUID()); + jsonObj.put("subject", OAUTH2_ACCOUNT_CREATION); + jsonObj.put("username", fullName); // bean + jsonObj.put("email", email); // bean + jsonObj.put("provider", provider); // bean + eventTemplate.convertAndSend("routing-gateway", jsonObj.toString());// send + } +} \ No newline at end of file diff --git a/gateway/src/main/java/org/georchestra/gateway/accounts/events/rabbitmq/RabbitmqEventsConfiguration.java b/gateway/src/main/java/org/georchestra/gateway/accounts/events/rabbitmq/RabbitmqEventsConfiguration.java new file mode 100644 index 00000000..1a035b0d --- /dev/null +++ b/gateway/src/main/java/org/georchestra/gateway/accounts/events/rabbitmq/RabbitmqEventsConfiguration.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2023 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.accounts.events.rabbitmq; + +import org.georchestra.gateway.accounts.admin.AccountCreated; +import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.actuate.amqp.RabbitHealthIndicator; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ImportResource; + +/** + * {@link Configuration @Configuration} to enable sending events over rabbitmq * + *

+ * When an account is created in geOrchestra's LDAP in response to a + * pre-authenticated or OIDC successful authentication, an + * {@link AccountCreated} event will be catch up and sent over the wire. + * + * @see RabbitmqEventsConfigurationProperties + * + */ +@Configuration +@EnableConfigurationProperties(RabbitmqEventsConfigurationProperties.class) +@ImportResource({ "classpath:rabbit-listener-context.xml", "classpath:rabbit-sender-context.xml" }) +public class RabbitmqEventsConfiguration { + + @Bean + RabbitmqAccountCreatedEventSender eventsSender(@Qualifier("eventTemplate") RabbitTemplate eventTemplate) { + return new RabbitmqAccountCreatedEventSender(eventTemplate); + } + + @Bean + org.springframework.amqp.rabbit.connection.CachingConnectionFactory connectionFactory( + RabbitmqEventsConfigurationProperties config) { + + com.rabbitmq.client.ConnectionFactory fac = new com.rabbitmq.client.ConnectionFactory(); + fac.setHost(config.getHost()); + fac.setPort(config.getPort()); + fac.setUsername(config.getUser()); + fac.setPassword(config.getPassword()); + + return new CachingConnectionFactory(fac); + } + + @Bean + RabbitHealthIndicator rabbitHealthIndicator(@Qualifier("eventTemplate") RabbitTemplate eventTemplate) { + return new RabbitHealthIndicator(eventTemplate); + } + +} diff --git a/gateway/src/main/java/org/georchestra/gateway/accounts/events/rabbitmq/RabbitmqEventsConfigurationProperties.java b/gateway/src/main/java/org/georchestra/gateway/accounts/events/rabbitmq/RabbitmqEventsConfigurationProperties.java new file mode 100644 index 00000000..002549b8 --- /dev/null +++ b/gateway/src/main/java/org/georchestra/gateway/accounts/events/rabbitmq/RabbitmqEventsConfigurationProperties.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2023 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.accounts.events.rabbitmq; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +import lombok.Data; +import lombok.Generated; + +/** + * Configuration properties to enable rabbit-mq event dispatching of accounts + * created + */ +@Data +@Generated +@Validated +@ConfigurationProperties(prefix = RabbitmqEventsConfigurationProperties.PREFIX) +public class RabbitmqEventsConfigurationProperties { + + public static final String PREFIX = "georchestra.gateway.security.events.rabbitmq"; + public static final String ENABLED = PREFIX + ".enabled"; + + /** + * Whether rabbit-mq events should be sent when an LDAP account was created upon + * a first successful login through OAuth2 + */ + private boolean enabled; + /** + * The rabbit-mq host name + */ + private String host; + /** + * The rabbit-mq host port number + */ + private int port; + /** + * The rabbit-mq authentication user + */ + private String user; + /** + * The rabbit-mq authentication password + */ + private String password; +} diff --git a/gateway/src/main/java/org/georchestra/gateway/events/RabbitmqEventsListener.java b/gateway/src/main/java/org/georchestra/gateway/accounts/events/rabbitmq/RabbitmqEventsListener.java similarity index 57% rename from gateway/src/main/java/org/georchestra/gateway/events/RabbitmqEventsListener.java rename to gateway/src/main/java/org/georchestra/gateway/accounts/events/rabbitmq/RabbitmqEventsListener.java index bb2f5b39..c34807d0 100644 --- a/gateway/src/main/java/org/georchestra/gateway/events/RabbitmqEventsListener.java +++ b/gateway/src/main/java/org/georchestra/gateway/accounts/events/rabbitmq/RabbitmqEventsListener.java @@ -1,14 +1,34 @@ -package org.georchestra.gateway.events; +/* + * Copyright (C) 2023 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.accounts.events.rabbitmq; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; -import lombok.extern.slf4j.Slf4j; import org.json.JSONObject; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageListener; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; +import lombok.extern.slf4j.Slf4j; +//TODO: remove class as dead code? @Slf4j(topic = "org.georchestra.gateway.events") public class RabbitmqEventsListener implements MessageListener { 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 4b08b342..4253b7e7 100644 --- a/gateway/src/main/java/org/georchestra/gateway/app/GeorchestraGatewayApplication.java +++ b/gateway/src/main/java/org/georchestra/gateway/app/GeorchestraGatewayApplication.java @@ -18,7 +18,15 @@ */ package org.georchestra.gateway.app; -import lombok.extern.slf4j.Slf4j; +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; + +import javax.annotation.PostConstruct; + import org.georchestra.gateway.security.GeorchestraUserMapper; import org.georchestra.gateway.security.ldap.LdapConfigProperties; import org.georchestra.security.model.GeorchestraUser; @@ -43,12 +51,9 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.server.ServerWebExchange; -import reactor.core.publisher.Mono; -import javax.annotation.PostConstruct; -import java.io.File; -import java.nio.charset.StandardCharsets; -import java.util.*; +import lombok.extern.slf4j.Slf4j; +import reactor.core.publisher.Mono; @Controller @Slf4j diff --git a/gateway/src/main/java/org/georchestra/gateway/autoconfigure/accounts/ConditionalOnCreateLdapAccounts.java b/gateway/src/main/java/org/georchestra/gateway/autoconfigure/accounts/ConditionalOnCreateLdapAccounts.java new file mode 100644 index 00000000..50d87c39 --- /dev/null +++ b/gateway/src/main/java/org/georchestra/gateway/autoconfigure/accounts/ConditionalOnCreateLdapAccounts.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2023 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.autoconfigure.accounts; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; + +/** + * + * @see ConditionalOnDefaultGeorchestraLdapEnabled + */ +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@ConditionalOnDefaultGeorchestraLdapEnabled +@ConditionalOnProperty(name = "georchestra.gateway.security.createNonExistingUsersInLDAP", havingValue = "true", matchIfMissing = false) +public @interface ConditionalOnCreateLdapAccounts { + +} diff --git a/gateway/src/main/java/org/georchestra/gateway/autoconfigure/accounts/ConditionalOnDefaultGeorchestraLdapEnabled.java b/gateway/src/main/java/org/georchestra/gateway/autoconfigure/accounts/ConditionalOnDefaultGeorchestraLdapEnabled.java new file mode 100644 index 00000000..dc0a7759 --- /dev/null +++ b/gateway/src/main/java/org/georchestra/gateway/autoconfigure/accounts/ConditionalOnDefaultGeorchestraLdapEnabled.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2023 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.autoconfigure.accounts; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.georchestra.gateway.autoconfigure.security.ConditionalOnLdapEnabled; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; + +/** + * + */ +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@ConditionalOnLdapEnabled +@ConditionalOnProperty(name = "georchestra.gateway.security.ldap.default.enabled", havingValue = "true", matchIfMissing = false) +public @interface ConditionalOnDefaultGeorchestraLdapEnabled { + +} diff --git a/gateway/src/main/java/org/georchestra/gateway/autoconfigure/accounts/GeorchestraLdapAccountsCreationAutoConfiguration.java b/gateway/src/main/java/org/georchestra/gateway/autoconfigure/accounts/GeorchestraLdapAccountsCreationAutoConfiguration.java new file mode 100644 index 00000000..39210892 --- /dev/null +++ b/gateway/src/main/java/org/georchestra/gateway/autoconfigure/accounts/GeorchestraLdapAccountsCreationAutoConfiguration.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2023 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.autoconfigure.accounts; + +import org.georchestra.gateway.accounts.admin.ldap.GeorchestraLdapAccountManagementConfiguration; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.context.annotation.Import; + +/** + * {@link AutoConfiguration @AutoConfiguration} + * + * @see ConditionalOnCreateLdapAccounts + * @see GeorchestraLdapAccountManagementConfiguration + */ +@AutoConfiguration +@ConditionalOnCreateLdapAccounts +@Import(GeorchestraLdapAccountManagementConfiguration.class) +public class GeorchestraLdapAccountsCreationAutoConfiguration { +} diff --git a/gateway/src/main/java/org/georchestra/gateway/autoconfigure/accounts/RabbitmqEventsAutoConfiguration.java b/gateway/src/main/java/org/georchestra/gateway/autoconfigure/accounts/RabbitmqEventsAutoConfiguration.java new file mode 100644 index 00000000..332e305d --- /dev/null +++ b/gateway/src/main/java/org/georchestra/gateway/autoconfigure/accounts/RabbitmqEventsAutoConfiguration.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2023 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.autoconfigure.accounts; + +import org.georchestra.gateway.accounts.admin.AccountCreated; +import org.georchestra.gateway.accounts.events.rabbitmq.RabbitmqEventsConfiguration; +import org.georchestra.gateway.accounts.events.rabbitmq.RabbitmqEventsConfigurationProperties; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Import; + +/** + * {@link AutoConfiguration @AutoConfiguration} to enable sending events over + * rabbitmq when it is enabled through + * {@literal georchestra.gateway.security.events.rabbitmq = true}. + *

+ * When an account is created in geOrchestra's LDAP in response to a + * pre-authenticated or OIDC successful authentication, an + * {@link AccountCreated} event will be catch up and sent over the wire. + * + * + * @see ConditionalOnCreateLdapAccounts + * @see RabbitmqEventsConfiguration + */ +@AutoConfiguration +@ConditionalOnCreateLdapAccounts +@ConditionalOnProperty(name = RabbitmqEventsConfigurationProperties.ENABLED, havingValue = "true", matchIfMissing = false) +@Import(RabbitmqEventsConfiguration.class) +public class RabbitmqEventsAutoConfiguration { + +} diff --git a/gateway/src/main/java/org/georchestra/gateway/autoconfigure/app/FiltersAutoConfiguration.java b/gateway/src/main/java/org/georchestra/gateway/autoconfigure/app/FiltersAutoConfiguration.java index 68c76389..4c2d19b4 100644 --- a/gateway/src/main/java/org/georchestra/gateway/autoconfigure/app/FiltersAutoConfiguration.java +++ b/gateway/src/main/java/org/georchestra/gateway/autoconfigure/app/FiltersAutoConfiguration.java @@ -25,18 +25,15 @@ import org.geoserver.cloud.gateway.filter.RouteProfileGatewayFilterFactory; import org.geoserver.cloud.gateway.filter.StripBasePathGatewayFilterFactory; import org.geoserver.cloud.gateway.predicate.RegExpQueryRoutePredicateFactory; -import org.springframework.boot.actuate.health.Health; -import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigureBefore; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.gateway.config.GatewayAutoConfiguration; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; -@Configuration(proxyBeanMethods = false) +@AutoConfiguration @AutoConfigureBefore(GatewayAutoConfiguration.class) @Import(HeaderFiltersConfiguration.class) @EnableConfigurationProperties(GatewayConfigProperties.class) @@ -67,9 +64,4 @@ public class FiltersAutoConfiguration { public @Bean StripBasePathGatewayFilterFactory stripBasePathGatewayFilterFactory() { return new StripBasePathGatewayFilterFactory(); } - - @ConditionalOnProperty(name = "enableRabbitmqEvents", havingValue = "false", matchIfMissing = true) - public @Bean HealthIndicator rabbitHealthIndicator() { - return () -> Health.up().withDetail("version", "mock").build(); - } } diff --git a/gateway/src/main/java/org/georchestra/gateway/autoconfigure/app/RoutePredicateFactoriesAutoConfiguration.java b/gateway/src/main/java/org/georchestra/gateway/autoconfigure/app/RoutePredicateFactoriesAutoConfiguration.java index 18a70bf2..d97bf371 100644 --- a/gateway/src/main/java/org/georchestra/gateway/autoconfigure/app/RoutePredicateFactoriesAutoConfiguration.java +++ b/gateway/src/main/java/org/georchestra/gateway/autoconfigure/app/RoutePredicateFactoriesAutoConfiguration.java @@ -19,10 +19,10 @@ package org.georchestra.gateway.autoconfigure.app; import org.georchestra.gateway.handler.predicate.QueryParamRoutePredicateFactory; +import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -@Configuration(proxyBeanMethods = false) +@AutoConfiguration public class RoutePredicateFactoriesAutoConfiguration { public @Bean QueryParamRoutePredicateFactory queryParamRoutePredicateFactory() { diff --git a/gateway/src/main/java/org/georchestra/gateway/autoconfigure/security/AtLeastOneLdapDatasourceEnabledCondition.java b/gateway/src/main/java/org/georchestra/gateway/autoconfigure/security/AtLeastOneLdapDatasourceEnabledCondition.java index d0034e32..2739b41e 100644 --- a/gateway/src/main/java/org/georchestra/gateway/autoconfigure/security/AtLeastOneLdapDatasourceEnabledCondition.java +++ b/gateway/src/main/java/org/georchestra/gateway/autoconfigure/security/AtLeastOneLdapDatasourceEnabledCondition.java @@ -28,10 +28,12 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import org.georchestra.gateway.security.ldap.LdapConfigProperties; import org.springframework.boot.autoconfigure.condition.ConditionMessage; import org.springframework.boot.autoconfigure.condition.ConditionMessage.ItemsBuilder; import org.springframework.boot.autoconfigure.condition.ConditionOutcome; import org.springframework.boot.autoconfigure.condition.SpringBootCondition; +import org.springframework.context.annotation.Condition; import org.springframework.context.annotation.ConditionContext; import org.springframework.core.env.AbstractEnvironment; import org.springframework.core.env.EnumerablePropertySource; @@ -42,7 +44,11 @@ import com.google.common.collect.Streams; /** - * + * {@link Condition} that matches if at least one LDAP config is enabled from + * the externalized config properties + * {@code georchestra.gateway.security.ldap..enabled} + * + * @see LdapConfigProperties */ class AtLeastOneLdapDatasourceEnabledCondition extends SpringBootCondition { 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 e1e0bade..783c0975 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,11 +20,9 @@ import javax.annotation.PostConstruct; -import org.georchestra.gateway.security.ldap.LdapSecurityConfiguration; -import org.georchestra.gateway.security.ldap.basic.BasicLdapAuthenticationConfiguration; -import org.georchestra.gateway.security.ldap.extended.ExtendedLdapAuthenticationConfiguration; +import org.georchestra.gateway.security.ldap.LdapAuthenticationConfiguration; +import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import lombok.extern.slf4j.Slf4j; @@ -32,14 +30,11 @@ /** * {@link EnableAutoConfiguration AutoConfiguration} to set up LDAP security * - * @see LdapSecurityConfiguration - * @see BasicLdapAuthenticationConfiguration - * @see ExtendedLdapAuthenticationConfiguration - * @see ActiveDirectoryAuthenticationConfiguration + * @see LdapAuthenticationConfiguration */ -@Configuration(proxyBeanMethods = false) +@AutoConfiguration @ConditionalOnLdapEnabled -@Import(LdapSecurityConfiguration.class) +@Import(LdapAuthenticationConfiguration.class) @Slf4j(topic = "org.georchestra.gateway.autoconfigure.security") public class LdapSecurityAutoConfiguration { diff --git a/gateway/src/main/java/org/georchestra/gateway/autoconfigure/security/OAuth2SecurityAutoConfiguration.java b/gateway/src/main/java/org/georchestra/gateway/autoconfigure/security/OAuth2SecurityAutoConfiguration.java index 8c625439..671b5e6b 100644 --- a/gateway/src/main/java/org/georchestra/gateway/autoconfigure/security/OAuth2SecurityAutoConfiguration.java +++ b/gateway/src/main/java/org/georchestra/gateway/autoconfigure/security/OAuth2SecurityAutoConfiguration.java @@ -21,13 +21,14 @@ import javax.annotation.PostConstruct; import org.georchestra.gateway.security.oauth2.OAuth2Configuration; +import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import lombok.extern.slf4j.Slf4j; -@Configuration(proxyBeanMethods = false) +@AutoConfiguration @Slf4j(topic = "org.georchestra.gateway.autoconfigure.security") @Import({ OAuth2SecurityAutoConfiguration.Enabled.class, OAuth2SecurityAutoConfiguration.Disabled.class }) public class OAuth2SecurityAutoConfiguration { diff --git a/gateway/src/main/java/org/georchestra/gateway/autoconfigure/security/WebSecurityAutoConfiguration.java b/gateway/src/main/java/org/georchestra/gateway/autoconfigure/security/WebSecurityAutoConfiguration.java index 8dfa0330..5107c626 100644 --- a/gateway/src/main/java/org/georchestra/gateway/autoconfigure/security/WebSecurityAutoConfiguration.java +++ b/gateway/src/main/java/org/georchestra/gateway/autoconfigure/security/WebSecurityAutoConfiguration.java @@ -20,11 +20,11 @@ import org.georchestra.gateway.security.GatewaySecurityConfiguration; import org.georchestra.gateway.security.accessrules.AccessRulesConfiguration; +import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.security.ConditionalOnDefaultWebSecurity; -import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; -@Configuration(proxyBeanMethods = false) +@AutoConfiguration @ConditionalOnDefaultWebSecurity @Import({ GatewaySecurityConfiguration.class, AccessRulesConfiguration.class }) public class WebSecurityAutoConfiguration { diff --git a/gateway/src/main/java/org/georchestra/gateway/events/RabbitmqEventsAutoConfiguration.java b/gateway/src/main/java/org/georchestra/gateway/events/RabbitmqEventsAutoConfiguration.java index 536b879a..e69de29b 100644 --- a/gateway/src/main/java/org/georchestra/gateway/events/RabbitmqEventsAutoConfiguration.java +++ b/gateway/src/main/java/org/georchestra/gateway/events/RabbitmqEventsAutoConfiguration.java @@ -1,38 +0,0 @@ -package org.georchestra.gateway.events; - -import org.springframework.amqp.core.AmqpTemplate; -import org.springframework.amqp.rabbit.connection.ConnectionFactory; -import org.springframework.amqp.rabbit.listener.MessageListenerContainer; -import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer; -import org.springframework.boot.autoconfigure.AutoConfigureAfter; -import org.springframework.amqp.core.Queue; -import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.cloud.gateway.config.GatewayAutoConfiguration; -import org.springframework.context.annotation.*; - -@Profile("!test && !it") -@Configuration(proxyBeanMethods = false) -@AutoConfigureAfter(GatewayAutoConfiguration.class) -@ImportResource({ "classpath:rabbit-listener-context.xml", "classpath:rabbit-sender-context.xml" }) -@ConditionalOnProperty(name = "enableRabbitmqEvents", havingValue = "true", matchIfMissing = false) -public class RabbitmqEventsAutoConfiguration { - - @Bean - @DependsOn({ "eventTemplate" }) - public RabbitmqEventsSender eventsSender(AmqpTemplate eventTemplate) { - return new RabbitmqEventsSender(eventTemplate); - } - - Queue OAuth2ReplyQueue() { - return new Queue("OAuth2ReplyQueue", false); - } - - MessageListenerContainer messageListenerContainer(ConnectionFactory connectionFactory) { - SimpleMessageListenerContainer simpleMessageListenerContainer = new SimpleMessageListenerContainer(); - simpleMessageListenerContainer.setConnectionFactory(connectionFactory); - simpleMessageListenerContainer.setQueues(OAuth2ReplyQueue()); - simpleMessageListenerContainer.setMessageListener(new RabbitmqEventsListener()); - return simpleMessageListenerContainer; - } -} \ No newline at end of file diff --git a/gateway/src/main/java/org/georchestra/gateway/events/RabbitmqEventsSender.java b/gateway/src/main/java/org/georchestra/gateway/events/RabbitmqEventsSender.java deleted file mode 100644 index 7e46a787..00000000 --- a/gateway/src/main/java/org/georchestra/gateway/events/RabbitmqEventsSender.java +++ /dev/null @@ -1,35 +0,0 @@ -package org.georchestra.gateway.events; - -import org.json.JSONObject; -import org.springframework.amqp.core.AmqpTemplate; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.ApplicationContext; - -import java.util.UUID; - -public class RabbitmqEventsSender { - - public static final String OAUTH2_ACCOUNT_CREATION = "OAUTH2-ACCOUNT-CREATION"; - - @Autowired - private ApplicationContext applicationContext; - - private AmqpTemplate eventTemplate; - - public RabbitmqEventsSender(AmqpTemplate eventTemplate) { - this.eventTemplate = eventTemplate; - } - - public void sendNewOAuthAccountMessage(String username, String email, String provider) throws Exception { - // beans - // getting a reference to - // the sender - JSONObject jsonObj = new JSONObject(); - jsonObj.put("uid", UUID.randomUUID()); - jsonObj.put("subject", OAUTH2_ACCOUNT_CREATION); - jsonObj.put("username", username); // bean - jsonObj.put("email", email); // bean - jsonObj.put("provider", provider); // bean - eventTemplate.convertAndSend("routing-gateway", jsonObj.toString());// send - } -} \ No newline at end of file diff --git a/gateway/src/main/java/org/georchestra/gateway/filter/headers/CookieAffinityGatewayFilterFactory.java b/gateway/src/main/java/org/georchestra/gateway/filter/headers/CookieAffinityGatewayFilterFactory.java index e4bc4dcc..1073ea8d 100644 --- a/gateway/src/main/java/org/georchestra/gateway/filter/headers/CookieAffinityGatewayFilterFactory.java +++ b/gateway/src/main/java/org/georchestra/gateway/filter/headers/CookieAffinityGatewayFilterFactory.java @@ -1,8 +1,7 @@ package org.georchestra.gateway.filter.headers; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import lombok.Setter; +import javax.validation.constraints.NotEmpty; + import org.georchestra.gateway.filter.global.ResolveTargetGlobalFilter; import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.GatewayFilterChain; @@ -11,9 +10,11 @@ import org.springframework.http.ResponseCookie; import org.springframework.validation.annotation.Validated; import org.springframework.web.server.ServerWebExchange; -import reactor.core.publisher.Mono; -import javax.validation.constraints.NotEmpty; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import reactor.core.publisher.Mono; public class CookieAffinityGatewayFilterFactory extends AbstractGatewayFilterFactory { diff --git a/gateway/src/main/java/org/georchestra/gateway/filter/headers/HeaderFiltersConfiguration.java b/gateway/src/main/java/org/georchestra/gateway/filter/headers/HeaderFiltersConfiguration.java index a5e8070a..1e62361c 100644 --- a/gateway/src/main/java/org/georchestra/gateway/filter/headers/HeaderFiltersConfiguration.java +++ b/gateway/src/main/java/org/georchestra/gateway/filter/headers/HeaderFiltersConfiguration.java @@ -26,12 +26,14 @@ import org.georchestra.gateway.filter.headers.providers.JsonPayloadHeadersContributor; import org.georchestra.gateway.filter.headers.providers.SecProxyHeaderContributor; import org.georchestra.gateway.model.GatewayConfigProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.gateway.filter.factory.GatewayFilterFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties(GatewayConfigProperties.class) public class HeaderFiltersConfiguration { /** @@ -44,33 +46,40 @@ public class HeaderFiltersConfiguration { * @see #userSecurityHeadersProvider() * @see #organizationSecurityHeadersProvider() */ - public @Bean AddSecHeadersGatewayFilterFactory addSecHeadersGatewayFilterFactory( - List providers) { + + @Bean + AddSecHeadersGatewayFilterFactory addSecHeadersGatewayFilterFactory(List providers) { return new AddSecHeadersGatewayFilterFactory(providers); } - public @Bean CookieAffinityGatewayFilterFactory cookieAffinityGatewayFilterFactory() { + @Bean + CookieAffinityGatewayFilterFactory cookieAffinityGatewayFilterFactory() { return new CookieAffinityGatewayFilterFactory(); } - public @Bean ProxyGatewayFilterFactory proxyGatewayFilterFactory() { + @Bean + ProxyGatewayFilterFactory proxyGatewayFilterFactory() { return new ProxyGatewayFilterFactory(); } - public @Bean GeorchestraUserHeadersContributor userSecurityHeadersProvider() { + @Bean + GeorchestraUserHeadersContributor userSecurityHeadersProvider() { return new GeorchestraUserHeadersContributor(); } - public @Bean SecProxyHeaderContributor secProxyHeaderProvider(GatewayConfigProperties configProps) { + @Bean + SecProxyHeaderContributor secProxyHeaderProvider(GatewayConfigProperties configProps) { BooleanSupplier secProxyEnabledSupplier = () -> configProps.getDefaultHeaders().getProxy().orElse(false); return new SecProxyHeaderContributor(secProxyEnabledSupplier); } - public @Bean GeorchestraOrganizationHeadersContributor organizationSecurityHeadersProvider() { + @Bean + GeorchestraOrganizationHeadersContributor organizationSecurityHeadersProvider() { return new GeorchestraOrganizationHeadersContributor(); } - public @Bean JsonPayloadHeadersContributor jsonPayloadHeadersContributor() { + @Bean + JsonPayloadHeadersContributor jsonPayloadHeadersContributor() { return new JsonPayloadHeadersContributor(); } @@ -78,7 +87,8 @@ public class HeaderFiltersConfiguration { * General purpose {@link GatewayFilterFactory} to remove incoming HTTP request * headers based on a Java regular expression */ - public @Bean RemoveHeadersGatewayFilterFactory removeHeadersGatewayFilterFactory() { + @Bean + RemoveHeadersGatewayFilterFactory removeHeadersGatewayFilterFactory() { return new RemoveHeadersGatewayFilterFactory(); } @@ -86,7 +96,8 @@ public class HeaderFiltersConfiguration { * {@link GatewayFilterFactory} to remove incoming HTTP {@literal sec-*} HTTP * request headers to prevent impersonation from outside */ - public @Bean RemoveSecurityHeadersGatewayFilterFactory removeSecurityHeadersGatewayFilterFactory() { + @Bean + RemoveSecurityHeadersGatewayFilterFactory removeSecurityHeadersGatewayFilterFactory() { return new RemoveSecurityHeadersGatewayFilterFactory(); } } diff --git a/gateway/src/main/java/org/georchestra/gateway/filter/headers/ProxyGatewayFilterFactory.java b/gateway/src/main/java/org/georchestra/gateway/filter/headers/ProxyGatewayFilterFactory.java index 8b44dc00..43f1f06e 100644 --- a/gateway/src/main/java/org/georchestra/gateway/filter/headers/ProxyGatewayFilterFactory.java +++ b/gateway/src/main/java/org/georchestra/gateway/filter/headers/ProxyGatewayFilterFactory.java @@ -1,14 +1,14 @@ package org.georchestra.gateway.filter.headers; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; + import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; -import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.cloud.gateway.route.Route; import org.springframework.cloud.gateway.support.ServerWebExchangeUtils; - -import java.net.URI; -import java.net.URISyntaxException; -import java.util.List; +import org.springframework.http.server.reactive.ServerHttpRequest; public class ProxyGatewayFilterFactory extends AbstractGatewayFilterFactory { public ProxyGatewayFilterFactory() { diff --git a/gateway/src/main/java/org/georchestra/gateway/filter/headers/RemoveHeadersGatewayFilterFactory.java b/gateway/src/main/java/org/georchestra/gateway/filter/headers/RemoveHeadersGatewayFilterFactory.java index 032db0f1..2fbd6748 100644 --- a/gateway/src/main/java/org/georchestra/gateway/filter/headers/RemoveHeadersGatewayFilterFactory.java +++ b/gateway/src/main/java/org/georchestra/gateway/filter/headers/RemoveHeadersGatewayFilterFactory.java @@ -74,7 +74,6 @@ public List shortcutFieldOrder() { @Override public GatewayFilter apply(RegExConfig regexConfig) { - return (exchange, chain) -> { final RegExConfig config = regexConfig;// == null ? DEFAULT_SECURITY_HEADERS_CONFIG : regexConfig; HttpHeaders incoming = exchange.getRequest().getHeaders(); diff --git a/gateway/src/main/java/org/georchestra/gateway/filter/headers/RemoveSecurityHeadersGatewayFilterFactory.java b/gateway/src/main/java/org/georchestra/gateway/filter/headers/RemoveSecurityHeadersGatewayFilterFactory.java index e94eca27..584ed9b8 100644 --- a/gateway/src/main/java/org/georchestra/gateway/filter/headers/RemoveSecurityHeadersGatewayFilterFactory.java +++ b/gateway/src/main/java/org/georchestra/gateway/filter/headers/RemoveSecurityHeadersGatewayFilterFactory.java @@ -49,12 +49,13 @@ public class RemoveSecurityHeadersGatewayFilterFactory extends AbstractGatewayFi private static final String DEFAULT_SEC_HEADERS_PATTERN = "(?i)(sec-.*|Authorization)"; - private final RemoveHeadersGatewayFilterFactory delegate = new RemoveHeadersGatewayFilterFactory(); + private final RemoveHeadersGatewayFilterFactory delegate; private final RemoveHeadersGatewayFilterFactory.RegExConfig config = new RemoveHeadersGatewayFilterFactory.RegExConfig( DEFAULT_SEC_HEADERS_PATTERN); public RemoveSecurityHeadersGatewayFilterFactory() { super(Object.class); + delegate = new RemoveHeadersGatewayFilterFactory(); } @Override diff --git a/gateway/src/main/java/org/georchestra/gateway/filter/headers/providers/GeorchestraUserHeadersContributor.java b/gateway/src/main/java/org/georchestra/gateway/filter/headers/providers/GeorchestraUserHeadersContributor.java index c0bcd8d7..7bcd6b6e 100644 --- a/gateway/src/main/java/org/georchestra/gateway/filter/headers/providers/GeorchestraUserHeadersContributor.java +++ b/gateway/src/main/java/org/georchestra/gateway/filter/headers/providers/GeorchestraUserHeadersContributor.java @@ -21,7 +21,6 @@ import java.util.List; import java.util.Optional; import java.util.function.Consumer; -import java.util.stream.Collectors; import org.georchestra.gateway.filter.headers.HeaderContributor; import org.georchestra.gateway.model.GeorchestraTargetConfig; diff --git a/gateway/src/main/java/org/georchestra/gateway/security/ExtendedRedirectServerAuthenticationFailureHandler.java b/gateway/src/main/java/org/georchestra/gateway/security/ExtendedRedirectServerAuthenticationFailureHandler.java index 9a342acd..814f8dda 100644 --- a/gateway/src/main/java/org/georchestra/gateway/security/ExtendedRedirectServerAuthenticationFailureHandler.java +++ b/gateway/src/main/java/org/georchestra/gateway/security/ExtendedRedirectServerAuthenticationFailureHandler.java @@ -1,14 +1,15 @@ package org.georchestra.gateway.security; +import java.net.URI; + import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.server.DefaultServerRedirectStrategy; import org.springframework.security.web.server.ServerRedirectStrategy; import org.springframework.security.web.server.WebFilterExchange; import org.springframework.security.web.server.authentication.RedirectServerAuthenticationFailureHandler; import org.springframework.util.Assert; -import reactor.core.publisher.Mono; -import java.net.URI; +import reactor.core.publisher.Mono; public class ExtendedRedirectServerAuthenticationFailureHandler extends RedirectServerAuthenticationFailureHandler { diff --git a/gateway/src/main/java/org/georchestra/gateway/security/GatewaySecurityConfiguration.java b/gateway/src/main/java/org/georchestra/gateway/security/GatewaySecurityConfiguration.java index 95bcfb37..31b3df91 100644 --- a/gateway/src/main/java/org/georchestra/gateway/security/GatewaySecurityConfiguration.java +++ b/gateway/src/main/java/org/georchestra/gateway/security/GatewaySecurityConfiguration.java @@ -18,7 +18,10 @@ */ package org.georchestra.gateway.security; -import lombok.extern.slf4j.Slf4j; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + import org.georchestra.gateway.model.GatewayConfigProperties; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -26,12 +29,11 @@ import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.config.web.server.ServerHttpSecurity.LogoutSpec; import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler; -import java.util.List; -import java.util.Map; -import java.util.stream.Stream; +import lombok.extern.slf4j.Slf4j; /** * {@link Configuration} to initialize the Gateway's @@ -52,17 +54,22 @@ @Slf4j(topic = "org.georchestra.gateway.security") public class GatewaySecurityConfiguration { + @Autowired(required = false) + ServerLogoutSuccessHandler oidcLogoutSuccessHandler; + +// @Primary +// @Bean +// ReactiveAuthenticationManager authManagerDelegator(List managers) { +// return new DelegatingReactiveAuthenticationManager(managers); +// } + /** * Relies on available {@link ServerHttpSecurityCustomizer} extensions to * configure the different aspects of the {@link ServerHttpSecurity} used to * {@link ServerHttpSecurity#build build} the {@link SecurityWebFilterChain}. */ - - @Autowired(required = false) - ServerLogoutSuccessHandler oidcLogoutSuccessHandler; - @Bean - public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http, + SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http, List customizers) throws Exception { log.info("Initializing security filter chain..."); @@ -78,24 +85,26 @@ public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http, log.info("Security filter chain initialized"); + LogoutSpec logoutUrl = http.formLogin().loginPage("/login").and().logout().logoutUrl("/logout"); if (oidcLogoutSuccessHandler != null) { - return http.formLogin().loginPage("/login").and().logout().logoutUrl("/logout") - .logoutSuccessHandler(oidcLogoutSuccessHandler).and().build(); - } else { - return http.formLogin().loginPage("/login").and().logout().logoutUrl("/logout").and().build(); + logoutUrl = logoutUrl.logoutSuccessHandler(oidcLogoutSuccessHandler); } + + return logoutUrl.and().build(); } private Stream sortedCustomizers(List customizers) { return customizers.stream().sorted((c1, c2) -> Integer.compare(c1.getOrder(), c2.getOrder())); } - public @Bean GeorchestraUserMapper georchestraUserResolver(List resolvers, + @Bean + GeorchestraUserMapper georchestraUserResolver(List resolvers, List customizers) { return new GeorchestraUserMapper(resolvers, customizers); } - public @Bean ResolveGeorchestraUserGlobalFilter resolveGeorchestraUserGlobalFilter(GeorchestraUserMapper resolver) { + @Bean + ResolveGeorchestraUserGlobalFilter resolveGeorchestraUserGlobalFilter(GeorchestraUserMapper resolver) { return new ResolveGeorchestraUserGlobalFilter(resolver); } @@ -103,7 +112,8 @@ private Stream sortedCustomizers(List> rolesMappings = config.getRolesMappings(); log.info("Creating {}", RolesMappingsUserCustomizer.class.getSimpleName()); return new RolesMappingsUserCustomizer(rolesMappings); diff --git a/gateway/src/main/java/org/georchestra/gateway/security/GeorchestraUserCustomizerExtension.java b/gateway/src/main/java/org/georchestra/gateway/security/GeorchestraUserCustomizerExtension.java index c5898135..8bb49fd2 100644 --- a/gateway/src/main/java/org/georchestra/gateway/security/GeorchestraUserCustomizerExtension.java +++ b/gateway/src/main/java/org/georchestra/gateway/security/GeorchestraUserCustomizerExtension.java @@ -19,10 +19,11 @@ package org.georchestra.gateway.security; -import java.util.function.Function; +import java.util.function.BiFunction; import org.georchestra.security.model.GeorchestraUser; import org.springframework.core.Ordered; +import org.springframework.security.core.Authentication; /** * Extension point to customize the state of a {@link GeorchestraUser} once it @@ -31,7 +32,8 @@ * * @see GeorchestraUserMapper */ -public interface GeorchestraUserCustomizerExtension extends Ordered, Function { +public interface GeorchestraUserCustomizerExtension + extends Ordered, BiFunction { default int getOrder() { return 0; diff --git a/gateway/src/main/java/org/georchestra/gateway/security/GeorchestraUserMapper.java b/gateway/src/main/java/org/georchestra/gateway/security/GeorchestraUserMapper.java index e53889c3..352f9ab8 100644 --- a/gateway/src/main/java/org/georchestra/gateway/security/GeorchestraUserMapper.java +++ b/gateway/src/main/java/org/georchestra/gateway/security/GeorchestraUserMapper.java @@ -82,13 +82,13 @@ public Optional resolve(@NonNull Authentication authToken) { .map(resolver -> resolver.resolve(authToken))// .filter(Optional::isPresent)// .map(Optional::orElseThrow)// - .map(this::customize).findFirst(); + .map(mapped -> customize(authToken, mapped)).findFirst(); } - private GeorchestraUser customize(GeorchestraUser user) { - GeorchestraUser customized = user; + private GeorchestraUser customize(@NonNull Authentication authToken, GeorchestraUser mapped) { + GeorchestraUser customized = mapped; for (GeorchestraUserCustomizerExtension customizer : customizers) { - customized = customizer.apply(customized); + customized = customizer.apply(authToken, customized); } return customized; } diff --git a/gateway/src/main/java/org/georchestra/gateway/security/RolesMappingsUserCustomizer.java b/gateway/src/main/java/org/georchestra/gateway/security/RolesMappingsUserCustomizer.java index 8272a482..b84792d2 100644 --- a/gateway/src/main/java/org/georchestra/gateway/security/RolesMappingsUserCustomizer.java +++ b/gateway/src/main/java/org/georchestra/gateway/security/RolesMappingsUserCustomizer.java @@ -28,6 +28,7 @@ import java.util.stream.Collectors; import org.georchestra.security.model.GeorchestraUser; +import org.springframework.security.core.Authentication; import com.google.common.annotations.VisibleForTesting; import com.google.common.cache.Cache; @@ -82,14 +83,14 @@ static Pattern toPattern(String role) { } @Override - public GeorchestraUser apply(GeorchestraUser user) { + public GeorchestraUser apply(Authentication origAuthToken, GeorchestraUser mappedUser) { - Set additionalRoles = computeAdditionalRoles(user.getRoles()); + Set additionalRoles = computeAdditionalRoles(mappedUser.getRoles()); if (!additionalRoles.isEmpty()) { - additionalRoles.addAll(user.getRoles()); - user.setRoles(new ArrayList<>(additionalRoles)); + additionalRoles.addAll(mappedUser.getRoles()); + mappedUser.setRoles(new ArrayList<>(additionalRoles)); } - return user; + return mappedUser; } /** diff --git a/gateway/src/main/java/org/georchestra/gateway/security/accessrules/AccessRulesCustomizer.java b/gateway/src/main/java/org/georchestra/gateway/security/accessrules/AccessRulesCustomizer.java index cb20901b..119e3107 100644 --- a/gateway/src/main/java/org/georchestra/gateway/security/accessrules/AccessRulesCustomizer.java +++ b/gateway/src/main/java/org/georchestra/gateway/security/accessrules/AccessRulesCustomizer.java @@ -29,14 +29,14 @@ import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.config.web.server.ServerHttpSecurity.AuthorizeExchangeSpec; import org.springframework.security.config.web.server.ServerHttpSecurity.AuthorizeExchangeSpec.Access; +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; +import org.springframework.web.server.ServerWebExchange; import com.google.common.annotations.VisibleForTesting; import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; -import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; /** diff --git a/gateway/src/main/java/org/georchestra/gateway/security/ldap/LdapSecurityConfiguration.java b/gateway/src/main/java/org/georchestra/gateway/security/ldap/LdapAuthenticationConfiguration.java similarity index 97% rename from gateway/src/main/java/org/georchestra/gateway/security/ldap/LdapSecurityConfiguration.java rename to gateway/src/main/java/org/georchestra/gateway/security/ldap/LdapAuthenticationConfiguration.java index 2571f9ac..5e007690 100644 --- a/gateway/src/main/java/org/georchestra/gateway/security/ldap/LdapSecurityConfiguration.java +++ b/gateway/src/main/java/org/georchestra/gateway/security/ldap/LdapAuthenticationConfiguration.java @@ -71,16 +71,14 @@ * @see LdapConfigProperties * @see BasicLdapAuthenticationConfiguration * @see ExtendedLdapAuthenticationConfiguration - * @see ActiveDirectoryAuthenticationConfiguration */ @Configuration(proxyBeanMethods = true) @EnableConfigurationProperties(LdapConfigProperties.class) @Import({ // BasicLdapAuthenticationConfiguration.class, // - ExtendedLdapAuthenticationConfiguration.class // -}) + ExtendedLdapAuthenticationConfiguration.class }) @Slf4j(topic = "org.georchestra.gateway.security.ldap") -public class LdapSecurityConfiguration { +public class LdapAuthenticationConfiguration { public static final class LDAPAuthenticationCustomizer implements ServerHttpSecurityCustomizer { public @Override void customize(ServerHttpSecurity http) { 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 index fa4fb501..00a09dad 100644 --- a/gateway/src/main/java/org/georchestra/gateway/security/ldap/LdapConfigBuilder.java +++ b/gateway/src/main/java/org/georchestra/gateway/security/ldap/LdapConfigBuilder.java @@ -64,6 +64,7 @@ public ExtendedLdapConfig asExtendedLdapConfig(String name, Server config) { .rolesRdn(config.getRoles().getRdn())// .rolesSearchFilter(config.getRoles().getSearchFilter())// .orgsRdn(config.getOrgs().getRdn())// + .pendingOrgsRdn(config.getOrgs().getPendingRdn())// .adminDn(toOptional(config.getAdminDn()))// .adminPassword(toOptional(config.getAdminPassword()))// .build(); 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 9c817ab0..97fd43ff 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 @@ -174,6 +174,11 @@ public class LdapConfigProperties implements Validator { * Organizations search base. Default: ou=orgs */ private String rdn = "ou=orgs"; + + /** + * Pending organizations search base. Default: ou=pendingorgs + */ + private String pendingRdn = "ou=pendingorgs"; } public @Override boolean supports(Class clazz) { 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 index 114840cd..7d99a597 100644 --- 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 @@ -23,17 +23,16 @@ import org.georchestra.ds.users.AccountDao; import org.georchestra.gateway.security.ldap.extended.ExtendedLdapAuthenticationProvider; import org.georchestra.gateway.security.ldap.extended.ExtendedPasswordPolicyAwareContextSource; -import org.springframework.context.annotation.Configuration; import org.springframework.ldap.core.support.BaseLdapPathContextSource; 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.search.FilterBasedLdapUserSearch; import org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator; +import org.springframework.security.ldap.userdetails.LdapUserDetailsMapper; import lombok.Setter; import lombok.experimental.Accessors; -import org.springframework.security.ldap.userdetails.LdapUserDetailsMapper; /** */ 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 index f6737da8..afe7ec7a 100644 --- 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 @@ -50,7 +50,6 @@ import org.springframework.context.annotation.Configuration; import org.springframework.ldap.core.LdapTemplate; import org.springframework.ldap.core.support.LdapContextSource; -import org.springframework.security.ldap.authentication.LdapAuthenticationProvider; import org.springframework.security.ldap.userdetails.LdapUserDetails; import lombok.extern.slf4j.Slf4j; @@ -69,7 +68,7 @@ public class ExtendedLdapAuthenticationConfiguration { @Bean - public GeorchestraLdapAuthenticatedUserMapper georchestraLdapAuthenticatedUserMapper(DemultiplexingUsersApi users) { + GeorchestraLdapAuthenticatedUserMapper georchestraLdapAuthenticatedUserMapper(DemultiplexingUsersApi users) { return users.getTargetNames().isEmpty() ? null : new GeorchestraLdapAuthenticatedUserMapper(users); } diff --git a/gateway/src/main/java/org/georchestra/gateway/security/ldap/extended/ExtendedLdapConfig.java b/gateway/src/main/java/org/georchestra/gateway/security/ldap/extended/ExtendedLdapConfig.java index c6d5b628..820628e4 100644 --- a/gateway/src/main/java/org/georchestra/gateway/security/ldap/extended/ExtendedLdapConfig.java +++ b/gateway/src/main/java/org/georchestra/gateway/security/ldap/extended/ExtendedLdapConfig.java @@ -46,4 +46,5 @@ public class ExtendedLdapConfig { private @NonNull Optional adminPassword; private @NonNull String orgsRdn; + private @NonNull String pendingOrgsRdn; } \ No newline at end of file diff --git a/gateway/src/main/java/org/georchestra/gateway/security/ldap/extended/ExtendedPasswordPolicyAwareContextSource.java b/gateway/src/main/java/org/georchestra/gateway/security/ldap/extended/ExtendedPasswordPolicyAwareContextSource.java index 227421d3..740ddbcd 100644 --- a/gateway/src/main/java/org/georchestra/gateway/security/ldap/extended/ExtendedPasswordPolicyAwareContextSource.java +++ b/gateway/src/main/java/org/georchestra/gateway/security/ldap/extended/ExtendedPasswordPolicyAwareContextSource.java @@ -1,14 +1,18 @@ package org.georchestra.gateway.security.ldap.extended; -import org.springframework.core.log.LogMessage; -import org.springframework.ldap.support.LdapUtils; -import org.springframework.security.ldap.ppolicy.*; - import javax.naming.Context; import javax.naming.directory.DirContext; import javax.naming.ldap.Control; import javax.naming.ldap.LdapContext; +import org.springframework.core.log.LogMessage; +import org.springframework.ldap.support.LdapUtils; +import org.springframework.security.ldap.ppolicy.PasswordPolicyAwareContextSource; +import org.springframework.security.ldap.ppolicy.PasswordPolicyControl; +import org.springframework.security.ldap.ppolicy.PasswordPolicyControlExtractor; +import org.springframework.security.ldap.ppolicy.PasswordPolicyException; +import org.springframework.security.ldap.ppolicy.PasswordPolicyResponseControl; + public class ExtendedPasswordPolicyAwareContextSource extends PasswordPolicyAwareContextSource { public ExtendedPasswordPolicyAwareContextSource(String providerUrl) { diff --git a/gateway/src/main/java/org/georchestra/gateway/security/ldap/extended/GeorchestraLdapAuthenticatedUserMapper.java b/gateway/src/main/java/org/georchestra/gateway/security/ldap/extended/GeorchestraLdapAuthenticatedUserMapper.java index 90e5cb9f..e837f316 100644 --- a/gateway/src/main/java/org/georchestra/gateway/security/ldap/extended/GeorchestraLdapAuthenticatedUserMapper.java +++ b/gateway/src/main/java/org/georchestra/gateway/security/ldap/extended/GeorchestraLdapAuthenticatedUserMapper.java @@ -23,6 +23,7 @@ import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.georchestra.gateway.security.GeorchestraUserMapperExtension; import org.georchestra.security.api.UsersApi; @@ -31,10 +32,10 @@ import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.ldap.userdetails.LdapUserDetails; +import org.springframework.security.ldap.userdetails.LdapUserDetailsImpl; import lombok.NonNull; import lombok.RequiredArgsConstructor; -import org.springframework.security.ldap.userdetails.LdapUserDetailsImpl; /** * {@link GeorchestraUserMapperExtension} that maps LDAP-authenticated token to @@ -81,12 +82,13 @@ private GeorchestraUser fixPrefixedRoleNames(GeorchestraUser user, // Fix role name mismatch between authority provider (adds ROLE_ prefix) and // users api - Set prefixedRoleNames = token.getAuthorities().stream().filter(SimpleGrantedAuthority.class::isInstance) - .map(GrantedAuthority::getAuthority).filter(role -> role.startsWith("ROLE_")) - .collect(Collectors.toSet()); + Stream authorityRoleNames = token.getAuthorities().stream() + .filter(SimpleGrantedAuthority.class::isInstance).map(GrantedAuthority::getAuthority) + .map(this::normalize); + + Stream userRoles = user.getRoles().stream().map(this::normalize); - List roles = user.getRoles().stream() - .map(r -> prefixedRoleNames.contains("ROLE_" + r) ? "ROLE_" + r : r).collect(Collectors.toList()); + List roles = Stream.concat(authorityRoleNames, userRoles).distinct().collect(Collectors.toList()); user.setRoles(roles); if (principal.getTimeBeforeExpiration() < Integer.MAX_VALUE) { @@ -98,4 +100,8 @@ private GeorchestraUser fixPrefixedRoleNames(GeorchestraUser user, return user; } + + private String normalize(String role) { + return role.startsWith("ROLE_") ? role : "ROLE_" + role; + } } diff --git a/gateway/src/main/java/org/georchestra/gateway/security/oauth2/ExtendedOAuth2ClientProperties.java b/gateway/src/main/java/org/georchestra/gateway/security/oauth2/ExtendedOAuth2ClientProperties.java index 0c3489c0..884799b6 100644 --- a/gateway/src/main/java/org/georchestra/gateway/security/oauth2/ExtendedOAuth2ClientProperties.java +++ b/gateway/src/main/java/org/georchestra/gateway/security/oauth2/ExtendedOAuth2ClientProperties.java @@ -1,12 +1,30 @@ +/* + * Copyright (C) 2023 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.oauth2; +import java.util.HashMap; +import java.util.Map; + import org.springframework.beans.factory.InitializingBean; import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties; import org.springframework.boot.context.properties.ConfigurationProperties; -import java.util.HashMap; -import java.util.Map; - @ConfigurationProperties(prefix = "spring.security.oauth2.client") public class ExtendedOAuth2ClientProperties implements InitializingBean { diff --git a/gateway/src/main/java/org/georchestra/gateway/security/oauth2/OAuth2Configuration.java b/gateway/src/main/java/org/georchestra/gateway/security/oauth2/OAuth2Configuration.java index 61981bc0..3fbc1961 100644 --- a/gateway/src/main/java/org/georchestra/gateway/security/oauth2/OAuth2Configuration.java +++ b/gateway/src/main/java/org/georchestra/gateway/security/oauth2/OAuth2Configuration.java @@ -18,26 +18,22 @@ */ package org.georchestra.gateway.security.oauth2; -import com.nimbusds.jwt.JWT; -import com.nimbusds.jwt.JWTParser; -import org.georchestra.ds.roles.RoleDao; -import org.georchestra.ds.roles.RoleDaoImpl; -import org.georchestra.ds.roles.RoleProtected; -import org.georchestra.ds.users.AccountDao; -import org.georchestra.ds.users.AccountDaoImpl; +import java.lang.reflect.Field; +import java.nio.charset.StandardCharsets; +import java.text.ParseException; +import java.util.Arrays; +import java.util.Collections; + +import javax.crypto.spec.SecretKeySpec; + import org.georchestra.gateway.security.ServerHttpSecurityCustomizer; import org.georchestra.gateway.security.ldap.LdapConfigProperties; -import org.georchestra.gateway.security.ldap.extended.ExtendedLdapConfig; import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; import org.springframework.http.client.reactive.ReactorClientHttpConnector; -import org.springframework.ldap.core.LdapTemplate; -import org.springframework.ldap.core.support.LdapContextSource; -import org.springframework.ldap.pool.factory.PoolingContextSource; -import org.springframework.ldap.pool.validation.DefaultDirContextValidator; import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.config.web.server.ServerHttpSecurity.OAuth2LoginSpec; @@ -57,21 +53,16 @@ import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler; import org.springframework.web.reactive.function.client.WebClient; +import com.nimbusds.jwt.JWT; +import com.nimbusds.jwt.JWTParser; + import lombok.extern.slf4j.Slf4j; import reactor.netty.http.client.HttpClient; import reactor.netty.transport.ProxyProvider; -import javax.crypto.spec.SecretKeySpec; -import java.lang.reflect.Field; -import java.nio.charset.StandardCharsets; -import java.text.ParseException; -import java.util.Arrays; -import java.util.Collections; import java.util.Map; import java.util.stream.Collectors; -import static java.util.Objects.requireNonNull; - @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties({ OAuth2ProxyConfigProperties.class, OpenIdConnectCustomClaimsConfigProperties.class, LdapConfigProperties.class, ExtendedOAuth2ClientProperties.class }) @@ -87,11 +78,10 @@ public static final class OAuth2AuthenticationCustomizer implements ServerHttpSe } @Bean - private ServerLogoutSuccessHandler oidcLogoutSuccessHandler( + @Profile("!test") + ServerLogoutSuccessHandler oidcLogoutSuccessHandler( InMemoryReactiveClientRegistrationRepository clientRegistrationRepository, ExtendedOAuth2ClientProperties properties) { - OidcClientInitiatedServerLogoutSuccessHandler oidcLogoutSuccessHandler = new OidcClientInitiatedServerLogoutSuccessHandler( - clientRegistrationRepository); clientRegistrationRepository.forEach(client -> { if (client.getProviderDetails().getConfigurationMetadata().isEmpty() && properties.getProvider().get(client.getRegistrationId()) != null @@ -106,98 +96,13 @@ private ServerLogoutSuccessHandler oidcLogoutSuccessHandler( } } }); - oidcLogoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}/login?logout"); + OidcClientInitiatedServerLogoutSuccessHandler oidcLogoutSuccessHandler = new OidcClientInitiatedServerLogoutSuccessHandler( + clientRegistrationRepository); + oidcLogoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}/login?logout"); return oidcLogoutSuccessHandler; } - @Bean - @ConditionalOnExpression("${georchestra.gateway.security.createNonExistingUsersInLDAP:true}") - public LdapContextSource singleContextSource(LdapConfigProperties config) { - ExtendedLdapConfig ldapConfig = config.extendedEnabled().get(0); - LdapContextSource singleContextSource = new LdapContextSource(); - singleContextSource.setUrl(ldapConfig.getUrl()); - singleContextSource.setBase(ldapConfig.getBaseDn()); - singleContextSource.setUserDn(ldapConfig.getAdminDn().get()); - singleContextSource.setPassword(ldapConfig.getAdminPassword().get()); - return singleContextSource; - } - - @Bean - @ConditionalOnExpression("${georchestra.gateway.security.createNonExistingUsersInLDAP:true}") - public PoolingContextSource contextSource(LdapConfigProperties config, LdapContextSource singleContextSource) { - ExtendedLdapConfig ldapConfig = config.extendedEnabled().get(0); - PoolingContextSource contextSource = new PoolingContextSource(); - contextSource.setContextSource(singleContextSource); - contextSource.setDirContextValidator(new DefaultDirContextValidator()); - contextSource.setTestOnBorrow(true); - contextSource.setMaxActive(8); - contextSource.setMinIdle(1); - contextSource.setMaxIdle(8); - contextSource.setMaxTotal(-1); - contextSource.setMaxWait(-1); - return contextSource; - } - - @Bean - @ConditionalOnExpression("${georchestra.gateway.security.createNonExistingUsersInLDAP:true}") - public LdapTemplate ldapTemplate(PoolingContextSource contextSource) throws Exception { - LdapTemplate ldapTemplate = new LdapTemplate(contextSource); - return ldapTemplate; - } - - @Bean - @ConditionalOnExpression("${georchestra.gateway.security.createNonExistingUsersInLDAP:true}") - public RoleDao roleDao(LdapTemplate ldapTemplate, LdapConfigProperties config) { - RoleDaoImpl impl = new RoleDaoImpl(); - impl.setLdapTemplate(ldapTemplate); - impl.setRoleSearchBaseDN(config.extendedEnabled().get(0).getRolesRdn()); - return impl; - } - - @Bean - @ConditionalOnExpression("${georchestra.gateway.security.createNonExistingUsersInLDAP:true}") - public AccountDao accountDao(LdapTemplate ldapTemplate, LdapConfigProperties config) throws Exception { - ExtendedLdapConfig ldapConfig = config.extendedEnabled().get(0); - 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; - } - - @Bean - @ConditionalOnExpression("${georchestra.gateway.security.createNonExistingUsersInLDAP:true}") - public RoleProtected roleProtected() { - RoleProtected roleProtected = new RoleProtected(); - roleProtected.setListOfprotectedRoles( - new String[] { "ADMINISTRATOR", "GN_.*", "ORGADMIN", "REFERENT", "USER", "SUPERUSER" }); - return roleProtected; - } - @Bean ServerHttpSecurityCustomizer oauth2LoginEnablingCustomizer() { return new OAuth2AuthenticationCustomizer(); diff --git a/gateway/src/main/java/org/georchestra/gateway/security/oauth2/OAuth2UserMapper.java b/gateway/src/main/java/org/georchestra/gateway/security/oauth2/OAuth2UserMapper.java index 92217e16..f5ed3f00 100644 --- a/gateway/src/main/java/org/georchestra/gateway/security/oauth2/OAuth2UserMapper.java +++ b/gateway/src/main/java/org/georchestra/gateway/security/oauth2/OAuth2UserMapper.java @@ -78,6 +78,9 @@ protected Optional map(OAuth2AuthenticationToken token) { OAuth2User oAuth2User = token.getPrincipal(); GeorchestraUser user = new GeorchestraUser(); + final String oAuth2ProviderId = String.format("%s;%s", token.getAuthorizedClientRegistrationId(), + token.getName()); + user.setOAuth2ProviderId(oAuth2ProviderId); Map attributes = oAuth2User.getAttributes(); diff --git a/gateway/src/main/java/org/georchestra/gateway/security/oauth2/OpenIdConnectUserMapper.java b/gateway/src/main/java/org/georchestra/gateway/security/oauth2/OpenIdConnectUserMapper.java index 5af88485..559fd445 100644 --- a/gateway/src/main/java/org/georchestra/gateway/security/oauth2/OpenIdConnectUserMapper.java +++ b/gateway/src/main/java/org/georchestra/gateway/security/oauth2/OpenIdConnectUserMapper.java @@ -19,28 +19,17 @@ package org.georchestra.gateway.security.oauth2; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.function.Consumer; import java.util.function.Predicate; -import java.util.stream.Collectors; import java.util.stream.Stream; -import org.georchestra.ds.DataServiceException; -import org.georchestra.ds.roles.Role; -import org.georchestra.ds.roles.RoleDao; -import org.georchestra.ds.security.UserMapperImpl; -import org.georchestra.ds.security.UsersApiImpl; -import org.georchestra.ds.users.*; -import org.georchestra.gateway.events.RabbitmqEventsSender; import org.georchestra.gateway.security.ldap.LdapConfigProperties; import org.georchestra.security.model.GeorchestraUser; import org.slf4j.Logger; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.core.Ordered; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; @@ -146,20 +135,6 @@ @Slf4j(topic = "org.georchestra.gateway.security.oauth2") public class OpenIdConnectUserMapper extends OAuth2UserMapper { - private @Value("${enableRabbitmqEvents:false}") boolean enableRabbitmq; - - @Autowired - LdapConfigProperties config; - - @Autowired(required = false) - private AccountDao accountDao; - - @Autowired(required = false) - private RoleDao roleDao; - - @Autowired(required = false) - private RabbitmqEventsSender eventsSender; - private final @NonNull OpenIdConnectCustomClaimsConfigProperties nonStandardClaimsConfig; protected @Override Predicate tokenFilter() { @@ -172,64 +147,16 @@ public class OpenIdConnectUserMapper extends OAuth2UserMapper { } protected @Override Optional map(OAuth2AuthenticationToken token) { - - if (config.isCreateNonExistingUsersInLDAP()) { - String oAuth2ProviderId = String.format("%s;%s", token.getAuthorizedClientRegistrationId(), - token.getName()); - - UserMapperImpl mapper = new UserMapperImpl(); - mapper.setRoleDao(roleDao); - List protectedUsers = Collections.emptyList(); - UserRule rule = new UserRule(); - rule.setListOfprotectedUsers(protectedUsers.toArray(String[]::new)); - UsersApiImpl usersApi = new UsersApiImpl(); - usersApi.setAccountsDao(accountDao); - usersApi.setMapper(mapper); - usersApi.setUserRule(rule); - - Optional userOpt = usersApi.findByOAuth2ProviderId(oAuth2ProviderId); - - if (userOpt.isEmpty()) { - try { - OidcUser oidcUser = (OidcUser) token.getPrincipal(); - Account newAccount = AccountFactory.createBrief(oidcUser.getEmail(), null, oidcUser.getGivenName(), - oidcUser.getFamilyName(), oidcUser.getEmail(), "", "", "", oAuth2ProviderId); - newAccount.setPending(false); - accountDao.insert(newAccount); - roleDao.addUser(Role.USER, newAccount); - userOpt = usersApi.findByOAuth2ProviderId(oAuth2ProviderId); - if (enableRabbitmq && eventsSender != null) { - eventsSender.sendNewOAuthAccountMessage( - oidcUser.getGivenName() + " " + oidcUser.getFamilyName(), oidcUser.getEmail(), - token.getAuthorizedClientRegistrationId()); - } - } catch (DuplicatedUidException e) { - throw new IllegalStateException(e); - } catch (DuplicatedEmailException e) { - throw new IllegalStateException(e); - } catch (DataServiceException e) { - throw new IllegalStateException(e); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - List roles = userOpt.get().getRoles().stream().map(r -> r.contains("ROLE_") ? r : "ROLE_" + r) - .collect(Collectors.toList()); - userOpt.get().setRoles(roles); - return userOpt; - } else { - GeorchestraUser user = super.map(token).orElseGet(GeorchestraUser::new); - OidcUser oidcUser = (OidcUser) token.getPrincipal(); - try { - applyStandardClaims(oidcUser, user); - applyNonStandardClaims(oidcUser.getClaims(), user); - } catch (Exception e) { - log.error("Error mapping non-standard OIDC claims for authenticated user", e); - throw new IllegalStateException(e); - } - return Optional.of(user); + GeorchestraUser user = super.map(token).orElseGet(GeorchestraUser::new); + OidcUser oidcUser = (OidcUser) token.getPrincipal(); + try { + applyStandardClaims(oidcUser, user); + applyNonStandardClaims(oidcUser.getClaims(), user); + } catch (Exception e) { + log.error("Error mapping non-standard OIDC claims for authenticated user", e); + throw new IllegalStateException(e); } + return Optional.of(user); } /** diff --git a/gateway/src/main/java/org/geoserver/cloud/gateway/filter/GlobalUriFilter.java b/gateway/src/main/java/org/geoserver/cloud/gateway/filter/GlobalUriFilter.java index 00a32797..2cb23bef 100644 --- a/gateway/src/main/java/org/geoserver/cloud/gateway/filter/GlobalUriFilter.java +++ b/gateway/src/main/java/org/geoserver/cloud/gateway/filter/GlobalUriFilter.java @@ -7,6 +7,8 @@ import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR; import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR; +import java.net.URI; + import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.cloud.gateway.filter.ReactiveLoadBalancerClientFilter; @@ -17,8 +19,6 @@ import reactor.core.publisher.Mono; -import java.net.URI; - /** * See gateway's issue #2065 diff --git a/gateway/src/main/java/org/geoserver/cloud/gateway/filter/RouteProfileGatewayFilterFactory.java b/gateway/src/main/java/org/geoserver/cloud/gateway/filter/RouteProfileGatewayFilterFactory.java index 3cae8e21..e354243a 100644 --- a/gateway/src/main/java/org/geoserver/cloud/gateway/filter/RouteProfileGatewayFilterFactory.java +++ b/gateway/src/main/java/org/geoserver/cloud/gateway/filter/RouteProfileGatewayFilterFactory.java @@ -6,10 +6,11 @@ import static org.springframework.cloud.gateway.support.GatewayToStringStyler.filterToStringCreator; -import lombok.Data; -import lombok.NonNull; -import lombok.RequiredArgsConstructor; -import lombok.experimental.Accessors; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import javax.validation.constraints.NotEmpty; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.gateway.filter.GatewayFilter; @@ -21,14 +22,12 @@ import org.springframework.validation.annotation.Validated; import org.springframework.web.server.ServerWebExchange; +import lombok.Data; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.experimental.Accessors; import reactor.core.publisher.Mono; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -import javax.validation.constraints.NotEmpty; - /** Allows to enable routes only if a given spring profile is enabled */ public class RouteProfileGatewayFilterFactory extends AbstractGatewayFilterFactory { diff --git a/gateway/src/main/java/org/geoserver/cloud/gateway/filter/StripBasePathGatewayFilterFactory.java b/gateway/src/main/java/org/geoserver/cloud/gateway/filter/StripBasePathGatewayFilterFactory.java index 4527cb21..3157e3c9 100644 --- a/gateway/src/main/java/org/geoserver/cloud/gateway/filter/StripBasePathGatewayFilterFactory.java +++ b/gateway/src/main/java/org/geoserver/cloud/gateway/filter/StripBasePathGatewayFilterFactory.java @@ -6,7 +6,7 @@ import static com.google.common.base.Preconditions.checkArgument; -import lombok.Data; +import java.util.List; import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; @@ -15,7 +15,7 @@ import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.util.StringUtils; -import java.util.List; +import lombok.Data; /** * See gateway's issue - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/gateway/src/main/resources/rabbit-sender-context.xml b/gateway/src/main/resources/rabbit-sender-context.xml index dc368c9d..95035157 100644 --- a/gateway/src/main/resources/rabbit-sender-context.xml +++ b/gateway/src/main/resources/rabbit-sender-context.xml @@ -1,11 +1,12 @@ - - - + - - + + + + + \ No newline at end of file diff --git a/gateway/src/main/resources/templates/login.html b/gateway/src/main/resources/templates/login.html index 73821a94..790f2641 100644 --- a/gateway/src/main/resources/templates/login.html +++ b/gateway/src/main/resources/templates/login.html @@ -18,11 +18,11 @@