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 May 7, 2024
1 parent ce36b3d commit 204f91a
Show file tree
Hide file tree
Showing 27 changed files with 320 additions and 171 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 @@ -18,18 +18,16 @@
*/
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.gateway.security.exceptions.DuplicatedEmailFoundException;
import org.georchestra.security.model.GeorchestraUser;

import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.georchestra.gateway.security.exceptions.DuplicatedEmailFoundException;
import org.georchestra.security.model.GeorchestraUser;
import org.springframework.context.ApplicationEventPublisher;

import java.util.Optional;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

@RequiredArgsConstructor
public abstract class AbstractAccountsManager implements AccountManager {

Expand Down Expand Up @@ -58,7 +56,7 @@ protected Optional<GeorchestraUser> findInternal(GeorchestraUser mappedUser) {
return findByUsername(mappedUser.getUsername());
}

GeorchestraUser createIfMissing(GeorchestraUser mapped) throws DuplicatedEmailFoundException {
protected GeorchestraUser createIfMissing(GeorchestraUser mapped) throws DuplicatedEmailFoundException {
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 @@ -20,24 +20,18 @@

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.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;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
Expand All @@ -48,44 +42,35 @@
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) {

UsersApi usersApi = ldapUsersApi(accountDao, roleDao);
return new LdapAccountsManager(eventPublisher::publishEvent, accountDao, roleDao, orgsDao, usersApi);
AccountDao accountDao, //
RoleDao roleDao, //
OrgsDao orgsDao, //
DemultiplexingUsersApi demultiplexingUsersApi,
GeorchestraGatewaySecurityConfigProperties configProperties) {

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

@Bean
CreateAccountUserCustomizer createAccountUserCustomizer(AccountManager accountManager) {
return new CreateAccountUserCustomizer(accountManager);
}

private UsersApi ldapUsersApi(AccountDao accountDao, RoleDao roleDao) {
UserMapperImpl mapper = new UserMapperImpl();
mapper.setRoleDao(roleDao);
List<String> 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) {
LdapContextSource singleContextSource(GeorchestraGatewaySecurityConfigProperties 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());
singleContextSource.setUserDn(ldapConfig.getAdminDn().orElseThrow());
singleContextSource.setPassword(ldapConfig.getAdminPassword().orElseThrow());
return singleContextSource;
}

Expand All @@ -105,20 +90,19 @@ PoolingContextSource contextSource(LdapContextSource singleContextSource) {

@Bean
LdapTemplate ldapTemplate(PoolingContextSource contextSource) throws Exception {
LdapTemplate ldapTemplate = new LdapTemplate(contextSource);
return ldapTemplate;
return new LdapTemplate(contextSource);
}

@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 +113,7 @@ OrgsDao orgsDao(LdapTemplate ldapTemplate, LdapConfigProperties config) {
}

@Bean
AccountDao accountDao(LdapTemplate ldapTemplate, LdapConfigProperties config) throws Exception {
AccountDao accountDao(LdapTemplate ldapTemplate, GeorchestraGatewaySecurityConfigProperties config) {
ExtendedLdapConfig ldapConfig = config.extendedEnabled().get(0);
String baseDn = ldapConfig.getBaseDn();
String userSearchBaseDN = ldapConfig.getUsersRdn();
Expand All @@ -144,9 +128,7 @@ AccountDao accountDao(LdapTemplate ldapTemplate, LdapConfigProperties config) th
impl.setBasePath(baseDn);
impl.setUserSearchBaseDN(userSearchBaseDN);
impl.setRoleSearchBaseDN(roleSearchBaseDN);
if (pendingUsersSearchBaseDN != null) {
impl.setPendingUserSearchBaseDN(pendingUsersSearchBaseDN);
}
impl.setPendingUserSearchBaseDN(pendingUsersSearchBaseDN);

String orgSearchBaseDN = ldapConfig.getOrgsRdn();
requireNonNull(orgSearchBaseDN);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
*/
package org.georchestra.gateway.accounts.admin.ldap;

import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
Expand All @@ -38,11 +37,11 @@
import org.georchestra.ds.users.DuplicatedUidException;
import org.georchestra.gateway.accounts.admin.AbstractAccountsManager;
import org.georchestra.gateway.accounts.admin.AccountManager;
import org.georchestra.gateway.security.GeorchestraGatewaySecurityConfigProperties;
import org.georchestra.gateway.security.exceptions.DuplicatedEmailFoundException;
import org.georchestra.gateway.security.exceptions.DuplicatedUsernameFoundException;
import org.georchestra.security.api.UsersApi;
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;

Expand All @@ -57,30 +56,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 All @@ -103,7 +103,14 @@ protected void createInternal(GeorchestraUser mapped) throws DuplicatedEmailFoun
throw new DuplicatedUsernameFoundException(accountError.getMessage());
}

ensureOrgExists(newAccount);
try {
ensureOrgExists(newAccount);
} catch (IllegalStateException orgError) {
log.error("Error when trying to create / update the organisation {}, reverting the account creation",
newAccount.getOrg(), orgError);
rollbackAccount(newAccount, newAccount.getOrg());
throw orgError;
}

ensureRolesExist(mapped, newAccount);
}
Expand Down Expand Up @@ -156,48 +163,65 @@ 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);
}
return newAccount;
}

/**
* @throws IllegalStateException if the org can't be created/updated
*/
private void ensureOrgExists(@NonNull Account newAccount) {
String orgId = newAccount.getOrg();
if (StringUtils.isEmpty(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<String> 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.setShortName(orgId);
org.setOrgType("Other");
org.setMembers(Arrays.asList(newAccount.getUid()));
orgsDao.insert(org);
}
final String orgId = newAccount.getOrg();
if (!StringUtils.isEmpty(orgId)) {
findOrg(orgId).ifPresentOrElse(org -> addAccountToOrg(newAccount, org),
() -> createOrgAndAddAccount(newAccount, orgId));
}
}

private void createOrgAndAddAccount(Account newAccount, final String orgId) {
try {
log.info("Org {} does not exist, trying to create it", orgId);
Org org = newOrg(orgId);
org.getMembers().add(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);
}
}

private void addAccountToOrg(Account newAccount, Org org) {
// org already in the LDAP, add the newly created account to it
org.getMembers().add(newAccount.getUid());
orgsDao.update(org);
}

private Optional<Org> findOrg(String orgId) {
try {
return Optional.of(orgsDao.findByCommonName(orgId));
} catch (NameNotFoundException e) {
return Optional.empty();
}
}

private void rollbackAccount(Account newAccount, final String orgId) {
try {// roll-back account
accountDao.delete(newAccount);
} catch (NameNotFoundException | DataServiceException rollbackError) {
log.warn("Error reverting user creation after orgsDao update failure", rollbackError);
}
}

private Org newOrg(final String orgId) {
Org org = new Org();
org.setId(orgId);
org.setName(orgId);
org.setShortName(orgId);
org.setOrgType("Other");
return org;
}
}
Loading

0 comments on commit 204f91a

Please sign in to comment.