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