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