Table of Contents

LDAP

LDAP ist ein Format zum Speichern von Daten in Verzeichnissen (!= Ordner. Verzeichnis wie z.B. Telefonverzeichnis, Katalog)

Hier eine tolle Einleitung:

LDAP Server

Implementierung : http://directory.apache.org/apacheds/

LDAP Client

Nice implementation: http://directory.apache.org/studio/




Standard structure

Standard structure. May look as following.

Under Organisational Units “groups” and “users” put the Posix-Groups and Posix-Users

Achtung: The default POSIX group “users” must be created first.
Because at least one Group's GID (group id) must be referenced during user creation.
After that you can create

Example configuration with Jenkins

version: 1

dn: ou=JenkinsAdmins,ou=groups,dc=otm,dc=intra
objectclass: groupOfNames
objectclass: top
cn: JenkinsAdmins
member: cn=admin,ou=users,dc=otm,dc=intra
member: cn=alexander.friesen,ou=users,dc=otm,dc=intra
member: cn=boris.jelzin,ou=users,dc=otm,dc=intra
ou: JenkinsAdmins

dn: cn=alexander.friesen,ou=users,dc=otm,dc=intra
objectClass: person
objectClass: top
cn: alexander.friesen
sn: alexander.friesen
userPassword:: e1NIQX13dVFVZXE0NjJuODJnbWJRYzZ6VXFSV01XZlU9

dn: ou=users,dc=otm,dc=intra
objectclass: organizationalUnit
objectclass: top
ou: users

dn: cn=stas.archontov,ou=users,dc=otm,dc=intra
objectclass: person
objectclass: top
cn: stas.archotov
cn: stas.archontov
sn: stas.archontov
userPassword:: e1NIQX13dVFVZXE0NjJuODJnbWJRYzZ6VXFSV01XZlU9

dn: cn=boris.jelzin,ou=users,dc=otm,dc=intra
objectclass: person
objectclass: top
cn: boris.jelzin
sn: boris.jelzin
userPassword:: e1NIQX13dVFVZXE0NjJuODJnbWJRYzZ6VXFSV01XZlU9

dn: ou=JenkinsHotSyncer,ou=groups,dc=otm,dc=intra
objectclass: groupOfNames
objectclass: top
cn: JenkinsHotSyncer
member: cn=admin,ou=users,dc=otm,dc=intra
member: cn=stas.archontov,ou=users,dc=otm,dc=intra
ou: JenkinsHotSyncer

dn: ou=JenkinsSyncer,ou=groups,dc=otm,dc=intra
objectclass: groupOfNames
objectclass: top
cn: JenkinsSyncer
member: cn=admin,ou=users,dc=otm,dc=intra
member: cn=stas.archontov,ou=users,dc=otm,dc=intra
ou: JenkinsSyncer

dn: cn=admin,ou=users,dc=otm,dc=intra
objectClass: person
objectClass: top
cn: admin
sn: admin
userPassword:: e1NIQX13dVFVZXE0NjJuODJnbWJRYzZ6VXFSV01XZlU9

dn: dc=otm,dc=intra
objectclass: top
objectclass: domain
dc: otm

dn: ou=groups,dc=otm,dc=intra
objectclass: organizationalUnit
objectclass: top
ou: groups

Configure Jenkins to use your LDAP server

Privilidges via ACLs - Access Control Lists

https://youtu.be/gNc3NcHOGC8?t=6m56s

Beispiel

Attributes

uid User id
cn Common Name
sn Surname
l Location
ou Organisational Unit
o Organisation
dc Domain Component
st State
c Country
dn distinguished name, like cn=sadique5,ou=people,dc=ldap,dc=example,dc=com

Search Scope

3 types of scope:

base limits to just the base object
onelevel limits to just the immediate children
sub search the entire subtree from base down

Filling LDAP manually without file from script

ldapadd -H "ldap://localhost" -c -x -D "cn=admin,dc=ldap,dc=example,dc=com" -w "Jpk66g63ZifGYIcShSGM" << EOF

dn: cn=sadique5,ou=people,dc=ldap,dc=example,dc=com
cn: sadique5
sn: sadique
uid: sadique
displayName: Sadique Puthen Peedikayil
givenName: Sadique
mail: sadique@vanillanetworks.com
mobile: 9895643639
homePhone: 0466-2254274
objectClass: inetOrgPerson
userPassword: Jpk66g63ZifGYIcShSGM

EOF

OpenLDAP and phpldapadmin

Starting both in docker:

docker network create ldapnetwork

sudo docker run --restart=always  -td --net ldapnetwork -h "opendj" --env ROOT_USER_DN="cn=Directory Manager" --env OPENDJ_USER="opendj" --env BASE_DN="dc=project,dc=intra" --env ROOT_PASSWORD="123abc" -p 1389:1389 -p 1636:1636 -p 3000:4444 --name opendj openidentityplatform/opendj

sudo docker run --restart=always  --env PHPLDAPADMIN_LDAP_HOSTS="#PYTHON2BASH:[{'opendj': [{'server': [{'tls': False}, {'port': 1389}]}, {'login': [{'bind_id': 'cn=Directory Manager'}, {'bind_pass': '123abc'}]}]}]"  --net ldapnetwork -p 4200:80 --env PHPLDAPADMIN_HTTPS=false  --detach --name php osixia/phpldapadmin:0.7.2 --loglevel debug

Navigate to http://localhost:4200

Login with

Login DN:
cn=Directory Manager

Pass:
123abc
display the configs
slapcat -b cn=config

Or within contianer

docker exec ldap slapcat -b cn=config

LDAP with Spring

Spring LDAP reference: https://docs.spring.io/autorepo/docs/spring-ldap/current/reference/#dns-as-attribute-values

Examples:

Schema extension

This is an example for an extended schema

stored as local.schema

# local.schema -- aaainetOrgPerson
# $OpenLDAP$
## This work is part of OpenLDAP Software <http://www.openldap.org/>.
##
## Copyright 1998-2014 The OpenLDAP Foundation.
## All rights reserved.
##
## Redistribution and use in source and binary forms, with or without
## modification, are permitted only as authorized by the OpenLDAP
## Public License.
##
## A copy of this license is available in the file LICENSE in the
## top-level directory of the distribution or, alternatively, at
## <http://www.OpenLDAP.org/license.html>.
#
# aaainetOrgPerson
#
#   Defines additional attributes required by the SSP
#   TODO: request and add unique Private Enterprise Number from IANA Services
#

attributetype ( 5.12.345.1.123456.3.1.321
	NAME 'x-company-auth0userId'
	DESC 'the id identifying ofthe auth0 user'
	EQUALITY caseIgnoreMatch
	SUBSTR caseIgnoreSubstringsMatch
	SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )

attributetype ( 5.12.345.1.123456.3.1.322
	NAME 'x-company-auth0clientId'
	DESC 'the clientId of the machine to machine user'
	EQUALITY caseIgnoreMatch
	SUBSTR caseIgnoreSubstringsMatch
	SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )

attributetype ( 5.12.345.1.123456.3.1.323
	NAME 'x-company-tags'
	DESC 'the tags field containing all the tags without empty spaces'
	EQUALITY caseIgnoreMatch
	SUBSTR caseIgnoreSubstringsMatch
	SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )

attributetype ( 5.12.345.1.123456.3.1.324
	NAME 'x-company-b360id'
	DESC 'the building 360 identifier of a customer'
	EQUALITY caseIgnoreMatch
	SUBSTR caseIgnoreSubstringsMatch
	SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )

objectclass	( 5.12.345.1.123456.3.1.2
    NAME 'x-company-auth0subject'
	DESC 'auth0 service registered person or machine'
	AUXILIARY
	MAY ( x-company-auth0userId $ x-company-auth0clientId ) )

objectclass	( 5.12.345.1.123456.3.1.3
    NAME 'x-company-hastags'
	DESC 'an object which has a tag'
	AUXILIARY
	MAY ( x-company-tags ) )

objectclass	( 5.12.345.1.123456.3.1.4
    NAME 'x-company-b360customer'
	DESC 'b360 customer'
	AUXILIARY
	MAY ( x-company-b360id ) )

Here is how to build an OpenLdap container with extended schema

FROM osixia/openldap:1.2.4

RUN mkdir -p /container/service/slapd/assets/config/bootstrap/ldif/custom
COPY ./ldif/ /container/service/slapd/assets/config/bootstrap/ldif/custom

RUN mkdir -p /container/service/slapd/assets/config/bootstrap/schema
COPY ./schema/local.schema /container/service/slapd/assets/config/bootstrap/schema/

COPY ./containerscripts/start.sh /usr/bin/start.sh

ENTRYPOINT start.sh && /container/tool/run

Pooling with Spring Boot

The following code can be used, to activate Pooling (https://docs.spring.io/spring-ldap/docs/1.3.2.RELEASE/reference/html/pooling.html) in a

- SpringBoot application - using Spring Data to talk to the Ldap server

The important part missing up there - was to pass the `PoolingContextSource` to the `LdapTemplate`

see `new LdapTemplate(poolingLdapContextSource());` in

@PropertySource("ldap-${spring.profiles.active}.properties")
@Configuration
public class LdapConfiguration {

  private static Logger LOG = LoggerFactory.getLogger(LdapConfiguration.class);

  @Autowired
  private Environment env;


  @Bean
  public LdapContextSource contextSource() {
    LdapContextSource contextSource = new LdapContextSource();
    contextSource.setUrl(env.getRequiredProperty("ldap.url"));
    contextSource.setBase(env.getRequiredProperty("ldap.base"));
    contextSource.setUserDn(env.getRequiredProperty("ldap.user"));
    contextSource.setPassword(env.getRequiredProperty("ldap.password"));
    contextSource.setPooled(false); // pooling is enabled on the wrapper

    Map<String, Object> environment = new HashMap<>();

    // DEBUGGING
    environment.put("com.sun.jndi.ldap.connect.pool.debug", "fine");    // many debug infos
    contextSource.setBaseEnvironmentProperties(environment);

    return contextSource;
  }

    /**
     * Configure the LDAP connection pool to
     * validate connections https://docs.spring.io/spring-ldap/docs/1.3.2.RELEASE/reference/html/pooling.html
     *
     * so that the connections do not expire in pool
     *
     * @return
     */
    @Bean
    public ContextSource poolingLdapContextSource() {
        PoolingContextSource poolingContextSource = new PoolingContextSource();
        poolingContextSource.setDirContextValidator(new DefaultDirContextValidator());
        poolingContextSource.setContextSource(contextSource());
        poolingContextSource.setTestOnBorrow(true);
        poolingContextSource.setTestWhileIdle(true);
        poolingContextSource.setTimeBetweenEvictionRunsMillis(60000);
        poolingContextSource.setNumTestsPerEvictionRun(3);

        TransactionAwareContextSourceProxy proxy = new TransactionAwareContextSourceProxy(poolingContextSource);
        return proxy;
    }

  @Bean
  public LdapTemplate ldapTemplate() {
    LdapTemplate ldapTemplate = new LdapTemplate(poolingLdapContextSource());
    ldapTemplate.setIgnoreNameNotFoundException(true);
    return ldapTemplate;
  }

}

BaseClass for Spring Data

public abstract class BaseLdapService<T extends WithLdapId> implements BaseLdapNameAware {

    public static final String EMPTY_LDAP_ATTRIBUTE_PLACEHOLDER = "empty";
    private static Logger LOG = LoggerFactory.getLogger(BaseLdapService.class);

    // Operations IS is the default one
    protected enum QueryOperation {
        IS, LIKE
    }

    @Autowired
    protected Tools tools;

    @Autowired
    private LdapTemplate ldapTemplate;

    @Autowired
    private RoleService roleService;

    @Autowired
    private GroupService groupService;

    @Autowired
    private Environment env;

    private LdapName baseLdapPath;

    @Override
    public void setBaseLdapPath(LdapName baseLdapPath) {
        this.baseLdapPath = baseLdapPath;
    }

    protected void initPostConstruct() {
        final String baseDn = env.getRequiredProperty("ldap.base");
        this.baseLdapPath = LdapUtils.newLdapName(baseDn);
    }

    public List<T> findByProperty(final String propertyName, Name propertyValue, Class<T> clazz) {
        return findByProperty(propertyName, propertyValue, clazz, null);
    }

    public List<T> findByProperty(final String propertyName, Name propertyValue, Class<T> clazz, Name queryBase) {
        String propertyValueStr = null;
        if (propertyValue != null) {
            propertyValueStr = propertyName.toString();
        }
        return findByProperty(propertyName, propertyValue.toString(), clazz);
    }

    public List<T> findByProperty(final String propertyName, String propertyValue, Class<T> clazz) {
        return findByProperty(propertyName, propertyValue, clazz, null);
    }

    public List<T> findByProperty(final String propertyName, String propertyValue, Class<T> clazz, Name queryBase) {
        return findByProperty(propertyName, propertyValue, clazz, queryBase, QueryOperation.IS);
    }


    public List<T> findByProperty(final String propertyName, String propertyValue, Class<T> clazz, Name queryBase, QueryOperation operation) {

        // normalize dn
        if (propertyName == "dn") {
            // for dn value - propertyValue msut be an LdapName
            final LdapName ldapName = LdapNameBuilder.newInstance()
                    .add(propertyValue)
                    .build();
            // enforce absolute names
            final LdapName absLdapId = tools.toRelativeDn(ldapName);

            // write back
            propertyValue = absLdapId.toString();
        }

        final LdapQueryBuilder query = query();
        if (queryBase != null) {
            // make sure the query base is relative (cut dc=digitaltwin,dc=intra) Otherwise you will get "Base context not found"
            final LdapName relQueryBase = tools.toRelativeDn(queryBase);
            query.base(relQueryBase);
        }

        List<T> results = Collections.emptyList();
        if (QueryOperation.LIKE.equals(operation)) {
            results = ldapTemplate.find(query.where(propertyName).like(propertyValue), clazz);

        } else if (QueryOperation.IS.equals(operation)) {
            results = ldapTemplate.find(query.where(propertyName).is(propertyValue), clazz);

        } else {
            throw new RuntimeException("unknown operation");
        }


        if (results != null) {
            results = results.stream().map(t -> deriveTransientProperties(t)).collect(Collectors.toList());
        }

        return results;
    }

    protected List<T> find(ContainerCriteria query) {
        List<T> found = ldapTemplate.find(query, getServiceRepoType());
        if (found != null) {
            found = found.stream().map(t -> deriveTransientProperties(t)).collect(Collectors.toList());
        }
        return found;
    }

    public List<T> findAll() {
        List<T> all = ldapTemplate.findAll(getServiceRepoType());
        if (all != null) {
            all = all.stream().map(t -> deriveTransientProperties(t)).collect(Collectors.toList());
        }
        return all;
    }

    public void bind(Name ldapId, Attributes attributes) {
        final LdapName absLdapId = tools.toRelativeDn(ldapId);
        ldapTemplate.bind(absLdapId, null, attributes);
    }


    public Optional<T> findByDn(String ldapIdStr) {
        LOG.info("findByDn " + ldapIdStr);
        if (ldapIdStr == null) {
            return Optional.empty();
        }
        final LdapName ldapId = LdapNameBuilder.newInstance(ldapIdStr).build();
        return this.findByDn(ldapId);
    }

    public Optional<T> findOne(LdapQuery query) {
        try {
            return Optional.ofNullable(ldapTemplate.findOne(query, getServiceRepoType()))
                    .map(t -> deriveTransientProperties(t));
        } catch (Exception e) {
            LOG.warn("Exception thrown in findOne", e.getMessage());
            return Optional.empty();
        }
    }

    public Optional<T> findByDn(Name ldapId) {
        try {
            final Name relLdapId = tools.toRelativeDn(ldapId); // require a relative ldap id to for searching, without the domain at the end
            final T result = ldapTemplate.findByDn(relLdapId, getServiceRepoType());
            return Optional.ofNullable(result)
                    .map(t -> deriveTransientProperties(t));
        } catch (Exception e) {
            LOG.warn("Exception thrown in findByDn", e.getMessage());
            return Optional.empty();
        }
    }

    public void delete(String ldapIdStr) {
        final LdapName ldapId = LdapNameBuilder.newInstance(ldapIdStr).build();
        final LdapName absLdapId = tools.toRelativeDn(ldapId);
        this.delete(absLdapId);
    }

    public void delete(Name ldapId) {
        final LdapName relLdapId = tools.toRelativeDn(ldapId);

        if (!exists(relLdapId)) {
            LOG.warn(String.format("Tried to remove the object %s, but it does not exist", relLdapId.toString()));
            return;
        }

        ldapTemplate.unbind(relLdapId, true);

        // remove the id from member-lists in groups
        final List<Group> groups = groupService.findGroupsByMember(ldapId);
        groups.forEach(group -> {
            group.removeMember(ldapId);
            groupService.save(group);
        });

        // remove the id from member-lists in roles
        final List<Role> roles = roleService.findRolesByMember(ldapId);
        roles.forEach(role -> {
            role.removeRoleOccupants(ldapId);
            roleService.save(role);
        });
    }


    /**
     * For the external use.
     * Checks, whether the given object exists.
     * Catches the exceptions, which flow, when the object - does not exist, returning false.
     *
     * @param object - the ldap entity
     * @return
     */
    public boolean exists(T object) {
        try {
            return existsCanThrow(object);
        } catch (org.springframework.ldap.NameNotFoundException | org.springframework.web.server.ResponseStatusException | java.util.NoSuchElementException | NullPointerException e) {
            return false;
        }
    }

    /**
     * Secure for checking if a user exists.
     * Will catch any "Entity not found" runtime exception
     *
     * @param name - the ldap name
     * @return
     */
    public boolean exists(Name name) {
        try {
            final LdapName absLdapId = tools.toRelativeDn(name);
            return existsCanThrow(absLdapId);
        } catch (org.springframework.ldap.NameNotFoundException | org.springframework.web.server.ResponseStatusException | java.util.NoSuchElementException e) {
            return false;
        }
    }

    /**
     * Secure for checking if a user exists.
     * Will catch any "Entity not found" runtime exception
     *
     * @param ldapName - the ldap name
     * @return
     */
    public boolean exists(String ldapName) {
        try {
            LdapName name = LdapNameBuilder.newInstance(ldapName).build();
            final LdapName absLdapId = tools.toRelativeDn(name);
            return exists(absLdapId);
        } catch (org.springframework.ldap.InvalidNameException e) {
            return false;
        }
    }


    protected Name replacePrefixInName(Name original, Name replacementSnippet) {
        LdapName copyOrig = LdapNameBuilder.newInstance(original).build();
        LdapName copySnippet = LdapNameBuilder.newInstance(replacementSnippet).build();

        return replaceBySnippetInName(copyOrig, 0, copySnippet);
    }

    /**
     * Replaces a piece of the original name - by a given snippet.
     * Start replacing at the given index. In ou=tenant1AdminGroup,ou=Groups,ou=tenant1,ou=Tenants - the index 0 value is "ou=tenant1AdminGroup"
     * <p>
     * The length of the original - must be larger, than that of the snippet
     *
     * @param original           - the name where to replace a piece by a snippet
     * @param origIndexStart     - the index where to start replacement
     * @param replacementSnippet - the snippet to insert into the original
     * @return
     */
    private Name replaceBySnippetInName(LdapName original, int origIndexStart, LdapName replacementSnippet) {
        Assert.isTrue(original != null, "Name must be not null");
        Assert.isTrue(replacementSnippet != null, "Name must be not null");
        Assert.isTrue(original.size() >= replacementSnippet.size(), "Name must be not null");


        if (!replacementSnippet.isEmpty()) {
            final List<Rdn> rdnsOrig = original.getRdns(); // [ou=Tenants;ou=tenant1; ou=Groups; ou=tenant1AdminGroup]
            final List<Rdn> rdnsReplacementSnippet = replacementSnippet.getRdns();  // [ou=Groups; ou=tenant1AdminGroupRENAMED; ]

            final List<Rdn> rdnsOrigRev = new ArrayList<>(rdnsOrig); // [ou=tenant1AdminGroup; ou=Groups; ou=tenant1; ou=Tenants ]
            Collections.reverse(rdnsOrigRev);

            final List<Rdn> rdnsReplacementSnippetRev = new ArrayList<>(rdnsReplacementSnippet);
            Collections.reverse(rdnsReplacementSnippetRev); // [ou=tenant1AdminGroupRENAMED; ou=Groups;]

            // cut away a piece of orig, to be replaced by snippet
            final List<Rdn> preOrigRndRev = new ArrayList<>(rdnsOrigRev.subList(0, origIndexStart));
            final List<Rdn> postOrigRndRev = new ArrayList<>(rdnsOrigRev.subList(origIndexStart + rdnsReplacementSnippet.size(), rdnsOrigRev.size()));

            // insert snippet
            final List<Rdn> result = new ArrayList<>();
            result.addAll(preOrigRndRev);
            result.addAll(rdnsReplacementSnippetRev);
            result.addAll(postOrigRndRev);

            // switch interface
            Collections.reverse(result); // reverse back prior to making a Name out of it
            final Name copyOriginalRev = LdapNameBuilder.newInstance().build().addAll(result);

            return LdapNameBuilder.newInstance(copyOriginalRev).build();
        }

        // copy original to return a copy - not the same object. To have a consistant behaviour on empty snippet.
        return LdapNameBuilder.newInstance(original).build();
    }

    /**
     * Checks, whether the given object exists
     *
     * @param t
     * @return
     */
    protected boolean existsCanThrow(T t) throws org.springframework.ldap.NameNotFoundException, org.springframework.web.server.ResponseStatusException {
        return t != null && true == existsCanThrow(t.getId());
    }

    /**
     * Checks, whether the given object exists
     *
     * @param name
     * @return
     */
    protected boolean existsCanThrow(Name name) throws org.springframework.ldap.NameNotFoundException, org.springframework.web.server.ResponseStatusException {
        return findByDn(name).isPresent();
    }


    protected Optional<T> findOneByAttribute(String ldapAttributeName, String ldapAttributeValue, Class<T> clazz) {
        return findOneByAttribute(ldapAttributeName, ldapAttributeValue, clazz, LdapUtils.emptyLdapName());
    }

    /**
     * Gets the 0 or 1 object by the given attribute.
     * If more objects are found - there will be an exception.
     *
     * @param ldapAttributeName  - the name of the attribute
     * @param ldapAttributeValue - the attribute value
     * @param clazz              - the output type
     * @return
     */
    protected Optional<T> findOneByAttribute(String ldapAttributeName, String ldapAttributeValue, Class<T> clazz, Name baseTenantRdn) {
        try {
            List<T> objList = ldapTemplate.find(query().base(baseTenantRdn).where(ldapAttributeName).is(ldapAttributeValue), clazz);
            if(objList != null) {
                objList = objList.stream().map(t -> deriveTransientProperties(t)).collect(Collectors.toList());
            }
            Assert.isTrue(objList.size() <= 1, String.format("Expect max one object attr %s and value %s. Found %s instead. %s", ldapAttributeName, ldapAttributeValue, objList.size(), objList));
            if (objList.isEmpty()) {
                return Optional.empty();
            }
            T obj = objList.get(0);
            return Optional.of(obj);

        } catch (org.springframework.ldap.NameNotFoundException | java.util.NoSuchElementException e) {
            return Optional.empty();
        }
    }


    /**
     * Derives properties in the object, before the object is returned
     *
     * @param object
     * @return
     */
    T deriveTransientProperties(T object) {
        // nothing on default
        return object;
    }

    List<T> deriveTransientProperties(List<T> list) {
        if(list == null){
            return list;
        }
        final List<T> result = list.stream().map(t -> deriveTransientProperties(t)).collect(Collectors.toList());
        return result;
    }

    /**
     * Catch the null value and fill in some string
     * if a value is not set/known
     * so that LDAP doesnt throw an error
     *
     * @param str - the string which may be null
     * @return - the str or some default placeholder string to be stored in LDAP
     */
    protected String nullToString(String str) {
        if (str == null) {
            return EMPTY_LDAP_ATTRIBUTE_PLACEHOLDER;
        }
        return str;
    }


    public LdapTemplate getLdapTemplate() {
        return ldapTemplate;
    }

    /**
     * Build the attributes, which will be used to save the LDAP entity.
     *
     * @param t
     * @return
     */
    protected abstract Attributes buildAttributes(T t);

    /**
     * The type of the T parameter, which will
     *
     * @return
     */
    protected abstract Class<T> getServiceRepoType();


    protected void replaceIdInGroups(Name oldMemberId, Name newMemberId) {
        final Name newMemberAbsDn = tools.toAbsoluteDn(newMemberId);
        final Name oldMemberAbsDn = tools.toAbsoluteDn(oldMemberId);

        // update teh role membership
        // update teh group membership
        // groups
        final Collection<Group> groups = groupService.findByMember(oldMemberAbsDn);
        for (Group group : groups) {
            group.removeMember(oldMemberAbsDn);
            group.addMember(newMemberAbsDn);
            groupService.save(group);
        }
    }

    protected void replaceIdInRoles(Name oldMemberId, Name newMemberId) {
        final Name newMemberAbsDn = tools.toAbsoluteDn(newMemberId);
        final Name oldMemberAbsDn = tools.toAbsoluteDn(oldMemberId);

        // roles
        // The user has moved - we need to update role references.
        List<Role> roles = roleService.findRolesByMember(oldMemberAbsDn);
        roles.parallelStream().forEach(role -> {
            role.removeRoleOccupants(oldMemberAbsDn);
            role.addRoleOccupant(newMemberAbsDn);
            roleService.save(role);
        });
    }


//
//    /**
//     * Should save the object, if necessary - give it a new id.
//     * @param modifiedOrNew
//     * @return
//     */
//    public abstract T save(T modifiedOrNew);

    /**
     * Save a one. Create if new. Update if exists. Generate a new id, if necessary.
     *
     * @param objectWithId
     * @return
     */
    public final T save(T objectWithId) {
        validate(objectWithId);

        // is it a new role without an id? Try to derive the id from the tenant.
        if (!exists(objectWithId)) {

            Name objectId = objectWithId.getId();

            // init the id, if necessary
            if (objectId == null) {
                objectId = generateId(objectWithId);

                // write the id back to the object
                objectWithId.setId(objectId);
            }

            initNewObjectBeforeSaving(objectWithId);

            // store a new one
            final Attributes attributes = filterNullOrEmptyAttributes(buildAttributes(objectWithId));
            bind(objectWithId.getId(), attributes);

        } else {
            // check, if the id must be adopted on the changed properties (cn/ou/..)
            final Name oldId = LdapNameBuilder.newInstance(objectWithId.getId()).build();
            final Name idFromObjectProperties = applyAttributesToId(oldId, objectWithId);

            if (!idFromObjectProperties.toString().equals(oldId.toString())) {
                // override the old id
                objectWithId.setId(idFromObjectProperties);
                // persist
                update(oldId, objectWithId);
            } else {
                // persist
                update(objectWithId);
            }
        }

        return objectWithId;
    }

    /**
     * This filter is applied BEFORE BIND.
     * Bind doe not except NULL arguments, opposed to "update".
     * <p>
     * In "update" setting an argument to null is for removing it.
     * In "bind" setting an argument to null causes a  org.springframework.ldap.InvalidAttributeValueException
     *
     * @param buildAttributes
     * @return
     */
    protected Attributes filterNullOrEmptyAttributes(Attributes buildAttributes) {
        if (!(buildAttributes instanceof BasicAttributes)){
            return buildAttributes;
        }
        final BasicAttributes result = (BasicAttributes) buildAttributes.clone();
        final Iterator<? extends Attribute> attributesIterator = buildAttributes.getAll().asIterator();

        while (attributesIterator.hasNext()) {
            final Attribute attribute = attributesIterator.next();

            boolean invalidAttribute = true;
            try {
                invalidAttribute = (attribute.get() == null); // not null

            } catch (java.util.NoSuchElementException e) {
                invalidAttribute = false; // invalid, because the value was empty

            } catch (NamingException e) {
                // rethrow, because the error was in the wrong name of the attribure - user should know
                throw new RuntimeException(e);
            }

            if (invalidAttribute) {
                result.remove(attribute.getID());
            }
        }
        return result;
    }

    /**
     * Apply the attributes in the new object - to the id.
     * Here you get the chance to adopt the LDAP id - if the properties have changed.
     *
     * @param oldId
     * @param objectWithId
     * @return
     */
    protected abstract Name applyAttributesToId(Name oldId, T objectWithId);


    /**
     * The object must exist already.
     * Save, when the id has changed.
     * May fall back to the creation of a new one, if no explicit id is provided.
     * Will clean up the old object, if the
     *
     * @param originalId the existing id of an existing role.
     * @param modified   the role, populated with new data. May contain a new id.
     * @return the updated entry
     */
    public final T update(final Name originalId, T modified) {

        final Name newId = modified.getId();
        Assert.isTrue(newId != null, String.format("The new object %s is expected to have a new id in it.", modified));

        if (originalId != null && !originalId.equals(newId)) {
            Assert.isTrue(exists(originalId), String.format("The object with id %s is expected to exist already, as this method sujest renaming of ids.", originalId));

            // renaming the objects id
            rename(originalId, newId);

            // now update the membership
            updateMembershipInLdapObjectsOnIdChange(tools.toAbsoluteDn(originalId), tools.toAbsoluteDn(newId));
        }

        // persist
        update(modified);

        return modified;
    }

    public T rename(Name oldId, Name newId) {
        Assert.isTrue(newId != null, "Can not rename to null");
        Assert.isTrue(exists(oldId), String.format("The object with the id %s does not exist. Cant rename it.", oldId));
        if (!newId.equals(oldId)) {
            ldapTemplate.rename(oldId, newId);
        }
        return findByDn(newId).orElseThrow();
    }

    /**
     * The object must exist already.
     *
     * @param t
     * @return
     */
    protected T update(T t) {
        final Name anyDn = t.getId();
        final LdapName absLdapId = tools.toRelativeDn(anyDn);

        final Iterator<? extends Attribute> attributesIterator = buildAttributes(t).getAll().asIterator();
        while (attributesIterator.hasNext()) {
            final Attribute attribute = attributesIterator.next();
            ModificationItem item = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, attribute);
            ldapTemplate.modifyAttributes(absLdapId, new ModificationItem[]{item});
        }
        return t;
    }

    protected void initNewObjectBeforeSaving(T objectWithId) {
        // nothing on default
    }


    /**
     * Generates a new id.
     *
     * @param objectWithId - the objectWithId used as data container. Probably will not have an id yet but all the information to generate one.
     * @return
     */
    public abstract Name generateId(T objectWithId);

    /**
     * Update the membership in the ldap objects on id change.
     * Implementation depends on the type of the objects.
     * Users will have to update membership in Groups, roles.
     * Groups will have to update membership in roles only etc.
     *
     * @param oldMemberAbsDn
     * @param newMemberAbsDn
     */
    protected abstract void updateMembershipInLdapObjectsOnIdChange(LdapName oldMemberAbsDn, LdapName newMemberAbsDn);

    /**
     * Validate a modified object here. E.g. when it is updated.
     * Especially check, if the cn/ou or other field participating in the id - must be aligned with the current state of the object
     *
     * @param modifiedObject
     * @throws IllegalArgumentException - when the validation fails. User the Asser.* style for the checks and catch them in the RestController
     */
    protected void validate(T modifiedObject) throws IllegalArgumentException {
        // no default validation
    }

}

LDAP tools


@Component
public class Tools {

    private static Logger LOG = LoggerFactory.getLogger(Tools.class);

    private final LdapName baseLdapPath;

    @Autowired
    public Tools(Environment env) {
        this.baseLdapPath = LdapUtils.newLdapName(env.getRequiredProperty("ldap.base"));
    }

    public static final <T> List<T> toList(Iterable<T> i) {
        return StreamSupport.stream(i.spliterator(), true).collect(Collectors.toList());
    }

    public LdapName toAbsoluteDn(Name relativeName) {
        // check if already absolute
        if (relativeName.startsWith(baseLdapPath)) {
            LOG.info(String.format("The dn %s already contains the baseLdapPath %s so skip reappending it", relativeName, baseLdapPath));
            return LdapNameBuilder.newInstance(relativeName)
                    .build();
        } else {
            return LdapNameBuilder.newInstance(baseLdapPath)
                    .add(relativeName)
                    .build();
        }
    }

    public static Optional<LdapName> toLdapId(String str) {
        try {
            return Optional.of(LdapNameBuilder.newInstance(str).build());
        } catch (NullPointerException | org.springframework.ldap.InvalidNameException e) {
            return Optional.empty();
        }
    }

    public static boolean isLdapId(String str) {
        try {
            LdapName name = LdapNameBuilder.newInstance(str).build();
            return true;
        } catch (NullPointerException | org.springframework.ldap.InvalidNameException e) {
            return false;
        }
    }

    public LdapName toRelativeDn(Name absoluteName) {
        Name name = absoluteName;
        if (absoluteName.startsWith(baseLdapPath)) {
            name = absoluteName.getSuffix(baseLdapPath.size());
        }
        LOG.info(String.format("The dn %s doesnt contains the baseLdapPath %s so skip reappending it", absoluteName, baseLdapPath));
        return LdapNameBuilder.newInstance(name).build();
    }


    /**
     * The relative DN is typically what is needed to perform lookups and searches in
     * the LDAP tree, whereas the absolute DN is needed when authenticating and when
     * an LDAP entry is referred to in e.g. a group. This wrapper class contains
     * both of these representations.
     * <p>
     * See LdapEntryIdentification.class comment
     *
     * @param ldapIds
     * @return
     */
    public List<Name> toAbsLdapIds(Collection<Name> ldapIds) {
        final ArrayList<Name> list = new ArrayList<>();
        ldapIds.forEach(ldapId -> {
            final LdapName absLdapId = toAbsoluteDn(ldapId);
            list.add(absLdapId);
        });
        return list;
    }


    public boolean ldapNameContainsSegment(final Name ldapName, final Name fragment) {
        if (ldapName == null || fragment == null || ldapName.size() < fragment.size()) {
            return false;
        }

        // Create an empty list
        final List<String> listLdapName = new ArrayList<>();
        ldapName.getAll().asIterator().forEachRemaining(listLdapName::add);

        final List<String> listFragment = new ArrayList<>();
        fragment.getAll().asIterator().forEachRemaining(listFragment::add);

        // is the fragment like "ou=People" present on the path?
        return (Collections.indexOfSubList(listLdapName, listFragment) != -1);
    }

    /**
     * Assumes that tags are separated by empty spaces
     *
     * @param tags
     * @return
     */
    public List<String> toTagsList(final String tags) {
        if (tags == null ) return null;
        if (tags.isEmpty() ) return Collections.EMPTY_LIST;
        final String[] tagsList = tags.trim().split("[ ]+");
        return Arrays.asList(tagsList);
    }





    /**
     * Takes two ldap names and replaces the suffix
     * by cutting off the length of suffix from the ldapName and appending the suffix
     *
     * The suffix will be appended on the right side of String.
     * Eg in "cn=dudidum,ou=People,ou=BIGFISH,ou=Tenants" the suffix with size 2 is "ou=BIGFISH,ou=Tenants"
     *
     * @param ldapName  - name, the suffix of which sould be replaced
     * @param newSuffix - suffix to append.
     * @return name with new suffix
     */
    public LdapName replaceSuffix(final LdapName ldapName, final LdapName newSuffix) {
        Assert.isTrue(ldapName.size() >= newSuffix.size(), String.format("The length of the ldapName %s must be bigger, than that of suffix %s", ldapName, newSuffix));
        if (newSuffix.size() == 0) return ldapName;

        try {
            // cut off the the initial suffix, prepare merge with new suffix
            final LdapName copyName = LdapUtils.newLdapName(ldapName);
            for (int i = 0; i < newSuffix.size(); i++) {
                copyName.remove(0);
            }
            copyName.addAll(0, newSuffix);
            return copyName;

        } catch (InvalidNameException e) {
            throw new RuntimeException(e);
        }
    }
}