From 13417cf9c6eec555b51da49742e47939d2f5715b Mon Sep 17 00:00:00 2001 From: James Moger <james.moger@gitblit.com> Date: Fri, 19 Oct 2012 22:47:33 -0400 Subject: [PATCH] Exclude submodules from zip downloads (issue 151) --- src/com/gitblit/FileUserService.java | 679 +++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 files changed, 640 insertions(+), 39 deletions(-) diff --git a/src/com/gitblit/FileUserService.java b/src/com/gitblit/FileUserService.java index 9dc8008..d411b68 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,90 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.gitblit.Constants.AccessPermission; +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) { + } + + /** + * Does the user service support changes to credentials? + * + * @return true or false + * @since 1.0.0 + */ + @Override + public boolean supportsCredentialChanges() { + return true; + } + + /** + * Does the user service support changes to user display name? + * + * @return true or false + * @since 1.0.0 + */ + @Override + public boolean supportsDisplayNameChanges() { + return false; + } + + /** + * Does the user service support changes to user email address? + * + * @return true or false + * @since 1.0.0 + */ + @Override + public boolean supportsEmailAddressChanges() { + return false; + } + + /** + * Does the user service support changes to team memberships? + * + * @return true or false + * @since 1.0.0 + */ + public boolean supportsTeamMembershipChanges() { + return true; } /** @@ -69,13 +134,16 @@ * @return cookie value */ @Override - public char[] getCookie(UserModel model) { + public String getCookie(UserModel model) { + if (!StringUtils.isEmpty(model.cookie)) { + return model.cookie; + } Properties allUsers = super.read(); String value = allUsers.getProperty(model.username); String[] roles = value.split(","); String password = roles[0]; String cookie = StringUtils.getSHA1(model.username + password); - return cookie.toCharArray(); + return cookie; } /** @@ -116,14 +184,32 @@ 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; + } + + /** + * Logout a user. + * + * @param user + */ + @Override + public void logout(UserModel user) { } /** @@ -135,11 +221,11 @@ @Override public UserModel getUserModel(String username) { Properties allUsers = read(); - String userInfo = allUsers.getProperty(username); + String userInfo = allUsers.getProperty(username.toLowerCase()); if (userInfo == null) { return null; } - UserModel model = new UserModel(username); + UserModel model = new UserModel(username.toLowerCase()); String[] userValues = userInfo.split(","); model.password = userValues[0]; for (int i = 1; i < userValues.length; i++) { @@ -149,12 +235,22 @@ // Permissions if (role.equalsIgnoreCase(Constants.ADMIN_ROLE)) { model.canAdmin = true; + } else if (role.equalsIgnoreCase(Constants.FORK_ROLE)) { + model.canFork = true; + } else if (role.equalsIgnoreCase(Constants.CREATE_ROLE)) { + model.canCreate = true; } else if (role.equalsIgnoreCase(Constants.NOT_FEDERATED_ROLE)) { model.excludeFromFederation = true; } break; default: - model.addRepository(role); + model.addRepositoryPermission(role); + } + } + // set the teams for the user + for (TeamModel team : teams.values()) { + if (team.hasUser(username)) { + model.teams.add(DeepCopier.copy(team)); } } return model; @@ -172,6 +268,29 @@ } /** + * Updates/writes all specified user objects. + * + * @param model a list of user models + * @return true if update is successful + * @since 1.2.0 + */ + @Override + public boolean updateUserModels(List<UserModel> models) { + try { + Properties allUsers = read(); + for (UserModel model : models) { + updateUserCache(allUsers, model.username, model); + } + write(allUsers); + return true; + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to update {0} user models!", models.size()), + t); + } + return false; + } + + /** * Updates/writes and replaces a complete user object keyed by username. * This method allows for renaming a user. * @@ -183,20 +302,64 @@ */ @Override public boolean updateUserModel(String username, UserModel model) { - try { + try { Properties allUsers = read(); - ArrayList<String> roles = new ArrayList<String>(model.repositories); + updateUserCache(allUsers, username, model); + write(allUsers); + return true; + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to update user model {0}!", model.username), + 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 + */ + private boolean updateUserCache(Properties allUsers, String username, UserModel model) { + try { + UserModel oldUser = getUserModel(username); + List<String> roles; + if (model.permissions == null) { + // legacy, use repository list + roles = new ArrayList<String>(model.repositories); + } else { + // discrete repository permissions + roles = 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 + roles.add(entry.getValue().asRole(entry.getKey())); + } + } + } // Permissions if (model.canAdmin) { roles.add(Constants.ADMIN_ROLE); + } + if (model.canFork) { + roles.add(Constants.FORK_ROLE); + } + if (model.canCreate) { + roles.add(Constants.CREATE_ROLE); } if (model.excludeFromFederation) { roles.add(Constants.NOT_FEDERATED_ROLE); } StringBuilder sb = new StringBuilder(); - sb.append(model.password); + if (!StringUtils.isEmpty(model.password)) { + sb.append(model.password); + } sb.append(','); for (String role : roles) { sb.append(role); @@ -204,10 +367,34 @@ } // trim trailing comma sb.setLength(sb.length() - 1); - allUsers.remove(username); - allUsers.put(model.username, sb.toString()); + allUsers.remove(username.toLowerCase()); + allUsers.put(model.username.toLowerCase(), sb.toString()); - write(allUsers); + // 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); + } + } + } + } return true; } catch (Throwable t) { logger.error(MessageFormat.format("Failed to update user model {0}!", model.username), @@ -238,7 +425,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) { @@ -255,7 +452,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; } @@ -273,6 +494,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) @@ -287,11 +511,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 @@ -341,12 +566,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(','); } @@ -385,9 +609,9 @@ String[] roles = value.split(","); // skip first value (password) for (int i = 1; i < roles.length; i++) { - String r = roles[i]; - if (r.equalsIgnoreCase(oldRole)) { - needsRenameRole.remove(username); + String repository = AccessPermission.repositoryFromRole(roles[i]); + if (repository.equalsIgnoreCase(oldRole)) { + needsRenameRole.add(username); break; } } @@ -401,14 +625,18 @@ 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); + String repository = AccessPermission.repositoryFromRole(values[i]); + if (repository.equalsIgnoreCase(oldRole)) { + AccessPermission permission = AccessPermission.permissionFromRole(values[i]); + sb.append(permission.asRole(newRole)); + sb.append(','); + } else { + sb.append(values[i]); sb.append(','); } } @@ -445,10 +673,10 @@ String value = allUsers.getProperty(username); String[] roles = value.split(","); // skip first value (password) - for (int i = 1; i < roles.length; i++) { - String r = roles[i]; - if (r.equalsIgnoreCase(role)) { - needsDeleteRole.remove(username); + for (int i = 1; i < roles.length; i++) { + String repository = AccessPermission.repositoryFromRole(roles[i]); + if (repository.equalsIgnoreCase(role)) { + needsDeleteRole.add(username); break; } } @@ -462,13 +690,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); + for (int i = 1; i < values.length; i++) { + String repository = AccessPermission.repositoryFromRole(values[i]); + if (!repository.equalsIgnoreCase(role)) { + sb.append(values[i]); sb.append(','); } } @@ -499,7 +725,7 @@ 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. @@ -526,17 +752,392 @@ @Override protected synchronized Properties read() { long lastRead = lastModified(); + boolean reload = forceReload(); Properties allUsers = super.read(); - if (lastRead != lastModified()) { + if (reload || (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 { + switch (role.charAt(0)) { + case '#': + // Permissions + if (role.equalsIgnoreCase(Constants.ADMIN_ROLE)) { + team.canAdmin = true; + } else if (role.equalsIgnoreCase(Constants.FORK_ROLE)) { + team.canFork = true; + } else if (role.equalsIgnoreCase(Constants.CREATE_ROLE)) { + team.canCreate = true; + } + break; + default: + repositories.add(role); + } + repositories.add(role); + } + } + team.addRepositoryPermissions(repositories); + team.addUsers(users); + team.addMailingLists(mailingLists); + team.preReceiveScripts.addAll(preReceive); + team.postReceiveScripts.addAll(postReceive); + teams.put(team.name.toLowerCase(), team); + } else { + // user definition + String password = roles[0]; + cookies.put(StringUtils.getSHA1(username.toLowerCase() + password), username.toLowerCase()); + } } } 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 all specified team objects. + * + * @param models a list of team models + * @return true if update is successful + * @since 1.2.0 + */ + public boolean updateTeamModels(List<TeamModel> models) { + try { + Properties allUsers = read(); + for (TeamModel model : models) { + updateTeamCache(allUsers, model.name, model); + } + write(allUsers); + return true; + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to update {0} team 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 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(); + List<String> roles; + if (model.permissions == null) { + // legacy, use repository list + if (model.repositories != null) { + roles = new ArrayList<String>(model.repositories); + } else { + roles = new ArrayList<String>(); + } + } else { + // discrete repository permissions + roles = 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 + roles.add(entry.getValue().asRole(entry.getKey())); + } + } + } + + // Permissions + if (model.canAdmin) { + roles.add(Constants.ADMIN_ROLE); + } + if (model.canFork) { + roles.add(Constants.FORK_ROLE); + } + if (model.canCreate) { + roles.add(Constants.CREATE_ROLE); + } + + for (String role : roles) { + sb.append(role); + 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