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 |
} |