Florian Zschocke
2013-07-09 a0c34e37fe8e456a21c7a57e9d45e637ab40cce8
Add an Apache htpasswd user service

Add a new class, HtpasswdUserService, which performs authentication
against a text file created with the Apache 'htpasswd' program.

Added dependency on commons-codec:1.7
5 files added
10 files modified
1072 ■■■■■ changed files
.classpath 2 ●●● patch | view | raw | blame | history
NOTICE 10 ●●●●● patch | view | raw | blame | history
build.moxie 1 ●●●● patch | view | raw | blame | history
gitblit.iml 22 ●●●● patch | view | raw | blame | history
releases.moxie 7 ●●●● patch | view | raw | blame | history
src/main/distrib/data/gitblit.properties 33 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/Constants.java 2 ●●● patch | view | raw | blame | history
src/main/java/com/gitblit/HtpasswdUserService.java 356 ●●●●● patch | view | raw | blame | history
src/site/design.mkd 1 ●●●● patch | view | raw | blame | history
src/site/setup_authentication.mkd 8 ●●●●● patch | view | raw | blame | history
src/test/java/com/gitblit/tests/GitBlitSuite.java 2 ●●● patch | view | raw | blame | history
src/test/java/com/gitblit/tests/HtpasswdUserServiceTest.java 556 ●●●●● patch | view | raw | blame | history
src/test/resources/htpasswdUSTest/htpasswd-user.in 15 ●●●●● patch | view | raw | blame | history
src/test/resources/htpasswdUSTest/htpasswd.in 31 ●●●●● patch | view | raw | blame | history
src/test/resources/htpasswdUSTest/users.conf.in 26 ●●●●● patch | view | raw | blame | history
.classpath
@@ -46,6 +46,7 @@
    <classpathentry kind="lib" path="ext/jna-3.5.0.jar" sourcepath="ext/src/jna-3.5.0.jar" />
    <classpathentry kind="lib" path="ext/guava-13.0.1.jar" sourcepath="ext/src/guava-13.0.1.jar" />
    <classpathentry kind="lib" path="ext/libpam4j-1.7.jar" sourcepath="ext/src/libpam4j-1.7.jar" />
    <classpathentry kind="lib" path="ext/commons-codec-1.7.jar" sourcepath="ext/src/commons-codec-1.7.jar" />
    <classpathentry kind="lib" path="ext/junit-4.11.jar" sourcepath="ext/src/junit-4.11.jar" />
    <classpathentry kind="lib" path="ext/hamcrest-core-1.3.jar" sourcepath="ext/src/hamcrest-core-1.3.jar" />
    <classpathentry kind="lib" path="ext/selenium-java-2.28.0.jar" sourcepath="ext/src/selenium-java-2.28.0.jar" />
@@ -58,7 +59,6 @@
    <classpathentry kind="lib" path="ext/httpclient-4.2.1.jar" sourcepath="ext/src/httpclient-4.2.1.jar" />
    <classpathentry kind="lib" path="ext/httpcore-4.2.1.jar" sourcepath="ext/src/httpcore-4.2.1.jar" />
    <classpathentry kind="lib" path="ext/commons-logging-1.1.1.jar" sourcepath="ext/src/commons-logging-1.1.1.jar" />
    <classpathentry kind="lib" path="ext/commons-codec-1.6.jar" sourcepath="ext/src/commons-codec-1.6.jar" />
    <classpathentry kind="lib" path="ext/commons-exec-1.1.jar" sourcepath="ext/src/commons-exec-1.1.jar" />
    <classpathentry kind="lib" path="ext/commons-io-2.2.jar" sourcepath="ext/src/commons-io-2.2.jar" />
    <classpathentry kind="output" path="bin/classes" />
NOTICE
@@ -310,4 +310,12 @@
   MIT license.
   
   https://github.com/kohsuke/libpam4j
---------------------------------------------------------------------------
commons-codec
---------------------------------------------------------------------------
   commons-codec, release under the
   Apache License 2.0.
   http://commons.apache.org/proper/commons-codec
build.moxie
@@ -152,6 +152,7 @@
- compile 'org.freemarker:freemarker:2.3.19' :war
- compile 'com.github.dblock.waffle:waffle-jna:1.5' :war
- compile 'org.kohsuke:libpam4j:1.7' :war
- compile 'commons-codec:commons-codec:1.7' :war
- test 'junit'
# Dependencies for Selenium web page testing
- test 'org.seleniumhq.selenium:selenium-java:${selenium.version}' @jar
gitblit.iml
@@ -479,6 +479,17 @@
        </SOURCES>
      </library>
    </orderEntry>
    <orderEntry type="module-library">
      <library name="commons-codec-1.7.jar">
        <CLASSES>
          <root url="jar://$MODULE_DIR$/ext/commons-codec-1.7.jar!/" />
        </CLASSES>
        <JAVADOC />
        <SOURCES>
          <root url="jar://$MODULE_DIR$/ext/src/commons-codec-1.7.jar!/" />
        </SOURCES>
      </library>
    </orderEntry>
    <orderEntry type="module-library" scope="TEST">
      <library name="junit-4.11.jar">
        <CLASSES>
@@ -608,17 +619,6 @@
        <JAVADOC />
        <SOURCES>
          <root url="jar://$MODULE_DIR$/ext/src/commons-logging-1.1.1.jar!/" />
        </SOURCES>
      </library>
    </orderEntry>
    <orderEntry type="module-library" scope="TEST">
      <library name="commons-codec-1.6.jar">
        <CLASSES>
          <root url="jar://$MODULE_DIR$/ext/commons-codec-1.6.jar!/" />
        </CLASSES>
        <JAVADOC />
        <SOURCES>
          <root url="jar://$MODULE_DIR$/ext/src/commons-codec-1.6.jar!/" />
        </SOURCES>
      </library>
    </orderEntry>
releases.moxie
@@ -20,13 +20,18 @@
    changes: ~
    additions:
    - Add setting for maximum number of days of activity to that may be requested
    dependencyChanges: ~
    - Added HtpasswdUserService to authenticate users against an htpasswd file
    dependencyChanges:
    - Added commons-codec 1.7
    contributors:
    - github/guriguri
    - Doug Ayers
    - Ori Livneh
    - Florian Zschocke
    settings:
    - { name: 'web.activityDurationMaximum', defaultValue: 30 }
    - { name: 'realm.htpasswd.userFile', defaultValue: '${baseFolder}/htpasswd' }
    - { name: 'realm.htpasswd.overrideLocalAuthentication', defaultValue: 'false' }
}
#
src/main/distrib/data/gitblit.properties
@@ -502,6 +502,7 @@
#    com.gitblit.SalesforceUserService
#    com.gitblit.WindowsUserService
#    com.gitblit.PAMUserService
#    com.gitblit.HtpasswdUserService
#
# Any custom user service implementation must have a public default constructor.
#
@@ -1233,6 +1234,38 @@
# SINCE 1.3.1
realm.pam.serviceName = system-auth
# The HtpasswdUserService must be backed by another user service for standard user
# and team management and attributes. This can be one of the local Gitblit user services.
# default: users.conf
#
# RESTART REQUIRED
# BASEFOLDER
# SINCE 1.3.2
realm.htpasswd.backingUserService = ${baseFolder}/users.conf
# The Apache htpasswd file that contains the users and passwords.
# default: ${baseFolder}/htpasswd
#
# RESTART REQUIRED
# BASEFOLDER
# SINCE 1.3.2
realm.htpasswd.userfile = ${baseFolder}/htpasswd
#  Determines how accounts are looked up upon login.
#
# If set to false, then authentication for local accounts is done against
# the backing user service.
# If set to true, then authentication will first be checked against the
# htpasswd store, even if the account appears as a local account in the
# backing user service. If the user is found in the htpasswd store, then
# an already existing local account will be turned into an external account.
# In this case an initial local password is never used and gets overwritten
# by the externally stored password upon login.
# default: false
#
# SINCE 1.3.2
realm.htpasswd.overrideLocalAuthentication = false
# The SalesforceUserService must be backed by another user service for standard user
# and team management.
# default: users.conf
src/main/java/com/gitblit/Constants.java
@@ -480,7 +480,7 @@
    }
    
    public static enum AccountType {
        LOCAL, EXTERNAL, LDAP, REDMINE, SALESFORCE, WINDOWS, PAM;
        LOCAL, EXTERNAL, LDAP, REDMINE, SALESFORCE, WINDOWS, PAM, HTPASSWD;
        
        public boolean isLocal() {
            return this == LOCAL;
src/main/java/com/gitblit/HtpasswdUserService.java
New file
@@ -0,0 +1,356 @@
/*
 * Copyright 2013 Florian Zschocke
 * Copyright 2013 gitblit.com
 *
 * 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 com.gitblit;
import java.io.File;
import java.io.FileInputStream;
import java.text.MessageFormat;
import java.util.Map;
import java.util.Scanner;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.digest.Crypt;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.codec.digest.Md5Crypt;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.gitblit.Constants.AccountType;
import com.gitblit.models.UserModel;
import com.gitblit.utils.ArrayUtils;
import com.gitblit.utils.StringUtils;
/**
 * Implementation of a user service using an Apache htpasswd file for authentication.
 *
 * This user service implement custom authentication using entries in a file created
 * by the 'htpasswd' program of an Apache web server. All possible output
 * options of the 'htpasswd' program version 2.2 are supported:
 * plain text (only on Windows and Netware),
 * glibc crypt() (not on Windows and NetWare),
 * Apache MD5 (apr1),
 * unsalted SHA-1.
 *
 * Configuration options:
 * realm.htpasswd.backingUserService - Specify the backing user service that is used
 *                                     to keep the user data other than the password.
 *                                     The default is '${baseFolder}/users.conf'.
 * realm.htpasswd.userfile - The text file with the htpasswd entries to be used for
 *                           authentication.
 *                           The default is '${baseFolder}/htpasswd'.
 * realm.htpasswd.overrideLocalAuthentication - Specify if local accounts are overwritten
 *                                              when authentication matches for an
 *                                              external account.
 *
 * @author Florian Zschocke
 *
 */
public class HtpasswdUserService extends GitblitUserService
{
    private static final String KEY_BACKING_US = Keys.realm.htpasswd.backingUserService;
    private static final String DEFAULT_BACKING_US = "${baseFolder}/users.conf";
    private static final String KEY_HTPASSWD_FILE = Keys.realm.htpasswd.userfile;
    private static final String DEFAULT_HTPASSWD_FILE = "${baseFolder}/htpasswd";
    private static final String KEY_OVERRIDE_LOCALAUTH = Keys.realm.htpasswd.overrideLocalAuthentication;
    private static final boolean DEFAULT_OVERRIDE_LOCALAUTH = true;
    private static final String KEY_SUPPORT_PLAINTEXT_PWD = "realm.htpasswd.supportPlaintextPasswords";
    private final boolean SUPPORT_PLAINTEXT_PWD;
    private IStoredSettings settings;
    private File htpasswdFile;
    private final Logger logger = LoggerFactory.getLogger(HtpasswdUserService.class);
    private final Map<String, String> htUsers = new ConcurrentHashMap<String, String>();
    private volatile long lastModified;
    private volatile boolean forceReload;
    public HtpasswdUserService()
    {
        super();
        String os = System.getProperty("os.name").toLowerCase();
        if (os.startsWith("windows") || os.startsWith("netware")) {
            SUPPORT_PLAINTEXT_PWD = true;
        }
        else {
            SUPPORT_PLAINTEXT_PWD = false;
        }
    }
    /**
     * Setup the user service.
     *
     * The HtpasswdUserService extends the GitblitUserService and is thus
     * backed by the available user services provided by the GitblitUserService.
     * In addition the setup tries to read and parse the htpasswd file to be used
     * for authentication.
     *
     * @param settings
     * @since 0.7.0
     */
    @Override
    public void setup(IStoredSettings settings)
    {
        this.settings = settings;
        // This is done in two steps in order to avoid calling GitBlit.getFileOrFolder(String, String) which will segfault for unit tests.
        String file = settings.getString(KEY_BACKING_US, DEFAULT_BACKING_US);
        File realmFile = GitBlit.getFileOrFolder(file);
        serviceImpl = createUserService(realmFile);
        logger.info("Htpasswd User Service backed by " + serviceImpl.toString());
        read();
        logger.debug("Read " + htUsers.size() + " users from htpasswd file: " + this.htpasswdFile);
    }
    /**
     * For now, credentials are defined in the htpasswd file and can not be manipulated
     * from Gitblit.
     *
     * @return false
     * @since 1.0.0
     */
    @Override
    public boolean supportsCredentialChanges()
    {
        return false;
    }
    /**
     * Authenticate a user based on a username and password.
     *
     * If the account is determined to be a local account, authentication
     * will be done against the locally stored password.
     * Otherwise, the configured htpasswd file is read. All current output options
     * of htpasswd are supported: clear text, crypt(), Apache MD5 and unsalted SHA-1.
     *
     * @param username
     * @param password
     * @return a user object or null
     */
    @Override
    public UserModel authenticate(String username, char[] password)
    {
        if (isLocalAccount(username)) {
            // local account, bypass htpasswd authentication
            return super.authenticate(username, password);
        }
        read();
        String storedPwd = htUsers.get(username);
        if (storedPwd != null) {
            boolean authenticated = false;
            final String passwd = new String(password);
            // test Apache MD5 variant encrypted password
            if ( storedPwd.startsWith("$apr1$") ) {
                if ( storedPwd.equals(Md5Crypt.apr1Crypt(passwd, storedPwd)) ) {
                    logger.debug("Apache MD5 encoded password matched for user '" + username + "'");
                    authenticated = true;
                }
            }
            // test unsalted SHA password
            else if ( storedPwd.startsWith("{SHA}") ) {
                String passwd64 = Base64.encodeBase64String(DigestUtils.sha1(passwd));
                if ( storedPwd.substring("{SHA}".length()).equals(passwd64) ) {
                    logger.debug("Unsalted SHA-1 encoded password matched for user '" + username + "'");
                    authenticated = true;
                }
            }
            // test libc crypt() encoded password
            else if ( supportCryptPwd() && storedPwd.equals(Crypt.crypt(passwd, storedPwd)) ) {
                logger.debug("Libc crypt encoded password matched for user '" + username + "'");
                authenticated = true;
            }
            // test clear text
            else if ( supportPlaintextPwd() && storedPwd.equals(passwd) ){
                logger.debug("Clear text password matched for user '" + username + "'");
                authenticated = true;
            }
            if (authenticated) {
                logger.debug("Htpasswd authenticated: " + username);
                UserModel user = getUserModel(username);
                if (user == null) {
                    // create user object for new authenticated user
                    user = new UserModel(username);
                }
                // create a user cookie
                if (StringUtils.isEmpty(user.cookie) && !ArrayUtils.isEmpty(password)) {
                    user.cookie = StringUtils.getSHA1(user.username + passwd);
                }
                // Set user attributes, hide password from backing user service.
                user.password = Constants.EXTERNAL_ACCOUNT;
                user.accountType = getAccountType();
                // Push the looked up values to backing file
                super.updateUserModel(user);
                return user;
            }
        }
        return null;
    }
    /**
     * Determine if the account is to be treated as a local account.
     *
     * This influences authentication. A local account will be authenticated
     * by the backing user service while an external account will be handled
     * by this user service.
     * <br/>
     * The decision also depends on the setting of the key
     * realm.htpasswd.overrideLocalAuthentication.
     * If it is set to true, then passwords will first be checked against the
     * htpasswd store. If an account exists and is marked as local in the backing
     * user service, that setting will be overwritten by the result. This
     * means that an account that looks local to the backing user service will
     * be turned into an external account upon valid login of a user that has
     * an entry in the htpasswd file.
     * If the key is set to false, then it is determined if the account is local
     * according to the logic of the GitblitUserService.
     */
    protected boolean isLocalAccount(String username)
    {
        if ( settings.getBoolean(KEY_OVERRIDE_LOCALAUTH, DEFAULT_OVERRIDE_LOCALAUTH) ) {
            read();
            if ( htUsers.containsKey(username) ) return false;
        }
        return super.isLocalAccount(username);
    }
    /**
     * Get the account type used for this user service.
     *
     * @return AccountType.HTPASSWD
     */
    protected AccountType getAccountType()
    {
        return AccountType.HTPASSWD;
    }
    private String htpasswdFilePath = null;
    /**
     * Reads the realm file and rebuilds the in-memory lookup tables.
     */
    protected synchronized void read()
    {
        // This is done in two steps in order to avoid calling GitBlit.getFileOrFolder(String, String) which will segfault for unit tests.
        String file = settings.getString(KEY_HTPASSWD_FILE, DEFAULT_HTPASSWD_FILE);
        if ( !file.equals(htpasswdFilePath) ) {
            // The htpasswd file setting changed. Rediscover the file.
            this.htpasswdFilePath = file;
            this.htpasswdFile = GitBlit.getFileOrFolder(file);
            this.htUsers.clear();
            this.forceReload = true;
        }
        if (htpasswdFile.exists() && (forceReload || (htpasswdFile.lastModified() != lastModified))) {
            forceReload = false;
            lastModified = htpasswdFile.lastModified();
            htUsers.clear();
            Pattern entry = Pattern.compile("^([^:]+):(.+)");
            Scanner scanner = null;
            try {
                scanner = new Scanner(new FileInputStream(htpasswdFile));
                while( scanner.hasNextLine()) {
                    String line = scanner.nextLine().trim();
                    if ( !line.isEmpty() &&  !line.startsWith("#") ) {
                        Matcher m = entry.matcher(line);
                        if ( m.matches() ) {
                            htUsers.put(m.group(1), m.group(2));
                        }
                    }
                }
            } catch (Exception e) {
                logger.error(MessageFormat.format("Failed to read {0}", htpasswdFile), e);
            }
            finally {
                if (scanner != null) scanner.close();
            }
        }
    }
    private boolean supportPlaintextPwd()
    {
        return this.settings.getBoolean(KEY_SUPPORT_PLAINTEXT_PWD, SUPPORT_PLAINTEXT_PWD);
    }
    private boolean supportCryptPwd()
    {
        return !supportPlaintextPwd();
    }
    @Override
    public String toString()
    {
        return getClass().getSimpleName() + "(" + ((htpasswdFile != null) ? htpasswdFile.getAbsolutePath() : "null") + ")";
    }
    /*
     * Method only used for unit tests. Return number of users read from htpasswd file.
     */
    public int getNumberHtpasswdUsers()
    {
        return this.htUsers.size();
    }
}
src/site/design.mkd
@@ -52,6 +52,7 @@
- [JNA](https://github.com/twall/jna) (LGPL 2.1)
- [Guava](https://code.google.com/p/guava-libraries) (Apache 2.0)
- [libpam4j](https://github.com/kohsuke/libpam4j) (MIT)
- [commons-codec](http://commons.apache.org/proper/commons-codec) (Apache 2.0)
### Other Build Dependencies
- [Fancybox image viewer](http://fancybox.net) (MIT and GPL dual-licensed)
src/site/setup_authentication.mkd
@@ -7,6 +7,7 @@
* LDAP authentication
* Windows authentication
* PAM authentication
* Htpasswd authentication
* Redmine auhentication
* Salesforce.com authentication
* Servlet container authentication
@@ -91,6 +92,13 @@
    realm.userService = com.gitblit.PAMUserService
    realm.pam.serviceName = system-auth
### Htpasswd Authentication
Htpasswd authentication allows you to maintain your user credentials in an Apache htpasswd file thay may be shared with other htpasswd-capable servers.
    realm.userService = com.gitblit.HtpasswdUserService
    realm.htpasswd.userFile = /path/to/htpasswd
### Redmine Authentication
You may authenticate your users against a Redmine installation as long as your Redmine install has properly enabled [API authentication](http://www.redmine.org/projects/redmine/wiki/Rest_Api#Authentication).  This user service only supports user authentication; it does not support team creation based on Redmine groups.  Redmine administrators will also be Gitblit administrators.
src/test/java/com/gitblit/tests/GitBlitSuite.java
@@ -60,7 +60,7 @@
        DiffUtilsTest.class, MetricUtilsTest.class, TicgitUtilsTest.class, X509UtilsTest.class,
        GitBlitTest.class, FederationTests.class, RpcTests.class, GitServletTest.class, GitDaemonTest.class,
        GroovyScriptTest.class, LuceneExecutorTest.class, IssuesTest.class, RepositoryModelTest.class,
        FanoutServiceTest.class, Issue0259Test.class, Issue0271Test.class })
        FanoutServiceTest.class, Issue0259Test.class, Issue0271Test.class, HtpasswdUserServiceTest.class })
public class GitBlitSuite {
    public static final File REPOSITORIES = new File("data/git");
src/test/java/com/gitblit/tests/HtpasswdUserServiceTest.java
New file
@@ -0,0 +1,556 @@
package com.gitblit.tests;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.HashMap;
import org.apache.commons.io.FileUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import com.gitblit.HtpasswdUserService;
import com.gitblit.models.UserModel;
import com.gitblit.tests.mock.MemorySettings;
import com.gitblit.utils.StringUtils;
/**
 * Test the Htpasswd user service.
 *
 */
public class HtpasswdUserServiceTest {
    private static final String RESOURCE_DIR = "src/test/resources/htpasswdUSTest/";
    private static final String KEY_SUPPORT_PLAINTEXT_PWD = "realm.htpasswd.supportPlaintextPasswords";
    private static final int NUM_USERS_HTPASSWD = 10;
    private static final MemorySettings MS = new MemorySettings(new HashMap<String, Object>());
    private HtpasswdUserService htpwdUserService;
    private MemorySettings getSettings( String userfile, String groupfile, Boolean overrideLA)
    {
        MS.put("realm.htpasswd.backingUserService", RESOURCE_DIR + "users.conf");
        MS.put("realm.htpasswd.userfile", (userfile == null) ? (RESOURCE_DIR+"htpasswd") : userfile);
        MS.put("realm.htpasswd.groupfile", (groupfile == null) ? (RESOURCE_DIR+"htgroup") : groupfile);
        MS.put("realm.htpasswd.overrideLocalAuthentication", (overrideLA == null) ? "false" : overrideLA.toString());
        // Default to keep test the same on all platforms.
        MS.put(KEY_SUPPORT_PLAINTEXT_PWD, "false");
        return MS;
    }
    private MemorySettings getSettings()
    {
        return getSettings(null, null, null);
    }
    private MemorySettings getSettings(boolean overrideLA)
    {
        return getSettings(null, null, new Boolean(overrideLA));
    }
    private void setupUS()
    {
        htpwdUserService = new HtpasswdUserService();
        htpwdUserService.setup(getSettings());
    }
    private void setupUS(boolean overrideLA)
    {
        htpwdUserService = new HtpasswdUserService();
        htpwdUserService.setup(getSettings(overrideLA));
    }
    private void copyInFiles() throws IOException
    {
        File dir = new File(RESOURCE_DIR);
        FilenameFilter filter = new FilenameFilter() {
            public boolean accept(File dir, String file) {
                return file.endsWith(".in");
                }
            };
        for (File inf : dir.listFiles(filter)) {
            File dest = new File(inf.getParent(), inf.getName().substring(0, inf.getName().length()-3));
            FileUtils.copyFile(inf, dest);
        }
    }
    private void deleteGeneratedFiles()
    {
        File dir = new File(RESOURCE_DIR);
        FilenameFilter filter = new FilenameFilter() {
            public boolean accept(File dir, String file) {
                return !(file.endsWith(".in"));
                }
            };
        for (File file : dir.listFiles(filter)) {
            file.delete();
        }
    }
    @Before
    public void setup() throws IOException
    {
        copyInFiles();
        setupUS();
    }
    @After
    public void tearDown()
    {
        deleteGeneratedFiles();
    }
    @Test
    public void testSetup() throws IOException
    {
        assertEquals(NUM_USERS_HTPASSWD, htpwdUserService.getNumberHtpasswdUsers());
    }
    @Test
    public void testAuthenticate()
    {
        MS.put(KEY_SUPPORT_PLAINTEXT_PWD, "true");
        UserModel user = htpwdUserService.authenticate("user1", "pass1".toCharArray());
        assertNotNull(user);
        assertEquals("user1", user.username);
        user = htpwdUserService.authenticate("user2", "pass2".toCharArray());
        assertNotNull(user);
        assertEquals("user2", user.username);
        // Test different encryptions
        user = htpwdUserService.authenticate("plain", "passWord".toCharArray());
        assertNotNull(user);
        assertEquals("plain", user.username);
        MS.put(KEY_SUPPORT_PLAINTEXT_PWD, "false");
        user = htpwdUserService.authenticate("crypt", "password".toCharArray());
        assertNotNull(user);
        assertEquals("crypt", user.username);
        user = htpwdUserService.authenticate("md5", "password".toCharArray());
        assertNotNull(user);
        assertEquals("md5", user.username);
        user = htpwdUserService.authenticate("sha", "password".toCharArray());
        assertNotNull(user);
        assertEquals("sha", user.username);
        // Test leading and trailing whitespace
        user = htpwdUserService.authenticate("trailing", "whitespace".toCharArray());
        assertNotNull(user);
        assertEquals("trailing", user.username);
        user = htpwdUserService.authenticate("tabbed", "frontAndBack".toCharArray());
        assertNotNull(user);
        assertEquals("tabbed", user.username);
        user = htpwdUserService.authenticate("leading", "whitespace".toCharArray());
        assertNotNull(user);
        assertEquals("leading", user.username);
        // Test local account
        user = htpwdUserService.authenticate("admin", "admin".toCharArray());
        assertNotNull(user);
        assertEquals("admin", user.username);
    }
    @Test
    public void testAttributes()
    {
        MS.put(KEY_SUPPORT_PLAINTEXT_PWD, "true");
        UserModel user = htpwdUserService.authenticate("user1", "pass1".toCharArray());
        assertNotNull(user);
        assertEquals("El Capitan", user.displayName);
        assertEquals("cheffe@example.com", user.emailAddress);
        assertTrue(user.canAdmin);
        user = htpwdUserService.authenticate("user2", "pass2".toCharArray());
        assertNotNull(user);
        assertEquals("User Two", user.displayName);
        assertTrue(user.canCreate);
        assertTrue(user.canFork);
        user = htpwdUserService.authenticate("admin", "admin".toCharArray());
        assertNotNull(user);
        assertTrue(user.canAdmin);
        user = htpwdUserService.authenticate("staylocal", "localUser".toCharArray());
        assertNotNull(user);
        assertEquals("Local User", user.displayName);
        assertFalse(user.canCreate);
        assertFalse(user.canFork);
        assertFalse(user.canAdmin);
    }
    @Test
    public void testAuthenticateDenied()
    {
        UserModel user = null;
        MS.put(KEY_SUPPORT_PLAINTEXT_PWD, "true");
        user = htpwdUserService.authenticate("user1", "".toCharArray());
        assertNull("User 'user1' falsely authenticated.", user);
        user = htpwdUserService.authenticate("user1", "pass2".toCharArray());
        assertNull("User 'user1' falsely authenticated.", user);
        user = htpwdUserService.authenticate("user2", "lalala".toCharArray());
        assertNull("User 'user2' falsely authenticated.", user);
        user = htpwdUserService.authenticate("user3", "disabled".toCharArray());
        assertNull("User 'user3' falsely authenticated.", user);
        user = htpwdUserService.authenticate("user4", "disabled".toCharArray());
        assertNull("User 'user4' falsely authenticated.", user);
        user = htpwdUserService.authenticate("plain", "text".toCharArray());
        assertNull("User 'plain' falsely authenticated.", user);
        user = htpwdUserService.authenticate("plain", "password".toCharArray());
        assertNull("User 'plain' falsely authenticated.", user);
        MS.put(KEY_SUPPORT_PLAINTEXT_PWD, "false");
        user = htpwdUserService.authenticate("crypt", "".toCharArray());
        assertNull("User 'cyrpt' falsely authenticated.", user);
        user = htpwdUserService.authenticate("crypt", "passwd".toCharArray());
        assertNull("User 'crypt' falsely authenticated.", user);
        user = htpwdUserService.authenticate("md5", "".toCharArray());
        assertNull("User 'md5' falsely authenticated.", user);
        user = htpwdUserService.authenticate("md5", "pwd".toCharArray());
        assertNull("User 'md5' falsely authenticated.", user);
        user = htpwdUserService.authenticate("sha", "".toCharArray());
        assertNull("User 'sha' falsely authenticated.", user);
        user = htpwdUserService.authenticate("sha", "letmein".toCharArray());
        assertNull("User 'sha' falsely authenticated.", user);
        user = htpwdUserService.authenticate("  tabbed", "frontAndBack".toCharArray());
        assertNull("User 'tabbed' falsely authenticated.", user);
        user = htpwdUserService.authenticate("    leading", "whitespace".toCharArray());
        assertNull("User 'leading' falsely authenticated.", user);
    }
    @Test
    public void testNewLocalAccount()
    {
        UserModel newUser = new UserModel("newlocal");
        newUser.displayName = "Local User 2";
        newUser.password = StringUtils.MD5_TYPE + StringUtils.getMD5("localPwd2");
        assertTrue("Failed to add local account.", htpwdUserService.updateUserModel(newUser));
        UserModel localAccount = htpwdUserService.authenticate(newUser.username, "localPwd2".toCharArray());
        assertNotNull(localAccount);
        assertEquals(newUser, localAccount);
        localAccount = htpwdUserService.authenticate(newUser.username, "localPwd2".toCharArray());
        assertNotNull(localAccount);
        assertEquals(newUser, localAccount);
        assertTrue("Failed to delete local account.", htpwdUserService.deleteUser(localAccount.username));
        assertNull(htpwdUserService.authenticate(newUser.username, "localPwd2".toCharArray()));
    }
    @Test
    public void testCleartextIntrusion()
    {
        MS.put(KEY_SUPPORT_PLAINTEXT_PWD, "true");
        assertNull(htpwdUserService.authenticate("md5", "$apr1$qAGGNfli$sAn14mn.WKId/3EQS7KSX0".toCharArray()));
        assertNull(htpwdUserService.authenticate("sha", "{SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=".toCharArray()));
        assertNull(htpwdUserService.authenticate("user1", "#externalAccount".toCharArray()));
        MS.put(KEY_SUPPORT_PLAINTEXT_PWD, "false");
        assertNull(htpwdUserService.authenticate("md5", "$apr1$qAGGNfli$sAn14mn.WKId/3EQS7KSX0".toCharArray()));
        assertNull(htpwdUserService.authenticate("sha", "{SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=".toCharArray()));
        assertNull(htpwdUserService.authenticate("user1", "#externalAccount".toCharArray()));
    }
    @Test
    public void testCryptVsPlaintext()
    {
        MS.put(KEY_SUPPORT_PLAINTEXT_PWD, "false");
        assertNull(htpwdUserService.authenticate("crypt", "6TmlbxqZ2kBIA".toCharArray()));
        assertNotNull(htpwdUserService.authenticate("crypt", "password".toCharArray()));
        MS.put(KEY_SUPPORT_PLAINTEXT_PWD, "true");
        assertNotNull(htpwdUserService.authenticate("crypt", "6TmlbxqZ2kBIA".toCharArray()));
        assertNull(htpwdUserService.authenticate("crypt", "password".toCharArray()));
    }
    /*
     * Test case: User exists in user.conf with a local password and in htpasswd with an external password.
     * If overrideLocalAuthentication is false, the local account takes precedence and is never updated.
     */
    @Test
    public void testPreparedAccountPreferLocal() throws IOException
    {
        setupUS(false);
        UserModel user = htpwdUserService.authenticate("leaderred", "localPassword".toCharArray());
        assertNotNull(user);
        assertEquals("leaderred", user.getName());
        user = htpwdUserService.authenticate("leaderred", "localPassword".toCharArray());
        assertNotNull(user);
        assertEquals("leaderred", user.getName());
        user = htpwdUserService.authenticate("leaderred", "externalPassword".toCharArray());
        assertNull(user);
        user = htpwdUserService.authenticate("staylocal", "localUser".toCharArray());
        assertNotNull(user);
        assertEquals("staylocal", user.getName());
        deleteGeneratedFiles();
        copyInFiles();
        setupUS(false);
        user = htpwdUserService.authenticate("leaderred", "externalPassword".toCharArray());
        assertNull(user);
        user = htpwdUserService.authenticate("leaderred", "localPassword".toCharArray());
        assertNotNull(user);
        assertEquals("leaderred", user.getName());
        user = htpwdUserService.authenticate("leaderred", "localPassword".toCharArray());
        assertNotNull(user);
        assertEquals("leaderred", user.getName());
        user = htpwdUserService.authenticate("staylocal", "localUser".toCharArray());
        assertNotNull(user);
        assertEquals("staylocal", user.getName());
    }
    /*
     * Test case: User exists in user.conf with a local password and in htpasswd with an external password.
     * If overrideLocalAuthentication is true, the external account takes precedence,
     * the initial local password is never used and discarded.
     */
    @Test
    public void testPreparedAccountPreferExternal() throws IOException
    {
        setupUS(true);
        UserModel user = htpwdUserService.authenticate("leaderred", "externalPassword".toCharArray());
        assertNotNull(user);
        assertEquals("leaderred", user.getName());
        user = htpwdUserService.authenticate("leaderred", "externalPassword".toCharArray());
        assertNotNull(user);
        assertEquals("leaderred", user.getName());
        user = htpwdUserService.authenticate("leaderred", "localPassword".toCharArray());
        assertNull(user);
        user = htpwdUserService.authenticate("staylocal", "localUser".toCharArray());
        assertNotNull(user);
        assertEquals("staylocal", user.getName());
        deleteGeneratedFiles();
        copyInFiles();
        setupUS(true);
        user = htpwdUserService.authenticate("leaderred", "localPassword".toCharArray());
        assertNull(user);
        user = htpwdUserService.authenticate("leaderred", "externalPassword".toCharArray());
        assertNotNull(user);
        assertEquals("leaderred", user.getName());
        user = htpwdUserService.authenticate("leaderred", "externalPassword".toCharArray());
        assertNotNull(user);
        assertEquals("leaderred", user.getName());
        user = htpwdUserService.authenticate("staylocal", "localUser".toCharArray());
        assertNotNull(user);
        assertEquals("staylocal", user.getName());
        // Make sure no authentication by using the string constant for external accounts is possible.
        user = htpwdUserService.authenticate("leaderred", "#externalAccount".toCharArray());
        assertNull(user);
    }
    /*
     * Test case: User exists in user.conf with a local password and in htpasswd with an external password.
     * If overrideLocalAuthentication is true, the external account takes precedence,
     * the initial local password is never used and discarded.
     */
    @Test
    public void testPreparedAccountChangeSetting() throws IOException
    {
        getSettings(false);
        UserModel user = htpwdUserService.authenticate("leaderred", "localPassword".toCharArray());
        assertNotNull(user);
        assertEquals("leaderred", user.getName());
        user = htpwdUserService.authenticate("leaderred", "externalPassword".toCharArray());
        assertNull(user);
        user = htpwdUserService.authenticate("staylocal", "localUser".toCharArray());
        assertNotNull(user);
        assertEquals("staylocal", user.getName());
        getSettings(true);
        user = htpwdUserService.authenticate("leaderred", "localPassword".toCharArray());
        assertNull(user);
        user = htpwdUserService.authenticate("leaderred", "externalPassword".toCharArray());
        assertNotNull(user);
        assertEquals("leaderred", user.getName());
        user = htpwdUserService.authenticate("staylocal", "localUser".toCharArray());
        assertNotNull(user);
        assertEquals("staylocal", user.getName());
        // Make sure no authentication by using the string constant for external accounts is possible.
        user = htpwdUserService.authenticate("leaderred", "#externalAccount".toCharArray());
        assertNull(user);
        getSettings(false);
        // The preference is now back to local accounts but since the prepared account got switched
        // to an external account, it will stay this way.
        user = htpwdUserService.authenticate("leaderred", "localPassword".toCharArray());
        assertNull(user);
        user = htpwdUserService.authenticate("leaderred", "externalPassword".toCharArray());
        assertNotNull(user);
        assertEquals("leaderred", user.getName());
        user = htpwdUserService.authenticate("staylocal", "localUser".toCharArray());
        assertNotNull(user);
        assertEquals("staylocal", user.getName());
        // Make sure no authentication by using the string constant for external accounts is possible.
        user = htpwdUserService.authenticate("leaderred", "#externalAccount".toCharArray());
        assertNull(user);
    }
    @Test
    public void testChangeHtpasswdFile()
    {
        UserModel user;
        // User default set up.
        user = htpwdUserService.authenticate("md5", "password".toCharArray());
        assertNotNull(user);
        assertEquals("md5", user.username);
        user = htpwdUserService.authenticate("sha", "password".toCharArray());
        assertNotNull(user);
        assertEquals("sha", user.username);
        user = htpwdUserService.authenticate("blueone", "GoBlue!".toCharArray());
        assertNull(user);
        user = htpwdUserService.authenticate("bluetwo", "YayBlue!".toCharArray());
        assertNull(user);
        // Switch to different htpasswd file.
        getSettings(RESOURCE_DIR + "htpasswd-user", null, null);
        user = htpwdUserService.authenticate("md5", "password".toCharArray());
        assertNull(user);
        user = htpwdUserService.authenticate("sha", "password".toCharArray());
        assertNull(user);
        user = htpwdUserService.authenticate("blueone", "GoBlue!".toCharArray());
        assertNotNull(user);
        assertEquals("blueone", user.username);
        user = htpwdUserService.authenticate("bluetwo", "YayBlue!".toCharArray());
        assertNotNull(user);
        assertEquals("bluetwo", user.username);
    }
    @Test
    public void testChangeHtpasswdFileNotExisting()
    {
        UserModel user;
        // User default set up.
        user = htpwdUserService.authenticate("md5", "password".toCharArray());
        assertNotNull(user);
        assertEquals("md5", user.username);
        user = htpwdUserService.authenticate("sha", "password".toCharArray());
        assertNotNull(user);
        assertEquals("sha", user.username);
        user = htpwdUserService.authenticate("blueone", "GoBlue!".toCharArray());
        assertNull(user);
        user = htpwdUserService.authenticate("bluetwo", "YayBlue!".toCharArray());
        assertNull(user);
        // Switch to different htpasswd file that doesn't exist.
        // Currently we stop working with old users upon this change.
        getSettings(RESOURCE_DIR + "no-such-file", null, null);
        user = htpwdUserService.authenticate("md5", "password".toCharArray());
        assertNull(user);
        user = htpwdUserService.authenticate("sha", "password".toCharArray());
        assertNull(user);
        user = htpwdUserService.authenticate("blueone", "GoBlue!".toCharArray());
        assertNull(user);
        user = htpwdUserService.authenticate("bluetwo", "YayBlue!".toCharArray());
        assertNull(user);
    }
}
src/test/resources/htpasswdUSTest/htpasswd-user.in
New file
@@ -0,0 +1,15 @@
# User database
# htpasswd generated entries
# Plaintext
redone:Yonder
# Unix crypt() "GoRed!"
redtwo:RMghf6oG.QwAs
 # Apache MD5  "GoBlue!"
blueone:$apr1$phRTn/7N$237Owfhw5wZTdTyP9NPvC1
# SHA1  "YayBlue!"
bluetwo:{SHA}ITMvZI9OU5+Rx324C4jpf+MHAL8=
src/test/resources/htpasswdUSTest/htpasswd.in
New file
@@ -0,0 +1,31 @@
# User database
user1:pass1
user2:pass2
# "externalPassword"
leaderred:{SHA}2VZsTsVQYmWAMfQUjNAScpaAlJI=
#user3:disabled
    # user4:disabled
# htpasswd generated entries
# Plaintext
plain:passWord
# Unix crypt()  "password"
crypt:6TmlbxqZ2kBIA
 # Apache MD5   "password"
md5:$apr1$qAGGNfli$sAn14mn.WKId/3EQS7KSX0
# SHA1  "password"
sha:{SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=
trailing:.dAxRAQiOOlN.
    tabbed:$apr1$Is7zctsH$CMAXrGkgACQKgRYuQ5vHq.
    leading:$apr1$O1nQtxjE$8gN15gMeuF3W1Nr8Yz/6J.
src/test/resources/htpasswdUSTest/users.conf.in
New file
@@ -0,0 +1,26 @@
[user "admin"]
    password = admin
    cookie = dd94709528bb1c83d08f3088d4043f4742891f4f
    role = "#admin"
    role = "#notfederated"
[user "user1"]
    password = "#externalAccount"
    cookie = 6c7d13cf0aa43054d0fb620546e3a4d79e3d3e89
    displayName = El Capitan
    emailAddress = cheffe@example.com
    role = "#admin"
[user "user2"]
    password = "#externalAccount"
    cookie = d15eabb3a83c44a05ccbdaf3bf5fd1402d971e99
    displayName = User Two
    role = "#create"
    role = "#fork"
[user "staylocal"]
    password = localUser
    cookie = 0a99767e0259dc06ccae5ee6349177be289968f3
    displayName = Local User
    role = "#none"
[user "leaderRed"]
    password = localPassword
    displayName = Red Leader
    role = "#create"