From 38a0c2948b355a055ad76e7a6f536681cf2ad075 Mon Sep 17 00:00:00 2001
From: brinn <brinn>
Date: Wed, 23 Jan 2013 09:22:28 +0000
Subject: [PATCH] Add IAuthenticationService.isConfigured() and use this to
 exclude non-configured services from StackedAuthenticationService. Change
 timeout configuration for LDAP from milli-seconds to seconds. Add timeout
 setting for Crowd and set it to 10s by default (used to be 5min hard-coded).

SVN: 28162
---
 .../DummyAuthenticationService.java           |   6 +
 .../IAuthenticationService.java               |   5 +
 .../NullAuthenticationService.java            |   6 +
 .../crowd/CrowdAuthenticationService.java     |  39 +++-
 .../crowd/CrowdConfiguration.java             | 208 ++++++++++++++++++
 .../file/CachingAuthenticationService.java    |   6 +
 .../file/FileAuthenticationService.java       |  10 +
 .../ldap/LDAPAuthenticationService.java       |   9 +
 .../ldap/LDAPDirectoryConfiguration.java      |  54 +++--
 .../stacked/StackedAuthenticationService.java |  25 ++-
 .../StackedAuthenticationServiceTest.java     |  81 +++++--
 common/source/java/genericCommonContext.xml   |  14 +-
 openbis/dist/server/service.properties        |  34 ++-
 13 files changed, 438 insertions(+), 59 deletions(-)
 create mode 100644 authentication/source/java/ch/systemsx/cisd/authentication/crowd/CrowdConfiguration.java

diff --git a/authentication/source/java/ch/systemsx/cisd/authentication/DummyAuthenticationService.java b/authentication/source/java/ch/systemsx/cisd/authentication/DummyAuthenticationService.java
index 91cabbd442a..c5e247303cf 100644
--- a/authentication/source/java/ch/systemsx/cisd/authentication/DummyAuthenticationService.java
+++ b/authentication/source/java/ch/systemsx/cisd/authentication/DummyAuthenticationService.java
@@ -186,4 +186,10 @@ public final class DummyAuthenticationService implements IAuthenticationService
         // Always available.
     }
 
+    @Override
+    public boolean isConfigured()
+    {
+        return true;
+    }
+
 }
\ No newline at end of file
diff --git a/authentication/source/java/ch/systemsx/cisd/authentication/IAuthenticationService.java b/authentication/source/java/ch/systemsx/cisd/authentication/IAuthenticationService.java
index f5d376ab488..2c19cf1ca0e 100644
--- a/authentication/source/java/ch/systemsx/cisd/authentication/IAuthenticationService.java
+++ b/authentication/source/java/ch/systemsx/cisd/authentication/IAuthenticationService.java
@@ -27,6 +27,11 @@ import ch.systemsx.cisd.common.utilities.ISelfTestable;
  */
 public interface IAuthenticationService extends ISelfTestable
 {
+    /**
+     * Returns <code>true</code> if this authentication service is configured.
+     */
+    public boolean isConfigured();
+    
     /**
      * Attempts authentication for the given user credentials.
      * 
diff --git a/authentication/source/java/ch/systemsx/cisd/authentication/NullAuthenticationService.java b/authentication/source/java/ch/systemsx/cisd/authentication/NullAuthenticationService.java
index 431ad693858..8d86a580994 100644
--- a/authentication/source/java/ch/systemsx/cisd/authentication/NullAuthenticationService.java
+++ b/authentication/source/java/ch/systemsx/cisd/authentication/NullAuthenticationService.java
@@ -157,4 +157,10 @@ public class NullAuthenticationService implements IAuthenticationService
         return null;
     }
 
+    @Override
+    public boolean isConfigured()
+    {
+        return false;
+    }
+
 }
diff --git a/authentication/source/java/ch/systemsx/cisd/authentication/crowd/CrowdAuthenticationService.java b/authentication/source/java/ch/systemsx/cisd/authentication/crowd/CrowdAuthenticationService.java
index fad6abe0552..e5b364e894f 100644
--- a/authentication/source/java/ch/systemsx/cisd/authentication/crowd/CrowdAuthenticationService.java
+++ b/authentication/source/java/ch/systemsx/cisd/authentication/crowd/CrowdAuthenticationService.java
@@ -59,7 +59,8 @@ import ch.systemsx.cisd.common.logging.LogFactory;
  */
 public class CrowdAuthenticationService implements IAuthenticationService
 {
-    private static final int CONNECTION_TIMEOUT = (int) (5 * DateUtils.MILLIS_PER_MINUTE);
+    private static final int DEFAULT_CONNECTION_TIMEOUT_MILLIS =
+            (int) (5 * DateUtils.MILLIS_PER_MINUTE);
 
     private static final String DUMMY_TOKEN_STR = "DUMMY-TOKEN";
 
@@ -133,7 +134,7 @@ public class CrowdAuthenticationService implements IAuthenticationService
                             + "   </soap:Body>\n"
                             + "</soap:Envelope>\n");
 
-    private static IRequestExecutor createExecutor()
+    private static IRequestExecutor createExecutor(final int timeoutMillis)
     {
         return new IRequestExecutor()
             {
@@ -149,7 +150,7 @@ public class CrowdAuthenticationService implements IAuthenticationService
                     {
                         final HttpClient client = new HttpClient();
                         final PostMethod post = new PostMethod(serviceUrl);
-                        post.getParams().setSoTimeout(CONNECTION_TIMEOUT);
+                        post.getParams().setSoTimeout(timeoutMillis);
                         final StringRequestEntity entity =
                                 new StringRequestEntity(message, "application/soap+xml", "utf-8");
                         post.setRequestEntity(entity);
@@ -178,15 +179,36 @@ public class CrowdAuthenticationService implements IAuthenticationService
 
     private final String applicationPassword;
 
-    private final IRequestExecutor requestExecutor;
+    private final boolean configured;
 
+    private final IRequestExecutor requestExecutor;
+    
     private final AtomicReference<String> applicationTokenHolder = new AtomicReference<String>();
 
+    public CrowdAuthenticationService(CrowdConfiguration configuration)
+    {
+        this.url = configuration.getServerURL();
+        this.application = configuration.getApplication();
+        this.applicationPassword = configuration.getApplicationPassword();
+        this.configured = configuration.isConfigured();
+        this.requestExecutor = createExecutor(configuration.getTimeout());
+        if (operationLog.isDebugEnabled())
+        {
+            final String msg =
+                    "A new CrowdAuthenticationService instance has been created for [" + "url="
+                            + url + ", application=" + application + "], timeout: "
+                            + ((configuration.getTimeout() == 0) ? "-." : (configuration.getTimeout()
+                            / 1000 + " s."));
+            operationLog.debug(msg);
+        }
+    }
+
+    // Keep this constructor for backward compatibility with old Spring application context files.
     public CrowdAuthenticationService(final String host, final String port,
             final String application, final String applicationPassword)
     {
         this("https://" + host + ":" + checkPort(port) + "/crowd/services/SecurityServer",
-                application, applicationPassword, createExecutor());
+                application, applicationPassword, createExecutor(DEFAULT_CONNECTION_TIMEOUT_MILLIS));
     }
 
     CrowdAuthenticationService(final String url, final String application,
@@ -196,6 +218,7 @@ public class CrowdAuthenticationService implements IAuthenticationService
         this.application = application;
         this.applicationPassword = applicationPassword;
         this.requestExecutor = requestExecutor;
+        this.configured = true;
         if (operationLog.isDebugEnabled())
         {
             final String msg =
@@ -643,4 +666,10 @@ public class CrowdAuthenticationService implements IAuthenticationService
         return false;
     }
 
+    @Override
+    public boolean isConfigured()
+    {
+        return configured;
+    }
+
 }
diff --git a/authentication/source/java/ch/systemsx/cisd/authentication/crowd/CrowdConfiguration.java b/authentication/source/java/ch/systemsx/cisd/authentication/crowd/CrowdConfiguration.java
new file mode 100644
index 00000000000..6f0c159ffa7
--- /dev/null
+++ b/authentication/source/java/ch/systemsx/cisd/authentication/crowd/CrowdConfiguration.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright 2013 ETH Zuerich, CISD
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ch.systemsx.cisd.authentication.crowd;
+
+import org.apache.commons.lang.StringUtils;
+
+/**
+ * A configuration object for Crowd.
+ * 
+ * @author Bernd Rinn
+ */
+public class CrowdConfiguration
+{
+    private String host;
+
+    private int port = 443;
+
+    private String application;
+
+    private String applicationPassword;
+
+    private int timeout = 10000;
+
+    /**
+     * Returns the host of the Crowd service.
+     */
+    public String getHost()
+    {
+        return host;
+    }
+
+    /**
+     * Sets the host of the Crowd service.
+     */
+    public void setHost(String host)
+    {
+        if (isResolved(host))
+        {
+            this.host = host;
+        }
+    }
+
+    /**
+     * Returns the port that the Crowd service is running on.
+     */
+    public int getPort()
+    {
+        return port;
+    }
+
+    /**
+     * Sets the port that the Crowd service is running on.
+     */
+    public void setPort(int port)
+    {
+        if (port > 0)
+        {
+            this.port = port;
+        }
+    }
+
+    /**
+     * Returns the port that the Crowd service is running on (as String).
+     */
+    public String getPortStr()
+    {
+        return Integer.toString(port);
+    }
+    
+    /**
+     * Sets the port (as String) that the Crowd service is running on. Only set if a positive integer.
+     */
+    public void setPortStr(String portStr)
+    {
+        if (isResolved(portStr))
+        {
+            try
+            {
+                setPort(Integer.parseInt(portStr));
+            } catch (NumberFormatException ex)
+            {
+                // Not set.
+            }
+        }
+    }
+
+    /**
+     * Returns the server URL of the Crowd service.
+     */
+    public String getServerURL()
+    {
+        if (isConfigured())
+        {
+            return "https://" + host + ":" + port + "/crowd/services/SecurityServer";
+        } else
+        {
+            return null;
+        }
+    }
+    
+    /**
+     * Returns the application name that this application sends to the Crowd service.
+     */
+    public String getApplication()
+    {
+        return application;
+    }
+
+    /**
+     * Sets the application name that this application sends to the Crowd service.
+     */
+    public void setApplication(String application)
+    {
+        if (isResolved(application))
+        {
+            this.application = application;
+        }
+    }
+
+    /**
+     * Returns the application password that this application sends to the Crowd service.
+     */
+    public String getApplicationPassword()
+    {
+        return applicationPassword;
+    }
+
+    /**
+     * Sets the application password that this application sends to the Crowd service.
+     */
+    public void setApplicationPassword(String applicationPassword)
+    {
+        if (isResolved(applicationPassword))
+        {
+            this.applicationPassword = applicationPassword;
+        }
+    }
+
+    /**
+     * Returns the timeout, i.e.  how long to wait for a result from Crowd (in ms).
+     */
+    public int getTimeout()
+    {
+        return timeout;
+    }
+
+    /**
+     * Sets the timeout, i.e. how long to wait for a result from Crowd (in ms).
+     */
+    public void setTimeout(int timeoutMillis)
+    {
+        this.timeout = (timeoutMillis < 0) ? 0 : timeoutMillis;
+    }
+
+    /**
+     * Sets the timeout, i.e. how long to wait for a result from Crowd (as String, in s).
+     */
+    public void setTimeoutStr(String timeoutStr)
+    {
+        if (isResolved(timeoutStr))
+        {
+            try
+            {
+                setTimeout(Integer.parseInt(timeoutStr) * 1000);
+            } catch (NumberFormatException ex)
+            {
+                // Not set.
+            }
+        }
+    }
+    
+    /**
+     * Returns the timeout, i.e. how long to wait for a result from Crowd (as String, in s).
+     */
+    public String getTimeoutStr()
+    {
+        return Integer.toString(getTimeout() / 1000);
+    }
+
+    /**
+     * Returns <code>true</code> if the configuration is complete.
+     */
+    public boolean isConfigured()
+    {
+        return StringUtils.isNotBlank(host) && StringUtils.isNotBlank(application)
+                && StringUtils.isNotBlank(applicationPassword);
+    }
+
+    private static boolean isResolved(String name)
+    {
+        return StringUtils.isNotBlank(name) && name.startsWith("${") == false;
+    }
+
+}
diff --git a/authentication/source/java/ch/systemsx/cisd/authentication/file/CachingAuthenticationService.java b/authentication/source/java/ch/systemsx/cisd/authentication/file/CachingAuthenticationService.java
index 1b59ea56ade..d9ca05b94a0 100644
--- a/authentication/source/java/ch/systemsx/cisd/authentication/file/CachingAuthenticationService.java
+++ b/authentication/source/java/ch/systemsx/cisd/authentication/file/CachingAuthenticationService.java
@@ -576,4 +576,10 @@ public class CachingAuthenticationService implements IAuthenticationService
         delegate.check();
     }
 
+    @Override
+    public boolean isConfigured()
+    {
+        return delegate.isConfigured();
+    }
+
 }
diff --git a/authentication/source/java/ch/systemsx/cisd/authentication/file/FileAuthenticationService.java b/authentication/source/java/ch/systemsx/cisd/authentication/file/FileAuthenticationService.java
index 94dff20a1ce..d8fefeee771 100644
--- a/authentication/source/java/ch/systemsx/cisd/authentication/file/FileAuthenticationService.java
+++ b/authentication/source/java/ch/systemsx/cisd/authentication/file/FileAuthenticationService.java
@@ -57,6 +57,10 @@ public class FileAuthenticationService implements IAuthenticationService
 
     private static IUserStore<? extends UserEntry> createUserStore(final String passwordFileName)
     {
+        if (passwordFileName == null)
+        {
+            return null;
+        }
         final ILineStore lineStore =
                 new FileBasedLineStore(new File(passwordFileName), "Password file");
         return createUserStore(lineStore);
@@ -306,4 +310,10 @@ public class FileAuthenticationService implements IAuthenticationService
         return (listingServiceOrNull != null) && listingServiceOrNull.isRemote();
     }
 
+    @Override
+    public boolean isConfigured()
+    {
+        return userStore != null;
+    }
+
 }
diff --git a/authentication/source/java/ch/systemsx/cisd/authentication/ldap/LDAPAuthenticationService.java b/authentication/source/java/ch/systemsx/cisd/authentication/ldap/LDAPAuthenticationService.java
index 7c0c6371565..ea31ac5d5b4 100644
--- a/authentication/source/java/ch/systemsx/cisd/authentication/ldap/LDAPAuthenticationService.java
+++ b/authentication/source/java/ch/systemsx/cisd/authentication/ldap/LDAPAuthenticationService.java
@@ -40,10 +40,13 @@ public class LDAPAuthenticationService implements IAuthenticationService
             LogFactory.getLogger(LogCategory.OPERATION, LDAPAuthenticationService.class);
 
     private final LDAPPrincipalQuery query;
+    
+    private final boolean configured;
 
     public LDAPAuthenticationService(LDAPDirectoryConfiguration config)
     {
         query = new LDAPPrincipalQuery(config);
+        this.configured = config.isConfigured();
     }
 
     @Override
@@ -198,4 +201,10 @@ public class LDAPAuthenticationService implements IAuthenticationService
         return query.isRemote();
     }
 
+    @Override
+    public boolean isConfigured()
+    {
+        return configured;
+    }
+
 }
diff --git a/authentication/source/java/ch/systemsx/cisd/authentication/ldap/LDAPDirectoryConfiguration.java b/authentication/source/java/ch/systemsx/cisd/authentication/ldap/LDAPDirectoryConfiguration.java
index 12170485865..51bffb56957 100644
--- a/authentication/source/java/ch/systemsx/cisd/authentication/ldap/LDAPDirectoryConfiguration.java
+++ b/authentication/source/java/ch/systemsx/cisd/authentication/ldap/LDAPDirectoryConfiguration.java
@@ -37,8 +37,9 @@ import org.apache.commons.lang.StringUtils;
 public final class LDAPDirectoryConfiguration
 {
 
-    static final String DEFAULT_QUERY_TEMPLATE = "(&(objectClass=organizationalPerson)(objectCategory=person)"
-                        + "(objectClass=user)(%s))";
+    static final String DEFAULT_QUERY_TEMPLATE =
+            "(&(objectClass=organizationalPerson)(objectCategory=person)"
+                    + "(objectClass=user)(%s))";
 
     private String userIdAttributeName = "uid";
 
@@ -47,11 +48,11 @@ public final class LDAPDirectoryConfiguration
     private String firstNameAttributeName = "givenName";
 
     private String emailAttributeName = "mail";
-    
+
     private String emailAliasesAttributeName = "proxyAddresses";
-    
+
     private String emailAttributePrefix = "smtp:";
-    
+
     private String queryEmailForAliases = "false";
 
     private String securityProtocol = "ssl";
@@ -59,11 +60,11 @@ public final class LDAPDirectoryConfiguration
     private String securityAuthenticationMethod = "simple";
 
     private String referral = "follow";
-    
+
     private long timeout = 10000L;
-    
+
     private int maxRetries = 1;
-    
+
     private long timeToWaitAfterFailure = 10000L;
 
     private String queryTemplate =
@@ -75,6 +76,16 @@ public final class LDAPDirectoryConfiguration
 
     private String securityPrincipalPassword;
 
+    /**
+     * Returns <code>true</code> if this configuration is complete.
+     */
+    public boolean isConfigured()
+    {
+        return StringUtils.isNotBlank(serverUrl)
+                && StringUtils.isNotBlank(securityPrincipalDistinguishedName)
+                && StringUtils.isNotBlank(securityPrincipalPassword);
+    }
+
     /**
      * Default value: <code>uid</code>
      */
@@ -161,8 +172,8 @@ public final class LDAPDirectoryConfiguration
     }
 
     /**
-     * If the query for emails should use the email aliases instead of the canonical email addresses.
-     * 
+     * If the query for emails should use the email aliases instead of the canonical email
+     * addresses.
      * Default: <code>false</code>.
      */
     public void setQueryEmailForAliases(String queryEmailForAliases)
@@ -316,29 +327,27 @@ public final class LDAPDirectoryConfiguration
 
     /**
      * The read timeout (in ms).
-     * 
      * Default value: <code>-1</code> (which means: wait forever)
      */
     public String getTimeoutStr()
     {
-        return Long.toString(timeout);
+        return Long.toString(timeout / 1000);
     }
 
     /**
-     * Set the read timeout (in ms).
+     * Set the read timeout (in s).
      */
     public void setTimeoutStr(String timeoutMillis)
     {
         if (isResolved(timeoutMillis))
         {
-            this.timeout = Long.parseLong(timeoutMillis);
+            this.timeout = Long.parseLong(timeoutMillis) * 1000L;
         }
     }
 
     /**
      * The time to wait after failure before retrying (in ms).
-     * 
-     * Default value: <code>-1</code> (which means: wait forever)
+     * Default value: <code>10000</code> (10s)
      */
     public long getTimeToWaitAfterFailure()
     {
@@ -346,13 +355,12 @@ public final class LDAPDirectoryConfiguration
     }
 
     /**
-     * The time to wait after failure before retrying (in ms).
-     * 
-     * Default value: <code>-1</code> (which means: wait forever)
+     * The time to wait after failure before retrying (in s).
+     * Default value: <code>10</code>
      */
     public String getTimeToWaitAfterFailureStr()
     {
-        return Long.toString(timeToWaitAfterFailure);
+        return Long.toString(timeToWaitAfterFailure / 1000);
     }
 
     /**
@@ -362,13 +370,12 @@ public final class LDAPDirectoryConfiguration
     {
         if (isResolved(timeToWaitOnFailureMillis))
         {
-            this.timeToWaitAfterFailure = Long.parseLong(timeToWaitOnFailureMillis);
+            this.timeToWaitAfterFailure = Long.parseLong(timeToWaitOnFailureMillis) * 1000L;
         }
     }
 
     /**
      * The maximum number of times a failed query is retried.
-     * 
      * Default value: <code>9</code>
      */
     public int getMaxRetries()
@@ -378,7 +385,6 @@ public final class LDAPDirectoryConfiguration
 
     /**
      * The maximum number of times a failed query is retried.
-     * 
      * Default value: <code>9</code>
      */
     public String getMaxRetriesStr()
@@ -416,5 +422,5 @@ public final class LDAPDirectoryConfiguration
     {
         return StringUtils.isNotBlank(name) && name.startsWith("${") == false;
     }
-    
+
 }
diff --git a/authentication/source/java/ch/systemsx/cisd/authentication/stacked/StackedAuthenticationService.java b/authentication/source/java/ch/systemsx/cisd/authentication/stacked/StackedAuthenticationService.java
index 4a7ec39f6a6..f0453fa60cd 100644
--- a/authentication/source/java/ch/systemsx/cisd/authentication/stacked/StackedAuthenticationService.java
+++ b/authentication/source/java/ch/systemsx/cisd/authentication/stacked/StackedAuthenticationService.java
@@ -41,12 +41,12 @@ public class StackedAuthenticationService implements IAuthenticationService
     private final boolean supportsListingByEmail;
 
     private final boolean supportsListingByLastName;
-    
+
     private final boolean supportsAuthenticatingByEmail;
 
     public StackedAuthenticationService(List<IAuthenticationService> authenticationServices)
     {
-        this.delegates = authenticationServices;
+        this.delegates = filterConfigured(authenticationServices);
         boolean foundRemote = false;
         boolean foundSupportsListingByUserId = false;
         boolean foundSupportsListingByEmail = false;
@@ -67,6 +67,21 @@ public class StackedAuthenticationService implements IAuthenticationService
         this.supportsAuthenticatingByEmail = foundSupportsAuthenticateByEmail;
     }
 
+    private static List<IAuthenticationService> filterConfigured(
+            List<IAuthenticationService> services)
+    {
+        final List<IAuthenticationService> configuredServices =
+                new ArrayList<IAuthenticationService>(services.size());
+        for (IAuthenticationService service : services)
+        {
+            if (service.isConfigured())
+            {
+                configuredServices.add(service);
+            }
+        }
+        return configuredServices;
+    }
+
     @Override
     public String authenticateApplication()
     {
@@ -260,4 +275,10 @@ public class StackedAuthenticationService implements IAuthenticationService
         return remote;
     }
 
+    @Override
+    public boolean isConfigured()
+    {
+        return (delegates.isEmpty() == false);
+    }
+
 }
diff --git a/authentication/sourceTest/java/ch/systemsx/cisd/authentication/stacked/StackedAuthenticationServiceTest.java b/authentication/sourceTest/java/ch/systemsx/cisd/authentication/stacked/StackedAuthenticationServiceTest.java
index 8b633549fbc..c5e9c97f8a8 100644
--- a/authentication/sourceTest/java/ch/systemsx/cisd/authentication/stacked/StackedAuthenticationServiceTest.java
+++ b/authentication/sourceTest/java/ch/systemsx/cisd/authentication/stacked/StackedAuthenticationServiceTest.java
@@ -43,6 +43,8 @@ public class StackedAuthenticationServiceTest
 
     private IAuthenticationService authService2;
 
+    private IAuthenticationService authService3;
+
     private IAuthenticationService stackedAuthService;
 
     @BeforeMethod
@@ -51,11 +53,29 @@ public class StackedAuthenticationServiceTest
         context = new Mockery();
         authService1 = context.mock(IAuthenticationService.class, "auth service 1");
         authService2 = context.mock(IAuthenticationService.class, "auth service 2");
+        authService3 = context.mock(IAuthenticationService.class, "auth service 3");
         addStandardExpectations();
+        addAlways();
         stackedAuthService =
-                new StackedAuthenticationService(Arrays.asList(authService1, authService2));
+                new StackedAuthenticationService(Arrays.asList(authService1, authService2,
+                        authService3));
     }
 
+    private void addAlways()
+    {
+        context.checking(new Expectations()
+        {
+            {
+                allowing(authService1).isConfigured();
+                will(returnValue(true));
+                allowing(authService2).isConfigured();
+                will(returnValue(true));
+                allowing(authService3).isConfigured();
+                will(returnValue(false));
+            }
+        });
+    }
+    
     private void addStandardExpectations()
     {
         context.checking(new Expectations()
@@ -66,7 +86,7 @@ public class StackedAuthenticationServiceTest
                     one(authService1).supportsListingByEmail();
                     one(authService1).supportsListingByLastName();
                     one(authService1).supportsAuthenticatingByEmail();
-                    
+
                     one(authService2).isRemote();
                     one(authService2).supportsListingByUserId();
                     one(authService2).supportsListingByEmail();
@@ -92,6 +112,7 @@ public class StackedAuthenticationServiceTest
                     one(authService2).check();
                 }
             });
+        assertTrue(stackedAuthService.isConfigured());
         stackedAuthService.check();
         context.assertIsSatisfied();
     }
@@ -123,6 +144,7 @@ public class StackedAuthenticationServiceTest
         context = new Mockery();
         authService1 = context.mock(IAuthenticationService.class, "auth service 1");
         authService2 = context.mock(IAuthenticationService.class, "auth service 2");
+        addAlways();
         context.checking(new Expectations()
             {
                 {
@@ -131,7 +153,7 @@ public class StackedAuthenticationServiceTest
                     one(authService1).supportsListingByEmail();
                     one(authService1).supportsListingByLastName();
                     one(authService1).supportsAuthenticatingByEmail();
-                    
+
                     one(authService2).isRemote();
                     one(authService2).supportsListingByUserId();
                     will(returnValue(true));
@@ -152,6 +174,7 @@ public class StackedAuthenticationServiceTest
         context = new Mockery();
         authService1 = context.mock(IAuthenticationService.class, "auth service 1");
         authService2 = context.mock(IAuthenticationService.class, "auth service 2");
+        addAlways();
         context.checking(new Expectations()
             {
                 {
@@ -161,7 +184,7 @@ public class StackedAuthenticationServiceTest
                     will(returnValue(true));
                     one(authService1).supportsListingByLastName();
                     one(authService1).supportsAuthenticatingByEmail();
-                    
+
                     one(authService2).isRemote();
                     one(authService2).supportsListingByUserId();
                     one(authService2).supportsListingByEmail();
@@ -181,6 +204,7 @@ public class StackedAuthenticationServiceTest
         context = new Mockery();
         authService1 = context.mock(IAuthenticationService.class, "auth service 1");
         authService2 = context.mock(IAuthenticationService.class, "auth service 2");
+        addAlways();
         context.checking(new Expectations()
             {
                 {
@@ -189,7 +213,7 @@ public class StackedAuthenticationServiceTest
                     one(authService1).supportsListingByEmail();
                     one(authService1).supportsListingByLastName();
                     one(authService1).supportsAuthenticatingByEmail();
-                    
+
                     one(authService2).isRemote();
                     one(authService2).supportsListingByUserId();
                     one(authService2).supportsListingByEmail();
@@ -217,6 +241,7 @@ public class StackedAuthenticationServiceTest
                     one(authService2).tryGetAndAuthenticateUser(user, password);
                 }
             });
+        addAlways();
         assertFalse(stackedAuthService.authenticateUser(user, password));
         context.assertIsSatisfied();
     }
@@ -235,6 +260,7 @@ public class StackedAuthenticationServiceTest
                     will(returnValue(principal));
                 }
             });
+        addAlways();
         assertTrue(stackedAuthService.authenticateUser(user, password));
         context.assertIsSatisfied();
     }
@@ -254,6 +280,7 @@ public class StackedAuthenticationServiceTest
                     will(returnValue(principal));
                 }
             });
+        addAlways();
         assertTrue(stackedAuthService.authenticateUser(user, password));
         context.assertIsSatisfied();
     }
@@ -274,6 +301,7 @@ public class StackedAuthenticationServiceTest
                     will(returnValue(principal));
                 }
             });
+        addAlways();
         assertEquals(principal, stackedAuthService.getPrincipal(user));
         context.assertIsSatisfied();
     }
@@ -295,6 +323,7 @@ public class StackedAuthenticationServiceTest
                     will(returnValue(principal));
                 }
             });
+        addAlways();
         assertEquals(principal, stackedAuthService.getPrincipal(user));
         context.assertIsSatisfied();
     }
@@ -311,6 +340,7 @@ public class StackedAuthenticationServiceTest
                     one(authService2).tryGetAndAuthenticateUser(user, null);
                 }
             });
+        addAlways();
         stackedAuthService.getPrincipal(user);
     }
 
@@ -330,6 +360,8 @@ public class StackedAuthenticationServiceTest
             {
                 {
                     one(authService1).isRemote();
+                    one(authService1).isConfigured();
+                    will(returnValue(true));
                     one(authService1).supportsListingByUserId();
                     exactly(2).of(authService1).supportsListingByEmail();
                     will(returnValue(true));
@@ -337,6 +369,8 @@ public class StackedAuthenticationServiceTest
                     one(authService1).supportsListingByLastName();
 
                     one(authService2).isRemote();
+                    one(authService2).isConfigured();
+                    will(returnValue(true));
                     one(authService2).supportsListingByUserId();
                     exactly(2).of(authService2).supportsListingByEmail();
                     one(authService2).supportsAuthenticatingByEmail();
@@ -346,6 +380,7 @@ public class StackedAuthenticationServiceTest
                     will(returnValue(Arrays.asList(principal1, principal2)));
                 }
             });
+        addAlways();
         stackedAuthService =
                 new StackedAuthenticationService(Arrays.asList(authService1, authService2));
         final List<Principal> result = stackedAuthService.listPrincipalsByEmail(emailQuery);
@@ -369,12 +404,16 @@ public class StackedAuthenticationServiceTest
             {
                 {
                     one(authService1).isRemote();
+                    one(authService1).isConfigured();
+                    will(returnValue(true));
                     one(authService1).supportsListingByUserId();
                     exactly(2).of(authService1).supportsListingByEmail();
                     one(authService1).supportsAuthenticatingByEmail();
                     one(authService1).supportsListingByLastName();
-                    
+
                     one(authService2).isRemote();
+                    one(authService2).isConfigured();
+                    will(returnValue(true));
                     one(authService2).supportsListingByUserId();
                     exactly(2).of(authService2).supportsListingByEmail();
                     will(returnValue(true));
@@ -385,6 +424,7 @@ public class StackedAuthenticationServiceTest
                     will(returnValue(Arrays.asList(principal)));
                 }
             });
+        addAlways();
         stackedAuthService =
                 new StackedAuthenticationService(Arrays.asList(authService1, authService2));
         final List<Principal> result = stackedAuthService.listPrincipalsByEmail(emailQuery);
@@ -411,13 +451,17 @@ public class StackedAuthenticationServiceTest
             {
                 {
                     one(authService1).isRemote();
+                    one(authService1).isConfigured();
+                    will(returnValue(true));
                     one(authService1).supportsListingByUserId();
                     exactly(2).of(authService1).supportsListingByEmail();
                     will(returnValue(true));
                     one(authService1).supportsAuthenticatingByEmail();
                     one(authService1).supportsListingByLastName();
-                    
+
                     one(authService2).isRemote();
+                    one(authService2).isConfigured();
+                    will(returnValue(true));
                     one(authService2).supportsListingByUserId();
                     exactly(2).of(authService2).supportsListingByEmail();
                     will(returnValue(true));
@@ -430,6 +474,7 @@ public class StackedAuthenticationServiceTest
                     will(returnValue(Arrays.asList(principal3)));
                 }
             });
+        addAlways();
         stackedAuthService =
                 new StackedAuthenticationService(Arrays.asList(authService1, authService2));
         final List<Principal> result = stackedAuthService.listPrincipalsByEmail(emailQuery);
@@ -461,7 +506,7 @@ public class StackedAuthenticationServiceTest
                     one(authService1).supportsListingByEmail();
                     one(authService1).supportsListingByLastName();
                     one(authService1).supportsAuthenticatingByEmail();
-                    
+
                     one(authService2).isRemote();
                     exactly(2).of(authService2).supportsListingByUserId();
                     one(authService2).supportsListingByEmail();
@@ -472,6 +517,7 @@ public class StackedAuthenticationServiceTest
                     will(returnValue(Arrays.asList(principal1, principal2)));
                 }
             });
+        addAlways();
         stackedAuthService =
                 new StackedAuthenticationService(Arrays.asList(authService1, authService2));
         final List<Principal> result = stackedAuthService.listPrincipalsByUserId(userIdQuery);
@@ -499,7 +545,7 @@ public class StackedAuthenticationServiceTest
                     one(authService1).supportsListingByEmail();
                     one(authService1).supportsListingByLastName();
                     one(authService1).supportsAuthenticatingByEmail();
-                    
+
                     one(authService2).isRemote();
                     exactly(2).of(authService2).supportsListingByUserId();
                     will(returnValue(true));
@@ -511,6 +557,7 @@ public class StackedAuthenticationServiceTest
                     will(returnValue(Arrays.asList(principal)));
                 }
             });
+        addAlways();
         stackedAuthService =
                 new StackedAuthenticationService(Arrays.asList(authService1, authService2));
         final List<Principal> result = stackedAuthService.listPrincipalsByUserId(userIdQuery);
@@ -542,7 +589,7 @@ public class StackedAuthenticationServiceTest
                     one(authService1).supportsListingByEmail();
                     one(authService1).supportsListingByLastName();
                     one(authService1).supportsAuthenticatingByEmail();
-                    
+
                     one(authService2).isRemote();
                     exactly(2).of(authService2).supportsListingByUserId();
                     will(returnValue(true));
@@ -556,6 +603,7 @@ public class StackedAuthenticationServiceTest
                     will(returnValue(Arrays.asList(principal3)));
                 }
             });
+        addAlways();
         stackedAuthService =
                 new StackedAuthenticationService(Arrays.asList(authService1, authService2));
         final List<Principal> result = stackedAuthService.listPrincipalsByUserId(userIdQuery);
@@ -587,7 +635,7 @@ public class StackedAuthenticationServiceTest
                     exactly(2).of(authService1).supportsListingByLastName();
                     will(returnValue(true));
                     one(authService1).supportsAuthenticatingByEmail();
-                    
+
                     one(authService2).isRemote();
                     one(authService2).supportsListingByUserId();
                     one(authService2).supportsListingByEmail();
@@ -598,6 +646,7 @@ public class StackedAuthenticationServiceTest
                     will(returnValue(Arrays.asList(principal1, principal2)));
                 }
             });
+        addAlways();
         stackedAuthService =
                 new StackedAuthenticationService(Arrays.asList(authService1, authService2));
         final List<Principal> result = stackedAuthService.listPrincipalsByLastName(lastNameQuery);
@@ -625,7 +674,7 @@ public class StackedAuthenticationServiceTest
                     one(authService1).supportsListingByEmail();
                     exactly(2).of(authService1).supportsListingByLastName();
                     one(authService1).supportsAuthenticatingByEmail();
-                    
+
                     one(authService2).isRemote();
                     one(authService2).supportsListingByUserId();
                     one(authService2).supportsListingByEmail();
@@ -637,6 +686,7 @@ public class StackedAuthenticationServiceTest
                     will(returnValue(Arrays.asList(principal)));
                 }
             });
+        addAlways();
         stackedAuthService =
                 new StackedAuthenticationService(Arrays.asList(authService1, authService2));
         final List<Principal> result = stackedAuthService.listPrincipalsByLastName(lastNameQuery);
@@ -663,13 +713,17 @@ public class StackedAuthenticationServiceTest
             {
                 {
                     one(authService1).isRemote();
+                    one(authService1).isConfigured();
+                    will(returnValue(true));
                     one(authService1).supportsListingByUserId();
                     one(authService1).supportsListingByEmail();
                     one(authService1).supportsAuthenticatingByEmail();
                     exactly(2).of(authService1).supportsListingByLastName();
                     will(returnValue(true));
-                    
+
                     one(authService2).isRemote();
+                    one(authService2).isConfigured();
+                    will(returnValue(true));
                     one(authService2).supportsListingByUserId();
                     one(authService2).supportsListingByEmail();
                     one(authService2).supportsAuthenticatingByEmail();
@@ -682,6 +736,7 @@ public class StackedAuthenticationServiceTest
                     will(returnValue(Arrays.asList(principal3)));
                 }
             });
+        addAlways();
         stackedAuthService =
                 new StackedAuthenticationService(Arrays.asList(authService1, authService2));
         final List<Principal> result = stackedAuthService.listPrincipalsByLastName(lastNameQuery);
diff --git a/common/source/java/genericCommonContext.xml b/common/source/java/genericCommonContext.xml
index 005e6fe2122..ac6e0a8a304 100644
--- a/common/source/java/genericCommonContext.xml
+++ b/common/source/java/genericCommonContext.xml
@@ -44,12 +44,18 @@
     <bean id="no-authentication-service"
         class="ch.systemsx.cisd.authentication.NullAuthenticationService" />
 
+    <bean id="crowd-configuration" 
+        class="ch.systemsx.cisd.authentication.crowd.CrowdConfiguration">
+        <property name="host" value="${crowd.service.host}" />
+        <property name="portStr" value="${crowd.service.port}" />
+        <property name="timeoutStr" value="${crowd.service.timeout}" />
+        <property name="application" value="${crowd.application.name}" />
+        <property name="applicationPassword" value="${crowd.application.password}" />
+    </bean>
+
     <bean id="crowd-authentication-service"
         class="ch.systemsx.cisd.authentication.crowd.CrowdAuthenticationService">
-        <constructor-arg value="${crowd.service.host}" />
-        <constructor-arg value="${crowd.service.port}" />
-        <constructor-arg value="${crowd.application.name}" />
-        <constructor-arg value="${crowd.application.password}" />
+        <constructor-arg ref="crowd-configuration" />
     </bean>
 
     <bean id="ldap-directory-configuration" 
diff --git a/openbis/dist/server/service.properties b/openbis/dist/server/service.properties
index 6d897c0e190..b5258b60310 100644
--- a/openbis/dist/server/service.properties
+++ b/openbis/dist/server/service.properties
@@ -39,21 +39,33 @@ authentication-service = file-authentication-service
 # ---------------------------------------------------------------------------
 # Crowd configuration
 # ---------------------------------------------------------------------------
-crowd.service.host = crowd-bsse.ethz.ch
-crowd.service.port = 8443
+#
+# The Crowd host.
+# Mandatory.
+crowd.service.host = 
+# The Crowd service port. Default: 443
+crowd.service.port =
+# The timeout (in s) to wait for a Crowd query to return, -1 for "wait indefinitely". Default: 10. 
+crowd.service.timeout =
+# The Crowd application name.
+# Mandatory. 
 crowd.application.name = openbis
+# The Crowd application password. 
+# Mandatory.
 crowd.application.password =
 
 # ---------------------------------------------------------------------------
 # LDAP configuration
 # ---------------------------------------------------------------------------
-# The URL of the LDAP server, e.g. "ldap://d.ethz.ch/DC=d,DC=ethz,DC=ch"
-ldap.server.url = <LDAP URL>
-# The distinguished name of the security principal,
-# e.g. "CN=carl,OU=EthUsers,DC=d,DC=ethz,DC=ch"
-ldap.security.principal.distinguished.name = <distinguished name to login to the LDAP server>
-# Password of the LDAP user account that will be used to login to the LDAP server to perform the queries
-ldap.security.principal.password = <password of the user to connect to the LDAP server>
+# The URL of the LDAP server, e.g. "ldap://d.ethz.ch/DC=d,DC=ethz,DC=ch". 
+# Mandatory.
+ldap.server.url = 
+# The distinguished name of the security principal, e.g. "CN=carl,OU=EthUsers,DC=d,DC=ethz,DC=ch".
+# Mandatory.
+ldap.security.principal.distinguished.name = 
+# Password of the LDAP user account that will be used to login to the LDAP server to perform the queries. 
+# Mandatory.
+ldap.security.principal.password = 
 # The security protocol to use, use "ssl" or "none", default is "ssl"
 ldap.security.protocol =
 # The authentication method to use: "none" (no authentication), "simple", "strong" (SASL), defaults to "simple"
@@ -84,9 +96,9 @@ ldap.queryEmailForAliases = true
 ldap.queryTemplate =
 # The number of times a failed LDAP query is retried at the max. Default: 1.
 ldap.maxRetries = 
-# The timeout (in ms) to wait for an LDAP query to return, -1 for "wait indefinitely". Default: 10000. 
+# The timeout (in s) to wait for an LDAP query to return, -1 for "wait indefinitely". Default: 10. 
 ldap.timeout = 
-# Time time (in ms) to wait after a failure before retrying the query. Default: 10000. 
+# Time time (in s) to wait after a failure before retrying the query. Default: 10. 
 ldap.timeToWaitAfterFailure=
 
 # ---------------------------------------------------------------------------
-- 
GitLab