Skip to content

Commit

Permalink
Returns ExtendedGeorchestra* objects when createUserInLdap set to true
Browse files Browse the repository at this point in the history
Considering the following configuration scenario:

* external authentication (oidc/oauth2 or pre-auth) being configured
* `createUsersInGeorchestraLdap` set to true

Then the resolved GeorchestraUser should be an
`ExtendedGeorchestraUser`, in order to have a behaviour coherent with
the extended geOrchestra LDAP authentication.

Without doing so, users externally authenticated will resolve as a
classic GeorchestraUser, leading to missing http headers and breaking
some geOrchestra applications (e.g. datafeeder, which requires the
`sec-orgname` provided only when resolving to an
`ExtendedGeorchestraUser`).

This also refactors the LdapConfigProperties to
GeorchestraGatewaySecurityConfigProperties, as the object is not only
about LDAP, but also nests some other configureable features (OIDC,
...).

Documentation has been updated to describe / explain the behaviour.

Tests:
* testsuite adapted
* added specific tests case scenario
  • Loading branch information
pmauduit committed Mar 29, 2024
1 parent 7344454 commit 0bebc62
Show file tree
Hide file tree
Showing 24 changed files with 212 additions and 102 deletions.
21 changes: 21 additions & 0 deletions docs/authzn.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -276,3 +276,24 @@ sec-roles: ROLE_ORG_6007280321;ROLE_GDI_PLANER;ROLE_GDI_EDITOR;ROLE_USER
sec-org: 6007280321
```

== Automatically creating users in a geOrchestra LDAP

As in the <<pre-authentication.adoc#,pre-authentication method>>, it is possible
to create externally authenticated users into a geOrchestra (extended) LDAP, so
that an administrator can promote the user to a higher role than `USER` by default.

In order to do so, you will need to set the following property, and make sure
an `extended` LDAP named `default` is defined, as in the following configuration
snippet:

```
georchestra:
gateway:
security:
create-non-existing-users-in-l-d-a-p: true
ldap:
default:
enabled: true
extended: true
[...]
```
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ protected Optional<GeorchestraUser> findInternal(GeorchestraUser mappedUser) {
return findByUsername(mappedUser.getUsername());
}

GeorchestraUser createIfMissing(GeorchestraUser mapped) {
protected GeorchestraUser createIfMissing(GeorchestraUser mapped) {
lock.writeLock().lock();
try {
GeorchestraUser existing = findInternal(mapped).orElse(null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
import lombok.Value;

/**
* Application event published when a new account was created
* Application event published when a new account has been created
*/
@Value
public class AccountCreated {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@
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.GeorchestraGatewaySecurityConfigProperties;
import org.georchestra.gateway.security.ldap.extended.DemultiplexingUsersApi;
import org.georchestra.gateway.security.ldap.extended.ExtendedLdapConfig;
import org.georchestra.security.api.UsersApi;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
Expand All @@ -48,16 +49,21 @@
import org.springframework.ldap.pool.validation.DefaultDirContextValidator;

@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(LdapConfigProperties.class)
@EnableConfigurationProperties(GeorchestraGatewaySecurityConfigProperties.class)
public class GeorchestraLdapAccountManagementConfiguration {

@Bean
AccountManager ldapAccountsManager(//
ApplicationEventPublisher eventPublisher, //
AccountDao accountDao, RoleDao roleDao, OrgsDao orgsDao) {
AccountDao accountDao, //
RoleDao roleDao, //
OrgsDao orgsDao, //
DemultiplexingUsersApi demultiplexingUsersApi,
GeorchestraGatewaySecurityConfigProperties configProperties) {

UsersApi usersApi = ldapUsersApi(accountDao, roleDao);
return new LdapAccountsManager(eventPublisher::publishEvent, accountDao, roleDao, orgsDao, usersApi);
return new LdapAccountsManager(eventPublisher::publishEvent, accountDao, roleDao, orgsDao,
demultiplexingUsersApi, configProperties);
}

@Bean
Expand All @@ -79,7 +85,7 @@ private UsersApi ldapUsersApi(AccountDao accountDao, RoleDao roleDao) {
}

@Bean
LdapContextSource singleContextSource(LdapConfigProperties config) {
LdapContextSource singleContextSource(GeorchestraGatewaySecurityConfigProperties config) {
ExtendedLdapConfig ldapConfig = config.extendedEnabled().get(0);
LdapContextSource singleContextSource = new LdapContextSource();
singleContextSource.setUrl(ldapConfig.getUrl());
Expand Down Expand Up @@ -110,15 +116,15 @@ LdapTemplate ldapTemplate(PoolingContextSource contextSource) throws Exception {
}

@Bean
RoleDao roleDao(LdapTemplate ldapTemplate, LdapConfigProperties config) {
RoleDao roleDao(LdapTemplate ldapTemplate, GeorchestraGatewaySecurityConfigProperties config) {
RoleDaoImpl impl = new RoleDaoImpl();
impl.setLdapTemplate(ldapTemplate);
impl.setRoleSearchBaseDN(config.extendedEnabled().get(0).getRolesRdn());
return impl;
}

@Bean
OrgsDao orgsDao(LdapTemplate ldapTemplate, LdapConfigProperties config) {
OrgsDao orgsDao(LdapTemplate ldapTemplate, GeorchestraGatewaySecurityConfigProperties config) {
OrgsDaoImpl impl = new OrgsDaoImpl();
impl.setLdapTemplate(ldapTemplate);
ExtendedLdapConfig ldapConfig = config.extendedEnabled().get(0);
Expand All @@ -129,7 +135,8 @@ OrgsDao orgsDao(LdapTemplate ldapTemplate, LdapConfigProperties config) {
}

@Bean
AccountDao accountDao(LdapTemplate ldapTemplate, LdapConfigProperties config) throws Exception {
AccountDao accountDao(LdapTemplate ldapTemplate, GeorchestraGatewaySecurityConfigProperties config)
throws Exception {
ExtendedLdapConfig ldapConfig = config.extendedEnabled().get(0);
String baseDn = ldapConfig.getBaseDn();
String userSearchBaseDN = ldapConfig.getUsersRdn();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,34 +18,29 @@
*/
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.stream.Collectors;

import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
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.ds.users.*;
import org.georchestra.gateway.accounts.admin.AbstractAccountsManager;
import org.georchestra.gateway.accounts.admin.AccountManager;
import org.georchestra.security.api.UsersApi;
import org.georchestra.gateway.security.GeorchestraGatewaySecurityConfigProperties;
import org.georchestra.gateway.security.ldap.extended.DemultiplexingUsersApi;
import org.georchestra.security.model.GeorchestraUser;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.ldap.NameNotFoundException;

import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;

/**
* {@link AccountManager} that fetches and creates {@link GeorchestraUser}s from
Expand All @@ -55,30 +50,31 @@
@Slf4j(topic = "org.georchestra.gateway.accounts.admin.ldap")
class LdapAccountsManager extends AbstractAccountsManager {

private @Value("${georchestra.gateway.security.defaultOrganization:}") String defaultOrganization;
private final @NonNull GeorchestraGatewaySecurityConfigProperties georchestraGatewaySecurityConfigProperties;
private final @NonNull AccountDao accountDao;
private final @NonNull RoleDao roleDao;

private final @NonNull OrgsDao orgsDao;
private final @NonNull UsersApi usersApi;
private final @NonNull DemultiplexingUsersApi demultiplexingUsersApi;

public LdapAccountsManager(ApplicationEventPublisher eventPublisher, AccountDao accountDao, RoleDao roleDao,
OrgsDao orgsDao, UsersApi usersApi) {
OrgsDao orgsDao, DemultiplexingUsersApi demultiplexingUsersApi,
GeorchestraGatewaySecurityConfigProperties georchestraGatewaySecurityConfigProperties) {
super(eventPublisher);
this.accountDao = accountDao;
this.roleDao = roleDao;
this.orgsDao = orgsDao;
this.usersApi = usersApi;
this.demultiplexingUsersApi = demultiplexingUsersApi;
this.georchestraGatewaySecurityConfigProperties = georchestraGatewaySecurityConfigProperties;
}

@Override
protected Optional<GeorchestraUser> findByOAuth2Uid(@NonNull String oAuth2Provider, @NonNull String oAuth2Uid) {
return usersApi.findByOAuth2Uid(oAuth2Provider, oAuth2Uid).map(this::ensureRolesPrefixed);
return demultiplexingUsersApi.findByOAuth2Uid(oAuth2Provider, oAuth2Uid).map(this::ensureRolesPrefixed);
}

@Override
protected Optional<GeorchestraUser> findByUsername(@NonNull String username) {
return usersApi.findByUsername(username).map(this::ensureRolesPrefixed);
return demultiplexingUsersApi.findByUsername(username).map(this::ensureRolesPrefixed);
}

private GeorchestraUser ensureRolesPrefixed(GeorchestraUser user) {
Expand Down Expand Up @@ -150,8 +146,9 @@ private Account mapToAccountBrief(@NonNull GeorchestraUser preAuth) {
Account newAccount = AccountFactory.createBrief(username, password, firstName, lastName, email, phone, title,
description, oAuth2Provider, oAuth2Uid);
newAccount.setPending(false);
if (StringUtils.isEmpty(org) && !StringUtils.isBlank(defaultOrganization)) {
newAccount.setOrg(defaultOrganization);
String defaultOrg = this.georchestraGatewaySecurityConfigProperties.getDefaultOrganization();
if (StringUtils.isEmpty(org) && !StringUtils.isBlank(defaultOrg)) {
newAccount.setOrg(defaultOrg);
} else {
newAccount.setOrg(org);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
import javax.annotation.PostConstruct;

import org.georchestra.gateway.security.GeorchestraUserMapper;
import org.georchestra.gateway.security.ldap.LdapConfigProperties;
import org.georchestra.gateway.security.GeorchestraGatewaySecurityConfigProperties;
import org.georchestra.security.model.GeorchestraUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
Expand Down Expand Up @@ -58,13 +58,13 @@
@Controller
@Slf4j
@SpringBootApplication
@EnableConfigurationProperties(LdapConfigProperties.class)
@EnableConfigurationProperties(GeorchestraGatewaySecurityConfigProperties.class)
public class GeorchestraGatewayApplication {

private @Autowired RouteLocator routeLocator;
private @Autowired GeorchestraUserMapper userMapper;

private @Autowired(required = false) LdapConfigProperties ldapConfigProperties;
private @Autowired(required = false) GeorchestraGatewaySecurityConfigProperties georchestraGatewaySecurityConfigProperties;

private boolean ldapEnabled = false;

Expand All @@ -79,8 +79,9 @@ public static void main(String[] args) {

@PostConstruct
void initialize() {
if (ldapConfigProperties != null) {
ldapEnabled = ldapConfigProperties.getLdap().values().stream().anyMatch((server -> server.isEnabled()));
if (georchestraGatewaySecurityConfigProperties != null) {
ldapEnabled = georchestraGatewaySecurityConfigProperties.getLdap().values().stream()
.anyMatch((server -> server.isEnabled()));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ConditionalOnDefaultGeorchestraLdapEnabled
@ConditionalOnProperty(name = "georchestra.gateway.security.createNonExistingUsersInLDAP", havingValue = "true", matchIfMissing = false)
@ConditionalOnProperty(name = "georchestra.gateway.security.create-non-existing-users-in-l-d-a-p", havingValue = "true", matchIfMissing = false)
public @interface ConditionalOnCreateLdapAccounts {

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,15 @@
package org.georchestra.gateway.autoconfigure.accounts;

import org.georchestra.gateway.accounts.admin.ldap.GeorchestraLdapAccountManagementConfiguration;
import org.georchestra.gateway.security.ldap.extended.ExtendedLdapAuthenticationConfiguration;
import org.georchestra.gateway.security.ldap.extended.ExtendedLdapConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Import;

import javax.annotation.PostConstruct;
import java.util.List;

/**
* {@link AutoConfiguration @AutoConfiguration}
*
Expand All @@ -30,6 +36,16 @@
*/
@AutoConfiguration
@ConditionalOnCreateLdapAccounts
@Import(GeorchestraLdapAccountManagementConfiguration.class)
@Import({ GeorchestraLdapAccountManagementConfiguration.class, ExtendedLdapAuthenticationConfiguration.class })
public class GeorchestraLdapAccountsCreationAutoConfiguration {
}

@Autowired
private List<ExtendedLdapConfig> configs;

@PostConstruct
void failIfNoExtendedLdapCongfigs() {
if (configs.isEmpty()) {
throw new IllegalStateException("LDAP account creation requires an extended LDAP configuration");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.georchestra.gateway.security.ldap.LdapConfigProperties;
import org.georchestra.gateway.security.GeorchestraGatewaySecurityConfigProperties;
import org.springframework.boot.autoconfigure.condition.ConditionMessage;
import org.springframework.boot.autoconfigure.condition.ConditionMessage.ItemsBuilder;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
Expand All @@ -48,7 +48,7 @@
* the externalized config properties
* {@code georchestra.gateway.security.ldap.<configName>.enabled}
*
* @see LdapConfigProperties
* @see GeorchestraGatewaySecurityConfigProperties
*/
class AtLeastOneLdapDatasourceEnabledCondition extends SpringBootCondition {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
* You should have received a copy of the GNU General Public License along with
* geOrchestra. If not, see <http://www.gnu.org/licenses/>.
*/
package org.georchestra.gateway.security.ldap;
package org.georchestra.gateway.security;

import java.util.List;
import java.util.Map;
Expand All @@ -26,6 +26,8 @@

import javax.validation.Valid;

import org.georchestra.gateway.security.ldap.LdapConfigBuilder;
import org.georchestra.gateway.security.ldap.LdapConfigPropertiesValidations;
import org.georchestra.gateway.security.ldap.basic.LdapServerConfig;
import org.georchestra.gateway.security.ldap.extended.ExtendedLdapConfig;
import org.springframework.boot.context.properties.ConfigurationProperties;
Expand Down Expand Up @@ -61,10 +63,12 @@
@Validated
@Accessors(chain = true)
@ConfigurationProperties(prefix = "georchestra.gateway.security")
public class LdapConfigProperties implements Validator {
public class GeorchestraGatewaySecurityConfigProperties implements Validator {

private boolean createNonExistingUsersInLDAP = true;

private String defaultOrganization = "";

@Valid
private Map<String, Server> ldap = Map.of();

Expand Down Expand Up @@ -182,12 +186,12 @@ public class LdapConfigProperties implements Validator {
}

public @Override boolean supports(Class<?> clazz) {
return LdapConfigProperties.class.equals(clazz);
return GeorchestraGatewaySecurityConfigProperties.class.equals(clazz);
}

@Override
public void validate(Object target, Errors errors) {
LdapConfigProperties config = (LdapConfigProperties) target;
GeorchestraGatewaySecurityConfigProperties config = (GeorchestraGatewaySecurityConfigProperties) target;
Map<String, Server> ldap = config.getLdap();
if (ldap == null || ldap.isEmpty()) {
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.georchestra.gateway.security.GeorchestraGatewaySecurityConfigProperties;
import org.georchestra.gateway.security.ServerHttpSecurityCustomizer;
import org.georchestra.gateway.security.ldap.basic.BasicLdapAuthenticationConfiguration;
import org.georchestra.gateway.security.ldap.basic.BasicLdapAuthenticationProvider;
Expand Down Expand Up @@ -49,9 +50,11 @@
* authorization across multiple LDAP databases.
* <p>
* This configuration sets up the required beans for spring-based LDAP
* authentication and authorization, using {@link LdapConfigProperties} to get
* the {@link LdapConfigProperties#getUrl() connection URL} and the
* {@link LdapConfigProperties#getBaseDn() base DN}.
* authentication and authorization, using
* {@link GeorchestraGatewaySecurityConfigProperties} to get the
* {@link GeorchestraGatewaySecurityConfigProperties#getUrl() connection URL}
* and the {@link GeorchestraGatewaySecurityConfigProperties#getBaseDn() base
* DN}.
* <p>
* As a result, the {@link ServerHttpSecurity} will have HTTP-Basic
* authentication enabled and {@link ServerHttpSecurity#formLogin() form login}
Expand All @@ -68,12 +71,12 @@
* the matching gateway-route configuration. See
* {@link ExtendedLdapAuthenticationConfiguration} for further details.
*
* @see LdapConfigProperties
* @see GeorchestraGatewaySecurityConfigProperties
* @see BasicLdapAuthenticationConfiguration
* @see ExtendedLdapAuthenticationConfiguration
*/
@Configuration(proxyBeanMethods = true)
@EnableConfigurationProperties(LdapConfigProperties.class)
@EnableConfigurationProperties(GeorchestraGatewaySecurityConfigProperties.class)
@Import({ //
BasicLdapAuthenticationConfiguration.class, //
ExtendedLdapAuthenticationConfiguration.class })
Expand Down
Loading

0 comments on commit 0bebc62

Please sign in to comment.