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