summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorFlorian Zschocke <florian.zschocke@cycos.com>2013-07-09 13:07:13 +0200
committerJames Moger <james.moger@gitblit.com>2013-08-12 16:32:12 -0400
commita0c34e37fe8e456a21c7a57e9d45e637ab40cce8 (patch)
treea85998534a5075716263d7d3c4529e5b3b9a11b5 /src
parent13208e8c3b34c321b470aa181b705f78fcc09c5f (diff)
downloadgitblit-a0c34e37fe8e456a21c7a57e9d45e637ab40cce8.tar.gz
gitblit-a0c34e37fe8e456a21c7a57e9d45e637ab40cce8.zip
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
Diffstat (limited to 'src')
-rw-r--r--src/main/distrib/data/gitblit.properties33
-rw-r--r--src/main/java/com/gitblit/Constants.java2
-rw-r--r--src/main/java/com/gitblit/HtpasswdUserService.java356
-rw-r--r--src/site/design.mkd1
-rw-r--r--src/site/setup_authentication.mkd8
-rw-r--r--src/test/java/com/gitblit/tests/GitBlitSuite.java2
-rw-r--r--src/test/java/com/gitblit/tests/HtpasswdUserServiceTest.java556
-rw-r--r--src/test/resources/htpasswdUSTest/htpasswd-user.in15
-rw-r--r--src/test/resources/htpasswdUSTest/htpasswd.in31
-rw-r--r--src/test/resources/htpasswdUSTest/users.conf.in26
10 files changed, 1028 insertions, 2 deletions
diff --git a/src/main/distrib/data/gitblit.properties b/src/main/distrib/data/gitblit.properties
index 770bd39d..9be7f645 100644
--- a/src/main/distrib/data/gitblit.properties
+++ b/src/main/distrib/data/gitblit.properties
@@ -502,6 +502,7 @@ web.projectsFile = ${baseFolder}/projects.conf
# 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 @@ realm.pam.backingUserService = ${baseFolder}/users.conf
# 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
diff --git a/src/main/java/com/gitblit/Constants.java b/src/main/java/com/gitblit/Constants.java
index c180bafb..2c67bfff 100644
--- a/src/main/java/com/gitblit/Constants.java
+++ b/src/main/java/com/gitblit/Constants.java
@@ -480,7 +480,7 @@ public class Constants {
}
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;
diff --git a/src/main/java/com/gitblit/HtpasswdUserService.java b/src/main/java/com/gitblit/HtpasswdUserService.java
new file mode 100644
index 00000000..62198f4a
--- /dev/null
+++ b/src/main/java/com/gitblit/HtpasswdUserService.java
@@ -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();
+ }
+}
diff --git a/src/site/design.mkd b/src/site/design.mkd
index 601d68ab..ce676201 100644
--- a/src/site/design.mkd
+++ b/src/site/design.mkd
@@ -52,6 +52,7 @@ The following dependencies are automatically downloaded by Gitblit GO (or alread
- [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)
diff --git a/src/site/setup_authentication.mkd b/src/site/setup_authentication.mkd
index 0ec07fa5..3fb4a6c1 100644
--- a/src/site/setup_authentication.mkd
+++ b/src/site/setup_authentication.mkd
@@ -7,6 +7,7 @@ Gitblit supports additional authentication mechanisms aside from it's internal o
* LDAP authentication
* Windows authentication
* PAM authentication
+* Htpasswd authentication
* Redmine auhentication
* Salesforce.com authentication
* Servlet container authentication
@@ -91,6 +92,13 @@ PAM authentication is based on the use of libpam4j and JNA. To use this service
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.
diff --git a/src/test/java/com/gitblit/tests/GitBlitSuite.java b/src/test/java/com/gitblit/tests/GitBlitSuite.java
index 6fff241c..64398b22 100644
--- a/src/test/java/com/gitblit/tests/GitBlitSuite.java
+++ b/src/test/java/com/gitblit/tests/GitBlitSuite.java
@@ -60,7 +60,7 @@ import com.gitblit.utils.JGitUtils;
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");
diff --git a/src/test/java/com/gitblit/tests/HtpasswdUserServiceTest.java b/src/test/java/com/gitblit/tests/HtpasswdUserServiceTest.java
new file mode 100644
index 00000000..ef9d35ff
--- /dev/null
+++ b/src/test/java/com/gitblit/tests/HtpasswdUserServiceTest.java
@@ -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);
+ }
+
+}
diff --git a/src/test/resources/htpasswdUSTest/htpasswd-user.in b/src/test/resources/htpasswdUSTest/htpasswd-user.in
new file mode 100644
index 00000000..3ea87ede
--- /dev/null
+++ b/src/test/resources/htpasswdUSTest/htpasswd-user.in
@@ -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=
diff --git a/src/test/resources/htpasswdUSTest/htpasswd.in b/src/test/resources/htpasswdUSTest/htpasswd.in
new file mode 100644
index 00000000..f2900e70
--- /dev/null
+++ b/src/test/resources/htpasswdUSTest/htpasswd.in
@@ -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.
diff --git a/src/test/resources/htpasswdUSTest/users.conf.in b/src/test/resources/htpasswdUSTest/users.conf.in
new file mode 100644
index 00000000..142265a4
--- /dev/null
+++ b/src/test/resources/htpasswdUSTest/users.conf.in
@@ -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"