From cb285cbfddfc0b633d6b8cdb4dc0d2bd2b8b51ef Mon Sep 17 00:00:00 2001 From: James Moger <james.moger@gitblit.com> Date: Thu, 05 Jan 2012 17:34:05 -0500 Subject: [PATCH] Fixed bug in receive hook for repositories in subfolders --- src/com/gitblit/FileUserService.java | 456 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 files changed, 434 insertions(+), 22 deletions(-) diff --git a/src/com/gitblit/FileUserService.java b/src/com/gitblit/FileUserService.java index 12164ce..37ca9a7 100644 --- a/src/com/gitblit/FileUserService.java +++ b/src/com/gitblit/FileUserService.java @@ -20,6 +20,7 @@ import java.io.IOException; import java.text.MessageFormat; import java.util.ArrayList; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -30,26 +31,46 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.gitblit.models.TeamModel; import com.gitblit.models.UserModel; +import com.gitblit.utils.ArrayUtils; +import com.gitblit.utils.DeepCopier; import com.gitblit.utils.StringUtils; /** - * FileUserService is Gitblit's default user service implementation. + * FileUserService is Gitblit's original default user service implementation. * * Users and their repository memberships are stored in a simple properties file * which is cached and dynamically reloaded when modified. * + * This class was deprecated in Gitblit 0.8.0 in favor of ConfigUserService + * which is still a human-readable, editable, plain-text file but it is more + * flexible for storing additional fields. + * * @author James Moger * */ +@Deprecated public class FileUserService extends FileSettings implements IUserService { private final Logger logger = LoggerFactory.getLogger(FileUserService.class); private final Map<String, String> cookies = new ConcurrentHashMap<String, String>(); + private final Map<String, TeamModel> teams = new ConcurrentHashMap<String, TeamModel>(); + public FileUserService(File realmFile) { super(realmFile.getAbsolutePath()); + } + + /** + * Setup the user service. + * + * @param settings + * @since 0.7.0 + */ + @Override + public void setup(IStoredSettings settings) { } /** @@ -116,11 +137,20 @@ UserModel returnedUser = null; UserModel user = getUserModel(username); if (user.password.startsWith(StringUtils.MD5_TYPE)) { + // password digest String md5 = StringUtils.MD5_TYPE + StringUtils.getMD5(new String(password)); if (user.password.equalsIgnoreCase(md5)) { returnedUser = user; } + } else if (user.password.startsWith(StringUtils.COMBINED_MD5_TYPE)) { + // username+password digest + String md5 = StringUtils.COMBINED_MD5_TYPE + + StringUtils.getMD5(username.toLowerCase() + new String(password)); + if (user.password.equalsIgnoreCase(md5)) { + returnedUser = user; + } } else if (user.password.equals(new String(password))) { + // plain-text password returnedUser = user; } return returnedUser; @@ -149,10 +179,18 @@ // Permissions if (role.equalsIgnoreCase(Constants.ADMIN_ROLE)) { model.canAdmin = true; + } else if (role.equalsIgnoreCase(Constants.NOT_FEDERATED_ROLE)) { + model.excludeFromFederation = true; } break; default: model.addRepository(role); + } + } + // set the teams for the user + for (TeamModel team : teams.values()) { + if (team.hasUser(username)) { + model.teams.add(DeepCopier.copy(team)); } } return model; @@ -183,11 +221,15 @@ public boolean updateUserModel(String username, UserModel model) { try { Properties allUsers = read(); + UserModel oldUser = getUserModel(username); ArrayList<String> roles = new ArrayList<String>(model.repositories); // Permissions if (model.canAdmin) { roles.add(Constants.ADMIN_ROLE); + } + if (model.excludeFromFederation) { + roles.add(Constants.NOT_FEDERATED_ROLE); } StringBuilder sb = new StringBuilder(); @@ -201,6 +243,32 @@ sb.setLength(sb.length() - 1); allUsers.remove(username); allUsers.put(model.username, sb.toString()); + + // null check on "final" teams because JSON-sourced UserModel + // can have a null teams object + if (model.teams != null) { + // update team cache + for (TeamModel team : model.teams) { + TeamModel t = getTeamModel(team.name); + if (t == null) { + // new team + t = team; + } + t.removeUser(username); + t.addUser(model.username); + updateTeamCache(allUsers, t.name, t); + } + + // check for implicit team removal + if (oldUser != null) { + for (TeamModel team : oldUser.teams) { + if (!model.isTeamMember(team.name)) { + team.removeUser(username); + updateTeamCache(allUsers, team.name, team); + } + } + } + } write(allUsers); return true; @@ -233,7 +301,17 @@ try { // Read realm file Properties allUsers = read(); + UserModel user = getUserModel(username); allUsers.remove(username); + for (TeamModel team : user.teams) { + TeamModel t = getTeamModel(team.name); + if (t == null) { + // new team + t = team; + } + t.removeUser(username); + updateTeamCache(allUsers, t.name, t); + } write(allUsers); return true; } catch (Throwable t) { @@ -250,7 +328,31 @@ @Override public List<String> getAllUsernames() { Properties allUsers = read(); - List<String> list = new ArrayList<String>(allUsers.stringPropertyNames()); + List<String> list = new ArrayList<String>(); + for (String user : allUsers.stringPropertyNames()) { + if (user.charAt(0) == '@') { + // skip team user definitions + continue; + } + list.add(user); + } + Collections.sort(list); + return list; + } + + /** + * Returns the list of all users available to the login service. + * + * @return list of all usernames + */ + @Override + public List<UserModel> getAllUsers() { + read(); + List<UserModel> list = new ArrayList<UserModel>(); + for (String username : getAllUsernames()) { + list.add(getUserModel(username)); + } + Collections.sort(list); return list; } @@ -268,6 +370,9 @@ try { Properties allUsers = read(); for (String username : allUsers.stringPropertyNames()) { + if (username.charAt(0) == '@') { + continue; + } String value = allUsers.getProperty(username); String[] values = value.split(","); // skip first value (password) @@ -282,11 +387,12 @@ } catch (Throwable t) { logger.error(MessageFormat.format("Failed to get usernames for role {0}!", role), t); } + Collections.sort(list); return list; } /** - * Sets the list of all uses who are allowed to bypass the access + * Sets the list of all users who are allowed to bypass the access * restriction placed on the specified repository. * * @param role @@ -336,12 +442,11 @@ StringBuilder sb = new StringBuilder(); sb.append(password); sb.append(','); - List<String> revisedRoles = new ArrayList<String>(); + // skip first value (password) for (int i = 1; i < values.length; i++) { String value = values[i]; if (!value.equalsIgnoreCase(role)) { - revisedRoles.add(value); sb.append(value); sb.append(','); } @@ -382,7 +487,7 @@ for (int i = 1; i < roles.length; i++) { String r = roles[i]; if (r.equalsIgnoreCase(oldRole)) { - needsRenameRole.remove(username); + needsRenameRole.add(username); break; } } @@ -396,13 +501,13 @@ StringBuilder sb = new StringBuilder(); sb.append(password); sb.append(','); - List<String> revisedRoles = new ArrayList<String>(); - revisedRoles.add(newRole); + sb.append(newRole); + sb.append(','); + // skip first value (password) for (int i = 1; i < values.length; i++) { String value = values[i]; if (!value.equalsIgnoreCase(oldRole)) { - revisedRoles.add(value); sb.append(value); sb.append(','); } @@ -443,7 +548,7 @@ for (int i = 1; i < roles.length; i++) { String r = roles[i]; if (r.equalsIgnoreCase(role)) { - needsDeleteRole.remove(username); + needsDeleteRole.add(username); break; } } @@ -457,12 +562,10 @@ StringBuilder sb = new StringBuilder(); sb.append(password); sb.append(','); - List<String> revisedRoles = new ArrayList<String>(); // skip first value (password) for (int i = 1; i < values.length; i++) { String value = values[i]; if (!value.equalsIgnoreCase(role)) { - revisedRoles.add(value); sb.append(value); sb.append(','); } @@ -494,19 +597,20 @@ FileWriter writer = new FileWriter(realmFileCopy); properties .store(writer, - "# Gitblit realm file format: username=password,\\#permission,repository1,repository2..."); + " Gitblit realm file format:\n username=password,\\#permission,repository1,repository2...\n @teamname=!username1,!username2,!username3,repository1,repository2..."); writer.close(); // 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 (propertiesFile.delete()) { - if (!realmFileCopy.renameTo(propertiesFile)) { - throw new IOException(MessageFormat.format("Failed to rename {0} to {1}!", - realmFileCopy.getAbsolutePath(), propertiesFile.getAbsolutePath())); + if (propertiesFile.exists()) { + if (!propertiesFile.delete()) { + throw new IOException(MessageFormat.format("Failed to delete {0}!", + propertiesFile.getAbsolutePath())); } - } else { - throw new IOException(MessageFormat.format("Failed to delete (0)!", - propertiesFile.getAbsolutePath())); + } + if (!realmFileCopy.renameTo(propertiesFile)) { + throw new IOException(MessageFormat.format("Failed to rename {0} to {1}!", + realmFileCopy.getAbsolutePath(), propertiesFile.getAbsolutePath())); } } else { throw new IOException(MessageFormat.format("Failed to save {0}!", @@ -524,13 +628,321 @@ if (lastRead != lastModified()) { // reload hash cache cookies.clear(); + teams.clear(); + for (String username : allUsers.stringPropertyNames()) { String value = allUsers.getProperty(username); String[] roles = value.split(","); - String password = roles[0]; - cookies.put(StringUtils.getSHA1(username + password), username); + if (username.charAt(0) == '@') { + // team definition + TeamModel team = new TeamModel(username.substring(1)); + List<String> repositories = new ArrayList<String>(); + List<String> users = new ArrayList<String>(); + List<String> mailingLists = new ArrayList<String>(); + List<String> preReceive = new ArrayList<String>(); + List<String> postReceive = new ArrayList<String>(); + for (String role : roles) { + if (role.charAt(0) == '!') { + users.add(role.substring(1)); + } else if (role.charAt(0) == '&') { + mailingLists.add(role.substring(1)); + } else if (role.charAt(0) == '^') { + preReceive.add(role.substring(1)); + } else if (role.charAt(0) == '%') { + postReceive.add(role.substring(1)); + } else { + repositories.add(role); + } + } + team.addRepositories(repositories); + team.addUsers(users); + team.addMailingLists(mailingLists); + teams.put(team.name.toLowerCase(), team); + } else { + // user definition + String password = roles[0]; + cookies.put(StringUtils.getSHA1(username + password), username); + } } } return allUsers; } + + @Override + public String toString() { + return getClass().getSimpleName() + "(" + propertiesFile.getAbsolutePath() + ")"; + } + + /** + * Returns the list of all teams available to the login service. + * + * @return list of all teams + * @since 0.8.0 + */ + @Override + public List<String> getAllTeamNames() { + 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 List<TeamModel> getAllTeams() { + List<TeamModel> list = new ArrayList<TeamModel>(teams.values()); + list = DeepCopier.copy(list); + Collections.sort(list); + return list; + } + + /** + * Returns the list of all teams who are allowed to bypass the access + * restriction placed on the specified repository. + * + * @param role + * the repository name + * @return list of all teamnames that can bypass the access restriction + */ + @Override + public List<String> getTeamnamesForRepositoryRole(String role) { + List<String> list = new ArrayList<String>(); + try { + Properties allUsers = read(); + for (String team : allUsers.stringPropertyNames()) { + if (team.charAt(0) != '@') { + // skip users + continue; + } + String value = allUsers.getProperty(team); + String[] values = value.split(","); + for (int i = 0; i < values.length; i++) { + String r = values[i]; + if (r.equalsIgnoreCase(role)) { + // strip leading @ + list.add(team.substring(1)); + break; + } + } + } + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to get teamnames for role {0}!", role), t); + } + Collections.sort(list); + return list; + } + + /** + * Sets the list of all teams who are allowed to bypass the access + * restriction placed on the specified repository. + * + * @param role + * the repository name + * @param teamnames + * @return true if successful + */ + @Override + public boolean setTeamnamesForRepositoryRole(String role, List<String> teamnames) { + try { + Set<String> specifiedTeams = new HashSet<String>(teamnames); + Set<String> needsAddRole = new HashSet<String>(specifiedTeams); + Set<String> needsRemoveRole = new HashSet<String>(); + + // identify teams which require add and remove role + Properties allUsers = read(); + for (String team : allUsers.stringPropertyNames()) { + if (team.charAt(0) != '@') { + // skip users + continue; + } + String name = team.substring(1); + String value = allUsers.getProperty(team); + String[] values = value.split(","); + for (int i = 0; i < values.length; i++) { + String r = values[i]; + if (r.equalsIgnoreCase(role)) { + // team has role, check against revised team list + if (specifiedTeams.contains(name)) { + needsAddRole.remove(name); + } else { + // remove role from team + needsRemoveRole.add(name); + } + break; + } + } + } + + // add roles to teams + for (String name : needsAddRole) { + String team = "@" + name; + String teamValues = allUsers.getProperty(team); + teamValues += "," + role; + allUsers.put(team, teamValues); + } + + // remove role from team + for (String name : needsRemoveRole) { + String team = "@" + name; + String[] values = allUsers.getProperty(team).split(","); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < values.length; i++) { + String value = values[i]; + if (!value.equalsIgnoreCase(role)) { + sb.append(value); + sb.append(','); + } + } + sb.setLength(sb.length() - 1); + + // update properties + allUsers.put(team, sb.toString()); + } + + // persist changes + write(allUsers); + return true; + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to set teamnames for role {0}!", role), t); + } + return false; + } + + /** + * Retrieve the team object for the specified team name. + * + * @param teamname + * @return a team object or null + * @since 0.8.0 + */ + @Override + public TeamModel getTeamModel(String teamname) { + read(); + TeamModel team = teams.get(teamname.toLowerCase()); + if (team != null) { + // clone the model, otherwise all changes to this object are + // live and unpersisted + team = DeepCopier.copy(team); + } + return team; + } + + /** + * Updates/writes a complete team object. + * + * @param model + * @return true if update is successful + * @since 0.8.0 + */ + @Override + public boolean updateTeamModel(TeamModel model) { + return updateTeamModel(model.name, model); + } + + /** + * 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 boolean updateTeamModel(String teamname, TeamModel model) { + try { + Properties allUsers = read(); + updateTeamCache(allUsers, teamname, model); + write(allUsers); + return true; + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to update team model {0}!", model.name), t); + } + return false; + } + + private void updateTeamCache(Properties allUsers, String teamname, TeamModel model) { + StringBuilder sb = new StringBuilder(); + if (!ArrayUtils.isEmpty(model.repositories)) { + for (String repository : model.repositories) { + sb.append(repository); + sb.append(','); + } + } + if (!ArrayUtils.isEmpty(model.users)) { + for (String user : model.users) { + sb.append('!'); + sb.append(user); + sb.append(','); + } + } + if (!ArrayUtils.isEmpty(model.mailingLists)) { + for (String address : model.mailingLists) { + sb.append('&'); + sb.append(address); + sb.append(','); + } + } + if (!ArrayUtils.isEmpty(model.preReceiveScripts)) { + for (String script : model.preReceiveScripts) { + sb.append('^'); + sb.append(script); + sb.append(','); + } + } + if (!ArrayUtils.isEmpty(model.postReceiveScripts)) { + for (String script : model.postReceiveScripts) { + sb.append('%'); + sb.append(script); + sb.append(','); + } + } + // trim trailing comma + sb.setLength(sb.length() - 1); + allUsers.remove("@" + teamname); + allUsers.put("@" + model.name, sb.toString()); + + // update team cache + teams.remove(teamname.toLowerCase()); + teams.put(model.name.toLowerCase(), model); + } + + /** + * Deletes the team object from the user service. + * + * @param model + * @return true if successful + * @since 0.8.0 + */ + @Override + public 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 boolean deleteTeam(String teamname) { + Properties allUsers = read(); + teams.remove(teamname.toLowerCase()); + allUsers.remove("@" + teamname); + try { + write(allUsers); + return true; + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to delete team {0}!", teamname), t); + } + return false; + } } -- Gitblit v1.9.1