James Moger
2012-09-10 fabe060d3a435f116128851f828e35c2af5fde67
commit | author | age
93f472 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  */
16 package com.gitblit;
17
18 import java.io.File;
19 import java.io.IOException;
20 import java.text.MessageFormat;
21 import java.util.ArrayList;
22 import java.util.Arrays;
d7905a 23 import java.util.Collections;
93f472 24 import java.util.HashSet;
JM 25 import java.util.List;
26 import java.util.Map;
27 import java.util.Set;
28 import java.util.concurrent.ConcurrentHashMap;
29
30 import org.eclipse.jgit.lib.StoredConfig;
31 import org.eclipse.jgit.storage.file.FileBasedConfig;
32 import org.eclipse.jgit.util.FS;
33 import org.slf4j.Logger;
34 import org.slf4j.LoggerFactory;
35
fe24a0 36 import com.gitblit.models.TeamModel;
93f472 37 import com.gitblit.models.UserModel;
0db5c4 38 import com.gitblit.utils.ArrayUtils;
fe24a0 39 import com.gitblit.utils.DeepCopier;
93f472 40 import com.gitblit.utils.StringUtils;
JM 41
42 /**
43  * ConfigUserService is Gitblit's default user service implementation since
44  * version 0.8.0.
45  * 
46  * Users and their repository memberships are stored in a git-style config file
47  * which is cached and dynamically reloaded when modified. This file is
48  * plain-text, human-readable, and may be edited with a text editor.
49  * 
50  * Additionally, this format allows for expansion of the user model without
51  * bringing in the complexity of a database.
52  * 
53  * @author James Moger
54  * 
55  */
56 public class ConfigUserService implements IUserService {
57
fe24a0 58     private static final String TEAM = "team";
JM 59
60     private static final String USER = "user";
61
62     private static final String PASSWORD = "password";
fdefa2 63     
JM 64     private static final String DISPLAYNAME = "displayName";
65     
66     private static final String EMAILADDRESS = "emailAddress";
62aeb9 67     
JM 68     private static final String COOKIE = "cookie";
fe24a0 69
JM 70     private static final String REPOSITORY = "repository";
71
72     private static final String ROLE = "role";
d7905a 73
0b9119 74     private static final String MAILINGLIST = "mailingList";
d7905a 75
JM 76     private static final String PRERECEIVE = "preReceiveScript";
77
78     private static final String POSTRECEIVE = "postReceiveScript";
fe24a0 79
93f472 80     private final File realmFile;
JM 81
82     private final Logger logger = LoggerFactory.getLogger(ConfigUserService.class);
83
84     private final Map<String, UserModel> users = new ConcurrentHashMap<String, UserModel>();
85
86     private final Map<String, UserModel> cookies = new ConcurrentHashMap<String, UserModel>();
87
fe24a0 88     private final Map<String, TeamModel> teams = new ConcurrentHashMap<String, TeamModel>();
93f472 89
JM 90     private volatile long lastModified;
5e58f0 91     
JM 92     private volatile boolean forceReload;
93f472 93
JM 94     public ConfigUserService(File realmFile) {
95         this.realmFile = realmFile;
96     }
97
98     /**
99      * Setup the user service.
100      * 
101      * @param settings
fe24a0 102      * @since 0.7.0
93f472 103      */
JM 104     @Override
105     public void setup(IStoredSettings settings) {
106     }
107
108     /**
6cca86 109      * Does the user service support changes to credentials?
JM 110      * 
111      * @return true or false
112      * @since 1.0.0
113      */
114     @Override
115     public boolean supportsCredentialChanges() {
116         return true;
117     }
0aa8cf 118
JM 119     /**
120      * Does the user service support changes to user display name?
121      * 
122      * @return true or false
123      * @since 1.0.0
124      */
125     @Override
126     public boolean supportsDisplayNameChanges() {
127         return true;
128     }
129
130     /**
131      * Does the user service support changes to user email address?
132      * 
133      * @return true or false
134      * @since 1.0.0
135      */
136     @Override
137     public boolean supportsEmailAddressChanges() {
138         return true;
139     }
140
6cca86 141     /**
JM 142      * Does the user service support changes to team memberships?
143      * 
144      * @return true or false
145      * @since 1.0.0
146      */    
147     public boolean supportsTeamMembershipChanges() {
148         return true;
149     }
150     
151     /**
93f472 152      * Does the user service support cookie authentication?
JM 153      * 
154      * @return true or false
155      */
156     @Override
157     public boolean supportsCookies() {
158         return true;
159     }
160
161     /**
162      * Returns the cookie value for the specified user.
163      * 
164      * @param model
165      * @return cookie value
166      */
167     @Override
62aeb9 168     public String getCookie(UserModel model) {
JM 169         if (!StringUtils.isEmpty(model.cookie)) {
170             return model.cookie;
171         }
93f472 172         read();
JM 173         UserModel storedModel = users.get(model.username.toLowerCase());
62aeb9 174         return storedModel.cookie;
93f472 175     }
JM 176
177     /**
178      * Authenticate a user based on their cookie.
179      * 
180      * @param cookie
181      * @return a user object or null
182      */
183     @Override
184     public UserModel authenticate(char[] cookie) {
185         String hash = new String(cookie);
186         if (StringUtils.isEmpty(hash)) {
187             return null;
188         }
189         read();
190         UserModel model = null;
191         if (cookies.containsKey(hash)) {
192             model = cookies.get(hash);
193         }
194         return model;
195     }
196
197     /**
198      * Authenticate a user based on a username and password.
199      * 
200      * @param username
201      * @param password
202      * @return a user object or null
203      */
204     @Override
205     public UserModel authenticate(String username, char[] password) {
206         read();
207         UserModel returnedUser = null;
208         UserModel user = getUserModel(username);
209         if (user == null) {
210             return null;
211         }
212         if (user.password.startsWith(StringUtils.MD5_TYPE)) {
213             // password digest
214             String md5 = StringUtils.MD5_TYPE + StringUtils.getMD5(new String(password));
215             if (user.password.equalsIgnoreCase(md5)) {
216                 returnedUser = user;
217             }
218         } else if (user.password.startsWith(StringUtils.COMBINED_MD5_TYPE)) {
219             // username+password digest
220             String md5 = StringUtils.COMBINED_MD5_TYPE
221                     + StringUtils.getMD5(username.toLowerCase() + new String(password));
222             if (user.password.equalsIgnoreCase(md5)) {
223                 returnedUser = user;
224             }
225         } else if (user.password.equals(new String(password))) {
226             // plain-text password
227             returnedUser = user;
228         }
229         return returnedUser;
230     }
231
232     /**
ea094a 233      * Logout a user.
JM 234      * 
235      * @param user
236      */
237     @Override
238     public void logout(UserModel user) {    
239     }
240     
241     /**
93f472 242      * Retrieve the user object for the specified username.
JM 243      * 
244      * @param username
245      * @return a user object or null
246      */
247     @Override
248     public UserModel getUserModel(String username) {
249         read();
250         UserModel model = users.get(username.toLowerCase());
fe24a0 251         if (model != null) {
JM 252             // clone the model, otherwise all changes to this object are
253             // live and unpersisted
254             model = DeepCopier.copy(model);
255         }
93f472 256         return model;
JM 257     }
258
259     /**
260      * Updates/writes a complete user object.
261      * 
262      * @param model
263      * @return true if update is successful
264      */
265     @Override
266     public boolean updateUserModel(UserModel model) {
267         return updateUserModel(model.username, model);
268     }
269
270     /**
271      * Updates/writes and replaces a complete user object keyed by username.
272      * This method allows for renaming a user.
273      * 
274      * @param username
275      *            the old username
276      * @param model
277      *            the user object to use for username
278      * @return true if update is successful
279      */
280     @Override
281     public boolean updateUserModel(String username, UserModel model) {
2987f6 282         UserModel originalUser = null;
93f472 283         try {
JM 284             read();
2987f6 285             originalUser = users.remove(username.toLowerCase());
93f472 286             users.put(model.username.toLowerCase(), model);
fe24a0 287             // null check on "final" teams because JSON-sourced UserModel
JM 288             // can have a null teams object
289             if (model.teams != null) {
290                 for (TeamModel team : model.teams) {
291                     TeamModel t = teams.get(team.name.toLowerCase());
292                     if (t == null) {
293                         // new team
294                         team.addUser(username);
295                         teams.put(team.name.toLowerCase(), team);
296                     } else {
297                         // do not clobber existing team definition
298                         // maybe because this is a federated user
299                         t.removeUser(username);
300                         t.addUser(model.username);
301                     }
302                 }
303
304                 // check for implicit team removal
2987f6 305                 if (originalUser != null) {
JM 306                     for (TeamModel team : originalUser.teams) {
fe24a0 307                         if (!model.isTeamMember(team.name)) {
JM 308                             team.removeUser(username);
309                         }
310                     }
311                 }
312             }
93f472 313             write();
JM 314             return true;
315         } catch (Throwable t) {
2987f6 316             if (originalUser != null) {
JM 317                 // restore original user
65f55e 318                 users.put(originalUser.username.toLowerCase(), originalUser);
JM 319             } else {
320                 // drop attempted add
321                 users.remove(model.username.toLowerCase());
2987f6 322             }
93f472 323             logger.error(MessageFormat.format("Failed to update user model {0}!", model.username),
JM 324                     t);
325         }
326         return false;
327     }
328
329     /**
330      * Deletes the user object from the user service.
331      * 
332      * @param model
333      * @return true if successful
334      */
335     @Override
336     public boolean deleteUserModel(UserModel model) {
337         return deleteUser(model.username);
338     }
339
340     /**
341      * Delete the user object with the specified username
342      * 
343      * @param username
344      * @return true if successful
345      */
346     @Override
347     public boolean deleteUser(String username) {
348         try {
349             // Read realm file
350             read();
fe24a0 351             UserModel model = users.remove(username.toLowerCase());
JM 352             // remove user from team
353             for (TeamModel team : model.teams) {
354                 TeamModel t = teams.get(team.name);
355                 if (t == null) {
356                     // new team
357                     team.removeUser(username);
358                     teams.put(team.name.toLowerCase(), team);
359                 } else {
360                     // existing team
361                     t.removeUser(username);
362                 }
363             }
93f472 364             write();
JM 365             return true;
366         } catch (Throwable t) {
367             logger.error(MessageFormat.format("Failed to delete user {0}!", username), t);
fe24a0 368         }
JM 369         return false;
370     }
371
372     /**
373      * Returns the list of all teams available to the login service.
374      * 
375      * @return list of all teams
376      * @since 0.8.0
377      */
378     @Override
379     public List<String> getAllTeamNames() {
380         read();
381         List<String> list = new ArrayList<String>(teams.keySet());
d7905a 382         Collections.sort(list);
fe24a0 383         return list;
JM 384     }
0b9119 385
fe24a0 386     /**
abeaaf 387      * Returns the list of all teams available to the login service.
JM 388      * 
389      * @return list of all teams
390      * @since 0.8.0
391      */
392     @Override
393     public List<TeamModel> getAllTeams() {
394         read();
395         List<TeamModel> list = new ArrayList<TeamModel>(teams.values());
396         list = DeepCopier.copy(list);
397         Collections.sort(list);
398         return list;
399     }
400
401     /**
fe24a0 402      * Returns the list of all users who are allowed to bypass the access
JM 403      * restriction placed on the specified repository.
404      * 
405      * @param role
406      *            the repository name
407      * @return list of all usernames that can bypass the access restriction
408      */
409     @Override
410     public List<String> getTeamnamesForRepositoryRole(String role) {
411         List<String> list = new ArrayList<String>();
412         try {
413             read();
414             for (Map.Entry<String, TeamModel> entry : teams.entrySet()) {
415                 TeamModel model = entry.getValue();
416                 if (model.hasRepository(role)) {
417                     list.add(model.name);
418                 }
419             }
420         } catch (Throwable t) {
421             logger.error(MessageFormat.format("Failed to get teamnames for role {0}!", role), t);
422         }
d7905a 423         Collections.sort(list);
fe24a0 424         return list;
JM 425     }
426
427     /**
428      * Sets the list of all teams who are allowed to bypass the access
429      * restriction placed on the specified repository.
430      * 
431      * @param role
432      *            the repository name
433      * @param teamnames
434      * @return true if successful
435      */
436     @Override
437     public boolean setTeamnamesForRepositoryRole(String role, List<String> teamnames) {
438         try {
439             Set<String> specifiedTeams = new HashSet<String>();
440             for (String teamname : teamnames) {
441                 specifiedTeams.add(teamname.toLowerCase());
442             }
443
444             read();
445
446             // identify teams which require add or remove role
447             for (TeamModel team : teams.values()) {
448                 // team has role, check against revised team list
449                 if (specifiedTeams.contains(team.name.toLowerCase())) {
450                     team.addRepository(role);
451                 } else {
452                     // remove role from team
453                     team.removeRepository(role);
454                 }
455             }
456
457             // persist changes
458             write();
459             return true;
460         } catch (Throwable t) {
461             logger.error(MessageFormat.format("Failed to set teams for role {0}!", role), t);
462         }
463         return false;
464     }
465
466     /**
467      * Retrieve the team object for the specified team name.
468      * 
469      * @param teamname
470      * @return a team object or null
471      * @since 0.8.0
472      */
473     @Override
474     public TeamModel getTeamModel(String teamname) {
475         read();
476         TeamModel model = teams.get(teamname.toLowerCase());
477         if (model != null) {
478             // clone the model, otherwise all changes to this object are
479             // live and unpersisted
480             model = DeepCopier.copy(model);
481         }
482         return model;
483     }
484
485     /**
486      * Updates/writes a complete team object.
487      * 
488      * @param model
489      * @return true if update is successful
490      * @since 0.8.0
491      */
492     @Override
493     public boolean updateTeamModel(TeamModel model) {
494         return updateTeamModel(model.name, model);
495     }
496
497     /**
498      * Updates/writes and replaces a complete team object keyed by teamname.
499      * This method allows for renaming a team.
500      * 
501      * @param teamname
502      *            the old teamname
503      * @param model
504      *            the team object to use for teamname
505      * @return true if update is successful
506      * @since 0.8.0
507      */
508     @Override
509     public boolean updateTeamModel(String teamname, TeamModel model) {
2987f6 510         TeamModel original = null;
fe24a0 511         try {
JM 512             read();
2987f6 513             original = teams.remove(teamname.toLowerCase());
fe24a0 514             teams.put(model.name.toLowerCase(), model);
JM 515             write();
516             return true;
517         } catch (Throwable t) {
2987f6 518             if (original != null) {
JM 519                 // restore original team
65f55e 520                 teams.put(original.name.toLowerCase(), original);
JM 521             } else {
522                 // drop attempted add
523                 teams.remove(model.name.toLowerCase());
2987f6 524             }
fe24a0 525             logger.error(MessageFormat.format("Failed to update team model {0}!", model.name), t);
JM 526         }
527         return false;
528     }
529
530     /**
531      * Deletes the team object from the user service.
532      * 
533      * @param model
534      * @return true if successful
535      * @since 0.8.0
536      */
537     @Override
538     public boolean deleteTeamModel(TeamModel model) {
539         return deleteTeam(model.name);
540     }
541
542     /**
543      * Delete the team object with the specified teamname
544      * 
545      * @param teamname
546      * @return true if successful
547      * @since 0.8.0
548      */
549     @Override
550     public boolean deleteTeam(String teamname) {
551         try {
552             // Read realm file
553             read();
554             teams.remove(teamname.toLowerCase());
555             write();
556             return true;
557         } catch (Throwable t) {
558             logger.error(MessageFormat.format("Failed to delete team {0}!", teamname), t);
93f472 559         }
JM 560         return false;
561     }
562
563     /**
564      * Returns the list of all users available to the login service.
565      * 
566      * @return list of all usernames
567      */
568     @Override
569     public List<String> getAllUsernames() {
570         read();
571         List<String> list = new ArrayList<String>(users.keySet());
d7905a 572         Collections.sort(list);
93f472 573         return list;
JM 574     }
abeaaf 575     
JM 576     /**
577      * Returns the list of all users available to the login service.
578      * 
579      * @return list of all usernames
580      */
581     @Override
582     public List<UserModel> getAllUsers() {
583         read();
584         List<UserModel> list = new ArrayList<UserModel>(users.values());
585         list = DeepCopier.copy(list);
586         Collections.sort(list);
587         return list;
588     }    
93f472 589
JM 590     /**
591      * Returns the list of all users who are allowed to bypass the access
592      * restriction placed on the specified repository.
593      * 
594      * @param role
595      *            the repository name
596      * @return list of all usernames that can bypass the access restriction
597      */
598     @Override
599     public List<String> getUsernamesForRepositoryRole(String role) {
600         List<String> list = new ArrayList<String>();
601         try {
602             read();
603             for (Map.Entry<String, UserModel> entry : users.entrySet()) {
604                 UserModel model = entry.getValue();
605                 if (model.hasRepository(role)) {
606                     list.add(model.username);
607                 }
608             }
609         } catch (Throwable t) {
610             logger.error(MessageFormat.format("Failed to get usernames for role {0}!", role), t);
611         }
d7905a 612         Collections.sort(list);
93f472 613         return list;
JM 614     }
615
616     /**
617      * Sets the list of all uses who are allowed to bypass the access
618      * restriction placed on the specified repository.
619      * 
620      * @param role
621      *            the repository name
622      * @param usernames
623      * @return true if successful
624      */
625     @Override
626     public boolean setUsernamesForRepositoryRole(String role, List<String> usernames) {
627         try {
628             Set<String> specifiedUsers = new HashSet<String>();
629             for (String username : usernames) {
630                 specifiedUsers.add(username.toLowerCase());
631             }
632
633             read();
634
635             // identify users which require add or remove role
636             for (UserModel user : users.values()) {
637                 // user has role, check against revised user list
638                 if (specifiedUsers.contains(user.username.toLowerCase())) {
639                     user.addRepository(role);
640                 } else {
641                     // remove role from user
642                     user.removeRepository(role);
643                 }
644             }
645
646             // persist changes
647             write();
648             return true;
649         } catch (Throwable t) {
650             logger.error(MessageFormat.format("Failed to set usernames for role {0}!", role), t);
651         }
652         return false;
653     }
654
655     /**
656      * Renames a repository role.
657      * 
658      * @param oldRole
659      * @param newRole
660      * @return true if successful
661      */
662     @Override
663     public boolean renameRepositoryRole(String oldRole, String newRole) {
664         try {
665             read();
666             // identify users which require role rename
667             for (UserModel model : users.values()) {
668                 if (model.hasRepository(oldRole)) {
669                     model.removeRepository(oldRole);
670                     model.addRepository(newRole);
671                 }
672             }
673
fe24a0 674             // identify teams which require role rename
JM 675             for (TeamModel model : teams.values()) {
676                 if (model.hasRepository(oldRole)) {
677                     model.removeRepository(oldRole);
678                     model.addRepository(newRole);
679                 }
680             }
93f472 681             // persist changes
JM 682             write();
683             return true;
684         } catch (Throwable t) {
685             logger.error(
686                     MessageFormat.format("Failed to rename role {0} to {1}!", oldRole, newRole), t);
687         }
688         return false;
689     }
690
691     /**
692      * Removes a repository role from all users.
693      * 
694      * @param role
695      * @return true if successful
696      */
697     @Override
698     public boolean deleteRepositoryRole(String role) {
699         try {
700             read();
701
702             // identify users which require role rename
703             for (UserModel user : users.values()) {
704                 user.removeRepository(role);
705             }
706
fe24a0 707             // identify teams which require role rename
JM 708             for (TeamModel team : teams.values()) {
709                 team.removeRepository(role);
710             }
711
93f472 712             // persist changes
JM 713             write();
714             return true;
715         } catch (Throwable t) {
716             logger.error(MessageFormat.format("Failed to delete role {0}!", role), t);
717         }
718         return false;
719     }
720
721     /**
722      * Writes the properties file.
723      * 
724      * @param properties
725      * @throws IOException
726      */
727     private synchronized void write() throws IOException {
728         // Write a temporary copy of the users file
729         File realmFileCopy = new File(realmFile.getAbsolutePath() + ".tmp");
730
731         StoredConfig config = new FileBasedConfig(realmFileCopy, FS.detect());
fe24a0 732
JM 733         // write users
93f472 734         for (UserModel model : users.values()) {
6cca86 735             if (!StringUtils.isEmpty(model.password)) {
JM 736                 config.setString(USER, model.username, PASSWORD, model.password);
737             }
62aeb9 738             if (!StringUtils.isEmpty(model.cookie)) {
JM 739                 config.setString(USER, model.username, COOKIE, model.cookie);
740             }
fdefa2 741             if (!StringUtils.isEmpty(model.displayName)) {
JM 742                 config.setString(USER, model.username, DISPLAYNAME, model.displayName);
743             }
744             if (!StringUtils.isEmpty(model.emailAddress)) {
745                 config.setString(USER, model.username, EMAILADDRESS, model.emailAddress);
746             }
93f472 747
JM 748             // user roles
749             List<String> roles = new ArrayList<String>();
750             if (model.canAdmin) {
751                 roles.add(Constants.ADMIN_ROLE);
752             }
753             if (model.excludeFromFederation) {
754                 roles.add(Constants.NOT_FEDERATED_ROLE);
755             }
ce2a40 756             if (roles.size() == 0) {
JM 757                 // we do this to ensure that user record with no password
758                 // is written.  otherwise, StoredConfig optimizes that account
759                 // away. :(
760                 roles.add(Constants.NO_ROLE);
761             }
fe24a0 762             config.setStringList(USER, model.username, ROLE, roles);
93f472 763
JM 764             // repository memberships
fe24a0 765             // null check on "final" repositories because JSON-sourced UserModel
JM 766             // can have a null repositories object
0db5c4 767             if (!ArrayUtils.isEmpty(model.repositories)) {
fe24a0 768                 config.setStringList(USER, model.username, REPOSITORY, new ArrayList<String>(
JM 769                         model.repositories));
770             }
93f472 771         }
fe24a0 772
JM 773         // write teams
774         for (TeamModel model : teams.values()) {
775             // null check on "final" repositories because JSON-sourced TeamModel
776             // can have a null repositories object
0db5c4 777             if (!ArrayUtils.isEmpty(model.repositories)) {
fe24a0 778                 config.setStringList(TEAM, model.name, REPOSITORY, new ArrayList<String>(
JM 779                         model.repositories));
780             }
781
782             // null check on "final" users because JSON-sourced TeamModel
783             // can have a null users object
0db5c4 784             if (!ArrayUtils.isEmpty(model.users)) {
fe24a0 785                 config.setStringList(TEAM, model.name, USER, new ArrayList<String>(model.users));
JM 786             }
0b9119 787
JM 788             // null check on "final" mailing lists because JSON-sourced
d7905a 789             // TeamModel can have a null users object
0db5c4 790             if (!ArrayUtils.isEmpty(model.mailingLists)) {
0b9119 791                 config.setStringList(TEAM, model.name, MAILINGLIST, new ArrayList<String>(
JM 792                         model.mailingLists));
d7905a 793             }
JM 794
795             // null check on "final" preReceiveScripts because JSON-sourced
796             // TeamModel can have a null preReceiveScripts object
0db5c4 797             if (!ArrayUtils.isEmpty(model.preReceiveScripts)) {
d7905a 798                 config.setStringList(TEAM, model.name, PRERECEIVE, model.preReceiveScripts);
JM 799             }
800
801             // null check on "final" postReceiveScripts because JSON-sourced
802             // TeamModel can have a null postReceiveScripts object
0db5c4 803             if (!ArrayUtils.isEmpty(model.postReceiveScripts)) {
d7905a 804                 config.setStringList(TEAM, model.name, POSTRECEIVE, model.postReceiveScripts);
0b9119 805             }
fe24a0 806         }
JM 807
93f472 808         config.save();
5e58f0 809         // manually set the forceReload flag because not all JVMs support real
JM 810         // millisecond resolution of lastModified. (issue-55)
811         forceReload = true;
93f472 812
JM 813         // If the write is successful, delete the current file and rename
814         // the temporary copy to the original filename.
815         if (realmFileCopy.exists() && realmFileCopy.length() > 0) {
816             if (realmFile.exists()) {
817                 if (!realmFile.delete()) {
818                     throw new IOException(MessageFormat.format("Failed to delete {0}!",
819                             realmFile.getAbsolutePath()));
820                 }
821             }
822             if (!realmFileCopy.renameTo(realmFile)) {
823                 throw new IOException(MessageFormat.format("Failed to rename {0} to {1}!",
824                         realmFileCopy.getAbsolutePath(), realmFile.getAbsolutePath()));
825             }
826         } else {
827             throw new IOException(MessageFormat.format("Failed to save {0}!",
828                     realmFileCopy.getAbsolutePath()));
829         }
830     }
831
832     /**
833      * Reads the realm file and rebuilds the in-memory lookup tables.
834      */
835     protected synchronized void read() {
5e58f0 836         if (realmFile.exists() && (forceReload || (realmFile.lastModified() != lastModified))) {
JM 837             forceReload = false;
93f472 838             lastModified = realmFile.lastModified();
JM 839             users.clear();
840             cookies.clear();
fe24a0 841             teams.clear();
JM 842
93f472 843             try {
JM 844                 StoredConfig config = new FileBasedConfig(realmFile, FS.detect());
845                 config.load();
fe24a0 846                 Set<String> usernames = config.getSubsections(USER);
93f472 847                 for (String username : usernames) {
ae0b13 848                     UserModel user = new UserModel(username.toLowerCase());
fdefa2 849                     user.password = config.getString(USER, username, PASSWORD);                    
JM 850                     user.displayName = config.getString(USER, username, DISPLAYNAME);
851                     user.emailAddress = config.getString(USER, username, EMAILADDRESS);
62aeb9 852                     user.cookie = config.getString(USER, username, COOKIE);
JM 853                     if (StringUtils.isEmpty(user.cookie) && !StringUtils.isEmpty(user.password)) {
854                         user.cookie = StringUtils.getSHA1(user.username + user.password);
855                     }
93f472 856
JM 857                     // user roles
858                     Set<String> roles = new HashSet<String>(Arrays.asList(config.getStringList(
fe24a0 859                             USER, username, ROLE)));
93f472 860                     user.canAdmin = roles.contains(Constants.ADMIN_ROLE);
JM 861                     user.excludeFromFederation = roles.contains(Constants.NOT_FEDERATED_ROLE);
862
863                     // repository memberships
864                     Set<String> repositories = new HashSet<String>(Arrays.asList(config
fe24a0 865                             .getStringList(USER, username, REPOSITORY)));
93f472 866                     for (String repository : repositories) {
JM 867                         user.addRepository(repository);
868                     }
869
870                     // update cache
ae0b13 871                     users.put(user.username, user);
62aeb9 872                     if (!StringUtils.isEmpty(user.cookie)) {
JM 873                         cookies.put(user.cookie, user);
874                     }
93f472 875                 }
fe24a0 876
JM 877                 // load the teams
878                 Set<String> teamnames = config.getSubsections(TEAM);
879                 for (String teamname : teamnames) {
880                     TeamModel team = new TeamModel(teamname);
881                     team.addRepositories(Arrays.asList(config.getStringList(TEAM, teamname,
882                             REPOSITORY)));
883                     team.addUsers(Arrays.asList(config.getStringList(TEAM, teamname, USER)));
d7905a 884                     team.addMailingLists(Arrays.asList(config.getStringList(TEAM, teamname,
JM 885                             MAILINGLIST)));
886                     team.preReceiveScripts.addAll(Arrays.asList(config.getStringList(TEAM,
887                             teamname, PRERECEIVE)));
888                     team.postReceiveScripts.addAll(Arrays.asList(config.getStringList(TEAM,
889                             teamname, POSTRECEIVE)));
fe24a0 890
JM 891                     teams.put(team.name.toLowerCase(), team);
892
893                     // set the teams on the users
894                     for (String user : team.users) {
895                         UserModel model = users.get(user);
896                         if (model != null) {
897                             model.teams.add(team);
898                         }
899                     }
900                 }
93f472 901             } catch (Exception e) {
JM 902                 logger.error(MessageFormat.format("Failed to read {0}", realmFile), e);
903             }
904         }
905     }
906
907     protected long lastModified() {
908         return lastModified;
909     }
910
911     @Override
912     public String toString() {
913         return getClass().getSimpleName() + "(" + realmFile.getAbsolutePath() + ")";
914     }
915 }