/* | |
* Copyright 2011 gitblit.com. | |
* | |
* Licensed under the Apache License, Version 2.0 (the "License"); | |
* you may not use this file except in compliance with the License. | |
* You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, software | |
* distributed under the License is distributed on an "AS IS" BASIS, | |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
* See the License for the specific language governing permissions and | |
* limitations under the License. | |
*/ | |
package com.gitblit; | |
import java.io.File; | |
import java.io.IOException; | |
import java.text.MessageFormat; | |
import java.util.ArrayList; | |
import java.util.Arrays; | |
import java.util.Collection; | |
import java.util.Collections; | |
import java.util.HashSet; | |
import java.util.List; | |
import java.util.Locale; | |
import java.util.Map; | |
import java.util.Set; | |
import java.util.concurrent.ConcurrentHashMap; | |
import org.eclipse.jgit.lib.StoredConfig; | |
import org.eclipse.jgit.storage.file.FileBasedConfig; | |
import org.eclipse.jgit.util.FS; | |
import org.slf4j.Logger; | |
import org.slf4j.LoggerFactory; | |
import com.gitblit.Constants.AccessPermission; | |
import com.gitblit.Constants.AccountType; | |
import com.gitblit.Constants.Role; | |
import com.gitblit.Constants.Transport; | |
import com.gitblit.manager.IRuntimeManager; | |
import com.gitblit.models.TeamModel; | |
import com.gitblit.models.UserModel; | |
import com.gitblit.models.UserRepositoryPreferences; | |
import com.gitblit.utils.ArrayUtils; | |
import com.gitblit.utils.DeepCopier; | |
import com.gitblit.utils.StringUtils; | |
/** | |
* ConfigUserService is Gitblit's default user service implementation since | |
* version 0.8.0. | |
* | |
* Users and their repository memberships are stored in a git-style config file | |
* which is cached and dynamically reloaded when modified. This file is | |
* plain-text, human-readable, and may be edited with a text editor. | |
* | |
* Additionally, this format allows for expansion of the user model without | |
* bringing in the complexity of a database. | |
* | |
* @author James Moger | |
* | |
*/ | |
public class ConfigUserService implements IUserService { | |
private static final String TEAM = "team"; | |
private static final String USER = "user"; | |
private static final String PASSWORD = "password"; | |
private static final String DISPLAYNAME = "displayName"; | |
private static final String EMAILADDRESS = "emailAddress"; | |
private static final String ORGANIZATIONALUNIT = "organizationalUnit"; | |
private static final String ORGANIZATION = "organization"; | |
private static final String LOCALITY = "locality"; | |
private static final String STATEPROVINCE = "stateProvince"; | |
private static final String COUNTRYCODE = "countryCode"; | |
private static final String COOKIE = "cookie"; | |
private static final String REPOSITORY = "repository"; | |
private static final String ROLE = "role"; | |
private static final String MAILINGLIST = "mailingList"; | |
private static final String PRERECEIVE = "preReceiveScript"; | |
private static final String POSTRECEIVE = "postReceiveScript"; | |
private static final String STARRED = "starred"; | |
private static final String LOCALE = "locale"; | |
private static final String EMAILONMYTICKETCHANGES = "emailMeOnMyTicketChanges"; | |
private static final String TRANSPORT = "transport"; | |
private static final String ACCOUNTTYPE = "accountType"; | |
private static final String DISABLED = "disabled"; | |
private final File realmFile; | |
private final Logger logger = LoggerFactory.getLogger(ConfigUserService.class); | |
private final Map<String, UserModel> users = new ConcurrentHashMap<String, UserModel>(); | |
private final Map<String, UserModel> cookies = new ConcurrentHashMap<String, UserModel>(); | |
private final Map<String, TeamModel> teams = new ConcurrentHashMap<String, TeamModel>(); | |
private volatile long lastModified; | |
private volatile boolean forceReload; | |
public ConfigUserService(File realmFile) { | |
this.realmFile = realmFile; | |
} | |
/** | |
* Setup the user service. | |
* | |
* @param runtimeManager | |
* @since 1.4.0 | |
*/ | |
@Override | |
public void setup(IRuntimeManager runtimeManager) { | |
} | |
/** | |
* Returns the cookie value for the specified user. | |
* | |
* @param model | |
* @return cookie value | |
*/ | |
@Override | |
public synchronized String getCookie(UserModel model) { | |
if (!StringUtils.isEmpty(model.cookie)) { | |
return model.cookie; | |
} | |
UserModel storedModel = getUserModel(model.username); | |
if (storedModel == null) { | |
return null; | |
} | |
return storedModel.cookie; | |
} | |
/** | |
* Gets the user object for the specified cookie. | |
* | |
* @param cookie | |
* @return a user object or null | |
*/ | |
@Override | |
public synchronized UserModel getUserModel(char[] cookie) { | |
String hash = new String(cookie); | |
if (StringUtils.isEmpty(hash)) { | |
return null; | |
} | |
read(); | |
UserModel model = null; | |
if (cookies.containsKey(hash)) { | |
model = cookies.get(hash); | |
} | |
if (model != null) { | |
// clone the model, otherwise all changes to this object are | |
// live and unpersisted | |
model = DeepCopier.copy(model); | |
} | |
return model; | |
} | |
/** | |
* Retrieve the user object for the specified username. | |
* | |
* @param username | |
* @return a user object or null | |
*/ | |
@Override | |
public synchronized UserModel getUserModel(String username) { | |
read(); | |
UserModel model = users.get(username.toLowerCase()); | |
if (model != null) { | |
// clone the model, otherwise all changes to this object are | |
// live and unpersisted | |
model = DeepCopier.copy(model); | |
} | |
return model; | |
} | |
/** | |
* Updates/writes a complete user object. | |
* | |
* @param model | |
* @return true if update is successful | |
*/ | |
@Override | |
public synchronized boolean updateUserModel(UserModel model) { | |
return updateUserModel(model.username, model); | |
} | |
/** | |
* Updates/writes all specified user objects. | |
* | |
* @param models a list of user models | |
* @return true if update is successful | |
* @since 1.2.0 | |
*/ | |
@Override | |
public synchronized boolean updateUserModels(Collection<UserModel> models) { | |
try { | |
read(); | |
for (UserModel model : models) { | |
UserModel originalUser = users.remove(model.username.toLowerCase()); | |
users.put(model.username.toLowerCase(), model); | |
// null check on "final" teams because JSON-sourced UserModel | |
// can have a null teams object | |
if (model.teams != null) { | |
Set<TeamModel> userTeams = new HashSet<TeamModel>(); | |
for (TeamModel team : model.teams) { | |
TeamModel t = teams.get(team.name.toLowerCase()); | |
if (t == null) { | |
// new team | |
t = team; | |
teams.put(team.name.toLowerCase(), t); | |
} | |
// do not clobber existing team definition | |
// maybe because this is a federated user | |
t.addUser(model.username); | |
userTeams.add(t); | |
} | |
// replace Team-Models in users by new ones. | |
model.teams.clear(); | |
model.teams.addAll(userTeams); | |
// check for implicit team removal | |
if (originalUser != null) { | |
for (TeamModel team : originalUser.teams) { | |
if (!model.isTeamMember(team.name)) { | |
team.removeUser(model.username); | |
} | |
} | |
} | |
} | |
} | |
write(); | |
return true; | |
} catch (Throwable t) { | |
logger.error(MessageFormat.format("Failed to update user {0} models!", models.size()), | |
t); | |
} | |
return false; | |
} | |
/** | |
* Updates/writes and replaces a complete user object keyed by username. | |
* This method allows for renaming a user. | |
* | |
* @param username | |
* the old username | |
* @param model | |
* the user object to use for username | |
* @return true if update is successful | |
*/ | |
@Override | |
public synchronized boolean updateUserModel(String username, UserModel model) { | |
UserModel originalUser = null; | |
try { | |
if (!model.isLocalAccount()) { | |
// do not persist password | |
model.password = Constants.EXTERNAL_ACCOUNT; | |
} | |
read(); | |
originalUser = users.remove(username.toLowerCase()); | |
if (originalUser != null) { | |
cookies.remove(originalUser.cookie); | |
} | |
users.put(model.username.toLowerCase(), model); | |
// null check on "final" teams because JSON-sourced UserModel | |
// can have a null teams object | |
if (model.teams != null) { | |
for (TeamModel team : model.teams) { | |
TeamModel t = teams.get(team.name.toLowerCase()); | |
if (t == null) { | |
// new team | |
team.addUser(username); | |
teams.put(team.name.toLowerCase(), team); | |
} else { | |
// do not clobber existing team definition | |
// maybe because this is a federated user | |
t.removeUser(username); | |
t.addUser(model.username); | |
} | |
} | |
// check for implicit team removal | |
if (originalUser != null) { | |
for (TeamModel team : originalUser.teams) { | |
if (!model.isTeamMember(team.name)) { | |
team.removeUser(username); | |
} | |
} | |
} | |
} | |
write(); | |
return true; | |
} catch (Throwable t) { | |
if (originalUser != null) { | |
// restore original user | |
users.put(originalUser.username.toLowerCase(), originalUser); | |
} else { | |
// drop attempted add | |
users.remove(model.username.toLowerCase()); | |
} | |
logger.error(MessageFormat.format("Failed to update user model {0}!", model.username), | |
t); | |
} | |
return false; | |
} | |
/** | |
* Deletes the user object from the user service. | |
* | |
* @param model | |
* @return true if successful | |
*/ | |
@Override | |
public synchronized boolean deleteUserModel(UserModel model) { | |
return deleteUser(model.username); | |
} | |
/** | |
* Delete the user object with the specified username | |
* | |
* @param username | |
* @return true if successful | |
*/ | |
@Override | |
public synchronized boolean deleteUser(String username) { | |
try { | |
// Read realm file | |
read(); | |
UserModel model = users.remove(username.toLowerCase()); | |
if (model == null) { | |
// user does not exist | |
return false; | |
} | |
// remove user from team | |
for (TeamModel team : model.teams) { | |
TeamModel t = teams.get(team.name); | |
if (t == null) { | |
// new team | |
team.removeUser(username); | |
teams.put(team.name.toLowerCase(), team); | |
} else { | |
// existing team | |
t.removeUser(username); | |
} | |
} | |
write(); | |
return true; | |
} catch (Throwable t) { | |
logger.error(MessageFormat.format("Failed to delete user {0}!", username), t); | |
} | |
return false; | |
} | |
/** | |
* Returns the list of all teams available to the login service. | |
* | |
* @return list of all teams | |
* @since 0.8.0 | |
*/ | |
@Override | |
public synchronized List<String> getAllTeamNames() { | |
read(); | |
List<String> list = new ArrayList<String>(teams.keySet()); | |
Collections.sort(list); | |
return list; | |
} | |
/** | |
* Returns the list of all teams available to the login service. | |
* | |
* @return list of all teams | |
* @since 0.8.0 | |
*/ | |
@Override | |
public synchronized List<TeamModel> getAllTeams() { | |
read(); | |
List<TeamModel> list = new ArrayList<TeamModel>(teams.values()); | |
list = DeepCopier.copy(list); | |
Collections.sort(list); | |
return list; | |
} | |
/** | |
* Returns the list of all users who are allowed to bypass the access | |
* restriction placed on the specified repository. | |
* | |
* @param role | |
* the repository name | |
* @return list of all usernames that can bypass the access restriction | |
*/ | |
@Override | |
public synchronized List<String> getTeamNamesForRepositoryRole(String role) { | |
List<String> list = new ArrayList<String>(); | |
try { | |
read(); | |
for (Map.Entry<String, TeamModel> entry : teams.entrySet()) { | |
TeamModel model = entry.getValue(); | |
if (model.hasRepositoryPermission(role)) { | |
list.add(model.name); | |
} | |
} | |
} catch (Throwable t) { | |
logger.error(MessageFormat.format("Failed to get teamnames for role {0}!", role), t); | |
} | |
Collections.sort(list); | |
return list; | |
} | |
/** | |
* Retrieve the team object for the specified team name. | |
* | |
* @param teamname | |
* @return a team object or null | |
* @since 0.8.0 | |
*/ | |
@Override | |
public synchronized TeamModel getTeamModel(String teamname) { | |
read(); | |
TeamModel model = teams.get(teamname.toLowerCase()); | |
if (model != null) { | |
// clone the model, otherwise all changes to this object are | |
// live and unpersisted | |
model = DeepCopier.copy(model); | |
} | |
return model; | |
} | |
/** | |
* Updates/writes a complete team object. | |
* | |
* @param model | |
* @return true if update is successful | |
* @since 0.8.0 | |
*/ | |
@Override | |
public synchronized boolean updateTeamModel(TeamModel model) { | |
return updateTeamModel(model.name, model); | |
} | |
/** | |
* Updates/writes all specified team objects. | |
* | |
* @param models a list of team models | |
* @return true if update is successful | |
* @since 1.2.0 | |
*/ | |
@Override | |
public synchronized boolean updateTeamModels(Collection<TeamModel> models) { | |
try { | |
read(); | |
for (TeamModel team : models) { | |
teams.put(team.name.toLowerCase(), team); | |
} | |
write(); | |
return true; | |
} catch (Throwable t) { | |
logger.error(MessageFormat.format("Failed to update team {0} models!", models.size()), t); | |
} | |
return false; | |
} | |
/** | |
* Updates/writes and replaces a complete team object keyed by teamname. | |
* This method allows for renaming a team. | |
* | |
* @param teamname | |
* the old teamname | |
* @param model | |
* the team object to use for teamname | |
* @return true if update is successful | |
* @since 0.8.0 | |
*/ | |
@Override | |
public synchronized boolean updateTeamModel(String teamname, TeamModel model) { | |
TeamModel original = null; | |
try { | |
read(); | |
original = teams.remove(teamname.toLowerCase()); | |
teams.put(model.name.toLowerCase(), model); | |
write(); | |
return true; | |
} catch (Throwable t) { | |
if (original != null) { | |
// restore original team | |
teams.put(original.name.toLowerCase(), original); | |
} else { | |
// drop attempted add | |
teams.remove(model.name.toLowerCase()); | |
} | |
logger.error(MessageFormat.format("Failed to update team model {0}!", model.name), t); | |
} | |
return false; | |
} | |
/** | |
* Deletes the team object from the user service. | |
* | |
* @param model | |
* @return true if successful | |
* @since 0.8.0 | |
*/ | |
@Override | |
public synchronized boolean deleteTeamModel(TeamModel model) { | |
return deleteTeam(model.name); | |
} | |
/** | |
* Delete the team object with the specified teamname | |
* | |
* @param teamname | |
* @return true if successful | |
* @since 0.8.0 | |
*/ | |
@Override | |
public synchronized boolean deleteTeam(String teamname) { | |
try { | |
// Read realm file | |
read(); | |
teams.remove(teamname.toLowerCase()); | |
write(); | |
return true; | |
} catch (Throwable t) { | |
logger.error(MessageFormat.format("Failed to delete team {0}!", teamname), t); | |
} | |
return false; | |
} | |
/** | |
* Returns the list of all users available to the login service. | |
* | |
* @return list of all usernames | |
*/ | |
@Override | |
public synchronized List<String> getAllUsernames() { | |
read(); | |
List<String> list = new ArrayList<String>(users.keySet()); | |
Collections.sort(list); | |
return list; | |
} | |
/** | |
* Returns the list of all users available to the login service. | |
* | |
* @return list of all usernames | |
*/ | |
@Override | |
public synchronized List<UserModel> getAllUsers() { | |
read(); | |
List<UserModel> list = new ArrayList<UserModel>(users.values()); | |
list = DeepCopier.copy(list); | |
Collections.sort(list); | |
return list; | |
} | |
/** | |
* Returns the list of all users who are allowed to bypass the access | |
* restriction placed on the specified repository. | |
* | |
* @param role | |
* the repository name | |
* @return list of all usernames that can bypass the access restriction | |
*/ | |
@Override | |
public synchronized List<String> getUsernamesForRepositoryRole(String role) { | |
List<String> list = new ArrayList<String>(); | |
try { | |
read(); | |
for (Map.Entry<String, UserModel> entry : users.entrySet()) { | |
UserModel model = entry.getValue(); | |
if (model.hasRepositoryPermission(role)) { | |
list.add(model.username); | |
} | |
} | |
} catch (Throwable t) { | |
logger.error(MessageFormat.format("Failed to get usernames for role {0}!", role), t); | |
} | |
Collections.sort(list); | |
return list; | |
} | |
/** | |
* Renames a repository role. | |
* | |
* @param oldRole | |
* @param newRole | |
* @return true if successful | |
*/ | |
@Override | |
public synchronized boolean renameRepositoryRole(String oldRole, String newRole) { | |
try { | |
read(); | |
// identify users which require role rename | |
for (UserModel model : users.values()) { | |
if (model.hasRepositoryPermission(oldRole)) { | |
AccessPermission permission = model.removeRepositoryPermission(oldRole); | |
model.setRepositoryPermission(newRole, permission); | |
} | |
} | |
// identify teams which require role rename | |
for (TeamModel model : teams.values()) { | |
if (model.hasRepositoryPermission(oldRole)) { | |
AccessPermission permission = model.removeRepositoryPermission(oldRole); | |
model.setRepositoryPermission(newRole, permission); | |
} | |
} | |
// persist changes | |
write(); | |
return true; | |
} catch (Throwable t) { | |
logger.error( | |
MessageFormat.format("Failed to rename role {0} to {1}!", oldRole, newRole), t); | |
} | |
return false; | |
} | |
/** | |
* Removes a repository role from all users. | |
* | |
* @param role | |
* @return true if successful | |
*/ | |
@Override | |
public synchronized boolean deleteRepositoryRole(String role) { | |
try { | |
read(); | |
// identify users which require role rename | |
for (UserModel user : users.values()) { | |
user.removeRepositoryPermission(role); | |
} | |
// identify teams which require role rename | |
for (TeamModel team : teams.values()) { | |
team.removeRepositoryPermission(role); | |
} | |
// persist changes | |
write(); | |
return true; | |
} catch (Throwable t) { | |
logger.error(MessageFormat.format("Failed to delete role {0}!", role), t); | |
} | |
return false; | |
} | |
/** | |
* Writes the properties file. | |
* | |
* @throws IOException | |
*/ | |
private synchronized void write() throws IOException { | |
// Write a temporary copy of the users file | |
File realmFileCopy = new File(realmFile.getAbsolutePath() + ".tmp"); | |
StoredConfig config = new FileBasedConfig(realmFileCopy, FS.detect()); | |
// write users | |
for (UserModel model : users.values()) { | |
if (!StringUtils.isEmpty(model.password)) { | |
config.setString(USER, model.username, PASSWORD, model.password); | |
} | |
if (!StringUtils.isEmpty(model.cookie)) { | |
config.setString(USER, model.username, COOKIE, model.cookie); | |
} | |
if (!StringUtils.isEmpty(model.displayName)) { | |
config.setString(USER, model.username, DISPLAYNAME, model.displayName); | |
} | |
if (!StringUtils.isEmpty(model.emailAddress)) { | |
config.setString(USER, model.username, EMAILADDRESS, model.emailAddress); | |
} | |
if (model.accountType != null) { | |
config.setString(USER, model.username, ACCOUNTTYPE, model.accountType.name()); | |
} | |
if (!StringUtils.isEmpty(model.organizationalUnit)) { | |
config.setString(USER, model.username, ORGANIZATIONALUNIT, model.organizationalUnit); | |
} | |
if (!StringUtils.isEmpty(model.organization)) { | |
config.setString(USER, model.username, ORGANIZATION, model.organization); | |
} | |
if (!StringUtils.isEmpty(model.locality)) { | |
config.setString(USER, model.username, LOCALITY, model.locality); | |
} | |
if (!StringUtils.isEmpty(model.stateProvince)) { | |
config.setString(USER, model.username, STATEPROVINCE, model.stateProvince); | |
} | |
if (!StringUtils.isEmpty(model.countryCode)) { | |
config.setString(USER, model.username, COUNTRYCODE, model.countryCode); | |
} | |
if (model.disabled) { | |
config.setBoolean(USER, model.username, DISABLED, true); | |
} | |
if (model.getPreferences() != null) { | |
Locale locale = model.getPreferences().getLocale(); | |
if (locale != null) { | |
String val; | |
if (StringUtils.isEmpty(locale.getCountry())) { | |
val = locale.getLanguage(); | |
} else { | |
val = locale.getLanguage() + "_" + locale.getCountry(); | |
} | |
config.setString(USER, model.username, LOCALE, val); | |
} | |
config.setBoolean(USER, model.username, EMAILONMYTICKETCHANGES, model.getPreferences().isEmailMeOnMyTicketChanges()); | |
if (model.getPreferences().getTransport() != null) { | |
config.setString(USER, model.username, TRANSPORT, model.getPreferences().getTransport().name()); | |
} | |
} | |
// user roles | |
List<String> roles = new ArrayList<String>(); | |
if (model.canAdmin) { | |
roles.add(Role.ADMIN.getRole()); | |
} | |
if (model.canFork) { | |
roles.add(Role.FORK.getRole()); | |
} | |
if (model.canCreate) { | |
roles.add(Role.CREATE.getRole()); | |
} | |
if (model.excludeFromFederation) { | |
roles.add(Role.NOT_FEDERATED.getRole()); | |
} | |
if (roles.size() == 0) { | |
// we do this to ensure that user record with no password | |
// is written. otherwise, StoredConfig optimizes that account | |
// away. :( | |
roles.add(Role.NONE.getRole()); | |
} | |
config.setStringList(USER, model.username, ROLE, roles); | |
// discrete repository permissions | |
if (model.permissions != null && !model.canAdmin) { | |
List<String> permissions = new ArrayList<String>(); | |
for (Map.Entry<String, AccessPermission> entry : model.permissions.entrySet()) { | |
if (entry.getValue().exceeds(AccessPermission.NONE)) { | |
permissions.add(entry.getValue().asRole(entry.getKey())); | |
} | |
} | |
config.setStringList(USER, model.username, REPOSITORY, permissions); | |
} | |
// user preferences | |
if (model.getPreferences() != null) { | |
List<String> starred = model.getPreferences().getStarredRepositories(); | |
if (starred.size() > 0) { | |
config.setStringList(USER, model.username, STARRED, starred); | |
} | |
} | |
} | |
// write teams | |
for (TeamModel model : teams.values()) { | |
// team roles | |
List<String> roles = new ArrayList<String>(); | |
if (model.canAdmin) { | |
roles.add(Role.ADMIN.getRole()); | |
} | |
if (model.canFork) { | |
roles.add(Role.FORK.getRole()); | |
} | |
if (model.canCreate) { | |
roles.add(Role.CREATE.getRole()); | |
} | |
if (roles.size() == 0) { | |
// we do this to ensure that team record is written. | |
// Otherwise, StoredConfig might optimizes that record away. | |
roles.add(Role.NONE.getRole()); | |
} | |
config.setStringList(TEAM, model.name, ROLE, roles); | |
if (model.accountType != null) { | |
config.setString(TEAM, model.name, ACCOUNTTYPE, model.accountType.name()); | |
} | |
if (!model.canAdmin) { | |
// write team permission for non-admin teams | |
if (model.permissions == null) { | |
// null check on "final" repositories because JSON-sourced TeamModel | |
// can have a null repositories object | |
if (!ArrayUtils.isEmpty(model.repositories)) { | |
config.setStringList(TEAM, model.name, REPOSITORY, new ArrayList<String>( | |
model.repositories)); | |
} | |
} else { | |
// discrete repository permissions | |
List<String> permissions = new ArrayList<String>(); | |
for (Map.Entry<String, AccessPermission> entry : model.permissions.entrySet()) { | |
if (entry.getValue().exceeds(AccessPermission.NONE)) { | |
// code:repository (e.g. RW+:~james/myrepo.git | |
permissions.add(entry.getValue().asRole(entry.getKey())); | |
} | |
} | |
config.setStringList(TEAM, model.name, REPOSITORY, permissions); | |
} | |
} | |
// null check on "final" users because JSON-sourced TeamModel | |
// can have a null users object | |
if (!ArrayUtils.isEmpty(model.users)) { | |
config.setStringList(TEAM, model.name, USER, new ArrayList<String>(model.users)); | |
} | |
// null check on "final" mailing lists because JSON-sourced | |
// TeamModel can have a null users object | |
if (!ArrayUtils.isEmpty(model.mailingLists)) { | |
config.setStringList(TEAM, model.name, MAILINGLIST, new ArrayList<String>( | |
model.mailingLists)); | |
} | |
// null check on "final" preReceiveScripts because JSON-sourced | |
// TeamModel can have a null preReceiveScripts object | |
if (!ArrayUtils.isEmpty(model.preReceiveScripts)) { | |
config.setStringList(TEAM, model.name, PRERECEIVE, model.preReceiveScripts); | |
} | |
// null check on "final" postReceiveScripts because JSON-sourced | |
// TeamModel can have a null postReceiveScripts object | |
if (!ArrayUtils.isEmpty(model.postReceiveScripts)) { | |
config.setStringList(TEAM, model.name, POSTRECEIVE, model.postReceiveScripts); | |
} | |
} | |
config.save(); | |
// manually set the forceReload flag because not all JVMs support real | |
// millisecond resolution of lastModified. (issue-55) | |
forceReload = true; | |
// If the write is successful, delete the current file and rename | |
// the temporary copy to the original filename. | |
if (realmFileCopy.exists() && realmFileCopy.length() > 0) { | |
if (realmFile.exists()) { | |
if (!realmFile.delete()) { | |
throw new IOException(MessageFormat.format("Failed to delete {0}!", | |
realmFile.getAbsolutePath())); | |
} | |
} | |
if (!realmFileCopy.renameTo(realmFile)) { | |
throw new IOException(MessageFormat.format("Failed to rename {0} to {1}!", | |
realmFileCopy.getAbsolutePath(), realmFile.getAbsolutePath())); | |
} | |
} else { | |
throw new IOException(MessageFormat.format("Failed to save {0}!", | |
realmFileCopy.getAbsolutePath())); | |
} | |
} | |
/** | |
* Reads the realm file and rebuilds the in-memory lookup tables. | |
*/ | |
protected synchronized void read() { | |
if (realmFile.exists() && (forceReload || (realmFile.lastModified() != lastModified))) { | |
forceReload = false; | |
lastModified = realmFile.lastModified(); | |
users.clear(); | |
cookies.clear(); | |
teams.clear(); | |
try { | |
StoredConfig config = new FileBasedConfig(realmFile, FS.detect()); | |
config.load(); | |
Set<String> usernames = config.getSubsections(USER); | |
for (String username : usernames) { | |
UserModel user = new UserModel(username.toLowerCase()); | |
user.password = config.getString(USER, username, PASSWORD); | |
user.displayName = config.getString(USER, username, DISPLAYNAME); | |
user.emailAddress = config.getString(USER, username, EMAILADDRESS); | |
user.accountType = AccountType.fromString(config.getString(USER, username, ACCOUNTTYPE)); | |
if (Constants.EXTERNAL_ACCOUNT.equals(user.password) && user.accountType.isLocal()) { | |
user.accountType = AccountType.EXTERNAL; | |
} | |
user.disabled = config.getBoolean(USER, username, DISABLED, false); | |
user.organizationalUnit = config.getString(USER, username, ORGANIZATIONALUNIT); | |
user.organization = config.getString(USER, username, ORGANIZATION); | |
user.locality = config.getString(USER, username, LOCALITY); | |
user.stateProvince = config.getString(USER, username, STATEPROVINCE); | |
user.countryCode = config.getString(USER, username, COUNTRYCODE); | |
user.cookie = config.getString(USER, username, COOKIE); | |
if (StringUtils.isEmpty(user.cookie) && !StringUtils.isEmpty(user.password)) { | |
user.cookie = StringUtils.getSHA1(user.username + user.password); | |
} | |
// preferences | |
user.getPreferences().setLocale(config.getString(USER, username, LOCALE)); | |
user.getPreferences().setEmailMeOnMyTicketChanges(config.getBoolean(USER, username, EMAILONMYTICKETCHANGES, true)); | |
user.getPreferences().setTransport(Transport.fromString(config.getString(USER, username, TRANSPORT))); | |
// user roles | |
Set<String> roles = new HashSet<String>(Arrays.asList(config.getStringList( | |
USER, username, ROLE))); | |
user.canAdmin = roles.contains(Role.ADMIN.getRole()); | |
user.canFork = roles.contains(Role.FORK.getRole()); | |
user.canCreate = roles.contains(Role.CREATE.getRole()); | |
user.excludeFromFederation = roles.contains(Role.NOT_FEDERATED.getRole()); | |
// repository memberships | |
if (!user.canAdmin) { | |
// non-admin, read permissions | |
Set<String> repositories = new HashSet<String>(Arrays.asList(config | |
.getStringList(USER, username, REPOSITORY))); | |
for (String repository : repositories) { | |
user.addRepositoryPermission(repository); | |
} | |
} | |
// starred repositories | |
Set<String> starred = new HashSet<String>(Arrays.asList(config | |
.getStringList(USER, username, STARRED))); | |
for (String repository : starred) { | |
UserRepositoryPreferences prefs = user.getPreferences().getRepositoryPreferences(repository); | |
prefs.starred = true; | |
} | |
// update cache | |
users.put(user.username, user); | |
if (!StringUtils.isEmpty(user.cookie)) { | |
cookies.put(user.cookie, user); | |
} | |
} | |
// load the teams | |
Set<String> teamnames = config.getSubsections(TEAM); | |
for (String teamname : teamnames) { | |
TeamModel team = new TeamModel(teamname); | |
Set<String> roles = new HashSet<String>(Arrays.asList(config.getStringList( | |
TEAM, teamname, ROLE))); | |
team.canAdmin = roles.contains(Role.ADMIN.getRole()); | |
team.canFork = roles.contains(Role.FORK.getRole()); | |
team.canCreate = roles.contains(Role.CREATE.getRole()); | |
team.accountType = AccountType.fromString(config.getString(TEAM, teamname, ACCOUNTTYPE)); | |
if (!team.canAdmin) { | |
// non-admin team, read permissions | |
team.addRepositoryPermissions(Arrays.asList(config.getStringList(TEAM, teamname, | |
REPOSITORY))); | |
} | |
team.addUsers(Arrays.asList(config.getStringList(TEAM, teamname, USER))); | |
team.addMailingLists(Arrays.asList(config.getStringList(TEAM, teamname, | |
MAILINGLIST))); | |
team.preReceiveScripts.addAll(Arrays.asList(config.getStringList(TEAM, | |
teamname, PRERECEIVE))); | |
team.postReceiveScripts.addAll(Arrays.asList(config.getStringList(TEAM, | |
teamname, POSTRECEIVE))); | |
teams.put(team.name.toLowerCase(), team); | |
// set the teams on the users | |
for (String user : team.users) { | |
UserModel model = users.get(user); | |
if (model != null) { | |
model.teams.add(team); | |
} | |
} | |
} | |
} catch (Exception e) { | |
logger.error(MessageFormat.format("Failed to read {0}", realmFile), e); | |
} | |
} | |
} | |
protected long lastModified() { | |
return lastModified; | |
} | |
@Override | |
public String toString() { | |
return getClass().getSimpleName() + "(" + realmFile.getAbsolutePath() + ")"; | |
} | |
} |