Add a new class, HtpasswdUserService, which performs authentication against a text file created with the Apache 'htpasswd' program. Added dependency on commons-codec:1.7tags/v1.3.2
@@ -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" /> |
@@ -310,4 +310,12 @@ libpam4j | |||
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 | |||
@@ -152,6 +152,7 @@ dependencies: | |||
- 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 |
@@ -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> | |||
@@ -611,17 +622,6 @@ | |||
</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> | |||
<orderEntry type="module-library" scope="TEST"> | |||
<library name="commons-exec-1.1.jar"> | |||
<CLASSES> |
@@ -20,13 +20,18 @@ r19: { | |||
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' } | |||
} | |||
# |
@@ -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 |
@@ -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; |
@@ -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(); | |||
} | |||
} |
@@ -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) |
@@ -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. |
@@ -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"); |
@@ -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); | |||
} | |||
} |
@@ -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= |
@@ -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. |
@@ -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" |