===== LDAP ===== LDAP ist ein Format zum Speichern von Daten in Verzeichnissen (!= Ordner. Verzeichnis wie z.B. Telefonverzeichnis, Katalog) Hier eine tolle Einleitung: - http://www.effinger.org/blog/wp-content/uploads/2008/12/ldap-grundlagen.pdf - http://archive.oreilly.com/pub/a/perl/excerpts/system-admin-with-perl/ten-minute-ldap-utorial.html * Partitions - unter Root hängt eine Partition. z.B. **dc=otm,dc=intra** * AttributeType - beschreibt den Type eines Wertes * ObjectClass - definiert einen Satz von attributen. z.B. Class person hat name, surname, .. als attribute * Creating a Partition - http://directory.apache.org/apacheds/basic-ug/1.4.3-adding-partition.html * Introduciton in schema definition - https://www.zytrax.com/books//ldap/ch3/ ===== LDAP Server ===== Implementierung : http://directory.apache.org/apacheds/ {{http://i520.photobucket.com/albums/w327/schajtan/2017-04-14_04-07-16_zpsdaqotfa6.png}} ===== LDAP Client ===== Nice implementation: http://directory.apache.org/studio/ {{http://i520.photobucket.com/albums/w327/schajtan/2017-04-14_03-57-49_zpsgwjdi84q.png?400}} \\ {{http://i520.photobucket.com/albums/w327/schajtan/2017-04-14_04-02-26_zpswct7zby3.png?400}} \\ {{http://i520.photobucket.com/albums/w327/schajtan/2017-04-14_04-04-27_zps6g1k5ngl.png?400}} \\ {{http://i520.photobucket.com/albums/w327/schajtan/2017-04-14_04-06-17_zps1u0uimu8.png?400}} ===== 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 {{https://i.imgur.com/nN6AOLk.png}} ===== Example configuration with Jenkins ===== * Exported partitition otm.intra. * The groups are managed in a separate **ou**. * The passwords (**userPassword** attributeType) are not mandatory, but have to be added as a separate attribute. Their values may be stored as saulted hashes. 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 {{http://i520.photobucket.com/albums/w327/schajtan/2017-04-13_17-47-45_zpsqiq2rxwr.png}} ===== 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: * https://github.com/jopek/spring-boot-ldap-useradmin * https://github.com/ivangfr/springboot-ldap ===== 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 . ## ## 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 ## . # # 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 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 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 findByProperty(final String propertyName, Name propertyValue, Class clazz) { return findByProperty(propertyName, propertyValue, clazz, null); } public List findByProperty(final String propertyName, Name propertyValue, Class clazz, Name queryBase) { String propertyValueStr = null; if (propertyValue != null) { propertyValueStr = propertyName.toString(); } return findByProperty(propertyName, propertyValue.toString(), clazz); } public List findByProperty(final String propertyName, String propertyValue, Class clazz) { return findByProperty(propertyName, propertyValue, clazz, null); } public List findByProperty(final String propertyName, String propertyValue, Class clazz, Name queryBase) { return findByProperty(propertyName, propertyValue, clazz, queryBase, QueryOperation.IS); } public List findByProperty(final String propertyName, String propertyValue, Class 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 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 find(ContainerCriteria query) { List found = ldapTemplate.find(query, getServiceRepoType()); if (found != null) { found = found.stream().map(t -> deriveTransientProperties(t)).collect(Collectors.toList()); } return found; } public List findAll() { List 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 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 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 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 groups = groupService.findGroupsByMember(ldapId); groups.forEach(group -> { group.removeMember(ldapId); groupService.save(group); }); // remove the id from member-lists in roles final List 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" *

* 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 rdnsOrig = original.getRdns(); // [ou=Tenants;ou=tenant1; ou=Groups; ou=tenant1AdminGroup] final List rdnsReplacementSnippet = replacementSnippet.getRdns(); // [ou=Groups; ou=tenant1AdminGroupRENAMED; ] final List rdnsOrigRev = new ArrayList<>(rdnsOrig); // [ou=tenant1AdminGroup; ou=Groups; ou=tenant1; ou=Tenants ] Collections.reverse(rdnsOrigRev); final List rdnsReplacementSnippetRev = new ArrayList<>(rdnsReplacementSnippet); Collections.reverse(rdnsReplacementSnippetRev); // [ou=tenant1AdminGroupRENAMED; ou=Groups;] // cut away a piece of orig, to be replaced by snippet final List preOrigRndRev = new ArrayList<>(rdnsOrigRev.subList(0, origIndexStart)); final List postOrigRndRev = new ArrayList<>(rdnsOrigRev.subList(origIndexStart + rdnsReplacementSnippet.size(), rdnsOrigRev.size())); // insert snippet final List 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 findOneByAttribute(String ldapAttributeName, String ldapAttributeValue, Class 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 findOneByAttribute(String ldapAttributeName, String ldapAttributeValue, Class clazz, Name baseTenantRdn) { try { List 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 deriveTransientProperties(List list) { if(list == null){ return list; } final List 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 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 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 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". *

* 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 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 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 List toList(Iterable 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 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. *

* See LdapEntryIdentification.class comment * * @param ldapIds * @return */ public List toAbsLdapIds(Collection ldapIds) { final ArrayList 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 listLdapName = new ArrayList<>(); ldapName.getAll().asIterator().forEachRemaining(listLdapName::add); final List 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 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); } } }