From fe24a0be919653d9e502f7729d9a804f2e28435d Mon Sep 17 00:00:00 2001 From: James Moger <james.moger@gitblit.com> Date: Wed, 07 Dec 2011 19:33:10 -0500 Subject: [PATCH] Teams support. --- src/com/gitblit/FileUserService.java | 323 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 files changed, 316 insertions(+), 7 deletions(-) diff --git a/src/com/gitblit/FileUserService.java b/src/com/gitblit/FileUserService.java index a98e417..880ca7b 100644 --- a/src/com/gitblit/FileUserService.java +++ b/src/com/gitblit/FileUserService.java @@ -30,7 +30,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.gitblit.models.TeamModel; import com.gitblit.models.UserModel; +import com.gitblit.utils.DeepCopier; import com.gitblit.utils.StringUtils; /** @@ -53,6 +55,8 @@ 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()); } @@ -61,7 +65,7 @@ * Setup the user service. * * @param settings - * @since 0.6.1 + * @since 0.7.0 */ @Override public void setup(IStoredSettings settings) { @@ -181,6 +185,12 @@ 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; } @@ -209,6 +219,7 @@ public boolean updateUserModel(String username, UserModel model) { try { Properties allUsers = read(); + UserModel oldUser = getUserModel(username); ArrayList<String> roles = new ArrayList<String>(model.repositories); // Permissions @@ -230,6 +241,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; @@ -262,7 +299,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) { @@ -279,7 +326,14 @@ @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); + } return list; } @@ -297,6 +351,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) @@ -315,7 +372,7 @@ } /** - * 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 @@ -426,7 +483,7 @@ sb.append(','); sb.append(newRole); sb.append(','); - + // skip first value (password) for (int i = 1; i < values.length; i++) { String value = values[i]; @@ -520,7 +577,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. @@ -551,11 +608,31 @@ 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>(); + for (String role : roles) { + if (role.charAt(0) == '!') { + users.add(role.substring(1)); + } else { + repositories.add(role); + } + } + team.addRepositories(repositories); + team.addUsers(users); + teams.put(team.name.toLowerCase(), team); + } else { + // user definition + String password = roles[0]; + cookies.put(StringUtils.getSHA1(username + password), username); + } } } return allUsers; @@ -565,4 +642,236 @@ 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()); + 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); + } + 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(); + for (String repository : model.repositories) { + sb.append(repository); + sb.append(','); + } + for (String user : model.users) { + sb.append('!'); + sb.append(user); + 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