ldap
Differences
This shows you the differences between two versions of the page.
| Both sides previous revisionPrevious revisionNext revision | Previous revision | ||
| ldap [2019/04/01 11:21] – [OpenLDAP and phpldapadmin] 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 213: | Line 216: | ||
| 123abc | 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.1554117693.txt.gz · Last modified: (external edit)
