Browse Source

add setting 'External Web Services Secrets' and required syntax support for named passwords

Jason Rivard 8 years ago
parent
commit
7867839896

+ 21 - 1
src/main/java/password/pwm/config/Configuration.java

@@ -51,6 +51,7 @@ import password.pwm.config.value.FileValue;
 import password.pwm.config.value.FormValue;
 import password.pwm.config.value.LocalizedStringArrayValue;
 import password.pwm.config.value.LocalizedStringValue;
+import password.pwm.config.value.NamedSecretValue;
 import password.pwm.config.value.NumericValue;
 import password.pwm.config.value.PasswordValue;
 import password.pwm.config.value.StringArrayValue;
@@ -190,6 +191,11 @@ public class Configuration implements Serializable, SettingReader {
         return JavaTypeConverter.valueToPassword(readStoredValue(setting));
     }
 
+    public Map<String,NamedSecretData> readSettingAsNamedPasswords(final PwmSetting setting)
+    {
+        return JavaTypeConverter.valueToNamedPassword(readStoredValue(setting));
+    }
+
     public abstract static class JavaTypeConverter {
         public static long valueToLong(final StoredValue value) {
             if (!(value instanceof NumericValue)) {
@@ -225,7 +231,21 @@ public class Configuration implements Serializable, SettingReader {
             }
             return (PasswordData)nativeObject;
         }
-        
+
+        public static Map<String,NamedSecretData> valueToNamedPassword(final StoredValue value) {
+            if (value == null) {
+                return null;
+            }
+            if ((!(value instanceof NamedSecretValue))) {
+                throw new IllegalArgumentException("setting value is not readable as named password");
+            }
+            final Object nativeObject = value.toNativeObject();
+            if (nativeObject == null) {
+                return null;
+            }
+            return (Map<String,NamedSecretData>)nativeObject;
+        }
+
         public static List<ActionConfiguration> valueToAction(final PwmSetting setting, final StoredValue storedValue) {
             if (PwmSettingSyntax.ACTION != setting.getSyntax()) {
                 throw new IllegalArgumentException("may not read ACTION value for setting: " + setting.toString());

+ 37 - 0
src/main/java/password/pwm/config/NamedSecretData.java

@@ -0,0 +1,37 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2017 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;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import password.pwm.util.PasswordData;
+
+import java.io.Serializable;
+import java.util.List;
+
+@Getter
+@AllArgsConstructor
+public class NamedSecretData implements Serializable {
+    private PasswordData password;
+    private List<String> usage;
+}

+ 1 - 1
src/main/java/password/pwm/config/PwmSetting.java

@@ -1098,7 +1098,7 @@ public enum PwmSetting {
     WEBSERVICES_THIRDPARTY_QUERY_MATCH(
             "webservices.thirdParty.queryMatch", PwmSettingSyntax.USER_PERMISSION, PwmSettingCategory.REST_SERVER),
     WEBSERVICES_EXTERNAL_SECRET(
-            "webservices.external.secret", PwmSettingSyntax.PASSWORD, PwmSettingCategory.REST_SERVER),
+            "webservices.external.secrets", PwmSettingSyntax.NAMED_SECRET, PwmSettingCategory.REST_SERVER),
 
 
     EXTERNAL_MACROS_DEST_TOKEN_URLS(

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

@@ -30,6 +30,7 @@ import password.pwm.config.value.FileValue;
 import password.pwm.config.value.FormValue;
 import password.pwm.config.value.LocalizedStringArrayValue;
 import password.pwm.config.value.LocalizedStringValue;
+import password.pwm.config.value.NamedSecretValue;
 import password.pwm.config.value.NumericValue;
 import password.pwm.config.value.OptionListValue;
 import password.pwm.config.value.PasswordValue;
@@ -68,6 +69,7 @@ public enum PwmSettingSyntax {
     PROFILE(StringArrayValue.factory()),
     VERIFICATION_METHOD(VerificationMethodValue.factory()),
     PRIVATE_KEY(PrivateKeyValue.factory()),
+    NAMED_SECRET(NamedSecretValue.factory()),
 
     ;
 

+ 27 - 0
src/main/java/password/pwm/config/option/WebServiceUsage.java

@@ -0,0 +1,27 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2017 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.option;
+
+public enum WebServiceUsage {
+    SigningForm
+}

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

@@ -41,6 +41,7 @@ import password.pwm.config.PwmSettingTemplate;
 import password.pwm.config.PwmSettingTemplateSet;
 import password.pwm.config.StoredValue;
 import password.pwm.config.option.ADPolicyComplexity;
+import password.pwm.config.value.NamedSecretValue;
 import password.pwm.config.value.PasswordValue;
 import password.pwm.config.value.PrivateKeyValue;
 import password.pwm.config.value.StringArrayValue;
@@ -810,6 +811,9 @@ public class StoredConfigurationImpl implements Serializable, StoredConfiguratio
             } else if (setting.getSyntax() == PwmSettingSyntax.PRIVATE_KEY) {
                 final List<Element> valueElements = ((PrivateKeyValue)value).toXmlValues("value", getKey());
                 settingElement.addContent(valueElements);
+            } else if (setting.getSyntax() == PwmSettingSyntax.NAMED_SECRET) {
+                final List<Element> valueElements = ((NamedSecretValue)value).toXmlValues("value", getKey());
+                settingElement.addContent(valueElements);
             } else {
                 settingElement.addContent(value.toXmlValues("value"));
             }

+ 197 - 0
src/main/java/password/pwm/config/value/NamedSecretValue.java

@@ -0,0 +1,197 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2017 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.value;
+
+import com.google.gson.reflect.TypeToken;
+import org.jdom2.Element;
+import password.pwm.PwmConstants;
+import password.pwm.config.NamedSecretData;
+import password.pwm.config.PwmSetting;
+import password.pwm.config.StoredValue;
+import password.pwm.error.ErrorInformation;
+import password.pwm.error.PwmError;
+import password.pwm.error.PwmOperationalException;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.util.PasswordData;
+import password.pwm.util.java.JsonUtil;
+import password.pwm.util.secure.PwmBlockAlgorithm;
+import password.pwm.util.secure.PwmSecurityKey;
+import password.pwm.util.secure.SecureEngine;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+public class NamedSecretValue implements StoredValue {
+    private static final String ELEMENT_NAME = "name";
+    private static final String ELEMENT_PASSWORD = "password";
+    private static final String ELEMENT_USAGE = "usage";
+
+    private Map<String,NamedSecretData> values;
+
+    NamedSecretValue() {
+    }
+
+
+    public NamedSecretValue(final Map<String,NamedSecretData> values) {
+        this.values = values;
+    }
+
+    public static StoredValue.StoredValueFactory factory()
+    {
+        return new StoredValue.StoredValueFactory() {
+            public NamedSecretValue fromJson(final String value)
+            {
+                try {
+                    final Map<String,NamedSecretData> values = JsonUtil.deserialize(value,new TypeToken<Map<String,NamedSecretData>>() {
+                    }.getType());
+                    final Map<String,NamedSecretData> linkedValues = new LinkedHashMap<>(values);
+                    return new NamedSecretValue(linkedValues);
+                } catch (Exception e) {
+                    throw new IllegalStateException(
+                            "NamedPasswordValue can not be json de-serialized: " + e.getMessage());
+                }
+            }
+
+            public NamedSecretValue fromXmlElement(
+                    final Element settingElement,
+                    final PwmSecurityKey key
+            )
+                    throws PwmOperationalException, PwmUnrecoverableException
+            {
+                final Map<String,NamedSecretData> values = new LinkedHashMap<>();
+                final List<Element> valueElements = settingElement.getChildren("value");
+
+                try {
+                    if (valueElements != null) {
+                        for (final Element value : valueElements) {
+                            if (value.getChild(ELEMENT_NAME) != null && value.getChild(ELEMENT_PASSWORD) != null) {
+                                final String name = value.getChild(ELEMENT_NAME).getText();
+                                final String encodedValue = value.getChild(ELEMENT_PASSWORD).getText();
+                                final PasswordData passwordData = new PasswordData(SecureEngine.decryptStringValue(encodedValue, key, PwmBlockAlgorithm.CONFIG));
+                                final List<Element> usages = value.getChildren(ELEMENT_USAGE);
+                                final List<String> strUsages = new ArrayList<>();
+                                if (usages != null) {
+                                    for (final Element usageElement : usages) {
+                                        strUsages.add(usageElement.getText());
+                                    }
+                                }
+                                values.put(name, new NamedSecretData(passwordData, Collections.unmodifiableList(strUsages)));
+                            }
+                        }
+                    }
+                } catch (Exception e) {
+                    final String errorMsg = "unable to decode encrypted password value for setting: " + e.getMessage();
+                    final ErrorInformation errorInfo = new ErrorInformation(PwmError.CONFIG_FORMAT_ERROR, errorMsg);
+                    throw new PwmOperationalException(errorInfo);
+                }
+                return new NamedSecretValue(values);
+            }
+        };
+    }
+
+    public List<Element> toXmlValues(final String valueElementName) {
+        throw new IllegalStateException("password xml output requires hash key");
+    }
+
+    @Override
+    public Object toNativeObject()
+    {
+        return values;
+    }
+
+    @Override
+    public List<String> validateValue(final PwmSetting pwm)
+    {
+        return Collections.emptyList();
+    }
+
+    @Override
+    public int currentSyntaxVersion()
+    {
+        return 0;
+    }
+
+    public List<Element> toXmlValues(final String valueElementName, final PwmSecurityKey key) {
+        if (values == null) {
+            final Element valueElement = new Element(valueElementName);
+            return Collections.singletonList(valueElement);
+        }
+        final List<Element> valuesElement = new ArrayList<>();
+        try {
+            for (final String name : values.keySet()) {
+                final PasswordData passwordData = values.get(name).getPassword();
+                final String encodedValue = SecureEngine.encryptToString(passwordData.getStringValue(), key, PwmBlockAlgorithm.CONFIG);
+                final Element newValueElement = new Element("value");
+                final Element nameElement = new Element(ELEMENT_NAME);
+                nameElement.setText(name);
+                final Element encodedValueElement = new Element(ELEMENT_PASSWORD);
+                encodedValueElement.setText(encodedValue);
+
+                newValueElement.addContent(nameElement);
+                newValueElement.addContent(encodedValueElement);
+
+                for (final String usages : values.get(name).getUsage()) {
+                    final Element usageElement = new Element(ELEMENT_USAGE);
+                    usageElement.setText(usages);
+                    newValueElement.addContent(usageElement);
+                }
+
+
+                valuesElement.add(newValueElement);
+            }
+        } catch (Exception e) {
+            throw new RuntimeException("missing required AES and SHA1 libraries, or other crypto fault: " + e.getMessage());
+        }
+        return Collections.unmodifiableList(valuesElement);
+    }
+
+    public String toString() {
+        return PwmConstants.LOG_REMOVED_VALUE_REPLACEMENT;
+    }
+
+    @Override
+    public String toDebugString(final Locale locale) {
+        return PwmConstants.LOG_REMOVED_VALUE_REPLACEMENT;
+    }
+
+    @Override
+    public Serializable toDebugJsonObject(final Locale locale) {
+        return PwmConstants.LOG_REMOVED_VALUE_REPLACEMENT;
+    }
+
+    public boolean requiresStoredUpdate()
+    {
+        return false;
+    }
+
+    @Override
+    public String valueHash() throws PwmUnrecoverableException {
+        return values == null ? "" : SecureEngine.hash(JsonUtil.serializeMap(values), PwmConstants.SETTING_CHECKSUM_HASH_METHOD);
+    }
+
+}

+ 10 - 2
src/main/java/password/pwm/util/BasicAuthInfo.java

@@ -31,6 +31,7 @@ import password.pwm.http.PwmRequest;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.logging.PwmLogger;
 
+import javax.servlet.http.HttpServletRequest;
 import java.io.Serializable;
 
 /**
@@ -55,7 +56,14 @@ public class BasicAuthInfo implements Serializable {
             final PwmApplication pwmApplication,
             final PwmRequest pwmRequest
     ) {
-        final String authHeader = pwmRequest.readHeaderValueAsString(HttpHeader.Authorization);
+        return parseAuthHeader(pwmApplication, pwmRequest.getHttpServletRequest());
+    }
+
+    public static BasicAuthInfo parseAuthHeader(
+            final PwmApplication pwmApplication,
+            final HttpServletRequest httpServletRequest
+    ) {
+        final String authHeader = httpServletRequest.getHeader(HttpHeader.Authorization.getHttpName());
 
         if (authHeader != null) {
             if (authHeader.contains(PwmConstants.HTTP_BASIC_AUTH_PREFIX)) {
@@ -73,7 +81,7 @@ public class BasicAuthInfo implements Serializable {
                     //   "cn=user,o=company:chpass" or "user:chpass"
                     return parseHeaderString(decoded);
                 } catch (Exception e) {
-                    LOGGER.debug(pwmRequest, "error decoding auth header");
+                    LOGGER.debug("error decoding auth header");
                 }
             }
         }

+ 8 - 40
src/main/java/password/pwm/ws/server/RestRequestBean.java

@@ -22,56 +22,24 @@
 
 package password.pwm.ws.server;
 
+import lombok.Getter;
+import lombok.Setter;
 import password.pwm.PwmApplication;
 import password.pwm.bean.UserIdentity;
+import password.pwm.config.option.WebServiceUsage;
 import password.pwm.http.PwmSession;
 
 import java.io.Serializable;
+import java.util.HashSet;
+import java.util.Set;
 
+@Getter
+@Setter
 public class RestRequestBean implements Serializable {
     private boolean authenticated;
     private boolean external;
     private UserIdentity userIdentity;
     private PwmSession pwmSession;
     private PwmApplication pwmApplication;
-
-    public boolean isAuthenticated() {
-        return authenticated;
-    }
-
-    public void setAuthenticated(final boolean authenticated) {
-        this.authenticated = authenticated;
-    }
-
-    public boolean isExternal() {
-        return external;
-    }
-
-    public void setExternal(final boolean external) {
-        this.external = external;
-    }
-
-    public UserIdentity getUserIdentity() {
-        return userIdentity;
-    }
-
-    public void setUserIdentity(final UserIdentity userIdentity) {
-        this.userIdentity = userIdentity;
-    }
-
-    public PwmSession getPwmSession() {
-        return pwmSession;
-    }
-
-    public void setPwmSession(final PwmSession pwmSession) {
-        this.pwmSession = pwmSession;
-    }
-
-    public PwmApplication getPwmApplication() {
-        return pwmApplication;
-    }
-
-    public void setPwmApplication(final PwmApplication pwmApplication) {
-        this.pwmApplication = pwmApplication;
-    }
+    private final Set<WebServiceUsage> webServiceUsages = new HashSet<>();
 }

+ 0 - 16
src/main/java/password/pwm/ws/server/RestServerHelper.java

@@ -23,7 +23,6 @@
 package password.pwm.ws.server;
 
 import com.novell.ldapchai.exception.ChaiUnavailableException;
-import password.pwm.AppProperty;
 import password.pwm.Permission;
 import password.pwm.PwmApplication;
 import password.pwm.PwmApplicationMode;
@@ -41,7 +40,6 @@ import password.pwm.http.filter.AuthenticationFilter;
 import password.pwm.ldap.search.UserSearchEngine;
 import password.pwm.svc.intruder.RecordType;
 import password.pwm.util.LocaleHelper;
-import password.pwm.util.PasswordData;
 import password.pwm.util.logging.PwmLogger;
 
 import javax.servlet.http.HttpServletRequest;
@@ -126,20 +124,6 @@ public abstract class RestServerHelper {
                     throw new PwmUnrecoverableException(errorInformation);
                 }
             }
-
-            final PasswordData secretKey = pwmApplication.getConfig().readSettingAsPassword(PwmSetting.WEBSERVICES_EXTERNAL_SECRET);
-            if (secretKey != null) {
-                final String headerName = pwmApplication.getConfig().readAppProperty(AppProperty.SECURITY_WS_REST_SERVER_SECRET_HEADER);
-                final String headerValue = pwmRequest.readHeaderValueAsString(headerName);
-                if (headerValue == null || headerValue.isEmpty()) {
-                    final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_UNAUTHORIZED, "request is missing security header " + headerName);
-                    throw new PwmUnrecoverableException(errorInformation);
-                }
-                if (!headerValue.equals(secretKey.getStringValue())) {
-                    final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_UNAUTHORIZED, "authenticated user does not have correct security header");
-                    throw new PwmUnrecoverableException(errorInformation);
-                }
-            }
         }
 
         final boolean adminPermission= restRequestBean.getPwmSession().getSessionManager().checkPermission(pwmApplication, Permission.PWMADMIN);

+ 89 - 0
src/main/java/password/pwm/ws/server/StandaloneRestHelper.java

@@ -0,0 +1,89 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2017 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.ws.server;
+
+import com.novell.ldapchai.util.StringHelper;
+import password.pwm.PwmApplication;
+import password.pwm.config.NamedSecretData;
+import password.pwm.config.PwmSetting;
+import password.pwm.config.option.WebServiceUsage;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.http.ContextManager;
+import password.pwm.util.BasicAuthInfo;
+import password.pwm.util.java.JavaHelper;
+import password.pwm.util.logging.PwmLogger;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class StandaloneRestHelper {
+    private static final PwmLogger LOGGER = PwmLogger.forClass(StandaloneRestHelper.class);
+
+    public static StandaloneRestRequestBean initialize(final HttpServletRequest httpServletRequest)
+            throws PwmUnrecoverableException
+    {
+        final PwmApplication pwmApplication = ContextManager.getPwmApplication(httpServletRequest.getServletContext());
+
+        final Set<WebServiceUsage> usages = readWebServiceSecretAuthorizations(pwmApplication, httpServletRequest);
+
+        return StandaloneRestRequestBean.builder()
+                .pwmApplication(pwmApplication)
+                .authorizedUsages(Collections.unmodifiableSet(usages))
+                .build();
+    }
+
+    private static Set<WebServiceUsage> readWebServiceSecretAuthorizations(
+            final PwmApplication pwmApplication,
+            final HttpServletRequest httpServletRequest
+    )
+    {
+        final Set<WebServiceUsage> usages = new HashSet<>();
+
+        final BasicAuthInfo basicAuthInfo = BasicAuthInfo.parseAuthHeader(pwmApplication, httpServletRequest);
+        if (basicAuthInfo != null) {
+            final Map<String, NamedSecretData> secrets = pwmApplication.getConfig().readSettingAsNamedPasswords(PwmSetting.WEBSERVICES_EXTERNAL_SECRET);
+            final NamedSecretData namedSecretData = secrets.get(basicAuthInfo.getUsername());
+            if (namedSecretData != null) {
+                if (namedSecretData.getPassword().equals(basicAuthInfo.getPassword())) {
+                    final List<WebServiceUsage> namedSecrets = JavaHelper.readEnumListFromStringCollection(WebServiceUsage.class, namedSecretData.getUsage());
+                    usages.addAll(namedSecrets);
+                    LOGGER.trace("REST request to " + httpServletRequest.getRequestURI() + " specified a basic auth username (\""
+                            + basicAuthInfo.getUsername() + "\"), granting usage access to "
+                            + StringHelper.stringCollectionToString(namedSecretData.getUsage(), ","));
+                } else {
+                    LOGGER.trace("REST request to " + httpServletRequest.getRequestURI() + " specified a basic auth username (\""
+                            + basicAuthInfo.getUsername() + "\") with an incorrect password");
+                }
+            } else {
+                LOGGER.trace("REST request to " + httpServletRequest.getRequestURI() + " specified a basic auth username (\""
+                        + basicAuthInfo.getUsername() + "\") that does not correspond to a configured web secret");
+            }
+        }
+
+        return Collections.unmodifiableSet(usages);
+    }
+}

+ 38 - 0
src/main/java/password/pwm/ws/server/StandaloneRestRequestBean.java

@@ -0,0 +1,38 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2017 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.ws.server;
+
+import lombok.Builder;
+import lombok.Getter;
+import password.pwm.PwmApplication;
+import password.pwm.config.option.WebServiceUsage;
+
+import java.util.HashSet;
+import java.util.Set;
+
+@Getter
+@Builder
+public class StandaloneRestRequestBean {
+    private Set<WebServiceUsage> authorizedUsages = new HashSet<>();
+    private PwmApplication pwmApplication;
+}

+ 11 - 11
src/main/java/password/pwm/ws/server/rest/RestSigningServer.java

@@ -26,15 +26,15 @@ import lombok.AllArgsConstructor;
 import lombok.Getter;
 import password.pwm.AppProperty;
 import password.pwm.PwmApplication;
+import password.pwm.config.option.WebServiceUsage;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.secure.SecureService;
-import password.pwm.ws.server.RestRequestBean;
 import password.pwm.ws.server.RestResultBean;
-import password.pwm.ws.server.RestServerHelper;
-import password.pwm.ws.server.ServicePermissions;
+import password.pwm.ws.server.StandaloneRestHelper;
+import password.pwm.ws.server.StandaloneRestRequestBean;
 
 import javax.ws.rs.Consumes;
 import javax.ws.rs.POST;
@@ -49,12 +49,6 @@ import java.util.concurrent.TimeUnit;
 
 @Path("/signing")
 public class RestSigningServer extends AbstractRestServer {
-    private static final ServicePermissions SERVICE_PERMISSIONS = ServicePermissions.builder()
-            .adminOnly(false)
-            .authRequired(false)
-            .blockExternal(false)
-            .build();
-
 
     @POST
     @Produces(MediaType.APPLICATION_JSON + ";charset=UTF-8")
@@ -66,13 +60,19 @@ public class RestSigningServer extends AbstractRestServer {
     )
             throws PwmUnrecoverableException
     {
-        final RestRequestBean restRequestBean;
+        final StandaloneRestRequestBean restRequestBean;
         try {
-            restRequestBean = RestServerHelper.initializeRestRequest(request, response, SERVICE_PERMISSIONS, null);
+            restRequestBean = StandaloneRestHelper.initialize(request);
         } catch (PwmUnrecoverableException e) {
             return RestResultBean.fromError(e.getErrorInformation()).asJsonResponse();
         }
 
+        if (!restRequestBean.getAuthorizedUsages().contains(WebServiceUsage.SigningForm)) {
+            final String errorMsg = "request is not authenticated with permission for SigningForm";
+            final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_UNAUTHORIZED, errorMsg);
+            return RestResultBean.fromError(errorInformation).asJsonResponse();
+        }
+
         try {
             if (inputFormData != null) {
                 final SecureService securityService = restRequestBean.getPwmApplication().getSecureService();

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

@@ -253,7 +253,7 @@ security.input.trim=true
 security.input.password.trim=false
 security.input.themeMatchRegex=^[0-9a-zA-Z-_]*$
 security.ws.rest.clientKeyLength=32
-security.ws.rest.server.secretKeyHeader=RestSecretKey
+security.ws.rest.server.secretKeyHeader=AuthorizationSecret
 security.sharedHistory.hashIterations=100000
 security.sharedHistory.hashName=SHA-512
 security.sharedHistory.caseInsensitive=true

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

@@ -3563,10 +3563,11 @@
             <value>false</value>
         </default>
     </setting>
-    <setting hidden="false" key="webservices.external.secret" level="2">
-        <default>
-            <value/>
-        </default>
+    <setting hidden="false" key="webservices.external.secrets" level="2">
+        <default/>
+        <options>
+            <option value="SigningForm">Signing Form Service - /signing/form</option>
+        </options>
     </setting>
     <setting hidden="false" key="webservices.queryMatch" level="2">
         <ldapPermission actor="proxy" access="read"/>

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

@@ -673,7 +673,7 @@ Setting_Description_urlshortener.classname=Specify the URL Shortening Service cl
 Setting_Description_urlshortener.parameters=Specify the Name/Value settings used to configure the selected URL shortening service. For example, an API key, user name, password or domain name. The settings must be in "name\=value" format, where name is the key value of a valid service setting.
 Setting_Description_useXForwardedForHeader=If present, use the X-Forwarded-For HTTP header value as the client IP address instead of the source IP address of the HTTP connection.  Typically, upstream proxies add X-Forwarded-For headers or firewalls and might be the only reliable way to identify the user's source IP address.
 Setting_Description_webservices.enableReadAnswers=Enable this option to allow @PwmAppName@ to use web services to read stored Challenge/Response answers of users.  The read responses are available in whatever hashing method format you selected.
-Setting_Description_webservices.external.secret=If specified, @PwmAppName@ requires external web service clients to provide this value when invoking web services.
+Setting_Description_webservices.external.secrets=<p>Add named secrets for web services clients.  Protected web services require authentication with a named secret.  Create a secret with a name and secret key (password), and then enable the services that that named secret will be able to invoke.</p><p>Authentication to the web services must be invoked using Basic Authentication with the name of the secret as the username and the secret as the password.</p>
 Setting_Description_webservices.healthStats.makePublic=Enable this option to enable the Health and Statistics web services publicly.  Normally, these require authentication as there might be security sensitive data available.  Enabling this option allows the users to use the web services without authentication.  You must enable this is setting for the public (non-authenticated) page at <i>/public/health.jsp</i> to be functional.
 Setting_Description_webservices.queryMatch=Add an LDAP filter that contains the users permitted to execute REST web services.
 Setting_Description_webservices.thirdParty.queryMatch=Add an LDAP filter that contains the users permitted to execute REST web services and specify a third party via the 'username' parameter.
@@ -1145,7 +1145,7 @@ Setting_Label_urlshortener.classname=Enable URL Shortening Service Class
 Setting_Label_urlshortener.parameters=Configuration Parameters for URL Shortening Service
 Setting_Label_useXForwardedForHeader=Use X-Forwarded-For Header
 Setting_Label_webservices.enableReadAnswers=Allow Web Services to Read Answers
-Setting_Label_webservices.external.secret=External Web Services Secret Key
+Setting_Label_webservices.external.secrets=External Web Services Secrets
 Setting_Label_webservices.healthStats.makePublic=Enable Public Health and Statistics Web Services
 Setting_Label_webservices.queryMatch=External Web Services Permissions
 Setting_Label_webservices.thirdParty.queryMatch=Web Services Third Party Permissions

+ 144 - 1
src/main/webapp/public/resources/js/configeditor-settings.js

@@ -2753,7 +2753,7 @@ SelectValueHandler.init = function(settingKey) {
 
     if (allowUserInput) {
         htmlBody += '<button class="btn" id="button_selectOverride_' + settingKey + '">'
-        + '<span class="btn-icon pwm-icon pwm-icon-plus-square"></span>Add Value</button>';
+            + '<span class="btn-icon pwm-icon pwm-icon-plus-square"></span>Add Value</button>';
 
     }
 
@@ -3191,3 +3191,146 @@ PrivateKeyHandler.draw = function(keyName) {
         UILibrary.uploadFileDialog(options);
     });
 };
+
+
+//--------- named secret handler ---
+var NamedSecretHandler = {};
+
+NamedSecretHandler.init = function(settingKey) {
+    var parentDiv = 'table_setting_' + settingKey;
+    var parentDivElement = PWM_MAIN.getObject(parentDiv);
+
+    if (parentDivElement) {
+        PWM_CFGEDIT.readSetting(settingKey,function(data){
+            PWM_VAR['clientSettingCache'][settingKey] = data;
+            var htmlBody = '';
+            htmlBody += '<table>';
+            var rowCounter = 0;
+            for (var key in data) {
+                var id = settingKey + '_' + key;
+                htmlBody += '<tr>';
+                htmlBody += '<td>' + key + '</td><td>Stored Value</td><td><button id="button-usage-' + id + '"><span class="btn-icon pwm-icon pwm-icon-sliders"/>Usage</button></td>';
+                htmlBody += '<td style="width:10px"><span class="delete-row-icon action-icon pwm-icon pwm-icon-times" id="button-deleteRow-' + id + '"></span></td>';
+                htmlBody += '</tr>';
+                rowCounter++;
+            }
+
+            if (rowCounter < 1) {
+                htmlBody += '<tr><td>No values.</td></tr>';
+            }
+
+            htmlBody += '</table>';
+
+            htmlBody += '<button id="button-addPassword-' + settingKey + '" class="btn"><span class="btn-icon pwm-icon pwm-icon-plus-square"></span>Add Value</button>';
+            parentDivElement.innerHTML = htmlBody;
+
+            PWM_MAIN.addEventHandler('button-addPassword-' + settingKey,'click',function(){
+                NamedSecretHandler.addPassword(settingKey);
+            });
+
+            for (var key in data) {
+                var id = settingKey + '_' + key;
+                PWM_MAIN.addEventHandler('button-deleteRow-' + id,'click',function(){
+                    NamedSecretHandler.deletePassword(settingKey, key);
+                });
+                PWM_MAIN.addEventHandler('button-usage-' + id,'click',function(){
+                    NamedSecretHandler.usagePopup(settingKey, key);
+                });
+            }
+        });
+    }
+};
+
+NamedSecretHandler.usagePopup = function(settingKey, key) {
+    var titleText = PWM_SETTINGS['settings'][settingKey]['label'] + ' - Usage - ' + key ;
+    var options = PWM_SETTINGS['settings'][settingKey]['options'];
+    var currentValues = PWM_VAR['clientSettingCache'][settingKey];
+    var html = '<table class="noborder">';
+    for (var loopKey in options) {
+        (function (optionKey) {
+            html += '<tr><td>';
+            var buttonID = key + "_usage_button_" + optionKey;
+            html += '<label class="checkboxWrapper" style="min-width:180px;">'
+                + '<input type="checkbox" id="' + buttonID + '"/>'
+                + options[optionKey] + '</label>';
+            html += '</td></tr>';
+        })(loopKey);
+    }
+    html += '</table>';
+    var loadFunction = function () {
+        for (var loopKey in options) {
+            (function (optionKey) {
+                var buttonID = key + "_usage_button_" + optionKey;
+                var checked = PWM_MAIN.JSLibrary.arrayContains(currentValues[key]['usage'],optionKey);
+                PWM_MAIN.getObject(buttonID).checked = checked;
+                PWM_MAIN.addEventHandler(buttonID,'click',function(){
+                    var nowChecked = PWM_MAIN.getObject(buttonID).checked;
+                    if (nowChecked) {
+                        currentValues[key]['usage'].push(optionKey);
+                    } else {
+                        PWM_MAIN.JSLibrary.removeFromArray(currentValues[key]['usage'], optionKey);
+                    }
+                });
+            })(loopKey);
+        }
+    };
+    var okFunction = function() {
+        var postWriteFunction = function() {
+            NamedSecretHandler.init(settingKey);
+        };
+        PWM_CFGEDIT.writeSetting(settingKey, currentValues, postWriteFunction);
+    };
+    PWM_MAIN.showDialog({title:titleText,text:html,loadFunction:loadFunction,okAction:okFunction});
+};
+
+NamedSecretHandler.addPassword = function(settingKey) {
+    var titleText = PWM_SETTINGS['settings'][settingKey]['label'] + ' - Name';
+    var stringEditorFinishFunc = function(nameValue) {
+        var currentValues = PWM_VAR['clientSettingCache'][settingKey];
+        if (nameValue in currentValues) {;
+            var errorTxt = '"' + nameValue + '" already exists.';
+            PWM_MAIN.showErrorDialog(errorTxt);
+            return;
+        }
+        var pwDialogOptions = {};
+        pwDialogOptions['title'] = PWM_SETTINGS['settings'][settingKey]['label'] + ' - Password';
+        pwDialogOptions['showRandomGenerator'] = true;
+        pwDialogOptions['showValues'] = true;
+        pwDialogOptions['writeFunction'] = function(pwValue) {
+            currentValues[nameValue] = {};
+            currentValues[nameValue]['password'] = pwValue;
+            currentValues[nameValue]['usage'] = [];
+
+            var postWriteFunction = function() {
+                NamedSecretHandler.init(settingKey);
+            };
+
+            PWM_CFGEDIT.writeSetting(settingKey, currentValues, postWriteFunction);
+        };
+        UILibrary.passwordDialogPopup(pwDialogOptions)
+    };
+    var instructions = 'Please enter the name for the new password.';
+    UILibrary.stringEditorDialog({
+        title: titleText,
+        regex: '[a-zA-Z]{2,20}',
+        instructions: instructions,
+        completeFunction: stringEditorFinishFunc
+    });
+};
+
+
+NamedSecretHandler.deletePassword = function(settingKey, key) {
+    PWM_MAIN.showConfirmDialog({
+        text:'Delete named password <b>' + key + '</b>?',
+        okAction:function() {
+            var currentValues = PWM_VAR['clientSettingCache'][settingKey];
+            delete currentValues[key];
+
+            var postWriteFunction = function() {
+                NamedSecretHandler.init(settingKey);
+            };
+            PWM_CFGEDIT.writeSetting(settingKey, currentValues, postWriteFunction);
+        }
+    });
+
+};

+ 4 - 0
src/main/webapp/public/resources/js/configeditor.js

@@ -948,6 +948,10 @@ PWM_CFGEDIT.initSettingDisplay = function(setting, options) {
             ChangePasswordHandler.init(settingKey);
             break;
 
+        case 'NAMED_SECRET':
+            NamedSecretHandler.init(settingKey);
+            break;
+
         case 'NUMERIC':
             NumericValueHandler.init(settingKey);
             break;

+ 8 - 0
src/main/webapp/public/resources/js/main.js

@@ -1342,6 +1342,14 @@ PWM_MAIN.JSLibrary.arrayContains = function(array,element) {
     return array.indexOf(element) > -1;
 };
 
+PWM_MAIN.JSLibrary.removeFromArray = function(array,element) {
+    for(var i = array.length - 1; i >= 0; i--) {
+        if(array[i] === element) {
+            array.splice(i, 1);
+        }
+    }
+};
+
 PWM_MAIN.toggleFullscreen = function(iconObj,divName) {
     var obj = PWM_MAIN.getObject(divName);