ldap
Differences
This shows you the differences between two versions of the page.
Both sides previous revisionPrevious revisionNext revision | Previous revision | ||
ldap [2017/10/10 08:56] – skipidar | ldap [2020/12/27 20:35] (current) – external edit 127.0.0.1 | ||
---|---|---|---|
Line 14: | Line 14: | ||
* Creating a Partition - http:// | * Creating a Partition - http:// | ||
+ | |||
+ | * Introduciton in schema definition - https:// | ||
===== LDAP Server ===== | ===== LDAP Server ===== | ||
Line 155: | Line 157: | ||
st State | st State | ||
c Country | c Country | ||
+ | dn distinguished name, like cn=sadique5, | ||
</ | </ | ||
Line 189: | Line 192: | ||
</ | </ | ||
+ | |||
+ | |||
+ | |||
+ | ===== OpenLDAP and phpldapadmin ===== | ||
+ | |||
+ | Starting both in docker: | ||
+ | < | ||
+ | docker network create ldapnetwork | ||
+ | |||
+ | sudo docker run --restart=always | ||
+ | |||
+ | sudo docker run --restart=always | ||
+ | </ | ||
+ | |||
+ | Navigate to http:// | ||
+ | |||
+ | 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:// | ||
+ | |||
+ | Examples: | ||
+ | * https:// | ||
+ | * https:// | ||
+ | |||
+ | |||
+ | |||
+ | ===== 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, | ||
+ | ## Public License. | ||
+ | ## | ||
+ | ## A copy of this license is available in the file LICENSE in the | ||
+ | ## top-level directory of the distribution or, alternatively, | ||
+ | ## < | ||
+ | # | ||
+ | # aaainetOrgPerson | ||
+ | # | ||
+ | # | ||
+ | # TODO: request and add unique Private Enterprise Number from IANA Services | ||
+ | # | ||
+ | |||
+ | attributetype ( 5.12.345.1.123456.3.1.321 | ||
+ | NAME ' | ||
+ | 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 ' | ||
+ | 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 ' | ||
+ | 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 ' | ||
+ | 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 ' | ||
+ | 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 ' | ||
+ | DESC 'an object which has a tag' | ||
+ | AUXILIARY | ||
+ | MAY ( x-company-tags ) ) | ||
+ | |||
+ | objectclass ( 5.12.345.1.123456.3.1.4 | ||
+ | NAME ' | ||
+ | DESC 'b360 customer' | ||
+ | AUXILIARY | ||
+ | MAY ( x-company-b360id ) ) | ||
+ | |||
+ | |||
+ | </ | ||
+ | |||
+ | Here is how to build an OpenLdap container with extended schema | ||
+ | |||
+ | < | ||
+ | FROM osixia/ | ||
+ | |||
+ | RUN mkdir -p / | ||
+ | COPY ./ldif/ / | ||
+ | |||
+ | RUN mkdir -p / | ||
+ | COPY ./ | ||
+ | |||
+ | COPY ./ | ||
+ | |||
+ | ENTRYPOINT start.sh && / | ||
+ | </ | ||
+ | |||
+ | |||
+ | |||
+ | ===== Pooling with Spring Boot ===== | ||
+ | |||
+ | The following code can be used, to activate Pooling (https:// | ||
+ | |||
+ | - 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()); | ||
+ | |||
+ | < | ||
+ | @PropertySource(" | ||
+ | @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(" | ||
+ | contextSource.setBase(env.getRequiredProperty(" | ||
+ | contextSource.setUserDn(env.getRequiredProperty(" | ||
+ | contextSource.setPassword(env.getRequiredProperty(" | ||
+ | contextSource.setPooled(false); | ||
+ | |||
+ | Map< | ||
+ | |||
+ | // DEBUGGING | ||
+ | environment.put(" | ||
+ | contextSource.setBaseEnvironmentProperties(environment); | ||
+ | |||
+ | return contextSource; | ||
+ | } | ||
+ | |||
+ | /** | ||
+ | * Configure the LDAP connection pool to | ||
+ | * validate connections https:// | ||
+ | * | ||
+ | * 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 ===== | ||
+ | |||
+ | <sxh java> | ||
+ | public abstract class BaseLdapService< | ||
+ | |||
+ | public static final String EMPTY_LDAP_ATTRIBUTE_PLACEHOLDER = " | ||
+ | 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(" | ||
+ | this.baseLdapPath = LdapUtils.newLdapName(baseDn); | ||
+ | } | ||
+ | |||
+ | public List< | ||
+ | return findByProperty(propertyName, | ||
+ | } | ||
+ | |||
+ | public List< | ||
+ | String propertyValueStr = null; | ||
+ | if (propertyValue != null) { | ||
+ | propertyValueStr = propertyName.toString(); | ||
+ | } | ||
+ | return findByProperty(propertyName, | ||
+ | } | ||
+ | |||
+ | public List< | ||
+ | return findByProperty(propertyName, | ||
+ | } | ||
+ | |||
+ | public List< | ||
+ | return findByProperty(propertyName, | ||
+ | } | ||
+ | |||
+ | |||
+ | public List< | ||
+ | |||
+ | // normalize dn | ||
+ | if (propertyName == " | ||
+ | // 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, | ||
+ | final LdapName relQueryBase = tools.toRelativeDn(queryBase); | ||
+ | query.base(relQueryBase); | ||
+ | } | ||
+ | |||
+ | List< | ||
+ | if (QueryOperation.LIKE.equals(operation)) { | ||
+ | results = ldapTemplate.find(query.where(propertyName).like(propertyValue), | ||
+ | |||
+ | } else if (QueryOperation.IS.equals(operation)) { | ||
+ | results = ldapTemplate.find(query.where(propertyName).is(propertyValue), | ||
+ | |||
+ | } else { | ||
+ | throw new RuntimeException(" | ||
+ | } | ||
+ | |||
+ | |||
+ | if (results != null) { | ||
+ | results = results.stream().map(t -> deriveTransientProperties(t)).collect(Collectors.toList()); | ||
+ | } | ||
+ | |||
+ | return results; | ||
+ | } | ||
+ | |||
+ | protected List< | ||
+ | List< | ||
+ | if (found != null) { | ||
+ | found = found.stream().map(t -> deriveTransientProperties(t)).collect(Collectors.toList()); | ||
+ | } | ||
+ | return found; | ||
+ | } | ||
+ | |||
+ | public List< | ||
+ | List< | ||
+ | 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, | ||
+ | } | ||
+ | |||
+ | |||
+ | public Optional< | ||
+ | LOG.info(" | ||
+ | if (ldapIdStr == null) { | ||
+ | return Optional.empty(); | ||
+ | } | ||
+ | final LdapName ldapId = LdapNameBuilder.newInstance(ldapIdStr).build(); | ||
+ | return this.findByDn(ldapId); | ||
+ | } | ||
+ | |||
+ | public Optional< | ||
+ | try { | ||
+ | return Optional.ofNullable(ldapTemplate.findOne(query, | ||
+ | .map(t -> deriveTransientProperties(t)); | ||
+ | } catch (Exception e) { | ||
+ | LOG.warn(" | ||
+ | return Optional.empty(); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | public Optional< | ||
+ | try { | ||
+ | final Name relLdapId = tools.toRelativeDn(ldapId); | ||
+ | final T result = ldapTemplate.findByDn(relLdapId, | ||
+ | return Optional.ofNullable(result) | ||
+ | .map(t -> deriveTransientProperties(t)); | ||
+ | } catch (Exception e) { | ||
+ | LOG.warn(" | ||
+ | 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(" | ||
+ | return; | ||
+ | } | ||
+ | |||
+ | ldapTemplate.unbind(relLdapId, | ||
+ | |||
+ | // remove the id from member-lists in groups | ||
+ | final List< | ||
+ | groups.forEach(group -> { | ||
+ | group.removeMember(ldapId); | ||
+ | groupService.save(group); | ||
+ | }); | ||
+ | |||
+ | // remove the id from member-lists in roles | ||
+ | final List< | ||
+ | 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 " | ||
+ | * | ||
+ | * @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 " | ||
+ | * | ||
+ | * @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, | ||
+ | } | ||
+ | |||
+ | /** | ||
+ | * Replaces a piece of the original name - by a given snippet. | ||
+ | * Start replacing at the given index. In ou=tenant1AdminGroup, | ||
+ | * <p> | ||
+ | * The length of the original - must be larger, than that of the snippet | ||
+ | * | ||
+ | * @param original | ||
+ | * @param origIndexStart | ||
+ | * @param replacementSnippet - the snippet to insert into the original | ||
+ | * @return | ||
+ | */ | ||
+ | private Name replaceBySnippetInName(LdapName original, int origIndexStart, | ||
+ | Assert.isTrue(original != null, "Name must be not null" | ||
+ | Assert.isTrue(replacementSnippet != null, "Name must be not null" | ||
+ | Assert.isTrue(original.size() >= replacementSnippet.size(), | ||
+ | |||
+ | |||
+ | if (!replacementSnippet.isEmpty()) { | ||
+ | final List< | ||
+ | final List< | ||
+ | |||
+ | final List< | ||
+ | Collections.reverse(rdnsOrigRev); | ||
+ | |||
+ | final List< | ||
+ | Collections.reverse(rdnsReplacementSnippetRev); | ||
+ | |||
+ | // cut away a piece of orig, to be replaced by snippet | ||
+ | final List< | ||
+ | final List< | ||
+ | |||
+ | // insert snippet | ||
+ | final List< | ||
+ | result.addAll(preOrigRndRev); | ||
+ | result.addAll(rdnsReplacementSnippetRev); | ||
+ | result.addAll(postOrigRndRev); | ||
+ | |||
+ | // switch interface | ||
+ | Collections.reverse(result); | ||
+ | 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, | ||
+ | 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, | ||
+ | return findByDn(name).isPresent(); | ||
+ | } | ||
+ | |||
+ | |||
+ | protected Optional< | ||
+ | return findOneByAttribute(ldapAttributeName, | ||
+ | } | ||
+ | |||
+ | /** | ||
+ | * Gets the 0 or 1 object by the given attribute. | ||
+ | * If more objects are found - there will be an exception. | ||
+ | * | ||
+ | * @param ldapAttributeName | ||
+ | * @param ldapAttributeValue - the attribute value | ||
+ | * @param clazz - the output type | ||
+ | * @return | ||
+ | */ | ||
+ | protected Optional< | ||
+ | try { | ||
+ | List< | ||
+ | if(objList != null) { | ||
+ | objList = objList.stream().map(t -> deriveTransientProperties(t)).collect(Collectors.toList()); | ||
+ | } | ||
+ | Assert.isTrue(objList.size() <= 1, String.format(" | ||
+ | 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< | ||
+ | if(list == null){ | ||
+ | return list; | ||
+ | } | ||
+ | final List< | ||
+ | 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< | ||
+ | |||
+ | |||
+ | protected void replaceIdInGroups(Name oldMemberId, | ||
+ | final Name newMemberAbsDn = tools.toAbsoluteDn(newMemberId); | ||
+ | final Name oldMemberAbsDn = tools.toAbsoluteDn(oldMemberId); | ||
+ | |||
+ | // update teh role membership | ||
+ | // update teh group membership | ||
+ | // groups | ||
+ | final Collection< | ||
+ | for (Group group : groups) { | ||
+ | group.removeMember(oldMemberAbsDn); | ||
+ | group.addMember(newMemberAbsDn); | ||
+ | groupService.save(group); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | protected void replaceIdInRoles(Name oldMemberId, | ||
+ | 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.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(), | ||
+ | |||
+ | } 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, | ||
+ | |||
+ | if (!idFromObjectProperties.toString().equals(oldId.toString())) { | ||
+ | // override the old id | ||
+ | objectWithId.setId(idFromObjectProperties); | ||
+ | // persist | ||
+ | update(oldId, | ||
+ | } else { | ||
+ | // persist | ||
+ | update(objectWithId); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | return objectWithId; | ||
+ | } | ||
+ | |||
+ | /** | ||
+ | * This filter is applied BEFORE BIND. | ||
+ | * Bind doe not except NULL arguments, opposed to " | ||
+ | * <p> | ||
+ | * In " | ||
+ | * In " | ||
+ | * | ||
+ | * @param buildAttributes | ||
+ | * @return | ||
+ | */ | ||
+ | protected Attributes filterNullOrEmptyAttributes(Attributes buildAttributes) { | ||
+ | if (!(buildAttributes instanceof BasicAttributes)){ | ||
+ | return buildAttributes; | ||
+ | } | ||
+ | final BasicAttributes result = (BasicAttributes) buildAttributes.clone(); | ||
+ | final Iterator<? | ||
+ | |||
+ | 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 | ||
+ | * @return the updated entry | ||
+ | */ | ||
+ | public final T update(final Name originalId, T modified) { | ||
+ | |||
+ | final Name newId = modified.getId(); | ||
+ | Assert.isTrue(newId != null, String.format(" | ||
+ | |||
+ | if (originalId != null && !originalId.equals(newId)) { | ||
+ | Assert.isTrue(exists(originalId), | ||
+ | |||
+ | // renaming the objects id | ||
+ | rename(originalId, | ||
+ | |||
+ | // now update the membership | ||
+ | updateMembershipInLdapObjectsOnIdChange(tools.toAbsoluteDn(originalId), | ||
+ | } | ||
+ | |||
+ | // 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), | ||
+ | if (!newId.equals(oldId)) { | ||
+ | ldapTemplate.rename(oldId, | ||
+ | } | ||
+ | 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<? | ||
+ | while (attributesIterator.hasNext()) { | ||
+ | final Attribute attribute = attributesIterator.next(); | ||
+ | ModificationItem item = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, | ||
+ | ldapTemplate.modifyAttributes(absLdapId, | ||
+ | } | ||
+ | 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, | ||
+ | |||
+ | /** | ||
+ | * 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 ==== | ||
+ | |||
+ | <sxh java> | ||
+ | |||
+ | @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(" | ||
+ | } | ||
+ | |||
+ | public static final <T> List< | ||
+ | return StreamSupport.stream(i.spliterator(), | ||
+ | } | ||
+ | |||
+ | public LdapName toAbsoluteDn(Name relativeName) { | ||
+ | // check if already absolute | ||
+ | if (relativeName.startsWith(baseLdapPath)) { | ||
+ | LOG.info(String.format(" | ||
+ | return LdapNameBuilder.newInstance(relativeName) | ||
+ | .build(); | ||
+ | } else { | ||
+ | return LdapNameBuilder.newInstance(baseLdapPath) | ||
+ | .add(relativeName) | ||
+ | .build(); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | public static Optional< | ||
+ | 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(" | ||
+ | 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< | ||
+ | final 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< | ||
+ | ldapName.getAll().asIterator().forEachRemaining(listLdapName:: | ||
+ | |||
+ | final List< | ||
+ | fragment.getAll().asIterator().forEachRemaining(listFragment:: | ||
+ | |||
+ | // is the fragment like " | ||
+ | return (Collections.indexOfSubList(listLdapName, | ||
+ | } | ||
+ | |||
+ | /** | ||
+ | * Assumes that tags are separated by empty spaces | ||
+ | * | ||
+ | * @param tags | ||
+ | * @return | ||
+ | */ | ||
+ | public List< | ||
+ | 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 " | ||
+ | * | ||
+ | * @param ldapName | ||
+ | * @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(), | ||
+ | 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(); | ||
+ | copyName.remove(0); | ||
+ | } | ||
+ | copyName.addAll(0, | ||
+ | return copyName; | ||
+ | |||
+ | } catch (InvalidNameException e) { | ||
+ | throw new RuntimeException(e); | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | |||
+ | </ | ||
ldap.1507625802.txt.gz · Last modified: (external edit)