LDAP ist ein Format zum Speichern von Daten in Verzeichnissen (!= Ordner. Verzeichnis wie z.B. Telefonverzeichnis, Katalog)
Hier eine tolle Einleitung:
Implementierung : http://directory.apache.org/apacheds/
Nice implementation: http://directory.apache.org/studio/
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
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
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
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
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
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
slapcat -b cn=config
Or within contianer
docker exec ldap slapcat -b cn=config
Spring LDAP reference: https://docs.spring.io/autorepo/docs/spring-ldap/current/reference/#dns-as-attribute-values
Examples:
This is an example for an extended schema
stored as local.schema
# local.schema -- aaainetOrgPerson
# $OpenLDAP$
## This work is part of OpenLDAP Software <http://www.openldap.org/>.
##
## Copyright 1998-2014 The OpenLDAP Foundation.
## All rights reserved.
##
## Redistribution and use in source and binary forms, with or without
## modification, are permitted only as authorized by the OpenLDAP
## Public License.
##
## A copy of this license is available in the file LICENSE in the
## top-level directory of the distribution or, alternatively, at
## <http://www.OpenLDAP.org/license.html>.
#
# aaainetOrgPerson
#
# Defines additional attributes required by the SSP
# TODO: request and add unique Private Enterprise Number from IANA Services
#
attributetype ( 5.12.345.1.123456.3.1.321
NAME 'x-company-auth0userId'
DESC 'the id identifying ofthe auth0 user'
EQUALITY caseIgnoreMatch
SUBSTR caseIgnoreSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )
attributetype ( 5.12.345.1.123456.3.1.322
NAME 'x-company-auth0clientId'
DESC 'the clientId of the machine to machine user'
EQUALITY caseIgnoreMatch
SUBSTR caseIgnoreSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )
attributetype ( 5.12.345.1.123456.3.1.323
NAME 'x-company-tags'
DESC 'the tags field containing all the tags without empty spaces'
EQUALITY caseIgnoreMatch
SUBSTR caseIgnoreSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )
attributetype ( 5.12.345.1.123456.3.1.324
NAME 'x-company-b360id'
DESC 'the building 360 identifier of a customer'
EQUALITY caseIgnoreMatch
SUBSTR caseIgnoreSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )
objectclass ( 5.12.345.1.123456.3.1.2
NAME 'x-company-auth0subject'
DESC 'auth0 service registered person or machine'
AUXILIARY
MAY ( x-company-auth0userId $ x-company-auth0clientId ) )
objectclass ( 5.12.345.1.123456.3.1.3
NAME 'x-company-hastags'
DESC 'an object which has a tag'
AUXILIARY
MAY ( x-company-tags ) )
objectclass ( 5.12.345.1.123456.3.1.4
NAME 'x-company-b360customer'
DESC 'b360 customer'
AUXILIARY
MAY ( x-company-b360id ) )
Here is how to build an OpenLdap container with extended schema
FROM osixia/openldap:1.2.4 RUN mkdir -p /container/service/slapd/assets/config/bootstrap/ldif/custom COPY ./ldif/ /container/service/slapd/assets/config/bootstrap/ldif/custom RUN mkdir -p /container/service/slapd/assets/config/bootstrap/schema COPY ./schema/local.schema /container/service/slapd/assets/config/bootstrap/schema/ COPY ./containerscripts/start.sh /usr/bin/start.sh ENTRYPOINT start.sh && /container/tool/run
The following code can be used, to activate Pooling (https://docs.spring.io/spring-ldap/docs/1.3.2.RELEASE/reference/html/pooling.html) in a
- SpringBoot application - using Spring Data to talk to the Ldap server
The important part missing up there - was to pass the `PoolingContextSource` to the `LdapTemplate`
see `new LdapTemplate(poolingLdapContextSource());` in
@PropertySource("ldap-${spring.profiles.active}.properties")
@Configuration
public class LdapConfiguration {
private static Logger LOG = LoggerFactory.getLogger(LdapConfiguration.class);
@Autowired
private Environment env;
@Bean
public LdapContextSource contextSource() {
LdapContextSource contextSource = new LdapContextSource();
contextSource.setUrl(env.getRequiredProperty("ldap.url"));
contextSource.setBase(env.getRequiredProperty("ldap.base"));
contextSource.setUserDn(env.getRequiredProperty("ldap.user"));
contextSource.setPassword(env.getRequiredProperty("ldap.password"));
contextSource.setPooled(false); // pooling is enabled on the wrapper
Map<String, Object> environment = new HashMap<>();
// DEBUGGING
environment.put("com.sun.jndi.ldap.connect.pool.debug", "fine"); // many debug infos
contextSource.setBaseEnvironmentProperties(environment);
return contextSource;
}
/**
* Configure the LDAP connection pool to
* validate connections https://docs.spring.io/spring-ldap/docs/1.3.2.RELEASE/reference/html/pooling.html
*
* so that the connections do not expire in pool
*
* @return
*/
@Bean
public ContextSource poolingLdapContextSource() {
PoolingContextSource poolingContextSource = new PoolingContextSource();
poolingContextSource.setDirContextValidator(new DefaultDirContextValidator());
poolingContextSource.setContextSource(contextSource());
poolingContextSource.setTestOnBorrow(true);
poolingContextSource.setTestWhileIdle(true);
poolingContextSource.setTimeBetweenEvictionRunsMillis(60000);
poolingContextSource.setNumTestsPerEvictionRun(3);
TransactionAwareContextSourceProxy proxy = new TransactionAwareContextSourceProxy(poolingContextSource);
return proxy;
}
@Bean
public LdapTemplate ldapTemplate() {
LdapTemplate ldapTemplate = new LdapTemplate(poolingLdapContextSource());
ldapTemplate.setIgnoreNameNotFoundException(true);
return ldapTemplate;
}
}
public abstract class BaseLdapService<T extends WithLdapId> implements BaseLdapNameAware {
public static final String EMPTY_LDAP_ATTRIBUTE_PLACEHOLDER = "empty";
private static Logger LOG = LoggerFactory.getLogger(BaseLdapService.class);
// Operations IS is the default one
protected enum QueryOperation {
IS, LIKE
}
@Autowired
protected Tools tools;
@Autowired
private LdapTemplate ldapTemplate;
@Autowired
private RoleService roleService;
@Autowired
private GroupService groupService;
@Autowired
private Environment env;
private LdapName baseLdapPath;
@Override
public void setBaseLdapPath(LdapName baseLdapPath) {
this.baseLdapPath = baseLdapPath;
}
protected void initPostConstruct() {
final String baseDn = env.getRequiredProperty("ldap.base");
this.baseLdapPath = LdapUtils.newLdapName(baseDn);
}
public List<T> findByProperty(final String propertyName, Name propertyValue, Class<T> clazz) {
return findByProperty(propertyName, propertyValue, clazz, null);
}
public List<T> findByProperty(final String propertyName, Name propertyValue, Class<T> clazz, Name queryBase) {
String propertyValueStr = null;
if (propertyValue != null) {
propertyValueStr = propertyName.toString();
}
return findByProperty(propertyName, propertyValue.toString(), clazz);
}
public List<T> findByProperty(final String propertyName, String propertyValue, Class<T> clazz) {
return findByProperty(propertyName, propertyValue, clazz, null);
}
public List<T> findByProperty(final String propertyName, String propertyValue, Class<T> clazz, Name queryBase) {
return findByProperty(propertyName, propertyValue, clazz, queryBase, QueryOperation.IS);
}
public List<T> findByProperty(final String propertyName, String propertyValue, Class<T> clazz, Name queryBase, QueryOperation operation) {
// normalize dn
if (propertyName == "dn") {
// for dn value - propertyValue msut be an LdapName
final LdapName ldapName = LdapNameBuilder.newInstance()
.add(propertyValue)
.build();
// enforce absolute names
final LdapName absLdapId = tools.toRelativeDn(ldapName);
// write back
propertyValue = absLdapId.toString();
}
final LdapQueryBuilder query = query();
if (queryBase != null) {
// make sure the query base is relative (cut dc=digitaltwin,dc=intra) Otherwise you will get "Base context not found"
final LdapName relQueryBase = tools.toRelativeDn(queryBase);
query.base(relQueryBase);
}
List<T> results = Collections.emptyList();
if (QueryOperation.LIKE.equals(operation)) {
results = ldapTemplate.find(query.where(propertyName).like(propertyValue), clazz);
} else if (QueryOperation.IS.equals(operation)) {
results = ldapTemplate.find(query.where(propertyName).is(propertyValue), clazz);
} else {
throw new RuntimeException("unknown operation");
}
if (results != null) {
results = results.stream().map(t -> deriveTransientProperties(t)).collect(Collectors.toList());
}
return results;
}
protected List<T> find(ContainerCriteria query) {
List<T> found = ldapTemplate.find(query, getServiceRepoType());
if (found != null) {
found = found.stream().map(t -> deriveTransientProperties(t)).collect(Collectors.toList());
}
return found;
}
public List<T> findAll() {
List<T> all = ldapTemplate.findAll(getServiceRepoType());
if (all != null) {
all = all.stream().map(t -> deriveTransientProperties(t)).collect(Collectors.toList());
}
return all;
}
public void bind(Name ldapId, Attributes attributes) {
final LdapName absLdapId = tools.toRelativeDn(ldapId);
ldapTemplate.bind(absLdapId, null, attributes);
}
public Optional<T> findByDn(String ldapIdStr) {
LOG.info("findByDn " + ldapIdStr);
if (ldapIdStr == null) {
return Optional.empty();
}
final LdapName ldapId = LdapNameBuilder.newInstance(ldapIdStr).build();
return this.findByDn(ldapId);
}
public Optional<T> findOne(LdapQuery query) {
try {
return Optional.ofNullable(ldapTemplate.findOne(query, getServiceRepoType()))
.map(t -> deriveTransientProperties(t));
} catch (Exception e) {
LOG.warn("Exception thrown in findOne", e.getMessage());
return Optional.empty();
}
}
public Optional<T> findByDn(Name ldapId) {
try {
final Name relLdapId = tools.toRelativeDn(ldapId); // require a relative ldap id to for searching, without the domain at the end
final T result = ldapTemplate.findByDn(relLdapId, getServiceRepoType());
return Optional.ofNullable(result)
.map(t -> deriveTransientProperties(t));
} catch (Exception e) {
LOG.warn("Exception thrown in findByDn", e.getMessage());
return Optional.empty();
}
}
public void delete(String ldapIdStr) {
final LdapName ldapId = LdapNameBuilder.newInstance(ldapIdStr).build();
final LdapName absLdapId = tools.toRelativeDn(ldapId);
this.delete(absLdapId);
}
public void delete(Name ldapId) {
final LdapName relLdapId = tools.toRelativeDn(ldapId);
if (!exists(relLdapId)) {
LOG.warn(String.format("Tried to remove the object %s, but it does not exist", relLdapId.toString()));
return;
}
ldapTemplate.unbind(relLdapId, true);
// remove the id from member-lists in groups
final List<Group> groups = groupService.findGroupsByMember(ldapId);
groups.forEach(group -> {
group.removeMember(ldapId);
groupService.save(group);
});
// remove the id from member-lists in roles
final List<Role> roles = roleService.findRolesByMember(ldapId);
roles.forEach(role -> {
role.removeRoleOccupants(ldapId);
roleService.save(role);
});
}
/**
* For the external use.
* Checks, whether the given object exists.
* Catches the exceptions, which flow, when the object - does not exist, returning false.
*
* @param object - the ldap entity
* @return
*/
public boolean exists(T object) {
try {
return existsCanThrow(object);
} catch (org.springframework.ldap.NameNotFoundException | org.springframework.web.server.ResponseStatusException | java.util.NoSuchElementException | NullPointerException e) {
return false;
}
}
/**
* Secure for checking if a user exists.
* Will catch any "Entity not found" runtime exception
*
* @param name - the ldap name
* @return
*/
public boolean exists(Name name) {
try {
final LdapName absLdapId = tools.toRelativeDn(name);
return existsCanThrow(absLdapId);
} catch (org.springframework.ldap.NameNotFoundException | org.springframework.web.server.ResponseStatusException | java.util.NoSuchElementException e) {
return false;
}
}
/**
* Secure for checking if a user exists.
* Will catch any "Entity not found" runtime exception
*
* @param ldapName - the ldap name
* @return
*/
public boolean exists(String ldapName) {
try {
LdapName name = LdapNameBuilder.newInstance(ldapName).build();
final LdapName absLdapId = tools.toRelativeDn(name);
return exists(absLdapId);
} catch (org.springframework.ldap.InvalidNameException e) {
return false;
}
}
protected Name replacePrefixInName(Name original, Name replacementSnippet) {
LdapName copyOrig = LdapNameBuilder.newInstance(original).build();
LdapName copySnippet = LdapNameBuilder.newInstance(replacementSnippet).build();
return replaceBySnippetInName(copyOrig, 0, copySnippet);
}
/**
* Replaces a piece of the original name - by a given snippet.
* Start replacing at the given index. In ou=tenant1AdminGroup,ou=Groups,ou=tenant1,ou=Tenants - the index 0 value is "ou=tenant1AdminGroup"
* <p>
* The length of the original - must be larger, than that of the snippet
*
* @param original - the name where to replace a piece by a snippet
* @param origIndexStart - the index where to start replacement
* @param replacementSnippet - the snippet to insert into the original
* @return
*/
private Name replaceBySnippetInName(LdapName original, int origIndexStart, LdapName replacementSnippet) {
Assert.isTrue(original != null, "Name must be not null");
Assert.isTrue(replacementSnippet != null, "Name must be not null");
Assert.isTrue(original.size() >= replacementSnippet.size(), "Name must be not null");
if (!replacementSnippet.isEmpty()) {
final List<Rdn> rdnsOrig = original.getRdns(); // [ou=Tenants;ou=tenant1; ou=Groups; ou=tenant1AdminGroup]
final List<Rdn> rdnsReplacementSnippet = replacementSnippet.getRdns(); // [ou=Groups; ou=tenant1AdminGroupRENAMED; ]
final List<Rdn> rdnsOrigRev = new ArrayList<>(rdnsOrig); // [ou=tenant1AdminGroup; ou=Groups; ou=tenant1; ou=Tenants ]
Collections.reverse(rdnsOrigRev);
final List<Rdn> rdnsReplacementSnippetRev = new ArrayList<>(rdnsReplacementSnippet);
Collections.reverse(rdnsReplacementSnippetRev); // [ou=tenant1AdminGroupRENAMED; ou=Groups;]
// cut away a piece of orig, to be replaced by snippet
final List<Rdn> preOrigRndRev = new ArrayList<>(rdnsOrigRev.subList(0, origIndexStart));
final List<Rdn> postOrigRndRev = new ArrayList<>(rdnsOrigRev.subList(origIndexStart + rdnsReplacementSnippet.size(), rdnsOrigRev.size()));
// insert snippet
final List<Rdn> result = new ArrayList<>();
result.addAll(preOrigRndRev);
result.addAll(rdnsReplacementSnippetRev);
result.addAll(postOrigRndRev);
// switch interface
Collections.reverse(result); // reverse back prior to making a Name out of it
final Name copyOriginalRev = LdapNameBuilder.newInstance().build().addAll(result);
return LdapNameBuilder.newInstance(copyOriginalRev).build();
}
// copy original to return a copy - not the same object. To have a consistant behaviour on empty snippet.
return LdapNameBuilder.newInstance(original).build();
}
/**
* Checks, whether the given object exists
*
* @param t
* @return
*/
protected boolean existsCanThrow(T t) throws org.springframework.ldap.NameNotFoundException, org.springframework.web.server.ResponseStatusException {
return t != null && true == existsCanThrow(t.getId());
}
/**
* Checks, whether the given object exists
*
* @param name
* @return
*/
protected boolean existsCanThrow(Name name) throws org.springframework.ldap.NameNotFoundException, org.springframework.web.server.ResponseStatusException {
return findByDn(name).isPresent();
}
protected Optional<T> findOneByAttribute(String ldapAttributeName, String ldapAttributeValue, Class<T> clazz) {
return findOneByAttribute(ldapAttributeName, ldapAttributeValue, clazz, LdapUtils.emptyLdapName());
}
/**
* Gets the 0 or 1 object by the given attribute.
* If more objects are found - there will be an exception.
*
* @param ldapAttributeName - the name of the attribute
* @param ldapAttributeValue - the attribute value
* @param clazz - the output type
* @return
*/
protected Optional<T> findOneByAttribute(String ldapAttributeName, String ldapAttributeValue, Class<T> clazz, Name baseTenantRdn) {
try {
List<T> objList = ldapTemplate.find(query().base(baseTenantRdn).where(ldapAttributeName).is(ldapAttributeValue), clazz);
if(objList != null) {
objList = objList.stream().map(t -> deriveTransientProperties(t)).collect(Collectors.toList());
}
Assert.isTrue(objList.size() <= 1, String.format("Expect max one object attr %s and value %s. Found %s instead. %s", ldapAttributeName, ldapAttributeValue, objList.size(), objList));
if (objList.isEmpty()) {
return Optional.empty();
}
T obj = objList.get(0);
return Optional.of(obj);
} catch (org.springframework.ldap.NameNotFoundException | java.util.NoSuchElementException e) {
return Optional.empty();
}
}
/**
* Derives properties in the object, before the object is returned
*
* @param object
* @return
*/
T deriveTransientProperties(T object) {
// nothing on default
return object;
}
List<T> deriveTransientProperties(List<T> list) {
if(list == null){
return list;
}
final List<T> result = list.stream().map(t -> deriveTransientProperties(t)).collect(Collectors.toList());
return result;
}
/**
* Catch the null value and fill in some string
* if a value is not set/known
* so that LDAP doesnt throw an error
*
* @param str - the string which may be null
* @return - the str or some default placeholder string to be stored in LDAP
*/
protected String nullToString(String str) {
if (str == null) {
return EMPTY_LDAP_ATTRIBUTE_PLACEHOLDER;
}
return str;
}
public LdapTemplate getLdapTemplate() {
return ldapTemplate;
}
/**
* Build the attributes, which will be used to save the LDAP entity.
*
* @param t
* @return
*/
protected abstract Attributes buildAttributes(T t);
/**
* The type of the T parameter, which will
*
* @return
*/
protected abstract Class<T> getServiceRepoType();
protected void replaceIdInGroups(Name oldMemberId, Name newMemberId) {
final Name newMemberAbsDn = tools.toAbsoluteDn(newMemberId);
final Name oldMemberAbsDn = tools.toAbsoluteDn(oldMemberId);
// update teh role membership
// update teh group membership
// groups
final Collection<Group> groups = groupService.findByMember(oldMemberAbsDn);
for (Group group : groups) {
group.removeMember(oldMemberAbsDn);
group.addMember(newMemberAbsDn);
groupService.save(group);
}
}
protected void replaceIdInRoles(Name oldMemberId, Name newMemberId) {
final Name newMemberAbsDn = tools.toAbsoluteDn(newMemberId);
final Name oldMemberAbsDn = tools.toAbsoluteDn(oldMemberId);
// roles
// The user has moved - we need to update role references.
List<Role> roles = roleService.findRolesByMember(oldMemberAbsDn);
roles.parallelStream().forEach(role -> {
role.removeRoleOccupants(oldMemberAbsDn);
role.addRoleOccupant(newMemberAbsDn);
roleService.save(role);
});
}
//
// /**
// * Should save the object, if necessary - give it a new id.
// * @param modifiedOrNew
// * @return
// */
// public abstract T save(T modifiedOrNew);
/**
* Save a one. Create if new. Update if exists. Generate a new id, if necessary.
*
* @param objectWithId
* @return
*/
public final T save(T objectWithId) {
validate(objectWithId);
// is it a new role without an id? Try to derive the id from the tenant.
if (!exists(objectWithId)) {
Name objectId = objectWithId.getId();
// init the id, if necessary
if (objectId == null) {
objectId = generateId(objectWithId);
// write the id back to the object
objectWithId.setId(objectId);
}
initNewObjectBeforeSaving(objectWithId);
// store a new one
final Attributes attributes = filterNullOrEmptyAttributes(buildAttributes(objectWithId));
bind(objectWithId.getId(), attributes);
} else {
// check, if the id must be adopted on the changed properties (cn/ou/..)
final Name oldId = LdapNameBuilder.newInstance(objectWithId.getId()).build();
final Name idFromObjectProperties = applyAttributesToId(oldId, objectWithId);
if (!idFromObjectProperties.toString().equals(oldId.toString())) {
// override the old id
objectWithId.setId(idFromObjectProperties);
// persist
update(oldId, objectWithId);
} else {
// persist
update(objectWithId);
}
}
return objectWithId;
}
/**
* This filter is applied BEFORE BIND.
* Bind doe not except NULL arguments, opposed to "update".
* <p>
* In "update" setting an argument to null is for removing it.
* In "bind" setting an argument to null causes a org.springframework.ldap.InvalidAttributeValueException
*
* @param buildAttributes
* @return
*/
protected Attributes filterNullOrEmptyAttributes(Attributes buildAttributes) {
if (!(buildAttributes instanceof BasicAttributes)){
return buildAttributes;
}
final BasicAttributes result = (BasicAttributes) buildAttributes.clone();
final Iterator<? extends Attribute> attributesIterator = buildAttributes.getAll().asIterator();
while (attributesIterator.hasNext()) {
final Attribute attribute = attributesIterator.next();
boolean invalidAttribute = true;
try {
invalidAttribute = (attribute.get() == null); // not null
} catch (java.util.NoSuchElementException e) {
invalidAttribute = false; // invalid, because the value was empty
} catch (NamingException e) {
// rethrow, because the error was in the wrong name of the attribure - user should know
throw new RuntimeException(e);
}
if (invalidAttribute) {
result.remove(attribute.getID());
}
}
return result;
}
/**
* Apply the attributes in the new object - to the id.
* Here you get the chance to adopt the LDAP id - if the properties have changed.
*
* @param oldId
* @param objectWithId
* @return
*/
protected abstract Name applyAttributesToId(Name oldId, T objectWithId);
/**
* The object must exist already.
* Save, when the id has changed.
* May fall back to the creation of a new one, if no explicit id is provided.
* Will clean up the old object, if the
*
* @param originalId the existing id of an existing role.
* @param modified the role, populated with new data. May contain a new id.
* @return the updated entry
*/
public final T update(final Name originalId, T modified) {
final Name newId = modified.getId();
Assert.isTrue(newId != null, String.format("The new object %s is expected to have a new id in it.", modified));
if (originalId != null && !originalId.equals(newId)) {
Assert.isTrue(exists(originalId), String.format("The object with id %s is expected to exist already, as this method sujest renaming of ids.", originalId));
// renaming the objects id
rename(originalId, newId);
// now update the membership
updateMembershipInLdapObjectsOnIdChange(tools.toAbsoluteDn(originalId), tools.toAbsoluteDn(newId));
}
// persist
update(modified);
return modified;
}
public T rename(Name oldId, Name newId) {
Assert.isTrue(newId != null, "Can not rename to null");
Assert.isTrue(exists(oldId), String.format("The object with the id %s does not exist. Cant rename it.", oldId));
if (!newId.equals(oldId)) {
ldapTemplate.rename(oldId, newId);
}
return findByDn(newId).orElseThrow();
}
/**
* The object must exist already.
*
* @param t
* @return
*/
protected T update(T t) {
final Name anyDn = t.getId();
final LdapName absLdapId = tools.toRelativeDn(anyDn);
final Iterator<? extends Attribute> attributesIterator = buildAttributes(t).getAll().asIterator();
while (attributesIterator.hasNext()) {
final Attribute attribute = attributesIterator.next();
ModificationItem item = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, attribute);
ldapTemplate.modifyAttributes(absLdapId, new ModificationItem[]{item});
}
return t;
}
protected void initNewObjectBeforeSaving(T objectWithId) {
// nothing on default
}
/**
* Generates a new id.
*
* @param objectWithId - the objectWithId used as data container. Probably will not have an id yet but all the information to generate one.
* @return
*/
public abstract Name generateId(T objectWithId);
/**
* Update the membership in the ldap objects on id change.
* Implementation depends on the type of the objects.
* Users will have to update membership in Groups, roles.
* Groups will have to update membership in roles only etc.
*
* @param oldMemberAbsDn
* @param newMemberAbsDn
*/
protected abstract void updateMembershipInLdapObjectsOnIdChange(LdapName oldMemberAbsDn, LdapName newMemberAbsDn);
/**
* Validate a modified object here. E.g. when it is updated.
* Especially check, if the cn/ou or other field participating in the id - must be aligned with the current state of the object
*
* @param modifiedObject
* @throws IllegalArgumentException - when the validation fails. User the Asser.* style for the checks and catch them in the RestController
*/
protected void validate(T modifiedObject) throws IllegalArgumentException {
// no default validation
}
}
@Component
public class Tools {
private static Logger LOG = LoggerFactory.getLogger(Tools.class);
private final LdapName baseLdapPath;
@Autowired
public Tools(Environment env) {
this.baseLdapPath = LdapUtils.newLdapName(env.getRequiredProperty("ldap.base"));
}
public static final <T> List<T> toList(Iterable<T> i) {
return StreamSupport.stream(i.spliterator(), true).collect(Collectors.toList());
}
public LdapName toAbsoluteDn(Name relativeName) {
// check if already absolute
if (relativeName.startsWith(baseLdapPath)) {
LOG.info(String.format("The dn %s already contains the baseLdapPath %s so skip reappending it", relativeName, baseLdapPath));
return LdapNameBuilder.newInstance(relativeName)
.build();
} else {
return LdapNameBuilder.newInstance(baseLdapPath)
.add(relativeName)
.build();
}
}
public static Optional<LdapName> toLdapId(String str) {
try {
return Optional.of(LdapNameBuilder.newInstance(str).build());
} catch (NullPointerException | org.springframework.ldap.InvalidNameException e) {
return Optional.empty();
}
}
public static boolean isLdapId(String str) {
try {
LdapName name = LdapNameBuilder.newInstance(str).build();
return true;
} catch (NullPointerException | org.springframework.ldap.InvalidNameException e) {
return false;
}
}
public LdapName toRelativeDn(Name absoluteName) {
Name name = absoluteName;
if (absoluteName.startsWith(baseLdapPath)) {
name = absoluteName.getSuffix(baseLdapPath.size());
}
LOG.info(String.format("The dn %s doesnt contains the baseLdapPath %s so skip reappending it", absoluteName, baseLdapPath));
return LdapNameBuilder.newInstance(name).build();
}
/**
* The relative DN is typically what is needed to perform lookups and searches in
* the LDAP tree, whereas the absolute DN is needed when authenticating and when
* an LDAP entry is referred to in e.g. a group. This wrapper class contains
* both of these representations.
* <p>
* See LdapEntryIdentification.class comment
*
* @param ldapIds
* @return
*/
public List<Name> toAbsLdapIds(Collection<Name> ldapIds) {
final ArrayList<Name> list = new ArrayList<>();
ldapIds.forEach(ldapId -> {
final LdapName absLdapId = toAbsoluteDn(ldapId);
list.add(absLdapId);
});
return list;
}
public boolean ldapNameContainsSegment(final Name ldapName, final Name fragment) {
if (ldapName == null || fragment == null || ldapName.size() < fragment.size()) {
return false;
}
// Create an empty list
final List<String> listLdapName = new ArrayList<>();
ldapName.getAll().asIterator().forEachRemaining(listLdapName::add);
final List<String> listFragment = new ArrayList<>();
fragment.getAll().asIterator().forEachRemaining(listFragment::add);
// is the fragment like "ou=People" present on the path?
return (Collections.indexOfSubList(listLdapName, listFragment) != -1);
}
/**
* Assumes that tags are separated by empty spaces
*
* @param tags
* @return
*/
public List<String> toTagsList(final String tags) {
if (tags == null ) return null;
if (tags.isEmpty() ) return Collections.EMPTY_LIST;
final String[] tagsList = tags.trim().split("[ ]+");
return Arrays.asList(tagsList);
}
/**
* Takes two ldap names and replaces the suffix
* by cutting off the length of suffix from the ldapName and appending the suffix
*
* The suffix will be appended on the right side of String.
* Eg in "cn=dudidum,ou=People,ou=BIGFISH,ou=Tenants" the suffix with size 2 is "ou=BIGFISH,ou=Tenants"
*
* @param ldapName - name, the suffix of which sould be replaced
* @param newSuffix - suffix to append.
* @return name with new suffix
*/
public LdapName replaceSuffix(final LdapName ldapName, final LdapName newSuffix) {
Assert.isTrue(ldapName.size() >= newSuffix.size(), String.format("The length of the ldapName %s must be bigger, than that of suffix %s", ldapName, newSuffix));
if (newSuffix.size() == 0) return ldapName;
try {
// cut off the the initial suffix, prepare merge with new suffix
final LdapName copyName = LdapUtils.newLdapName(ldapName);
for (int i = 0; i < newSuffix.size(); i++) {
copyName.remove(0);
}
copyName.addAll(0, newSuffix);
return copyName;
} catch (InvalidNameException e) {
throw new RuntimeException(e);
}
}
}