From 73f1adb2e84b8b9cd4045bcdd7d9afa72d3875b5 Mon Sep 17 00:00:00 2001 From: James Moger <james.moger@gitblit.com> Date: Tue, 27 Nov 2012 17:13:03 -0500 Subject: [PATCH] Added short commit id column to log and history tables (issue 168) --- src/com/gitblit/ConfigUserService.java | 392 +++++++++++++++++++++++++++++++++++++++++++++++++------ 1 files changed, 344 insertions(+), 48 deletions(-) diff --git a/src/com/gitblit/ConfigUserService.java b/src/com/gitblit/ConfigUserService.java index 7e4600c..068bbe3 100644 --- a/src/com/gitblit/ConfigUserService.java +++ b/src/com/gitblit/ConfigUserService.java @@ -33,8 +33,10 @@ 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; @@ -59,6 +61,22 @@ 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"; @@ -81,6 +99,8 @@ 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; @@ -96,6 +116,49 @@ 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 true; + } + + /** + * Does the user service support changes to user email address? + * + * @return true or false + * @since 1.0.0 + */ + @Override + public boolean supportsEmailAddressChanges() { + return true; + } + + /** + * Does the user service support changes to team memberships? + * + * @return true or false + * @since 1.0.0 + */ + public boolean supportsTeamMembershipChanges() { + return true; + } + /** * Does the user service support cookie authentication? * @@ -113,11 +176,13 @@ * @return cookie value */ @Override - public char[] getCookie(UserModel model) { + public String getCookie(UserModel model) { + if (!StringUtils.isEmpty(model.cookie)) { + return model.cookie; + } read(); UserModel storedModel = users.get(model.username.toLowerCase()); - String cookie = StringUtils.getSHA1(model.username + storedModel.password); - return cookie.toCharArray(); + return storedModel.cookie; } /** @@ -176,6 +241,15 @@ } /** + * Logout a user. + * + * @param user + */ + @Override + public void logout(UserModel user) { + } + + /** * Retrieve the user object for the specified username. * * @param username @@ -205,6 +279,55 @@ } /** + * 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 boolean updateUserModels(List<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) { + for (TeamModel team : model.teams) { + TeamModel t = teams.get(team.name.toLowerCase()); + if (t == null) { + // new team + team.addUser(model.username); + teams.put(team.name.toLowerCase(), team); + } else { + // do not clobber existing team definition + // maybe because this is a federated user + t.addUser(model.username); + } + } + + // 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. * @@ -216,9 +339,10 @@ */ @Override public boolean updateUserModel(String username, UserModel model) { + UserModel originalUser = null; try { read(); - UserModel oldUser = users.remove(username.toLowerCase()); + originalUser = users.remove(username.toLowerCase()); users.put(model.username.toLowerCase(), model); // null check on "final" teams because JSON-sourced UserModel // can have a null teams object @@ -238,8 +362,8 @@ } // check for implicit team removal - if (oldUser != null) { - for (TeamModel team : oldUser.teams) { + if (originalUser != null) { + for (TeamModel team : originalUser.teams) { if (!model.isTeamMember(team.name)) { team.removeUser(username); } @@ -249,6 +373,13 @@ 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); } @@ -313,6 +444,21 @@ } /** + * 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() { + 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. * @@ -327,7 +473,7 @@ read(); for (Map.Entry<String, TeamModel> entry : teams.entrySet()) { TeamModel model = entry.getValue(); - if (model.hasRepository(role)) { + if (model.hasRepositoryPermission(role)) { list.add(model.name); } } @@ -361,10 +507,10 @@ for (TeamModel team : teams.values()) { // team has role, check against revised team list if (specifiedTeams.contains(team.name.toLowerCase())) { - team.addRepository(role); + team.addRepositoryPermission(role); } else { // remove role from team - team.removeRepository(role); + team.removeRepositoryPermission(role); } } @@ -409,6 +555,28 @@ } /** + * 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 boolean updateTeamModels(List<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. * @@ -421,13 +589,21 @@ */ @Override public boolean updateTeamModel(String teamname, TeamModel model) { + TeamModel original = null; try { read(); - teams.remove(teamname.toLowerCase()); + 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; @@ -478,6 +654,20 @@ 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>(users.values()); + list = DeepCopier.copy(list); + Collections.sort(list); + return list; + } /** * Returns the list of all users who are allowed to bypass the access @@ -494,7 +684,7 @@ read(); for (Map.Entry<String, UserModel> entry : users.entrySet()) { UserModel model = entry.getValue(); - if (model.hasRepository(role)) { + if (model.hasRepositoryPermission(role)) { list.add(model.username); } } @@ -515,6 +705,7 @@ * @return true if successful */ @Override + @Deprecated public boolean setUsernamesForRepositoryRole(String role, List<String> usernames) { try { Set<String> specifiedUsers = new HashSet<String>(); @@ -528,10 +719,10 @@ for (UserModel user : users.values()) { // user has role, check against revised user list if (specifiedUsers.contains(user.username.toLowerCase())) { - user.addRepository(role); + user.addRepositoryPermission(role); } else { // remove role from user - user.removeRepository(role); + user.removeRepositoryPermission(role); } } @@ -557,17 +748,17 @@ read(); // identify users which require role rename for (UserModel model : users.values()) { - if (model.hasRepository(oldRole)) { - model.removeRepository(oldRole); - model.addRepository(newRole); + 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.hasRepository(oldRole)) { - model.removeRepository(oldRole); - model.addRepository(newRole); + if (model.hasRepositoryPermission(oldRole)) { + AccessPermission permission = model.removeRepositoryPermission(oldRole); + model.setRepositoryPermission(newRole, permission); } } // persist changes @@ -593,12 +784,12 @@ // identify users which require role rename for (UserModel user : users.values()) { - user.removeRepository(role); + user.removeRepositoryPermission(role); } // identify teams which require role rename for (TeamModel team : teams.values()) { - team.removeRepository(role); + team.removeRepositoryPermission(role); } // persist changes @@ -624,63 +815,140 @@ // write users for (UserModel model : users.values()) { - config.setString(USER, model.username, PASSWORD, model.password); + 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 (!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); + } // user roles List<String> roles = new ArrayList<String>(); 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); } + 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(Constants.NO_ROLE); + } config.setStringList(USER, model.username, ROLE, roles); - // repository memberships - // null check on "final" repositories because JSON-sourced UserModel - // can have a null repositories object - if (model.repositories != null) { - config.setStringList(USER, model.username, REPOSITORY, new ArrayList<String>( - model.repositories)); + // 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); } } // write teams for (TeamModel model : teams.values()) { - // null check on "final" repositories because JSON-sourced TeamModel - // can have a null repositories object - if (model.repositories != null) { - config.setStringList(TEAM, model.name, REPOSITORY, new ArrayList<String>( - model.repositories)); + // team roles + List<String> roles = new ArrayList<String>(); + 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 (roles.size() == 0) { + // we do this to ensure that team record is written. + // Otherwise, StoredConfig might optimizes that record away. + roles.add(Constants.NO_ROLE); + } + config.setStringList(TEAM, model.name, ROLE, roles); + + 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 (model.users != null) { + 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 (model.mailingLists != null) { + 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 (model.preReceiveScripts != null) { + 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 (model.postReceiveScripts != null) { + 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. @@ -705,7 +973,8 @@ * Reads the realm file and rebuilds the in-memory lookup tables. */ protected synchronized void read() { - if (realmFile.exists() && (realmFile.lastModified() > lastModified)) { + if (realmFile.exists() && (forceReload || (realmFile.lastModified() != lastModified))) { + forceReload = false; lastModified = realmFile.lastModified(); users.clear(); cookies.clear(); @@ -716,33 +985,60 @@ config.load(); Set<String> usernames = config.getSubsections(USER); for (String username : usernames) { - UserModel user = new UserModel(username); - user.password = config.getString(USER, username, PASSWORD); + 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.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); + } // user roles Set<String> roles = new HashSet<String>(Arrays.asList(config.getStringList( USER, username, ROLE))); user.canAdmin = roles.contains(Constants.ADMIN_ROLE); + user.canFork = roles.contains(Constants.FORK_ROLE); + user.canCreate = roles.contains(Constants.CREATE_ROLE); user.excludeFromFederation = roles.contains(Constants.NOT_FEDERATED_ROLE); // repository memberships - Set<String> repositories = new HashSet<String>(Arrays.asList(config - .getStringList(USER, username, REPOSITORY))); - for (String repository : repositories) { - user.addRepository(repository); + 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); + } } // update cache - users.put(username, user); - cookies.put(StringUtils.getSHA1(username + user.password), user); + 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); - team.addRepositories(Arrays.asList(config.getStringList(TEAM, teamname, - REPOSITORY))); + Set<String> roles = new HashSet<String>(Arrays.asList(config.getStringList( + TEAM, teamname, ROLE))); + team.canAdmin = roles.contains(Constants.ADMIN_ROLE); + team.canFork = roles.contains(Constants.FORK_ROLE); + team.canCreate = roles.contains(Constants.CREATE_ROLE); + + 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))); -- Gitblit v1.9.1