James Moger
2011-10-24 4c837a1de9f7706c7bfb0cbb14a7082f916826ae
commit | author | age
f13c4c 1 /*
JM 2  * Copyright 2011 gitblit.com.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
fc948c 16 package com.gitblit;
JM 17
dfb889 18 import java.io.File;
JM 19 import java.io.FileWriter;
20 import java.io.IOException;
f98825 21 import java.text.MessageFormat;
dfb889 22 import java.util.ArrayList;
f98825 23 import java.util.HashSet;
JM 24 import java.util.List;
85c2e6 25 import java.util.Map;
dfb889 26 import java.util.Properties;
f98825 27 import java.util.Set;
85c2e6 28 import java.util.concurrent.ConcurrentHashMap;
dfb889 29
f98825 30 import org.slf4j.Logger;
JM 31 import org.slf4j.LoggerFactory;
fc948c 32
1f9dae 33 import com.gitblit.models.UserModel;
8c9a20 34 import com.gitblit.utils.StringUtils;
fc948c 35
892570 36 /**
JM 37  * FileUserService is Gitblit's default user service implementation.
38  * 
39  * Users and their repository memberships are stored in a simple properties file
40  * which is cached and dynamically reloaded when modified.
41  * 
42  * @author James Moger
43  * 
44  */
85c2e6 45 public class FileUserService extends FileSettings implements IUserService {
f98825 46
85c2e6 47     private final Logger logger = LoggerFactory.getLogger(FileUserService.class);
fc948c 48
85c2e6 49     private final Map<String, String> cookies = new ConcurrentHashMap<String, String>();
JM 50
51     public FileUserService(File realmFile) {
8c9a20 52         super(realmFile.getAbsolutePath());
85c2e6 53     }
JM 54
892570 55     /**
63ee41 56      * Setup the user service.
JM 57      * 
58      * @param settings
59      * @since 0.6.1
60      */
61     @Override
62     public void setup(IStoredSettings settings) {
63     }
64
65     /**
892570 66      * Does the user service support cookie authentication?
JM 67      * 
68      * @return true or false
69      */
85c2e6 70     @Override
JM 71     public boolean supportsCookies() {
72         return true;
73     }
74
892570 75     /**
JM 76      * Returns the cookie value for the specified user.
77      * 
78      * @param model
79      * @return cookie value
80      */
85c2e6 81     @Override
JM 82     public char[] getCookie(UserModel model) {
83         Properties allUsers = super.read();
84         String value = allUsers.getProperty(model.username);
85         String[] roles = value.split(",");
86         String password = roles[0];
87         String cookie = StringUtils.getSHA1(model.username + password);
88         return cookie.toCharArray();
89     }
90
892570 91     /**
JM 92      * Authenticate a user based on their cookie.
93      * 
94      * @param cookie
95      * @return a user object or null
96      */
85c2e6 97     @Override
JM 98     public UserModel authenticate(char[] cookie) {
99         String hash = new String(cookie);
100         if (StringUtils.isEmpty(hash)) {
101             return null;
102         }
103         read();
104         UserModel model = null;
105         if (cookies.containsKey(hash)) {
106             String username = cookies.get(hash);
107             model = getUserModel(username);
108         }
109         return model;
fc948c 110     }
155bf7 111
892570 112     /**
JM 113      * Authenticate a user based on a username and password.
114      * 
115      * @param username
116      * @param password
117      * @return a user object or null
118      */
fc948c 119     @Override
511554 120     public UserModel authenticate(String username, char[] password) {
8c9a20 121         Properties allUsers = read();
JM 122         String userInfo = allUsers.getProperty(username);
123         if (StringUtils.isEmpty(userInfo)) {
fc948c 124             return null;
JM 125         }
8c9a20 126         UserModel returnedUser = null;
JM 127         UserModel user = getUserModel(username);
128         if (user.password.startsWith(StringUtils.MD5_TYPE)) {
d5623a 129             // password digest
8c9a20 130             String md5 = StringUtils.MD5_TYPE + StringUtils.getMD5(new String(password));
JM 131             if (user.password.equalsIgnoreCase(md5)) {
132                 returnedUser = user;
dfb889 133             }
d5623a 134         } else if (user.password.startsWith(StringUtils.COMBINED_MD5_TYPE)) {
JM 135             // username+password digest
136             String md5 = StringUtils.COMBINED_MD5_TYPE
137                     + StringUtils.getMD5(username.toLowerCase() + new String(password));
138             if (user.password.equalsIgnoreCase(md5)) {
139                 returnedUser = user;
140             }
5450d0 141         } else if (user.password.equals(new String(password))) {
d5623a 142             // plain-text password
8c9a20 143             returnedUser = user;
JM 144         }
145         return returnedUser;
fc948c 146     }
dfb889 147
892570 148     /**
JM 149      * Retrieve the user object for the specified username.
150      * 
151      * @param username
152      * @return a user object or null
153      */
dfb889 154     @Override
511554 155     public UserModel getUserModel(String username) {
8c9a20 156         Properties allUsers = read();
JM 157         String userInfo = allUsers.getProperty(username);
158         if (userInfo == null) {
a098da 159             return null;
JM 160         }
161         UserModel model = new UserModel(username);
8c9a20 162         String[] userValues = userInfo.split(",");
JM 163         model.password = userValues[0];
164         for (int i = 1; i < userValues.length; i++) {
165             String role = userValues[i];
166             switch (role.charAt(0)) {
167             case '#':
168                 // Permissions
169                 if (role.equalsIgnoreCase(Constants.ADMIN_ROLE)) {
170                     model.canAdmin = true;
831469 171                 } else if (role.equalsIgnoreCase(Constants.NOT_FEDERATED_ROLE)) {
JM 172                     model.excludeFromFederation = true;
dfb889 173                 }
8c9a20 174                 break;
JM 175             default:
176                 model.addRepository(role);
dfb889 177             }
JM 178         }
179         return model;
180     }
181
892570 182     /**
JM 183      * Updates/writes a complete user object.
184      * 
185      * @param model
186      * @return true if update is successful
187      */
dfb889 188     @Override
511554 189     public boolean updateUserModel(UserModel model) {
2a7306 190         return updateUserModel(model.username, model);
8a2e9c 191     }
2a7306 192
892570 193     /**
JM 194      * Updates/writes and replaces a complete user object keyed by username.
195      * This method allows for renaming a user.
196      * 
197      * @param username
198      *            the old username
199      * @param model
200      *            the user object to use for username
201      * @return true if update is successful
202      */
8a2e9c 203     @Override
JM 204     public boolean updateUserModel(String username, UserModel model) {
dfb889 205         try {
8c9a20 206             Properties allUsers = read();
2a7306 207             ArrayList<String> roles = new ArrayList<String>(model.repositories);
dfb889 208
JM 209             // Permissions
2a7306 210             if (model.canAdmin) {
dfb889 211                 roles.add(Constants.ADMIN_ROLE);
831469 212             }
JM 213             if (model.excludeFromFederation) {
214                 roles.add(Constants.NOT_FEDERATED_ROLE);
dfb889 215             }
JM 216
217             StringBuilder sb = new StringBuilder();
2a7306 218             sb.append(model.password);
dfb889 219             sb.append(',');
JM 220             for (String role : roles) {
221                 sb.append(role);
222                 sb.append(',');
223             }
224             // trim trailing comma
225             sb.setLength(sb.length() - 1);
8a2e9c 226             allUsers.remove(username);
2a7306 227             allUsers.put(model.username, sb.toString());
dfb889 228
8c9a20 229             write(allUsers);
dfb889 230             return true;
JM 231         } catch (Throwable t) {
2a7306 232             logger.error(MessageFormat.format("Failed to update user model {0}!", model.username),
JM 233                     t);
dfb889 234         }
JM 235         return false;
236     }
237
892570 238     /**
JM 239      * Deletes the user object from the user service.
240      * 
241      * @param model
242      * @return true if successful
243      */
dfb889 244     @Override
511554 245     public boolean deleteUserModel(UserModel model) {
2a7306 246         return deleteUser(model.username);
8a2e9c 247     }
JM 248
892570 249     /**
JM 250      * Delete the user object with the specified username
251      * 
252      * @param username
253      * @return true if successful
254      */
8a2e9c 255     @Override
JM 256     public boolean deleteUser(String username) {
dfb889 257         try {
JM 258             // Read realm file
8c9a20 259             Properties allUsers = read();
8a2e9c 260             allUsers.remove(username);
8c9a20 261             write(allUsers);
dfb889 262             return true;
JM 263         } catch (Throwable t) {
8a2e9c 264             logger.error(MessageFormat.format("Failed to delete user {0}!", username), t);
dfb889 265         }
JM 266         return false;
f98825 267     }
2a7306 268
892570 269     /**
JM 270      * Returns the list of all users available to the login service.
271      * 
272      * @return list of all usernames
273      */
f98825 274     @Override
JM 275     public List<String> getAllUsernames() {
8c9a20 276         Properties allUsers = read();
JM 277         List<String> list = new ArrayList<String>(allUsers.stringPropertyNames());
f98825 278         return list;
JM 279     }
280
892570 281     /**
JM 282      * Returns the list of all users who are allowed to bypass the access
283      * restriction placed on the specified repository.
284      * 
285      * @param role
286      *            the repository name
287      * @return list of all usernames that can bypass the access restriction
288      */
f98825 289     @Override
892570 290     public List<String> getUsernamesForRepositoryRole(String role) {
f98825 291         List<String> list = new ArrayList<String>();
JM 292         try {
8c9a20 293             Properties allUsers = read();
f98825 294             for (String username : allUsers.stringPropertyNames()) {
JM 295                 String value = allUsers.getProperty(username);
296                 String[] values = value.split(",");
297                 // skip first value (password)
298                 for (int i = 1; i < values.length; i++) {
299                     String r = values[i];
300                     if (r.equalsIgnoreCase(role)) {
301                         list.add(username);
302                         break;
303                     }
304                 }
305             }
306         } catch (Throwable t) {
307             logger.error(MessageFormat.format("Failed to get usernames for role {0}!", role), t);
308         }
309         return list;
310     }
311
892570 312     /**
JM 313      * Sets the list of all uses who are allowed to bypass the access
314      * restriction placed on the specified repository.
315      * 
316      * @param role
317      *            the repository name
318      * @param usernames
319      * @return true if successful
320      */
f98825 321     @Override
892570 322     public boolean setUsernamesForRepositoryRole(String role, List<String> usernames) {
f98825 323         try {
JM 324             Set<String> specifiedUsers = new HashSet<String>(usernames);
325             Set<String> needsAddRole = new HashSet<String>(specifiedUsers);
326             Set<String> needsRemoveRole = new HashSet<String>();
327
328             // identify users which require add and remove role
8c9a20 329             Properties allUsers = read();
f98825 330             for (String username : allUsers.stringPropertyNames()) {
JM 331                 String value = allUsers.getProperty(username);
332                 String[] values = value.split(",");
333                 // skip first value (password)
334                 for (int i = 1; i < values.length; i++) {
335                     String r = values[i];
336                     if (r.equalsIgnoreCase(role)) {
337                         // user has role, check against revised user list
338                         if (specifiedUsers.contains(username)) {
339                             needsAddRole.remove(username);
340                         } else {
341                             // remove role from user
342                             needsRemoveRole.add(username);
343                         }
344                         break;
345                     }
346                 }
347             }
348
349             // add roles to users
350             for (String user : needsAddRole) {
351                 String userValues = allUsers.getProperty(user);
2a7306 352                 userValues += "," + role;
f98825 353                 allUsers.put(user, userValues);
JM 354             }
355
356             // remove role from user
357             for (String user : needsRemoveRole) {
358                 String[] values = allUsers.getProperty(user).split(",");
359                 String password = values[0];
360                 StringBuilder sb = new StringBuilder();
361                 sb.append(password);
362                 sb.append(',');
363                 List<String> revisedRoles = new ArrayList<String>();
364                 // skip first value (password)
365                 for (int i = 1; i < values.length; i++) {
366                     String value = values[i];
367                     if (!value.equalsIgnoreCase(role)) {
368                         revisedRoles.add(value);
369                         sb.append(value);
370                         sb.append(',');
371                     }
372                 }
373                 sb.setLength(sb.length() - 1);
374
375                 // update properties
376                 allUsers.put(user, sb.toString());
377             }
378
379             // persist changes
8c9a20 380             write(allUsers);
f98825 381             return true;
JM 382         } catch (Throwable t) {
383             logger.error(MessageFormat.format("Failed to set usernames for role {0}!", role), t);
384         }
385         return false;
386     }
387
892570 388     /**
JM 389      * Renames a repository role.
390      * 
391      * @param oldRole
392      * @param newRole
393      * @return true if successful
394      */
f98825 395     @Override
85c2e6 396     public boolean renameRepositoryRole(String oldRole, String newRole) {
f98825 397         try {
8c9a20 398             Properties allUsers = read();
f98825 399             Set<String> needsRenameRole = new HashSet<String>();
JM 400
401             // identify users which require role rename
402             for (String username : allUsers.stringPropertyNames()) {
403                 String value = allUsers.getProperty(username);
404                 String[] roles = value.split(",");
405                 // skip first value (password)
406                 for (int i = 1; i < roles.length; i++) {
407                     String r = roles[i];
408                     if (r.equalsIgnoreCase(oldRole)) {
409                         needsRenameRole.remove(username);
410                         break;
411                     }
412                 }
413             }
414
415             // rename role for identified users
416             for (String user : needsRenameRole) {
417                 String userValues = allUsers.getProperty(user);
418                 String[] values = userValues.split(",");
419                 String password = values[0];
420                 StringBuilder sb = new StringBuilder();
421                 sb.append(password);
422                 sb.append(',');
423                 List<String> revisedRoles = new ArrayList<String>();
424                 revisedRoles.add(newRole);
425                 // skip first value (password)
426                 for (int i = 1; i < values.length; i++) {
427                     String value = values[i];
428                     if (!value.equalsIgnoreCase(oldRole)) {
429                         revisedRoles.add(value);
430                         sb.append(value);
431                         sb.append(',');
432                     }
433                 }
434                 sb.setLength(sb.length() - 1);
435
436                 // update properties
437                 allUsers.put(user, sb.toString());
438             }
439
440             // persist changes
8c9a20 441             write(allUsers);
f98825 442             return true;
JM 443         } catch (Throwable t) {
2a7306 444             logger.error(
JM 445                     MessageFormat.format("Failed to rename role {0} to {1}!", oldRole, newRole), t);
f98825 446         }
JM 447         return false;
448     }
449
892570 450     /**
JM 451      * Removes a repository role from all users.
452      * 
453      * @param role
454      * @return true if successful
455      */
f98825 456     @Override
85c2e6 457     public boolean deleteRepositoryRole(String role) {
f98825 458         try {
8c9a20 459             Properties allUsers = read();
f98825 460             Set<String> needsDeleteRole = new HashSet<String>();
JM 461
462             // identify users which require role rename
463             for (String username : allUsers.stringPropertyNames()) {
464                 String value = allUsers.getProperty(username);
465                 String[] roles = value.split(",");
466                 // skip first value (password)
467                 for (int i = 1; i < roles.length; i++) {
468                     String r = roles[i];
469                     if (r.equalsIgnoreCase(role)) {
470                         needsDeleteRole.remove(username);
471                         break;
472                     }
473                 }
474             }
475
476             // delete role for identified users
477             for (String user : needsDeleteRole) {
478                 String userValues = allUsers.getProperty(user);
479                 String[] values = userValues.split(",");
480                 String password = values[0];
481                 StringBuilder sb = new StringBuilder();
482                 sb.append(password);
483                 sb.append(',');
484                 List<String> revisedRoles = new ArrayList<String>();
485                 // skip first value (password)
486                 for (int i = 1; i < values.length; i++) {
487                     String value = values[i];
488                     if (!value.equalsIgnoreCase(role)) {
489                         revisedRoles.add(value);
490                         sb.append(value);
491                         sb.append(',');
492                     }
493                 }
494                 sb.setLength(sb.length() - 1);
495
496                 // update properties
497                 allUsers.put(user, sb.toString());
498             }
499
500             // persist changes
8c9a20 501             write(allUsers);
8a2e9c 502             return true;
f98825 503         } catch (Throwable t) {
JM 504             logger.error(MessageFormat.format("Failed to delete role {0}!", role), t);
505         }
506         return false;
507     }
508
892570 509     /**
JM 510      * Writes the properties file.
511      * 
512      * @param properties
513      * @throws IOException
514      */
8c9a20 515     private void write(Properties properties) throws IOException {
892570 516         // Write a temporary copy of the users file
8c9a20 517         File realmFileCopy = new File(propertiesFile.getAbsolutePath() + ".tmp");
f98825 518         FileWriter writer = new FileWriter(realmFileCopy);
2a7306 519         properties
JM 520                 .store(writer,
c22722 521                         "# Gitblit realm file format: username=password,\\#permission,repository1,repository2...");
f98825 522         writer.close();
892570 523         // If the write is successful, delete the current file and rename
JM 524         // the temporary copy to the original filename.
f98825 525         if (realmFileCopy.exists() && realmFileCopy.length() > 0) {
831469 526             if (propertiesFile.exists()) {
JM 527                 if (!propertiesFile.delete()) {
528                     throw new IOException(MessageFormat.format("Failed to delete {0}!",
529                             propertiesFile.getAbsolutePath()));
2a7306 530                 }
831469 531             }
JM 532             if (!realmFileCopy.renameTo(propertiesFile)) {
533                 throw new IOException(MessageFormat.format("Failed to rename {0} to {1}!",
534                         realmFileCopy.getAbsolutePath(), propertiesFile.getAbsolutePath()));
2a7306 535             }
f98825 536         } else {
2a7306 537             throw new IOException(MessageFormat.format("Failed to save {0}!",
JM 538                     realmFileCopy.getAbsolutePath()));
f98825 539         }
dfb889 540     }
85c2e6 541
892570 542     /**
JM 543      * Reads the properties file and rebuilds the in-memory cookie lookup table.
544      */
85c2e6 545     @Override
JM 546     protected synchronized Properties read() {
892570 547         long lastRead = lastModified();
85c2e6 548         Properties allUsers = super.read();
892570 549         if (lastRead != lastModified()) {
85c2e6 550             // reload hash cache
JM 551             cookies.clear();
552             for (String username : allUsers.stringPropertyNames()) {
553                 String value = allUsers.getProperty(username);
554                 String[] roles = value.split(",");
555                 String password = roles[0];
556                 cookies.put(StringUtils.getSHA1(username + password), username);
557             }
558         }
559         return allUsers;
560     }
fc948c 561 }