/**
* <a href="http://www.openolat.org">
* OpenOLAT - Online Learning and Training</a><br>
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); <br>
* you may not use this file except in compliance with the License.<br>
* You may obtain a copy of the License at the
* <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
* <p>
* Unless required by applicable law or agreed to in writing,<br>
* software distributed under the License is distributed on an "AS IS" BASIS, <br>
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
* See the License for the specific language governing permissions and <br>
* limitations under the License.
* <p>
* Initial code contributed and copyrighted by<br>
* frentix GmbH, http://www.frentix.com
* <p>
*/
package org.olat.ldap.manager;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.SizeLimitExceededException;
import javax.naming.directory.Attribute;
import javax.naming.directory.DirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.Control;
import javax.naming.ldap.LdapContext;
import javax.naming.ldap.PagedResultsControl;
import javax.naming.ldap.PagedResultsResponseControl;
import net.fortuna.ical4j.util.TimeZones;
import org.olat.core.logging.OLog;
import org.olat.core.logging.Tracing;
import org.olat.core.util.StringHelper;
import org.olat.ldap.LDAPConstants;
import org.olat.ldap.LDAPLoginModule;
import org.olat.ldap.LDAPSyncConfiguration;
import org.olat.ldap.model.LDAPGroup;
import org.olat.ldap.model.LDAPUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
*
* Initial date: 24.11.2014<br>
* @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
*
*/
@Service
public class LDAPDAO {
private static final OLog log = Tracing.createLoggerFor(LDAPDAO.class);
private static final int PAGE_SIZE = 50;
private static final TimeZone UTC_TIME_ZONE;
private static final String PAGED_RESULT_CONTROL_OID = "1.2.840.113556.1.4.319";
static {
UTC_TIME_ZONE = TimeZone.getTimeZone(TimeZones.UTC_ID);
}
private boolean pagingSupportedAlreadyFound = false;
@Autowired
private LDAPLoginModule ldapLoginModule;
@Autowired
private LDAPSyncConfiguration syncConfiguration;
public List<LDAPGroup> searchGroups(LdapContext ctx, List<String> groupDNs) {
String filter = syncConfiguration.getLdapGroupFilter();
List<LDAPGroup> ldapGroups = new ArrayList<>();
String[] groupAttributes = new String[]{"cn", "member"};
for(String groupDN:groupDNs) {
LDAPGroupVisitor visitor = new LDAPGroupVisitor();
search(visitor, groupDN, filter, groupAttributes, ctx);
ldapGroups.addAll(visitor.getGroups());
}
return ldapGroups;
}
public void search(LDAPVisitor visitor, String ldapBase, String filter, String[] returningAttrs, LdapContext ctx) {
SearchControls ctls = new SearchControls();
ctls.setSearchScope(SearchControls.SUBTREE_SCOPE);
ctls.setReturningAttributes(returningAttrs);
ctls.setCountLimit(0); // set no limits
boolean paging = isPagedResultControlSupported(ctx);
int counter = 0;
try {
if(paging) {
byte[] cookie = null;
ctx.setRequestControls(new Control[] { new PagedResultsControl(PAGE_SIZE, Control.NONCRITICAL) });
do {
NamingEnumeration<SearchResult> enm = ctx.search(ldapBase, filter, ctls);
while (enm.hasMore()) {
visitor.visit(enm.next());
}
cookie = getCookie(ctx);
} while (cookie != null);
} else {
ctx.setRequestControls(null); // reset on failure, see FXOLAT-299
NamingEnumeration<SearchResult> enm = ctx.search(ldapBase, filter, ctls);
while (enm.hasMore()) {
visitor.visit(enm.next());
}
counter++;
}
} catch (SizeLimitExceededException e) {
log.error("SizeLimitExceededException after "
+ counter
+ " records when getting all users from LDAP, reconfigure your LDAP server, hints: http://www.ldapbrowser.com/forum/viewtopic.php?t=14", null);
} catch (NamingException e) {
log.error("NamingException when trying to search from LDAP using ldapBase::" + ldapBase + " on row::" + counter, e);
} catch (Exception e) {
log.error("Exception when trying to search from LDAP using ldapBase::" + ldapBase + " on row::" + counter, e);
}
log.debug("finished search for ldapBase:: " + ldapBase);
}
public void searchInLdap(LDAPVisitor visitor, String filter, String[] returningAttrs, LdapContext ctx) {
SearchControls ctls = new SearchControls();
ctls.setSearchScope(SearchControls.SUBTREE_SCOPE);
ctls.setReturningAttributes(returningAttrs);
ctls.setCountLimit(0); // set no limits
boolean paging = isPagedResultControlSupported(ctx);
for (String ldapBase : syncConfiguration.getLdapBases()) {
int counter = 0;
try {
if(paging) {
byte[] cookie = null;
ctx.setRequestControls(new Control[] { new PagedResultsControl(PAGE_SIZE, Control.NONCRITICAL) });
do {
NamingEnumeration<SearchResult> enm = ctx.search(ldapBase, filter, ctls);
while (enm.hasMore()) {
visitor.visit(enm.next());
counter++;
}
cookie = getCookie(ctx);
} while (cookie != null);
} else {
ctx.setRequestControls(null); // reset on failure, see FXOLAT-299
NamingEnumeration<SearchResult> enm = ctx.search(ldapBase, filter, ctls);
while (enm.hasMore()) {
visitor.visit(enm.next());
counter++;
}
}
} catch (SizeLimitExceededException e) {
log.error("SizeLimitExceededException after "
+ counter
+ " records when getting all users from LDAP, reconfigure your LDAP server, hints: http://www.ldapbrowser.com/forum/viewtopic.php?t=14", e);
} catch (NamingException e) {
log.error("NamingException when trying to search users from LDAP using ldapBase::" + ldapBase + " on row::" + counter, e);
} catch (Exception e) {
log.error("Exception when trying to search users from LDAP using ldapBase::" + ldapBase + " on row::" + counter, e);
}
log.debug("finished search for ldapBase:: " + ldapBase);
}
}
public String searchUserForLogin(String login, DirContext ctx) {
if(ctx == null) return null;
String ldapUserIDAttribute = syncConfiguration.getLdapUserLoginAttribute();
String filter = buildSearchUserFilter(ldapUserIDAttribute, login);
return searchUserDN(login, filter, ctx);
}
public String searchUserDNByUid(String uid, DirContext ctx) {
if(ctx == null) return null;
String ldapUserIDAttribute = syncConfiguration.getOlatPropertyToLdapAttribute(LDAPConstants.LDAP_USER_IDENTIFYER);
String filter = buildSearchUserFilter(ldapUserIDAttribute, uid);
return searchUserDN(uid, filter, ctx);
}
private String searchUserDN(String username, String filter, DirContext ctx) {
List<String> ldapBases = syncConfiguration.getLdapBases();
String[] serachAttr = { "dn" };
SearchControls ctls = new SearchControls();
ctls.setSearchScope(SearchControls.SUBTREE_SCOPE);
ctls.setReturningAttributes(serachAttr);
String userDN = null;
for (String ldapBase : ldapBases) {
try {
NamingEnumeration<SearchResult> enm = ctx.search(ldapBase, filter, ctls);
while (enm.hasMore()) {
SearchResult result = enm.next();
userDN = result.getNameInNamespace();
}
if (userDN != null) {
break;
}
} catch (NamingException e) {
log.error("NamingException when trying to bind user with username::" + username + " on ldapBase::" + ldapBase, e);
}
}
return userDN;
}
private String buildSearchUserFilter(String attribute, String uid) {
String ldapUserFilter = syncConfiguration.getLdapUserFilter();
StringBuilder filter = new StringBuilder();
if (ldapUserFilter != null) {
// merge preconfigured filter (e.g. object class, group filters) with username using AND rule
filter.append("(&").append(ldapUserFilter);
}
filter.append("(").append(attribute).append("=").append(uid).append(")");
if (ldapUserFilter != null) {
filter.append(")");
}
return filter.toString();
}
/**
*
* Creates list of all LDAP Users or changed Users since syncTime
*
* Configuration: userAttr = ldapContext.xml (property=userAttrs) LDAP Base =
* ldapContext.xml (property=ldapBase)
*
*
*
* @param syncTime The time to search in LDAP for changes since this time.
* SyncTime has to formatted: JJJJMMddHHmm
* @param ctx The LDAP system connection, if NULL or closed NamingExecpiton is
* thrown
*
* @return Returns list of Arguments of found users or empty list if search
* fails or nothing is changed
*
* @throws NamingException
*/
public List<LDAPUser> getUserAttributesModifiedSince(Date syncTime, LdapContext ctx) {
final boolean debug = log.isDebug();
String userFilter = syncConfiguration.getLdapUserFilter();
StringBuilder filter = new StringBuilder();
if (syncTime == null) {
if(debug) log.debug("LDAP get user attribs since never -> full sync!");
if (filter != null) {
filter.append(userFilter);
}
} else {
String dateFormat = ldapLoginModule.getLdapDateFormat();
SimpleDateFormat generalizedTimeFormatter = new SimpleDateFormat(dateFormat);
generalizedTimeFormatter.setTimeZone(UTC_TIME_ZONE);
String syncTimeForm = generalizedTimeFormatter.format(syncTime);
if(debug) log.debug("LDAP get user attribs since " + syncTime + " -> means search with date restriction-filter: " + syncTimeForm);
if (userFilter != null) {
// merge user filter with time fileter using and rule
filter.append("(&").append(userFilter);
}
filter.append("(|(");
filter.append(syncConfiguration.getLdapUserLastModifiedTimestampAttribute()).append(">=").append(syncTimeForm);
filter.append(")(");
filter.append(syncConfiguration.getLdapUserCreatedTimestampAttribute()).append(">=").append(syncTimeForm);
filter.append("))");
if (userFilter != null) {
filter.append(")");
}
}
String[] userAttrs = getEnhancedUserAttributes();
LDAPUserVisitor userVisitor = new LDAPUserVisitor(syncConfiguration);
searchInLdap(userVisitor, filter.toString(), userAttrs, ctx);
List<LDAPUser> ldapUserList = userVisitor.getLdapUserList();
if(debug) {
log.debug("attrib search returned " + ldapUserList.size() + " results");
}
return ldapUserList;
}
public String[] getEnhancedUserAttributes() {
String[] userAttrs = syncConfiguration.getUserAttributes();
List<String> userAttrList = new ArrayList<>(userAttrs.length + 7);
for(String userAttr:userAttrs) {
userAttrList.add(userAttr);
}
if(StringHelper.containsNonWhitespace(syncConfiguration.getCoachRoleAttribute())) {
userAttrList.add(syncConfiguration.getCoachRoleAttribute());
}
if(StringHelper.containsNonWhitespace(syncConfiguration.getAuthorRoleAttribute())) {
userAttrList.add(syncConfiguration.getAuthorRoleAttribute());
}
if(StringHelper.containsNonWhitespace(syncConfiguration.getUserManagerRoleAttribute())) {
userAttrList.add(syncConfiguration.getUserManagerRoleAttribute());
}
if(StringHelper.containsNonWhitespace(syncConfiguration.getGroupManagerRoleAttribute())) {
userAttrList.add(syncConfiguration.getGroupManagerRoleAttribute());
}
if(StringHelper.containsNonWhitespace(syncConfiguration.getQpoolManagerRoleAttribute())) {
userAttrList.add(syncConfiguration.getQpoolManagerRoleAttribute());
}
if(StringHelper.containsNonWhitespace(syncConfiguration.getLearningResourceManagerRoleAttribute())) {
userAttrList.add(syncConfiguration.getLearningResourceManagerRoleAttribute());
}
if(StringHelper.containsNonWhitespace(syncConfiguration.getGroupAttribute())) {
userAttrList.add(syncConfiguration.getGroupAttribute());
}
if(StringHelper.containsNonWhitespace(syncConfiguration.getCoachedGroupAttribute())) {
userAttrList.add(syncConfiguration.getCoachedGroupAttribute());
}
return userAttrList.toArray(new String[userAttrList.size()]);
}
private byte[] getCookie(LdapContext ctx) throws NamingException, IOException {
byte[] cookie = null;
// Examine the paged results control response
Control[] controls = ctx.getResponseControls();
if (controls != null) {
for (int i = 0; i < controls.length; i++) {
if (controls[i] instanceof PagedResultsResponseControl) {
PagedResultsResponseControl prrc = (PagedResultsResponseControl) controls[i];
cookie = prrc.getCookie();
}
}
}
// Re-activate paged results
ctx.setRequestControls(new Control[] { new PagedResultsControl(PAGE_SIZE, cookie, Control.NONCRITICAL) });
return cookie;
}
private boolean isPagedResultControlSupported(LdapContext ctx) {
// FXOLAT-299, might return false on 2nd execution
if (pagingSupportedAlreadyFound == true) return true;
try {
SearchControls ctl = new SearchControls();
ctl.setReturningAttributes(new String[]{"supportedControl"});
ctl.setSearchScope(SearchControls.OBJECT_SCOPE);
/* search for the rootDSE object */
NamingEnumeration<SearchResult> results = ctx.search("", "(objectClass=*)", ctl);
while(results.hasMore()){
SearchResult entry = results.next();
NamingEnumeration<? extends Attribute> attrs = entry.getAttributes().getAll();
while (attrs.hasMore()){
Attribute attr = attrs.next();
NamingEnumeration<?> vals = attr.getAll();
while (vals.hasMore()){
String value = (String) vals.next();
if(value.equals(PAGED_RESULT_CONTROL_OID))
pagingSupportedAlreadyFound = true;
return true;
}
}
}
return false;
} catch (Exception e) {
log.error("Exception when trying to know if the server support paged results.", e);
return false;
}
}
}