Przeglądaj źródła

Merge remote-tracking branch 'origin/master' into hf-changes

James Albright 9 lat temu
rodzic
commit
0d7381752f
46 zmienionych plików z 754 dodań i 318 usunięć
  1. 4 2
      pom.xml
  2. 14 0
      src/main/java/password/pwm/config/ActionConfiguration.java
  3. 7 0
      src/main/java/password/pwm/config/Configuration.java
  4. 5 0
      src/main/java/password/pwm/config/FormConfiguration.java
  5. 47 8
      src/main/java/password/pwm/config/FormUtility.java
  6. 2 0
      src/main/java/password/pwm/config/PwmSettingFlag.java
  7. 2 1
      src/main/java/password/pwm/config/SettingUIFunction.java
  8. 16 23
      src/main/java/password/pwm/config/function/AbstractUriCertImportFunction.java
  9. 83 0
      src/main/java/password/pwm/config/function/ActionCertImportFunction.java
  10. 3 7
      src/main/java/password/pwm/config/function/HttpsCertParseFunction.java
  11. 2 2
      src/main/java/password/pwm/config/function/LdapCertImportFunction.java
  12. 22 2
      src/main/java/password/pwm/config/function/NAAFCertImportFunction.java
  13. 21 2
      src/main/java/password/pwm/config/function/OAuthCertImportFunction.java
  14. 2 2
      src/main/java/password/pwm/config/function/SyslogCertImportFunction.java
  15. 3 3
      src/main/java/password/pwm/config/function/UserMatchViewerFunction.java
  16. 28 0
      src/main/java/password/pwm/config/value/ActionValue.java
  17. 1 1
      src/main/java/password/pwm/config/value/X509CertificateValue.java
  18. 37 1
      src/main/java/password/pwm/health/CertificateChecker.java
  19. 3 1
      src/main/java/password/pwm/http/servlet/LoginServlet.java
  20. 8 2
      src/main/java/password/pwm/http/servlet/configeditor/ConfigEditorServlet.java
  21. 1 1
      src/main/java/password/pwm/http/servlet/configguide/ConfigGuideServlet.java
  22. 2 2
      src/main/java/password/pwm/http/servlet/configmanager/ConfigManagerServlet.java
  23. 3 3
      src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskDetailInfoBean.java
  24. 1 5
      src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskServlet.java
  25. 8 16
      src/main/java/password/pwm/http/servlet/newuser/NewUserUserDataReader.java
  26. 6 5
      src/main/java/password/pwm/http/servlet/peoplesearch/AttributeDetailBean.java
  27. 32 2
      src/main/java/password/pwm/http/servlet/peoplesearch/PeopleSearchServlet.java
  28. 14 0
      src/main/java/password/pwm/http/tag/conditional/PwmIfTest.java
  29. 50 21
      src/main/java/password/pwm/ldap/LdapUserDataReader.java
  30. 13 8
      src/main/java/password/pwm/ldap/UserDataReader.java
  31. 3 11
      src/main/java/password/pwm/util/JsonUtil.java
  32. 3 5
      src/main/java/password/pwm/util/StringUtil.java
  33. 21 15
      src/main/java/password/pwm/util/cli/MainClass.java
  34. 10 1
      src/main/java/password/pwm/util/operations/ActionExecutor.java
  35. 4 4
      src/main/resources/password/pwm/config/PwmSetting.xml
  36. 3 2
      src/main/resources/password/pwm/i18n/Config.properties
  37. 1 1
      src/main/resources/password/pwm/i18n/PwmSetting.properties
  38. 1 1
      src/main/webapp/WEB-INF/Command.bat
  39. 3 3
      src/main/webapp/WEB-INF/jsp/configguide-end.jsp
  40. 4 2
      src/main/webapp/WEB-INF/jsp/configmanager.jsp
  41. 6 3
      src/main/webapp/WEB-INF/jsp/helpdesk-detail.jsp
  42. 211 116
      src/main/webapp/public/resources/js/configeditor-settings.js
  43. 21 20
      src/main/webapp/public/resources/js/configeditor.js
  44. 22 12
      src/main/webapp/public/resources/js/peoplesearch.js
  45. 1 1
      src/main/webapp/public/resources/themes/pwm/configStyle.css
  46. 0 1
      supplemental/docker/server.xml.source

+ 4 - 2
pom.xml

@@ -10,7 +10,7 @@
     <licenses>
         <license>
             <name>The GNU General Public License (GPL) Version 2</name>
-            <url>LICENSE</url>
+            <url>http://www.gnu.org/licenses/gpl-2.0.html</url>
             <distribution>repo</distribution>
         </license>
     </licenses>
@@ -140,7 +140,9 @@
                 <version>2.6</version>
                 <configuration>
                     <archiveClasses>true</archiveClasses>
+                    <!--
                     <packagingExcludes>WEB-INF/classes</packagingExcludes>
+                    -->
                 </configuration>
             </plugin>
             <plugin>
@@ -282,7 +284,7 @@
 
         <dependency>
             <groupId>com.github.ldapchai</groupId>
-            <version>0.6.6</version>
+            <version>0.6.7</version>
             <artifactId>ldapchai</artifactId>
         </dependency>
         <dependency>

+ 14 - 0
src/main/java/password/pwm/config/ActionConfiguration.java

@@ -25,8 +25,10 @@ package password.pwm.config;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmOperationalException;
+import password.pwm.util.JsonUtil;
 
 import java.io.Serializable;
+import java.security.cert.X509Certificate;
 import java.util.Map;
 
 public class ActionConfiguration implements Serializable {
@@ -44,6 +46,8 @@ public class ActionConfiguration implements Serializable {
     private Map<String,String> headers;
     private String url;
     private String body;
+    private X509Certificate[] certificates;
+
 
     private LdapMethod ldapMethod = LdapMethod.replace;
     private String attributeName;
@@ -53,6 +57,10 @@ public class ActionConfiguration implements Serializable {
         return name;
     }
 
+    public X509Certificate[] getCertificates() {
+        return certificates;
+    }
+
     public String getDescription() {
         return description;
     }
@@ -127,4 +135,10 @@ public class ActionConfiguration implements Serializable {
             }
         }
     }
+
+    public ActionConfiguration copyWithNewCertificate(final X509Certificate[] certificates) {
+        final ActionConfiguration clone = JsonUtil.cloneUsingJson(this, ActionConfiguration.class);
+        clone.certificates = certificates;
+        return clone;
+    }
 }

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

@@ -813,6 +813,11 @@ public class Configuration implements Serializable, SettingReader {
         return newProfile;
     }
 
+    public StoredConfigurationImpl getStoredConfiguration() throws PwmUnrecoverableException {
+        final StoredConfigurationImpl copiedStoredConfiguration = StoredConfigurationImpl.copy(storedConfiguration);
+        copiedStoredConfiguration.lock();
+        return copiedStoredConfiguration;
+    }
 
     public boolean isDevDebugMode() {
         return Boolean.parseBoolean(readAppProperty(AppProperty.LOGGING_DEV_OUTPUT));
@@ -831,4 +836,6 @@ public class Configuration implements Serializable, SettingReader {
         }
         return returnSet;
     }
+
+
 }

+ 5 - 0
src/main/java/password/pwm/config/FormConfiguration.java

@@ -51,6 +51,7 @@ public class FormConfiguration implements Serializable {
     private boolean confirmationRequired;
     private boolean readonly;
     private boolean unique;
+    private boolean multivalue;
     private Map<String,String> labels = Collections.singletonMap("", "");
     private Map<String,String> regexErrors = Collections.singletonMap("","");
     private Map<String,String> description = Collections.singletonMap("","");
@@ -199,6 +200,10 @@ public class FormConfiguration implements Serializable {
         return unique;
     }
 
+    public boolean isMultivalue() {
+        return multivalue;
+    }
+
     public String getRegex() {
         return regex;
     }

+ 47 - 8
src/main/java/password/pwm/config/FormUtility.java

@@ -29,7 +29,6 @@ import com.novell.ldapchai.exception.ChaiUnavailableException;
 import com.novell.ldapchai.util.SearchHelper;
 import password.pwm.AppProperty;
 import password.pwm.PwmApplication;
-import password.pwm.util.Validator;
 import password.pwm.bean.SessionLabel;
 import password.pwm.bean.UserIdentity;
 import password.pwm.error.*;
@@ -41,6 +40,7 @@ import password.pwm.svc.cache.CachePolicy;
 import password.pwm.svc.cache.CacheService;
 import password.pwm.util.JsonUtil;
 import password.pwm.util.StringUtil;
+import password.pwm.util.Validator;
 import password.pwm.util.logging.PwmLogger;
 
 import java.util.*;
@@ -133,7 +133,7 @@ public class FormUtility {
                 final String confirmFieldName = formConfiguration.getName() + Validator.PARAM_CONFIRM_SUFFIX;
                 returnObj.put(confirmFieldName, input.get(formConfiguration));
             }
-            
+
         }
         return returnObj;
     }
@@ -359,12 +359,44 @@ public class FormUtility {
             final UserDataReader userDataReader
     )
             throws PwmUnrecoverableException
+    {
+        final Map<FormConfiguration, List<String>> valueMap = populateFormMapFromLdap(formFields, sessionLabel, userDataReader);
+        for (FormConfiguration formConfiguration : formMap.keySet()) {
+            if (valueMap.containsKey(formConfiguration)) {
+                final List<String> values = valueMap.get(formConfiguration);
+                if (values != null && !values.isEmpty()) {
+                    final String value = values.iterator().next();
+                    formMap.put(formConfiguration, value);
+                }
+            }
+        }
+    }
+
+    public static Map<FormConfiguration, List<String>> populateFormMapFromLdap(
+            final List<FormConfiguration> formFields,
+            final SessionLabel sessionLabel,
+            final UserDataReader userDataReader
+    )
+            throws PwmUnrecoverableException
     {
         final List<String> formFieldNames = FormConfiguration.convertToListOfNames(formFields);
         LOGGER.trace(sessionLabel, "preparing to load form data from ldap for fields " + JsonUtil.serializeCollection(formFieldNames));
-        final Map<String,String> userData = new LinkedHashMap<>();
+        final Map<String,List<String>> dataFromLdap = new LinkedHashMap<>();
         try {
-            userData.putAll(userDataReader.readStringAttributes(formFieldNames, true));
+            for (final FormConfiguration formConfiguration : formFields) {
+                final String attribute = formConfiguration.getName();
+                if (formConfiguration.isMultivalue()) {
+                    final List<String> values = userDataReader.readMultiStringAttribute(attribute, UserDataReader.Flag.ignoreCache);
+                    if (values != null && !values.isEmpty()) {
+                        dataFromLdap.put(attribute, values);
+                    }
+                } else {
+                    final String value = userDataReader.readStringAttribute(attribute);
+                    if (value != null) {
+                        dataFromLdap.put(attribute, Collections.singletonList(value));
+                    }
+                }
+            }
         } catch (Exception e) {
             PwmError error = null;
             if (e instanceof ChaiException) {
@@ -379,13 +411,20 @@ public class FormUtility {
             throw new PwmUnrecoverableException(errorInformation);
         }
 
+        final Map<FormConfiguration, List<String>> returnMap = new LinkedHashMap<>();
         for (final FormConfiguration formItem : formFields) {
             final String attrName = formItem.getName();
-            if (userData.containsKey(attrName)) {
-                final String value = parseInputValueToFormValue(formItem, userData.get(attrName));
-                formMap.put(formItem, value);
-                LOGGER.trace(sessionLabel, "loaded value for form item '" + attrName + "' with value=" + value);
+            if (dataFromLdap.containsKey(attrName)) {
+                final List<String> values = new ArrayList<>();
+                for (final String value : dataFromLdap.get(attrName)) {
+                    final String parsedValue = parseInputValueToFormValue(formItem, value);
+                    values.add(parsedValue);
+                    LOGGER.trace(sessionLabel, "loaded value for form item '" + attrName + "' with value=" + value);
+                }
+
+                returnMap.put(formItem, values);
             }
         }
+        return returnMap;
     }
 }

+ 2 - 0
src/main/java/password/pwm/config/PwmSettingFlag.java

@@ -39,6 +39,8 @@ public enum PwmSettingFlag {
     Form_ShowUniqueOption,
     Form_ShowReadOnlyOption,
     Form_ShowRequiredOption,
+    Form_ShowMultiValueOption,
+    Form_HideStandardOptions,
 
     Verification_HideMinimumOptional,
 

+ 2 - 1
src/main/java/password/pwm/config/SettingUIFunction.java

@@ -32,7 +32,8 @@ public interface SettingUIFunction {
             final PwmRequest pwmRequest,
             final StoredConfigurationImpl storedConfiguration,
             final PwmSetting setting,
-            final String profile
+            final String profile,
+            String extraData
     )
             throws Exception;
 }

+ 16 - 23
src/main/java/password/pwm/config/function/AbstractUriCertImportFunction.java

@@ -22,7 +22,6 @@
 
 package password.pwm.config.function;
 
-import password.pwm.PwmApplication;
 import password.pwm.bean.UserIdentity;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.SettingUIFunction;
@@ -35,9 +34,6 @@ import password.pwm.util.X509Utils;
 
 import java.net.URI;
 import java.security.cert.X509Certificate;
-import java.util.Arrays;
-import java.util.LinkedHashSet;
-import java.util.Set;
 
 abstract class AbstractUriCertImportFunction implements SettingUIFunction {
 
@@ -46,20 +42,16 @@ abstract class AbstractUriCertImportFunction implements SettingUIFunction {
             PwmRequest pwmRequest,
             StoredConfigurationImpl storedConfiguration,
             PwmSetting setting,
-            String profile
-    )
-            throws PwmOperationalException, PwmUnrecoverableException {
-        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
+            String profile,
+            String extraData)
+            throws PwmOperationalException, PwmUnrecoverableException
+    {
         final PwmSession pwmSession = pwmRequest.getPwmSession();
-        final Set<X509Certificate> resultCertificates = new LinkedHashSet<>();
+        final X509Certificate[] certs;
 
-        final String naafUrl = (String)storedConfiguration.readSetting(getSetting()).toNativeObject();
-        if (naafUrl != null && !naafUrl.isEmpty()) {
+        final String urlString = getUri(storedConfiguration, setting, profile, extraData);
             try {
-                final X509Certificate[] certs = X509Utils.readRemoteCertificates(URI.create(naafUrl));
-                if (certs != null) {
-                    resultCertificates.addAll(Arrays.asList(certs));
-                }
+                certs = X509Utils.readRemoteCertificates(URI.create(urlString));
             } catch (Exception e) {
                 if (e instanceof PwmException) {
                     throw new PwmOperationalException(((PwmException) e).getErrorInformation());
@@ -67,24 +59,25 @@ abstract class AbstractUriCertImportFunction implements SettingUIFunction {
                 ErrorInformation errorInformation = new ErrorInformation(PwmError.CONFIG_FORMAT_ERROR,"error importing certificates: " + e.getMessage());
                 throw new PwmOperationalException(errorInformation);
             }
-        } else {
-            ErrorInformation errorInformation = new ErrorInformation(PwmError.CONFIG_FORMAT_ERROR,"Setting " + getSetting().toMenuLocationDebug(null, null) + " must first be configured");
-            throw new PwmOperationalException(errorInformation);
-        }
+
 
         final UserIdentity userIdentity = pwmSession.isAuthenticated() ? pwmSession.getUserInfoBean().getUserIdentity() : null;
-        storedConfiguration.writeSetting(setting, new X509CertificateValue(resultCertificates), userIdentity);
+        store(certs, storedConfiguration, setting, profile, extraData, userIdentity);
 
         final StringBuffer returnStr = new StringBuffer();
-        for (final X509Certificate loopCert : resultCertificates) {
+        for (final X509Certificate loopCert : certs) {
             returnStr.append(X509Utils.makeDebugText(loopCert));
             returnStr.append("\n\n");
         }
         return returnStr.toString();
-        //return Message.getLocalizedMessage(pwmSession.getSessionStateBean().getLocale(), Message.Success_Unknown, pwmApplication.getConfig());
     }
 
-    abstract PwmSetting getSetting();
+    abstract String getUri(StoredConfigurationImpl storedConfiguration, final PwmSetting pwmSetting, final String profile, final String extraData) throws PwmOperationalException;
+
+
+    void store(X509Certificate[] certs, StoredConfigurationImpl storedConfiguration, final PwmSetting pwmSetting, final String profile, final String extraData, final UserIdentity userIdentity) throws PwmOperationalException, PwmUnrecoverableException {
+        storedConfiguration.writeSetting(pwmSetting, new X509CertificateValue(certs), userIdentity);
+    }
 
 
 }

+ 83 - 0
src/main/java/password/pwm/config/function/ActionCertImportFunction.java

@@ -0,0 +1,83 @@
+/*
+ * 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.function;
+
+import password.pwm.bean.UserIdentity;
+import password.pwm.config.ActionConfiguration;
+import password.pwm.config.PwmSetting;
+import password.pwm.config.stored.StoredConfigurationImpl;
+import password.pwm.config.value.ActionValue;
+import password.pwm.error.ErrorInformation;
+import password.pwm.error.PwmError;
+import password.pwm.error.PwmOperationalException;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.util.JsonUtil;
+
+import java.net.URI;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.List;
+
+public class ActionCertImportFunction extends AbstractUriCertImportFunction {
+
+    @Override
+    String getUri(StoredConfigurationImpl storedConfiguration, PwmSetting pwmSetting, String profile, String extraData) throws PwmOperationalException {
+        final ActionValue actionValue = (ActionValue)storedConfiguration.readSetting(pwmSetting, profile);
+        final String actionName = actionNameFromExtraData(extraData);
+        final ActionConfiguration action =  actionValue.forName(actionName);
+        final String uriString = action.getUrl();
+
+        if (uriString == null || uriString.isEmpty()) {
+            ErrorInformation errorInformation = new ErrorInformation(PwmError.CONFIG_FORMAT_ERROR,"Setting " + pwmSetting.toMenuLocationDebug(profile, null) + " action " + actionName + " must first be configured");
+            throw new PwmOperationalException(errorInformation);
+        }
+        try {
+            URI.create(uriString);
+        } catch (IllegalArgumentException e) {
+            ErrorInformation errorInformation = new ErrorInformation(PwmError.CONFIG_FORMAT_ERROR,"Setting " + pwmSetting.toMenuLocationDebug(profile, null) + " action " + actionName + " has an invalid URL syntax");
+            throw new PwmOperationalException(errorInformation);
+        }
+        return uriString;
+    }
+
+    private String actionNameFromExtraData(final String extraData) {
+        return extraData;
+    }
+
+    void store(X509Certificate[] certs, StoredConfigurationImpl storedConfiguration, final PwmSetting pwmSetting, final String profile, final String extraData, final UserIdentity userIdentity) throws PwmOperationalException, PwmUnrecoverableException {
+        final ActionValue actionValue = (ActionValue)storedConfiguration.readSetting(pwmSetting, profile);
+        final String actionName = actionNameFromExtraData(extraData);
+        final List<ActionConfiguration> newList = new ArrayList<>();
+        for (ActionConfiguration loopConfiguration : actionValue.toNativeObject()) {
+            if (actionName.equals(loopConfiguration.getName())) {
+                final ActionConfiguration newConfig = loopConfiguration.copyWithNewCertificate(certs);
+                newList.add(newConfig);
+            } else {
+                newList.add(JsonUtil.cloneUsingJson(loopConfiguration,ActionConfiguration.class));
+            }
+        }
+        final ActionValue newActionValue = new ActionValue(newList);
+        storedConfiguration.writeSetting(pwmSetting, profile, newActionValue, userIdentity);
+    }
+
+}

+ 3 - 7
src/main/java/password/pwm/config/function/HttpsCertParseFunction.java

@@ -26,6 +26,7 @@ import password.pwm.PwmApplication;
 import password.pwm.PwmEnvironment;
 import password.pwm.config.Configuration;
 import password.pwm.config.PwmSetting;
+import password.pwm.config.SettingUIFunction;
 import password.pwm.config.stored.StoredConfigurationImpl;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
@@ -38,10 +39,10 @@ import java.security.KeyStoreException;
 import java.security.cert.X509Certificate;
 import java.util.Enumeration;
 
-public class HttpsCertParseFunction extends AbstractUriCertImportFunction {
+public class HttpsCertParseFunction implements SettingUIFunction {
 
     @Override
-    public String provideFunction(PwmRequest pwmRequest, StoredConfigurationImpl storedConfiguration, PwmSetting setting, String profile)
+    public String provideFunction(PwmRequest pwmRequest, StoredConfigurationImpl storedConfiguration, PwmSetting setting, String profile, String extraData)
             throws PwmUnrecoverableException
     {
         final PwmEnvironment pwmEnvironment = pwmRequest.getPwmApplication().getPwmEnvironment().makeRuntimeInstance(new Configuration(storedConfiguration));
@@ -50,11 +51,6 @@ public class HttpsCertParseFunction extends AbstractUriCertImportFunction {
         return keyStoreToStringOutput(httpsCertificateManager.configToKeystore());
     }
 
-    @Override
-    PwmSetting getSetting() {
-        return null;
-    }
-
     String keyStoreToStringOutput(final KeyStore keyStore) throws PwmUnrecoverableException {
 
         final StringBuilder sb = new StringBuilder();

+ 2 - 2
src/main/java/password/pwm/config/function/LdapCertImportFunction.java

@@ -49,8 +49,8 @@ public class LdapCertImportFunction implements SettingUIFunction {
             PwmRequest pwmRequest,
             StoredConfigurationImpl storedConfiguration,
             PwmSetting setting,
-            String profile
-    )
+            String profile,
+            String extraData)
             throws PwmOperationalException, PwmUnrecoverableException {
         final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
         final PwmSession pwmSession = pwmRequest.getPwmSession();

+ 22 - 2
src/main/java/password/pwm/config/function/NAAFCertImportFunction.java

@@ -23,11 +23,31 @@
 package password.pwm.config.function;
 
 import password.pwm.config.PwmSetting;
+import password.pwm.config.stored.StoredConfigurationImpl;
+import password.pwm.error.ErrorInformation;
+import password.pwm.error.PwmError;
+import password.pwm.error.PwmOperationalException;
+
+import java.net.URI;
 
 public class NAAFCertImportFunction extends AbstractUriCertImportFunction {
 
+    final static PwmSetting uriSourceSetting = PwmSetting.NAAF_WS_URL;
+
     @Override
-    PwmSetting getSetting() {
-        return PwmSetting.NAAF_WS_URL;
+    String getUri(StoredConfigurationImpl storedConfiguration, PwmSetting pwmSetting, String profile, String extraData) throws PwmOperationalException {
+        final String uriString = (String)storedConfiguration.readSetting(uriSourceSetting).toNativeObject();
+        if (uriString == null || uriString.isEmpty()) {
+            ErrorInformation errorInformation = new ErrorInformation(PwmError.CONFIG_FORMAT_ERROR,"Setting " + uriSourceSetting.toMenuLocationDebug(profile, null) + " must first be configured");
+            throw new PwmOperationalException(errorInformation);
+        }
+        try {
+            URI.create(uriString);
+        } catch (IllegalArgumentException e) {
+            ErrorInformation errorInformation = new ErrorInformation(PwmError.CONFIG_FORMAT_ERROR,"Setting " + uriSourceSetting.toMenuLocationDebug(profile, null) + " has an invalid URL syntax");
+            throw new PwmOperationalException(errorInformation);
+        }
+        return uriString;
     }
+
 }

+ 21 - 2
src/main/java/password/pwm/config/function/OAuthCertImportFunction.java

@@ -23,11 +23,30 @@
 package password.pwm.config.function;
 
 import password.pwm.config.PwmSetting;
+import password.pwm.config.stored.StoredConfigurationImpl;
+import password.pwm.error.ErrorInformation;
+import password.pwm.error.PwmError;
+import password.pwm.error.PwmOperationalException;
+
+import java.net.URI;
 
 public class OAuthCertImportFunction extends AbstractUriCertImportFunction {
 
+    final static PwmSetting uriSourceSetting = PwmSetting.OAUTH_ID_CODERESOLVE_URL;
+
     @Override
-    PwmSetting getSetting() {
-        return PwmSetting.OAUTH_ID_CODERESOLVE_URL;
+    String getUri(StoredConfigurationImpl storedConfiguration, PwmSetting pwmSetting, String profile, String extraData) throws PwmOperationalException {
+        final String uriString = (String)storedConfiguration.readSetting(uriSourceSetting).toNativeObject();
+        if (uriString == null || uriString.isEmpty()) {
+            ErrorInformation errorInformation = new ErrorInformation(PwmError.CONFIG_FORMAT_ERROR,"Setting " + uriSourceSetting.toMenuLocationDebug(profile, null) + " must first be configured");
+            throw new PwmOperationalException(errorInformation);
+        }
+        try {
+            URI.create(uriString);
+        } catch (IllegalArgumentException e) {
+            ErrorInformation errorInformation = new ErrorInformation(PwmError.CONFIG_FORMAT_ERROR,"Setting " + uriSourceSetting.toMenuLocationDebug(profile, null) + " has an invalid URL syntax");
+            throw new PwmOperationalException(errorInformation);
+        }
+        return uriString;
     }
 }

+ 2 - 2
src/main/java/password/pwm/config/function/SyslogCertImportFunction.java

@@ -47,8 +47,8 @@ public class SyslogCertImportFunction implements SettingUIFunction {
             PwmRequest pwmRequest,
             StoredConfigurationImpl storedConfiguration,
             PwmSetting setting,
-            String profile
-    )
+            String profile,
+            String extraData)
             throws PwmOperationalException, PwmUnrecoverableException {
         final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
         final PwmSession pwmSession = pwmRequest.getPwmSession();

+ 3 - 3
src/main/java/password/pwm/config/function/UserMatchViewerFunction.java

@@ -53,11 +53,11 @@ public class UserMatchViewerFunction implements SettingUIFunction {
 
     @Override
     public Serializable provideFunction(
-            PwmRequest pwmRequest,
+            final PwmRequest pwmRequest,
             final StoredConfigurationImpl storedConfiguration,
             final PwmSetting setting,
-            final String profile
-    )
+            final String profile,
+            String extraData)
             throws Exception
     {
         final PwmApplication pwmApplication = pwmRequest.getPwmApplication();

+ 28 - 0
src/main/java/password/pwm/config/value/ActionValue.java

@@ -32,6 +32,7 @@ import password.pwm.error.PwmOperationalException;
 import password.pwm.util.JsonUtil;
 import password.pwm.util.secure.PwmSecurityKey;
 
+import java.security.cert.X509Certificate;
 import java.util.*;
 
 public class ActionValue extends AbstractValue implements StoredValue {
@@ -170,4 +171,31 @@ public class ActionValue extends AbstractValue implements StoredValue {
         return sb.toString();
     }
 
+
+    public List<Map<String,Object>> toInfoMap() {
+        final String originalJson = JsonUtil.serializeCollection(values);
+        final List<Map<String,Object>> tempObj = JsonUtil.deserialize(originalJson, new TypeToken<List<Map<String,Object>>>() {
+        });
+        for (final Map<String,Object> mapObj : tempObj) {
+            ActionConfiguration actionConfiguration = forName((String)mapObj.get("name"));
+            if (actionConfiguration != null && actionConfiguration.getCertificates() != null) {
+                final List<Map<String,Object>> certificateInfos = new ArrayList<>();
+                for (final X509Certificate certificate : actionConfiguration.getCertificates()) {
+                    certificateInfos.add(X509CertificateValue.toInfoMap(certificate,true));
+                }
+                mapObj.put("certificateInfos", certificateInfos);
+            }
+        }
+        return tempObj;
+    }
+
+    public ActionConfiguration forName(final String name) {
+        for (final ActionConfiguration actionConfiguration : values) {
+            if (name.equals(actionConfiguration.getName())) {
+                return actionConfiguration;
+            }
+        }
+        return null;
+    }
+
 }

+ 1 - 1
src/main/java/password/pwm/config/value/X509CertificateValue.java

@@ -153,7 +153,7 @@ public class X509CertificateValue extends AbstractValue implements StoredValue {
         return Collections.unmodifiableList(list);
     }
 
-    private static Map<String,Object> toInfoMap(final X509Certificate cert, final boolean includeDetail) {
+     static Map<String,Object> toInfoMap(final X509Certificate cert, final boolean includeDetail) {
         final LinkedHashMap<String,Object> map = new LinkedHashMap<>();
         map.put("subject",cert.getSubjectDN().toString());
         map.put("serial", X509Utils.hexSerial(cert));

+ 37 - 1
src/main/java/password/pwm/health/CertificateChecker.java

@@ -25,14 +25,21 @@ package password.pwm.health;
 import password.pwm.AppProperty;
 import password.pwm.PwmApplication;
 import password.pwm.PwmConstants;
+import password.pwm.config.ActionConfiguration;
 import password.pwm.config.Configuration;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSettingSyntax;
 import password.pwm.config.profile.LdapProfile;
+import password.pwm.config.stored.StoredConfigReference;
+import password.pwm.config.stored.StoredConfigurationImpl;
+import password.pwm.config.stored.StoredConfigurationUtil;
+import password.pwm.config.value.ActionValue;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmOperationalException;
+import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.TimeDuration;
+import password.pwm.util.logging.PwmLogger;
 
 import java.security.cert.CertificateException;
 import java.security.cert.X509Certificate;
@@ -42,9 +49,17 @@ import java.util.Date;
 import java.util.List;
 
 public class CertificateChecker implements HealthChecker {
+    private static final PwmLogger LOGGER = PwmLogger.forClass(CertificateChecker.class);
+
     @Override
     public List<HealthRecord> doHealthCheck(PwmApplication pwmApplication) {
-        return doHealthCheck(pwmApplication.getConfig());
+        final List<HealthRecord> records = new ArrayList<>();
+        records.addAll(doHealthCheck(pwmApplication.getConfig()));
+        try {
+            records.addAll(doActionHealthCheck(pwmApplication.getConfig()));
+        } catch (PwmUnrecoverableException e) {
+            LOGGER.error("error while checking action certificates: " + e.getMessage(),e);
+        } return records;
     }
 
     private static List<HealthRecord> doHealthCheck(Configuration configuration) {
@@ -64,6 +79,27 @@ public class CertificateChecker implements HealthChecker {
         return Collections.unmodifiableList(returnList);
     }
 
+    private static List<HealthRecord> doActionHealthCheck(final Configuration configuration) throws PwmUnrecoverableException {
+
+        final StoredConfigurationImpl storedConfiguration = configuration.getStoredConfiguration();
+
+        final List<HealthRecord> returnList = new ArrayList<>();
+        List<StoredConfigReference> modifiedReferences = StoredConfigurationUtil.modifiedSettings(storedConfiguration);
+        for (StoredConfigReference storedConfigReference : modifiedReferences) {
+            if (storedConfigReference.getRecordType() == StoredConfigReference.RecordType.SETTING) {
+                final PwmSetting pwmSetting = PwmSetting.forKey(storedConfigReference.getRecordID());
+                if (pwmSetting != null && pwmSetting.getSyntax() == PwmSettingSyntax.ACTION) {
+                    ActionValue value = (ActionValue)storedConfiguration.readSetting(pwmSetting, storedConfigReference.getProfileID());
+                    for (ActionConfiguration actionConfiguration : value.toNativeObject()) {
+                        final X509Certificate[] certificates = actionConfiguration.getCertificates();
+                        returnList.addAll(doHealthCheck(configuration, pwmSetting, storedConfigReference.getProfileID(), certificates));
+                    }
+                }
+            }
+        }
+        return Collections.unmodifiableList(returnList);
+    }
+
     private static List<HealthRecord> doHealthCheck(Configuration configuration, PwmSetting setting, final String profileID, X509Certificate[] certificates) {
         final long warnDurationMs = 1000 * Long.parseLong(configuration.readAppProperty(AppProperty.HEALTH_CERTIFICATE_WARN_SECONDS));
 

+ 3 - 1
src/main/java/password/pwm/http/servlet/LoginServlet.java

@@ -159,7 +159,9 @@ public class LoginServlet extends AbstractPwmServlet {
         }
 
         final String username = valueMap.get(PwmConstants.PARAM_USERNAME);
-        final PasswordData password = new PasswordData(valueMap.get(PwmConstants.PARAM_PASSWORD));
+        final PasswordData password = valueMap.containsKey(PwmConstants.PARAM_PASSWORD)
+                ? new PasswordData(valueMap.get(PwmConstants.PARAM_PASSWORD))
+                : null;
         final String context = valueMap.get(PwmConstants.PARAM_CONTEXT);
         final String ldapProfile = valueMap.get(PwmConstants.PARAM_LDAP_PROFILE);
 

+ 8 - 2
src/main/java/password/pwm/http/servlet/configeditor/ConfigEditorServlet.java

@@ -33,6 +33,7 @@ import password.pwm.config.function.HttpsCertParseFunction;
 import password.pwm.config.stored.ConfigurationProperty;
 import password.pwm.config.stored.StoredConfigurationImpl;
 import password.pwm.config.stored.ValueMetaData;
+import password.pwm.config.value.ActionValue;
 import password.pwm.config.value.FileValue;
 import password.pwm.config.value.ValueFactory;
 import password.pwm.config.value.X509CertificateValue;
@@ -270,11 +271,12 @@ public class ConfigEditorServlet extends AbstractPwmServlet {
         final PwmSetting pwmSetting = PwmSetting.forKey(requestMap.get("setting"));
         final String functionName = requestMap.get("function");
         final String profileID = pwmSetting.getCategory().hasProfiles() ? pwmRequest.readParameterAsString("profile") : null;
+        final String extraData = requestMap.get("extraData");
 
         try {
             Class implementingClass = Class.forName(functionName);
             SettingUIFunction function = (SettingUIFunction) implementingClass.newInstance();
-            final Serializable result = function.provideFunction(pwmRequest, configManagerBean.getStoredConfiguration(), pwmSetting, profileID);
+            final Serializable result = function.provideFunction(pwmRequest, configManagerBean.getStoredConfiguration(), pwmSetting, profileID, extraData);
             RestResultBean restResultBean = new RestResultBean();
             restResultBean.setSuccessMessage(Message.Success_Unknown.getLocalizedMessage(pwmRequest.getLocale(),pwmRequest.getConfig()));
             restResultBean.setData(result);
@@ -350,6 +352,10 @@ public class ConfigEditorServlet extends AbstractPwmServlet {
                     returnValue = ((X509CertificateValue) storedConfig.readSetting(theSetting, profile)).toInfoMap(true);
                     break;
 
+                case ACTION:
+                    returnValue = ((ActionValue)storedConfig.readSetting(theSetting, profile)).toInfoMap();
+                    break;
+
                 case FILE:
                     returnValue = ((FileValue) storedConfig.readSetting(theSetting, profile)).toInfoMap();
                     break;
@@ -667,7 +673,7 @@ public class ConfigEditorServlet extends AbstractPwmServlet {
         final Date startTime = new Date();
         LOGGER.debug(pwmRequest, "beginning restHttpsCertificateView");
         final HttpsCertParseFunction httpsCertParseFunction = new HttpsCertParseFunction();
-        final String output = httpsCertParseFunction.provideFunction(pwmRequest, configManagerBean.getStoredConfiguration(),null,null);
+        final String output = httpsCertParseFunction.provideFunction(pwmRequest, configManagerBean.getStoredConfiguration(),null,null, null);
         final RestResultBean restResultBean = new RestResultBean();
         restResultBean.setData(output);
         pwmRequest.outputJsonResult(restResultBean);

+ 1 - 1
src/main/java/password/pwm/http/servlet/configguide/ConfigGuideServlet.java

@@ -379,7 +379,7 @@ public class ConfigGuideServlet extends AbstractPwmServlet {
         try {
             final UserMatchViewerFunction userMatchViewerFunction = new UserMatchViewerFunction();
             final StoredConfigurationImpl storedConfiguration = ConfigGuideForm.generateStoredConfig(configGuideBean);
-            final Serializable output = userMatchViewerFunction.provideFunction(pwmRequest, storedConfiguration, PwmSetting.QUERY_MATCH_PWM_ADMIN, null);
+            final Serializable output = userMatchViewerFunction.provideFunction(pwmRequest, storedConfiguration, PwmSetting.QUERY_MATCH_PWM_ADMIN, null, null);
             pwmRequest.outputJsonResult(new RestResultBean(output));
         } catch (PwmException e) {
             LOGGER.error(pwmRequest,e.getErrorInformation());

+ 2 - 2
src/main/java/password/pwm/http/servlet/configmanager/ConfigManagerServlet.java

@@ -38,6 +38,7 @@ import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.*;
 import password.pwm.http.bean.ConfigManagerBean;
 import password.pwm.http.servlet.AbstractPwmServlet;
+import password.pwm.http.servlet.PwmServletDefinition;
 import password.pwm.http.servlet.configguide.ConfigGuideServlet;
 import password.pwm.i18n.Config;
 import password.pwm.i18n.Display;
@@ -295,8 +296,7 @@ public class ConfigManagerServlet extends AbstractPwmServlet {
     static void forwardToEditor(final PwmRequest pwmRequest)
             throws IOException, ServletException, PwmUnrecoverableException
     {
-        final String url = pwmRequest.getHttpServletRequest().getContextPath() + "/private/config/ConfigEditor";
-        pwmRequest.sendRedirect(url);
+        pwmRequest.sendRedirect(PwmServletDefinition.ConfigEditor);
     }
 
     private void doDownloadConfig(final PwmRequest pwmRequest)

+ 3 - 3
src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskDetailInfoBean.java

@@ -41,7 +41,7 @@ public class HelpdeskDetailInfoBean implements Serializable {
 
     private Date lastLoginTime;
     private List<UserAuditRecord> userHistory;
-    private Map<FormConfiguration, String> searchDetails;
+    private Map<FormConfiguration, List<String>> searchDetails;
     private String passwordSetDelta;
 
     public String getUserDisplayName() {
@@ -92,11 +92,11 @@ public class HelpdeskDetailInfoBean implements Serializable {
         this.userHistory = userHistory;
     }
 
-    public Map<FormConfiguration, String> getSearchDetails() {
+    public Map<FormConfiguration, List<String>> getSearchDetails() {
         return searchDetails;
     }
 
-    public void setSearchDetails(Map<FormConfiguration, String> searchDetails) {
+    public void setSearchDetails(Map<FormConfiguration, List<String>> searchDetails) {
         this.searchDetails = searchDetails;
     }
 

+ 1 - 5
src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskServlet.java

@@ -580,11 +580,7 @@ public class HelpdeskServlet extends AbstractPwmServlet {
 
         {
             final List<FormConfiguration> detailFormConfig = helpdeskProfile.readSettingAsForm(PwmSetting.HELPDESK_DETAIL_FORM);
-            final Map<FormConfiguration,String> formData = new LinkedHashMap<>();
-            for (final FormConfiguration formConfiguration : detailFormConfig) {
-                formData.put(formConfiguration,"");
-            }
-            FormUtility.populateFormMapFromLdap(detailFormConfig, pwmRequest.getPwmSession().getLabel(), formData, userDataReader);
+            final Map<FormConfiguration,List<String>> formData = FormUtility.populateFormMapFromLdap(detailFormConfig, pwmRequest.getPwmSession().getLabel(), userDataReader);
             detailInfo.setSearchDetails(formData);
         }
 

+ 8 - 16
src/main/java/password/pwm/http/servlet/newuser/NewUserUserDataReader.java

@@ -42,17 +42,10 @@ class NewUserUserDataReader implements UserDataReader {
         return null;
     }
 
-    @Override
-    public String readStringAttribute(String attribute)
-            throws ChaiUnavailableException, ChaiOperationException
-    {
-        return readStringAttribute(attribute, false);
-    }
-
     @Override
     public String readStringAttribute(
             String attribute,
-            boolean ignoreCache
+            Flag...flags
     )
             throws ChaiUnavailableException, ChaiOperationException
     {
@@ -66,17 +59,10 @@ class NewUserUserDataReader implements UserDataReader {
         return null;
     }
 
-    @Override
-    public Map<String, String> readStringAttributes(Collection<String> attributes)
-            throws ChaiUnavailableException, ChaiOperationException
-    {
-        return readStringAttributes(attributes, false);
-    }
-
     @Override
     public Map<String, String> readStringAttributes(
             Collection<String> attributes,
-            boolean ignoreCache
+            Flag... flags
     )
             throws ChaiUnavailableException, ChaiOperationException
     {
@@ -86,4 +72,10 @@ class NewUserUserDataReader implements UserDataReader {
         }
         return Collections.unmodifiableMap(returnObj);
     }
+
+    @Override
+    public List<String> readMultiStringAttribute(String attribute, Flag... flags) throws ChaiUnavailableException, ChaiOperationException {
+        final String value = readStringAttribute(attribute, flags);
+        return value == null ? null : Collections.singletonList(value);
+    }
 }

+ 6 - 5
src/main/java/password/pwm/http/servlet/peoplesearch/AttributeDetailBean.java

@@ -26,12 +26,13 @@ import password.pwm.config.FormConfiguration;
 
 import java.io.Serializable;
 import java.util.Collection;
+import java.util.List;
 
 class AttributeDetailBean implements Serializable {
     private String name;
     private String label;
     private FormConfiguration.Type type;
-    private String value;
+    private List<String> values;
     private Collection<UserReferenceBean> userReferences;
     private boolean searchable;
 
@@ -59,12 +60,12 @@ class AttributeDetailBean implements Serializable {
         this.type = type;
     }
 
-    public String getValue() {
-        return value;
+    public List<String> getValues() {
+        return values;
     }
 
-    public void setValue(String value) {
-        this.value = value;
+    public void setValues(List<String> values) {
+        this.values = values;
     }
 
     public Collection<UserReferenceBean> getUserReferences() {

+ 32 - 2
src/main/java/password/pwm/http/servlet/peoplesearch/PeopleSearchServlet.java

@@ -670,8 +670,15 @@ public class PeopleSearchServlet extends AbstractPwmServlet {
                         bean.setUserReferences(userReferences.values());
                     }
                 } else {
-                    bean.setValue(searchResults.containsKey(formConfiguration.getName()) ? searchResults.get(
-                            formConfiguration.getName()) : "");
+                    if (formConfiguration.isMultivalue()) {
+                        bean.setValues(readUserMultiAttributeValues(pwmRequest, userIdentity, formConfiguration.getName()));
+                    } else {
+                        if (searchResults.containsKey(formConfiguration.getName())) {
+                            bean.setValues(Collections.singletonList(searchResults.get(formConfiguration.getName())));
+                        } else {
+                            bean.setValues(Collections.<String>emptyList());
+                        }
+                    }
                 }
                 returnObj.put(formConfiguration.getName(),bean);
             }
@@ -863,6 +870,29 @@ public class PeopleSearchServlet extends AbstractPwmServlet {
         return returnObj;
     }
 
+    private static List<String> readUserMultiAttributeValues(
+            final PwmRequest pwmRequest,
+            final UserIdentity userIdentity,
+            final String attributeName
+    )
+            throws PwmUnrecoverableException
+    {
+
+        final List<UserIdentity> returnObj = new ArrayList<>();
+
+        final int MAX_VALUES = Integer.parseInt(pwmRequest.getConfig().readAppProperty(AppProperty.PEOPLESEARCH_VALUE_MAXCOUNT));
+        final ChaiUser chaiUser = getChaiUser(pwmRequest, userIdentity);
+        try {
+            final Set<String> ldapValues = chaiUser.readMultiStringAttribute(attributeName);
+            return ldapValues != null ? new ArrayList<>(ldapValues) : Collections.<String>emptyList();
+        } catch (ChaiOperationException e) {
+            throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_DIRECTORY_UNAVAILABLE, "error reading attribute value '" + attributeName + "', error:" +  e.getMessage()));
+        } catch (ChaiUnavailableException e) {
+            throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_DIRECTORY_UNAVAILABLE, e.getMessage()));
+        }
+
+    }
+
     private CacheKey makeCacheKey(
             final PwmRequest pwmRequest,
             final String operationIdentifer,

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

@@ -70,6 +70,7 @@ public enum PwmIfTest {
     forwardUrlDefined(new ForwardUrlDefinedTest()),
 
     trialMode(new TrialModeTest()),
+    appliance(new EnvironmentFlagTest(PwmEnvironment.ApplicationFlag.Appliance)),
 
     healthWarningsPresent(new HealthWarningsPresentTest()),
     usernameHasValue(new UsernameHasValueTest()),
@@ -363,4 +364,17 @@ public enum PwmIfTest {
         }
     }
 
+    private static class EnvironmentFlagTest implements Test {
+        private final PwmEnvironment.ApplicationFlag flag;
+
+        public EnvironmentFlagTest(PwmEnvironment.ApplicationFlag flag) {
+            this.flag = flag;
+        }
+
+        @Override
+        public boolean test(PwmRequest pwmRequest, PwmIfOptions options) throws ChaiUnavailableException, PwmUnrecoverableException {
+            return pwmRequest.getPwmApplication().getPwmEnvironment().getFlags().contains(flag);
+        }
+    }
+
 }

+ 50 - 21
src/main/java/password/pwm/ldap/LdapUserDataReader.java

@@ -33,6 +33,7 @@ import password.pwm.bean.UserIdentity;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.PwmSession;
+import password.pwm.util.Helper;
 
 import java.io.Serializable;
 import java.util.*;
@@ -47,6 +48,10 @@ public class LdapUserDataReader implements Serializable, UserDataReader {
     private final ChaiUser user;
     private final UserIdentity userIdentity;
 
+    private enum PrivateFlag {
+        MultiValueRead
+    }
+
     public LdapUserDataReader(
             UserIdentity userIdentity,
             ChaiUser user
@@ -62,7 +67,7 @@ public class LdapUserDataReader implements Serializable, UserDataReader {
             throws PwmUnrecoverableException
     {
         final ChaiUser user;
-            user = pwmApplication.getProxiedChaiUser(userIdentity);
+        user = pwmApplication.getProxiedChaiUser(userIdentity);
         return new LdapUserDataReader(userIdentity, user);
     }
 
@@ -87,21 +92,14 @@ public class LdapUserDataReader implements Serializable, UserDataReader {
         return this.user.getEntryDN();
     }
 
-    @Override
-    public String readStringAttribute(final String attribute)
-            throws ChaiUnavailableException, ChaiOperationException
-    {
-        return readStringAttribute(attribute, false);
-    }
-
     @Override
     public String readStringAttribute(
             final String attribute,
-            boolean ignoreCache
+            final Flag... flags
     )
             throws ChaiUnavailableException, ChaiOperationException
     {
-        Map<String,String> results = readStringAttributes(Collections.singletonList(attribute),ignoreCache);
+        Map<String,String> results = readStringAttributes(Collections.singletonList(attribute), flags);
         if (results == null || results.isEmpty()) {
             return null;
         }
@@ -117,20 +115,40 @@ public class LdapUserDataReader implements Serializable, UserDataReader {
     }
 
 
-    @Override
-    public Map<String,String> readStringAttributes(final Collection<String> attributes)
+    public Map<String,String> readStringAttributes(
+            final Collection<String> attributes,
+            final Flag... flags
+    )
+            throws ChaiUnavailableException, ChaiOperationException
+    {
+        final Map<String,List<String>> valueMap = readMultiStringAttributesImpl(attributes, Collections.<PrivateFlag>emptyList(), flags);
+        final Map<String,String> returnValue = new LinkedHashMap<>();
+        for (final String key : valueMap.keySet()) {
+            final List<String> values = valueMap.get(key);
+            if (values != null && !values.isEmpty()) {
+                returnValue.put(key, values.iterator().next());
+            }
+        }
+        return returnValue;
+    }
+
+    public Map<String,List<String>> readMultiStringAttributes(
+            final Collection<String> attributes,
+            final Flag... flags
+    )
             throws ChaiUnavailableException, ChaiOperationException
     {
-        return readStringAttributes(attributes, false);
+        return readMultiStringAttributesImpl(attributes, Collections.singletonList(PrivateFlag.MultiValueRead), flags);
     }
 
-    @Override
-    public Map<String,String> readStringAttributes(
+    private Map<String,List<String>> readMultiStringAttributesImpl(
             final Collection<String> attributes,
-            boolean ignoreCache
+            final Collection<PrivateFlag> privateFlags,
+            final Flag... flags
     )
             throws ChaiUnavailableException, ChaiOperationException
     {
+        final boolean ignoreCache = Helper.enumArrayContainsValue(flags, Flag.ignoreCache);
         if (user == null || attributes == null || attributes.isEmpty()) {
             return Collections.emptyMap();
         }
@@ -145,20 +163,31 @@ public class LdapUserDataReader implements Serializable, UserDataReader {
 
         // read uncached attributes into cache
         if (!uncachedAttributes.isEmpty()) {
-            final Map<String,String> readData = user.readStringAttributes(new HashSet<>(uncachedAttributes));
             for (final String attribute : attributes) {
-                cacheMap.put(attribute,readData.containsKey(attribute) ? readData.get(attribute) : NULL_CACHE_VALUE);
+                if (privateFlags.contains(PrivateFlag.MultiValueRead)) {
+                    final Set<String> readData = user.readMultiStringAttribute(attribute);
+                    final List<String> stringList = readData == null ? null : new ArrayList<>(readData);
+                    cacheMap.put(attribute, stringList != null && !stringList.isEmpty() ? stringList : NULL_CACHE_VALUE);
+                } else {
+                    final String readData = user.readStringAttribute(attribute);
+                    cacheMap.put(attribute, readData != null ? Collections.singletonList(readData) : NULL_CACHE_VALUE);
+                }
             }
         }
 
         // build result data from cache
-        final Map<String,String> returnMap = new HashMap<>();
+        final Map<String,List<String>> returnMap = new HashMap<>();
         for (final String attribute : attributes) {
             final Object cachedValue = cacheMap.get(attribute);
             if (cachedValue != null && !NULL_CACHE_VALUE.equals(cachedValue)) {
-                returnMap.put(attribute,(String)cachedValue);
+                returnMap.put(attribute,(List<String>)cachedValue);
             }
         }
-        return returnMap;
+        return Collections.unmodifiableMap(returnMap);
+    }
+
+    @Override
+    public List<String> readMultiStringAttribute(String attribute, Flag... flags) throws ChaiUnavailableException, ChaiOperationException {
+        return readMultiStringAttributesImpl(Collections.singletonList(attribute), Collections.singletonList(PrivateFlag.MultiValueRead), flags).get(attribute);
     }
 }

+ 13 - 8
src/main/java/password/pwm/ldap/UserDataReader.java

@@ -27,29 +27,34 @@ import com.novell.ldapchai.exception.ChaiUnavailableException;
 
 import java.util.Collection;
 import java.util.Date;
+import java.util.List;
 import java.util.Map;
 
 public interface UserDataReader {
-    String getUserDN();
+    enum Flag {
+        ignoreCache
+    }
 
-    String readStringAttribute(String attribute)
-            throws ChaiUnavailableException, ChaiOperationException;
+    String getUserDN();
 
     String readStringAttribute(
             String attribute,
-            boolean ignoreCache
+            Flag... flags
     )
                     throws ChaiUnavailableException, ChaiOperationException;
 
+    List<String> readMultiStringAttribute(
+            String attribute,
+            Flag... flags
+    )
+            throws ChaiUnavailableException, ChaiOperationException;
+
     Date readDateAttribute(String attribute)
                             throws ChaiUnavailableException, ChaiOperationException;
 
-    Map<String,String> readStringAttributes(Collection<String> attributes)
-                                    throws ChaiUnavailableException, ChaiOperationException;
-
     Map<String,String> readStringAttributes(
             Collection<String> attributes,
-            boolean ignoreCache
+            Flag... flags
     )
                                             throws ChaiUnavailableException, ChaiOperationException;
 }

+ 3 - 11
src/main/java/password/pwm/util/JsonUtil.java

@@ -52,25 +52,17 @@ public class JsonUtil {
             .create();
 
     private static Gson getGson(final Flag... flags) {
-        if (flags == null) {
-            return getGson(Collections.<Flag>emptySet());
-        } else {
-            return getGson(new HashSet(Arrays.asList(flags)));
-        }
-    }
-
-    private static Gson getGson(final Set<Flag> flags) {
-        if (flags == null || flags.isEmpty()) {
+        if (flags == null || flags.length == 0) {
             return GENERIC_GSON;
         }
 
         final GsonBuilder gsonBuilder = registerTypeAdapters(new GsonBuilder());
 
-        if (!flags.contains(Flag.HtmlEscape)) {
+        if (!Helper.enumArrayContainsValue(flags, Flag.HtmlEscape)) {
             gsonBuilder.disableHtmlEscaping();
         }
 
-        if (flags.contains(Flag.PrettyPrint)) {
+        if (Helper.enumArrayContainsValue(flags, Flag.PrettyPrint)) {
             gsonBuilder.setPrettyPrinting();
         }
 

+ 3 - 5
src/main/java/password/pwm/util/StringUtil.java

@@ -151,15 +151,13 @@ public abstract class StringUtil {
         URL_SAFE,
         ;
 
-        private static int asBase64UtilOptions(Base64Options[] options) {
+        private static int asBase64UtilOptions(final Base64Options... options) {
             int b64UtilOptions = 0;
-            Set<Base64Options> optionsEnum = EnumSet.noneOf(Base64Options.class);
-            optionsEnum.addAll(Arrays.asList(options));
 
-            if (optionsEnum.contains(Base64Options.GZIP)) {
+            if (Helper.enumArrayContainsValue(options, Base64Options.GZIP)) {
                 b64UtilOptions = b64UtilOptions | Base64.GZIP;
             }
-            if (optionsEnum.contains(Base64Options.URL_SAFE)) {
+            if (Helper.enumArrayContainsValue(options, Base64Options.URL_SAFE)) {
                 b64UtilOptions = b64UtilOptions | Base64.URL_SAFE;
             }
             return b64UtilOptions;

+ 21 - 15
src/main/java/password/pwm/util/cli/MainClass.java

@@ -57,7 +57,7 @@ public class MainClass {
 
     private static MainOptions MAIN_OPTIONS = new MainOptions();
 
-    public static final Map<String,CliCommand> COMMANDS;
+    private static final Map<String,CliCommand> COMMANDS;
     static {
         final List<CliCommand> commandList = new ArrayList<>();
         commandList.add(new LocalDBInfoCommand());
@@ -91,7 +91,7 @@ public class MainClass {
         COMMANDS = Collections.unmodifiableMap(sortedMap);
     }
 
-    public static String helpTextFromCommands(Collection<CliCommand> commands) {
+    static String helpTextFromCommands(Collection<CliCommand> commands) {
         final StringBuilder output = new StringBuilder();
         for (CliCommand command : commands) {
             output.append(command.getCliParameters().commandName);
@@ -112,13 +112,13 @@ public class MainClass {
         return output.toString();
     }
 
-    public static String makeHelpTextOutput() {
+    private static String makeHelpTextOutput() {
         final StringBuilder output = new StringBuilder();
         output.append(helpTextFromCommands(COMMANDS.values()));
         output.append("\n");
         output.append("options:\n");
         output.append(" -force                force operations skipping any confirmation\n");
-        output.append(" -debugLevel=x         set the debug level where x is TRACE, DEBUG, INFO, WARN or FATAL\n");
+        output.append(" -debugLevel=x         set the debug level where x is TRACE, DEBUG, INFO, ERROR, WARN or FATAL\n");
         output.append(" -applicationPath=x    set the application path, default is current path\n");
         output.append("\n");
         output.append("usage: \n");
@@ -260,6 +260,9 @@ public class MainClass {
                     try {
                         cliEnvironment = createEnv(command.getCliParameters(), argList);
                     } catch (Exception e) {
+                        final String errorMsg = "unable to establish operating environment: " + e.getMessage();
+                        final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_ENVIRONMENT_ERROR, errorMsg);
+                        LOGGER.error(errorInformation.toDebugStr(),e);
                         System.out.println("unable to establish operating environment: " + e.getMessage());
                         System.exit(-1);
                         return;
@@ -296,7 +299,7 @@ public class MainClass {
         }
     }
 
-    static String[] parseMainCommandLineOptions(String[] args) {
+    private static String[] parseMainCommandLineOptions(String[] args) {
         final String OPT_DEBUG_LEVEL = "-debugLevel";
         final String OPT_APP_PATH = "-applicationPath";
         final String OPT_APP_FLAGS= "-applicationFlags";
@@ -351,7 +354,7 @@ public class MainClass {
         return outputArgs.toArray(new String[outputArgs.size()]);
     }
 
-    static void initLog4j(PwmLogLevel logLevel) {
+    private static void initLog4j(PwmLogLevel logLevel) {
         if (logLevel == null) {
             Logger.getRootLogger().removeAllAppenders();
             Logger.getRootLogger().addAppender(new NullAppender());
@@ -371,7 +374,7 @@ public class MainClass {
         PwmLogger.markInitialized();
     }
 
-    static LocalDB loadPwmDB(
+    private static LocalDB loadPwmDB(
             final Configuration config,
             final boolean readonly,
             final File applicationPath
@@ -384,7 +387,7 @@ public class MainClass {
         return LocalDBFactory.getInstance(databaseDirectory, readonly, null, config);
     }
 
-    static ConfigurationReader loadConfiguration(final File configurationFile) throws Exception {
+    private static ConfigurationReader loadConfiguration(final File configurationFile) throws Exception {
         final ConfigurationReader reader = new ConfigurationReader(configurationFile);
 
         if (reader.getConfigMode() == PwmApplicationMode.ERROR) {
@@ -396,7 +399,7 @@ public class MainClass {
         return reader;
     }
 
-    static PwmApplication loadPwmApplication(
+    private static PwmApplication loadPwmApplication(
             final File applicationPath,
             final Collection<PwmEnvironment.ApplicationFlag> flags,
             final Configuration config,
@@ -406,9 +409,12 @@ public class MainClass {
             throws LocalDBException, PwmUnrecoverableException
     {
         final PwmApplicationMode mode = readonly ? PwmApplicationMode.READ_ONLY : PwmApplicationMode.RUNNING;
-        final Collection<PwmEnvironment.ApplicationFlag> applicationFlags = flags == null
-                ? PwmEnvironment.ParseHelper.readApplicationFlagsFromSystem(null)
-                : flags;
+        final Collection<PwmEnvironment.ApplicationFlag> applicationFlags = new HashSet<>();
+        if (flags == null) {
+            applicationFlags.addAll(PwmEnvironment.ParseHelper.readApplicationFlagsFromSystem(null));
+        } else {
+            applicationFlags.addAll(flags);
+        }
         applicationFlags.add(PwmEnvironment.ApplicationFlag.CommandLineInstance);
         final PwmEnvironment pwmEnvironment = new PwmEnvironment.Builder(config, applicationPath)
                 .setApplicationMode(mode)
@@ -426,7 +432,7 @@ public class MainClass {
         return pwmApplication;
     }
 
-    static File locateConfigurationFile(File applicationPath) {
+    private static File locateConfigurationFile(File applicationPath) {
         return new File(applicationPath + File.separator + PwmConstants.DEFAULT_CONFIG_FILE_FILENAME);
     }
 
@@ -434,7 +440,7 @@ public class MainClass {
         System.out.println(txt + "\n");
     }
 
-    public static class MainOptions {
+    static class MainOptions {
         private PwmLogLevel pwmLogLevel = null;
         private File applicationPath = null;
         private boolean forceFlag = false;
@@ -448,7 +454,7 @@ public class MainClass {
             return applicationPath;
         }
 
-        public boolean isForceFlag() {
+        boolean isForceFlag() {
             return forceFlag;
         }
     }

+ 10 - 1
src/main/java/password/pwm/util/operations/ActionExecutor.java

@@ -35,6 +35,7 @@ import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.HttpMethod;
 import password.pwm.http.PwmSession;
 import password.pwm.http.client.PwmHttpClient;
+import password.pwm.http.client.PwmHttpClientConfiguration;
 import password.pwm.http.client.PwmHttpClientRequest;
 import password.pwm.http.client.PwmHttpClientResponse;
 import password.pwm.util.logging.PwmLogger;
@@ -164,7 +165,15 @@ public class ActionExecutor {
             final HttpMethod method = HttpMethod.fromString(actionConfiguration.getMethod().toString());
 
             final PwmHttpClientRequest clientRequest = new PwmHttpClientRequest(method, url, body, headers);
-            final PwmHttpClient client = new PwmHttpClient(pwmApplication, pwmSession.getLabel());
+            final PwmHttpClient client;
+            {
+                if (actionConfiguration.getCertificates() != null) {
+                    final PwmHttpClientConfiguration clientConfiguration = new PwmHttpClientConfiguration(actionConfiguration.getCertificates());
+                    client = new PwmHttpClient(pwmApplication, pwmSession.getLabel(), clientConfiguration);
+                } else {
+                    client = new PwmHttpClient(pwmApplication, pwmSession.getLabel());
+                }
+            }
             final PwmHttpClientResponse clientResponse = client.makeRequest(clientRequest);
 
             if (clientResponse.getStatusCode() != 200) {

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

@@ -2136,10 +2136,8 @@
             <option value="SHA256_SALT">SHA-256 with Salt</option>
             <option value="SHA512_SALT">SHA-512 with Salt</option>
             <option value="PBKDF2">PBKDF2WithHmacSHA1</option>
-            <!--
             <option value="PBKDF2_SHA256">PBKDF2WithHmacSHA256</option>
             <option value="PBKDF2_SHA512">PBKDF2WithHmacSHA512</option>
-            -->
             <option value="BCRYPT">BCrypt</option>
             <option value="SCRYPT">SCrypt</option>
         </options>
@@ -2697,7 +2695,8 @@
         </default>
     </setting>
     <setting hidden="false" key="peopleSearch.detail.form" level="1" required="true">
-        <flag>Form_HideOptions</flag>
+        <flag>Form_HideStandardOptions</flag>
+        <flag>Form_ShowMultiValueOption</flag>
         <ldapPermission actor="proxy" access="read"/>
         <default>
             <value>{"name":"givenName","minimumLength":1,"maximumLength":64,"type":"text","required":false,"confirmationRequired":false,"readonly":true,"labels":{"":"First Name"},"regexErrors":{"":""},"description":{"":""},"selectOptions":{}}</value>
@@ -2928,7 +2927,8 @@
         </default>
     </setting>
     <setting hidden="false" key="helpdesk.detail.form" level="1" required="true">
-        <flag>Form_HideOptions</flag>
+        <flag>Form_HideStandardOptions</flag>
+        <flag>Form_ShowMultiValueOption</flag>
         <ldapPermission actor="helpdesk" access="read"/>
         <default>
             <value>{"name":"cn","minimumLength":1,"maximumLength":64,"type":"text","required":true,"confirmationRequired":false,"readonly":false,"labels":{"":"CN"},"regexErrors":{"":""},"description":{"":""},"selectOptions":{}}</value>

+ 3 - 2
src/main/resources/password/pwm/i18n/Config.properties

@@ -26,7 +26,7 @@ Actor_Label_proxy=Proxy User
 Actor_Label_self=Self (Logged in User)
 Actor_Label_helpdesk=Helpdesk Operator
 Actor_Label_guestManager=Guest Manager
-Actor_Description_proxy=Permissions required by the LDAP proxy user (defined by the setting <code>@PwmSettingReference:ldap.proxy.password@</code>).  The proxy user will utilize these attribute permissions against any user authenticating to @PwmAppName@.
+Actor_Description_proxy=Permissions required by the LDAP proxy user (defined by the setting <code>@PwmSettingReference:ldap.proxy.username@</code>).  The proxy user will utilize these attribute permissions against any user authenticating to @PwmAppName@.
 Actor_Description_self=Permissions required by logged in users.  Each logged in user should have these permissions against their own LDAP entry for these attributes, but not against other LDAP entries.
 Actor_Description_helpdesk=Permissions required by logged in user while using the helpdesk module.  The logged in user should have these attribute permissions against the LDAP entries of the user's being administered via the helpdesk module.  This is typically done using an LDAP group or permission-role object to assign permissions.
 Actor_Description_guestManager=Permissions required by logged in user while using the guest registration module.  The logged in user should have these attribute permissions against the LDAP entries of the user's being managed via the guest registration module.  This is typically done using an LDAP group or permission-role object to assign permissions.
@@ -62,7 +62,7 @@ Display_SettingNavigationNullProfile=[profile]
 Display_RememberLogin=Remember password for %1%.
 Display_ProfileNamingRules=<p>Profile names have the following requirements\:</p><ul><li>Start with a letter (a-Z)</li><li>Contain only letters, numbers and hyphens</li><li>Length between 2 and 15 characters</li></ul>
 Label_UserPasswordAttribute=[User Password]
-Label_ProfileListEditMenuItem=[- Edit List -]
+Label_ProfileListEditMenuItem=\u2772 Edit List \u2773
 MenuDisplay_AlternateNewConfig=Edit a new configuration in memory by selecting a new configuration template. After editing the configuration, you can download the <em>%1%</em> file.  This option will not modify the running configuration.
 MenuDisplay_AlternateUnlockConfig=The closing of the <em>%1%</em> file is controlled by the property "configIsEditable" within the file.  Set this property to "true" to return to the online configuration mode. Be aware that while this property is set to true anyone accessing this site can make modifications to the live configuration without authentication.
 MenuDisplay_AlternateUpload=Alternatively, you may upload a previously saved configuration file. The uploaded file will be saved as the new configuration.
@@ -149,6 +149,7 @@ Tooltip_FormOptions_Regex=Apply a regular expression pattern to the value.  The
 Tooltip_FormOptions_RegexError=Error message to show when the regular expression pattern is not matched.
 Tooltip_FormOptions_Placeholder=Placeholder text to display in the form field with the field is not populated with a value.
 Tooltip_FormOptions_Javascript=Javascript to be added to the browser.
+Tooltip_FormOptions_MultiValue=Display multiple values of the attribute.
 VerificationMethodDetail_PREVIOUS_AUTH=This method is passed when a user has previously authenticated using their browser.  There is no user interaction or display associated with this method.
 VerificationMethodDetail_ATTRIBUTES=User will be prompted for LDAP attribute values defined by the setting @PwmSettingReference:challenge.requiredAttributes@.
 VerificationMethodDetail_CHALLENGE_RESPONSES=Challenge/Response questions and answers.

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

@@ -209,7 +209,7 @@ Setting_Description_challenge.caseInsensitive=Control the case sensitivity of re
 Setting_Description_challenge.enable=If enabled, save responses page will be available to users.
 Setting_Description_challenge.enforceMinimumPasswordLifetime=When the user authenticates via ForgottenPassword, should the password minimum lifetime (if set) be enforced?  If this setting is true, the user cannot change password if the minimum lifetime hasn't passed.  If false, the user is permitted to change their password when authenticated via Forgotten Password even if the minimum lifetime has not changed.
 Setting_Description_challenge.forceSetup=If true, the user will be directed to configure challenge/response when logging in.  The user is forced to enter responses if they do not have a current valid response stored.
-Setting_Description_challenge.helpdesk.minRandomsSetup=The minimum number of helpdesk random questions the user is required to complete during Response Setup.  If this number is higher then the available randoms, or lower then the minimum required, it will be adjusted accordingly.  Set to zero to force all available randoms to be configured at time of setup.
+Setting_Description_challenge.helpdesk.minRandomsSetup=The minimum number of helpdesk random questions the user is required to complete during Response Setup.  If this number is higher than the available randoms, or lower than the minimum required, it will be adjusted accordingly.  Set to zero to force all available randoms to be configured at time of setup.
 Setting_Description_challenge.helpdesk.randomChallenges=The user may be required to supply answers to all or some of these questions when setting up their responses, as controlled by the "Minimum Helpdesk Random Challenges Required During Setup" setting.  The questions and answers will be visible to helpdesk users, but are not used for forgotten password recovery.
 Setting_Description_challenge.helpdesk.requiredChallenges=The user must supply answers for all of these questions when setting up their responses.  The questions and answers will be visible to helpdesk users, but are not used for forgotten password recovery.
 Setting_Description_challenge.minRandomRequired=Minimum number of random questions required at time of forgotten password recovery.

+ 1 - 1
src/main/webapp/WEB-INF/Command.bat

@@ -4,7 +4,7 @@ setLocal EnableDelayedExpansion
 if NOT DEFINED JAVA_HOME goto err
 
 set JAVA_OPTS="-Xmx1024m"
-set CLASSPATH="lib/*:classes"
+set CLASSPATH="lib/*;classes"
 
 %JAVA_HOME%\bin\java %JAVA_OPTS% -classpath !CLASSPATH! password.pwm.util.cli.MainClass %1 %2 %3 %4 %5 %6 %7 %8 %9
 goto finally

+ 3 - 3
src/main/webapp/WEB-INF/jsp/configguide-end.jsp

@@ -1,5 +1,5 @@
+<%@ page import="password.pwm.http.servlet.PwmServletDefinition" %>
 <%@ page import="password.pwm.http.servlet.configguide.ConfigGuideForm" %>
-<%@ page import="password.pwm.http.tag.conditional.PwmIfTest" %>
 <%@ page import="password.pwm.util.StringUtil" %>
 <%--
   ~ Password Management Servlets (PWM)
@@ -147,8 +147,8 @@
 
                 htmlBody += '<br/><br/><table><tr><td colspan="3" class="title">URLs</td></tr>';
                 htmlBody += '<tr><td class="key">Application</td><td> <a href="<pwm:context/>"><pwm:context/></a></td></tr>';
-                htmlBody += '<tr><td class="key">Configuration Manager</td><td> <a href="<pwm:context/>/private/config/ConfigManager"><pwm:context/>/private/config/ConfigManager</a></td></tr>';
-                htmlBody += '<tr><td class="key">Configuration Editor</td><td> <a href="<pwm:context/>/private/config/ConfigEditor"><pwm:context/>/private/config/ConfigEditor</a></td></tr>';
+                htmlBody += '<tr><td class="key">Configuration Manager</td><td> <a href="<pwm:context/><pwm:context/><%=PwmServletDefinition.ConfigManager.servletUrl()%>"><pwm:context/><pwm:context/><%=PwmServletDefinition.ConfigManager.servletUrl()%></a></td></tr>';
+                htmlBody += '<tr><td class="key">Configuration Editor</td><td> <a href="<pwm:context/><pwm:context/><%=PwmServletDefinition.ConfigEditor.servletUrl()%>"><pwm:context/><pwm:context/><%=PwmServletDefinition.ConfigEditor.servletUrl()%></a></td></tr>';
                 htmlBody += '</table>';
 
                 PWM_MAIN.showConfirmDialog({text:htmlBody,okAction:function(){

+ 4 - 2
src/main/webapp/WEB-INF/jsp/configmanager.jsp

@@ -77,7 +77,7 @@
                     <%=JspUtility.getAttribute(pageContext, PwmRequest.Attribute.ConfigHasPassword)%>
                 </td>
             </tr>
-            <% if (!JspUtility.getPwmRequest(pageContext).getPwmApplication().getPwmEnvironment().getFlags().contains(PwmEnvironment.ApplicationFlag.Appliance)) { %>
+            <pwm:if test="<%=PwmIfTest.appliance%>" negate="true">
             <tr>
                 <td>
                     Application Data Path
@@ -98,7 +98,7 @@
                     </div>
                 </td>
             </tr>
-            <% } %>
+            </pwm:if>
         </table>
         <br/>
         <div id="healthBody" class="health-body">
@@ -190,6 +190,7 @@
                         </div>
                     </pwm:if>
                     <pwm:if test="<%=PwmIfTest.configurationOpen%>" negate="true">
+                        <pwm:if test="<%=PwmIfTest.appliance%>" negate="true">
                         <a class="menubutton" id="MenuItem_UnlockConfig">
                             <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-unlock"></span></pwm:if>
                             <pwm:display key="MenuItem_UnlockConfig" bundle="Config"/>
@@ -209,6 +210,7 @@
                                 });
                             </script>
                         </pwm:script>
+                        </pwm:if>
                     </pwm:if>
                 </td>
                 <td class="buttoncell">

+ 6 - 3
src/main/webapp/WEB-INF/jsp/helpdesk-detail.jsp

@@ -28,7 +28,6 @@
 <%@ page import="password.pwm.config.FormConfiguration" %>
 <%@ page import="password.pwm.config.PwmSetting" %>
 <%@ page import="password.pwm.config.option.HelpdeskUIMode" %>
-<%@ page import="password.pwm.config.option.MessageSendMethod" %>
 <%@ page import="password.pwm.config.option.ViewStatusFields" %>
 <%@ page import="password.pwm.config.profile.HelpdeskProfile" %>
 <%@ page import="password.pwm.config.profile.PwmPasswordRule" %>
@@ -42,6 +41,7 @@
 <%@ page import="password.pwm.util.macro.MacroMachine" %>
 <%@ page import="java.text.DateFormat" %>
 <%@ page import="java.util.Date" %>
+<%@ page import="java.util.Iterator" %>
 <%@ page import="java.util.List" %>
 <%@ page import="java.util.Set" %>
 <!DOCTYPE html>
@@ -93,12 +93,15 @@
                                 <table class="nomargin">
                                     <% for (FormConfiguration formItem : helpdeskDetailInfoBean.getSearchDetails().keySet()) { %>
                                     <tr>
-                                        <td class="key" id="key_<%=formItem.getName()%>">
+                                        <td class="key" id="key_<%=StringUtil.escapeHtml(formItem.getName())%>" title="<%=StringUtil.escapeHtml(formItem.getDescription(pwmRequest.getLocale()))%>">
                                             <%= formItem.getLabel(pwmSession.getSessionStateBean().getLocale())%>
                                         </td>
                                         <td id="value_<%=formItem.getName()%>">
-                                            <% final String loopValue = helpdeskDetailInfoBean.getSearchDetails().get(formItem); %>
+                                            <% for (Iterator<String> iter = helpdeskDetailInfoBean.getSearchDetails().get(formItem).iterator(); iter.hasNext(); ) { %>
+                                            <% final String loopValue = iter.next(); %>
                                             <%= loopValue == null ? "" : StringUtil.escapeHtml(loopValue) %>
+                                            <% if (iter.hasNext()) { %> <br/> <% } %>
+                                            <% } %>
                                         </td>
                                     </tr>
                                     <%  } %>

+ 211 - 116
src/main/webapp/public/resources/js/configeditor-settings.js

@@ -327,11 +327,11 @@ StringArrayValueHandler.drawRow = function(settingKey, iteration, value, itemCou
                     if (data['error']) {
                         PWM_MAIN.showErrorDialog(data);
                     } else {
-                        PWM_MAIN.goto('ConfigEditor');
+                        PWM_MAIN.goto('editor');
                     }
                 };
                 PWM_MAIN.showWaitDialog({loadFunction:function(){
-                    PWM_MAIN.ajaxRequest("ConfigEditor?processAction=copyProfile",resultFunction,{content:options});
+                    PWM_MAIN.ajaxRequest("editor?processAction=copyProfile",resultFunction,{content:options});
                 }});
             };
             UILibrary.stringEditorDialog(editorOptions);
@@ -416,7 +416,7 @@ StringArrayValueHandler.writeSetting = function(settingKey, reload) {
     var syntax = PWM_SETTINGS['settings'][settingKey]['syntax'];
     var nextFunction = function() {
         if (syntax == 'PROFILE') {
-            PWM_MAIN.goto('ConfigEditor');
+            PWM_MAIN.goto('editor');
         }
         if (reload) {
             StringArrayValueHandler.init(settingKey);
@@ -657,11 +657,6 @@ FormTableHandler.drawRow = function(parentDiv, settingKey, iteration, value) {
         htmlRow += '<td style="background: #f6f9f8; border:1px solid #dae1e1; width:170px"><div style="" class="noWrapTextBox " id="' + inputID + 'label"><span>' + value['labels'][''] + '</span></div></td>';
 
         var userDNtypeAllowed = options['type-userDN'] == 'show';
-        //var optionList = [];
-        //if ('Form_Types' in properties) {
-        //    optionList = JSON.parse(properties['Form_Types']);
-        //}
-        //debugger;
         if (!PWM_MAIN.JSLibrary.isEmpty(options)) {
             htmlRow += '<td style="width:15px;">';
             htmlRow += '<select id="' + inputID + 'type">';
@@ -792,20 +787,25 @@ FormTableHandler.showOptionsDialog = function(keyName, iteration) {
     var type = PWM_VAR['clientSettingCache'][keyName][iteration]['type'];
     var settings = PWM_SETTINGS['settings'][keyName];
     var options = 'options' in PWM_SETTINGS['settings'][keyName] ? PWM_SETTINGS['settings'][keyName]['options'] : {};
-    var showConfirmation = type != 'checkbox' && type != 'select';
 
+    var hideStandardOptions = PWM_MAIN.JSLibrary.arrayContains(settings['flags'],'Form_HideStandardOptions');
     var showRequired = PWM_MAIN.JSLibrary.arrayContains(settings['flags'],'Form_ShowRequiredOption') && (type != 'checkbox');
     var showUnique = PWM_MAIN.JSLibrary.arrayContains(settings['flags'],'Form_ShowUniqueOption');
     var showReadOnly = PWM_MAIN.JSLibrary.arrayContains(settings['flags'],'Form_ShowReadOnlyOption');
+    var showMultiValue = PWM_MAIN.JSLibrary.arrayContains(settings['flags'],'Form_ShowMultiValueOption');
+    var showConfirmation = type != 'checkbox' && type != 'select' && !hideStandardOptions;
+
 
     require(["dijit/Dialog","dijit/form/Textarea","dijit/form/CheckBox","dijit/form/NumberSpinner"],function(){
         var inputID = 'value_' + keyName + '_' + iteration + "_";
         var bodyText = '<div style="max-height: 500px; overflow-y: auto"><table class="noborder">';
-        bodyText += '<tr>';
-        var descriptionValue = PWM_VAR['clientSettingCache'][keyName][iteration]['description'][''];
-        bodyText += '<td id="' + inputID + '-label-description" class="key" title="' + PWM_CONFIG.showString('Tooltip_FormOptions_Description') + '">Description</td><td>';
-        bodyText += '<div class="noWrapTextBox" id="' + inputID + 'description"><span class="btn-icon pwm-icon pwm-icon-edit"></span><span>' + descriptionValue + '...</span></div>';
-        bodyText += '</td>';
+        if (!hideStandardOptions) {
+            bodyText += '<tr>';
+            var descriptionValue = PWM_VAR['clientSettingCache'][keyName][iteration]['description'][''];
+            bodyText += '<td id="' + inputID + '-label-description" class="key" title="' + PWM_CONFIG.showString('Tooltip_FormOptions_Description') + '">Description</td><td>';
+            bodyText += '<div class="noWrapTextBox" id="' + inputID + 'description"><span class="btn-icon pwm-icon pwm-icon-edit"></span><span>' + descriptionValue + '...</span></div>';
+            bodyText += '</td>';
+        }
 
         bodyText += '</tr><tr>';
         if (showRequired) {
@@ -824,28 +824,35 @@ FormTableHandler.showOptionsDialog = function(keyName, iteration) {
             bodyText += '<td id="' + inputID + '-label-unique" class="key" title="' + PWM_CONFIG.showString('Tooltip_FormOptions_Unique') + '">Unique</td><td><input type="checkbox" id="' + inputID + 'unique' + '"/></td>';
             bodyText += '</tr><tr>';
         }
-        bodyText += '<td class="key">Minimum Length</td><td><input type="number" id="' + inputID + 'minimumLength' + '"/></td>';
-        bodyText += '</tr><tr>';
-        bodyText += '<td class="key">Maximum Length</td><td><input type="number" id="' + inputID + 'maximumLength' + '"/></td>';
-        bodyText += '</tr><tr>';
+        if (showMultiValue) {
+            bodyText += '<td id="' + inputID + '-label-multivalue" class="key" title="' + PWM_CONFIG.showString('Tooltip_FormOptions_MultiValue') + '">MultiValue</td><td><input type="checkbox" id="' + inputID + 'multivalue' + '"/></td>';
+            bodyText += '</tr><tr>';
+        }
 
-        { // regex
-            bodyText += '<td id="' + inputID + '-label-regex" class="key" title="' + PWM_CONFIG.showString('Tooltip_FormOptions_Regex') + '">Regular Expression</td><td><input type="text" class="configStringInput" id="' + inputID + 'regex' + '"/></td>';
+        if (!hideStandardOptions) {
+            bodyText += '<td class="key">Minimum Length</td><td><input type="number" id="' + inputID + 'minimumLength' + '"/></td>';
+            bodyText += '</tr><tr>';
+            bodyText += '<td class="key">Maximum Length</td><td><input type="number" id="' + inputID + 'maximumLength' + '"/></td>';
             bodyText += '</tr><tr>';
 
-            var regexErrorValue = PWM_VAR['clientSettingCache'][keyName][iteration]['regexErrors'][''];
-            bodyText += '<td id="' + inputID + '-label-regexError" class="key" title="' + PWM_CONFIG.showString('Tooltip_FormOptions_RegexError') + '">Regular Expression<br/>Error Message</td><td>';
-            bodyText += '<div class="noWrapTextBox" id="' + inputID + 'regexErrors"><span class="btn-icon pwm-icon pwm-icon-edit"></span><span>' + regexErrorValue + '...</span></div>';
-            bodyText += '</td>';
+            { // regex
+                bodyText += '<td id="' + inputID + '-label-regex" class="key" title="' + PWM_CONFIG.showString('Tooltip_FormOptions_Regex') + '">Regular Expression</td><td><input type="text" class="configStringInput" id="' + inputID + 'regex' + '"/></td>';
+                bodyText += '</tr><tr>';
+
+                var regexErrorValue = PWM_VAR['clientSettingCache'][keyName][iteration]['regexErrors'][''];
+                bodyText += '<td id="' + inputID + '-label-regexError" class="key" title="' + PWM_CONFIG.showString('Tooltip_FormOptions_RegexError') + '">Regular Expression<br/>Error Message</td><td>';
+                bodyText += '<div class="noWrapTextBox" id="' + inputID + 'regexErrors"><span class="btn-icon pwm-icon pwm-icon-edit"></span><span>' + regexErrorValue + '...</span></div>';
+                bodyText += '</td>';
+                bodyText += '</tr><tr>';
+            }
+            bodyText += '<td id="' + inputID + '-label-placeholder" class="key" title="' + PWM_CONFIG.showString('Tooltip_FormOptions_Placeholder') + '">Placeholder</td><td><input type="text" id="' + inputID + 'placeholder' + '"/></td>';
             bodyText += '</tr><tr>';
-        }
-        bodyText += '<td id="' + inputID + '-label-placeholder" class="key" title="' + PWM_CONFIG.showString('Tooltip_FormOptions_Placeholder') + '">Placeholder</td><td><input type="text" id="' + inputID + 'placeholder' + '"/></td>';
-        bodyText += '</tr><tr>';
-        bodyText += '<td id="' + inputID + '-label-js" class="key" title="' + PWM_CONFIG.showString('Tooltip_FormOptions_Javascript') + '">JavaScript</td><td><input type="text" id="' + inputID + 'javascript' + '"/></td>';
-        bodyText += '</tr><tr>';
-        if (PWM_VAR['clientSettingCache'][keyName][iteration]['type'] == 'select') {
-            bodyText += '<td class="key">Select Options</td><td><button id="' + inputID + 'editOptionsButton"><span class="btn-icon pwm-icon pwm-icon-list-ul"/> Edit</button></td>';
-            bodyText += '</tr>';
+            bodyText += '<td id="' + inputID + '-label-js" class="key" title="' + PWM_CONFIG.showString('Tooltip_FormOptions_Javascript') + '">JavaScript</td><td><input type="text" id="' + inputID + 'javascript' + '"/></td>';
+            bodyText += '</tr><tr>';
+            if (PWM_VAR['clientSettingCache'][keyName][iteration]['type'] == 'select') {
+                bodyText += '<td class="key">Select Options</td><td><button id="' + inputID + 'editOptionsButton"><span class="btn-icon pwm-icon pwm-icon-list-ul"/> Edit</button></td>';
+                bodyText += '</tr>';
+            }
         }
         bodyText += '</table></div>';
 
@@ -901,56 +908,72 @@ FormTableHandler.showOptionsDialog = function(keyName, iteration) {
                 }, inputID + "unique");
             }
 
-            PWM_MAIN.clearDijitWidget(inputID + "minimumLength");
-            new dijit.form.NumberSpinner({
-                value: PWM_VAR['clientSettingCache'][keyName][iteration]['minimumLength'],
-                onChange: function () {
-                    PWM_VAR['clientSettingCache'][keyName][iteration]['minimumLength'] = this.value;
-                    FormTableHandler.write(keyName)
-                },
-                constraints: {min: 0, max: 65536},
-                style: "width: 70px"
-            }, inputID + "minimumLength");
-
-            PWM_MAIN.clearDijitWidget(inputID + "maximumLength");
-            new dijit.form.NumberSpinner({
-                value: PWM_VAR['clientSettingCache'][keyName][iteration]['maximumLength'],
-                onChange: function () {
-                    PWM_VAR['clientSettingCache'][keyName][iteration]['maximumLength'] = this.value;
-                    FormTableHandler.write(keyName)
-                },
-                constraints: {min: 0, max: 65536},
-                style: "width: 70px"
-            }, inputID + "maximumLength");
+            if (showMultiValue) {
+                PWM_MAIN.clearDijitWidget(inputID + "multivalue");
+                new dijit.form.CheckBox({
+                    checked: PWM_VAR['clientSettingCache'][keyName][iteration]['multivalue'],
+                    onChange: function () {
+                        PWM_VAR['clientSettingCache'][keyName][iteration]['multivalue'] = this.checked;
+                        FormTableHandler.write(keyName)
+                    }
+                }, inputID + "multivalue");
+            }
 
-            PWM_MAIN.clearDijitWidget(inputID + "regex");
-            new dijit.form.Textarea({
-                value: PWM_VAR['clientSettingCache'][keyName][iteration]['regex'],
-                onChange: function () {
-                    PWM_VAR['clientSettingCache'][keyName][iteration]['regex'] = this.value;
-                    FormTableHandler.write(keyName)
-                }
-            }, inputID + "regex");
+            if (!hideStandardOptions) {
+                PWM_MAIN.clearDijitWidget(inputID + "minimumLength");
+                new dijit.form.NumberSpinner({
+                    value: PWM_VAR['clientSettingCache'][keyName][iteration]['minimumLength'],
+                    onChange: function () {
+                        PWM_VAR['clientSettingCache'][keyName][iteration]['minimumLength'] = this.value;
+                        FormTableHandler.write(keyName)
+                    },
+                    constraints: {min: 0, max: 65536},
+                    style: "width: 70px"
+                }, inputID + "minimumLength");
+
+                PWM_MAIN.clearDijitWidget(inputID + "maximumLength");
+                new dijit.form.NumberSpinner({
+                    value: PWM_VAR['clientSettingCache'][keyName][iteration]['maximumLength'],
+                    onChange: function () {
+                        PWM_VAR['clientSettingCache'][keyName][iteration]['maximumLength'] = this.value;
+                        FormTableHandler.write(keyName)
+                    },
+                    constraints: {min: 0, max: 65536},
+                    style: "width: 70px"
+                }, inputID + "maximumLength");
+
+                PWM_MAIN.clearDijitWidget(inputID + "regex");
+                new dijit.form.Textarea({
+                    value: PWM_VAR['clientSettingCache'][keyName][iteration]['regex'],
+                    onChange: function () {
+                        PWM_VAR['clientSettingCache'][keyName][iteration]['regex'] = this.value;
+                        FormTableHandler.write(keyName)
+                    }
+                }, inputID + "regex");
 
 
-            PWM_MAIN.addEventHandler(inputID + 'regexErrors','click',function(){
-                FormTableHandler.showRegexErrorsDialog(keyName, iteration);
-            });
+                PWM_MAIN.addEventHandler(inputID + 'regexErrors', 'click', function () {
+                    FormTableHandler.showRegexErrorsDialog(keyName, iteration);
+                });
 
-            PWM_MAIN.clearDijitWidget(inputID + "placeholder");
-            new dijit.form.Textarea({
-                value: PWM_VAR['clientSettingCache'][keyName][iteration]['placeholder'],
-                onChange: function () {
-                    PWM_VAR['clientSettingCache'][keyName][iteration]['placeholder'] = this.value;
-                    FormTableHandler.write(keyName)
-                }
-            }, inputID + "placeholder");
+                PWM_MAIN.clearDijitWidget(inputID + "placeholder");
+                new dijit.form.Textarea({
+                    value: PWM_VAR['clientSettingCache'][keyName][iteration]['placeholder'],
+                    onChange: function () {
+                        PWM_VAR['clientSettingCache'][keyName][iteration]['placeholder'] = this.value;
+                        FormTableHandler.write(keyName)
+                    }
+                }, inputID + "placeholder");
 
-            PWM_MAIN.clearDijitWidget(inputID + "javascript");
-            new dijit.form.Textarea({
-                value: PWM_VAR['clientSettingCache'][keyName][iteration]['javascript'],
-                onChange: function(){PWM_VAR['clientSettingCache'][keyName][iteration]['javascript'] = this.value;FormTableHandler.write(keyName)}
-            },inputID + "javascript");
+                PWM_MAIN.clearDijitWidget(inputID + "javascript");
+                new dijit.form.Textarea({
+                    value: PWM_VAR['clientSettingCache'][keyName][iteration]['javascript'],
+                    onChange: function () {
+                        PWM_VAR['clientSettingCache'][keyName][iteration]['javascript'] = this.value;
+                        FormTableHandler.write(keyName)
+                    }
+                }, inputID + "javascript");
+            }
         };
 
         PWM_MAIN.showDialog({
@@ -1509,6 +1532,7 @@ ActionHandler.addRow = function(keyName) {
     UILibrary.stringEditorDialog({
         title:'New Action',
         regex:'^[0-9a-zA-Z]+$',
+        placeholder:'Action Name',
         completeFunction:function(value){
             var currentSize = PWM_MAIN.JSLibrary.itemCount(PWM_VAR['clientSettingCache'][keyName]);
             PWM_VAR['clientSettingCache'][keyName][currentSize + 1] = ActionHandler.defaultValue;
@@ -1528,15 +1552,15 @@ ActionHandler.showOptionsDialog = function(keyName, iteration) {
         var value = PWM_VAR['clientSettingCache'][keyName][iteration];
         var titleText = 'title';
         var bodyText = '<table class="noborder">';
-        if (PWM_VAR['clientSettingCache'][keyName][iteration]['type'] == 'webservice') {
-            titleText = 'Web Service options for ' + PWM_VAR['clientSettingCache'][keyName][iteration]['name'];
+        if (value['type'] == 'webservice') {
+            titleText = 'Web Service options for ' + value['name'];
             bodyText += '<tr>';
             bodyText += '<td class="key">HTTP Method</td><td style="border:0;"><select id="select-' + inputID + '-method">';
 
             for (var optionItem in ActionHandler.httpMethodOptions) {
                 var label = ActionHandler.httpMethodOptions[optionItem]['label'];
                 var optionValue = ActionHandler.httpMethodOptions[optionItem]['value'];
-                var selected = optionValue == PWM_VAR['clientSettingCache'][keyName][iteration]['method'];
+                var selected = optionValue == value['method'];
                 bodyText += '<option value="' + optionValue + '"' + (selected ? ' selected' : '') + '>' + label + '</option>';
             }
             bodyText += '</td>';
@@ -1545,12 +1569,19 @@ ActionHandler.showOptionsDialog = function(keyName, iteration) {
             bodyText += '</tr><tr>';
             bodyText += '<td class="key">URL</td><td><input type="text" class="configstringinput" style="width:400px" placeholder="http://www.example.com/service"  id="input-' + inputID + '-url' + '" value="' + value['url'] + '"/></td>';
             bodyText += '</tr>';
-            if (PWM_VAR['clientSettingCache'][keyName][iteration]['method'] != 'get') {
+            if (value['method'] != 'get') {
                 bodyText += '<tr><td class="key">Body</td><td><textarea style="max-width:400px; height:100px; max-height:100px" class="configStringInput" id="input-' + inputID + '-body' + '"/>' + value['body'] + '</textarea></td></tr>';
             }
+            if (!PWM_MAIN.JSLibrary.isEmpty(value['certificateInfos'])) {
+                bodyText += '<tr><td class="key">Certificates</td><td><a id="button-' + inputID + '-certDetail">View Certificates</a></td>';
+                bodyText += '</tr>';
+            } else {
+                bodyText += '<tr><td class="key">Certificates</td><td>None</td>';
+                bodyText += '</tr>';
+            }
             bodyText += '';
-        } else if (PWM_VAR['clientSettingCache'][keyName][iteration]['type'] == 'ldap') {
-            titleText = 'LDAP options for ' + PWM_VAR['clientSettingCache'][keyName][iteration]['name'];
+        } else if (value['type'] == 'ldap') {
+            titleText = 'LDAP options for ' + value['name'];
             bodyText += '<tr>';
             bodyText += '<td class="key">Attribute Name</td><td><input style="width:300px" class="configStringInput" type="text" id="input-' + inputID + '-attributeName' + '" value="' + value['attributeName'] + '"/></td>';
             bodyText += '</tr><tr>';
@@ -1562,13 +1593,20 @@ ActionHandler.showOptionsDialog = function(keyName, iteration) {
             for (var optionItem in ActionHandler.ldapMethodOptions) {
                 var label = ActionHandler.ldapMethodOptions[optionItem]['label'];
                 var optionValue = ActionHandler.ldapMethodOptions[optionItem]['value'];
-                var selected = optionValue == PWM_VAR['clientSettingCache'][keyName][iteration]['ldapMethod'];
+                var selected = optionValue == value['ldapMethod'];
                 bodyText += '<option value="' + optionValue + '"' + (selected ? ' selected' : '') + '>' + label + '</option>';
             }
             bodyText += '</td></tr>';
         }
         bodyText += '</table>';
 
+        if (value['type'] == 'webservice') {
+            if (!PWM_MAIN.JSLibrary.isEmpty(value['certificateInfos'])) {
+                bodyText += '<button class="btn" id="button-' + inputID + '-clearCertificates"><span class="btn-icon pwm-icon pwm-icon-trash"></span>Clear Certificates</button>'
+            } else {
+                bodyText += '<button class="btn" id="button-' + inputID + '-importCertificates"><span class="btn-icon pwm-icon pwm-icon-download"></span>Import From Server</button>'
+            }
+        }
 
         PWM_MAIN.showDialog({
             title: titleText,
@@ -1577,34 +1615,80 @@ ActionHandler.showOptionsDialog = function(keyName, iteration) {
                 PWM_MAIN.addEventHandler('button-' + inputID + '-headers','click',function(){
                     ActionHandler.showHeadersDialog(keyName,iteration);
                 });
-                if (PWM_VAR['clientSettingCache'][keyName][iteration]['type'] == 'webservice') {
+                if (value['type'] == 'webservice') {
                     PWM_MAIN.addEventHandler('select-' + inputID + '-method','change',function(){
                         var value = PWM_MAIN.getObject('select-' + inputID + '-method').value;
                         if (value == 'get') {
-                            PWM_VAR['clientSettingCache'][keyName][iteration]['body'] = '';
+                            value['body'] = '';
                         }
-                        PWM_VAR['clientSettingCache'][keyName][iteration]['method'] = value;
+                        value['method'] = value;
                         ActionHandler.write(keyName, function(){ ActionHandler.showOptionsDialog(keyName,iteration)});
                     });
                     PWM_MAIN.addEventHandler('input-' + inputID + '-url','input',function(){
-                        PWM_VAR['clientSettingCache'][keyName][iteration]['url'] = PWM_MAIN.getObject('input-' + inputID + '-url').value;
+                        value['url'] = PWM_MAIN.getObject('input-' + inputID + '-url').value;
                         ActionHandler.write(keyName);
                     });
                     PWM_MAIN.addEventHandler('input-' + inputID + '-body','input',function(){
-                        PWM_VAR['clientSettingCache'][keyName][iteration]['body'] = PWM_MAIN.getObject('input-' + inputID + '-body').value;
+                        value['body'] = PWM_MAIN.getObject('input-' + inputID + '-body').value;
                         ActionHandler.write(keyName);
                     });
-                } else if (PWM_VAR['clientSettingCache'][keyName][iteration]['type'] == 'ldap') {
+                    if (!PWM_MAIN.JSLibrary.isEmpty(value['certificateInfos'])) {
+                        PWM_MAIN.addEventHandler('button-' + inputID + '-certDetail','click',function(){
+                            var bodyText = '';
+                            for (var i in value['certificateInfos']) {
+                                var certificate = value['certificateInfos'][i];
+                                bodyText += X509CertificateHandler.certificateToHtml(certificate,keyName,i);
+                            }
+                            var cancelFunction = function(){ ActionHandler.showOptionsDialog(keyName,iteration); };
+                            var loadFunction = function(){
+                                for (var i in value['certificateInfos']) {
+                                    var certificate = value['certificateInfos'][i];
+                                    X509CertificateHandler.certHtmlActions(certificate,keyName,i);
+                                }
+                            };
+                           PWM_MAIN.showDialog({
+                               title:'Certificate Detail',
+                               dialogClass: 'wide',
+                               text:bodyText,
+                               okAction:cancelFunction,
+                               loadFunction:loadFunction
+                           });
+                        });
+                        PWM_MAIN.addEventHandler('button-' + inputID + '-clearCertificates','click',function() {
+                            PWM_MAIN.showConfirmDialog({okAction:function(){
+                                delete value['certificates'];
+                                delete value['certificateInfos'];
+                                ActionHandler.write(keyName, function(){ ActionHandler.showOptionsDialog(keyName,iteration)});
+                            },cancelAction:function(){
+                                ActionHandler.showOptionsDialog(keyName,iteration);
+                            }});
+                        });
+                    } else {
+                        PWM_MAIN.addEventHandler('button-' + inputID + '-importCertificates','click',function() {
+                            var dataHandler = function(data) {
+                                var msgBody = '<div style="max-height: 400px; overflow-y: auto">' + data['successMessage'] + '</div>';
+                                PWM_MAIN.showDialog({width:700,title: 'Results', text: msgBody, okAction: function () {
+                                    PWM_CFGEDIT.readSetting(keyName, function(resultValue) {
+                                        PWM_VAR['clientSettingCache'][keyName] = resultValue;
+                                        ActionHandler.showOptionsDialog(keyName, iteration);
+                                    });
+                                }});
+                            };
+                            PWM_CFGEDIT.executeSettingFunction(keyName, 'password.pwm.config.function.ActionCertImportFunction', dataHandler, value['name'])
+                        });
+                    }
+
+                } else if (value['type'] == 'ldap') {
                     PWM_MAIN.addEventHandler('input-' + inputID + '-attributeName','input',function(){
-                        PWM_VAR['clientSettingCache'][keyName][iteration]['attributeName'] = PWM_MAIN.getObject('input-' + inputID + '-attributeName').value;
+                        value['attributeName'] = PWM_MAIN.getObject('input-' + inputID + '-attributeName').value;
                         ActionHandler.write(keyName);
                     });
                     PWM_MAIN.addEventHandler('input-' + inputID + '-attributeValue','input',function(){
-                        PWM_VAR['clientSettingCache'][keyName][iteration]['attributeValue'] = PWM_MAIN.getObject('input-' + inputID + '-attributeValue').value;
+                        value['attributeValue'] = PWM_MAIN.getObject('input-' + inputID + '-attributeValue').value;
                         ActionHandler.write(keyName);
                     });
                     PWM_MAIN.addEventHandler('select-' + inputID + '-ldapMethod','change',function(){
-                        PWM_VAR['clientSettingCache'][keyName][iteration]['ldapMethod'] = PWM_MAIN.getObject('select-' + inputID + '-ldapMethod').value;
+                        value['ldapMethod'] = PWM_MAIN.getObject('select-' + inputID + '-ldapMethod').value;
                         ActionHandler.write(keyName);
                     });
                 }
@@ -2390,7 +2474,7 @@ UserPermissionHandler.draw = function(keyName) {
             var html = PWM_CONFIG.convertListOfIdentitiesToHtml(data['data']);
             PWM_MAIN.showDialog({title:'Matches',text:html});
         };
-        PWM_CFGEDIT.executeSettingFunction(keyName,'password.pwm.config.function.UserMatchViewerFunction',null,dataHandler)
+        PWM_CFGEDIT.executeSettingFunction(keyName, 'password.pwm.config.function.UserMatchViewerFunction', dataHandler, null)
     });
 
     PWM_MAIN.addEventHandler('button-' + keyName + '-addFilterValue','click',function(){
@@ -2701,6 +2785,35 @@ X509CertificateHandler.init = function(keyName) {
     });
 };
 
+X509CertificateHandler.certificateToHtml = function(certificate, keyName, id) {
+    var htmlBody = '';
+    htmlBody += '<div style="max-width:100%; margin-bottom:8px"><table style="max-width:100%" id="table_certificate' + keyName + '-' + id + '">';
+    htmlBody += '<tr><td colspan="2" class="key" style="text-align: center">Certificate ' + id + '  <a id="certTimestamp-detail-' + keyName + '-' + id + '">(detail)</a></td></tr>';
+    htmlBody += '<tr><td>Subject</td><td><div class="setting_table_value">' + certificate['subject'] + '</div></td></tr>';
+    htmlBody += '<tr><td>Issuer</td><td><div class="setting_table_value">' + certificate['issuer'] + '</div></td></tr>';
+    htmlBody += '<tr><td>Serial</td><td><div class="setting_table_value">' + certificate['serial'] + '</div></td></tr>';
+    htmlBody += '<tr><td>Issue Date</td><td id="certTimestamp-issue-' + keyName + '-' + id + '" class="setting_table_value timestamp">' + certificate['issueDate'] + '</td></tr>';
+    htmlBody += '<tr><td>Expire Date</td><td id="certTimestamp-expire-' + keyName + '-' + id + '" class="setting_table_value timestamp">' + certificate['expireDate'] + '</td></tr>';
+    htmlBody += '<tr><td>MD5 Hash</td><td><div class="setting_table_value">' + certificate['md5Hash'] + '</div></td></tr>';
+    htmlBody += '<tr><td>SHA1 Hash</td><td><div class="setting_table_value">' + certificate['sha1Hash'] + '</div></td></tr>';
+    htmlBody += '<tr><td>SHA512 Hash</td><td><div class="setting_table_value">' + certificate['sha512Hash'] + '</div></td></tr>';
+    htmlBody += '</table></div>';
+    return htmlBody;
+};
+
+X509CertificateHandler.certHtmlActions = function(certificate, keyName, id) {
+    PWM_MAIN.TimestampHandler.initElement(PWM_MAIN.getObject('certTimestamp-issue-' + keyName + '-' + id));
+    PWM_MAIN.TimestampHandler.initElement(PWM_MAIN.getObject('certTimestamp-expire-' + keyName + '-' + id));
+    PWM_MAIN.addEventHandler('certTimestamp-detail-' + keyName + '-' + id,'click',function(){
+        PWM_MAIN.showDialog({
+            title: 'Detail - ' + PWM_SETTINGS['settings'][keyName]['label'] + ' - Certificate ' + id,
+            text: '<pre>' + certificate['detail'] + '</pre>',
+            dialogClass: 'wide',
+            showClose: true
+        });
+    });
+};
+
 X509CertificateHandler.draw = function(keyName) {
     var parentDiv = 'table_setting_' + keyName;
     var parentDivElement = PWM_MAIN.getObject(parentDiv);
@@ -2711,17 +2824,7 @@ X509CertificateHandler.draw = function(keyName) {
     for (var certCounter in resultValue) {
         (function (counter) {
             var certificate = resultValue[counter];
-            htmlBody += '<div style="max-width:100%; margin-bottom:8px"><table style="max-width:100%" id="table_certificate' + keyName + '-' + counter + '">';
-            htmlBody += '<tr><td colspan="2" class="key" style="text-align: center">Certificate ' + counter + '  <a id="certTimestamp-detail-' + keyName + '-' + counter + '">(detail)</a></td></tr>';
-            htmlBody += '<tr><td>Subject</td><td><div class="setting_table_value">' + certificate['subject'] + '</div></td></tr>';
-            htmlBody += '<tr><td>Issuer</td><td><div class="setting_table_value">' + certificate['issuer'] + '</div></td></tr>';
-            htmlBody += '<tr><td>Serial</td><td><div class="setting_table_value">' + certificate['serial'] + '</div></td></tr>';
-            htmlBody += '<tr><td>Issue Date</td><td id="certTimestamp-issue-' + keyName + '-' + counter + '" class="setting_table_value timestamp">' + certificate['issueDate'] + '</td></tr>';
-            htmlBody += '<tr><td>Expire Date</td><td id="certTimestamp-expire-' + keyName + '-' + counter + '" class="setting_table_value timestamp">' + certificate['expireDate'] + '</td></tr>';
-            htmlBody += '<tr><td>MD5 Hash</td><td><div class="setting_table_value">' + certificate['md5Hash'] + '</div></td></tr>';
-            htmlBody += '<tr><td>SHA1 Hash</td><td><div class="setting_table_value">' + certificate['sha1Hash'] + '</div></td></tr>';
-            htmlBody += '<tr><td>SHA512 Hash</td><td><div class="setting_table_value">' + certificate['sha512Hash'] + '</div></td></tr>';
-            htmlBody += '</table></div>'
+            htmlBody += X509CertificateHandler.certificateToHtml(certificate, keyName, counter);
         })(certCounter);
     }
     htmlBody += '</div>';
@@ -2734,16 +2837,8 @@ X509CertificateHandler.draw = function(keyName) {
 
     for (certCounter in resultValue) {
         (function (counter) {
-            PWM_MAIN.TimestampHandler.initElement(PWM_MAIN.getObject('certTimestamp-issue-' + keyName + '-' + counter));
-            PWM_MAIN.TimestampHandler.initElement(PWM_MAIN.getObject('certTimestamp-expire-' + keyName + '-' + counter));
-            PWM_MAIN.addEventHandler('certTimestamp-detail-' + keyName + '-' + counter,'click',function(){
-                PWM_MAIN.showDialog({
-                    title: 'Detail - ' + PWM_SETTINGS['settings'][keyName]['label'] + ' - Certificate ' + counter,
-                    text: '<pre>' + resultValue[counter]['detail'] + '</pre>',
-                    dialogClass: 'wide',
-                    showClose: true
-                });
-            });
+            var certificate = resultValue[counter];
+            X509CertificateHandler.certHtmlActions(certificate, keyName, counter)
         })(certCounter);
     }
 
@@ -2955,7 +3050,7 @@ FileValueHandler.draw = function(keyName) {
 
 FileValueHandler.uploadFile = function(keyName) {
     var options = {};
-    options['url'] = "ConfigEditor?processAction=uploadFile&key=" + keyName;
+    options['url'] = "editor?processAction=uploadFile&key=" + keyName;
     options['nextFunction'] = function() {
         PWM_MAIN.showWaitDialog({loadFunction:function(){
             FileValueHandler.init(keyName);

+ 21 - 20
src/main/webapp/public/resources/js/configeditor.js

@@ -37,7 +37,7 @@ PWM_CFGEDIT.readSetting = function(keyName, valueWriter) {
     var maxLevel = parseInt(PWM_CFGEDIT.readNavigationFilters()['level']);
     PWM_VAR['outstandingOperations']++;
     PWM_CFGEDIT.handleWorkingIcon();
-    var url = "ConfigEditor?processAction=readSetting&key=" + keyName;
+    var url = "editor?processAction=readSetting&key=" + keyName;
     if (PWM_CFGEDIT.readCurrentProfile()) {
         url = PWM_MAIN.addParamToUrl(url, 'profile', PWM_CFGEDIT.readCurrentProfile());
     }
@@ -100,7 +100,7 @@ PWM_CFGEDIT.updateLastModifiedInfo = function(keyName, data) {
 PWM_CFGEDIT.writeSetting = function(keyName, valueData, nextAction) {
     PWM_VAR['outstandingOperations']++;
     PWM_CFGEDIT.handleWorkingIcon();
-    var url = "ConfigEditor?processAction=writeSetting&key=" + keyName;
+    var url = "editor?processAction=writeSetting&key=" + keyName;
     if (PWM_CFGEDIT.readCurrentProfile()) {
         url = PWM_MAIN.addParamToUrl(url,'profile',PWM_CFGEDIT.readCurrentProfile());
     }
@@ -129,7 +129,7 @@ PWM_CFGEDIT.writeSetting = function(keyName, valueData, nextAction) {
 };
 
 PWM_CFGEDIT.resetSetting=function(keyName, nextAction) {
-    var url = "ConfigEditor?processAction=resetSetting&key=" + keyName;
+    var url = "editor?processAction=resetSetting&key=" + keyName;
     if (PWM_CFGEDIT.readCurrentProfile()) {
         url = PWM_MAIN.addParamToUrl(url,'profile',PWM_CFGEDIT.readCurrentProfile());
     }
@@ -243,7 +243,7 @@ PWM_CFGEDIT.saveConfiguration = function() {
     PWM_MAIN.preloadAll(function(){
         var confirmText = PWM_CONFIG.showString('MenuDisplay_SaveConfig');
         var confirmFunction = function(){
-            var url = "ConfigEditor?processAction=finishEditing";
+            var url = "editor?processAction=finishEditing";
             var loadFunction = function(data) {
                 if (data['error'] == true) {
                     PWM_MAIN.showErrorDialog(data);
@@ -264,7 +264,7 @@ PWM_CFGEDIT.saveConfiguration = function() {
 
 PWM_CFGEDIT.setConfigurationPassword = function(password) {
     if (password) {
-        var url = "ConfigEditor?processAction=setConfigurationPassword";
+        var url = "editor?processAction=setConfigurationPassword";
         var loadFunction = function(data) {
             if (data['error']) {
                 PWM_MAIN.closeWaitDialog();
@@ -336,10 +336,11 @@ PWM_CFGEDIT.initConfigEditor = function(nextFunction) {
     }
 };
 
-PWM_CFGEDIT.executeSettingFunction = function(setting, name, input, resultHandler) {
+PWM_CFGEDIT.executeSettingFunction = function (setting, name, resultHandler, extraData) {
     var jsonSendData = {};
     jsonSendData['setting'] = setting;
     jsonSendData['function'] = name;
+    jsonSendData['extraData'] = extraData;
 
     resultHandler = resultHandler !== undefined ? resultHandler : function(data) {
         var msgBody = '<div style="max-height: 400px; overflow-y: auto">' + data['successMessage'] + '</div>';
@@ -348,7 +349,7 @@ PWM_CFGEDIT.executeSettingFunction = function(setting, name, input, resultHandle
         }});
     };
 
-    var requestUrl = "ConfigEditor?processAction=executeSettingFunction";
+    var requestUrl = "editor?processAction=executeSettingFunction";
     if (PWM_CFGEDIT.readCurrentProfile()) {
         requestUrl = PWM_MAIN.addParamToUrl(requestUrl,'profile',PWM_CFGEDIT.readCurrentProfile());
     }
@@ -366,7 +367,7 @@ PWM_CFGEDIT.executeSettingFunction = function(setting, name, input, resultHandle
 };
 
 PWM_CFGEDIT.showChangeLog=function(confirmText, confirmFunction) {
-    var url = "ConfigEditor?processAction=readChangeLog";
+    var url = "editor?processAction=readChangeLog";
     var loadFunction = function(data) {
         PWM_MAIN.closeWaitDialog();
         if (data['error']) {
@@ -410,7 +411,7 @@ PWM_CFGEDIT.processSettingSearch = function(destinationDiv) {
     };
 
     console.log('beginning search #' + iteration);
-    var url = "ConfigEditor?processAction=search";
+    var url = "editor?processAction=search";
 
     var loadFunction = function(data) {
         resetDisplay();
@@ -558,7 +559,7 @@ PWM_CFGEDIT.gotoSetting = function(category,settingKey,profile) {
 
 
 PWM_CFGEDIT.cancelEditing = function() {
-    var url =  "ConfigEditor?processAction=readChangeLog";
+    var url =  "editor?processAction=readChangeLog";
     PWM_MAIN.showWaitDialog({loadFunction:function(){
         var loadFunction = function(data) {
             if (data['error']) {
@@ -574,7 +575,7 @@ PWM_CFGEDIT.cancelEditing = function() {
                     PWM_MAIN.showConfirmDialog({dialogClass:'wide',showClose:true,allowMove:true,text:bodyText,okAction:
                         function () {
                             PWM_MAIN.showWaitDialog({loadFunction: function () {
-                                PWM_MAIN.ajaxRequest('ConfigEditor?processAction=cancelEditing',function(){
+                                PWM_MAIN.ajaxRequest('editor?processAction=cancelEditing',function(){
                                     PWM_MAIN.goto('manager', {addFormID: true});
                                 });
                             }});
@@ -611,7 +612,7 @@ PWM_CFGEDIT.showMacroHelp = function() {
                     PWM_MAIN.getObject('panel-testMacroOutput').innerHTML = PWM_MAIN.showString('Display_PleaseWait');
                     var sendData = {};
                     sendData['input'] = PWM_MAIN.getObject('input-testMacroInput').value;
-                    var url = "ConfigEditor?processAction=testMacro";
+                    var url = "editor?processAction=testMacro";
                     var loadFunction = function(data) {
                         PWM_MAIN.getObject('panel-testMacroOutput').innerHTML = data['data'];
                     };
@@ -659,7 +660,7 @@ PWM_CFGEDIT.showDateTimeFormatHelp = function() {
 
 PWM_CFGEDIT.ldapHealthCheck = function() {
     PWM_MAIN.showWaitDialog({loadFunction:function() {
-        var url = "ConfigEditor?processAction=ldapHealthCheck";
+        var url = "editor?processAction=ldapHealthCheck";
         url = PWM_MAIN.addParamToUrl(url,'profile',PWM_CFGEDIT.readCurrentProfile());
         var loadFunction = function(data) {
             PWM_MAIN.closeWaitDialog();
@@ -678,7 +679,7 @@ PWM_CFGEDIT.ldapHealthCheck = function() {
 
 PWM_CFGEDIT.databaseHealthCheck = function() {
     PWM_MAIN.showWaitDialog({title:'Checking database connection...',loadFunction:function(){
-        var url =  "ConfigEditor?processAction=databaseHealthCheck";
+        var url =  "editor?processAction=databaseHealthCheck";
         var loadFunction = function(data) {
             PWM_MAIN.closeWaitDialog();
             if (data['error']) {
@@ -695,7 +696,7 @@ PWM_CFGEDIT.databaseHealthCheck = function() {
 
 PWM_CFGEDIT.httpsCertificateView = function() {
     PWM_MAIN.showWaitDialog({title:'Parsing...',loadFunction:function(){
-        var url =  "ConfigEditor?processAction=httpsCertificateView";
+        var url =  "editor?processAction=httpsCertificateView";
         var loadFunction = function(data) {
             PWM_MAIN.closeWaitDialog();
             if (data['error']) {
@@ -719,7 +720,7 @@ PWM_CFGEDIT.smsHealthCheck = function() {
         PWM_MAIN.showDialog({text:dialogBody,showCancel:true,title:'Test SMS connection',closeOnOk:false,okAction:function(){
             var formElement = PWM_MAIN.getObject("smsCheckParametersForm");
             var formData = domForm.toObject(formElement);
-            var url =  "ConfigEditor?processAction=smsHealthCheck";
+            var url =  "editor?processAction=smsHealthCheck";
             PWM_MAIN.showWaitDialog({loadFunction:function(){
                 var loadFunction = function(data) {
                     if (data['error']) {
@@ -742,8 +743,8 @@ PWM_CFGEDIT.selectTemplate = function(newTemplate) {
         text: PWM_CONFIG.showString('Warning_ChangeTemplate'),
         okAction: function () {
             PWM_MAIN.showWaitDialog({loadFunction: function () {
-                var url = "ConfigEditor?processAction=setOption&template=" + newTemplate;
-                PWM_MAIN.ajaxRequest(url, function(){ PWM_MAIN.goto('ConfigEditor'); });
+                var url = "editor?processAction=setOption&template=" + newTemplate;
+                PWM_MAIN.ajaxRequest(url, function(){ PWM_MAIN.goto('editor'); });
             }});
         }
     });
@@ -1059,7 +1060,7 @@ PWM_CFGEDIT.drawNavigationMenu = function() {
         );
     };
 
-    var url = 'ConfigEditor?processAction=menuTreeData';
+    var url = 'editor?processAction=menuTreeData';
     var filterParams = PWM_CFGEDIT.readNavigationFilters();
 
     PWM_MAIN.ajaxRequest(url,function(data){
@@ -1156,7 +1157,7 @@ PWM_CFGEDIT.drawDisplayTextPage = function(settingKey, keys) {
 
 
 PWM_CFGEDIT.initConfigSettingsDefinition=function(nextFunction) {
-    var clientConfigUrl = PWM_GLOBAL['url-context'] + "/private/config/ConfigEditor?processAction=settingData";
+    var clientConfigUrl = PWM_GLOBAL['url-context'] + "/private/config/editor?processAction=settingData";
     var loadFunction = function(data) {
         if (data['error'] == true) {
             console.error('unable to load ' + clientConfigUrl + ', error: ' + data['errorDetail'])

+ 22 - 12
src/main/webapp/public/resources/js/peoplesearch.js

@@ -130,15 +130,22 @@ PWM_PS.convertDetailResultToHtml = function(data) {
                     })(refIter);
                 }
                 htmlBody += '</div>';
-            } else if (type == 'email') {
-                var value = attributeData['value'];
-                htmlBody += '<a href="mailto:' + value + '">' + value + '</a>';
             } else {
-                htmlBody += attributeData['value'];
-            }
-            if (attributeData['searchable'] == true) {
-                var likeSearchID = 'link-' + attributeData['name'] + '-' + '-likeUserSearch';
-                htmlBody += '<span id="' + likeSearchID + '" class="icon-likeUserSearch btn-icon pwm-icon pwm-icon-search" title="' + PWM_MAIN.showString('Button_Search') + '"></span>';
+                var values = attributeData['values'];
+                for (var i in values) {
+                    if (i > 0) {
+                        htmlBody += '</br>'
+                    }
+                    if (type == 'email') {
+                        htmlBody += '<a href="mailto:' + values[i] + '">' + values[i] + '</a>';
+                    } else {
+                        htmlBody += values[i];
+                    }
+                    if (attributeData['searchable'] == true) {
+                        var likeSearchID = 'link-' + attributeData['name'] + '-' + values[i] + '-likeUserSearch';
+                        htmlBody += '<span id="' + likeSearchID + '" class="icon-likeUserSearch btn-icon pwm-icon pwm-icon-search" title="' + PWM_MAIN.showString('Button_Search') + ' &quot;' + values[i] + '&quot;"></span>';
+                    }
+                }
             }
             htmlBody += '</div></td>'
         })(iter);
@@ -165,10 +172,13 @@ PWM_PS.applyEventHandlersToDetailView = function(data) {
         (function(iterCount){
             var attributeData = data['detail'][iterCount];
             if (attributeData['searchable'] == true) {
-                var likeSearchID = 'link-' + attributeData['name'] + '-' + '-likeUserSearch';
-                PWM_MAIN.addEventHandler(likeSearchID,'click',function(){
-                    PWM_PS.submitLikeUserSearch(attributeData['value']);
-                });
+                var values = attributeData['values'];
+                for (var i in values) {
+                    var likeSearchID = 'link-' + attributeData['name'] + '-' + values[i] + '-likeUserSearch';
+                    PWM_MAIN.addEventHandler(likeSearchID,'click',function(){
+                        PWM_PS.submitLikeUserSearch(values[i]);
+                    });
+                }
             }
             var type = attributeData['type'];
             if (type == 'userDN') {

+ 1 - 1
src/main/webapp/public/resources/themes/pwm/configStyle.css

@@ -219,7 +219,7 @@ table {
 }
 
 .setting_table_value {
-    max-width: 475px;
+    max-width: 435px;
     overflow: auto;
 }
 

+ 0 - 1
supplemental/docker/server.xml.source

@@ -20,4 +20,3 @@
     </Engine>
   </Service>
 </Server>
-w