Browse Source

delete account module

Jason Rivard 9 years ago
parent
commit
86130f4794
36 changed files with 739 additions and 39 deletions
  1. 2 0
      src/main/java/password/pwm/PwmConstants.java
  2. 4 0
      src/main/java/password/pwm/config/Configuration.java
  3. 17 0
      src/main/java/password/pwm/config/PwmSetting.java
  4. 3 1
      src/main/java/password/pwm/config/PwmSettingCategory.java
  5. 55 0
      src/main/java/password/pwm/config/profile/DeleteAccountProfile.java
  6. 5 4
      src/main/java/password/pwm/config/profile/ProfileType.java
  7. 10 1
      src/main/java/password/pwm/http/PwmSession.java
  8. 5 4
      src/main/java/password/pwm/http/SessionManager.java
  9. 48 0
      src/main/java/password/pwm/http/bean/DeleteAccountBean.java
  10. 251 0
      src/main/java/password/pwm/http/servlet/DeleteAccountServlet.java
  11. 1 0
      src/main/java/password/pwm/http/servlet/PwmServletDefinition.java
  12. 21 0
      src/main/java/password/pwm/http/servlet/configmanager/DebugItemGenerator.java
  13. 1 0
      src/main/java/password/pwm/http/tag/conditional/PwmIfTest.java
  14. 1 2
      src/main/java/password/pwm/i18n/Admin.java
  15. 1 0
      src/main/java/password/pwm/i18n/Message.java
  16. 1 0
      src/main/java/password/pwm/svc/event/AuditEvent.java
  17. 8 0
      src/main/java/password/pwm/svc/event/AuditService.java
  18. 3 0
      src/main/java/password/pwm/svc/event/AuditVault.java
  19. 5 0
      src/main/java/password/pwm/svc/event/LocalDbAuditVault.java
  20. 8 13
      src/main/java/password/pwm/svc/sessiontrack/SessionTrackService.java
  21. 12 1
      src/main/java/password/pwm/util/ConditionalTaskExecutor.java
  22. 6 4
      src/main/java/password/pwm/util/PasswordData.java
  23. 2 0
      src/main/java/password/pwm/util/queue/EmailQueueManager.java
  24. 2 2
      src/main/resources/password/pwm/AppProperty.properties
  25. 51 0
      src/main/resources/password/pwm/config/PwmSetting.xml
  26. 2 1
      src/main/resources/password/pwm/i18n/Admin.properties
  27. 3 0
      src/main/resources/password/pwm/i18n/Display.properties
  28. 1 0
      src/main/resources/password/pwm/i18n/Message.properties
  29. 23 5
      src/main/resources/password/pwm/i18n/PwmSetting.properties
  30. 1 0
      src/main/resources/password/pwm/svc/event/AuditEvent.properties
  31. 13 1
      src/main/webapp/WEB-INF/jsp/admin-dashboard.jsp
  32. 91 0
      src/main/webapp/WEB-INF/jsp/deleteaccount-agreement.jsp
  33. 64 0
      src/main/webapp/WEB-INF/jsp/deleteaccount-confirm.jsp
  34. 13 0
      src/main/webapp/private/index.jsp
  35. 1 0
      src/main/webapp/public/reference/license.jsp
  36. 4 0
      src/main/webapp/public/resources/style.css

+ 2 - 0
src/main/java/password/pwm/PwmConstants.java

@@ -186,6 +186,8 @@ public abstract class PwmConstants {
         RECOVER_PASSWORD_ENTER_OTP("forgottenpassword-enterotp.jsp"),
         RECOVER_PASSWORD_NAAF("forgottenpassword-naaf.jsp"),
         RECOVER_PASSWORD_REMOTE("forgottenpassword-remote.jsp"),
+        SELF_DELETE_AGREE("deleteaccount-agreement.jsp"),
+        SELF_DELETE_CONFIRM("deleteaccount-confirm.jsp"),
         SETUP_RESPONSES("setupresponses.jsp"),
         SETUP_RESPONSES_CONFIRM("setupresponses-confirm.jsp"),
         SETUP_RESPONSES_HELPDESK("setupresponses-helpdesk.jsp"),

+ 4 - 0
src/main/java/password/pwm/config/Configuration.java

@@ -818,6 +818,10 @@ public class Configuration implements Serializable, SettingReader {
                 newProfile = UpdateAttributesProfile.makeFromStoredConfiguration(storedConfiguration, profileID);
                 break;
 
+            case DeleteAccount:
+                newProfile = DeleteAccountProfile.makeFromStoredConfiguration(storedConfiguration, profileID);
+                break;
+
             default: throw new IllegalArgumentException("unknown profile type: " + profileType.toString());
         }
 

+ 17 - 0
src/main/java/password/pwm/config/PwmSetting.java

@@ -172,6 +172,23 @@ public enum PwmSetting {
     ACCOUNT_INFORMATION_VIEW_STATUS_VALUES(
             "accountInfo.viewStatusValues", PwmSettingSyntax.OPTIONLIST, PwmSettingCategory.ACCOUNT_INFO),
 
+    // delete info
+    DELETE_ACCOUNT_PROFILE_LIST(
+            "deleteAccount.profile.list", PwmSettingSyntax.PROFILE, PwmSettingCategory.GENERAL),
+    DELETE_ACCOUNT_ENABLE(
+            "deleteAccount.enable", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.DELETE_ACCOUNT_SETTINGS),
+    DELETE_ACCOUNT_PERMISSION(
+            "deleteAccount.permission", PwmSettingSyntax.USER_PERMISSION, PwmSettingCategory.DELETE_ACCOUNT_PROFILE),
+    DELETE_ACCOUNT_AGREEMENT(
+            "deleteAccount.agreement", PwmSettingSyntax.LOCALIZED_TEXT_AREA, PwmSettingCategory.DELETE_ACCOUNT_PROFILE),
+    DELETE_ACCOUNT_DELETE_USER_ENTRY(
+            "deleteAccount.deleteEntry", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.DELETE_ACCOUNT_PROFILE),
+    DELETE_ACCOUNT_ACTIONS(
+            "deleteAccount.actions", PwmSettingSyntax.ACTION, PwmSettingCategory.DELETE_ACCOUNT_PROFILE),
+    DELETE_ACCOUNT_NEXT_URL(
+            "deleteAccount.nextUrl", PwmSettingSyntax.STRING, PwmSettingCategory.DELETE_ACCOUNT_PROFILE),
+
+
     //ldap directories
     LDAP_SERVER_URLS(
             "ldap.serverUrls", PwmSettingSyntax.STRING_ARRAY, PwmSettingCategory.LDAP_PROFILE),

+ 3 - 1
src/main/java/password/pwm/config/PwmSettingCategory.java

@@ -146,7 +146,9 @@ public enum PwmSettingCategory {
     HELPDESK_PROFILE            (HELPDESK),
     HELPDESK_SETTINGS           (HELPDESK),
 
-
+    DELETE_ACCOUNT              (MODULES_PRIVATE),
+    DELETE_ACCOUNT_SETTINGS     (DELETE_ACCOUNT),
+    DELETE_ACCOUNT_PROFILE      (DELETE_ACCOUNT)
 
     ;
 

+ 55 - 0
src/main/java/password/pwm/config/profile/DeleteAccountProfile.java

@@ -0,0 +1,55 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.config.profile;
+
+import password.pwm.config.PwmSetting;
+import password.pwm.config.StoredValue;
+import password.pwm.config.stored.StoredConfiguration;
+
+import java.util.Locale;
+import java.util.Map;
+
+public class DeleteAccountProfile extends AbstractProfile implements Profile {
+    private static final ProfileType PROFILE_TYPE = ProfileType.DeleteAccount;
+
+    protected DeleteAccountProfile(String identifier, Map<PwmSetting, StoredValue> storedValueMap) {
+        super(identifier, storedValueMap);
+    }
+
+    public static DeleteAccountProfile makeFromStoredConfiguration(final StoredConfiguration storedConfiguration, final String identifier) {
+        final Map<PwmSetting,StoredValue> valueMap = makeValueMap(storedConfiguration, identifier, PROFILE_TYPE.getCategory());
+        return new DeleteAccountProfile(identifier, valueMap);
+
+    }
+
+    @Override
+    public String getDisplayName(Locale locale)
+    {
+        return this.getIdentifier();
+    }
+
+    @Override
+    public ProfileType profileType() {
+        return PROFILE_TYPE;
+    }
+}

+ 5 - 4
src/main/java/password/pwm/config/profile/ProfileType.java

@@ -26,10 +26,11 @@ import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSettingCategory;
 
 public enum ProfileType {
-    Helpdesk            (true,  PwmSettingCategory.HELPDESK_PROFILE, PwmSetting.HELPDESK_PROFILE_QUERY_MATCH),
-    ForgottenPassword   (false, PwmSettingCategory.RECOVERY_PROFILE, PwmSetting.RECOVERY_PROFILE_QUERY_MATCH),
-    NewUser             (false, PwmSettingCategory.NEWUSER_PROFILE, null),
-    UpdateAttributes    (true,  PwmSettingCategory.UPDATE_PROFILE, PwmSetting.UPDATE_PROFILE_QUERY_MATCH),
+    Helpdesk            (true,  PwmSettingCategory.HELPDESK_PROFILE,    PwmSetting.HELPDESK_PROFILE_QUERY_MATCH),
+    ForgottenPassword   (false, PwmSettingCategory.RECOVERY_PROFILE,    PwmSetting.RECOVERY_PROFILE_QUERY_MATCH),
+    NewUser             (false, PwmSettingCategory.NEWUSER_PROFILE,     null),
+    UpdateAttributes    (true,  PwmSettingCategory.UPDATE_PROFILE,      PwmSetting.UPDATE_PROFILE_QUERY_MATCH),
+    DeleteAccount(true,  PwmSettingCategory.DELETE_ACCOUNT_PROFILE, PwmSetting.DELETE_ACCOUNT_PERMISSION),
     ;
     
     private final boolean authenticated;

+ 10 - 1
src/main/java/password/pwm/http/PwmSession.java

@@ -40,6 +40,8 @@ import password.pwm.util.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.secure.PwmRandom;
 
+import java.io.ByteArrayOutputStream;
+import java.io.ObjectOutputStream;
 import java.io.Serializable;
 import java.math.BigInteger;
 import java.util.*;
@@ -284,6 +286,13 @@ public class PwmSession implements Serializable {
     }
 
     public int size() {
-        return JsonUtil.serialize(this).length();
+        try {
+            final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+            final ObjectOutputStream out = new ObjectOutputStream(byteArrayOutputStream);
+            out.writeObject(this);
+            return byteArrayOutputStream.size();
+        } catch (Exception e) {
+            return 0;
+        }
     }
 }

+ 5 - 4
src/main/java/password/pwm/http/SessionManager.java

@@ -32,10 +32,7 @@ import password.pwm.bean.UserIdentity;
 import password.pwm.bean.UserInfoBean;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.UserPermission;
-import password.pwm.config.profile.HelpdeskProfile;
-import password.pwm.config.profile.Profile;
-import password.pwm.config.profile.ProfileType;
-import password.pwm.config.profile.UpdateAttributesProfile;
+import password.pwm.config.profile.*;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmUnrecoverableException;
@@ -276,4 +273,8 @@ public class SessionManager implements Serializable {
     public UpdateAttributesProfile getUpdateAttributeProfile(final PwmApplication pwmApplication) {
         return (UpdateAttributesProfile)getProfile(pwmApplication, ProfileType.UpdateAttributes);
     }
+
+    public DeleteAccountProfile getSelfDeleteProfile(final PwmApplication pwmApplication) {
+        return (DeleteAccountProfile) getProfile(pwmApplication, ProfileType.DeleteAccount);
+    }
 }

+ 48 - 0
src/main/java/password/pwm/http/bean/DeleteAccountBean.java

@@ -0,0 +1,48 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.http.bean;
+
+import java.util.Collections;
+import java.util.Set;
+
+public class DeleteAccountBean extends PwmSessionBean {
+
+    private boolean agreementPassed;
+
+    public boolean isAgreementPassed() {
+        return agreementPassed;
+    }
+
+    public void setAgreementPassed(boolean agreementPassed) {
+        this.agreementPassed = agreementPassed;
+    }
+
+    public Type getType() {
+        return Type.PUBLIC;
+    }
+
+    public Set<Flag> getFlags() {
+        return Collections.emptySet();
+    }
+
+}

+ 251 - 0
src/main/java/password/pwm/http/servlet/DeleteAccountServlet.java

@@ -0,0 +1,251 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.http.servlet;
+
+import com.novell.ldapchai.ChaiUser;
+import com.novell.ldapchai.exception.ChaiException;
+import com.novell.ldapchai.exception.ChaiUnavailableException;
+import password.pwm.PwmApplication;
+import password.pwm.PwmConstants;
+import password.pwm.bean.UserIdentity;
+import password.pwm.config.ActionConfiguration;
+import password.pwm.config.PwmSetting;
+import password.pwm.config.profile.ProfileType;
+import password.pwm.config.profile.DeleteAccountProfile;
+import password.pwm.error.ErrorInformation;
+import password.pwm.error.PwmError;
+import password.pwm.error.PwmOperationalException;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.http.HttpMethod;
+import password.pwm.http.PwmRequest;
+import password.pwm.http.bean.DeleteAccountBean;
+import password.pwm.svc.event.AuditEvent;
+import password.pwm.svc.event.AuditRecord;
+import password.pwm.svc.event.AuditRecordFactory;
+import password.pwm.util.logging.PwmLogger;
+import password.pwm.util.macro.MacroMachine;
+import password.pwm.util.operations.ActionExecutor;
+
+import javax.servlet.ServletException;
+import javax.servlet.annotation.WebServlet;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+@WebServlet(
+        name="SelfDeleteServlet",
+        urlPatterns={
+                PwmConstants.URL_PREFIX_PRIVATE + "/delete",
+                PwmConstants.URL_PREFIX_PRIVATE + "/DeleteAccount"
+        }
+)
+public class DeleteAccountServlet extends AbstractPwmServlet {
+
+    private static final PwmLogger LOGGER = PwmLogger.forClass(DeleteAccountServlet.class);
+
+    public enum DeleteAccountAction implements AbstractPwmServlet.ProcessAction {
+        agree(HttpMethod.POST),
+        delete(HttpMethod.POST),
+        reset(HttpMethod.POST),
+
+        ;
+
+        private final HttpMethod method;
+
+        DeleteAccountAction(HttpMethod method)
+        {
+            this.method = method;
+        }
+
+        public Collection<HttpMethod> permittedMethods()
+        {
+            return Collections.singletonList(method);
+        }
+    }
+
+    protected DeleteAccountAction readProcessAction(final PwmRequest request)
+            throws PwmUnrecoverableException
+
+    {
+        try {
+            return DeleteAccountAction.valueOf(request.readParameterAsString(PwmConstants.PARAM_ACTION_REQUEST));
+        } catch (IllegalArgumentException e) {
+            return null;
+        }
+    }
+
+    @Override
+    protected void processAction(PwmRequest pwmRequest)
+            throws ServletException, IOException, ChaiUnavailableException, PwmUnrecoverableException
+    {
+        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
+        final DeleteAccountProfile deleteAccountProfile = pwmRequest.getPwmSession().getSessionManager().getSelfDeleteProfile(pwmApplication);
+        final DeleteAccountBean deleteAccountBean = pwmApplication.getSessionStateService().getBean(pwmRequest, DeleteAccountBean.class);
+
+        if (!pwmApplication.getConfig().readSettingAsBoolean(PwmSetting.DELETE_ACCOUNT_ENABLE)) {
+            pwmRequest.respondWithError(new ErrorInformation(PwmError.ERROR_SERVICE_NOT_AVAILABLE, "Setting " + PwmSetting.DELETE_ACCOUNT_ENABLE.toMenuLocationDebug(null,null) + " is not enabled."));
+            return;
+        }
+
+        if (deleteAccountProfile == null) {
+            pwmRequest.respondWithError(new ErrorInformation(PwmError.ERROR_NO_PROFILE_ASSIGNED));
+            return;
+        }
+
+        final DeleteAccountAction action = readProcessAction(pwmRequest);
+        if (action != null) {
+            switch (action) {
+                case agree:
+                    handleAgreeRequest(pwmRequest, deleteAccountBean);
+                    break;
+
+                case reset:
+                    handleResetRequest(pwmRequest);
+                    return;
+
+                case delete:
+                    handleDeleteRequest(pwmRequest, deleteAccountProfile);
+                    return;
+
+            }
+        }
+
+        advancedToNextStage(pwmRequest, deleteAccountProfile, deleteAccountBean);
+    }
+
+    private void advancedToNextStage(PwmRequest pwmRequest, final DeleteAccountProfile profile, final DeleteAccountBean bean)
+            throws PwmUnrecoverableException, ServletException, IOException
+    {
+
+        final String selfDeleteAgreementText = profile.readSettingAsLocalizedString(
+                PwmSetting.DELETE_ACCOUNT_AGREEMENT,
+                pwmRequest.getPwmSession().getSessionStateBean().getLocale()
+        );
+
+        if (selfDeleteAgreementText != null && !selfDeleteAgreementText.trim().isEmpty()) {
+            if (!bean.isAgreementPassed()) {
+                final MacroMachine macroMachine = pwmRequest.getPwmSession().getSessionManager().getMacroMachine(pwmRequest.getPwmApplication());
+                final String expandedText = macroMachine.expandMacros(selfDeleteAgreementText);
+                pwmRequest.setAttribute(PwmRequest.Attribute.AgreementText, expandedText);
+                pwmRequest.forwardToJsp(PwmConstants.JSP_URL.SELF_DELETE_AGREE);
+                return;
+            }
+        }
+
+        pwmRequest.forwardToJsp(PwmConstants.JSP_URL.SELF_DELETE_CONFIRM);
+    }
+
+    private void handleResetRequest(
+            final PwmRequest pwmRequest
+    )
+            throws ServletException, IOException, PwmUnrecoverableException, ChaiUnavailableException
+    {
+        pwmRequest.getPwmApplication().getSessionStateService().clearBean(pwmRequest, DeleteAccountBean.class);
+        pwmRequest.sendRedirectToContinue();
+    }
+
+    private void handleAgreeRequest(
+            final PwmRequest pwmRequest,
+            final DeleteAccountBean deleteAccountBean
+    )
+            throws ServletException, IOException, PwmUnrecoverableException, ChaiUnavailableException
+    {
+        LOGGER.debug(pwmRequest, "user accepted agreement");
+
+        if (!deleteAccountBean.isAgreementPassed()) {
+            deleteAccountBean.setAgreementPassed(true);
+            AuditRecord auditRecord = new AuditRecordFactory(pwmRequest).createUserAuditRecord(
+                    AuditEvent.AGREEMENT_PASSED,
+                    pwmRequest.getUserInfoIfLoggedIn(),
+                    pwmRequest.getSessionLabel(),
+                    ProfileType.DeleteAccount.toString()
+            );
+            pwmRequest.getPwmApplication().getAuditManager().submit(auditRecord);
+        }
+    }
+
+    private void handleDeleteRequest(
+            final PwmRequest pwmRequest,
+            final DeleteAccountProfile profile
+    )
+            throws ServletException, IOException, PwmUnrecoverableException, ChaiUnavailableException {
+        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
+        final DeleteAccountProfile deleteAccountProfile = pwmRequest.getPwmSession().getSessionManager().getSelfDeleteProfile(pwmApplication);
+        final UserIdentity userIdentity = pwmRequest.getUserInfoIfLoggedIn();
+
+
+        {  // execute configured actions
+            final List<ActionConfiguration> actions = deleteAccountProfile.readSettingAsAction(PwmSetting.DELETE_ACCOUNT_ACTIONS);
+            if (actions != null && !actions.isEmpty()) {
+                LOGGER.debug(pwmRequest, "executing configured actions to user " + userIdentity);
+
+
+                final ActionExecutor actionExecutor = new ActionExecutor.ActionExecutorSettings(pwmApplication, userIdentity)
+                        .setExpandPwmMacros(true)
+                        .setMacroMachine(pwmRequest.getPwmSession().getSessionManager().getMacroMachine(pwmApplication))
+                        .createActionExecutor();
+
+                try {
+                    actionExecutor.executeActions(actions, pwmRequest.getPwmSession());
+                } catch (PwmOperationalException e) {
+                    LOGGER.error("error during user delete action execution: ", e);
+                    throw new PwmUnrecoverableException(e.getErrorInformation(),e.getCause());
+                }
+            }
+        }
+
+        // mark the event log
+        pwmApplication.getAuditManager().submit(AuditEvent.DELETE_ACCOUNT, pwmRequest.getPwmSession().getUserInfoBean(), pwmRequest.getPwmSession());
+
+        // perform ldap entry delete.
+        if (deleteAccountProfile.readSettingAsBoolean(PwmSetting.DELETE_ACCOUNT_DELETE_USER_ENTRY)) {
+            final ChaiUser chaiUser = pwmApplication.getProxiedChaiUser(pwmRequest.getUserInfoIfLoggedIn());
+            try {
+                chaiUser.getChaiProvider().deleteEntry(chaiUser.getEntryDN());
+            } catch (ChaiException e) {
+                final PwmUnrecoverableException pwmException = PwmUnrecoverableException.fromChaiException(e);
+                LOGGER.error("error during user delete", pwmException);
+                throw pwmException;
+            }
+        }
+
+        // clear the delete bean
+        pwmApplication.getSessionStateService().clearBean(pwmRequest, DeleteAccountBean.class);
+
+        final String nextUrl = profile.readSettingAsString(PwmSetting.DELETE_ACCOUNT_NEXT_URL);
+        if (nextUrl != null && !nextUrl.isEmpty()) {
+            final MacroMachine macroMachine = pwmRequest.getPwmSession().getSessionManager().getMacroMachine(pwmApplication);
+            final String macroedUrl = macroMachine.expandMacros(nextUrl);
+            LOGGER.debug(pwmRequest, "settinging forward url to post-delete next url: " + macroedUrl);
+            pwmRequest.getPwmSession().getSessionStateBean().setForwardURL(macroedUrl);
+        }
+
+        // delete finished, so logout and redirect.
+        pwmRequest.getPwmSession().unauthenticateUser(pwmRequest);
+        pwmRequest.sendRedirectToContinue();
+    }
+
+
+
+}

+ 1 - 0
src/main/java/password/pwm/http/servlet/PwmServletDefinition.java

@@ -53,6 +53,7 @@ public enum PwmServletDefinition {
     Shortcuts(password.pwm.http.servlet.ShortcutServlet.class),
     PeopleSearch(password.pwm.http.servlet.peoplesearch.PeopleSearchServlet.class),
     GuestRegistration(password.pwm.http.servlet.GuestRegistrationServlet.class),
+    SelfDelete(DeleteAccountServlet.class),
 
     Admin(password.pwm.http.servlet.AdminServlet.class),
     ConfigGuide(ConfigGuideServlet.class),

+ 21 - 0
src/main/java/password/pwm/http/servlet/configmanager/DebugItemGenerator.java

@@ -37,6 +37,7 @@ import password.pwm.http.PwmRequest;
 import password.pwm.ldap.LdapDebugDataGenerator;
 import password.pwm.svc.PwmService;
 import password.pwm.util.*;
+import password.pwm.util.localdb.LocalDB;
 import password.pwm.util.logging.LocalDBLogger;
 import password.pwm.util.logging.PwmLogEvent;
 import password.pwm.util.logging.PwmLogLevel;
@@ -67,6 +68,7 @@ public class DebugItemGenerator {
             LogDebugItemGenerator.class,
             LdapDebugItemGenerator.class,
             LDAPPermissionItemGenerator.class,
+            LocalDBDebugGenerator.class,
             SessionDataGenerator.class
     ));
 
@@ -493,6 +495,24 @@ public class DebugItemGenerator {
         }
     }
 
+    static class LocalDBDebugGenerator implements Generator {
+        @Override
+        public String getFilename() {
+            return "localDBDebug.json";
+        }
+
+        @Override
+        public void outputItem(
+                final PwmApplication pwmApplication,
+                final PwmRequest pwmRequest,
+                final OutputStream outputStream
+        ) throws Exception {
+            final LocalDB localDB = pwmApplication.getLocalDB();
+            final Map<String,Serializable> serializableMap = localDB.debugInfo();
+            outputStream.write(JsonUtil.serializeMap(serializableMap, JsonUtil.Flag.PrettyPrint).getBytes(PwmConstants.DEFAULT_CHARSET));
+        }
+    }
+
     static class SessionDataGenerator implements Generator {
         @Override
         public String getFilename() {
@@ -555,4 +575,5 @@ public class DebugItemGenerator {
                 OutputStream outputStream
         ) throws Exception;
     }
+
 }

+ 1 - 0
src/main/java/password/pwm/http/tag/conditional/PwmIfTest.java

@@ -62,6 +62,7 @@ public enum PwmIfTest {
 
     updateProfileAvailable(new BooleanPwmSettingTest(PwmSetting.UPDATE_PROFILE_ENABLE), new ActorHasProfileTest(ProfileType.UpdateAttributes)),
     helpdeskAvailable(new BooleanPwmSettingTest(PwmSetting.HELPDESK_ENABLE), new ActorHasProfileTest(ProfileType.Helpdesk)),
+    DeleteAccountAvailalable(new BooleanPwmSettingTest(PwmSetting.DELETE_ACCOUNT_ENABLE), new ActorHasProfileTest(ProfileType.DeleteAccount)),
     guestRegistrationAvailable(new BooleanPwmSettingTest(PwmSetting.GUEST_ENABLE), new BooleanPermissionTest(Permission.GUEST_REGISTRATION)),
 
     booleanSetting(new BooleanPwmSettingTest(null)),

+ 1 - 2
src/main/java/password/pwm/i18n/Admin.java

@@ -44,6 +44,7 @@ public enum Admin implements PwmDisplayBundle {
     EventLog_Narrative_ActivateUser,
     EventLog_Narrative_CreateUser,
     EventLog_Narrative_UpdateProfile,
+    EventLog_Narrative_DeleteAccount,
     EventLog_Narrative_IntruderUserLock,
     EventLog_Narrative_IntruderUserAttempt,
     EventLog_Narrative_TokenIssued,
@@ -63,8 +64,6 @@ public enum Admin implements PwmDisplayBundle {
     EventLog_Narrative_HelpdeskVerifyAttributes,
     EventLog_Narrative_HelpdeskVerifyAttributesIncorrect,
 
-
-
     ;
 
     @Override

+ 1 - 0
src/main/java/password/pwm/i18n/Message.java

@@ -72,6 +72,7 @@ public enum Message implements PwmDisplayBundle {
     EventLog_ActivateUser(null),
     EventLog_CreateUser(null),
     EventLog_UpdateProfile(null),
+    EventLog_DeleteAccount(null),
     EventLog_IntruderUserLock(null),
     EventLog_IntruderUserAttempt(null),
     EventLog_TokenIssued(null),

+ 1 - 0
src/main/java/password/pwm/svc/event/AuditEvent.java

@@ -46,6 +46,7 @@ public enum AuditEvent {
     TOKEN_ISSUED(                   Message.EventLog_TokenIssued,                       Admin.EventLog_Narrative_TokenIssued,                      Type.USER),
     TOKEN_CLAIMED(                  Message.EventLog_TokenClaimed,                      Admin.EventLog_Narrative_TokenClaimed,                     Type.USER),
     CLEAR_RESPONSES(                Message.EventLog_ClearResponses,                    Admin.EventLog_Narrative_ClearResponses,                   Type.USER),
+    DELETE_ACCOUNT (                Message.EventLog_DeleteAccount,                     Admin.EventLog_Narrative_DeleteAccount,                    Type.USER),
 
     // user events stored in user event history
     CHANGE_PASSWORD(                Message.EventLog_ChangePassword,                    Admin.EventLog_Narrative_ChangePassword,                   Type.USER),

+ 8 - 0
src/main/java/password/pwm/svc/event/AuditService.java

@@ -253,6 +253,14 @@ public class AuditService implements PwmService {
         return auditVault.size();
     }
 
+    public Date eldestValutRecord() {
+        if (status != STATUS.OPEN || auditVault == null) {
+            return null;
+        }
+
+        return auditVault.oldestRecord();
+    }
+
     public void submit(final AuditEvent auditEvent, final UserInfoBean userInfoBean, final PwmSession pwmSession)
             throws PwmUnrecoverableException
     {

+ 3 - 0
src/main/java/password/pwm/svc/event/AuditVault.java

@@ -24,6 +24,7 @@ package password.pwm.svc.event;
 
 import password.pwm.util.TimeDuration;
 
+import java.util.Date;
 import java.util.Iterator;
 
 public interface AuditVault {
@@ -32,6 +33,8 @@ public interface AuditVault {
 
     int size();
 
+    Date oldestRecord();
+
     Iterator<AuditRecord> readVault();
 
     void add(AuditRecord record);

+ 5 - 0
src/main/java/password/pwm/svc/event/LocalDbAuditVault.java

@@ -56,6 +56,11 @@ public class LocalDbAuditVault implements AuditVault {
         this.settings = settings;
     }
 
+    @Override
+    public Date oldestRecord() {
+        return oldestRecord;
+    }
+
     @Override
     public int size() {
         return auditDB.size();

+ 8 - 13
src/main/java/password/pwm/svc/sessiontrack/SessionTrackService.java

@@ -34,11 +34,12 @@ import password.pwm.svc.PwmService;
 import password.pwm.util.logging.PwmLogger;
 
 import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
 
 public class SessionTrackService implements PwmService {
     private static final PwmLogger LOGGER = PwmLogger.forClass(SessionTrackService.class);
 
-    private final Set<PwmSession> pwmSessions = Collections.newSetFromMap(new WeakHashMap<PwmSession, Boolean>());
+    private final transient Map<PwmSession,Boolean> pwmSessions = new ConcurrentHashMap<>();
 
     private PwmApplication pwmApplication;
 
@@ -54,9 +55,7 @@ public class SessionTrackService implements PwmService {
 
     @Override
     public void close() {
-        synchronized (pwmSessions) {
-            pwmSessions.clear();
-        }
+        pwmSessions.clear();
     }
 
     @Override
@@ -76,21 +75,17 @@ public class SessionTrackService implements PwmService {
     }
 
     public void addSessionData(final PwmSession pwmSession) {
-        synchronized (pwmSession) {
-            pwmSessions.add(pwmSession);
-        }
+        pwmSessions.put(pwmSession,Boolean.FALSE);
     }
 
     public void removeSessionData(final PwmSession pwmSession) {
-        synchronized (pwmSession) {
-            pwmSessions.add(pwmSession);
-        }
+        pwmSessions.remove(pwmSession);
     }
 
     private Set<PwmSession> copyOfSessionSet() {
-        synchronized (pwmSessions) {
-            return new HashSet<>(pwmSessions);
-        }
+        final Set<PwmSession> newSet = new HashSet<>();
+        newSet.addAll(pwmSessions.keySet());
+        return newSet;
 
     }
 

+ 12 - 1
src/main/java/password/pwm/util/ConditionalTaskExecutor.java

@@ -24,6 +24,8 @@ package password.pwm.util;
 
 import password.pwm.util.logging.PwmLogger;
 
+import java.util.concurrent.TimeUnit;
+
 /**
  * Executes a predefined task if a conditional has occurred.  Both the task and the conditional must be supplied by the caller.
  * All processing occurs in the current thread, no new threads will be created.
@@ -67,7 +69,16 @@ public class ConditionalTaskExecutor {
 
         public TimeDurationConditional(TimeDuration timeDuration) {
             this.timeDuration = timeDuration;
-            nextExecuteTimestamp = System.currentTimeMillis();
+            nextExecuteTimestamp = System.currentTimeMillis() + timeDuration.getTotalMilliseconds();
+        }
+
+        public TimeDurationConditional(long value, TimeUnit unit) {
+            this(new TimeDuration(value, unit));
+        }
+
+        public TimeDurationConditional setNextTimeFromNow(final long value, TimeUnit unit) {
+            nextExecuteTimestamp = System.currentTimeMillis() + unit.toMillis(value);
+            return this;
         }
 
         @Override

+ 6 - 4
src/main/java/password/pwm/util/PasswordData.java

@@ -33,20 +33,22 @@ import password.pwm.util.secure.PwmRandom;
 import password.pwm.util.secure.PwmSecurityKey;
 import password.pwm.util.secure.SecureEngine;
 
+import java.io.Serializable;
+
 /*
  * A in-memory password value wrapper.  Instances of this class cannot be serialized.  The actual password value is encrypted using a
  * a per-jvm instance key.
  *
  */
-public class PasswordData {
+public class PasswordData implements Serializable {
     private static final PwmLogger LOGGER = PwmLogger.forClass(PasswordData.class);
 
     private final byte[] passwordData;
     private final String keyHash; // not a secure value, used to detect if key is same over time.
 
-    private static final PwmSecurityKey staticKey;
-    private static final String staticKeyHash;
-    private static final ErrorInformation initializationError;
+    private static final transient PwmSecurityKey staticKey;
+    private static final transient String staticKeyHash;
+    private static final transient ErrorInformation initializationError;
 
     private String passwordHashCache;
 

+ 2 - 0
src/main/java/password/pwm/util/queue/EmailQueueManager.java

@@ -273,6 +273,8 @@ public class EmailQueueManager implements PwmService {
                         new String[]{ emailItemToDebugString(emailItemBean), Helper.readHostileExceptionMessage(e)}
                 );
             }
+
+            lastError = errorInformation;
             LOGGER.error(errorInformation);
 
             if (sendIsRetryable(e)) {

+ 2 - 2
src/main/resources/password/pwm/AppProperty.properties

@@ -157,7 +157,7 @@ localdb.implementation=password.pwm.util.localdb.Berkeley_LocalDB
 localdb.initParameters=
 localdb.logWriter.bufferSize=500
 localdb.logWriter.maxBufferWaitMs=60000
-localdb.logWriter.maxTrimSize=501
+localdb.logWriter.maxTrimSize=5001
 macro.randomChar.maxLength=100
 macro.ldapAttr.maxLength=100
 naaf.id=41414141414141414141414141414141
@@ -165,7 +165,7 @@ naaf.secret=876543210
 naaf.salt.length=30
 logging.devOutput.enable=false
 logging.pattern=%d{yyyy-MM-dd'T'HH:mm:ss'Z'}, %-5p, %c{2}, %m%n
-logging.file.maxSize=20MB
+logging.file.maxSize=10MB
 logging.file.maxRollover=50
 logging.file.path=logs
 newUser.ldap.useTempPassword=auto

+ 51 - 0
src/main/resources/password/pwm/config/PwmSetting.xml

@@ -262,6 +262,48 @@
             <option value="UserSMS">User SMS</option>
         </options>
     </setting>
+    <setting hidden="false" key="deleteAccount.enable" level="1" required="false">
+        <default>
+            <value>false</value>
+        </default>
+    </setting>
+    <setting hidden="false" key="deleteAccount.profile.list" level="1" required="false">
+        <regex>^(?!.*all.*)([a-zA-Z][a-zA-Z0-9-]{2,15})$</regex>
+        <properties>
+            <property key="Minimum">1</property>
+        </properties>
+        <default>
+            <value>default</value>
+        </default>
+    </setting>
+    <setting hidden="false" key="deleteAccount.permission" level="1" required="false">
+        <default>
+            <value/>
+        </default>
+    </setting>
+    <setting hidden="false" key="deleteAccount.agreement" level="1" required="false">
+        <flag>MacroSupport</flag>
+        <default>
+            <value/>
+        </default>
+    </setting>
+    <setting hidden="false" key="deleteAccount.deleteEntry" level="1" required="false">
+        <default>
+            <value/>
+        </default>
+    </setting>
+    <setting hidden="false" key="deleteAccount.actions" level="1" required="false">
+        <flag>MacroSupport</flag>
+        <default>
+            <value/>
+        </default>
+    </setting>
+    <setting hidden="false" key="deleteAccount.nextUrl" level="1" required="false">
+        <flag>MacroSupport</flag>
+        <default>
+            <value/>
+        </default>
+    </setting>
     <setting hidden="false" key="display.showLoginPageOptions" level="1" required="true">
         <default>
             <value>true</value>
@@ -1463,6 +1505,7 @@
             <value>ACTIVATE_USER</value>
             <value>CREATE_USER</value>
             <value>UPDATE_PROFILE</value>
+            <value>DELETE_ACCOUNT</value>
             <value>INTRUDER_USER_LOCK</value>
             <value>TOKEN_ISSUED</value>
             <value>TOKEN_CLAIMED</value>
@@ -1492,6 +1535,7 @@
             <option value="ACTIVATE_USER">Activate User</option>
             <option value="CREATE_USER">New User</option>
             <option value="UPDATE_PROFILE">Update Profile</option>
+            <option value="DELETE_ACCOUNT">Delete Account</option>
             <option value="INTRUDER_USER_LOCK">Intruder User Lock</option>
             <option value="INTRUDER_USER_ATTEMPT">Intruder User Attempt</option>
             <option value="TOKEN_ISSUED">Token Issued</option>
@@ -3651,4 +3695,11 @@
     </category>
     <category hidden="false" key="MODULES_PRIVATE">
     </category>
+    <category hidden="false" key="DELETE_ACCOUNT">
+    </category>
+    <category hidden="false" key="DELETE_ACCOUNT_SETTINGS">
+    </category>
+    <category hidden="false" key="DELETE_ACCOUNT_PROFILE">
+        <profile setting="deleteAccount.profile.list"/>
+    </category>
 </settings>

+ 2 - 1
src/main/resources/password/pwm/i18n/Admin.properties

@@ -341,4 +341,5 @@ EventLog_Narrative_ClearResponses=%perpetratorID% (%perpetratorDN%) has cleared
 EventLog_Narrative_RecoverPassword=%perpetratorID% (%perpetratorDN%) has verified their identity via forgotten password
 EventLog_Narrative_SetupResponses=%perpetratorID% (%perpetratorDN%) has saved their own stored responses
 Eventlog_Narrative_SetupOtpSecret=%perpetratorID% (%perpetratorDN%) has completed an OTP enrollment
-EventLog_Narrative_UpdateProfile=%perpetratorID% (%perpetratorDN%) has updated their profile data
+EventLog_Narrative_UpdateProfile=%perpetratorID% (%perpetratorDN%) has updated their profile data
+EventLog_Narrative_DeleteAccount=%perpetratorID% (%perpetratorDN%) has deleted their own account

+ 3 - 0
src/main/resources/password/pwm/i18n/Display.properties

@@ -80,6 +80,7 @@ Display_CommunicationError=Unable to communicate with server.  Continue when rea
 Display_ConfirmResponses=Be sure your answers and questions are correct.  Check the spelling and punctuation.  In you are unable to remember your password, you will be able to access your account by supplying the answers to these security questions.
 Display_Day=day
 Display_Days=days
+Display_DeleteUserConfirm=Are you sure you wish to delete your account?  This can not be undone.
 Display_ErrorBody=An error has occurred. Please close your browser and try again later.  If this error occurs repeatedly, please contact your help desk.
 Display_ErrorReference=error reference %1%
 Display_ExpirationDate=Account expiration date (maximum %1% days)
@@ -255,6 +256,7 @@ Long_Title_Shortcuts=Personalized shortcuts.
 Long_Title_UpdateProfile=Update your user profile data.
 Long_Title_UserEventHistory=Password event history.  See when you have changed your password in the past.
 Long_Title_UserInformation=Information about your password and password policies.
+Long_Title_DeleteAccount=Remove your account and profile from this service
 Long_Title_VerificationSend=Before this user can be selected, the user's identity must be verified.  Please select a verification method.
 Title_AnsweredQuestions=Answered Questions
 Title_ActivateUser=Activate Account
@@ -302,6 +304,7 @@ Title_UserEventHistory=Password History
 Title_UserInformation=My Account
 Title_ValidateCode=Validate Code
 Title_VerificationSend=Select verification method
+Title_DeleteAccount=Delete My Account
 Tooltip_PasswordStrength=The password strength meter shows how easy it is to guess your password. Try the following to make your password stronger:<ul><li>Make the password longer</li><li>Do not repeat letters or numbers</li><li>Use mixed (upper and lower) case letters</li><li>Add more numbers</li><li>Add more symbol characters</li></ul>
 Confirm_DeleteUser=Are you sure you wish to proceed?  If you continue, the selected user will be deleted permanently.  This can not be undone.
 Confirm=Are you sure you wish to proceed?

+ 1 - 0
src/main/resources/password/pwm/i18n/Message.properties

@@ -54,6 +54,7 @@ EventLog_RecoverPassword=Recover Forgotten Password
 EventLog_SetupResponses=Setup Password Responses
 Eventlog_SetupOtpSecret=Setup OTP Secret
 EventLog_UpdateProfile=Update Attributes
+EventLog_DeleteAccount=Delete Account
 Requirement_ADComplexity=Must have at least three types of the following characters: <ul><li>Uppercase (A-Z)</li><li>Lowercase (a-z)</li><li>Number (0-9)</li><li>Symbol (!, #, $, etc.)</li></ul>
 Requirement_ADComplexity2008=Must have at least %1% types of the following characters: <ul><li>Uppercase (A-Z)</li><li>Lowercase (a-z)</li><li>Number (0-9)</li><li>Symbol (!, #, $, etc.)</li><li>Other language characters not listed above</li></ul>
 Requirement_AllowNumeric=Must not include any numeric characters.

+ 23 - 5
src/main/resources/password/pwm/i18n/PwmSetting.properties

@@ -1081,13 +1081,8 @@ Setting_Label_webservices.queryMatch=External Web Services Permissions
 Setting_Label_webservices.thirdParty.queryMatch=Web Services Third Party Permissions
 Setting_Label_webservice.userAttributes=Web Service User Attributes
 Setting_Label_wordlistCaseSensitive=Word List Case Sensitivity
-
-
 Setting_Description_updateAttributes.email.verification=If true, an email will be sent to the user's email address before the account is updated.  The user must verify receipt of the email before the account will be updated.
 Setting_Description_updateAttributes.sms.verification=If true, an SMS will be sent to the user's mobile phone number before the account is updated.  The user must verify receipt of the SMS before the account will be updated.
-
-
-
 Setting_Label_helpdesk.verification.form=Verification Attributes
 Setting_Description_helpdesk.verification.form=Attributes used for <i>LDAP Attributes</i> on the setting <code>@PwmSettingReference\:helpdesk.verificationMethods@</code>.
 Category_Description_WORDLISTS=Word Lists
@@ -1096,3 +1091,26 @@ Category_Description_MODULES_PUBLIC=Public
 Category_Description_MODULES_PRIVATE=Authenticated
 Category_Label_MODULES_PUBLIC=Public
 Category_Label_MODULES_PRIVATE=Authenticated
+Category_Label_DELETE_ACCOUNT_SETTINGS=Settings
+Category_Description_DELETE_ACCOUNT_SETTINGS=Settings
+Category_Label_DELETE_ACCOUNT_PROFILE=Profiles
+Category_Description_DELETE_ACCOUNT_PROFILE=Profiles
+Category_Label_DELETE_ACCOUNT=Delete Account
+Category_Description_DELETE_ACCOUNT=Delete Account
+Setting_Label_deleteAccount.profile.list=Delete Account Profiles
+Setting_Description_deleteAccount.profile.list=List of Delete Account profiles.  In most cases, only a single profile is needed.  Only define multiple profiles if different user populations users will need different features/permissions.  Each profile has a <i>Delete Account Profile Match</i> setting used to define to whom the profile applies.  If multiple profiles could apply for a user, the first profile in the list defined here will be assigned.
+Setting_Label_deleteAccount.permission=Delete Account Profile Match
+Setting_Description_deleteAccount.permission=This setting is used to define the set of users for which this profile applies.
+Setting_Label_deleteAccount.enable=Enable Delete Account
+Setting_Description_deleteAccount.enable=Enable the delete account module.
+Setting_Label_deleteAccount.agreement=Delete Account Agreement
+Setting_Description_deleteAccount.agreement=<p>Message to display to user before being allowed to delete their account. If blank, the delete account user agreement page will not be displayed to the user. This message may include HTML tags.
+Setting_Label_deleteAccount.deleteEntry=Delete LDAP Entry
+Setting_Description_deleteAccount.deleteEntry=Control if the user's LDAP entry is actually deleted in the LDAP directory.  In many cases its desirable to not actually delete the LDAP entry, but instead disable the account and take other actions via the Pre-Delete Actions.
+Setting_Label_deleteAccount.actions=Pre-Delete Actions
+Setting_Description_deleteAccount.actions=Actions to execute during the user deletion process.  These actions will execute prior to the actual LDAP entry is deleted (if so configured).  Typically these actions are used to disable the LDAP account and trigger some type of process that will result in an eventual deletion.
+Setting_Label_deleteAccount.nextUrl=Next URL
+Setting_Description_deleteAccount.nextUrl=URL to send user to after deletion.  If blank, the normal logout handling will occur.
+
+
+

+ 1 - 0
src/main/resources/password/pwm/svc/event/AuditEvent.properties

@@ -33,6 +33,7 @@ TOKEN_CLAIMED={"xdasTaxonomy":"XDAS_AE_GRANT_ACCOUNT_ACCESS","xdasOutcome":"XDAS
 CLEAR_RESPONSES={"xdasTaxonomy":"XDAS_AE_DELETE_DATA_ITEM","xdasOutcome":"XDAS_OUT_SUCCESS"}
 CHANGE_PASSWORD={"xdasTaxonomy":"XDAS_AE_SET_CRED_ACCOUNT","xdasOutcome":"XDAS_OUT_SUCCESS"}
 UNLOCK_PASSWORD={"xdasTaxonomy":"XDAS_AE_GRANT_ACCOUNT_ACCESS","xdasOutcome":"XDAS_OUT_SUCCESS"}
+DELETE_ACCOUNT={"xdasTaxonomy":"XDAS_AE_DELETE_ACCOUNT","xdasOutcome":"XDAS_OUT_SUCCESS"}
 RECOVER_PASSWORD={"xdasTaxonomy":"XDAS_AE_GRANT_ACCOUNT_ACCESS","xdasOutcome":"XDAS_OUT_SUCCESS"}
 SET_RESPONSES={"xdasTaxonomy":"XDAS_AE_CREATE_TRUST","xdasOutcome":"XDAS_OUT_THRESHOLDS_SET"}
 SET_OTP_SECRET={"xdasTaxonomy":"XDAS_AE_CREATE_TRUST","xdasOutcome":"XDAS_OUT_THRESHOLDS_SET"}

+ 13 - 1
src/main/webapp/WEB-INF/jsp/admin-dashboard.jsp

@@ -427,6 +427,18 @@
                                 <%= numberFormat.format(dashboard_pwmApplication.getAuditManager().vaultSize()) %>
                             </td>
                         </tr>
+                        <tr>
+                            <td class="key">
+                                Oldest Local Audit Records
+                            </td>
+                            <td>
+                                <% Date eldestAuditRecord = dashboard_pwmApplication.getAuditManager().eldestValutRecord(); %>
+                                <%= eldestAuditRecord != null
+                                        ? TimeDuration.fromCurrent(eldestAuditRecord).asLongString()
+                                        : JspUtility.getMessage(pageContext, Display.Value_NotApplicable)
+                                %>
+                            </td>
+                        </tr>
                         <tr>
                             <td class="key">
                                 Log Events in LocalDB
@@ -441,7 +453,7 @@
                             </td>
                             <td>
                                 <%= dashboard_pwmApplication.getLocalDBLogger() != null
-                                        ? TimeDuration.fromCurrent(dashboard_pwmApplication.getLocalDBLogger().getTailDate()).asCompactString()
+                                        ? TimeDuration.fromCurrent(dashboard_pwmApplication.getLocalDBLogger().getTailDate()).asLongString()
                                         : JspUtility.getMessage(pageContext, Display.Value_NotApplicable)
                                 %>
                             </td>

+ 91 - 0
src/main/webapp/WEB-INF/jsp/deleteaccount-agreement.jsp

@@ -0,0 +1,91 @@
+<%@ page import="password.pwm.http.servlet.DeleteAccountServlet" %>
+<%@ page import="password.pwm.i18n.Display" %>
+<%--
+  ~ Password Management Servlets (PWM)
+  ~ http://www.pwm-project.org
+  ~
+  ~ Copyright (c) 2006-2009 Novell, Inc.
+  ~ Copyright (c) 2009-2016 The PWM Project
+  ~
+  ~ This program is free software; you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation; either version 2 of the License, or
+  ~ (at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program; if not, write to the Free Software
+  ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+  --%>
+
+<!DOCTYPE html>
+<%@ page language="java" session="true" isThreadSafe="true" contentType="text/html" %>
+<%@ taglib uri="pwm" prefix="pwm" %>
+<html lang="<pwm:value name="<%=PwmValue.localeCode%>"/>" dir="<pwm:value name="<%=PwmValue.localeDir%>"/>">
+<%@ include file="fragment/header.jsp" %>
+<body class="nihilo">
+<div id="wrapper">
+    <% JspUtility.setFlag(pageContext, PwmRequestFlag.HIDE_HEADER_BUTTONS); %>
+    <jsp:include page="fragment/header-body.jsp">
+        <jsp:param name="pwm.PageName" value="Title_DeleteAccount"/>
+    </jsp:include>
+    <div id="centerbody">
+        <div id="page-content-title"><pwm:display key="Title_DeleteAccount" displayIfMissing="true"/></div>
+        <%@ include file="fragment/message.jsp" %>
+        <% final String expandedText = (String)JspUtility.getAttribute(pageContext, PwmRequest.Attribute.AgreementText); %>
+        <div class="agreementText"><%= expandedText %></div>
+        <div class="buttonbar">
+            <form action="<pwm:current-url/>" method="post" enctype="application/x-www-form-urlencoded">
+                <%-- remove the next line to remove the "I Agree" checkbox --%>
+                <label class="checkboxWrapper">
+                    <input type="checkbox" id="agreeCheckBox"/>
+                    <pwm:display key="Button_Agree"/>
+                </label>
+                <br/><br/>
+                <input type="hidden" name="processAction" value="<%=DeleteAccountServlet.DeleteAccountAction.agree%>"/>
+                <button type="submit" name="button" class="btn" id="button_continue">
+                    <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-forward"></span></pwm:if>
+                    <pwm:display key="Button_Delete"/>
+                </button>
+                <input type="hidden" name="pwmFormID" id="pwmFormID" value="<pwm:FormID/>"/>
+            </form>
+            <form action="<pwm:current-url/>" method="post" enctype="application/x-www-form-urlencoded">
+                <input type="hidden" name="processAction" value="<%=DeleteAccountServlet.DeleteAccountAction.reset%>"/>
+                <button type="submit" name="button" class="btn" id="button_logout">
+                    <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-times"></span></pwm:if>
+                    <pwm:display key="Button_Cancel"/>
+                </button>
+                <input type="hidden" name="pwmFormID" value="<pwm:FormID/>"/>
+            </form>
+        </div>
+    </div>
+    <div class="push"></div>
+</div>
+<pwm:script>
+    <script type="text/javascript">
+        function updateContinueButton() {
+            var checkBox = PWM_MAIN.getObject("agreeCheckBox");
+            var continueButton = PWM_MAIN.getObject("button_continue");
+            if (checkBox != null && continueButton != null) {
+                if (checkBox.checked) {
+                    continueButton.removeAttribute('disabled');
+                } else {
+                    continueButton.disabled = "disabled";
+                }
+            }
+        }
+
+
+        PWM_GLOBAL['startupFunctions'].push(function(){
+            PWM_MAIN.addEventHandler('agreeCheckBox','click, change',function(){ updateContinueButton() });
+            updateContinueButton();
+        });
+    </script>
+</pwm:script>
+<%@ include file="fragment/footer.jsp" %>
+</body>
+</html>

+ 64 - 0
src/main/webapp/WEB-INF/jsp/deleteaccount-confirm.jsp

@@ -0,0 +1,64 @@
+<%@ page import="password.pwm.http.servlet.PwmServletDefinition" %>
+<%@ page import="password.pwm.http.tag.conditional.PwmIfTest" %>
+<%@ page import="password.pwm.i18n.Display" %>
+<%@ page import="password.pwm.http.servlet.DeleteAccountServlet" %>
+<%--
+  ~ Password Management Servlets (PWM)
+  ~ http://www.pwm-project.org
+  ~
+  ~ Copyright (c) 2006-2009 Novell, Inc.
+  ~ Copyright (c) 2009-2016 The PWM Project
+  ~
+  ~ This program is free software; you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation; either version 2 of the License, or
+  ~ (at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program; if not, write to the Free Software
+  ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+  --%>
+
+<!DOCTYPE html>
+<%@ page language="java" session="true" isThreadSafe="true" contentType="text/html" %>
+<%@ taglib uri="pwm" prefix="pwm" %>
+<html lang="<pwm:value name="<%=PwmValue.localeCode%>"/>" dir="<pwm:value name="<%=PwmValue.localeDir%>"/>">
+<%@ include file="fragment/header.jsp" %>
+<body class="nihilo">
+<div id="wrapper">
+    <jsp:include page="fragment/header-body.jsp">
+        <jsp:param name="pwm.PageName" value="Title_DeleteAccount"/>
+    </jsp:include>
+    <div id="centerbody">
+        <div id="page-content-title"><pwm:display key="Title_DeleteAccount" displayIfMissing="true"/></div>
+        <%@ include file="fragment/message.jsp" %>
+        <p><pwm:display key="Display_DeleteUserConfirm"/></p>
+        <div class="buttonbar">
+            <form action="<pwm:current-url/>" method="post" enctype="application/x-www-form-urlencoded">
+                <input type="hidden" name="processAction" value="<%=DeleteAccountServlet.DeleteAccountAction.delete%>"/>
+                <button type="submit" name="button" class="btn" id="button_continue">
+                    <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-forward"></span></pwm:if>
+                    <pwm:display key="Button_Delete"/>
+                </button>
+                <input type="hidden" name="pwmFormID" id="pwmFormID" value="<pwm:FormID/>"/>
+            </form>
+            <form action="<pwm:current-url/>" method="post" enctype="application/x-www-form-urlencoded">
+                <input type="hidden" name="processAction" value="<%=DeleteAccountServlet.DeleteAccountAction.reset%>"/>
+                <button type="submit" name="button" class="btn" id="button_logout">
+                    <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-times"></span></pwm:if>
+                    <pwm:display key="Button_Cancel"/>
+                </button>
+                <input type="hidden" name="pwmFormID" value="<pwm:FormID/>"/>
+            </form>
+        </div>
+    </div>
+    <div class="push"></div>
+</div>
+<%@ include file="fragment/footer.jsp" %>
+</body>
+</html>

+ 13 - 0
src/main/webapp/private/index.jsp

@@ -144,6 +144,19 @@
                 </a>
             </pwm:if>
 
+
+            <pwm:if test="<%=PwmIfTest.DeleteAccountAvailalable%>">
+                <a id="button_Helpdesk" href="<pwm:url addContext="true" url='<%=PwmServletDefinition.SelfDelete.servletUrl()%>'/>">
+                    <div class="tile">
+                        <div class="tile-content">
+                            <div class="tile-image selfdelete-image"></div>
+                            <div class="tile-title" title="<pwm:display key='Title_SelfDelete'/>"><pwm:display key="Title_DeleteAccount"/></div>
+                            <div class="tile-subtitle" title="<pwm:display key='Long_Title_SelfDelete'/>"><pwm:display key="Long_Title_DeleteAccount"/></div>
+                        </div>
+                    </div>
+                </a>
+            </pwm:if>
+
             <pwm:if test="<%=PwmIfTest.guestRegistrationAvailable%>">
                 <a id="button_GuestRegistration" href="<pwm:url url='<%=PwmServletDefinition.GuestRegistration.servletUrl()%>' addContext="true"/>">
                     <div class="tile">

+ 1 - 0
src/main/webapp/public/reference/license.jsp

@@ -83,6 +83,7 @@
                 Error: attribution file not found: attribution.xml
             </div>
         <% } %>
+        <span class="footnote">nanos gigantum humeris insidentes</span>
     </div>
     <div class="push"></div>
 </div>

+ 4 - 0
src/main/webapp/public/resources/style.css

@@ -366,6 +366,10 @@ input[type=password]::-ms-reveal{display: none;}
     content: "\f085";
 }
 
+.tile-image.selfdelete-image:before {
+    content: "\f235";
+}
+
 .tile-title {
     color: #434c50;
     font-size: 16px;