James Moger
2015-11-22 ed552ba47c02779c270ffd62841d6d1048dade70
commit | author | age
a0c34e 1 /*
FZ 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  */
04a985 17 package com.gitblit.auth;
a0c34e 18
FZ 19 import java.io.File;
20 import java.io.FileInputStream;
21 import java.text.MessageFormat;
22 import java.util.Map;
23 import java.util.Scanner;
24 import java.util.concurrent.ConcurrentHashMap;
25 import java.util.regex.Matcher;
26 import java.util.regex.Pattern;
27
28 import org.apache.commons.codec.binary.Base64;
29 import org.apache.commons.codec.digest.Crypt;
30 import org.apache.commons.codec.digest.DigestUtils;
31 import org.apache.commons.codec.digest.Md5Crypt;
32
04a985 33 import com.gitblit.Constants;
a0c34e 34 import com.gitblit.Constants.AccountType;
6e3481 35 import com.gitblit.Constants.Role;
04a985 36 import com.gitblit.Keys;
JM 37 import com.gitblit.auth.AuthenticationProvider.UsernamePasswordAuthenticationProvider;
6e3481 38 import com.gitblit.models.TeamModel;
a0c34e 39 import com.gitblit.models.UserModel;
FZ 40
41
42 /**
43  * Implementation of a user service using an Apache htpasswd file for authentication.
699e71 44  *
a0c34e 45  * This user service implement custom authentication using entries in a file created
FZ 46  * by the 'htpasswd' program of an Apache web server. All possible output
47  * options of the 'htpasswd' program version 2.2 are supported:
48  * plain text (only on Windows and Netware),
49  * glibc crypt() (not on Windows and NetWare),
50  * Apache MD5 (apr1),
51  * unsalted SHA-1.
699e71 52  *
a0c34e 53  * Configuration options:
FZ 54  * realm.htpasswd.backingUserService - Specify the backing user service that is used
55  *                                     to keep the user data other than the password.
56  *                                     The default is '${baseFolder}/users.conf'.
57  * realm.htpasswd.userfile - The text file with the htpasswd entries to be used for
58  *                           authentication.
59  *                           The default is '${baseFolder}/htpasswd'.
60  * realm.htpasswd.overrideLocalAuthentication - Specify if local accounts are overwritten
61  *                                              when authentication matches for an
62  *                                              external account.
699e71 63  *
a0c34e 64  * @author Florian Zschocke
FZ 65  *
66  */
04a985 67 public class HtpasswdAuthProvider extends UsernamePasswordAuthenticationProvider {
a0c34e 68
FZ 69     private static final String KEY_HTPASSWD_FILE = Keys.realm.htpasswd.userfile;
70     private static final String DEFAULT_HTPASSWD_FILE = "${baseFolder}/htpasswd";
71
72     private static final String KEY_SUPPORT_PLAINTEXT_PWD = "realm.htpasswd.supportPlaintextPasswords";
73
04a985 74     private boolean supportPlainTextPwd;
a0c34e 75
FZ 76     private File htpasswdFile;
77
78     private final Map<String, String> htUsers = new ConcurrentHashMap<String, String>();
79
80     private volatile long lastModified;
81
04a985 82     public HtpasswdAuthProvider() {
JM 83         super("htpasswd");
a0c34e 84     }
FZ 85
86     /**
87      * Setup the user service.
699e71 88      *
a0c34e 89      * The HtpasswdUserService extends the GitblitUserService and is thus
FZ 90      * backed by the available user services provided by the GitblitUserService.
91      * In addition the setup tries to read and parse the htpasswd file to be used
92      * for authentication.
699e71 93      *
04a985 94      * @param settings
JM 95      * @since 0.7.0
a0c34e 96      */
FZ 97     @Override
04a985 98     public void setup() {
JM 99         String os = System.getProperty("os.name").toLowerCase();
100         if (os.startsWith("windows") || os.startsWith("netware")) {
101             supportPlainTextPwd = true;
102         } else {
103             supportPlainTextPwd = false;
104         }
a0c34e 105         read();
FZ 106         logger.debug("Read " + htUsers.size() + " users from htpasswd file: " + this.htpasswdFile);
107     }
108
109     @Override
04a985 110     public boolean supportsCredentialChanges() {
a0c34e 111         return false;
FZ 112     }
113
04a985 114     @Override
JM 115     public boolean supportsDisplayNameChanges() {
116         return true;
117     }
a0c34e 118
04a985 119     @Override
JM 120     public boolean supportsEmailAddressChanges() {
121         return true;
122     }
123
124     @Override
125     public boolean supportsTeamMembershipChanges() {
126         return true;
127     }
a0c34e 128
6e3481 129     @Override
JM 130     public boolean supportsRoleChanges(UserModel user, Role role) {
131         return true;
132     }
133
134     @Override
135     public boolean supportsRoleChanges(TeamModel team, Role role) {
136         return true;
137     }
138
a0c34e 139     /**
FZ 140      * Authenticate a user based on a username and password.
141      *
142      * If the account is determined to be a local account, authentication
143      * will be done against the locally stored password.
144      * Otherwise, the configured htpasswd file is read. All current output options
145      * of htpasswd are supported: clear text, crypt(), Apache MD5 and unsalted SHA-1.
146      *
147      * @param username
148      * @param password
149      * @return a user object or null
150      */
151     @Override
04a985 152     public UserModel authenticate(String username, char[] password) {
a0c34e 153         read();
FZ 154         String storedPwd = htUsers.get(username);
155         if (storedPwd != null) {
156             boolean authenticated = false;
157             final String passwd = new String(password);
158
159             // test Apache MD5 variant encrypted password
04a985 160             if (storedPwd.startsWith("$apr1$")) {
JM 161                 if (storedPwd.equals(Md5Crypt.apr1Crypt(passwd, storedPwd))) {
a0c34e 162                     logger.debug("Apache MD5 encoded password matched for user '" + username + "'");
FZ 163                     authenticated = true;
164                 }
165             }
166             // test unsalted SHA password
04a985 167             else if (storedPwd.startsWith("{SHA}")) {
a0c34e 168                 String passwd64 = Base64.encodeBase64String(DigestUtils.sha1(passwd));
04a985 169                 if (storedPwd.substring("{SHA}".length()).equals(passwd64)) {
a0c34e 170                     logger.debug("Unsalted SHA-1 encoded password matched for user '" + username + "'");
FZ 171                     authenticated = true;
172                 }
173             }
174             // test libc crypt() encoded password
04a985 175             else if (supportCryptPwd() && storedPwd.equals(Crypt.crypt(passwd, storedPwd))) {
a0c34e 176                 logger.debug("Libc crypt encoded password matched for user '" + username + "'");
FZ 177                 authenticated = true;
178             }
179             // test clear text
04a985 180             else if (supportPlaintextPwd() && storedPwd.equals(passwd)){
a0c34e 181                 logger.debug("Clear text password matched for user '" + username + "'");
FZ 182                 authenticated = true;
183             }
184
185
186             if (authenticated) {
187                 logger.debug("Htpasswd authenticated: " + username);
188
04a985 189                 UserModel curr = userManager.getUserModel(username);
JM 190                 UserModel user;
191                 if (curr == null) {
a0c34e 192                     // create user object for new authenticated user
FZ 193                     user = new UserModel(username);
04a985 194                 } else {
JM 195                     user = curr;
a0c34e 196                 }
FZ 197
198                 // create a user cookie
c1b0e4 199                 setCookie(user, password);
a0c34e 200
FZ 201                 // Set user attributes, hide password from backing user service.
202                 user.password = Constants.EXTERNAL_ACCOUNT;
203                 user.accountType = getAccountType();
204
205                 // Push the looked up values to backing file
04a985 206                    updateUser(user);
a0c34e 207
FZ 208                 return user;
209             }
210         }
211
212         return null;
213     }
214
215     /**
216      * Get the account type used for this user service.
217      *
218      * @return AccountType.HTPASSWD
219      */
699e71 220     @Override
04a985 221     public AccountType getAccountType() {
a0c34e 222         return AccountType.HTPASSWD;
FZ 223     }
224
225     /**
226      * Reads the realm file and rebuilds the in-memory lookup tables.
227      */
04a985 228     protected synchronized void read() {
JM 229         boolean forceReload = false;
230         File file = getFileOrFolder(KEY_HTPASSWD_FILE, DEFAULT_HTPASSWD_FILE);
231         if (!file.equals(htpasswdFile)) {
232             this.htpasswdFile = file;
a0c34e 233             this.htUsers.clear();
04a985 234             forceReload = true;
a0c34e 235         }
FZ 236
237         if (htpasswdFile.exists() && (forceReload || (htpasswdFile.lastModified() != lastModified))) {
238             lastModified = htpasswdFile.lastModified();
239             htUsers.clear();
240
241             Pattern entry = Pattern.compile("^([^:]+):(.+)");
242
243             Scanner scanner = null;
244             try {
245                 scanner = new Scanner(new FileInputStream(htpasswdFile));
04a985 246                 while (scanner.hasNextLine()) {
a0c34e 247                     String line = scanner.nextLine().trim();
04a985 248                     if (!line.isEmpty() &&  !line.startsWith("#")) {
a0c34e 249                         Matcher m = entry.matcher(line);
04a985 250                         if (m.matches()) {
a0c34e 251                             htUsers.put(m.group(1), m.group(2));
FZ 252                         }
253                     }
254                 }
255             } catch (Exception e) {
256                 logger.error(MessageFormat.format("Failed to read {0}", htpasswdFile), e);
04a985 257             } finally {
JM 258                 if (scanner != null) {
259                     scanner.close();
260                 }
a0c34e 261             }
FZ 262         }
263     }
264
04a985 265     private boolean supportPlaintextPwd() {
JM 266         return this.settings.getBoolean(KEY_SUPPORT_PLAINTEXT_PWD, supportPlainTextPwd);
a0c34e 267     }
FZ 268
04a985 269     private boolean supportCryptPwd() {
a0c34e 270         return !supportPlaintextPwd();
FZ 271     }
272
273     /*
274      * Method only used for unit tests. Return number of users read from htpasswd file.
275      */
04a985 276     public int getNumberHtpasswdUsers() {
a0c34e 277         return this.htUsers.size();
FZ 278     }
04a985 279
JM 280     @Override
281     public String toString() {
282         return getClass().getSimpleName() + "(" + ((htpasswdFile != null) ? htpasswdFile.getAbsolutePath() : "null") + ")";
283     }
a0c34e 284 }