From 27ae9095639bb228a1b7ff86a3ebe4264abf05be Mon Sep 17 00:00:00 2001
From: mschaefers <mschaefers@scoop-gmbh.de>
Date: Thu, 29 Nov 2012 12:33:09 -0500
Subject: [PATCH] feature: when using LdapUserService one can configure Gitblit to fetch all users from ldap that can possibly login. This allows to see newly generated LDAP users instantly in Gitblit. By now an LDAP user had to log in once to appear in GitBlit.

---
 src/com/gitblit/ConfigUserService.java |  350 +++++++++++++++++++++++++++++++++++++++++++++++++++-------
 1 files changed, 308 insertions(+), 42 deletions(-)

diff --git a/src/com/gitblit/ConfigUserService.java b/src/com/gitblit/ConfigUserService.java
index 681efd5..068bbe3 100644
--- a/src/com/gitblit/ConfigUserService.java
+++ b/src/com/gitblit/ConfigUserService.java
@@ -33,6 +33,7 @@
 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;
@@ -60,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";
 
@@ -82,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;
@@ -97,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?
 	 * 
@@ -114,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;
 	}
 
 	/**
@@ -177,6 +241,15 @@
 	}
 
 	/**
+	 * Logout a user.
+	 * 
+	 * @param user
+	 */
+	@Override
+	public void logout(UserModel user) {	
+	}
+	
+	/**
 	 * Retrieve the user object for the specified username.
 	 * 
 	 * @param username
@@ -206,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.
 	 * 
@@ -217,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
@@ -239,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);
 						}
@@ -250,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);
 		}
@@ -343,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);
 				}
 			}
@@ -377,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);
 				}
 			}
 
@@ -425,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.
 	 * 
@@ -437,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;
@@ -524,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);
 				}
 			}
@@ -545,6 +705,7 @@
 	 * @return true if successful
 	 */
 	@Override
+	@Deprecated
 	public boolean setUsernamesForRepositoryRole(String role, List<String> usernames) {
 		try {
 			Set<String> specifiedUsers = new HashSet<String>();
@@ -558,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);
 				}
 			}
 
@@ -587,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
@@ -623,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
@@ -654,34 +815,108 @@
 
 		// 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 (!ArrayUtils.isEmpty(model.repositories)) {
-				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 (!ArrayUtils.isEmpty(model.repositories)) {
-				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
@@ -711,6 +946,9 @@
 		}
 
 		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.
@@ -735,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();
@@ -747,32 +986,59 @@
 				Set<String> usernames = config.getSubsections(USER);
 				for (String username : usernames) {
 					UserModel user = new UserModel(username.toLowerCase());
-					user.password = config.getString(USER, username, PASSWORD);
+					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(user.username, user);
-					cookies.put(StringUtils.getSHA1(user.username + user.password), 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