You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

HtpasswdUserService.java 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. /*
  2. * Copyright 2013 Florian Zschocke
  3. * Copyright 2013 gitblit.com
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. package com.gitblit;
  18. import java.io.File;
  19. import java.io.FileInputStream;
  20. import java.text.MessageFormat;
  21. import java.util.Map;
  22. import java.util.Scanner;
  23. import java.util.concurrent.ConcurrentHashMap;
  24. import java.util.regex.Matcher;
  25. import java.util.regex.Pattern;
  26. import org.apache.commons.codec.binary.Base64;
  27. import org.apache.commons.codec.digest.Crypt;
  28. import org.apache.commons.codec.digest.DigestUtils;
  29. import org.apache.commons.codec.digest.Md5Crypt;
  30. import org.slf4j.Logger;
  31. import org.slf4j.LoggerFactory;
  32. import com.gitblit.Constants.AccountType;
  33. import com.gitblit.models.UserModel;
  34. import com.gitblit.utils.ArrayUtils;
  35. import com.gitblit.utils.StringUtils;
  36. /**
  37. * Implementation of a user service using an Apache htpasswd file for authentication.
  38. *
  39. * This user service implement custom authentication using entries in a file created
  40. * by the 'htpasswd' program of an Apache web server. All possible output
  41. * options of the 'htpasswd' program version 2.2 are supported:
  42. * plain text (only on Windows and Netware),
  43. * glibc crypt() (not on Windows and NetWare),
  44. * Apache MD5 (apr1),
  45. * unsalted SHA-1.
  46. *
  47. * Configuration options:
  48. * realm.htpasswd.backingUserService - Specify the backing user service that is used
  49. * to keep the user data other than the password.
  50. * The default is '${baseFolder}/users.conf'.
  51. * realm.htpasswd.userfile - The text file with the htpasswd entries to be used for
  52. * authentication.
  53. * The default is '${baseFolder}/htpasswd'.
  54. * realm.htpasswd.overrideLocalAuthentication - Specify if local accounts are overwritten
  55. * when authentication matches for an
  56. * external account.
  57. *
  58. * @author Florian Zschocke
  59. *
  60. */
  61. public class HtpasswdUserService extends GitblitUserService
  62. {
  63. private static final String KEY_BACKING_US = Keys.realm.htpasswd.backingUserService;
  64. private static final String DEFAULT_BACKING_US = "${baseFolder}/users.conf";
  65. private static final String KEY_HTPASSWD_FILE = Keys.realm.htpasswd.userfile;
  66. private static final String DEFAULT_HTPASSWD_FILE = "${baseFolder}/htpasswd";
  67. private static final String KEY_OVERRIDE_LOCALAUTH = Keys.realm.htpasswd.overrideLocalAuthentication;
  68. private static final boolean DEFAULT_OVERRIDE_LOCALAUTH = true;
  69. private static final String KEY_SUPPORT_PLAINTEXT_PWD = "realm.htpasswd.supportPlaintextPasswords";
  70. private final boolean SUPPORT_PLAINTEXT_PWD;
  71. private IStoredSettings settings;
  72. private File htpasswdFile;
  73. private final Logger logger = LoggerFactory.getLogger(HtpasswdUserService.class);
  74. private final Map<String, String> htUsers = new ConcurrentHashMap<String, String>();
  75. private volatile long lastModified;
  76. private volatile boolean forceReload;
  77. public HtpasswdUserService()
  78. {
  79. super();
  80. String os = System.getProperty("os.name").toLowerCase();
  81. if (os.startsWith("windows") || os.startsWith("netware")) {
  82. SUPPORT_PLAINTEXT_PWD = true;
  83. }
  84. else {
  85. SUPPORT_PLAINTEXT_PWD = false;
  86. }
  87. }
  88. /**
  89. * Setup the user service.
  90. *
  91. * The HtpasswdUserService extends the GitblitUserService and is thus
  92. * backed by the available user services provided by the GitblitUserService.
  93. * In addition the setup tries to read and parse the htpasswd file to be used
  94. * for authentication.
  95. *
  96. * @param settings
  97. * @since 0.7.0
  98. */
  99. @Override
  100. public void setup(IStoredSettings settings)
  101. {
  102. this.settings = settings;
  103. // This is done in two steps in order to avoid calling GitBlit.getFileOrFolder(String, String) which will segfault for unit tests.
  104. String file = settings.getString(KEY_BACKING_US, DEFAULT_BACKING_US);
  105. File realmFile = GitBlit.getFileOrFolder(file);
  106. serviceImpl = createUserService(realmFile);
  107. logger.info("Htpasswd User Service backed by " + serviceImpl.toString());
  108. read();
  109. logger.debug("Read " + htUsers.size() + " users from htpasswd file: " + this.htpasswdFile);
  110. }
  111. /**
  112. * For now, credentials are defined in the htpasswd file and can not be manipulated
  113. * from Gitblit.
  114. *
  115. * @return false
  116. * @since 1.0.0
  117. */
  118. @Override
  119. public boolean supportsCredentialChanges()
  120. {
  121. return false;
  122. }
  123. /**
  124. * Authenticate a user based on a username and password.
  125. *
  126. * If the account is determined to be a local account, authentication
  127. * will be done against the locally stored password.
  128. * Otherwise, the configured htpasswd file is read. All current output options
  129. * of htpasswd are supported: clear text, crypt(), Apache MD5 and unsalted SHA-1.
  130. *
  131. * @param username
  132. * @param password
  133. * @return a user object or null
  134. */
  135. @Override
  136. public UserModel authenticate(String username, char[] password)
  137. {
  138. if (isLocalAccount(username)) {
  139. // local account, bypass htpasswd authentication
  140. return super.authenticate(username, password);
  141. }
  142. read();
  143. String storedPwd = htUsers.get(username);
  144. if (storedPwd != null) {
  145. boolean authenticated = false;
  146. final String passwd = new String(password);
  147. // test Apache MD5 variant encrypted password
  148. if ( storedPwd.startsWith("$apr1$") ) {
  149. if ( storedPwd.equals(Md5Crypt.apr1Crypt(passwd, storedPwd)) ) {
  150. logger.debug("Apache MD5 encoded password matched for user '" + username + "'");
  151. authenticated = true;
  152. }
  153. }
  154. // test unsalted SHA password
  155. else if ( storedPwd.startsWith("{SHA}") ) {
  156. String passwd64 = Base64.encodeBase64String(DigestUtils.sha1(passwd));
  157. if ( storedPwd.substring("{SHA}".length()).equals(passwd64) ) {
  158. logger.debug("Unsalted SHA-1 encoded password matched for user '" + username + "'");
  159. authenticated = true;
  160. }
  161. }
  162. // test libc crypt() encoded password
  163. else if ( supportCryptPwd() && storedPwd.equals(Crypt.crypt(passwd, storedPwd)) ) {
  164. logger.debug("Libc crypt encoded password matched for user '" + username + "'");
  165. authenticated = true;
  166. }
  167. // test clear text
  168. else if ( supportPlaintextPwd() && storedPwd.equals(passwd) ){
  169. logger.debug("Clear text password matched for user '" + username + "'");
  170. authenticated = true;
  171. }
  172. if (authenticated) {
  173. logger.debug("Htpasswd authenticated: " + username);
  174. UserModel user = getUserModel(username);
  175. if (user == null) {
  176. // create user object for new authenticated user
  177. user = new UserModel(username);
  178. }
  179. // create a user cookie
  180. if (StringUtils.isEmpty(user.cookie) && !ArrayUtils.isEmpty(password)) {
  181. user.cookie = StringUtils.getSHA1(user.username + passwd);
  182. }
  183. // Set user attributes, hide password from backing user service.
  184. user.password = Constants.EXTERNAL_ACCOUNT;
  185. user.accountType = getAccountType();
  186. // Push the looked up values to backing file
  187. super.updateUserModel(user);
  188. return user;
  189. }
  190. }
  191. return null;
  192. }
  193. /**
  194. * Determine if the account is to be treated as a local account.
  195. *
  196. * This influences authentication. A local account will be authenticated
  197. * by the backing user service while an external account will be handled
  198. * by this user service.
  199. * <br/>
  200. * The decision also depends on the setting of the key
  201. * realm.htpasswd.overrideLocalAuthentication.
  202. * If it is set to true, then passwords will first be checked against the
  203. * htpasswd store. If an account exists and is marked as local in the backing
  204. * user service, that setting will be overwritten by the result. This
  205. * means that an account that looks local to the backing user service will
  206. * be turned into an external account upon valid login of a user that has
  207. * an entry in the htpasswd file.
  208. * If the key is set to false, then it is determined if the account is local
  209. * according to the logic of the GitblitUserService.
  210. */
  211. protected boolean isLocalAccount(String username)
  212. {
  213. if ( settings.getBoolean(KEY_OVERRIDE_LOCALAUTH, DEFAULT_OVERRIDE_LOCALAUTH) ) {
  214. read();
  215. if ( htUsers.containsKey(username) ) return false;
  216. }
  217. return super.isLocalAccount(username);
  218. }
  219. /**
  220. * Get the account type used for this user service.
  221. *
  222. * @return AccountType.HTPASSWD
  223. */
  224. protected AccountType getAccountType()
  225. {
  226. return AccountType.HTPASSWD;
  227. }
  228. private String htpasswdFilePath = null;
  229. /**
  230. * Reads the realm file and rebuilds the in-memory lookup tables.
  231. */
  232. protected synchronized void read()
  233. {
  234. // This is done in two steps in order to avoid calling GitBlit.getFileOrFolder(String, String) which will segfault for unit tests.
  235. String file = settings.getString(KEY_HTPASSWD_FILE, DEFAULT_HTPASSWD_FILE);
  236. if ( !file.equals(htpasswdFilePath) ) {
  237. // The htpasswd file setting changed. Rediscover the file.
  238. this.htpasswdFilePath = file;
  239. this.htpasswdFile = GitBlit.getFileOrFolder(file);
  240. this.htUsers.clear();
  241. this.forceReload = true;
  242. }
  243. if (htpasswdFile.exists() && (forceReload || (htpasswdFile.lastModified() != lastModified))) {
  244. forceReload = false;
  245. lastModified = htpasswdFile.lastModified();
  246. htUsers.clear();
  247. Pattern entry = Pattern.compile("^([^:]+):(.+)");
  248. Scanner scanner = null;
  249. try {
  250. scanner = new Scanner(new FileInputStream(htpasswdFile));
  251. while( scanner.hasNextLine()) {
  252. String line = scanner.nextLine().trim();
  253. if ( !line.isEmpty() && !line.startsWith("#") ) {
  254. Matcher m = entry.matcher(line);
  255. if ( m.matches() ) {
  256. htUsers.put(m.group(1), m.group(2));
  257. }
  258. }
  259. }
  260. } catch (Exception e) {
  261. logger.error(MessageFormat.format("Failed to read {0}", htpasswdFile), e);
  262. }
  263. finally {
  264. if (scanner != null) scanner.close();
  265. }
  266. }
  267. }
  268. private boolean supportPlaintextPwd()
  269. {
  270. return this.settings.getBoolean(KEY_SUPPORT_PLAINTEXT_PWD, SUPPORT_PLAINTEXT_PWD);
  271. }
  272. private boolean supportCryptPwd()
  273. {
  274. return !supportPlaintextPwd();
  275. }
  276. @Override
  277. public String toString()
  278. {
  279. return getClass().getSimpleName() + "(" + ((htpasswdFile != null) ? htpasswdFile.getAbsolutePath() : "null") + ")";
  280. }
  281. /*
  282. * Method only used for unit tests. Return number of users read from htpasswd file.
  283. */
  284. public int getNumberHtpasswdUsers()
  285. {
  286. return this.htUsers.size();
  287. }
  288. }