James Moger
2011-10-17 b2fde8f0dfe2d60b08724e92f919c1f68223101f
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)) {
129             String md5 = StringUtils.MD5_TYPE + StringUtils.getMD5(new String(password));
130             if (user.password.equalsIgnoreCase(md5)) {
131                 returnedUser = user;
dfb889 132             }
5450d0 133         } else if (user.password.equals(new String(password))) {
8c9a20 134             returnedUser = user;
JM 135         }
136         return returnedUser;
fc948c 137     }
dfb889 138
892570 139     /**
JM 140      * Retrieve the user object for the specified username.
141      * 
142      * @param username
143      * @return a user object or null
144      */
dfb889 145     @Override
511554 146     public UserModel getUserModel(String username) {
8c9a20 147         Properties allUsers = read();
JM 148         String userInfo = allUsers.getProperty(username);
149         if (userInfo == null) {
a098da 150             return null;
JM 151         }
152         UserModel model = new UserModel(username);
8c9a20 153         String[] userValues = userInfo.split(",");
JM 154         model.password = userValues[0];
155         for (int i = 1; i < userValues.length; i++) {
156             String role = userValues[i];
157             switch (role.charAt(0)) {
158             case '#':
159                 // Permissions
160                 if (role.equalsIgnoreCase(Constants.ADMIN_ROLE)) {
161                     model.canAdmin = true;
831469 162                 } else if (role.equalsIgnoreCase(Constants.NOT_FEDERATED_ROLE)) {
JM 163                     model.excludeFromFederation = true;
dfb889 164                 }
8c9a20 165                 break;
JM 166             default:
167                 model.addRepository(role);
dfb889 168             }
JM 169         }
170         return model;
171     }
172
892570 173     /**
JM 174      * Updates/writes a complete user object.
175      * 
176      * @param model
177      * @return true if update is successful
178      */
dfb889 179     @Override
511554 180     public boolean updateUserModel(UserModel model) {
2a7306 181         return updateUserModel(model.username, model);
8a2e9c 182     }
2a7306 183
892570 184     /**
JM 185      * Updates/writes and replaces a complete user object keyed by username.
186      * This method allows for renaming a user.
187      * 
188      * @param username
189      *            the old username
190      * @param model
191      *            the user object to use for username
192      * @return true if update is successful
193      */
8a2e9c 194     @Override
JM 195     public boolean updateUserModel(String username, UserModel model) {
dfb889 196         try {
8c9a20 197             Properties allUsers = read();
2a7306 198             ArrayList<String> roles = new ArrayList<String>(model.repositories);
dfb889 199
JM 200             // Permissions
2a7306 201             if (model.canAdmin) {
dfb889 202                 roles.add(Constants.ADMIN_ROLE);
831469 203             }
JM 204             if (model.excludeFromFederation) {
205                 roles.add(Constants.NOT_FEDERATED_ROLE);
dfb889 206             }
JM 207
208             StringBuilder sb = new StringBuilder();
2a7306 209             sb.append(model.password);
dfb889 210             sb.append(',');
JM 211             for (String role : roles) {
212                 sb.append(role);
213                 sb.append(',');
214             }
215             // trim trailing comma
216             sb.setLength(sb.length() - 1);
8a2e9c 217             allUsers.remove(username);
2a7306 218             allUsers.put(model.username, sb.toString());
dfb889 219
8c9a20 220             write(allUsers);
dfb889 221             return true;
JM 222         } catch (Throwable t) {
2a7306 223             logger.error(MessageFormat.format("Failed to update user model {0}!", model.username),
JM 224                     t);
dfb889 225         }
JM 226         return false;
227     }
228
892570 229     /**
JM 230      * Deletes the user object from the user service.
231      * 
232      * @param model
233      * @return true if successful
234      */
dfb889 235     @Override
511554 236     public boolean deleteUserModel(UserModel model) {
2a7306 237         return deleteUser(model.username);
8a2e9c 238     }
JM 239
892570 240     /**
JM 241      * Delete the user object with the specified username
242      * 
243      * @param username
244      * @return true if successful
245      */
8a2e9c 246     @Override
JM 247     public boolean deleteUser(String username) {
dfb889 248         try {
JM 249             // Read realm file
8c9a20 250             Properties allUsers = read();
8a2e9c 251             allUsers.remove(username);
8c9a20 252             write(allUsers);
dfb889 253             return true;
JM 254         } catch (Throwable t) {
8a2e9c 255             logger.error(MessageFormat.format("Failed to delete user {0}!", username), t);
dfb889 256         }
JM 257         return false;
f98825 258     }
2a7306 259
892570 260     /**
JM 261      * Returns the list of all users available to the login service.
262      * 
263      * @return list of all usernames
264      */
f98825 265     @Override
JM 266     public List<String> getAllUsernames() {
8c9a20 267         Properties allUsers = read();
JM 268         List<String> list = new ArrayList<String>(allUsers.stringPropertyNames());
f98825 269         return list;
JM 270     }
271
892570 272     /**
JM 273      * Returns the list of all users who are allowed to bypass the access
274      * restriction placed on the specified repository.
275      * 
276      * @param role
277      *            the repository name
278      * @return list of all usernames that can bypass the access restriction
279      */
f98825 280     @Override
892570 281     public List<String> getUsernamesForRepositoryRole(String role) {
f98825 282         List<String> list = new ArrayList<String>();
JM 283         try {
8c9a20 284             Properties allUsers = read();
f98825 285             for (String username : allUsers.stringPropertyNames()) {
JM 286                 String value = allUsers.getProperty(username);
287                 String[] values = value.split(",");
288                 // skip first value (password)
289                 for (int i = 1; i < values.length; i++) {
290                     String r = values[i];
291                     if (r.equalsIgnoreCase(role)) {
292                         list.add(username);
293                         break;
294                     }
295                 }
296             }
297         } catch (Throwable t) {
298             logger.error(MessageFormat.format("Failed to get usernames for role {0}!", role), t);
299         }
300         return list;
301     }
302
892570 303     /**
JM 304      * Sets the list of all uses who are allowed to bypass the access
305      * restriction placed on the specified repository.
306      * 
307      * @param role
308      *            the repository name
309      * @param usernames
310      * @return true if successful
311      */
f98825 312     @Override
892570 313     public boolean setUsernamesForRepositoryRole(String role, List<String> usernames) {
f98825 314         try {
JM 315             Set<String> specifiedUsers = new HashSet<String>(usernames);
316             Set<String> needsAddRole = new HashSet<String>(specifiedUsers);
317             Set<String> needsRemoveRole = new HashSet<String>();
318
319             // identify users which require add and remove role
8c9a20 320             Properties allUsers = read();
f98825 321             for (String username : allUsers.stringPropertyNames()) {
JM 322                 String value = allUsers.getProperty(username);
323                 String[] values = value.split(",");
324                 // skip first value (password)
325                 for (int i = 1; i < values.length; i++) {
326                     String r = values[i];
327                     if (r.equalsIgnoreCase(role)) {
328                         // user has role, check against revised user list
329                         if (specifiedUsers.contains(username)) {
330                             needsAddRole.remove(username);
331                         } else {
332                             // remove role from user
333                             needsRemoveRole.add(username);
334                         }
335                         break;
336                     }
337                 }
338             }
339
340             // add roles to users
341             for (String user : needsAddRole) {
342                 String userValues = allUsers.getProperty(user);
2a7306 343                 userValues += "," + role;
f98825 344                 allUsers.put(user, userValues);
JM 345             }
346
347             // remove role from user
348             for (String user : needsRemoveRole) {
349                 String[] values = allUsers.getProperty(user).split(",");
350                 String password = values[0];
351                 StringBuilder sb = new StringBuilder();
352                 sb.append(password);
353                 sb.append(',');
354                 List<String> revisedRoles = new ArrayList<String>();
355                 // skip first value (password)
356                 for (int i = 1; i < values.length; i++) {
357                     String value = values[i];
358                     if (!value.equalsIgnoreCase(role)) {
359                         revisedRoles.add(value);
360                         sb.append(value);
361                         sb.append(',');
362                     }
363                 }
364                 sb.setLength(sb.length() - 1);
365
366                 // update properties
367                 allUsers.put(user, sb.toString());
368             }
369
370             // persist changes
8c9a20 371             write(allUsers);
f98825 372             return true;
JM 373         } catch (Throwable t) {
374             logger.error(MessageFormat.format("Failed to set usernames for role {0}!", role), t);
375         }
376         return false;
377     }
378
892570 379     /**
JM 380      * Renames a repository role.
381      * 
382      * @param oldRole
383      * @param newRole
384      * @return true if successful
385      */
f98825 386     @Override
85c2e6 387     public boolean renameRepositoryRole(String oldRole, String newRole) {
f98825 388         try {
8c9a20 389             Properties allUsers = read();
f98825 390             Set<String> needsRenameRole = new HashSet<String>();
JM 391
392             // identify users which require role rename
393             for (String username : allUsers.stringPropertyNames()) {
394                 String value = allUsers.getProperty(username);
395                 String[] roles = value.split(",");
396                 // skip first value (password)
397                 for (int i = 1; i < roles.length; i++) {
398                     String r = roles[i];
399                     if (r.equalsIgnoreCase(oldRole)) {
400                         needsRenameRole.remove(username);
401                         break;
402                     }
403                 }
404             }
405
406             // rename role for identified users
407             for (String user : needsRenameRole) {
408                 String userValues = allUsers.getProperty(user);
409                 String[] values = userValues.split(",");
410                 String password = values[0];
411                 StringBuilder sb = new StringBuilder();
412                 sb.append(password);
413                 sb.append(',');
414                 List<String> revisedRoles = new ArrayList<String>();
415                 revisedRoles.add(newRole);
416                 // skip first value (password)
417                 for (int i = 1; i < values.length; i++) {
418                     String value = values[i];
419                     if (!value.equalsIgnoreCase(oldRole)) {
420                         revisedRoles.add(value);
421                         sb.append(value);
422                         sb.append(',');
423                     }
424                 }
425                 sb.setLength(sb.length() - 1);
426
427                 // update properties
428                 allUsers.put(user, sb.toString());
429             }
430
431             // persist changes
8c9a20 432             write(allUsers);
f98825 433             return true;
JM 434         } catch (Throwable t) {
2a7306 435             logger.error(
JM 436                     MessageFormat.format("Failed to rename role {0} to {1}!", oldRole, newRole), t);
f98825 437         }
JM 438         return false;
439     }
440
892570 441     /**
JM 442      * Removes a repository role from all users.
443      * 
444      * @param role
445      * @return true if successful
446      */
f98825 447     @Override
85c2e6 448     public boolean deleteRepositoryRole(String role) {
f98825 449         try {
8c9a20 450             Properties allUsers = read();
f98825 451             Set<String> needsDeleteRole = new HashSet<String>();
JM 452
453             // identify users which require role rename
454             for (String username : allUsers.stringPropertyNames()) {
455                 String value = allUsers.getProperty(username);
456                 String[] roles = value.split(",");
457                 // skip first value (password)
458                 for (int i = 1; i < roles.length; i++) {
459                     String r = roles[i];
460                     if (r.equalsIgnoreCase(role)) {
461                         needsDeleteRole.remove(username);
462                         break;
463                     }
464                 }
465             }
466
467             // delete role for identified users
468             for (String user : needsDeleteRole) {
469                 String userValues = allUsers.getProperty(user);
470                 String[] values = userValues.split(",");
471                 String password = values[0];
472                 StringBuilder sb = new StringBuilder();
473                 sb.append(password);
474                 sb.append(',');
475                 List<String> revisedRoles = new ArrayList<String>();
476                 // skip first value (password)
477                 for (int i = 1; i < values.length; i++) {
478                     String value = values[i];
479                     if (!value.equalsIgnoreCase(role)) {
480                         revisedRoles.add(value);
481                         sb.append(value);
482                         sb.append(',');
483                     }
484                 }
485                 sb.setLength(sb.length() - 1);
486
487                 // update properties
488                 allUsers.put(user, sb.toString());
489             }
490
491             // persist changes
8c9a20 492             write(allUsers);
8a2e9c 493             return true;
f98825 494         } catch (Throwable t) {
JM 495             logger.error(MessageFormat.format("Failed to delete role {0}!", role), t);
496         }
497         return false;
498     }
499
892570 500     /**
JM 501      * Writes the properties file.
502      * 
503      * @param properties
504      * @throws IOException
505      */
8c9a20 506     private void write(Properties properties) throws IOException {
892570 507         // Write a temporary copy of the users file
8c9a20 508         File realmFileCopy = new File(propertiesFile.getAbsolutePath() + ".tmp");
f98825 509         FileWriter writer = new FileWriter(realmFileCopy);
2a7306 510         properties
JM 511                 .store(writer,
c22722 512                         "# Gitblit realm file format: username=password,\\#permission,repository1,repository2...");
f98825 513         writer.close();
892570 514         // If the write is successful, delete the current file and rename
JM 515         // the temporary copy to the original filename.
f98825 516         if (realmFileCopy.exists() && realmFileCopy.length() > 0) {
831469 517             if (propertiesFile.exists()) {
JM 518                 if (!propertiesFile.delete()) {
519                     throw new IOException(MessageFormat.format("Failed to delete {0}!",
520                             propertiesFile.getAbsolutePath()));
2a7306 521                 }
831469 522             }
JM 523             if (!realmFileCopy.renameTo(propertiesFile)) {
524                 throw new IOException(MessageFormat.format("Failed to rename {0} to {1}!",
525                         realmFileCopy.getAbsolutePath(), propertiesFile.getAbsolutePath()));
2a7306 526             }
f98825 527         } else {
2a7306 528             throw new IOException(MessageFormat.format("Failed to save {0}!",
JM 529                     realmFileCopy.getAbsolutePath()));
f98825 530         }
dfb889 531     }
85c2e6 532
892570 533     /**
JM 534      * Reads the properties file and rebuilds the in-memory cookie lookup table.
535      */
85c2e6 536     @Override
JM 537     protected synchronized Properties read() {
892570 538         long lastRead = lastModified();
85c2e6 539         Properties allUsers = super.read();
892570 540         if (lastRead != lastModified()) {
85c2e6 541             // reload hash cache
JM 542             cookies.clear();
543             for (String username : allUsers.stringPropertyNames()) {
544                 String value = allUsers.getProperty(username);
545                 String[] roles = value.split(",");
546                 String password = roles[0];
547                 cookies.put(StringUtils.getSHA1(username + password), username);
548             }
549         }
550         return allUsers;
551     }
fc948c 552 }