From 8f1c9fd7e0f7ea3d7d0b87788eb92ba2f0f09d59 Mon Sep 17 00:00:00 2001
From: James Moger <james.moger@gitblit.com>
Date: Fri, 29 Nov 2013 11:05:51 -0500
Subject: [PATCH] Extract UserManager from GitBlit singleton

---
 src/main/java/com/gitblit/SalesforceUserService.java  |    2 
 src/main/java/com/gitblit/RedmineUserService.java     |    2 
 src/main/java/com/gitblit/WindowsUserService.java     |    2 
 src/main/java/com/gitblit/GitblitUserService.java     |   13 
 src/main/java/com/gitblit/manager/UserManager.java    |  542 ++++++++++++++++++++++++++++++
 src/main/java/com/gitblit/GitBlit.java                |  417 ++--------------------
 src/main/java/com/gitblit/PAMUserService.java         |    2 
 src/main/java/com/gitblit/IUserService.java           |   11 
 src/test/java/de/akquinet/devops/GitBlit4UITests.java |    2 
 src/main/java/com/gitblit/DaggerModule.java           |    5 
 src/main/java/com/gitblit/Gitblit.java                |    4 
 src/main/java/com/gitblit/ConfigUserService.java      |    8 
 src/main/java/com/gitblit/HtpasswdUserService.java    |    2 
 src/main/java/com/gitblit/utils/StringUtils.java      |   21 +
 src/main/java/com/gitblit/manager/IUserManager.java   |    4 
 src/main/java/com/gitblit/LdapUserService.java        |    2 
 16 files changed, 653 insertions(+), 386 deletions(-)

diff --git a/src/main/java/com/gitblit/ConfigUserService.java b/src/main/java/com/gitblit/ConfigUserService.java
index 79cbdba..39374e8 100644
--- a/src/main/java/com/gitblit/ConfigUserService.java
+++ b/src/main/java/com/gitblit/ConfigUserService.java
@@ -35,6 +35,7 @@
 import org.slf4j.LoggerFactory;
 
 import com.gitblit.Constants.AccessPermission;
+import com.gitblit.Constants.AccountType;
 import com.gitblit.manager.IRuntimeManager;
 import com.gitblit.models.TeamModel;
 import com.gitblit.models.UserModel;
@@ -486,7 +487,7 @@
 	 * @return list of all usernames that can bypass the access restriction
 	 */
 	@Override
-	public synchronized List<String> getTeamnamesForRepositoryRole(String role) {
+	public synchronized List<String> getTeamNamesForRepositoryRole(String role) {
 		List<String> list = new ArrayList<String>();
 		try {
 			read();
@@ -1111,4 +1112,9 @@
 	public String toString() {
 		return getClass().getSimpleName() + "(" + realmFile.getAbsolutePath() + ")";
 	}
+
+	@Override
+	public AccountType getAccountType() {
+		return AccountType.LOCAL;
+	}
 }
diff --git a/src/main/java/com/gitblit/DaggerModule.java b/src/main/java/com/gitblit/DaggerModule.java
index 2f060c0..7f12fca 100644
--- a/src/main/java/com/gitblit/DaggerModule.java
+++ b/src/main/java/com/gitblit/DaggerModule.java
@@ -30,6 +30,7 @@
 import com.gitblit.manager.IUserManager;
 import com.gitblit.manager.NotificationManager;
 import com.gitblit.manager.RuntimeManager;
+import com.gitblit.manager.UserManager;
 import com.gitblit.wicket.GitBlitWebApp;
 import com.gitblit.wicket.GitblitWicketFilter;
 
@@ -100,8 +101,8 @@
 		return new NotificationManager(settings);
 	}
 
-	@Provides @Singleton IUserManager provideUserManager() {
-		return gitblit;
+	@Provides @Singleton IUserManager provideUserManager(IRuntimeManager runtimeManager) {
+		return new UserManager(runtimeManager);
 	}
 
 	@Provides @Singleton ISessionManager provideSessionManager() {
diff --git a/src/main/java/com/gitblit/GitBlit.java b/src/main/java/com/gitblit/GitBlit.java
index 4e77974..f7e7627 100644
--- a/src/main/java/com/gitblit/GitBlit.java
+++ b/src/main/java/com/gitblit/GitBlit.java
@@ -80,7 +80,6 @@
 
 import com.gitblit.Constants.AccessPermission;
 import com.gitblit.Constants.AccessRestrictionType;
-import com.gitblit.Constants.AccountType;
 import com.gitblit.Constants.AuthenticationType;
 import com.gitblit.Constants.AuthorizationControl;
 import com.gitblit.Constants.CommitMessageRenderer;
@@ -162,8 +161,7 @@
  */
 @WebListener
 public class GitBlit extends DaggerContextListener
-					 implements IUserManager,
-								ISessionManager,
+					 implements ISessionManager,
 								IRepositoryManager,
 								IProjectManager,
 								IFederationManager,
@@ -202,8 +200,6 @@
 
 	private File repositoriesFolder;
 
-	private IUserService userService;
-
 	private IStoredSettings settings;
 
 	private LuceneExecutor luceneExecutor;
@@ -221,13 +217,6 @@
 	public GitBlit() {
 		this.goSettings = null;
 		this.goBaseFolder = null;
-	}
-
-	protected GitBlit(final IUserService userService) {
-		this.goSettings = null;
-		this.goBaseFolder = null;
-		this.userService = userService;
-		gitblit = this;
 	}
 
 	public GitBlit(IStoredSettings settings, File baseFolder) {
@@ -335,7 +324,7 @@
 		if (user == null) {
 			user = UserModel.ANONYMOUS;
 		}
-		String username = encodeUsername(UserModel.ANONYMOUS.equals(user) ? "" : user.username);
+		String username = StringUtils.encodeUsername(UserModel.ANONYMOUS.equals(user) ? "" : user.username);
 
 		List<RepositoryUrl> list = new ArrayList<RepositoryUrl>();
 		// http/https url
@@ -486,75 +475,6 @@
 	}
 
 	/**
-	 * Set the user service. The user service authenticates all users and is
-	 * responsible for managing user permissions.
-	 *
-	 * @param userService
-	 */
-	public void setUserService(IUserService userService) {
-		logger.info("Setting up user service " + userService.toString());
-		this.userService = userService;
-		this.userService.setup(getManager(IRuntimeManager.class));
-	}
-
-	@Override
-	public boolean supportsAddUser() {
-		return supportsCredentialChanges(new UserModel(""));
-	}
-
-	/**
-	 * Returns true if the user's credentials can be changed.
-	 *
-	 * @param user
-	 * @return true if the user service supports credential changes
-	 */
-	@Override
-	public boolean supportsCredentialChanges(UserModel user) {
-		if (user == null) {
-			return false;
-		} else if (AccountType.LOCAL.equals(user.accountType)) {
-			// local account, we can change credentials
-			return true;
-		} else {
-			// external account, ask user service
-			return userService.supportsCredentialChanges();
-		}
-	}
-
-	/**
-	 * Returns true if the user's display name can be changed.
-	 *
-	 * @param user
-	 * @return true if the user service supports display name changes
-	 */
-	@Override
-	public boolean supportsDisplayNameChanges(UserModel user) {
-		return (user != null && user.isLocalAccount()) || userService.supportsDisplayNameChanges();
-	}
-
-	/**
-	 * Returns true if the user's email address can be changed.
-	 *
-	 * @param user
-	 * @return true if the user service supports email address changes
-	 */
-	@Override
-	public boolean supportsEmailAddressChanges(UserModel user) {
-		return (user != null && user.isLocalAccount()) || userService.supportsEmailAddressChanges();
-	}
-
-	/**
-	 * Returns true if the user's team memberships can be changed.
-	 *
-	 * @param user
-	 * @return true if the user service supports team membership changes
-	 */
-	@Override
-	public boolean supportsTeamMembershipChanges(UserModel user) {
-		return (user != null && user.isLocalAccount()) || userService.supportsTeamMembershipChanges();
-	}
-
-	/**
 	 * Returns true if the username represents an internal account
 	 *
 	 * @param username
@@ -580,7 +500,7 @@
 			// can not authenticate empty username
 			return null;
 		}
-		String usernameDecoded = decodeUsername(username);
+		String usernameDecoded = StringUtils.decodeUsername(username);
 		String pw = new String(password);
 		if (StringUtils.isEmpty(pw)) {
 			// can not authenticate empty password
@@ -598,10 +518,7 @@
 		}
 
 		// delegate authentication to the user service
-		if (userService == null) {
-			return null;
-		}
-		return userService.authenticate(usernameDecoded, password);
+		return getManager(IUserManager.class).authenticate(usernameDecoded, password);
 	}
 
 	/**
@@ -611,15 +528,12 @@
 	 * @return a user object or null
 	 */
 	protected UserModel authenticate(Cookie[] cookies) {
-		if (userService == null) {
-			return null;
-		}
-		if (userService.supportsCookies()) {
+		if (getManager(IUserManager.class).supportsCookies()) {
 			if (cookies != null && cookies.length > 0) {
 				for (Cookie cookie : cookies) {
 					if (cookie.getName().equals(Constants.NAME)) {
 						String value = cookie.getValue();
-						return userService.authenticate(value.toCharArray());
+						return getManager(IUserManager.class).authenticate(value.toCharArray());
 					}
 				}
 			}
@@ -658,7 +572,7 @@
 		UserModel model = HttpUtils.getUserModelFromCertificate(httpRequest, checkValidity, oids);
 		if (model != null) {
 			// grab real user model and preserve certificate serial number
-			UserModel user = getUserModel(model.username);
+			UserModel user = getManager(IUserManager.class).getUserModel(model.username);
 			X509Metadata metadata = HttpUtils.getCertificateMetadata(httpRequest);
 			if (user != null) {
 				flagWicketSession(AuthenticationType.CERTIFICATE);
@@ -682,7 +596,7 @@
 			String username = principal.getName();
 			if (!StringUtils.isEmpty(username)) {
 				boolean internalAccount = isInternalAccount(username);
-				UserModel user = getUserModel(username);
+				UserModel user = getManager(IUserManager.class).getUserModel(username);
 				if (user != null) {
 					// existing user
 					flagWicketSession(AuthenticationType.CONTAINER);
@@ -695,7 +609,7 @@
 					user = new UserModel(username.toLowerCase());
 					user.displayName = username;
 					user.password = Constants.EXTERNAL_ACCOUNT;
-					userService.updateUserModel(user);
+					getManager(IUserManager.class).updateUserModel(user);
 					flagWicketSession(AuthenticationType.CONTAINER);
 					logger.debug(MessageFormat.format("{0} authenticated and created by servlet container principal from {1}",
 							user.username, httpRequest.getRemoteAddr()));
@@ -708,7 +622,7 @@
 		}
 
 		// try to authenticate by cookie
-		if (supportsCookies()) {
+		if (getManager(IUserManager.class).supportsCookies()) {
 			UserModel user = authenticate(httpRequest.getCookies());
 			if (user != null) {
 				flagWicketSession(AuthenticationType.COOKIE);
@@ -726,7 +640,7 @@
 			String credentials = new String(Base64.decode(base64Credentials),
 					Charset.forName("UTF-8"));
 			// credentials = username:password
-			final String[] values = credentials.split(":",2);
+			final String[] values = credentials.split(":", 2);
 
 			if (values.length == 2) {
 				String username = values[0];
@@ -774,20 +688,17 @@
 	 */
 	@Override
 	public void setCookie(HttpServletResponse response, UserModel user) {
-		if (userService == null) {
-			return;
-		}
 		GitBlitWebSession session = GitBlitWebSession.get();
 		boolean standardLogin = session.authenticationType.isStandard();
 
-		if (userService.supportsCookies() && standardLogin) {
+		if (getManager(IUserManager.class).supportsCookies() && standardLogin) {
 			Cookie userCookie;
 			if (user == null) {
 				// clear cookie for logout
 				userCookie = new Cookie(Constants.NAME, "");
 			} else {
 				// set cookie for login
-				String cookie = userService.getCookie(user);
+				String cookie = getManager(IUserManager.class).getCookie(user);
 				if (StringUtils.isEmpty(cookie)) {
 					// create empty cookie
 					userCookie = new Cookie(Constants.NAME, "");
@@ -802,102 +713,12 @@
 		}
 	}
 
-	/**
-	 * Logout a user.
-	 *
-	 * @param user
-	 */
-	@Override
-	public void logout(UserModel user) {
-		if (userService == null) {
-			return;
-		}
-		userService.logout(user);
-	}
-
-	/**
-	 * Encode the username for user in an url.
-	 *
-	 * @param name
-	 * @return the encoded name
-	 */
-	protected String encodeUsername(String name) {
-		return name.replace("@", "%40").replace(" ", "%20").replace("\\", "%5C");
-	}
-
-	/**
-	 * Decode a username from an encoded url.
-	 *
-	 * @param name
-	 * @return the decoded name
-	 */
-	protected String decodeUsername(String name) {
-		return name.replace("%40", "@").replace("%20", " ").replace("%5C", "\\");
-	}
-
-	/**
-	 * Returns the list of all users available to the login service.
-	 *
-	 * @see IUserService.getAllUsernames()
-	 * @return list of all usernames
-	 */
-	@Override
-	public List<String> getAllUsernames() {
-		List<String> names = new ArrayList<String>(userService.getAllUsernames());
-		return names;
-	}
-
-	/**
-	 * Returns the list of all users available to the login service.
-	 *
-	 * @see IUserService.getAllUsernames()
-	 * @return list of all usernames
-	 */
-	@Override
-	public List<UserModel> getAllUsers() {
-		List<UserModel> users = userService.getAllUsers();
-		return users;
-	}
-
-	/**
-	 * Delete the user object with the specified username
-	 *
-	 * @see IUserService.deleteUser(String)
-	 * @param username
-	 * @return true if successful
-	 */
-	@Override
-	public boolean deleteUser(String username) {
-		if (StringUtils.isEmpty(username)) {
-			return false;
-		}
-		String usernameDecoded = decodeUsername(username);
-		return userService.deleteUser(usernameDecoded);
-	}
-
 	@Override
 	public UserModel getFederationUser() {
 		// the federation user is an administrator
 		UserModel federationUser = new UserModel(Constants.FEDERATION_USER);
 		federationUser.canAdmin = true;
 		return federationUser;
-	}
-
-	/**
-	 * Retrieve the user object for the specified username.
-	 *
-	 * @see IUserService.getUserModel(String)
-	 * @param username
-	 * @return a user object or null
-	 */
-	@Override
-	public UserModel getUserModel(String username) {
-		if (StringUtils.isEmpty(username)) {
-			return null;
-		}
-		String usernameDecoded = decodeUsername(username);
-		UserModel user = userService.getUserModel(usernameDecoded);
-		return user;
 	}
 
 	/**
@@ -965,7 +786,7 @@
 			return list;
 		}
 		// NAMED users and teams
-		for (UserModel user : userService.getAllUsers()) {
+		for (UserModel user : getManager(IUserManager.class).getAllUsers()) {
 			RegistrantAccessPermission ap = user.getRepositoryPermission(repository);
 			if (ap.permission.exceeds(AccessPermission.NONE)) {
 				list.add(ap);
@@ -987,12 +808,12 @@
 		for (RegistrantAccessPermission up : permissions) {
 			if (up.mutable) {
 				// only set editable defined permissions
-				UserModel user = userService.getUserModel(up.registrant);
+				UserModel user = getManager(IUserManager.class).getUserModel(up.registrant);
 				user.setRepositoryPermission(repository.name, up.permission);
 				users.add(user);
 			}
 		}
-		return userService.updateUserModels(users);
+		return getManager(IUserManager.class).updateUserModels(users);
 	}
 
 	/**
@@ -1005,7 +826,7 @@
 	 */
 	@Override
 	public List<String> getRepositoryUsers(RepositoryModel repository) {
-		return userService.getUsernamesForRepositoryRole(repository.name);
+		return getManager(IUserManager.class).getUsernamesForRepositoryRole(repository.name);
 	}
 
 	/**
@@ -1038,7 +859,7 @@
 	public void updateUserModel(String username, UserModel user, boolean isCreate)
 			throws GitBlitException {
 		if (!username.equalsIgnoreCase(user.username)) {
-			if (userService.getUserModel(user.username) != null) {
+			if (getManager(IUserManager.class).getUserModel(user.username) != null) {
 				throw new GitBlitException(MessageFormat.format(
 						"Failed to rename ''{0}'' because ''{1}'' already exists.", username,
 						user.username));
@@ -1060,43 +881,9 @@
 				}
 			}
 		}
-		if (!userService.updateUserModel(username, user)) {
+		if (!getManager(IUserManager.class).updateUserModel(username, user)) {
 			throw new GitBlitException(isCreate ? "Failed to add user!" : "Failed to update user!");
 		}
-	}
-
-	/**
-	 * Returns the list of available teams that a user or repository may be
-	 * assigned to.
-	 *
-	 * @return the list of teams
-	 */
-	public List<String> getAllTeamnames() {
-		List<String> teams = new ArrayList<String>(userService.getAllTeamNames());
-		return teams;
-	}
-
-	/**
-	 * Returns the list of available teams that a user or repository may be
-	 * assigned to.
-	 *
-	 * @return the list of teams
-	 */
-	@Override
-	public List<TeamModel> getAllTeams() {
-		List<TeamModel> teams = userService.getAllTeams();
-		return teams;
-	}
-
-	/**
-	 * Returns the TeamModel object for the specified name.
-	 *
-	 * @param teamname
-	 * @return a TeamModel object or null
-	 */
-	@Override
-	public TeamModel getTeamModel(String teamname) {
-		return userService.getTeamModel(teamname);
 	}
 
 	/**
@@ -1110,7 +897,7 @@
 	@Override
 	public List<RegistrantAccessPermission> getTeamAccessPermissions(RepositoryModel repository) {
 		List<RegistrantAccessPermission> list = new ArrayList<RegistrantAccessPermission>();
-		for (TeamModel team : userService.getAllTeams()) {
+		for (TeamModel team : getManager(IUserManager.class).getAllTeams()) {
 			RegistrantAccessPermission ap = team.getRepositoryPermission(repository);
 			if (ap.permission.exceeds(AccessPermission.NONE)) {
 				list.add(ap);
@@ -1133,12 +920,12 @@
 		for (RegistrantAccessPermission tp : permissions) {
 			if (tp.mutable) {
 				// only set explicitly defined access permissions
-				TeamModel team = userService.getTeamModel(tp.registrant);
+				TeamModel team = getManager(IUserManager.class).getTeamModel(tp.registrant);
 				team.setRepositoryPermission(repository.name, tp.permission);
 				teams.add(team);
 			}
 		}
-		return userService.updateTeamModels(teams);
+		return getManager(IUserManager.class).updateTeamModels(teams);
 	}
 
 	/**
@@ -1151,7 +938,7 @@
 	 */
 	@Override
 	public List<String> getRepositoryTeams(RepositoryModel repository) {
-		return userService.getTeamnamesForRepositoryRole(repository.name);
+		return getManager(IUserManager.class).getTeamNamesForRepositoryRole(repository.name);
 	}
 
 	/**
@@ -1181,27 +968,15 @@
 	public void updateTeamModel(String teamname, TeamModel team, boolean isCreate)
 			throws GitBlitException {
 		if (!teamname.equalsIgnoreCase(team.name)) {
-			if (userService.getTeamModel(team.name) != null) {
+			if (getManager(IUserManager.class).getTeamModel(team.name) != null) {
 				throw new GitBlitException(MessageFormat.format(
 						"Failed to rename ''{0}'' because ''{1}'' already exists.", teamname,
 						team.name));
 			}
 		}
-		if (!userService.updateTeamModel(teamname, team)) {
+		if (!getManager(IUserManager.class).updateTeamModel(teamname, team)) {
 			throw new GitBlitException(isCreate ? "Failed to add team!" : "Failed to update team!");
 		}
-	}
-
-	/**
-	 * Delete the team object with the specified teamname
-	 *
-	 * @see IUserService.deleteTeam(String)
-	 * @param teamname
-	 * @return true if successful
-	 */
-	@Override
-	public boolean deleteTeam(String teamname) {
-		return userService.deleteTeam(teamname);
 	}
 
 	/**
@@ -1519,7 +1294,7 @@
 	@Override
 	public long getStarCount(RepositoryModel repository) {
 		long count = 0;
-		for (UserModel user : getAllUsers()) {
+		for (UserModel user : getManager(IUserManager.class).getAllUsers()) {
 			if (user.getPreferences().isStarredRepository(repository.name)) {
 				count++;
 			}
@@ -1680,7 +1455,7 @@
 		if (project == null) {
 			project = new ProjectModel(name);
 			if (ModelUtils.isPersonalRepository(name)) {
-				UserModel user = getUserModel(ModelUtils.getUserNameFromRepoPath(name));
+				UserModel user = getManager(IUserManager.class).getUserModel(ModelUtils.getUserNameFromRepoPath(name));
 				if (user != null) {
 					project.title = user.getDisplayName();
 					project.description = "personal repositories";
@@ -2297,7 +2072,7 @@
 							repository.name));
 				}
 				// rename the roles
-				if (!userService.renameRepositoryRole(repositoryName, repository.name)) {
+				if (!getManager(IUserManager.class).renameRepositoryRole(repositoryName, repository.name)) {
 					throw new GitBlitException(MessageFormat.format(
 							"Failed to rename repository permissions ''{0}'' to ''{1}''.",
 							repositoryName, repository.name));
@@ -2514,7 +2289,7 @@
 			File folder = new File(repositoriesFolder, repositoryName);
 			if (folder.exists() && folder.isDirectory()) {
 				FileUtils.delete(folder, FileUtils.RECURSIVE | FileUtils.RETRY);
-				if (userService.deleteRepositoryRole(repositoryName)) {
+				if (getManager(IUserManager.class).deleteRepositoryRole(repositoryName)) {
 					logger.info(MessageFormat.format("Repository \"{0}\" deleted", repositoryName));
 					return true;
 				}
@@ -3046,8 +2821,8 @@
 
 		// Team Scripts
 		if (repository != null) {
-			for (String teamname : userService.getTeamnamesForRepositoryRole(repository.name)) {
-				TeamModel team = userService.getTeamModel(teamname);
+			for (String teamname : getManager(IUserManager.class).getTeamNamesForRepositoryRole(repository.name)) {
+				TeamModel team = getManager(IUserManager.class).getTeamModel(teamname);
 				if (!ArrayUtils.isEmpty(team.preReceiveScripts)) {
 					scripts.addAll(team.preReceiveScripts);
 				}
@@ -3100,8 +2875,8 @@
 		}
 		// Team Scripts
 		if (repository != null) {
-			for (String teamname : userService.getTeamnamesForRepositoryRole(repository.name)) {
-				TeamModel team = userService.getTeamModel(teamname);
+			for (String teamname : getManager(IUserManager.class).getTeamNamesForRepositoryRole(repository.name)) {
+				TeamModel team = getManager(IUserManager.class).getTeamModel(teamname);
 				if (!ArrayUtils.isEmpty(team.postReceiveScripts)) {
 					scripts.addAll(team.postReceiveScripts);
 				}
@@ -3156,10 +2931,14 @@
 	 * @return Map<String, SettingModel>
 	 */
 	private ServerSettings loadSettingModels(ServerSettings settingsModel) {
-		settingsModel.supportsCredentialChanges = userService.supportsCredentialChanges();
-		settingsModel.supportsDisplayNameChanges = userService.supportsDisplayNameChanges();
-		settingsModel.supportsEmailAddressChanges = userService.supportsEmailAddressChanges();
-		settingsModel.supportsTeamMembershipChanges = userService.supportsTeamMembershipChanges();
+		// this entire "supports" concept will go away with user service refactoring
+		UserModel externalUser = new UserModel(Constants.EXTERNAL_ACCOUNT);
+		externalUser.password = Constants.EXTERNAL_ACCOUNT;
+		IUserManager userManager = getManager(IUserManager.class);
+		settingsModel.supportsCredentialChanges = userManager.supportsCredentialChanges(externalUser);
+		settingsModel.supportsDisplayNameChanges = userManager.supportsDisplayNameChanges(externalUser);
+		settingsModel.supportsEmailAddressChanges = userManager.supportsEmailAddressChanges(externalUser);
+		settingsModel.supportsTeamMembershipChanges = userManager.supportsTeamMembershipChanges(externalUser);
 		try {
 			// Read bundled Gitblit properties to extract setting descriptions.
 			// This copy is pristine and only used for populating the setting
@@ -3321,7 +3100,7 @@
 				Gitblit gitblit = new Gitblit(
 						getManager(IRuntimeManager.class),
 						getManager(INotificationManager.class),
-						this,
+						getManager(IUserManager.class),
 						this,
 						this,
 						this,
@@ -3430,6 +3209,7 @@
 		runtime.getStatus().servletContainer = context.getServerInfo();
 
 		startManager(injector, INotificationManager.class);
+		startManager(injector, IUserManager.class);
 
 		repositoriesFolder = getRepositoriesFolder();
 
@@ -3452,19 +3232,6 @@
 		if (runtimeSettings.getBoolean(Keys.git.cacheRepositoryList,  true)) {
 			logger.info("Identifying available repositories...");
 			getRepositoryList();
-		}
-
-		if (this.userService == null) {
-			String realm = runtimeSettings.getString(Keys.realm.userService, "${baseFolder}/users.properties");
-			IUserService loginService = null;
-			try {
-				// check to see if this "file" is a login service class
-				Class<?> realmClass = Class.forName(realm);
-				loginService = (IUserService) realmClass.newInstance();
-			} catch (Throwable t) {
-				loginService = new GitblitUserService();
-			}
-			setUserService(loginService);
 		}
 
 		loadSettingModels(runtime.getSettingsModel());
@@ -3747,7 +3514,7 @@
 		// add the owner of the source repository to the clone's access list
 		if (!ArrayUtils.isEmpty(repository.owners)) {
 			for (String owner : repository.owners) {
-				UserModel originOwner = getUserModel(owner);
+				UserModel originOwner = getManager(IUserManager.class).getUserModel(owner);
 				if (originOwner != null) {
 					originOwner.setRepositoryPermission(cloneName, AccessPermission.CLONE);
 					updateUserModel(originOwner.username, originOwner, false);
@@ -3760,7 +3527,7 @@
 		List<UserModel> cloneUsers = new ArrayList<UserModel>();
 		for (String name : users) {
 			if (!name.equalsIgnoreCase(user.username)) {
-				UserModel cloneUser = getUserModel(name);
+				UserModel cloneUser = getManager(IUserManager.class).getUserModel(name);
 				if (cloneUser.canClone(repository)) {
 					// origin user can clone origin, grant clone access to fork
 					cloneUser.setRepositoryPermission(cloneName, AccessPermission.CLONE);
@@ -3768,116 +3535,30 @@
 				cloneUsers.add(cloneUser);
 			}
 		}
-		userService.updateUserModels(cloneUsers);
+		getManager(IUserManager.class).updateUserModels(cloneUsers);
 
 		// grant origin's team list clone permission to fork
 		List<String> teams = getRepositoryTeams(repository);
 		List<TeamModel> cloneTeams = new ArrayList<TeamModel>();
 		for (String name : teams) {
-			TeamModel cloneTeam = getTeamModel(name);
+			TeamModel cloneTeam = getManager(IUserManager.class).getTeamModel(name);
 			if (cloneTeam.canClone(repository)) {
 				// origin team can clone origin, grant clone access to fork
 				cloneTeam.setRepositoryPermission(cloneName, AccessPermission.CLONE);
 			}
 			cloneTeams.add(cloneTeam);
 		}
-		userService.updateTeamModels(cloneTeams);
+		getManager(IUserManager.class).updateTeamModels(cloneTeams);
 
 		// add this clone to the cached model
 		addToCachedRepositoryList(cloneModel);
 		return cloneModel;
 	}
 
-	/**
-	 * Allow to understand if GitBlit supports and is configured to allow
-	 * cookie-based authentication.
-	 *
-	 * @return status of Cookie authentication enablement.
-	 */
-	@Override
-	public boolean supportsCookies() {
-		return settings.getBoolean(Keys.web.allowCookieAuthentication, true) && userService.supportsCookies();
-	}
-
-	@Override
-	public String getCookie(UserModel model) {
-		return userService.getCookie(model);
-	}
-
-	@Override
-	public UserModel authenticate(char[] cookie) {
-		return userService.authenticate(cookie);
-	}
-
-	@Override
-	public boolean updateUserModel(UserModel model) {
-		return userService.updateUserModel(model);
-	}
-
-	@Override
-	public boolean updateUserModels(Collection<UserModel> models) {
-		return userService.updateUserModels(models);
-	}
-
-	@Override
-	public boolean updateUserModel(String username, UserModel model) {
-		return userService.updateUserModel(username, model);
-	}
-
-	@Override
-	public boolean deleteUserModel(UserModel model) {
-		return userService.deleteUserModel(model);
-	}
-
-	@Override
-	public List<String> getAllTeamNames() {
-		return userService.getAllTeamNames();
-	}
-
-	@Override
-	public List<String> getTeamnamesForRepositoryRole(String role) {
-		return userService.getTeamnamesForRepositoryRole(role);
-	}
-
-	@Override
-	public boolean updateTeamModel(TeamModel model) {
-		return userService.updateTeamModel(model);
-	}
-
-	@Override
-	public boolean updateTeamModels(Collection<TeamModel> models) {
-		return userService.updateTeamModels(models);
-	}
-
-	@Override
-	public boolean updateTeamModel(String teamname, TeamModel model) {
-		return userService.updateTeamModel(teamname, model);
-	}
-
-	@Override
-	public boolean deleteTeamModel(TeamModel model) {
-		return userService.deleteTeamModel(model);
-	}
-
-	@Override
-	public List<String> getUsernamesForRepositoryRole(String role) {
-		return userService.getUsernamesForRepositoryRole(role);
-	}
-
-	@Override
-	public boolean renameRepositoryRole(String oldRole, String newRole) {
-		return userService.renameRepositoryRole(oldRole, newRole);
-	}
-
-	@Override
-	public boolean deleteRepositoryRole(String role) {
-		return userService.deleteRepositoryRole(role);
-	}
-
 	@Override
 	public void logout(HttpServletResponse response, UserModel user) {
 		setCookie(response,  null);
-		userService.logout(user);
+		getManager(IUserManager.class).logout(user);
 	}
 
 	@Override
diff --git a/src/main/java/com/gitblit/Gitblit.java b/src/main/java/com/gitblit/Gitblit.java
index 687e4e2..7a5c73e 100644
--- a/src/main/java/com/gitblit/Gitblit.java
+++ b/src/main/java/com/gitblit/Gitblit.java
@@ -348,8 +348,8 @@
 	}
 
 	@Override
-	public List<String> getTeamnamesForRepositoryRole(String role) {
-		return userManager.getTeamnamesForRepositoryRole(role);
+	public List<String> getTeamNamesForRepositoryRole(String role) {
+		return userManager.getTeamNamesForRepositoryRole(role);
 	}
 
 	@Override
diff --git a/src/main/java/com/gitblit/GitblitUserService.java b/src/main/java/com/gitblit/GitblitUserService.java
index 7278b22..715aed9 100644
--- a/src/main/java/com/gitblit/GitblitUserService.java
+++ b/src/main/java/com/gitblit/GitblitUserService.java
@@ -126,6 +126,12 @@
 		return serviceImpl.getCookie(model);
 	}
 
+	/**
+	 * Authenticate a user based on their cookie.
+	 *
+	 * @param cookie
+	 * @return a user object or null
+	 */
 	@Override
 	public UserModel authenticate(char[] cookie) {
 		UserModel user = serviceImpl.authenticate(cookie);
@@ -226,8 +232,8 @@
 	}
 
 	@Override
-	public List<String> getTeamnamesForRepositoryRole(String role) {
-		return serviceImpl.getTeamnamesForRepositoryRole(role);
+	public List<String> getTeamNamesForRepositoryRole(String role) {
+		return serviceImpl.getTeamNamesForRepositoryRole(role);
 	}
 
 	@Override
@@ -312,7 +318,8 @@
 		}
 	}
 
-	protected AccountType getAccountType() {
+	@Override
+	public AccountType getAccountType() {
 		return AccountType.LOCAL;
 	}
 }
diff --git a/src/main/java/com/gitblit/HtpasswdUserService.java b/src/main/java/com/gitblit/HtpasswdUserService.java
index 3b7120f..ca5295c 100644
--- a/src/main/java/com/gitblit/HtpasswdUserService.java
+++ b/src/main/java/com/gitblit/HtpasswdUserService.java
@@ -275,7 +275,7 @@
      * @return AccountType.HTPASSWD
      */
     @Override
-	protected AccountType getAccountType()
+	public AccountType getAccountType()
     {
         return AccountType.HTPASSWD;
     }
diff --git a/src/main/java/com/gitblit/IUserService.java b/src/main/java/com/gitblit/IUserService.java
index 33f519f..316e4a5 100644
--- a/src/main/java/com/gitblit/IUserService.java
+++ b/src/main/java/com/gitblit/IUserService.java
@@ -18,6 +18,7 @@
 import java.util.Collection;
 import java.util.List;
 
+import com.gitblit.Constants.AccountType;
 import com.gitblit.manager.IRuntimeManager;
 import com.gitblit.models.TeamModel;
 import com.gitblit.models.UserModel;
@@ -205,7 +206,7 @@
 	 * @return list of all usernames that can bypass the access restriction
 	 * @since 0.8.0
 	 */
-	List<String> getTeamnamesForRepositoryRole(String role);
+	List<String> getTeamNamesForRepositoryRole(String role);
 
 	/**
 	 * Sets the list of all teams who are allowed to bypass the access
@@ -319,6 +320,14 @@
 	boolean deleteRepositoryRole(String role);
 
 	/**
+	 * Returns the account type for the user models.
+	 *
+	 * @return the account type
+	 * @since 1.4.0
+	 */
+	AccountType getAccountType();
+
+	/**
 	 * @See java.lang.Object.toString();
 	 * @return string representation of the login service
 	 */
diff --git a/src/main/java/com/gitblit/LdapUserService.java b/src/main/java/com/gitblit/LdapUserService.java
index 5619dad..c075afc 100644
--- a/src/main/java/com/gitblit/LdapUserService.java
+++ b/src/main/java/com/gitblit/LdapUserService.java
@@ -262,7 +262,7 @@
 	}
 
 	@Override
-	protected AccountType getAccountType() {
+	public AccountType getAccountType() {
 		 return AccountType.LDAP;
 	}
 
diff --git a/src/main/java/com/gitblit/PAMUserService.java b/src/main/java/com/gitblit/PAMUserService.java
index b348e64..db569fb 100644
--- a/src/main/java/com/gitblit/PAMUserService.java
+++ b/src/main/java/com/gitblit/PAMUserService.java
@@ -92,7 +92,7 @@
     }
 
 	 @Override
-	protected AccountType getAccountType() {
+	public AccountType getAccountType() {
 		return AccountType.PAM;
 	}
 
diff --git a/src/main/java/com/gitblit/RedmineUserService.java b/src/main/java/com/gitblit/RedmineUserService.java
index 62322eb..7c38ef2 100644
--- a/src/main/java/com/gitblit/RedmineUserService.java
+++ b/src/main/java/com/gitblit/RedmineUserService.java
@@ -91,7 +91,7 @@
     }
 
 	 @Override
-	protected AccountType getAccountType() {
+	public AccountType getAccountType() {
 		return AccountType.REDMINE;
 	}
 
diff --git a/src/main/java/com/gitblit/SalesforceUserService.java b/src/main/java/com/gitblit/SalesforceUserService.java
index 0eca6c9..6161ba9 100644
--- a/src/main/java/com/gitblit/SalesforceUserService.java
+++ b/src/main/java/com/gitblit/SalesforceUserService.java
@@ -22,7 +22,7 @@
 	private IStoredSettings settings;
 
 	@Override
-	protected AccountType getAccountType() {
+	public AccountType getAccountType() {
 		return AccountType.SALESFORCE;
 	}
 
diff --git a/src/main/java/com/gitblit/WindowsUserService.java b/src/main/java/com/gitblit/WindowsUserService.java
index 9b25efb..99077c6 100644
--- a/src/main/java/com/gitblit/WindowsUserService.java
+++ b/src/main/java/com/gitblit/WindowsUserService.java
@@ -104,7 +104,7 @@
     }
 
 	 @Override
-	protected AccountType getAccountType() {
+	public AccountType getAccountType() {
 		return AccountType.WINDOWS;
 	}
 
diff --git a/src/main/java/com/gitblit/manager/IUserManager.java b/src/main/java/com/gitblit/manager/IUserManager.java
index 3ce1e74..387a720 100644
--- a/src/main/java/com/gitblit/manager/IUserManager.java
+++ b/src/main/java/com/gitblit/manager/IUserManager.java
@@ -21,7 +21,7 @@
 import com.gitblit.models.TeamModel;
 import com.gitblit.models.UserModel;
 
-public interface IUserManager {
+public interface IUserManager extends IManager {
 
 	boolean supportsAddUser();
 
@@ -189,7 +189,7 @@
 	 * @return list of all usernames that can bypass the access restriction
 	 * @since 0.8.0
 	 */
-	List<String> getTeamnamesForRepositoryRole(String role);
+	List<String> getTeamNamesForRepositoryRole(String role);
 
 	/**
 	 * Retrieve the team object for the specified team name.
diff --git a/src/main/java/com/gitblit/manager/UserManager.java b/src/main/java/com/gitblit/manager/UserManager.java
new file mode 100644
index 0000000..f781c4c
--- /dev/null
+++ b/src/main/java/com/gitblit/manager/UserManager.java
@@ -0,0 +1,542 @@
+/*
+ * Copyright 2013 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.manager;
+
+import java.io.File;
+import java.io.IOException;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.ConfigUserService;
+import com.gitblit.Constants;
+import com.gitblit.Constants.AccountType;
+import com.gitblit.IStoredSettings;
+import com.gitblit.IUserService;
+import com.gitblit.Keys;
+import com.gitblit.models.TeamModel;
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.DeepCopier;
+import com.gitblit.utils.StringUtils;
+
+/**
+ * The user manager manages persistence and retrieval of users and teams.
+ *
+ * @author James Moger
+ *
+ */
+public class UserManager implements IUserManager {
+
+	private final Logger logger = LoggerFactory.getLogger(getClass());
+
+	private final IStoredSettings settings;
+
+	private final IRuntimeManager runtimeManager;
+
+	private IUserService userService;
+
+	public UserManager(IRuntimeManager runtimeManager) {
+		this.settings = runtimeManager.getSettings();
+		this.runtimeManager = runtimeManager;
+	}
+
+	/**
+	 * Set the user service. The user service authenticates local users and is
+	 * responsible for persisting and retrieving users and teams.
+	 *
+	 * @param userService
+	 */
+	public void setUserService(IUserService userService) {
+		logger.info("Setting up user service " + userService.toString());
+		this.userService = userService;
+		this.userService.setup(runtimeManager);
+	}
+
+	@Override
+	public IManager setup() {
+		if (this.userService == null) {
+			String realm = settings.getString(Keys.realm.userService, "${baseFolder}/users.properties");
+			IUserService service = null;
+			try {
+				// check to see if this "file" is a login service class
+				Class<?> realmClass = Class.forName(realm);
+				service = (IUserService) realmClass.newInstance();
+			} catch (Throwable t) {
+				File realmFile = runtimeManager.getFileOrFolder(Keys.realm.userService, "${baseFolder}/users.conf");
+				service = createUserService(realmFile);
+			}
+			setUserService(service);
+		}
+		return this;
+	}
+
+	protected IUserService createUserService(File realmFile) {
+		IUserService service = null;
+		if (realmFile.getName().toLowerCase().endsWith(".conf")) {
+			// v0.8.0+ config-based realm file
+			service = new ConfigUserService(realmFile);
+		}
+
+		assert service != null;
+
+		if (!realmFile.exists()) {
+			// Create the Administrator account for a new realm file
+			try {
+				realmFile.createNewFile();
+			} catch (IOException x) {
+				logger.error(MessageFormat.format("COULD NOT CREATE REALM FILE {0}!", realmFile), x);
+			}
+			UserModel admin = new UserModel("admin");
+			admin.password = "admin";
+			admin.canAdmin = true;
+			admin.excludeFromFederation = true;
+			service.updateUserModel(admin);
+		}
+
+		return service;
+	}
+
+	@Override
+	public IManager stop() {
+		return this;
+	}
+
+	@Override
+	public boolean supportsAddUser() {
+		return supportsCredentialChanges(new UserModel(""));
+	}
+
+	/**
+	 * Returns true if the user's credentials can be changed.
+	 *
+	 * @param user
+	 * @return true if the user service supports credential changes
+	 */
+	@Override
+	public boolean supportsCredentialChanges(UserModel user) {
+		if (user == null) {
+			return false;
+		} else if (AccountType.LOCAL.equals(user.accountType)) {
+			// local account, we can change credentials
+			return true;
+		} else {
+			// external account, ask user service
+			return userService.supportsCredentialChanges();
+		}
+	}
+
+	/**
+	 * Returns true if the user's display name can be changed.
+	 *
+	 * @param user
+	 * @return true if the user service supports display name changes
+	 */
+	@Override
+	public boolean supportsDisplayNameChanges(UserModel user) {
+		return (user != null && user.isLocalAccount()) || userService.supportsDisplayNameChanges();
+	}
+
+	/**
+	 * Returns true if the user's email address can be changed.
+	 *
+	 * @param user
+	 * @return true if the user service supports email address changes
+	 */
+	@Override
+	public boolean supportsEmailAddressChanges(UserModel user) {
+		return (user != null && user.isLocalAccount()) || userService.supportsEmailAddressChanges();
+	}
+
+	/**
+	 * Returns true if the user's team memberships can be changed.
+	 *
+	 * @param user
+	 * @return true if the user service supports team membership changes
+	 */
+	@Override
+	public boolean supportsTeamMembershipChanges(UserModel user) {
+		return (user != null && user.isLocalAccount()) || userService.supportsTeamMembershipChanges();
+	}
+
+	/**
+	 * Allow to understand if GitBlit supports and is configured to allow
+	 * cookie-based authentication.
+	 *
+	 * @return status of Cookie authentication enablement.
+	 */
+	@Override
+	public boolean supportsCookies() {
+		return settings.getBoolean(Keys.web.allowCookieAuthentication, true) && userService.supportsCookies();
+	}
+
+	/**
+	 * Returns the cookie value for the specified user.
+	 *
+	 * @param model
+	 * @return cookie value
+	 */
+	@Override
+	public String getCookie(UserModel model) {
+		return userService.getCookie(model);
+	}
+
+	/**
+	 * Authenticate a user based on a username and password.
+	 *
+	 * @param username
+	 * @param password
+	 * @return a user object or null
+	 */
+
+	@Override
+	public UserModel authenticate(String username, char[] password) {
+		UserModel user = userService.authenticate(username, password);
+		setAccountType(user);
+		return user;
+	}
+
+	/**
+	 * Authenticate a user based on their cookie.
+	 *
+	 * @param cookie
+	 * @return a user object or null
+	 */
+	@Override
+	public UserModel authenticate(char[] cookie) {
+		UserModel user = userService.authenticate(cookie);
+		setAccountType(user);
+		return user;
+	}
+
+	/**
+	 * Logout a user.
+	 *
+	 * @param user
+	 */
+	@Override
+	public void logout(UserModel user) {
+		if (userService == null) {
+			return;
+		}
+		userService.logout(user);
+	}
+
+	/**
+	 * Retrieve the user object for the specified username.
+	 *
+	 * @param username
+	 * @return a user object or null
+	 */
+	@Override
+	public UserModel getUserModel(String username) {
+		if (StringUtils.isEmpty(username)) {
+			return null;
+		}
+		String usernameDecoded = StringUtils.decodeUsername(username);
+		UserModel user = userService.getUserModel(usernameDecoded);
+		setAccountType(user);
+		return user;
+	}
+
+	/**
+	 * Updates/writes a complete user object.
+	 *
+	 * @param model
+	 * @return true if update is successful
+	 */
+	@Override
+	public boolean updateUserModel(UserModel model) {
+		return userService.updateUserModel(model);
+	}
+
+	/**
+	 * 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(Collection<UserModel> models) {
+		return userService.updateUserModels(models);
+	}
+
+	/**
+	 * Adds/updates a 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
+	 */
+	@Override
+	public boolean updateUserModel(String username, UserModel model) {
+		if (model.isLocalAccount() || userService.supportsCredentialChanges()) {
+			if (!model.isLocalAccount() && !userService.supportsTeamMembershipChanges()) {
+				//  teams are externally controlled - copy from original model
+				UserModel existingModel = getUserModel(username);
+
+				model = DeepCopier.copy(model);
+				model.teams.clear();
+				model.teams.addAll(existingModel.teams);
+			}
+			return userService.updateUserModel(username, model);
+		}
+		if (model.username.equals(username)) {
+			// passwords are not persisted by the backing user service
+			model.password = null;
+			if (!model.isLocalAccount() && !userService.supportsTeamMembershipChanges()) {
+				//  teams are externally controlled- copy from original model
+				UserModel existingModel = getUserModel(username);
+
+				model = DeepCopier.copy(model);
+				model.teams.clear();
+				model.teams.addAll(existingModel.teams);
+			}
+			return userService.updateUserModel(username, model);
+		}
+		logger.error("Users can not be renamed!");
+		return false;
+	}
+
+	/**
+	 * Deletes the user object from the user service.
+	 *
+	 * @param model
+	 * @return true if successful
+	 */
+	@Override
+	public boolean deleteUserModel(UserModel model) {
+		return userService.deleteUserModel(model);
+	}
+
+	/**
+	 * Delete the user object with the specified username
+	 *
+	 * @param username
+	 * @return true if successful
+	 */
+	@Override
+	public boolean deleteUser(String username) {
+		if (StringUtils.isEmpty(username)) {
+			return false;
+		}
+		String usernameDecoded = StringUtils.decodeUsername(username);
+		return userService.deleteUser(usernameDecoded);
+	}
+
+	/**
+	 * Returns the list of all users available to the login service.
+	 *
+	 * @return list of all usernames
+	 */
+	@Override
+	public List<String> getAllUsernames() {
+		List<String> names = new ArrayList<String>(userService.getAllUsernames());
+		return names;
+	}
+
+	/**
+	 * Returns the list of all users available to the login service.
+	 *
+	 * @return list of all users
+	 * @since 0.8.0
+	 */
+	@Override
+	public List<UserModel> getAllUsers() {
+		List<UserModel> users = userService.getAllUsers();
+    	for (UserModel user : users) {
+    		setAccountType(user);
+    	}
+		return users;
+	}
+
+	/**
+	 * 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() {
+		return userService.getAllTeamNames();
+	}
+
+	/**
+	 * 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> teams = userService.getAllTeams();
+		return teams;
+	}
+
+	/**
+	 * 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 teams that can bypass the access restriction
+	 * @since 0.8.0
+	 */
+	@Override
+	public List<String> getTeamNamesForRepositoryRole(String role) {
+		return userService.getTeamNamesForRepositoryRole(role);
+	}
+
+	/**
+	 * 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) {
+		return userService.getTeamModel(teamname);
+	}
+
+	/**
+	 * 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 userService.updateTeamModel(model);
+	}
+
+	/**
+	 * 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(Collection<TeamModel> models) {
+		return userService.updateTeamModels(models);
+	}
+
+	/**
+	 * 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) {
+		if (!userService.supportsTeamMembershipChanges()) {
+			// teams are externally controlled - copy from original model
+			TeamModel existingModel = getTeamModel(teamname);
+
+			model = DeepCopier.copy(model);
+			model.users.clear();
+			model.users.addAll(existingModel.users);
+		}
+		return userService.updateTeamModel(teamname, 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 userService.deleteTeamModel(model);
+	}
+
+	/**
+	 * Delete the team object with the specified teamname
+	 *
+	 * @param teamname
+	 * @return true if successful
+	 * @since 0.8.0
+	 */
+	@Override
+	public boolean deleteTeam(String teamname) {
+		return userService.deleteTeam(teamname);
+	}
+
+	/**
+	 * Returns the list of all users who are allowed to bypass the access
+	 * restriction placed on the specified repository.
+	 *
+	 * @param role
+	 *            the repository name
+	 * @return list of all usernames that can bypass the access restriction
+	 * @since 0.8.0
+	 */
+	@Override
+	public List<String> getUsernamesForRepositoryRole(String role) {
+		return userService.getUsernamesForRepositoryRole(role);
+	}
+
+	/**
+	 * Renames a repository role.
+	 *
+	 * @param oldRole
+	 * @param newRole
+	 * @return true if successful
+	 */
+	@Override
+	public boolean renameRepositoryRole(String oldRole, String newRole) {
+		return userService.renameRepositoryRole(oldRole, newRole);
+	}
+
+	/**
+	 * Removes a repository role from all users.
+	 *
+	 * @param role
+	 * @return true if successful
+	 */
+	@Override
+	public boolean deleteRepositoryRole(String role) {
+		return userService.deleteRepositoryRole(role);
+	}
+
+	protected void setAccountType(UserModel user) {
+		if (user != null) {
+			if (!StringUtils.isEmpty(user.password)
+					&& !Constants.EXTERNAL_ACCOUNT.equalsIgnoreCase(user.password)
+					&& !"StoredInLDAP".equalsIgnoreCase(user.password)) {
+				user.accountType = AccountType.LOCAL;
+			} else {
+				user.accountType = userService.getAccountType();
+			}
+		}
+	}
+}
diff --git a/src/main/java/com/gitblit/utils/StringUtils.java b/src/main/java/com/gitblit/utils/StringUtils.java
index e18bdc4..ed10eb6 100644
--- a/src/main/java/com/gitblit/utils/StringUtils.java
+++ b/src/main/java/com/gitblit/utils/StringUtils.java
@@ -747,4 +747,25 @@
 		}
 		return input.replace('\n',' ').replace('\r',  ' ').trim();
 	}
+
+
+	/**
+	 * Encode the username for user in an url.
+	 *
+	 * @param name
+	 * @return the encoded name
+	 */
+	public static String encodeUsername(String name) {
+		return name.replace("@", "%40").replace(" ", "%20").replace("\\", "%5C");
+	}
+
+	/**
+	 * Decode a username from an encoded url.
+	 *
+	 * @param name
+	 * @return the decoded name
+	 */
+	public static String decodeUsername(String name) {
+		return name.replace("%40", "@").replace("%20", " ").replace("%5C", "\\");
+	}
 }
\ No newline at end of file
diff --git a/src/test/java/de/akquinet/devops/GitBlit4UITests.java b/src/test/java/de/akquinet/devops/GitBlit4UITests.java
index d4e3222..9c559f8 100644
--- a/src/test/java/de/akquinet/devops/GitBlit4UITests.java
+++ b/src/test/java/de/akquinet/devops/GitBlit4UITests.java
@@ -9,7 +9,7 @@
 	private boolean luceneIndexingEnabled;
 
 	public GitBlit4UITests(boolean luceneIndexingEnabled) {
-		super(null);
+		super();
 		this.luceneIndexingEnabled = luceneIndexingEnabled;
 	}
 

--
Gitblit v1.9.1