/**
* OLAT - Online Learning and Training<br>
* http://www.olat.org
* <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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <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>
* Copyright (c) since 2004 at Multimedia- & E-Learning Services (MELS),<br>
* University of Zurich, Switzerland.
* <hr>
* <a href="http://www.openolat.org">
* OpenOLAT - Online Learning and Training</a><br>
* This file has been modified by the OpenOLAT community. Changes are licensed
* under the Apache 2.0 license as the original file.
*/
package org.olat.admin.user.delete.service;
import java.io.File;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.persistence.TemporalType;
import org.olat.admin.user.delete.SelectionController;
import org.olat.basesecurity.Authentication;
import org.olat.basesecurity.BaseSecurity;
import org.olat.basesecurity.IdentityImpl;
import org.olat.basesecurity.SecurityGroup;
import org.olat.basesecurity.manager.GroupDAO;
import org.olat.commons.lifecycle.LifeCycleManager;
import org.olat.core.CoreSpringFactory;
import org.olat.core.commons.persistence.DB;
import org.olat.core.commons.persistence.DBFactory;
import org.olat.core.commons.persistence.DBQuery;
import org.olat.core.gui.translator.Translator;
import org.olat.core.id.Identity;
import org.olat.core.id.User;
import org.olat.core.id.UserConstants;
import org.olat.core.manager.BasicManager;
import org.olat.core.util.StringHelper;
import org.olat.core.util.Util;
import org.olat.core.util.i18n.I18nManager;
import org.olat.core.util.mail.MailBundle;
import org.olat.core.util.mail.MailManager;
import org.olat.core.util.mail.MailTemplate;
import org.olat.core.util.mail.MailerResult;
import org.olat.properties.Property;
import org.olat.properties.PropertyManager;
import org.olat.registration.RegistrationManager;
import org.olat.registration.TemporaryKey;
import org.olat.repository.RepositoryDeletionModule;
import org.olat.user.UserDataDeletable;
import org.olat.user.UserManager;
import org.olat.user.propertyhandlers.UserPropertyHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* Manager for user-deletion.
*
* @author Christian Guretzki
*/
@Service("userDeletionManager")
public class UserDeletionManager extends BasicManager {
public static final String DELETED_USER_DELIMITER = "_bkp_";
/** Default value for last-login duration in month. */
private static final int DEFAULT_LAST_LOGIN_DURATION = 24;
/** Default value for delete-email duration in days. */
private static final int DEFAULT_DELETE_EMAIL_DURATION = 30;
private static final String LAST_LOGIN_DURATION_PROPERTY_NAME = "LastLoginDuration";
private static final String DELETE_EMAIL_DURATION_PROPERTY_NAME = "DeleteEmailDuration";
private static final String PROPERTY_CATEGORY = "UserDeletion";
private static UserDeletionManager INSTANCE;
public static final String SEND_DELETE_EMAIL_ACTION = "sendDeleteEmail";
private static final String USER_ARCHIVE_DIR = "archive_deleted_users";
private static final String USER_DELETED_ACTION = "userdeleted";
private static boolean keepUserLoginAfterDeletion;
private static boolean keepUserEmailAfterDeletion;
// Flag used in user-delete to indicate that all deletable managers are initialized
@Autowired
private RepositoryDeletionModule deletionModule;
@Autowired
private RegistrationManager registrationManager;
@Autowired
private BaseSecurity securityManager;
@Autowired
private MailManager mailManager;
@Autowired
private GroupDAO groupDao;
@Autowired
private DB dbInstance;
/**
* [used by spring]
*/
private UserDeletionManager() {
INSTANCE = this;
}
/**
* @return Singleton.
*/
public static UserDeletionManager getInstance() { return INSTANCE; }
/**
* Send 'delete'- emails to a list of identities. The delete email is an announcement for the user-deletion.
*
* @param selectedIdentities
* @return String with warning message (e.g. email-address not valid, could not send email).
* If there is no warning, the return String is empty ("").
*/
public String sendUserDeleteEmailTo(List<Identity> selectedIdentities, MailTemplate template,
boolean isTemplateChanged, String keyEmailSubject, String keyEmailBody, Identity sender, Translator pT ) {
StringBuilder buf = new StringBuilder();
if (template != null) {
template.addToContext("responseTo", deletionModule.getEmailResponseTo());
for (Iterator<Identity> iter = selectedIdentities.iterator(); iter.hasNext();) {
Identity identity = iter.next();
if (!isTemplateChanged) {
// Email template has NOT changed => take translated version of subject and body text
Translator identityTranslator = Util.createPackageTranslator(SelectionController.class, I18nManager.getInstance().getLocaleOrDefault(identity.getUser().getPreferences().getLanguage()));
template.setSubjectTemplate(identityTranslator.translate(keyEmailSubject));
template.setBodyTemplate(identityTranslator.translate(keyEmailBody));
}
template.putVariablesInMailContext(template.getContext(), identity);
logDebug(" Try to send Delete-email to identity=" + identity.getName() + " with email=" + identity.getUser().getProperty(UserConstants.EMAIL, null));
MailerResult result = new MailerResult();
MailBundle bundle = mailManager.makeMailBundle(null, identity, template, null, null, result);
if(bundle != null) {
mailManager.sendMessage(bundle);
}
if(template.getCpfrom()) {
MailBundle ccBundle = mailManager.makeMailBundle(null, sender, template, sender, null, result);
if(ccBundle != null) {
mailManager.sendMessage(ccBundle);
}
}
if (result.getReturnCode() != MailerResult.OK) {
buf.append(pT.translate("email.error.send.failed", new String[] {identity.getUser().getProperty(UserConstants.EMAIL, null), identity.getName()} )).append("\n");
}
logAudit("User-Deletion: Delete-email send to identity=" + identity.getName() + " with email=" + identity.getUser().getProperty(UserConstants.EMAIL, null));
markSendEmailEvent(identity);
}
} else {
// no template => User decides to sending no delete-email, mark only in lifecycle table 'sendEmail'
for (Iterator<Identity> iter = selectedIdentities.iterator(); iter.hasNext();) {
Identity identity = iter.next();
logAudit("User-Deletion: Move in 'Email sent' section without sending email, identity=" + identity.getName());
markSendEmailEvent(identity);
}
}
return buf.toString();
}
private void markSendEmailEvent(Identity identity) {
LifeCycleManager.createInstanceFor(identity).markTimestampFor(SEND_DELETE_EMAIL_ACTION);
}
/**
* Return list of identities which have last-login older than 'lastLoginDuration' parameter.
* This user are ready to start with user-deletion process.
* @param lastLoginDuration last-login duration in month
* @return List of Identity objects
*/
public List<Identity> getDeletableIdentities(int lastLoginDuration) {
Calendar lastLoginLimit = Calendar.getInstance();
lastLoginLimit.add(Calendar.MONTH, - lastLoginDuration);
logDebug("lastLoginLimit=" + lastLoginLimit);
// 1. get all 'active' identities with lastlogin > x
StringBuilder sb = new StringBuilder();
sb.append("select ident from ").append(IdentityImpl.class.getName()).append(" as ident")
.append(" inner join fetch ident.user as user")
.append(" where ident.status=").append(Identity.STATUS_ACTIV).append(" and (ident.lastLogin = null or ident.lastLogin < :lastLogin)");
List<Identity> identities = dbInstance.getCurrentEntityManager()
.createQuery(sb.toString(), Identity.class)
.setParameter("lastLogin", lastLoginLimit.getTime(), TemporalType.TIMESTAMP)
.getResultList();
// 2. get all 'active' identities in deletion process
String queryStr = "select ident from org.olat.core.id.Identity as ident"
+ " , org.olat.commons.lifecycle.LifeCycleEntry as le"
+ " where ident.key = le.persistentRef "
+ " and le.persistentTypeName ='" + IdentityImpl.class.getName() + "'"
+ " and le.action ='" + SEND_DELETE_EMAIL_ACTION + "' ";
DBQuery dbq = DBFactory.getInstance().createQuery(queryStr);
List<Identity> identitiesInProcess = dbq.list();
// 3. Remove all identities in deletion-process from all inactive-identities
identities.removeAll(identitiesInProcess);
return identities;
}
/**
* Return list of identities which are in user-deletion-process.
* user-deletion-process means delete-announcement.email send, duration of waiting for response is not expired.
* @param deleteEmailDuration Duration of user-deletion-process in days
* @return List of Identity objects
*/
public List<Identity> getIdentitiesInDeletionProcess(int deleteEmailDuration) {
Calendar deleteEmailLimit = Calendar.getInstance();
deleteEmailLimit.add(Calendar.DAY_OF_MONTH, - (deleteEmailDuration-1));
logDebug("deleteEmailLimit=" + deleteEmailLimit);
String queryStr = "select ident from org.olat.core.id.Identity as ident"
+ " , org.olat.commons.lifecycle.LifeCycleEntry as le"
+ " where ident.key = le.persistentRef "
+ " and ident.status = " + Identity.STATUS_ACTIV
+ " and le.persistentTypeName ='" + IdentityImpl.class.getName() + "'"
+ " and le.action ='" + SEND_DELETE_EMAIL_ACTION + "' and le.lcTimestamp >= :deleteEmailDate ";
DBQuery dbq = DBFactory.getInstance().createQuery(queryStr);
dbq.setDate("deleteEmailDate", deleteEmailLimit.getTime());
return dbq.list();
}
/**
* Return list of identities which are ready-to-delete in user-deletion-process.
* (delete-announcement.email send, duration of waiting for response is expired).
* @param deleteEmailDuration Duration of user-deletion-process in days
* @return List of Identity objects
*/
public List<Identity> getIdentitiesReadyToDelete(int deleteEmailDuration) {
Calendar deleteEmailLimit = Calendar.getInstance();
deleteEmailLimit.add(Calendar.DAY_OF_MONTH, - (deleteEmailDuration - 1));
logDebug("deleteEmailLimit=" + deleteEmailLimit);
String queryStr = "select ident from org.olat.core.id.Identity as ident"
+ " , org.olat.commons.lifecycle.LifeCycleEntry as le"
+ " where ident.key = le.persistentRef "
+ " and ident.status = " + Identity.STATUS_ACTIV
+ " and le.persistentTypeName ='" + IdentityImpl.class.getName() + "'"
+ " and le.action ='" + SEND_DELETE_EMAIL_ACTION + "' and le.lcTimestamp < :deleteEmailDate ";
DBQuery dbq = DBFactory.getInstance().createQuery(queryStr);
dbq.setDate("deleteEmailDate", deleteEmailLimit.getTime());
return dbq.list();
}
/**
* Delete all user-data in registered deleteable resources.
* @param identity
* @return true
*/
public void deleteIdentity(Identity identity) {
logInfo("Start deleteIdentity for identity=" + identity);
String newName = getBackupStringWithDate(identity.getName());
logInfo("Start Deleting user=" + identity);
File archiveFilePath = getArchivFilePath(identity);
Map<String,UserDataDeletable> userDataDeletableResourcesMap = CoreSpringFactory.getBeansOfType(UserDataDeletable.class);
List<UserDataDeletable> userDataDeletableResources = new ArrayList<>(userDataDeletableResourcesMap.values());
Collections.sort(userDataDeletableResources, new UserDataDeletableComparator());
for (UserDataDeletable element : userDataDeletableResources) {
logInfo("UserDataDeletable-Loop element=" + element);
element.deleteUserData(identity, newName, archiveFilePath);
}
// Delete all authentications for certain identity
List<Authentication> authentications = securityManager.getAuthentications(identity);
for (Authentication auth:authentications) {
logInfo("deleteAuthentication auth=" + auth);
securityManager.deleteAuthentication(auth);
logDebug("Delete auth=" + auth + " of identity=" + identity);
}
//remove identity from its security groups
List<SecurityGroup> securityGroups = securityManager.getSecurityGroupsForIdentity(identity);
for (SecurityGroup secGroup : securityGroups) {
securityManager.removeIdentityFromSecurityGroup(identity, secGroup);
logInfo("Removing user=" + identity + " from security group=" + secGroup.toString());
}
//remove identity from groups
groupDao.removeMemberships(identity);
String key = identity.getUser().getProperty("emchangeKey", null);
TemporaryKey tempKey = registrationManager.loadTemporaryKeyByRegistrationKey(key);
if (tempKey != null) {
registrationManager.deleteTemporaryKey(tempKey);
}
identity = securityManager.loadIdentityByKey(identity.getKey());
//keep login-name only -> change email
User persistedUser = identity.getUser();
List<UserPropertyHandler> userPropertyHandlers = UserManager.getInstance().getAllUserPropertyHandlers();
for (UserPropertyHandler userPropertyHandler : userPropertyHandlers) {
String actualProperty = userPropertyHandler.getName();
if (userPropertyHandler.isDeletable()
&& !(keepUserEmailAfterDeletion && UserConstants.EMAIL.equals(actualProperty))) {
persistedUser.setProperty(actualProperty, null);
}
if((!keepUserEmailAfterDeletion && UserConstants.EMAIL.equals(actualProperty))) {
String oldEmail = userPropertyHandler.getUserProperty(persistedUser, null);
String newEmail = "";
if (StringHelper.containsNonWhitespace(oldEmail)){
newEmail = getBackupStringWithDate(oldEmail);
}
logInfo("Update user-property user=" + persistedUser);
userPropertyHandler.setUserProperty(persistedUser, newEmail);
}
}
UserManager.getInstance().updateUserFromIdentity(identity);
logInfo("deleteUserProperties user=" + persistedUser);
dbInstance.commit();
identity = securityManager.loadIdentityByKey(identity.getKey());
//keep email only -> change login-name
if (!keepUserEmailAfterDeletion) {
identity = securityManager.saveIdentityName(identity, newName, null);
}
//keep everything, change identity.status to deleted
logInfo("Change stater identity=" + identity);
identity = securityManager.saveIdentityStatus(identity, Identity.STATUS_DELETED);
LifeCycleManager.createInstanceFor(identity).deleteTimestampFor(SEND_DELETE_EMAIL_ACTION);
LifeCycleManager.createInstanceFor(identity).markTimestampFor(USER_DELETED_ACTION, createLifeCycleLogDataFor(identity));
logAudit("User-Deletion: Delete all userdata for identity=" + identity);
}
public String getBackupStringWithDate(String original){
DateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmm");
String dateStamp = dateFormat.format(new Date());
return dateStamp + DELETED_USER_DELIMITER + original;
}
private String createLifeCycleLogDataFor(Identity identity) {
StringBuilder buf = new StringBuilder();
buf.append("<identity>");
buf.append("<username>").append(identity.getName()).append("</username>");
buf.append("<lastname>").append(identity.getName()).append("</lastname>");
buf.append("<firstname>").append(identity.getName()).append("</firstname>");
buf.append("<email>").append(identity.getName()).append("</email>");
buf.append("</identity>");
return buf.toString();
}
/**
* Re-activate an identity, lastLogin = now, reset deleteemaildate = null.
* @param identity
*/
public Identity setIdentityAsActiv(final Identity identity) {
securityManager.setIdentityLastLogin(identity);
LifeCycleManager lifeCycleManagerForIdenitiy = LifeCycleManager.createInstanceFor(identity);
if (lifeCycleManagerForIdenitiy.hasLifeCycleEntry(SEND_DELETE_EMAIL_ACTION)) {
logAudit("User-Deletion: Remove from delete-list identity=" + identity);
lifeCycleManagerForIdenitiy.deleteTimestampFor(SEND_DELETE_EMAIL_ACTION);
}
return identity;
}
/**
* @return Return duration in days for waiting for reaction on delete-email.
*/
public int getDeleteEmailDuration() {
return getPropertyByName(DELETE_EMAIL_DURATION_PROPERTY_NAME, DEFAULT_DELETE_EMAIL_DURATION);
}
/**
* @return Return last-login duration in month for user on delete-selection list.
*/
public int getLastLoginDuration() {
return getPropertyByName(LAST_LOGIN_DURATION_PROPERTY_NAME, DEFAULT_LAST_LOGIN_DURATION);
}
private int getPropertyByName(String name, int defaultValue) {
List<Property> properties = PropertyManager.getInstance().findProperties(null, null, null, PROPERTY_CATEGORY, name);
if (properties.size() == 0) {
return defaultValue;
} else {
return properties.get(0).getLongValue().intValue();
}
}
public void setLastLoginDuration(int lastLoginDuration) {
setProperty(LAST_LOGIN_DURATION_PROPERTY_NAME, lastLoginDuration);
}
public void setDeleteEmailDuration(int deleteEmailDuration) {
setProperty(DELETE_EMAIL_DURATION_PROPERTY_NAME, deleteEmailDuration);
}
private void setProperty(String propertyName, int value) {
List<Property> properties = PropertyManager.getInstance().findProperties(null, null, null, PROPERTY_CATEGORY, propertyName);
Property property = null;
if (properties.size() == 0) {
property = PropertyManager.getInstance().createPropertyInstance(null, null, null, PROPERTY_CATEGORY, propertyName, null, new Long(value), null, null);
} else {
property = properties.get(0);
property.setLongValue( new Long(value) );
}
PropertyManager.getInstance().saveProperty(property);
}
private File getArchivFilePath(Identity identity) {
String archiveFilePath = deletionModule.getArchiveRootPath() + File.separator + USER_ARCHIVE_DIR + File.separator + RepositoryDeletionModule.getArchiveDatePath()
+ File.separator + "del_identity_" + identity.getName();
File archiveIdentityRootDir = new File(archiveFilePath);
if (!archiveIdentityRootDir.exists()) {
archiveIdentityRootDir.mkdirs();
}
return archiveIdentityRootDir;
}
/**
* Setter method used by spring
* @param keepUserLoginAfterDeletion The keepUserLoginAfterDeletion to set.
*/
public void setKeepUserLoginAfterDeletion(boolean keepUserLoginAfterDeletion) {
UserDeletionManager.keepUserLoginAfterDeletion = keepUserLoginAfterDeletion;
}
/**
* Setter method used by spring
* @param keepUserEmailAfterDeletion The keepUserEmailAfterDeletion to set.
*/
public void setKeepUserEmailAfterDeletion(boolean keepUserEmailAfterDeletion) {
UserDeletionManager.keepUserEmailAfterDeletion = keepUserEmailAfterDeletion;
}
public static boolean isKeepUserLoginAfterDeletion() {
return keepUserLoginAfterDeletion;
}
public static class UserDataDeletableComparator implements Comparator<UserDataDeletable> {
@Override
public int compare(UserDataDeletable o1, UserDataDeletable o2) {
int p1 = o1.deleteUserDataPriority();
int p2 = o2.deleteUserDataPriority();
return -Integer.compare(p1, p2);
}
}
}