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