James Moger
2012-10-31 644bdd5a59a5ed5fbf93a0765f92608b0530c16a
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;
d7905a 23 import java.util.Collections;
f98825 24 import java.util.HashSet;
JM 25 import java.util.List;
85c2e6 26 import java.util.Map;
dfb889 27 import java.util.Properties;
f98825 28 import java.util.Set;
85c2e6 29 import java.util.concurrent.ConcurrentHashMap;
dfb889 30
f98825 31 import org.slf4j.Logger;
JM 32 import org.slf4j.LoggerFactory;
fc948c 33
20714a 34 import com.gitblit.Constants.AccessPermission;
fe24a0 35 import com.gitblit.models.TeamModel;
1f9dae 36 import com.gitblit.models.UserModel;
0db5c4 37 import com.gitblit.utils.ArrayUtils;
fe24a0 38 import com.gitblit.utils.DeepCopier;
8c9a20 39 import com.gitblit.utils.StringUtils;
fc948c 40
892570 41 /**
93f472 42  * FileUserService is Gitblit's original default user service implementation.
892570 43  * 
JM 44  * Users and their repository memberships are stored in a simple properties file
45  * which is cached and dynamically reloaded when modified.
46  * 
93f472 47  * This class was deprecated in Gitblit 0.8.0 in favor of ConfigUserService
JM 48  * which is still a human-readable, editable, plain-text file but it is more
49  * flexible for storing additional fields.
50  * 
892570 51  * @author James Moger
JM 52  * 
53  */
93f472 54 @Deprecated
85c2e6 55 public class FileUserService extends FileSettings implements IUserService {
f98825 56
85c2e6 57     private final Logger logger = LoggerFactory.getLogger(FileUserService.class);
fc948c 58
85c2e6 59     private final Map<String, String> cookies = new ConcurrentHashMap<String, String>();
JM 60
fe24a0 61     private final Map<String, TeamModel> teams = new ConcurrentHashMap<String, TeamModel>();
JM 62
85c2e6 63     public FileUserService(File realmFile) {
8c9a20 64         super(realmFile.getAbsolutePath());
85c2e6 65     }
JM 66
892570 67     /**
63ee41 68      * Setup the user service.
JM 69      * 
70      * @param settings
fe24a0 71      * @since 0.7.0
63ee41 72      */
JM 73     @Override
74     public void setup(IStoredSettings settings) {
75     }
76
77     /**
6cca86 78      * Does the user service support changes to credentials?
JM 79      * 
80      * @return true or false
81      * @since 1.0.0
82      */
83     @Override
84     public boolean supportsCredentialChanges() {
85         return true;
86     }
87
88     /**
0aa8cf 89      * Does the user service support changes to user display name?
JM 90      * 
91      * @return true or false
92      * @since 1.0.0
93      */
94     @Override
95     public boolean supportsDisplayNameChanges() {
96         return false;
97     }
98
99     /**
100      * Does the user service support changes to user email address?
101      * 
102      * @return true or false
103      * @since 1.0.0
104      */
105     @Override
106     public boolean supportsEmailAddressChanges() {
107         return false;
108     }
109
110     /**
6cca86 111      * Does the user service support changes to team memberships?
JM 112      * 
113      * @return true or false
114      * @since 1.0.0
115      */    
116     public boolean supportsTeamMembershipChanges() {
117         return true;
118     }
119
120     /**
892570 121      * Does the user service support cookie authentication?
JM 122      * 
123      * @return true or false
124      */
85c2e6 125     @Override
JM 126     public boolean supportsCookies() {
127         return true;
128     }
129
892570 130     /**
JM 131      * Returns the cookie value for the specified user.
132      * 
133      * @param model
134      * @return cookie value
135      */
85c2e6 136     @Override
62aeb9 137     public String getCookie(UserModel model) {
JM 138         if (!StringUtils.isEmpty(model.cookie)) {
139             return model.cookie;
140         }
85c2e6 141         Properties allUsers = super.read();
JM 142         String value = allUsers.getProperty(model.username);
143         String[] roles = value.split(",");
144         String password = roles[0];
145         String cookie = StringUtils.getSHA1(model.username + password);
62aeb9 146         return cookie;
85c2e6 147     }
JM 148
892570 149     /**
JM 150      * Authenticate a user based on their cookie.
151      * 
152      * @param cookie
153      * @return a user object or null
154      */
85c2e6 155     @Override
JM 156     public UserModel authenticate(char[] cookie) {
157         String hash = new String(cookie);
158         if (StringUtils.isEmpty(hash)) {
159             return null;
160         }
161         read();
162         UserModel model = null;
163         if (cookies.containsKey(hash)) {
164             String username = cookies.get(hash);
165             model = getUserModel(username);
166         }
167         return model;
fc948c 168     }
155bf7 169
892570 170     /**
JM 171      * Authenticate a user based on a username and password.
172      * 
173      * @param username
174      * @param password
175      * @return a user object or null
176      */
fc948c 177     @Override
511554 178     public UserModel authenticate(String username, char[] password) {
8c9a20 179         Properties allUsers = read();
JM 180         String userInfo = allUsers.getProperty(username);
181         if (StringUtils.isEmpty(userInfo)) {
fc948c 182             return null;
JM 183         }
8c9a20 184         UserModel returnedUser = null;
JM 185         UserModel user = getUserModel(username);
186         if (user.password.startsWith(StringUtils.MD5_TYPE)) {
d5623a 187             // password digest
8c9a20 188             String md5 = StringUtils.MD5_TYPE + StringUtils.getMD5(new String(password));
JM 189             if (user.password.equalsIgnoreCase(md5)) {
190                 returnedUser = user;
dfb889 191             }
d5623a 192         } else if (user.password.startsWith(StringUtils.COMBINED_MD5_TYPE)) {
JM 193             // username+password digest
194             String md5 = StringUtils.COMBINED_MD5_TYPE
195                     + StringUtils.getMD5(username.toLowerCase() + new String(password));
196             if (user.password.equalsIgnoreCase(md5)) {
197                 returnedUser = user;
198             }
5450d0 199         } else if (user.password.equals(new String(password))) {
d5623a 200             // plain-text password
8c9a20 201             returnedUser = user;
JM 202         }
203         return returnedUser;
fc948c 204     }
dfb889 205
892570 206     /**
ea094a 207      * Logout a user.
JM 208      * 
209      * @param user
210      */
211     @Override
212     public void logout(UserModel user) {    
213     }
214
215     /**
892570 216      * Retrieve the user object for the specified username.
JM 217      * 
218      * @param username
219      * @return a user object or null
220      */
dfb889 221     @Override
511554 222     public UserModel getUserModel(String username) {
8c9a20 223         Properties allUsers = read();
ae0b13 224         String userInfo = allUsers.getProperty(username.toLowerCase());
8c9a20 225         if (userInfo == null) {
a098da 226             return null;
JM 227         }
ae0b13 228         UserModel model = new UserModel(username.toLowerCase());
8c9a20 229         String[] userValues = userInfo.split(",");
JM 230         model.password = userValues[0];
231         for (int i = 1; i < userValues.length; i++) {
232             String role = userValues[i];
233             switch (role.charAt(0)) {
234             case '#':
235                 // Permissions
236                 if (role.equalsIgnoreCase(Constants.ADMIN_ROLE)) {
237                     model.canAdmin = true;
1e1b85 238                 } else if (role.equalsIgnoreCase(Constants.FORK_ROLE)) {
JM 239                     model.canFork = true;
6662e3 240                 } else if (role.equalsIgnoreCase(Constants.CREATE_ROLE)) {
JM 241                     model.canCreate = true;
831469 242                 } else if (role.equalsIgnoreCase(Constants.NOT_FEDERATED_ROLE)) {
JM 243                     model.excludeFromFederation = true;
dfb889 244                 }
8c9a20 245                 break;
JM 246             default:
20714a 247                 model.addRepositoryPermission(role);
dfb889 248             }
JM 249         }
fe24a0 250         // set the teams for the user
JM 251         for (TeamModel team : teams.values()) {
252             if (team.hasUser(username)) {
253                 model.teams.add(DeepCopier.copy(team));
254             }
255         }
dfb889 256         return model;
JM 257     }
258
892570 259     /**
JM 260      * Updates/writes a complete user object.
261      * 
262      * @param model
263      * @return true if update is successful
264      */
dfb889 265     @Override
511554 266     public boolean updateUserModel(UserModel model) {
2a7306 267         return updateUserModel(model.username, model);
8a2e9c 268     }
2a7306 269
892570 270     /**
20714a 271      * Updates/writes all specified user objects.
JM 272      * 
273      * @param model a list of user models
274      * @return true if update is successful
275      * @since 1.2.0
276      */
277     @Override
278     public boolean updateUserModels(List<UserModel> models) {
279         try {            
280             Properties allUsers = read();
281             for (UserModel model : models) {
282                 updateUserCache(allUsers, model.username, model);
283             }
284             write(allUsers);
285             return true;
286         } catch (Throwable t) {
287             logger.error(MessageFormat.format("Failed to update {0} user models!", models.size()),
288                     t);
289         }
290         return false;
291     }
292
293     /**
892570 294      * Updates/writes and replaces a complete user object keyed by username.
JM 295      * This method allows for renaming a user.
296      * 
297      * @param username
298      *            the old username
299      * @param model
300      *            the user object to use for username
301      * @return true if update is successful
302      */
8a2e9c 303     @Override
JM 304     public boolean updateUserModel(String username, UserModel model) {
ae0b13 305         try {            
8c9a20 306             Properties allUsers = read();
20714a 307             updateUserCache(allUsers, username, model);
JM 308             write(allUsers);
309             return true;
310         } catch (Throwable t) {
311             logger.error(MessageFormat.format("Failed to update user model {0}!", model.username),
312                     t);
313         }
314         return false;
315     }
316     
317     /**
318      * Updates/writes and replaces a complete user object keyed by username.
319      * This method allows for renaming a user.
320      * 
321      * @param username
322      *            the old username
323      * @param model
324      *            the user object to use for username
325      * @return true if update is successful
326      */
327     private boolean updateUserCache(Properties allUsers, String username, UserModel model) {
328         try {            
fe24a0 329             UserModel oldUser = getUserModel(username);
20714a 330             List<String> roles;
JM 331             if (model.permissions == null) {
822dfe 332                 roles = new ArrayList<String>();
20714a 333             } else {
JM 334                 // discrete repository permissions
335                 roles = new ArrayList<String>();
336                 for (Map.Entry<String, AccessPermission> entry : model.permissions.entrySet()) {
337                     if (entry.getValue().exceeds(AccessPermission.NONE)) {
338                         // code:repository (e.g. RW+:~james/myrepo.git
339                         roles.add(entry.getValue().asRole(entry.getKey()));
340                     }
341                 }
342             }
dfb889 343
JM 344             // Permissions
2a7306 345             if (model.canAdmin) {
dfb889 346                 roles.add(Constants.ADMIN_ROLE);
831469 347             }
1e1b85 348             if (model.canFork) {
JM 349                 roles.add(Constants.FORK_ROLE);
350             }
6662e3 351             if (model.canCreate) {
JM 352                 roles.add(Constants.CREATE_ROLE);
353             }
831469 354             if (model.excludeFromFederation) {
JM 355                 roles.add(Constants.NOT_FEDERATED_ROLE);
dfb889 356             }
JM 357
358             StringBuilder sb = new StringBuilder();
6cca86 359             if (!StringUtils.isEmpty(model.password)) {
JM 360                 sb.append(model.password);
361             }
dfb889 362             sb.append(',');
JM 363             for (String role : roles) {
364                 sb.append(role);
365                 sb.append(',');
366             }
367             // trim trailing comma
368             sb.setLength(sb.length() - 1);
ae0b13 369             allUsers.remove(username.toLowerCase());
JM 370             allUsers.put(model.username.toLowerCase(), sb.toString());
fe24a0 371
JM 372             // null check on "final" teams because JSON-sourced UserModel
373             // can have a null teams object
374             if (model.teams != null) {
375                 // update team cache
376                 for (TeamModel team : model.teams) {
377                     TeamModel t = getTeamModel(team.name);
378                     if (t == null) {
379                         // new team
380                         t = team;
381                     }
382                     t.removeUser(username);
383                     t.addUser(model.username);
384                     updateTeamCache(allUsers, t.name, t);
385                 }
386
387                 // check for implicit team removal
388                 if (oldUser != null) {
389                     for (TeamModel team : oldUser.teams) {
390                         if (!model.isTeamMember(team.name)) {
391                             team.removeUser(username);
392                             updateTeamCache(allUsers, team.name, team);
393                         }
394                     }
395                 }
396             }
dfb889 397             return true;
JM 398         } catch (Throwable t) {
2a7306 399             logger.error(MessageFormat.format("Failed to update user model {0}!", model.username),
JM 400                     t);
dfb889 401         }
JM 402         return false;
403     }
404
892570 405     /**
JM 406      * Deletes the user object from the user service.
407      * 
408      * @param model
409      * @return true if successful
410      */
dfb889 411     @Override
511554 412     public boolean deleteUserModel(UserModel model) {
2a7306 413         return deleteUser(model.username);
8a2e9c 414     }
JM 415
892570 416     /**
JM 417      * Delete the user object with the specified username
418      * 
419      * @param username
420      * @return true if successful
421      */
8a2e9c 422     @Override
JM 423     public boolean deleteUser(String username) {
dfb889 424         try {
JM 425             // Read realm file
8c9a20 426             Properties allUsers = read();
fe24a0 427             UserModel user = getUserModel(username);
8a2e9c 428             allUsers.remove(username);
fe24a0 429             for (TeamModel team : user.teams) {
JM 430                 TeamModel t = getTeamModel(team.name);
431                 if (t == null) {
432                     // new team
433                     t = team;
434                 }
435                 t.removeUser(username);
436                 updateTeamCache(allUsers, t.name, t);
437             }
8c9a20 438             write(allUsers);
dfb889 439             return true;
JM 440         } catch (Throwable t) {
8a2e9c 441             logger.error(MessageFormat.format("Failed to delete user {0}!", username), t);
dfb889 442         }
JM 443         return false;
f98825 444     }
2a7306 445
892570 446     /**
JM 447      * Returns the list of all users available to the login service.
448      * 
449      * @return list of all usernames
450      */
f98825 451     @Override
JM 452     public List<String> getAllUsernames() {
8c9a20 453         Properties allUsers = read();
fe24a0 454         List<String> list = new ArrayList<String>();
JM 455         for (String user : allUsers.stringPropertyNames()) {
456             if (user.charAt(0) == '@') {
457                 // skip team user definitions
458                 continue;
459             }
460             list.add(user);
461         }
d7905a 462         Collections.sort(list);
f98825 463         return list;
JM 464     }
465
892570 466     /**
abeaaf 467      * Returns the list of all users available to the login service.
JM 468      * 
469      * @return list of all usernames
470      */
471     @Override
472     public List<UserModel> getAllUsers() {
473         read();
474         List<UserModel> list = new ArrayList<UserModel>();
475         for (String username : getAllUsernames()) {
476             list.add(getUserModel(username));
477         }
478         Collections.sort(list);
479         return list;
480     }
481
482     /**
892570 483      * Returns the list of all users who are allowed to bypass the access
JM 484      * restriction placed on the specified repository.
485      * 
486      * @param role
487      *            the repository name
488      * @return list of all usernames that can bypass the access restriction
489      */
f98825 490     @Override
892570 491     public List<String> getUsernamesForRepositoryRole(String role) {
f98825 492         List<String> list = new ArrayList<String>();
JM 493         try {
8c9a20 494             Properties allUsers = read();
f98825 495             for (String username : allUsers.stringPropertyNames()) {
fe24a0 496                 if (username.charAt(0) == '@') {
JM 497                     continue;
498                 }
f98825 499                 String value = allUsers.getProperty(username);
JM 500                 String[] values = value.split(",");
501                 // skip first value (password)
502                 for (int i = 1; i < values.length; i++) {
503                     String r = values[i];
504                     if (r.equalsIgnoreCase(role)) {
505                         list.add(username);
506                         break;
507                     }
508                 }
509             }
510         } catch (Throwable t) {
511             logger.error(MessageFormat.format("Failed to get usernames for role {0}!", role), t);
512         }
d7905a 513         Collections.sort(list);
f98825 514         return list;
JM 515     }
516
892570 517     /**
fe24a0 518      * Sets the list of all users who are allowed to bypass the access
892570 519      * restriction placed on the specified repository.
JM 520      * 
521      * @param role
522      *            the repository name
523      * @param usernames
524      * @return true if successful
525      */
f98825 526     @Override
892570 527     public boolean setUsernamesForRepositoryRole(String role, List<String> usernames) {
f98825 528         try {
JM 529             Set<String> specifiedUsers = new HashSet<String>(usernames);
530             Set<String> needsAddRole = new HashSet<String>(specifiedUsers);
531             Set<String> needsRemoveRole = new HashSet<String>();
532
533             // identify users which require add and remove role
8c9a20 534             Properties allUsers = read();
f98825 535             for (String username : allUsers.stringPropertyNames()) {
JM 536                 String value = allUsers.getProperty(username);
537                 String[] values = value.split(",");
538                 // skip first value (password)
539                 for (int i = 1; i < values.length; i++) {
540                     String r = values[i];
541                     if (r.equalsIgnoreCase(role)) {
542                         // user has role, check against revised user list
543                         if (specifiedUsers.contains(username)) {
544                             needsAddRole.remove(username);
545                         } else {
546                             // remove role from user
547                             needsRemoveRole.add(username);
548                         }
549                         break;
550                     }
551                 }
552             }
553
554             // add roles to users
555             for (String user : needsAddRole) {
556                 String userValues = allUsers.getProperty(user);
2a7306 557                 userValues += "," + role;
f98825 558                 allUsers.put(user, userValues);
JM 559             }
560
561             // remove role from user
562             for (String user : needsRemoveRole) {
563                 String[] values = allUsers.getProperty(user).split(",");
564                 String password = values[0];
565                 StringBuilder sb = new StringBuilder();
566                 sb.append(password);
567                 sb.append(',');
93f472 568
f98825 569                 // skip first value (password)
JM 570                 for (int i = 1; i < values.length; i++) {
571                     String value = values[i];
572                     if (!value.equalsIgnoreCase(role)) {
573                         sb.append(value);
574                         sb.append(',');
575                     }
576                 }
577                 sb.setLength(sb.length() - 1);
578
579                 // update properties
580                 allUsers.put(user, sb.toString());
581             }
582
583             // persist changes
8c9a20 584             write(allUsers);
f98825 585             return true;
JM 586         } catch (Throwable t) {
587             logger.error(MessageFormat.format("Failed to set usernames for role {0}!", role), t);
588         }
589         return false;
590     }
591
892570 592     /**
JM 593      * Renames a repository role.
594      * 
595      * @param oldRole
596      * @param newRole
597      * @return true if successful
598      */
f98825 599     @Override
85c2e6 600     public boolean renameRepositoryRole(String oldRole, String newRole) {
f98825 601         try {
8c9a20 602             Properties allUsers = read();
f98825 603             Set<String> needsRenameRole = new HashSet<String>();
JM 604
605             // identify users which require role rename
606             for (String username : allUsers.stringPropertyNames()) {
607                 String value = allUsers.getProperty(username);
608                 String[] roles = value.split(",");
609                 // skip first value (password)
610                 for (int i = 1; i < roles.length; i++) {
20714a 611                     String repository = AccessPermission.repositoryFromRole(roles[i]);
JM 612                     if (repository.equalsIgnoreCase(oldRole)) {
93f472 613                         needsRenameRole.add(username);
f98825 614                         break;
JM 615                     }
616                 }
617             }
618
619             // rename role for identified users
620             for (String user : needsRenameRole) {
621                 String userValues = allUsers.getProperty(user);
622                 String[] values = userValues.split(",");
623                 String password = values[0];
624                 StringBuilder sb = new StringBuilder();
625                 sb.append(password);
626                 sb.append(',');
93f472 627                 sb.append(newRole);
JM 628                 sb.append(',');
fe24a0 629
f98825 630                 // skip first value (password)
JM 631                 for (int i = 1; i < values.length; i++) {
20714a 632                     String repository = AccessPermission.repositoryFromRole(values[i]);
JM 633                     if (repository.equalsIgnoreCase(oldRole)) {
634                         AccessPermission permission = AccessPermission.permissionFromRole(values[i]);
635                         sb.append(permission.asRole(newRole));
636                         sb.append(',');
637                     } else {
638                         sb.append(values[i]);
f98825 639                         sb.append(',');
JM 640                     }
641                 }
642                 sb.setLength(sb.length() - 1);
643
644                 // update properties
645                 allUsers.put(user, sb.toString());
646             }
647
648             // persist changes
8c9a20 649             write(allUsers);
f98825 650             return true;
JM 651         } catch (Throwable t) {
2a7306 652             logger.error(
JM 653                     MessageFormat.format("Failed to rename role {0} to {1}!", oldRole, newRole), t);
f98825 654         }
JM 655         return false;
656     }
657
892570 658     /**
JM 659      * Removes a repository role from all users.
660      * 
661      * @param role
662      * @return true if successful
663      */
f98825 664     @Override
85c2e6 665     public boolean deleteRepositoryRole(String role) {
f98825 666         try {
8c9a20 667             Properties allUsers = read();
f98825 668             Set<String> needsDeleteRole = new HashSet<String>();
JM 669
670             // identify users which require role rename
671             for (String username : allUsers.stringPropertyNames()) {
672                 String value = allUsers.getProperty(username);
673                 String[] roles = value.split(",");
674                 // skip first value (password)
20714a 675                 for (int i = 1; i < roles.length; i++) {                    
JM 676                     String repository = AccessPermission.repositoryFromRole(roles[i]);
677                     if (repository.equalsIgnoreCase(role)) {
93f472 678                         needsDeleteRole.add(username);
f98825 679                         break;
JM 680                     }
681                 }
682             }
683
684             // delete role for identified users
685             for (String user : needsDeleteRole) {
686                 String userValues = allUsers.getProperty(user);
687                 String[] values = userValues.split(",");
688                 String password = values[0];
689                 StringBuilder sb = new StringBuilder();
690                 sb.append(password);
691                 sb.append(',');
692                 // skip first value (password)
20714a 693                 for (int i = 1; i < values.length; i++) {                    
JM 694                     String repository = AccessPermission.repositoryFromRole(values[i]);
695                     if (!repository.equalsIgnoreCase(role)) {
696                         sb.append(values[i]);
f98825 697                         sb.append(',');
JM 698                     }
699                 }
700                 sb.setLength(sb.length() - 1);
701
702                 // update properties
703                 allUsers.put(user, sb.toString());
704             }
705
706             // persist changes
8c9a20 707             write(allUsers);
8a2e9c 708             return true;
f98825 709         } catch (Throwable t) {
JM 710             logger.error(MessageFormat.format("Failed to delete role {0}!", role), t);
711         }
712         return false;
713     }
714
892570 715     /**
JM 716      * Writes the properties file.
717      * 
718      * @param properties
719      * @throws IOException
720      */
8c9a20 721     private void write(Properties properties) throws IOException {
892570 722         // Write a temporary copy of the users file
8c9a20 723         File realmFileCopy = new File(propertiesFile.getAbsolutePath() + ".tmp");
f98825 724         FileWriter writer = new FileWriter(realmFileCopy);
2a7306 725         properties
JM 726                 .store(writer,
fe24a0 727                         " Gitblit realm file format:\n   username=password,\\#permission,repository1,repository2...\n   @teamname=!username1,!username2,!username3,repository1,repository2...");
f98825 728         writer.close();
892570 729         // If the write is successful, delete the current file and rename
JM 730         // the temporary copy to the original filename.
f98825 731         if (realmFileCopy.exists() && realmFileCopy.length() > 0) {
831469 732             if (propertiesFile.exists()) {
JM 733                 if (!propertiesFile.delete()) {
734                     throw new IOException(MessageFormat.format("Failed to delete {0}!",
735                             propertiesFile.getAbsolutePath()));
2a7306 736                 }
831469 737             }
JM 738             if (!realmFileCopy.renameTo(propertiesFile)) {
739                 throw new IOException(MessageFormat.format("Failed to rename {0} to {1}!",
740                         realmFileCopy.getAbsolutePath(), propertiesFile.getAbsolutePath()));
2a7306 741             }
f98825 742         } else {
2a7306 743             throw new IOException(MessageFormat.format("Failed to save {0}!",
JM 744                     realmFileCopy.getAbsolutePath()));
f98825 745         }
dfb889 746     }
85c2e6 747
892570 748     /**
JM 749      * Reads the properties file and rebuilds the in-memory cookie lookup table.
750      */
85c2e6 751     @Override
JM 752     protected synchronized Properties read() {
892570 753         long lastRead = lastModified();
5e58f0 754         boolean reload = forceReload();
85c2e6 755         Properties allUsers = super.read();
5e58f0 756         if (reload || (lastRead != lastModified())) {
85c2e6 757             // reload hash cache
JM 758             cookies.clear();
fe24a0 759             teams.clear();
JM 760
85c2e6 761             for (String username : allUsers.stringPropertyNames()) {
JM 762                 String value = allUsers.getProperty(username);
763                 String[] roles = value.split(",");
fe24a0 764                 if (username.charAt(0) == '@') {
JM 765                     // team definition
766                     TeamModel team = new TeamModel(username.substring(1));
767                     List<String> repositories = new ArrayList<String>();
768                     List<String> users = new ArrayList<String>();
0b9119 769                     List<String> mailingLists = new ArrayList<String>();
d7905a 770                     List<String> preReceive = new ArrayList<String>();
JM 771                     List<String> postReceive = new ArrayList<String>();
fe24a0 772                     for (String role : roles) {
JM 773                         if (role.charAt(0) == '!') {
774                             users.add(role.substring(1));
0b9119 775                         } else if (role.charAt(0) == '&') {
d7905a 776                             mailingLists.add(role.substring(1));
JM 777                         } else if (role.charAt(0) == '^') {
778                             preReceive.add(role.substring(1));
779                         } else if (role.charAt(0) == '%') {
780                             postReceive.add(role.substring(1));
fe24a0 781                         } else {
7f7051 782                             switch (role.charAt(0)) {
JM 783                             case '#':
784                                 // Permissions
785                                 if (role.equalsIgnoreCase(Constants.ADMIN_ROLE)) {
786                                     team.canAdmin = true;
787                                 } else if (role.equalsIgnoreCase(Constants.FORK_ROLE)) {
788                                     team.canFork = true;
789                                 } else if (role.equalsIgnoreCase(Constants.CREATE_ROLE)) {
790                                     team.canCreate = true;
791                                 }
792                                 break;
793                             default:
794                                 repositories.add(role);
795                             }
fe24a0 796                             repositories.add(role);
JM 797                         }
798                     }
20714a 799                     team.addRepositoryPermissions(repositories);
fe24a0 800                     team.addUsers(users);
0b9119 801                     team.addMailingLists(mailingLists);
6cca86 802                     team.preReceiveScripts.addAll(preReceive);
JM 803                     team.postReceiveScripts.addAll(postReceive);
fe24a0 804                     teams.put(team.name.toLowerCase(), team);
JM 805                 } else {
806                     // user definition
807                     String password = roles[0];
ae0b13 808                     cookies.put(StringUtils.getSHA1(username.toLowerCase() + password), username.toLowerCase());
fe24a0 809                 }
85c2e6 810             }
JM 811         }
812         return allUsers;
813     }
93f472 814
JM 815     @Override
816     public String toString() {
817         return getClass().getSimpleName() + "(" + propertiesFile.getAbsolutePath() + ")";
818     }
fe24a0 819
JM 820     /**
821      * Returns the list of all teams available to the login service.
822      * 
823      * @return list of all teams
824      * @since 0.8.0
825      */
826     @Override
827     public List<String> getAllTeamNames() {
828         List<String> list = new ArrayList<String>(teams.keySet());
d7905a 829         Collections.sort(list);
fe24a0 830         return list;
JM 831     }
832
833     /**
abeaaf 834      * Returns the list of all teams available to the login service.
JM 835      * 
836      * @return list of all teams
837      * @since 0.8.0
838      */
839     @Override
840     public List<TeamModel> getAllTeams() {
841         List<TeamModel> list = new ArrayList<TeamModel>(teams.values());
842         list = DeepCopier.copy(list);
843         Collections.sort(list);
844         return list;
845     }
846
847     /**
fe24a0 848      * Returns the list of all teams who are allowed to bypass the access
JM 849      * restriction placed on the specified repository.
850      * 
851      * @param role
852      *            the repository name
853      * @return list of all teamnames that can bypass the access restriction
854      */
855     @Override
856     public List<String> getTeamnamesForRepositoryRole(String role) {
857         List<String> list = new ArrayList<String>();
858         try {
859             Properties allUsers = read();
860             for (String team : allUsers.stringPropertyNames()) {
861                 if (team.charAt(0) != '@') {
862                     // skip users
863                     continue;
864                 }
865                 String value = allUsers.getProperty(team);
866                 String[] values = value.split(",");
867                 for (int i = 0; i < values.length; i++) {
868                     String r = values[i];
869                     if (r.equalsIgnoreCase(role)) {
870                         // strip leading @
871                         list.add(team.substring(1));
872                         break;
873                     }
874                 }
875             }
876         } catch (Throwable t) {
877             logger.error(MessageFormat.format("Failed to get teamnames for role {0}!", role), t);
878         }
d7905a 879         Collections.sort(list);
fe24a0 880         return list;
JM 881     }
882
883     /**
884      * Sets the list of all teams who are allowed to bypass the access
885      * restriction placed on the specified repository.
886      * 
887      * @param role
888      *            the repository name
889      * @param teamnames
890      * @return true if successful
891      */
892     @Override
893     public boolean setTeamnamesForRepositoryRole(String role, List<String> teamnames) {
894         try {
895             Set<String> specifiedTeams = new HashSet<String>(teamnames);
896             Set<String> needsAddRole = new HashSet<String>(specifiedTeams);
897             Set<String> needsRemoveRole = new HashSet<String>();
898
899             // identify teams which require add and remove role
900             Properties allUsers = read();
901             for (String team : allUsers.stringPropertyNames()) {
902                 if (team.charAt(0) != '@') {
903                     // skip users
904                     continue;
905                 }
906                 String name = team.substring(1);
907                 String value = allUsers.getProperty(team);
908                 String[] values = value.split(",");
909                 for (int i = 0; i < values.length; i++) {
910                     String r = values[i];
911                     if (r.equalsIgnoreCase(role)) {
912                         // team has role, check against revised team list
913                         if (specifiedTeams.contains(name)) {
914                             needsAddRole.remove(name);
915                         } else {
916                             // remove role from team
917                             needsRemoveRole.add(name);
918                         }
919                         break;
920                     }
921                 }
922             }
923
924             // add roles to teams
925             for (String name : needsAddRole) {
926                 String team = "@" + name;
927                 String teamValues = allUsers.getProperty(team);
928                 teamValues += "," + role;
929                 allUsers.put(team, teamValues);
930             }
931
932             // remove role from team
933             for (String name : needsRemoveRole) {
934                 String team = "@" + name;
abeaaf 935                 String[] values = allUsers.getProperty(team).split(",");
fe24a0 936                 StringBuilder sb = new StringBuilder();
JM 937                 for (int i = 0; i < values.length; i++) {
938                     String value = values[i];
939                     if (!value.equalsIgnoreCase(role)) {
940                         sb.append(value);
941                         sb.append(',');
942                     }
943                 }
944                 sb.setLength(sb.length() - 1);
945
946                 // update properties
947                 allUsers.put(team, sb.toString());
948             }
949
950             // persist changes
951             write(allUsers);
952             return true;
953         } catch (Throwable t) {
954             logger.error(MessageFormat.format("Failed to set teamnames for role {0}!", role), t);
955         }
956         return false;
957     }
958
959     /**
960      * Retrieve the team object for the specified team name.
961      * 
962      * @param teamname
963      * @return a team object or null
964      * @since 0.8.0
965      */
966     @Override
967     public TeamModel getTeamModel(String teamname) {
968         read();
969         TeamModel team = teams.get(teamname.toLowerCase());
970         if (team != null) {
971             // clone the model, otherwise all changes to this object are
972             // live and unpersisted
973             team = DeepCopier.copy(team);
974         }
975         return team;
976     }
977
978     /**
979      * Updates/writes a complete team object.
980      * 
981      * @param model
982      * @return true if update is successful
983      * @since 0.8.0
984      */
985     @Override
986     public boolean updateTeamModel(TeamModel model) {
987         return updateTeamModel(model.name, model);
988     }
20714a 989     
JM 990     /**
991      * Updates/writes all specified team objects.
992      * 
993      * @param models a list of team models
994      * @return true if update is successful
995      * @since 1.2.0
996      */
997     public boolean updateTeamModels(List<TeamModel> models) {
998         try {
999             Properties allUsers = read();
1000             for (TeamModel model : models) {
1001                 updateTeamCache(allUsers, model.name, model);
1002             }
1003             write(allUsers);
1004             return true;
1005         } catch (Throwable t) {
1006             logger.error(MessageFormat.format("Failed to update {0} team models!", models.size()), t);
1007         }
1008         return false;
1009     }
fe24a0 1010
JM 1011     /**
1012      * Updates/writes and replaces a complete team object keyed by teamname.
1013      * This method allows for renaming a team.
1014      * 
1015      * @param teamname
1016      *            the old teamname
1017      * @param model
1018      *            the team object to use for teamname
1019      * @return true if update is successful
1020      * @since 0.8.0
1021      */
1022     @Override
1023     public boolean updateTeamModel(String teamname, TeamModel model) {
1024         try {
1025             Properties allUsers = read();
1026             updateTeamCache(allUsers, teamname, model);
1027             write(allUsers);
1028             return true;
1029         } catch (Throwable t) {
1030             logger.error(MessageFormat.format("Failed to update team model {0}!", model.name), t);
1031         }
1032         return false;
1033     }
1034
1035     private void updateTeamCache(Properties allUsers, String teamname, TeamModel model) {
1036         StringBuilder sb = new StringBuilder();
20714a 1037         List<String> roles;
JM 1038         if (model.permissions == null) {
1039             // legacy, use repository list
1040             if (model.repositories != null) {
1041                 roles = new ArrayList<String>(model.repositories);
1042             } else {
1043                 roles = new ArrayList<String>();
1044             }
1045         } else {
1046             // discrete repository permissions
1047             roles = new ArrayList<String>();
1048             for (Map.Entry<String, AccessPermission> entry : model.permissions.entrySet()) {
1049                 if (entry.getValue().exceeds(AccessPermission.NONE)) {
1050                     // code:repository (e.g. RW+:~james/myrepo.git
1051                     roles.add(entry.getValue().asRole(entry.getKey()));
1052                 }
0db5c4 1053             }
fe24a0 1054         }
20714a 1055         
7f7051 1056         // Permissions
JM 1057         if (model.canAdmin) {
1058             roles.add(Constants.ADMIN_ROLE);
1059         }
1060         if (model.canFork) {
1061             roles.add(Constants.FORK_ROLE);
1062         }
1063         if (model.canCreate) {
1064             roles.add(Constants.CREATE_ROLE);
1065         }
1066
20714a 1067         for (String role : roles) {
JM 1068                 sb.append(role);
1069                 sb.append(',');
1070         }
1071         
0db5c4 1072         if (!ArrayUtils.isEmpty(model.users)) {
JM 1073             for (String user : model.users) {
1074                 sb.append('!');
1075                 sb.append(user);
1076                 sb.append(',');
1077             }
fe24a0 1078         }
0db5c4 1079         if (!ArrayUtils.isEmpty(model.mailingLists)) {
JM 1080             for (String address : model.mailingLists) {
1081                 sb.append('&');
1082                 sb.append(address);
1083                 sb.append(',');
1084             }
0b9119 1085         }
0db5c4 1086         if (!ArrayUtils.isEmpty(model.preReceiveScripts)) {
JM 1087             for (String script : model.preReceiveScripts) {
1088                 sb.append('^');
1089                 sb.append(script);
1090                 sb.append(',');
1091             }
d7905a 1092         }
0db5c4 1093         if (!ArrayUtils.isEmpty(model.postReceiveScripts)) {
JM 1094             for (String script : model.postReceiveScripts) {
1095                 sb.append('%');
1096                 sb.append(script);
1097                 sb.append(',');
1098             }
d7905a 1099         }
fe24a0 1100         // trim trailing comma
JM 1101         sb.setLength(sb.length() - 1);
1102         allUsers.remove("@" + teamname);
1103         allUsers.put("@" + model.name, sb.toString());
1104
1105         // update team cache
1106         teams.remove(teamname.toLowerCase());
1107         teams.put(model.name.toLowerCase(), model);
1108     }
1109
1110     /**
1111      * Deletes the team object from the user service.
1112      * 
1113      * @param model
1114      * @return true if successful
1115      * @since 0.8.0
1116      */
1117     @Override
1118     public boolean deleteTeamModel(TeamModel model) {
1119         return deleteTeam(model.name);
1120     }
1121
1122     /**
1123      * Delete the team object with the specified teamname
1124      * 
1125      * @param teamname
1126      * @return true if successful
1127      * @since 0.8.0
1128      */
1129     @Override
1130     public boolean deleteTeam(String teamname) {
1131         Properties allUsers = read();
1132         teams.remove(teamname.toLowerCase());
1133         allUsers.remove("@" + teamname);
1134         try {
1135             write(allUsers);
1136             return true;
1137         } catch (Throwable t) {
1138             logger.error(MessageFormat.format("Failed to delete team {0}!", teamname), t);
1139         }
1140         return false;
1141     }
fc948c 1142 }