Skip to content
Snippets Groups Projects
LDAPLoginManagerImpl.java 63.8 KiB
Newer Older
Alan Moran's avatar
Alan Moran committed
/**
 * <a href="http://www.openolat.org">
 * OpenOLAT - Online Learning and Training</a><br>
Alan Moran's avatar
Alan Moran committed
 * <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>
Alan Moran's avatar
Alan Moran committed
 * <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
Alan Moran's avatar
Alan Moran committed
 * <p>
 */

package org.olat.ldap.manager;
Alan Moran's avatar
Alan Moran committed

import java.util.ArrayList;
Alan Moran's avatar
Alan Moran committed
import java.util.Date;
import java.util.HashMap;
Alan Moran's avatar
Alan Moran committed
import java.util.Hashtable;
Alan Moran's avatar
Alan Moran committed
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import javax.naming.AuthenticationException;
import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.BasicAttribute;
import javax.naming.directory.DirContext;
import javax.naming.directory.ModificationItem;
import javax.naming.directory.SearchControls;
Alan Moran's avatar
Alan Moran committed
import javax.naming.directory.SearchResult;
import javax.naming.ldap.Control;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapContext;

import org.apache.commons.lang.ArrayUtils;
import org.apache.logging.log4j.Logger;
Alan Moran's avatar
Alan Moran committed
import org.olat.basesecurity.Authentication;
import org.olat.basesecurity.BaseSecurity;
srosse's avatar
srosse committed
import org.olat.basesecurity.BaseSecurityModule;
import org.olat.basesecurity.GroupRoles;
import org.olat.basesecurity.IdentityRef;
import org.olat.basesecurity.OrganisationRoles;
import org.olat.basesecurity.manager.AuthenticationDAO;
import org.olat.basesecurity.manager.OrganisationDAO;
import org.olat.basesecurity.model.IdentityRefImpl;
import org.olat.core.CoreSpringFactory;
import org.olat.core.commons.persistence.DB;
import org.olat.core.commons.services.taskexecutor.TaskExecutorManager;
Alan Moran's avatar
Alan Moran committed
import org.olat.core.gui.control.Event;
import org.olat.core.id.Identity;
import org.olat.core.id.Organisation;
import org.olat.core.id.Roles;
import org.olat.core.id.RolesByOrganisation;
Alan Moran's avatar
Alan Moran committed
import org.olat.core.id.User;
import org.olat.core.id.UserConstants;
import org.olat.core.logging.Tracing;
import org.olat.core.util.StringHelper;
import org.olat.core.util.WorkThreadInformations;
Alan Moran's avatar
Alan Moran committed
import org.olat.core.util.coordinate.Coordinator;
import org.olat.core.util.coordinate.CoordinatorManager;
import org.olat.core.util.event.FrameworkStartedEvent;
import org.olat.core.util.event.FrameworkStartupEventChannel;
Alan Moran's avatar
Alan Moran committed
import org.olat.core.util.event.GenericEventListener;
import org.olat.core.util.mail.MailHelper;
import org.olat.group.BusinessGroup;
import org.olat.group.BusinessGroupManagedFlag;
import org.olat.group.BusinessGroupService;
import org.olat.group.manager.BusinessGroupRelationDAO;
import org.olat.group.model.SearchBusinessGroupParams;
import org.olat.ldap.LDAPConstants;
import org.olat.ldap.LDAPError;
import org.olat.ldap.LDAPEvent;
import org.olat.ldap.LDAPLoginManager;
import org.olat.ldap.LDAPLoginModule;
import org.olat.ldap.LDAPSyncConfiguration;
import org.olat.ldap.model.LDAPGroup;
import org.olat.ldap.model.LDAPUser;
import org.olat.ldap.model.LDAPValidationResult;
Alan Moran's avatar
Alan Moran committed
import org.olat.ldap.ui.LDAPAuthenticationController;
import org.olat.login.auth.AuthenticationProviderSPI;
import org.olat.login.auth.OLATAuthManager;
import org.olat.login.validation.ValidationResult;
import org.olat.user.UserLifecycleManager;
Alan Moran's avatar
Alan Moran committed
import org.olat.user.UserManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
 * Description: This manager handles  communication between LDAP and OLAT.
Alan Moran's avatar
Alan Moran committed
 * The synching is done only on node 1 of a cluster.
 * 
 * @author Maurus Rohrer
 */
@Service("org.olat.ldap.LDAPLoginManager")
public class LDAPLoginManagerImpl implements LDAPLoginManager, AuthenticationProviderSPI, GenericEventListener {
	private static final Logger log = Tracing.createLoggerFor(LDAPLoginManagerImpl.class);
	private static final String TIMEOUT_KEY = "com.sun.jndi.ldap.connect.timeout";
Alan Moran's avatar
Alan Moran committed
	private static boolean batchSyncIsRunning = false;
	private static Date lastSyncDate = null; // first sync is always a full sync
	
	private Coordinator coordinator;
	private TaskExecutorManager taskExecutorManager;
	
	@Autowired
	private DB dbInstance;
	@Autowired
	private LDAPDAO ldapDao;
	@Autowired
Alan Moran's avatar
Alan Moran committed
	private UserManager userManager;
	@Autowired
	private BaseSecurity securityManager;
	@Autowired
	private OrganisationDAO organisationDao;
	@Autowired
	private LDAPLoginModule ldapLoginModule;
	@Autowired
	private BaseSecurityModule securityModule;
	@Autowired
	private AuthenticationDAO authenticationDao;
	@Autowired
	private LDAPSyncConfiguration syncConfiguration;
	@Autowired
	private UserLifecycleManager userLifecycleManager;
	@Autowired
	private BusinessGroupService businessGroupService;
	@Autowired
	private BusinessGroupRelationDAO businessGroupRelationDao;
	@Autowired
	public LDAPLoginManagerImpl(CoordinatorManager coordinatorManager, TaskExecutorManager taskExecutorManager) {
Alan Moran's avatar
Alan Moran committed
		this.coordinator = coordinatorManager.getCoordinator();
		this.taskExecutorManager = taskExecutorManager;
		coordinator.getEventBus().registerFor(this, null, ldapSyncLockOres);
		FrameworkStartupEventChannel.registerForStartupEvent(this);
	@Override
	public List<String> getProviderNames() {
		return Collections.singletonList("LDAP");
	}

	@Override
	public boolean canChangeAuthenticationUsername(String provider) {
		return "LDAP".equals(provider);
	}

	@Override
	public boolean changeAuthenticationUsername(Authentication authentication, String newUsername) {
		authentication.setAuthusername(newUsername);
		authentication = authenticationDao.updateAuthentication(authentication);
		return authentication != null;
	}

	@Override
	public ValidationResult validateAuthenticationUsername(String name, Identity identity) {

		LdapContext ctx = bindSystem();
		if(ctx != null) {
			String userDN = ldapDao.searchUserForLogin(name, ctx);
			if(userDN == null) {
				userDN = ldapDao.searchUserDNByUid(name, ctx);
			}
			if(StringHelper.containsNonWhitespace(userDN)) {
				Authentication currentAuth = authenticationDao.getAuthentication(name, LDAPAuthenticationController.PROVIDER_LDAP);
				if(currentAuth == null || currentAuth.getIdentity().equals(identity)) {
					return LDAPValidationResult.allOk();
				}
				return LDAPValidationResult.error("error.user.already.in.use");
			}
			return LDAPValidationResult.error("error.user.not.found");
		}
		return LDAPValidationResult.error("delete.error.connection");
	}

Alan Moran's avatar
Alan Moran committed
	@Override
	public void event(Event event) {
		if(event instanceof LDAPEvent) {
			if(LDAPEvent.SYNCHING.equals(event.getCommand())) {
				batchSyncIsRunning = true;
			} else if(LDAPEvent.SYNCHING_ENDED.equals(event.getCommand())) {
				batchSyncIsRunning = false;
				lastSyncDate = ((LDAPEvent)event).getTimestamp();
			} else if(LDAPEvent.DO_SYNCHING.equals(event.getCommand())) {
Alan Moran's avatar
Alan Moran committed
			}
		} else if(event instanceof FrameworkStartedEvent) {
			try {
				init();
			} catch (Exception e) {
				log.error("", e);
			}
		}
	}
	
	private void init() {
		if(ldapLoginModule.isLDAPEnabled()) {
			if (bindSystem() == null) {
				// don't disable ldap, maybe just a temporary problem, but still report
				// problem in logfile
				log.error("LDAP connection test failed during module initialization, edit config or contact network administrator");
			} else {
				log.info("LDAP login is enabled");
			}
			
			// Start LDAP cron sync job
			if (ldapLoginModule.isLdapSyncCronSync()) {
				LDAPError errors = new LDAPError();
					log.info("LDAP start sync: users synced");
				} else {
					log.warn("LDAP start sync error: {}", errors.get());
				}
			} else {
				log.info("LDAP cron sync is disabled");
			}
srosse's avatar
srosse committed
		//fxdiff: also run on nodes != 1 as nodeid = tomcat-id in fx-environment
		//if(WebappHelper.getNodeId() != 1) return;
		Runnable batchSyncTask = () -> {
			LDAPError errors = new LDAPError();
			doBatchSync(errors);
Alan Moran's avatar
Alan Moran committed
		};
Alan Moran's avatar
Alan Moran committed
	}

	/**
	 * Connect to the LDAP server with System DN and Password
	 * 
	 * Configuration: LDAP URL = ldapContext.xml (property=ldapURL) System DN =
	 * ldapContext.xml (property=ldapSystemDN) System PW = ldapContext.xml
Alan Moran's avatar
Alan Moran committed
	 * (property=ldapSystemPW)
	 * 
	 * @return The LDAP connection (LdapContext) or NULL if connect fails
	 * 
	 * @throws NamingException
	 */
Alan Moran's avatar
Alan Moran committed
	public LdapContext bindSystem() {
		// set LDAP connection attributes
		Hashtable<String, String> env = new Hashtable<>();
Alan Moran's avatar
Alan Moran committed
		env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
		env.put(Context.PROVIDER_URL, ldapLoginModule.getLdapUrl());
Alan Moran's avatar
Alan Moran committed
		env.put(Context.SECURITY_AUTHENTICATION, "simple");
		env.put(Context.SECURITY_PRINCIPAL, ldapLoginModule.getLdapSystemDN());
		env.put(Context.SECURITY_CREDENTIALS, ldapLoginModule.getLdapSystemPW());
		if(ldapLoginModule.getLdapConnectionTimeout() != null) {
			env.put(TIMEOUT_KEY, ldapLoginModule.getLdapConnectionTimeout().toString());
Alan Moran's avatar
Alan Moran committed

		// check ssl
		if (ldapLoginModule.isSslEnabled()) {
Alan Moran's avatar
Alan Moran committed
			enableSSL(env);
		}

		try {
			InitialLdapContext ctx = new InitialLdapContext(env, new Control[]{});
			ctx.getConnectControls();
			return ctx;
		} catch (NamingException e) {
			log.error("NamingException when trying to bind system with DN::" + ldapLoginModule.getLdapSystemDN() + " and PW::"
					+ ldapLoginModule.getLdapSystemPW() + " on URL::" + ldapLoginModule.getLdapUrl(), e);
Alan Moran's avatar
Alan Moran committed
			return null;
		} catch (Exception e) {
			log.error("Exception when trying to bind system with DN::" + ldapLoginModule.getLdapSystemDN() + " and PW::"
					+ ldapLoginModule.getLdapSystemPW() + " on URL::" + ldapLoginModule.getLdapUrl(), e);
Alan Moran's avatar
Alan Moran committed
			return null;
		}

	}

	/**
	 * 
	 * Connect to LDAP with the User-Name and Password given as parameters
	 * 
	 * Configuration: LDAP URL = ldapContext.xml (property=ldapURL) LDAP Base =
	 * ldapContext.xml (property=ldapBase) LDAP Attributes Map =
	 * ldapContext.xml (property=userAttrs)
Alan Moran's avatar
Alan Moran committed
	 * 
	 * 
	 * @param uid The users LDAP login name (can't be null)
	 * @param pwd The users LDAP password (can't be null)
	 * 
	 * @return After successful bind Attributes otherwise NULL
	 * 
	 * @throws NamingException
	 */
	public Attributes bindUser(String login, String pwd, LDAPError errors) {
Alan Moran's avatar
Alan Moran committed
		// get user name, password and attributes
		String ldapUrl = ldapLoginModule.getLdapUrl();
		String[] userAttr = syncConfiguration.getUserAttributes();
			log.debug("Error when trying to bind user, missing username or password. Username::{} pwd::{}", login, pwd);
Alan Moran's avatar
Alan Moran committed
			errors.insert("Username and password must be selected");
			return null;
		}
		
		dbInstance.commit();
Alan Moran's avatar
Alan Moran committed
		LdapContext ctx = bindSystem();
		if (ctx == null) {
			errors.insert("LDAP connection error");
			return null;
		}
		String userDN = ldapDao.searchUserForLogin(login, ctx);
Alan Moran's avatar
Alan Moran committed
		if (userDN == null) {
			log.info("Error when trying to bind user with username::" + login + " - user not found on LDAP server"
					+ (ldapLoginModule.isCacheLDAPPwdAsOLATPwdOnLogin() ? ", trying with OLAT login provider" : ""));
Alan Moran's avatar
Alan Moran committed
			errors.insert("Username or password incorrect");
			return null;
		}
		
		// Ok, so far so good, user exists. Now try to fetch attributes using the
		// users credentials
		Hashtable<String, String> env = new Hashtable<>();
Alan Moran's avatar
Alan Moran committed
		env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
		env.put(Context.PROVIDER_URL, ldapUrl);
		env.put(Context.SECURITY_AUTHENTICATION, "simple");
		env.put(Context.SECURITY_PRINCIPAL, userDN);
		env.put(Context.SECURITY_CREDENTIALS, pwd);
		if(ldapLoginModule.getLdapConnectionTimeout() != null) {
			env.put(TIMEOUT_KEY, ldapLoginModule.getLdapConnectionTimeout().toString());
		if(ldapLoginModule.isSslEnabled()) {
Alan Moran's avatar
Alan Moran committed
			enableSSL(env);
		}

		try {
			dbInstance.commit();
Alan Moran's avatar
Alan Moran committed
			Control[] connectCtls = new Control[]{};
			LdapContext userBind = new InitialLdapContext(env, connectCtls);
			Attributes attributes = userBind.getAttributes(userDN, userAttr);
			userBind.close();
			return attributes;
		} catch (AuthenticationException e) {
			log.info("Error when trying to bind user with username::{} - invalid LDAP password", login);
Alan Moran's avatar
Alan Moran committed
			errors.insert("Username or password incorrect");
			return null;
		} catch (NamingException e) {
			log.error("NamingException when trying to get attributes after binding user with username::{}", login, e);
Alan Moran's avatar
Alan Moran committed
			errors.insert("Username or password incorrect");
			return null;
		}
	}
	
	@Override
	public Identity authenticate(String username, String pwd, LDAPError ldapError) {
		long start = System.nanoTime();
		//authenticate against LDAP server
		Attributes attrs = bindUser(username, pwd, ldapError);
		long takes = System.nanoTime() - start;
		if(takes > LDAPLoginModule.WARNING_LIMIT) {
			log.warn("LDAP Authentication takes (ms): ({})", (takes / 1000000));
		}
		
		if (ldapError.isEmpty() && attrs != null) { 
			Identity identity = findIdentityByLdapAuthentication(attrs, ldapError);
			if (!ldapError.isEmpty()) {
				return null;
			}
			if (identity == null) {
				if(ldapLoginModule.isCreateUsersOnLogin()) {
					// User authenticated but not yet existing - create as new OLAT user
					createAndPersistUser(attrs);
					identity = findIdentityByLdapAuthentication(attrs, ldapError);
				} else {
					ldapError.insert("login.notauthenticated");
				}
			} else {
				// User does already exist - just sync attributes
				Map<String, String> olatProToSync = prepareUserPropertyForSync(attrs, identity);
				if (olatProToSync != null) {
					syncUser(olatProToSync, identity);
				}
			}
			// Add or update an OLAT authentication token for this user if configured in the module
			if (identity != null && ldapLoginModule.isCacheLDAPPwdAsOLATPwdOnLogin()) {
				// there is no WEBDAV token but an HA1, the HA1 is linked to the OLAT one.
				CoreSpringFactory.getImpl(OLATAuthManager.class)
					.synchronizeOlatPasswordAndUsername(identity, identity, username, pwd);
			}
			return identity;
		} 
		return null;
	}
	
Alan Moran's avatar
Alan Moran committed
	/**
	 * Change the password on the LDAP server.
	 */
	@Override
	public boolean changePassword(Authentication auth, String pwd, LDAPError errors) {
		String uid = auth.getAuthusername();
	
		String ldapUserPasswordAttribute = syncConfiguration.getLdapUserPasswordAttribute();
Alan Moran's avatar
Alan Moran committed
		try {
			LdapContext ctx = bindSystem();
			String dn = ldapDao.searchUserDNByUid(uid, ctx);
			if(dn == null) {
				dn = ldapDao.searchUserForLogin(uid, ctx);
			}

			List<ModificationItem> modificationItemList = new ArrayList<>();
			if(ldapLoginModule.isActiveDirectory()) {
				boolean resetLockoutTime = false;
				if(ldapLoginModule.isResetLockTimoutOnPasswordChange()) {
					String[] attrs = syncConfiguration.getUserAttributes();
					List<String> attrList = new ArrayList<>(Arrays.asList(attrs));
					attrList.add("lockoutTime");
					attrs = attrList.toArray(new String[attrList.size()]);
					Attributes attributes = ctx.getAttributes(dn, attrs);
					Attribute lockoutTimeAttr = attributes.get("lockoutTime");
					if(lockoutTimeAttr != null && lockoutTimeAttr.size() > 0) {
						Object lockoutTime = lockoutTimeAttr.get();
						if(lockoutTime != null && !lockoutTime.equals("0")) {
							resetLockoutTime = true;
						}
					}
				}

Alan Moran's avatar
Alan Moran committed
				//active directory need the password enquoted and unicoded (but little-endian)
				String quotedPassword = "\"" + pwd + "\"";
				char[] unicodePwd = quotedPassword.toCharArray();
				byte[] pwdArray = new byte[unicodePwd.length * 2];
				for (int i=0; i<unicodePwd.length; i++) {
					pwdArray[i*2 + 1] = (byte) (unicodePwd[i] >>> 8);
					pwdArray[i*2 + 0] = (byte) (unicodePwd[i] & 0xff);
				}
				BasicAttribute userPasswordAttribute = new BasicAttribute(ldapUserPasswordAttribute, pwdArray );
				modificationItemList.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE, userPasswordAttribute));
				if(resetLockoutTime) {
					BasicAttribute lockTimeoutAttribute = new BasicAttribute("lockoutTime", "0");
					modificationItemList.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE, lockTimeoutAttribute));
				}
Alan Moran's avatar
Alan Moran committed
			} else {
				BasicAttribute userPasswordAttribute = new BasicAttribute(ldapUserPasswordAttribute, pwd);
				modificationItemList.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE, userPasswordAttribute));
			ModificationItem[] modificationItems = modificationItemList.toArray(new ModificationItem[modificationItemList.size()]);
			ctx.modifyAttributes(dn, modificationItems);
Alan Moran's avatar
Alan Moran committed
			ctx.close();
Alan Moran's avatar
Alan Moran committed
		} catch (NamingException e) {
			log.error("NamingException when trying to change password with username::" + uid, e);
Alan Moran's avatar
Alan Moran committed
			errors.insert("Cannot change the password");
			return false;
		} catch(Exception e) {
			log.error("Unexpected exception when trying to change password with username::" + uid, e);
			errors.insert("Cannot change the password");
			return false;
Alan Moran's avatar
Alan Moran committed
		}
	}

	/**
	 * Delete all Identities in List and removes them from LDAPSecurityGroup
	 * 
	 * @param identityList List of Identities to delete
	 */
	public void deleteIdentities(List<Identity> identityList, Identity doer) {
Alan Moran's avatar
Alan Moran committed
		for (Identity identity:  identityList) {
			if(Identity.STATUS_PERMANENT.equals(identity.getStatus())) {
				log.info(Tracing.M_AUDIT, "{} was not deleted because is status is permanent.", identity.getKey());
			userLifecycleManager.deleteIdentity(identity, doer);
Alan Moran's avatar
Alan Moran committed
		}
	}

	/**
	 * Sync all OLATPropertys in Map of Identity
	 * 
	 * @param olatPropertyMap Map of changed OLAT properties
	 *          (OLATProperty,LDAPValue)
	 * @param identity Identity to sync
	 */
	public Identity syncUser(Map<String, String> olatPropertyMap, IdentityRef identityRef) {
		if (identityRef == null) {
			log.warn("Identiy is null - should not happen");
Alan Moran's avatar
Alan Moran committed
		}
		Identity identity = securityManager.loadIdentityByKey(identityRef.getKey());
Alan Moran's avatar
Alan Moran committed
		User user = identity.getUser();
		// remove user identifyer - can not be changed later
		olatPropertyMap.remove(LDAPConstants.LDAP_USER_IDENTIFYER);
		// remove attributes that are defined as sync-only-on-create
		Set<String> syncOnlyOnCreateProperties = syncConfiguration.getSyncOnlyOnCreateProperties();
Alan Moran's avatar
Alan Moran committed
		if (syncOnlyOnCreateProperties != null) {
			for (String syncOnlyOnCreateKey : syncOnlyOnCreateProperties) {
				olatPropertyMap.remove(syncOnlyOnCreateKey);
			}			
		}

		for(Map.Entry<String, String> keyValuePair : olatPropertyMap.entrySet()) {
			String propName = keyValuePair.getKey();
			String value = keyValuePair.getValue();
			if(value == null) {
				if(user.getProperty(propName, null) != null) {
					log.debug("removed property {} for identity {}", propName, identity);
Alan Moran's avatar
Alan Moran committed
					user.setProperty(propName, value);
				}
			} else {
				user.setProperty(propName, value);
			}
		}

		// Add static user properties from the configuration
		Map<String, String> staticProperties = syncConfiguration.getStaticUserProperties();
Alan Moran's avatar
Alan Moran committed
		if (staticProperties != null && staticProperties.size() > 0) {
			for (Map.Entry<String, String> staticProperty : staticProperties.entrySet()) {
				user.setProperty(staticProperty.getKey(), staticProperty.getValue());
			}
		}
srosse's avatar
srosse committed
		userManager.updateUser(user);
		dbInstance.commit();
		
		// check WebDAV authentication
		CoreSpringFactory.getImpl(OLATAuthManager.class).synchronizeCredentials(identity, identity);
	@Override
	public Identity createAndPersistUser(String uid) {
		String ldapUserIDAttribute = syncConfiguration.getOlatPropertyToLdapAttribute(LDAPConstants.LDAP_USER_IDENTIFYER);
		String filter = ldapDao.buildSearchUserFilter(ldapUserIDAttribute, uid);
		LdapContext ctx = bindSystem();
		String userDN = ldapDao.searchUserDNByUid(uid, ctx);
		log.info("create and persist user identifier by userDN: {} with filter: {}", userDN, filter);
		LDAPUserVisitor visitor = new LDAPUserVisitor(syncConfiguration);	
		ldapDao.search(visitor, userDN, filter, syncConfiguration.getUserAttributes(), ctx);

		Identity newIdentity = null;
		List<LDAPUser> ldapUser = visitor.getLdapUserList();
		if(ldapUser != null && !ldapUser.isEmpty()) {
			Attributes userAttributes = ldapUser.get(0).getAttributes();
			newIdentity = createAndPersistUser(userAttributes);
		}
		return newIdentity;
	}

Alan Moran's avatar
Alan Moran committed
	/**
	 * Creates User in OLAT and ads user to LDAP securityGroup Required Attributes
	 * have to be checked before this method.
	 * 
	 * @param userAttributes Set of LDAP Attribute of User to be created
	 */
	@Override
	public Identity createAndPersistUser(Attributes userAttributes) {
Alan Moran's avatar
Alan Moran committed
		// Get and Check Config
		String[] reqAttrs = syncConfiguration.checkRequestAttributes(userAttributes);
Alan Moran's avatar
Alan Moran committed
		if (reqAttrs != null) {
			log.warn("Can not create and persist user, the following attributes are missing::{}", ArrayUtils.toString(reqAttrs));
		String uid = getAttributeValue(userAttributes.get(syncConfiguration
				.getOlatPropertyToLdapAttribute(LDAPConstants.LDAP_USER_IDENTIFYER)));
		String email = getAttributeValue(userAttributes.get(syncConfiguration.getOlatPropertyToLdapAttribute(UserConstants.EMAIL)));
Alan Moran's avatar
Alan Moran committed
		// Lookup user
		if (securityManager.findIdentityByLogin(uid) != null) {
			log.error("Can't create user with username='{}', this username does already exist in the database", uid);
			return null;
		}
		if(!securityModule.isIdentityNameAutoGenerated() && securityManager.findIdentityByName(uid) != null) {
			log.error("Can't create user with username='{}', this identity name does already exist in the database", uid);
Alan Moran's avatar
Alan Moran committed
		}
		if (!MailHelper.isValidEmailAddress(email)) {
			// needed to prevent possibly an AssertException in findIdentityByEmail breaking the sync!
			log.error("Cannot try to lookup user {} by email with an invalid email::{}", uid, email);
Alan Moran's avatar
Alan Moran committed
		}
		if (!userManager.isEmailAllowed(email)) {
			log.error("Can't create user with email='{}', a user with that email does already exist in the database", email);
Alan Moran's avatar
Alan Moran committed
		}
		
		// Create User (first and lastname is added in next step)
		User user = userManager.createUser(null, null, email);
		// Set User Property's (Iterates over Attributes and gets OLAT Property out
		// of olatexconfig.xml)
		NamingEnumeration<? extends Attribute> neAttr = userAttributes.getAll();
Alan Moran's avatar
Alan Moran committed
		try {
			while (neAttr.hasMore()) {
				Attribute attr = neAttr.next();
				String olatProperty = mapLdapAttributeToOlatProperty(attr.getID());
				if (!attr.getID().equalsIgnoreCase(syncConfiguration.getOlatPropertyToLdapAttribute(LDAPConstants.LDAP_USER_IDENTIFYER)) ) {
Alan Moran's avatar
Alan Moran committed
					String ldapValue = getAttributeValue(attr);
					if (olatProperty == null || ldapValue == null) continue;
					user.setProperty(olatProperty, ldapValue);
srosse's avatar
srosse committed
				} 
Alan Moran's avatar
Alan Moran committed
			}
			// Add static user properties from the configuration
			Map<String, String> staticProperties = syncConfiguration.getStaticUserProperties();
Alan Moran's avatar
Alan Moran committed
			if (staticProperties != null && staticProperties.size() > 0) {
				for (Entry<String, String> staticProperty : staticProperties.entrySet()) {
					user.setProperty(staticProperty.getKey(), staticProperty.getValue());
				}
			}
		} catch (NamingException e) {
			log.error("NamingException when trying to create and persist LDAP user with username::" + uid, e);
			return null;
Alan Moran's avatar
Alan Moran committed
		} catch (Exception e) {
			// catch any exception here to properly log error
			log.error("Unknown exception when trying to create and persist LDAP user with username::" + uid, e);
			return null;
		// Create Identity and add it to the default organization
		String identityName = securityModule.isIdentityNameAutoGenerated() ? null : uid;
		Identity identity = securityManager.createAndPersistIdentityAndUserWithOrganisation(identityName, uid, null, user,
				LDAPAuthenticationController.PROVIDER_LDAP, uid, null, null);
		log.info("Created LDAP user username::{}", uid);
Alan Moran's avatar
Alan Moran committed
	}
	
	

	/**
	 * Checks if LDAP properties are different then OLAT properties of a User. If
	 * they are different a Map (OlatPropertyName,LDAPValue) is returned.
	 * 
	 * @param attributes Set of LDAP Attribute of Identity
	 * @param identity Identity to compare
	 * 
	 * @return Map(OlatPropertyName,LDAPValue) of properties Identity, where
	 *         property has changed. NULL is returned it no attributes have to be synced
	 */
	@SuppressWarnings("unchecked")
	public Map<String, String> prepareUserPropertyForSync(Attributes attributes, Identity identity) {
		Map<String, String> olatPropertyMap = new HashMap<>();
Alan Moran's avatar
Alan Moran committed
		User user = identity.getUser();
		NamingEnumeration<Attribute> neAttrs = (NamingEnumeration<Attribute>) attributes.getAll();
		try {
			while (neAttrs.hasMore()) {
				Attribute attr = neAttrs.next();
				String olatProperty = mapLdapAttributeToOlatProperty(attr.getID());
				if(olatProperty == null) {
					continue;
				}
				String ldapValue = getAttributeValue(attr);
				String olatValue = user.getProperty(olatProperty, null);
				if (olatValue == null) {
					// new property or user ID (will always be null, pseudo property)
					olatPropertyMap.put(olatProperty, ldapValue);
				} else {
					if (ldapValue.compareTo(olatValue) != 0) {
						olatPropertyMap.put(olatProperty, ldapValue);
					}
				}
			}
srosse's avatar
srosse committed
			if (olatPropertyMap.size() == 1 && olatPropertyMap.get(LDAPConstants.LDAP_USER_IDENTIFYER) != null) {
				log.debug("propertymap for identity " + identity.getKey() + " contains only userID, NOTHING TO SYNC!");
srosse's avatar
srosse committed
				return null;
			} else {
				log.debug("propertymap for identity " + identity.getKey() + " contains " + olatPropertyMap.size() + " items (" + olatPropertyMap.keySet() + ") to be synced later on");
srosse's avatar
srosse committed
				return olatPropertyMap;
			}
Alan Moran's avatar
Alan Moran committed

		} catch (NamingException e) {
			log.error("NamingException when trying to prepare user properties for LDAP sync", e);
Alan Moran's avatar
Alan Moran committed
			return null;
		}
	}
	
	/**
	 * Maps LDAP Attributes to the OLAT Property 
	 * 
	 * Configuration: LDAP Attributes Map = ldapContext.xml (property=userAttrs)
Alan Moran's avatar
Alan Moran committed
	 * 
	 * @param attrID LDAP Attribute
	 * @return OLAT Property
	 */
	private String mapLdapAttributeToOlatProperty(String attrID) {
		Map<String, String> userAttrMapper = syncConfiguration.getUserAttributeMap();
Alan Moran's avatar
Alan Moran committed
	}
	
	/**
	 * Extracts Value out of LDAP Attribute
	 * 
	 * 
	 * @param attribute LDAP Naming Attribute 
	 * @return String value of Attribute, null on Exception
	 * 
	 * @throws NamingException
	 */
	private String getAttributeValue(Attribute attribute) {
		try {
Alan Moran's avatar
Alan Moran committed
		} catch (NamingException e) {
			log.error("NamingException when trying to get attribute value for attribute::" + attribute, e);
Alan Moran's avatar
Alan Moran committed
			return null;
		}
	}

	/**
	 * The method search in LDAP the user, search the groups
	 * of which it is member of, and sync the groups. The method doesn't
	 * work if the login attribute is not the same as the user identifier.
	 * 
	 * @param identity The identity to sync
	 */
	@Override
	public void syncUserGroups(Identity identity) {	
		LdapContext ctx = bindSystem();
		if (ctx == null) {
			log.error("could not bind to ldap");
		
		// This doesn't work if the login attribute is not the same as the user identifier.
		String ldapUserIDAttribute = syncConfiguration.getOlatPropertyToLdapAttribute(LDAPConstants.LDAP_USER_IDENTIFYER);
		Authentication authentication = authenticationDao.getAuthentication(identity, LDAPAuthenticationController.PROVIDER_LDAP);
		String filter = ldapDao.buildSearchUserFilter(ldapUserIDAttribute, authentication.getAuthusername());

		boolean withCoacheOfGroups = StringHelper.containsNonWhitespace(syncConfiguration.getCoachedGroupAttribute());
		List<String> ldapBases = syncConfiguration.getLdapBases();
		String[] searchAttr;
		if(withCoacheOfGroups) {
			searchAttr = new String[]{ "dn", syncConfiguration.getCoachedGroupAttribute() };
		} else {
			searchAttr = new String[]{ "dn" };
		}

		SearchControls ctls = new SearchControls();
		ctls.setSearchScope(SearchControls.SUBTREE_SCOPE);
		ctls.setReturningAttributes(searchAttr);

		String userDN = null;
		List<String> groupList = 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(withCoacheOfGroups) {
						Attributes resAttributes = result.getAttributes();
						Attribute coachOfGroupsAttr = resAttributes.get(syncConfiguration.getCoachedGroupAttribute());
						if(coachOfGroupsAttr != null && coachOfGroupsAttr.get() instanceof String) {
							String groupString = (String)coachOfGroupsAttr.get();
							if(!"-".equals(groupString)) {
								String[] groupArr = groupString.split(syncConfiguration.getCoachedGroupAttributeSeparator());
								groupList = new ArrayList<>(groupArr.length);
								for(String group:groupArr) {
									groupList.add(group);
								}
							}
						}
					}
				}
				if (userDN != null) {
					break;
				}
			} catch (NamingException e) {
				log.error("NamingException when trying to bind user with username::" + identity.getKey() + " on ldapBase::" + ldapBase, e);
			}
		}

		// get the potential groups
		if(userDN != null) {
			List<String> groupDNs = syncConfiguration.getLdapGroupBases();
			String groupFilter = "(&(objectClass=groupOfNames)(member=" + userDN + "))";
			List<LDAPGroup> groups = ldapDao.searchGroups(ctx, groupDNs, groupFilter);
			for(LDAPGroup group:groups) {
				BusinessGroup managedGroup = getManagerBusinessGroup(group.getCommonName());
				if(managedGroup != null) {
					List<String> roles = businessGroupRelationDao.getRoles(identity, managedGroup);
					if(roles.isEmpty()) {
						boolean coach = groupList != null && groupList.contains(group.getCommonName());
						if(coach) {
							businessGroupRelationDao.addRole(identity, managedGroup, GroupRoles.coach.name());
						} else {
							businessGroupRelationDao.addRole(identity, managedGroup, GroupRoles.participant.name());
						}
					}
				}
			}
		}
	}

Alan Moran's avatar
Alan Moran committed
	/**
	 * Searches for identity with the login attribute, with fallback to the uid attribute.
	 * If not found and configured to, try to convert an identity with an OLAT authentication
	 * to LDAP, or if the identity name are manually generated, try to convert an identity
	 * with the right name to LDAP.
Alan Moran's avatar
Alan Moran committed
	 * 
	 * @param uid Name of Identity
	 * @param errors LDAPError Object if user exits but not member of
	 *          LDAPSecurityGroup
	 * 
	 * @return Identity if it's found and member of LDAPSecurityGroup, null
	 *         otherwise (if user exists but not managed by LDAP, error Object is
	 *         modified)
	 */
	public Identity findIdentityByLdapAuthentication(Attributes attrs, LDAPError errors) {
		if(attrs == null) {
			errors.insert("findIdentyByLdapAuthentication: attrs::null");
			return null;
		}
		
		String token = getAttributeValue(attrs.get(syncConfiguration.getLdapUserLoginAttribute()));
		Authentication ldapAuth = authenticationDao.getAuthentication(token, LDAPAuthenticationController.PROVIDER_LDAP);
		if(ldapAuth != null) {
			return ldapAuth.getIdentity();
		String uid = getAttributeValue(attrs.get(syncConfiguration
				.getOlatPropertyToLdapAttribute(LDAPConstants.LDAP_USER_IDENTIFYER)));
		ldapAuth = authenticationDao.getAuthentication(uid, LDAPAuthenticationController.PROVIDER_LDAP);
		if(ldapAuth != null) {
			if(StringHelper.containsNonWhitespace(token) && !token.equals(ldapAuth.getAuthusername())) {
				ldapAuth = securityManager.updateAuthentication(ldapAuth);
Alan Moran's avatar
Alan Moran committed
			}
			return ldapAuth.getIdentity();
Alan Moran's avatar
Alan Moran committed
		}
		
		if(ldapLoginModule.isConvertExistingLocalUsersToLDAPUsers()) {
			Authentication defaultAuth = authenticationDao.getAuthentication(uid, "OLAT");
			if(defaultAuth != null) {
				// Add user to LDAP security group and add the ldap provider
				securityManager.createAndPersistAuthentication(defaultAuth.getIdentity(), LDAPAuthenticationController.PROVIDER_LDAP, token, null, null);
				log.info("Found identity by LDAP username that was not yet in LDAP security group. Converted user::{} to be an LDAP managed user", uid);
				return defaultAuth.getIdentity();
			}
			Identity identity = null;
			if(securityModule.isIdentityNameAutoGenerated()) {
				identity = securityManager.findIdentityByNickName(uid);
			} else {
				identity = securityManager.findIdentityByName(uid);
			}
			if(identity != null) {
				securityManager.createAndPersistAuthentication(identity, LDAPAuthenticationController.PROVIDER_LDAP, token, null, null);
				log.info(Tracing.M_AUDIT, "Found identity by identity name that was not yet in LDAP security group. Converted user::{} to be an LDAP managed user", uid);
				return identity;
Alan Moran's avatar
Alan Moran committed
	}

	/**
	 * 
	 * Creates list of all OLAT Users which have been deleted out of the LDAP
	 * directory but still exits in OLAT
	 * 
	 * Configuration: Required Attributes = ldapContext.xml (property=reqAttrs)
	 * LDAP Base = ldapContext.xml (property=ldapBase)
Alan Moran's avatar
Alan Moran committed
	 * 
	 * @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 Identity from the user which have been deleted in
	 *         LDAP
	 * 
	 * @throws NamingException
	 */
	public List<Identity> getIdentitiesDeletedInLdap(LdapContext ctx) {
			return Collections.emptyList();
Alan Moran's avatar
Alan Moran committed
		// Find all LDAP Users
		List<String> returningAttrList = new ArrayList<>(2);
		String userID = syncConfiguration.getOlatPropertyToLdapAttribute(LDAPConstants.LDAP_USER_IDENTIFYER);
		returningAttrList.add(userID);
		String loginAttr = syncConfiguration.getLdapUserLoginAttribute();
		if(loginAttr != null && !loginAttr.equals(userID)) {
			returningAttrList.add(loginAttr);
		}
		
		String[] returningAttrs = returningAttrList.toArray(new String[returningAttrList.size()]);
		String userFilter = syncConfiguration.getLdapUserFilter();
		final Set<String> ldapList = new HashSet<>();
		ldapDao.searchInLdap(new LDAPVisitor() {
			@Override
Alan Moran's avatar
Alan Moran committed
			public void visit(SearchResult result) throws NamingException {
				Attributes attrs = result.getAttributes();
				NamingEnumeration<? extends Attribute> aEnum = attrs.getAll();
				while (aEnum.hasMore()) {
					Attribute attr = aEnum.next();
					// use lowercase username
					ldapList.add(attr.get().toString().toLowerCase());
				}
			}
		}, (userFilter == null ? "" : userFilter), returningAttrs, ctx);
Alan Moran's avatar
Alan Moran committed

		if (ldapList.isEmpty()) {
			log.warn("No users in LDAP found, can't create the deletion list.");
Alan Moran's avatar
Alan Moran committed
		}
		List<Identity> identityListToDelete = new ArrayList<>();
		List<Authentication> ldapAuthentications = authenticationDao.getAuthentications(LDAPAuthenticationController.PROVIDER_LDAP);
		for (Authentication ldapAuthentication:ldapAuthentications) {
			if (!ldapList.contains(ldapAuthentication.getAuthusername().toLowerCase())) {
				identityListToDelete.add(ldapAuthentication.getIdentity());
		dbInstance.commitAndCloseSession();
Alan Moran's avatar
Alan Moran committed
		return identityListToDelete;
	}

	/**
	 * Execute Batch Sync. Will update all Attributes of LDAP users in OLAt, create new users and delete users in OLAT.
	 * Can be configured in ldapContext.xml
Alan Moran's avatar
Alan Moran committed
	 * 
	 * @param LDAPError
	 * 
	 */
	public boolean doBatchSync(LDAPError errors) {
srosse's avatar
srosse committed
		//fxdiff: also run on nodes != 1 as nodeid = tomcat-id in fx-environment
//		if(WebappHelper.getNodeId() != 1) {
//			log.warn("Sync happens only on node 1", null);
srosse's avatar
srosse committed
//			return false;
//		}
Alan Moran's avatar
Alan Moran committed
		
		// o_clusterNOK
		// Synchronize on class so that only one thread can read the
		// batchSyncIsRunning flag Only this read operation is synchronized to not
		// block the whole execution of the do BatchSync method. The method is used
		// in automatic cron scheduler job and also in GUI controllers that can't
		// wait for the concurrent running request to finish first, an immediate
		// feedback about the concurrent job is needed. -> only synchronize on the
		// property read.
		synchronized (LDAPLoginManagerImpl.class) {
			if (batchSyncIsRunning) {
				// don't run twice, skip this execution
				log.info("LDAP user doBatchSync started, but another job is still running - skipping this sync");
Alan Moran's avatar
Alan Moran committed
				errors.insert("BatchSync already running by concurrent process");
				return false;
			}
		}
		
		WorkThreadInformations.setLongRunningTask("ldapSync");
		
Alan Moran's avatar
Alan Moran committed
		coordinator.getEventBus().fireEventToListenersOf(new LDAPEvent(LDAPEvent.SYNCHING), ldapSyncLockOres);
		
Alan Moran's avatar
Alan Moran committed
		LdapContext ctx = null;
		boolean success = false;
		try {
			acquireSyncLock();
			long startTime = System.currentTimeMillis();
Alan Moran's avatar
Alan Moran committed
			ctx = bindSystem();
			if (ctx == null) {
				errors.insert("LDAP connection ERROR");
				log.error("LDAP batch sync: LDAP connection empty");
Alan Moran's avatar
Alan Moran committed
				freeSyncLock();
				return success;
			}
			Date timeBeforeSync = new Date();

			//check server capabilities
			// Get time before sync to have a save sync time when sync is successful
			String sinceSentence = (lastSyncDate == null ? "" : " since last sync from " + lastSyncDate);
Alan Moran's avatar
Alan Moran committed
			doBatchSyncDeletedUsers(ctx, sinceSentence);
srosse's avatar
srosse committed
			// bind again to use an initial unmodified context. lookup of server-properties might fail otherwise!
			ctx.close();
			ctx = bindSystem();
			Map<String,LDAPUser> dnToIdentityKeyMap = new HashMap<>();
			List<LDAPUser> ldapUsers = doBatchSyncNewAndModifiedUsers(ctx, sinceSentence, dnToIdentityKeyMap, errors);
			ctx.close();
srosse's avatar
srosse committed
			ctx = bindSystem();
			//sync groups by LDAP groups or attributes