/**
* <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;
import java.io.FileInputStream;
import java.security.KeyStore;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.text.ParseException;
import java.util.Date;
import java.util.Enumeration;
import org.olat.basesecurity.BaseSecurity;
import org.olat.basesecurity.SecurityGroup;
import org.olat.core.configuration.AbstractSpringModule;
import org.olat.core.logging.OLog;
import org.olat.core.logging.Tracing;
import org.olat.core.util.StringHelper;
import org.olat.core.util.coordinate.CoordinatorManager;
import org.quartz.CronTrigger;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
/**
* Description:
* This Module loads all needed configuration for the LDAP Login.
* All configuration is done in the spring olatextconfig.xml file.
* <p>
* LDAPLoginModule
* <p>
*
* @author maurus.rohrer@gmail.com
*/
@Service("org.olat.ldap.LDAPLoginModule")
public class LDAPLoginModule extends AbstractSpringModule {
// Connection configuration
public static final long WARNING_LIMIT = 15 *1000 * 1000 * 1000;
@Value("${ldap.ldapUrl}")
private String ldapUrl;
@Value("${ldap.enable:false}")
private boolean ldapEnabled;
@Value("${ldap.activeDirectory:false}")
private boolean activeDirectory;
@Value("${ldap.dateFormat}")
private String ldapDateFormat;
//SSL configuration
@Value("${ldap.sslEnabled}")
private boolean sslEnabled;
@Value("${ldap.trustStoreLocation}")
private String trustStoreLoc;
@Value("${ldap.trustStorePwd}")
private String trustStorePass;
@Value("${ldap.trustStoreType}")
private String trustStoreTyp;
// System user: used for getting all users and connection testing
@Value("${ldap.ldapSystemDN}")
private String systemDN;
@Value("${ldap.ldapSystemPW}")
private String systemPW;
@Value("${ldap.connectionTimeout}")
private Integer connectionTimeout;
/**
* Create LDAP users on the fly when authenticated successfully
*/
@Value("${ldap.ldapCreateUsersOnLogin}")
private boolean createUsersOnLogin;
/**
* When users log in via LDAP, the system can keep a copy of the password as encrypted
* hash in the database. This makes OLAT more independent from an offline LDAP server
* and users can use their LDAP password to use the WebDAV functionality.
* When setting to true (recommended), make sure you configured pwdchange=false in the
* org.olat.user.UserModule olat.propertes.
*/
@Value("${ldap.cacheLDAPPwdAsOLATPwdOnLogin}")
private boolean cacheLDAPPwdAsOLATPwdOnLogin;
/**
* When the system detects an LDAP user that does already exist in OLAT but is not marked
* as LDAP user, the OLAT user can be converted to an LDAP managed user.
* When enabling this feature you should make sure that you don't have a user 'administrator'
* in your ldapBases (not a problem but not recommended)
*/
@Value("${ldap.convertExistingLocalUsersToLDAPUsers}")
private boolean convertExistingLocalUsersToLDAPUsers;
//
/**
* Users that have been created via LDAP sync but now can't be found on the LDAP anymore
* can be deleted automatically. If unsure, set to false and delete those users manually
* in the user management.
*/
@Value("${ldap.deleteRemovedLDAPUsersOnSync}")
private boolean deleteRemovedLDAPUsersOnSync;
/**
* Sanity check when deleteRemovedLDAPUsersOnSync is set to 'true': if more than the defined
* percentages of user accounts are not found on the LDAP server and thus recognized as to be
* deleted, the LDAP sync will not happen and require a manual triggering of the delete job
* from the admin interface. This should prevent accidential deletion of OLAT user because of
* temporary LDAP problems or user relocation on the LDAP side.
* Value= 0 (never delete) to 100 (always delete).
*/
@Value("${ldap.deleteRemovedLDAPUsersPercentage}")
private int deleteRemovedLDAPUsersPercentage;
// Propagate the password changes onto the LDAP server
@Value("${ldap.propagatePasswordChangedOnLdapServer}")
private boolean propagatePasswordChangedOnLdapServer;
@Value("${ldap.resetLockTimoutOnPasswordChange}")
private boolean resetLockTimoutOnPasswordChange;
@Value("${ldap.changePasswordUrl}")
private String changePasswordUrl;
// Configuration for syncing user attributes
// Should users be created and synchronized automatically? If you set this
// configuration to false, the users will be generated on-the-fly when they
// log in
@Value("${ldap.ldapSyncOnStartup}")
private boolean ldapSyncOnStartup;
@Value("${ldap.ldapSyncCronSync}")
private boolean ldapSyncCronSync;
@Value("${ldap.ldapSyncCronSyncExpression}")
private String ldapSyncCronSyncExpression;
// User LDAP attributes to be synced and a map with the mandatory attributes
private static final OLog log = Tracing.createLoggerFor(LDAPLoginModule.class);
@Autowired
private LDAPSyncConfiguration syncConfiguration;
@Autowired
private Scheduler scheduler;
@Autowired
private BaseSecurity securityManager;
@Autowired
public LDAPLoginModule(CoordinatorManager coordinatorManager) {
super(coordinatorManager);
}
/**
* @see org.olat.core.configuration.Initializable#init()
*/
@Override
public void init() {
// Check if LDAP is enabled
if (!isLDAPEnabled()) {
log.info("LDAP login is disabled");
return;
}
log.info("Starting LDAP module");
// Create LDAP Security Group if not existing. Used to identify users that
// have to be synced with LDAP
SecurityGroup ldapGroup = securityManager.findSecurityGroupByName(LDAPConstants.SECURITY_GROUP_LDAP);
if (ldapGroup == null) {
ldapGroup = securityManager.createAndPersistNamedSecurityGroup(LDAPConstants.SECURITY_GROUP_LDAP);
}
// check for valid configuration
if (!checkConfigParameterIsNotEmpty(ldapUrl)) return;
if (!checkConfigParameterIsNotEmpty(systemDN)) return;
if (!checkConfigParameterIsNotEmpty(systemPW)) return;
if (syncConfiguration.getLdapBases() == null || syncConfiguration.getLdapBases().isEmpty()) {
log.error("Missing configuration 'ldapBases'. Add at least one LDAP Base to the this configuration in olatextconfig.xml first. Disabling LDAP");
setEnableLDAPLogins(false);
return;
}
if (syncConfiguration.getLdapUserFilter() != null) {
if (!syncConfiguration.getLdapUserFilter().startsWith("(") || !syncConfiguration.getLdapUserFilter().endsWith(")")) {
log.error("Wrong configuration 'ldapUserFilter'. Set filter to emtpy value or enclose filter in brackets like '(objectClass=person)'. Disabling LDAP");
setEnableLDAPLogins(false);
return;
}
}
if (!checkConfigParameterIsNotEmpty(syncConfiguration.getLdapUserCreatedTimestampAttribute())) {
return;
}
if (!checkConfigParameterIsNotEmpty(syncConfiguration.getLdapUserLastModifiedTimestampAttribute())) {
return;
}
if (syncConfiguration.getUserAttributeMap() == null || syncConfiguration.getUserAttributeMap().isEmpty()) {
log.error("Missing configuration 'userAttrMap'. Add at least the email propery to the this configuration in olatextconfig.xml first. Disabling LDAP");
setEnableLDAPLogins(false);
return;
}
if (syncConfiguration.getRequestAttributes() == null || syncConfiguration.getRequestAttributes().isEmpty()) {
log.error("Missing configuration 'reqAttr'. Add at least the email propery to the this configuration in olatextconfig.xml first. Disabling LDAP");
setEnableLDAPLogins(false);
return;
}
// check if OLAT user properties is defined in olat_userconfig.xml, if not disable the LDAP module
if(!syncConfiguration.checkIfOlatPropertiesExists(syncConfiguration.getUserAttributeMap())){
log.error("Invalid LDAP OLAT properties mapping configuration (userAttrMap). Disabling LDAP");
setEnableLDAPLogins(false);
return;
}
if(!syncConfiguration.checkIfOlatPropertiesExists(syncConfiguration.getRequestAttributes())){
log.error("Invalid LDAP OLAT properties mapping configuration (reqAttr). Disabling LDAP");
setEnableLDAPLogins(false);
return;
}
if(syncConfiguration.getSyncOnlyOnCreateProperties() != null
&& !syncConfiguration.checkIfStaticOlatPropertiesExists(syncConfiguration.getSyncOnlyOnCreateProperties())){
log.error("Invalid LDAP OLAT syncOnlyOnCreateProperties configuration. Disabling LDAP");
setEnableLDAPLogins(false);
return;
}
if(syncConfiguration.getStaticUserProperties() != null
&& !syncConfiguration.checkIfStaticOlatPropertiesExists(syncConfiguration.getStaticUserProperties().keySet())){
log.error("Invalid static OLAT properties configuration (staticUserProperties). Disabling LDAP");
setEnableLDAPLogins(false);
return;
}
// check SSL certifications, throws Startup Exception if certificate is not found
if(isSslEnabled()){
if (!checkServerCertValidity(0)) {
log.error("LDAP enabled but no valid server certificate found. Please fix!");
} else if (!checkServerCertValidity(30)) {
log.warn("Server Certificate will expire in less than 30 days.");
}
}
// Start LDAP cron sync job
if (isLdapSyncCronSync()) {
initCronSyncJob();
} else {
log.info("LDAP cron sync is disabled");
}
// OK, everything finished checkes passed
log.info("LDAP login is enabled");
}
@Override
protected void initFromChangedProperties() {
//
}
/**
* Internal helper to initialize the cron syncer job
*/
private void initCronSyncJob() {
try {
// Create job with cron trigger configuration
JobDetail jobDetail = new JobDetail("LDAP_Cron_Syncer_Job", Scheduler.DEFAULT_GROUP, LDAPUserSynchronizerJob.class);
CronTrigger trigger = new CronTrigger();
trigger.setName("LDAP_Cron_Syncer_Trigger");
trigger.setCronExpression(ldapSyncCronSyncExpression);
// Schedule job now
scheduler.scheduleJob(jobDetail, trigger);
log.info("LDAP cron syncer is enabled with expression::" + ldapSyncCronSyncExpression);
} catch (ParseException e) {
setLdapSyncCronSync(false);
log.error("LDAP configuration in attribute 'ldapSyncCronSyncExpression' is not valid ("
+ ldapSyncCronSyncExpression
+ "). See http://quartz.sourceforge.net/javadoc/org/quartz/CronTrigger.html to learn more about the cron syntax. Disabling LDAP cron syncing",
e);
} catch (SchedulerException e) {
log.error("Error while scheduling LDAP cron sync job. Disabling LDAP cron syncing", e);
}
}
/**
* Checks if SSL certification is know and accepted by Java JRE.
*
*
* @param dayFromNow Checks expiration
* @return true Certification accepted, false No valid certification
*
* @throws Exception
*
*/
private boolean checkServerCertValidity(int daysFromNow) {
KeyStore keyStore;
try {
keyStore = KeyStore.getInstance(getTrustStoreType());
keyStore.load(new FileInputStream(getTrustStoreLocation()), (getTrustStorePwd() != null) ? getTrustStorePwd().toCharArray() : null);
Enumeration<String> aliases = keyStore.aliases();
while (aliases.hasMoreElements()) {
String alias = aliases.nextElement();
Certificate cert = keyStore.getCertificate(alias);
if (cert instanceof X509Certificate) {
return isCertificateValid((X509Certificate)cert, daysFromNow);
}
}
} catch (Exception e) {
return false;
}
return false;
}
private boolean isCertificateValid(X509Certificate x509Cert, int daysFromNow) {
try {
x509Cert.checkValidity();
if (daysFromNow > 0) {
Date nowPlusDays = new Date(System.currentTimeMillis() + (new Long(daysFromNow).longValue() * 24l * 60l * 60l * 1000l));
x509Cert.checkValidity(nowPlusDays);
}
} catch (Exception e) {
return false;
}
return true;
}
/**
* Internal helper to check for emtpy config variables
*
* @param param
* @return true: not empty; false: empty or null
*/
private boolean checkConfigParameterIsNotEmpty(String param) {
if (StringHelper.containsNonWhitespace(param)) {
return true;
} else {
log.error("Missing configuration '" + param + "'. Add this configuration to olatextconfig.xml first. Disabling LDAP");
setEnableLDAPLogins(false);
return false;
}
}
/*
* Spring setter methods - don't use them to modify values at runtime!
*/
public void setEnableLDAPLogins(boolean enableLDAPLogins) {
ldapEnabled = enableLDAPLogins;
}
public void setSslEnabled(boolean sslEnabl) {
sslEnabled = sslEnabl;
}
public void setActiveDirectory(boolean aDirectory) {
activeDirectory = aDirectory;
}
public void setLdapDateFormat(String dateFormat) {
ldapDateFormat = dateFormat;
}
public void setTrustStoreLocation(String trustStoreLocation){
trustStoreLoc=trustStoreLocation.trim();
}
public void setTrustStorePwd(String trustStorePwd){
trustStorePass=trustStorePwd.trim();
}
public void setTrustStoreType(String trustStoreType){
trustStoreTyp= trustStoreType.trim();
}
public void setLdapSyncOnStartup(boolean ldapStartSyncs) {
ldapSyncOnStartup = ldapStartSyncs;
}
public String getLdapSystemDN() {
return systemDN;
}
public void setLdapSystemDN(String ldapSystemDN) {
systemDN = ldapSystemDN.trim();
}
public String getLdapSystemPW() {
return systemPW;
}
public void setLdapSystemPW(String ldapSystemPW) {
systemPW = ldapSystemPW.trim();
}
public String getLdapUrl() {
return ldapUrl;
}
public void setLdapUrl(String ldapUrlConfig) {
ldapUrl = ldapUrlConfig.trim();
}
public Integer getLdapConnectionTimeout() {
return connectionTimeout;
}
public void setLdapConnectionTimeout(Integer timeout) {
connectionTimeout = timeout;
}
public void setLdapSyncCronSync(boolean ldapSyncCronSync) {
this.ldapSyncCronSync = ldapSyncCronSync;
}
public void setLdapSyncCronSyncExpression(String ldapSyncCronSyncExpression) {
this.ldapSyncCronSyncExpression = ldapSyncCronSyncExpression.trim();
}
public void setCacheLDAPPwdAsOLATPwdOnLogin(boolean cacheLDAPPwdAsOLATPwdOnLogin) {
this.cacheLDAPPwdAsOLATPwdOnLogin = cacheLDAPPwdAsOLATPwdOnLogin;
}
public void setCreateUsersOnLogin(boolean createUsersOnLogin) {
this.createUsersOnLogin = createUsersOnLogin;
}
public void setConvertExistingLocalUsersToLDAPUsers(boolean convertExistingLocalUsersToLDAPUsers) {
this.convertExistingLocalUsersToLDAPUsers = convertExistingLocalUsersToLDAPUsers;
}
public void setDeleteRemovedLDAPUsersOnSync(boolean deleteRemovedLDAPUsersOnSync) {
this.deleteRemovedLDAPUsersOnSync = deleteRemovedLDAPUsersOnSync;
}
public void setDeleteRemovedLDAPUsersPercentage(int deleteRemovedLDAPUsersPercentage){
this.deleteRemovedLDAPUsersPercentage = deleteRemovedLDAPUsersPercentage;
}
public void setPropagatePasswordChangedOnLdapServer(boolean propagatePasswordChangedOnServer) {
this.propagatePasswordChangedOnLdapServer = propagatePasswordChangedOnServer;
}
public void setResetLockTimoutOnPasswordChange(boolean resetLockTimoutOnPasswordChange) {
this.resetLockTimoutOnPasswordChange = resetLockTimoutOnPasswordChange;
}
public boolean isLDAPEnabled() {
return ldapEnabled;
}
public boolean isSslEnabled() {
return sslEnabled;
}
public boolean isActiveDirectory() {
return activeDirectory;
}
public String getLdapDateFormat() {
if(StringHelper.containsNonWhitespace(ldapDateFormat)) {
return ldapDateFormat;
}
return "yyyyMMddHHmmss'Z'";//default
}
public String getTrustStoreLocation(){
return trustStoreLoc;
}
public String getTrustStorePwd(){
return trustStorePass;
}
public String getTrustStoreType(){
return trustStoreTyp;
}
public boolean isLdapSyncOnStartup() {
return ldapSyncOnStartup;
}
public boolean isLdapSyncCronSync() {
return ldapSyncCronSync;
}
public String getLdapSyncCronSyncExpression() {
return ldapSyncCronSyncExpression;
}
public boolean isCreateUsersOnLogin() {
return createUsersOnLogin;
}
public boolean isCacheLDAPPwdAsOLATPwdOnLogin() {
return cacheLDAPPwdAsOLATPwdOnLogin;
}
public boolean isConvertExistingLocalUsersToLDAPUsers() {
return convertExistingLocalUsersToLDAPUsers;
}
public boolean isDeleteRemovedLDAPUsersOnSync() {
return deleteRemovedLDAPUsersOnSync;
}
public int getDeleteRemovedLDAPUsersPercentage(){
return deleteRemovedLDAPUsersPercentage;
}
public boolean isPropagatePasswordChangedOnLdapServer(){
return propagatePasswordChangedOnLdapServer;
}
public boolean isResetLockTimoutOnPasswordChange() {
return resetLockTimoutOnPasswordChange;
}
public String getChangePasswordUrl() {
return changePasswordUrl;
}
}