diff options
Diffstat (limited to 'src/main')
-rw-r--r-- | src/main/distrib/data/gitblit.properties | 33 | ||||
-rw-r--r-- | src/main/java/com/gitblit/Constants.java | 2 | ||||
-rw-r--r-- | src/main/java/com/gitblit/HtpasswdUserService.java | 356 |
3 files changed, 390 insertions, 1 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(); + } +} |