You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ConfigUserService.java 28KB

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