.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
@@ -311,3 +311,11 @@ 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"