From 4e3c152fa7e97200855ba0d2716362dbe7976920 Mon Sep 17 00:00:00 2001
From: James Moger <james.moger@gitblit.com>
Date: Fri, 04 Jan 2013 17:23:23 -0500
Subject: [PATCH] Support local accounts with LdapUserService and RedmineUserService (issue-183)

---
 src/com/gitblit/wicket/panels/UsersPanel.java        |    4 
 tests/com/gitblit/tests/RedmineUserServiceTest.java  |   24 ++++
 src/com/gitblit/LdapUserService.java                 |   18 +++
 src/com/gitblit/ConfigUserService.java               |    4 
 src/com/gitblit/wicket/pages/EditUserPage.java       |   12 +-
 src/com/gitblit/client/UsersTableModel.java          |   21 ++-
 src/com/gitblit/models/UserModel.java                |    8 +
 src/com/gitblit/RedmineUserService.java              |   51 ++++++++--
 src/com/gitblit/GitBlit.java                         |   32 ++++-
 src/com/gitblit/wicket/pages/EditTeamPage.java       |    2 
 docs/04_releases.mkd                                 |    1 
 src/com/gitblit/wicket/pages/BasePage.java           |    2 
 src/com/gitblit/wicket/pages/ChangePasswordPage.java |    5 
 src/com/gitblit/wicket/panels/UsersPanel.html        |    4 
 src/com/gitblit/client/UsersPanel.java               |    4 
 src/com/gitblit/GitblitUserService.java              |   49 ++++++++-
 tests/com/gitblit/tests/LdapUserServiceTest.java     |   16 +++
 src/com/gitblit/wicket/panels/TeamsPanel.java        |    2 
 src/com/gitblit/Constants.java                       |    8 +
 19 files changed, 212 insertions(+), 55 deletions(-)

diff --git a/docs/04_releases.mkd b/docs/04_releases.mkd
index 6aec85c..d5e777c 100644
--- a/docs/04_releases.mkd
+++ b/docs/04_releases.mkd
@@ -12,6 +12,7 @@
 
 #### additions
 
+- Support for locally and remotely authenticated accounts in LdapUserService and RedmineUserService (issue 183)
 - Added Dutch translation (github/kwoot)
 
 #### changes
diff --git a/src/com/gitblit/ConfigUserService.java b/src/com/gitblit/ConfigUserService.java
index 068bbe3..67ad053 100644
--- a/src/com/gitblit/ConfigUserService.java
+++ b/src/com/gitblit/ConfigUserService.java
@@ -409,6 +409,10 @@
 			// Read realm file
 			read();
 			UserModel model = users.remove(username.toLowerCase());
+			if (model == null) {
+				// user does not exist
+				return false;
+			}
 			// remove user from team
 			for (TeamModel team : model.teams) {
 				TeamModel t = teams.get(team.name);
diff --git a/src/com/gitblit/Constants.java b/src/com/gitblit/Constants.java
index f2067f6..ca33269 100644
--- a/src/com/gitblit/Constants.java
+++ b/src/com/gitblit/Constants.java
@@ -405,6 +405,14 @@
 			return ordinal() <= COOKIE.ordinal();
 		}
 	}
+	
+	public static enum AccountType {
+		LOCAL, LDAP, REDMINE;
+		
+		public boolean isLocal() {
+			return this == LOCAL;
+		}
+	}
 
 	@Documented
 	@Retention(RetentionPolicy.RUNTIME)
diff --git a/src/com/gitblit/GitBlit.java b/src/com/gitblit/GitBlit.java
index 30071bb..74d32df 100644
--- a/src/com/gitblit/GitBlit.java
+++ b/src/com/gitblit/GitBlit.java
@@ -471,36 +471,48 @@
 		this.userService.setup(settings);
 	}
 	
+	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
 	 */
-	public boolean supportsCredentialChanges() {
-		return userService.supportsCredentialChanges();
+	public boolean supportsCredentialChanges(UserModel user) {
+		return (user != null && user.isLocalAccount()) || 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
 	 */
-	public boolean supportsDisplayNameChanges() {
-		return userService.supportsDisplayNameChanges();
+	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
 	 */
-	public boolean supportsEmailAddressChanges() {
-		return userService.supportsEmailAddressChanges();
+	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
 	 */
-	public boolean supportsTeamMembershipChanges() {
-		return userService.supportsTeamMembershipChanges();
+	public boolean supportsTeamMembershipChanges(UserModel user) {
+		return (user != null && user.isLocalAccount()) || userService.supportsTeamMembershipChanges();
 	}
 
 	/**
@@ -789,6 +801,10 @@
 	 * @return the effective list of permissions for the user
 	 */
 	public List<RegistrantAccessPermission> getUserAccessPermissions(UserModel user) {
+		if (StringUtils.isEmpty(user.username)) {
+			// new user
+			return new ArrayList<RegistrantAccessPermission>();
+		}
 		Set<RegistrantAccessPermission> set = new LinkedHashSet<RegistrantAccessPermission>();
 		set.addAll(user.getRepositoryPermissions());
 		// Flag missing repositories
diff --git a/src/com/gitblit/GitblitUserService.java b/src/com/gitblit/GitblitUserService.java
index 141ad8f..db450cf 100644
--- a/src/com/gitblit/GitblitUserService.java
+++ b/src/com/gitblit/GitblitUserService.java
@@ -23,9 +23,11 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.gitblit.Constants.AccountType;
 import com.gitblit.models.TeamModel;
 import com.gitblit.models.UserModel;
 import com.gitblit.utils.DeepCopier;
+import com.gitblit.utils.StringUtils;
 
 /**
  * This class wraps the default user service and is recommended as the starting
@@ -48,6 +50,8 @@
 public class GitblitUserService implements IUserService {
 
 	protected IUserService serviceImpl;
+	
+	protected final String ExternalAccount = "#externalAccount";
 
 	private final Logger logger = LoggerFactory.getLogger(GitblitUserService.class);
 
@@ -144,12 +148,16 @@
 
 	@Override
 	public UserModel authenticate(char[] cookie) {
-		return serviceImpl.authenticate(cookie);
+		UserModel user = serviceImpl.authenticate(cookie);
+		setAccountType(user);
+		return user;
 	}
 
 	@Override
 	public UserModel authenticate(String username, char[] password) {
-		return serviceImpl.authenticate(username, password);
+		UserModel user = serviceImpl.authenticate(username, password);
+		setAccountType(user);
+		return user;
 	}
 	
 	@Override
@@ -159,7 +167,9 @@
 
 	@Override
 	public UserModel getUserModel(String username) {
-		return serviceImpl.getUserModel(username);
+		UserModel user = serviceImpl.getUserModel(username);
+		setAccountType(user);
+		return user;
 	}
 
 	@Override
@@ -174,8 +184,8 @@
 
 	@Override
 	public boolean updateUserModel(String username, UserModel model) {
-		if (supportsCredentialChanges()) {
-			if (!supportsTeamMembershipChanges()) {
+		if (model.isLocalAccount() || supportsCredentialChanges()) {
+			if (!model.isLocalAccount() && !supportsTeamMembershipChanges()) {
 				//  teams are externally controlled - copy from original model
 				UserModel existingModel = getUserModel(username);
 				
@@ -188,7 +198,7 @@
 		if (model.username.equals(username)) {
 			// passwords are not persisted by the backing user service
 			model.password = null;
-			if (!supportsTeamMembershipChanges()) {
+			if (!model.isLocalAccount() && !supportsTeamMembershipChanges()) {
 				//  teams are externally controlled- copy from original model
 				UserModel existingModel = getUserModel(username);
 				
@@ -218,7 +228,11 @@
 
 	@Override
 	public List<UserModel> getAllUsers() {
-		return serviceImpl.getAllUsers();
+		List<UserModel> users = serviceImpl.getAllUsers();
+    	for (UserModel user : users) {
+    		setAccountType(user);
+    	}
+		return users; 
 	}
 
 	@Override
@@ -300,4 +314,25 @@
 	public boolean deleteRepositoryRole(String role) {
 		return serviceImpl.deleteRepositoryRole(role);
 	}
+	
+	protected boolean isLocalAccount(String username) {
+		UserModel user = getUserModel(username);
+		return user != null && user.isLocalAccount();
+	}
+	
+	protected void setAccountType(UserModel user) {
+		if (user != null) {
+			if (!StringUtils.isEmpty(user.password)
+					&& !ExternalAccount.equalsIgnoreCase(user.password)
+					&& !"StoredInLDAP".equalsIgnoreCase(user.password)) {
+				user.accountType = AccountType.LOCAL;
+			} else {
+				user.accountType = getAccountType();
+			}
+		}
+	}
+	
+	protected AccountType getAccountType() {
+		return AccountType.LOCAL;
+	}
 }
diff --git a/src/com/gitblit/LdapUserService.java b/src/com/gitblit/LdapUserService.java
index 9ce18f6..3c032b5 100644
--- a/src/com/gitblit/LdapUserService.java
+++ b/src/com/gitblit/LdapUserService.java
@@ -25,6 +25,7 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.gitblit.Constants.AccountType;
 import com.gitblit.models.TeamModel;
 import com.gitblit.models.UserModel;
 import com.gitblit.utils.ArrayUtils;
@@ -50,9 +51,9 @@
 public class LdapUserService extends GitblitUserService {
 
 	public static final Logger logger = LoggerFactory.getLogger(LdapUserService.class);
-	
-	private IStoredSettings settings;
 
+	private IStoredSettings settings;
+	
 	public LdapUserService() {
 		super();
 	}
@@ -155,9 +156,19 @@
 	public boolean supportsTeamMembershipChanges() {
 		return !settings.getBoolean(Keys.realm.ldap.maintainTeams, false);
 	}
+	
+	@Override
+	protected AccountType getAccountType() {
+		 return AccountType.LDAP;
+	}
 
 	@Override
 	public UserModel authenticate(String username, char[] password) {
+		if (isLocalAccount(username)) {
+			// local account, bypass LDAP authentication
+			return super.authenticate(username, password);
+		}
+		
 		String simpleUsername = getSimpleUsername(username);
 		
 		LDAPConnection ldapConnection = getLdapConnection();
@@ -239,7 +250,8 @@
 		setAdminAttribute(user);
 		
 		// Don't want visibility into the real password, make up a dummy
-		user.password = "StoredInLDAP";
+		user.password = ExternalAccount;
+		user.accountType = getAccountType();
 		
 		// Get full name Attribute
 		String displayName = settings.getString(Keys.realm.ldap.displayName, "");		
diff --git a/src/com/gitblit/RedmineUserService.java b/src/com/gitblit/RedmineUserService.java
index b890f21..2fa14b7 100644
--- a/src/com/gitblit/RedmineUserService.java
+++ b/src/com/gitblit/RedmineUserService.java
@@ -9,7 +9,9 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.gitblit.Constants.AccountType;
 import com.gitblit.models.UserModel;
+import com.gitblit.utils.ArrayUtils;
 import com.gitblit.utils.ConnectionUtils;
 import com.gitblit.utils.StringUtils;
 import com.google.gson.Gson;
@@ -71,9 +73,19 @@
     public boolean supportsTeamMembershipChanges() {
         return false;
     }
+    
+	 @Override
+	protected AccountType getAccountType() {
+		return AccountType.REDMINE;
+	}
 
     @Override
     public UserModel authenticate(String username, char[] password) {
+		if (isLocalAccount(username)) {
+			// local account, bypass Redmine authentication
+			return super.authenticate(username, password);
+		}
+
         String urlText = this.settings.getString(Keys.realm.redmine.url, "");
         if (!urlText.endsWith("/")) {
             urlText.concat("/");
@@ -87,19 +99,37 @@
             String login = current.user.login;
 
             boolean canAdmin = true;
-            // non admin user can not get login name
             if (StringUtils.isEmpty(login)) {
-                canAdmin = false;
                 login = current.user.mail;
+                
+            	// non admin user can not get login name
+            	// TODO review this assumption, if it is true, it is undocumented
+                canAdmin = false;
             }
-
-            UserModel userModel = new UserModel(login);
-            userModel.canAdmin = canAdmin;
-            userModel.displayName = current.user.firstname + " " + current.user.lastname;
-            userModel.emailAddress = current.user.mail;
-            userModel.cookie = StringUtils.getSHA1(userModel.username + new String(password));
-
-            return userModel;
+            
+            UserModel user = getUserModel(login);
+            if (user == null)	// create user object for new authenticated user
+            	user = new UserModel(login);
+            
+            // create a user cookie
+			if (StringUtils.isEmpty(user.cookie) && !ArrayUtils.isEmpty(password)) {
+				user.cookie = StringUtils.getSHA1(user.username + new String(password));
+			}
+            
+            // update user attributes from Redmine
+			user.accountType = getAccountType();
+			user.canAdmin = canAdmin;
+        	user.displayName = current.user.firstname + " " + current.user.lastname;
+        	user.emailAddress = current.user.mail;
+        	user.password = ExternalAccount;
+        	
+        	// TODO Redmine group mapping for administration & teams
+        	// http://www.redmine.org/projects/redmine/wiki/Rest_Users
+        	
+        	// push the changes to the backing user service
+        	super.updateUserModel(user);
+        	
+            return user;
         } catch (IOException e) {
             logger.error("authenticate", e);
         }
@@ -126,5 +156,4 @@
     public void setTestingCurrentUserAsJson(String json) {
         this.testingJson = json;
     }
-
 }
diff --git a/src/com/gitblit/client/UsersPanel.java b/src/com/gitblit/client/UsersPanel.java
index e14c001..c53a579 100644
--- a/src/com/gitblit/client/UsersPanel.java
+++ b/src/com/gitblit/client/UsersPanel.java
@@ -112,8 +112,8 @@
 		String name = table.getColumnName(UsersTableModel.Columns.Name.ordinal());
 		table.getColumn(name).setCellRenderer(nameRenderer);
 		
-		int w = 125;
-		name = table.getColumnName(UsersTableModel.Columns.AccessLevel.ordinal());
+		int w = 130;
+		name = table.getColumnName(UsersTableModel.Columns.Type.ordinal());
 		table.getColumn(name).setMinWidth(w);
 		table.getColumn(name).setMaxWidth(w);
 		name = table.getColumnName(UsersTableModel.Columns.Teams.ordinal());
diff --git a/src/com/gitblit/client/UsersTableModel.java b/src/com/gitblit/client/UsersTableModel.java
index b8ce45d..439d5af 100644
--- a/src/com/gitblit/client/UsersTableModel.java
+++ b/src/com/gitblit/client/UsersTableModel.java
@@ -36,7 +36,7 @@
 	List<UserModel> list;
 
 	enum Columns {
-		Name, Display_Name, AccessLevel, Teams, Repositories;
+		Name, Display_Name, Type, Teams, Repositories;
 
 		@Override
 		public String toString() {
@@ -71,8 +71,8 @@
 			return Translation.get("gb.name");
 		case Display_Name:
 			return Translation.get("gb.displayName");
-		case AccessLevel:
-			return Translation.get("gb.accessLevel");
+		case Type:
+			return Translation.get("gb.type");
 		case Teams:
 			return Translation.get("gb.teamMemberships");
 		case Repositories:
@@ -101,11 +101,18 @@
 			return model.username;
 		case Display_Name:
 			return model.displayName;
-		case AccessLevel:
-			if (model.canAdmin()) {
-				return "administrator";
+		case Type:
+			StringBuilder sb = new StringBuilder();
+			if (model.accountType != null) {
+				sb.append(model.accountType.name());
 			}
-			return "";
+			if (model.canAdmin()) {
+				if (sb.length() > 0) {
+					sb.append(", ");
+				}
+				sb.append("admin");
+			}
+			return sb.toString();
 		case Teams:
 			return (model.teams == null || model.teams.size() == 0) ? "" : String
 					.valueOf(model.teams.size());
diff --git a/src/com/gitblit/models/UserModel.java b/src/com/gitblit/models/UserModel.java
index ac67ff7..54e81cb 100644
--- a/src/com/gitblit/models/UserModel.java
+++ b/src/com/gitblit/models/UserModel.java
@@ -29,6 +29,7 @@
 
 import com.gitblit.Constants.AccessPermission;
 import com.gitblit.Constants.AccessRestrictionType;
+import com.gitblit.Constants.AccountType;
 import com.gitblit.Constants.AuthorizationControl;
 import com.gitblit.Constants.PermissionType;
 import com.gitblit.Constants.RegistrantType;
@@ -73,15 +74,22 @@
 
 	// non-persisted fields
 	public boolean isAuthenticated;
+	public AccountType accountType;
 	
 	public UserModel(String username) {
 		this.username = username;
 		this.isAuthenticated = true;
+		this.accountType = AccountType.LOCAL;
 	}
 
 	private UserModel() {
 		this.username = "$anonymous";
 		this.isAuthenticated = false;
+		this.accountType = AccountType.LOCAL;
+	}
+	
+	public boolean isLocalAccount() {
+		return accountType.isLocal();
 	}
 
 	/**
diff --git a/src/com/gitblit/wicket/pages/BasePage.java b/src/com/gitblit/wicket/pages/BasePage.java
index 9d46908..9f98135 100644
--- a/src/com/gitblit/wicket/pages/BasePage.java
+++ b/src/com/gitblit/wicket/pages/BasePage.java
@@ -433,7 +433,7 @@
 			GitBlitWebSession session = GitBlitWebSession.get();
 			if (session.isLoggedIn()) {				
 				UserModel user = session.getUser();
-				boolean editCredentials = GitBlit.self().supportsCredentialChanges();
+				boolean editCredentials = GitBlit.self().supportsCredentialChanges(user);
 				boolean standardLogin = session.authenticationType.isStandard();
 
 				// username, logout, and change password
diff --git a/src/com/gitblit/wicket/pages/ChangePasswordPage.java b/src/com/gitblit/wicket/pages/ChangePasswordPage.java
index 5e66300..3741853 100644
--- a/src/com/gitblit/wicket/pages/ChangePasswordPage.java
+++ b/src/com/gitblit/wicket/pages/ChangePasswordPage.java
@@ -51,12 +51,13 @@
 			throw new RestartResponseException(getApplication().getHomePage());
 		}
 		
-		if (!GitBlit.self().supportsCredentialChanges()) {
+		UserModel user = GitBlitWebSession.get().getUser();		
+		if (!GitBlit.self().supportsCredentialChanges(user)) {
 			error(MessageFormat.format(getString("gb.userServiceDoesNotPermitPasswordChanges"),
 					GitBlit.getString(Keys.realm.userService, "users.conf")), true);
 		}
 		
-		setupPage(getString("gb.changePassword"), GitBlitWebSession.get().getUsername());
+		setupPage(getString("gb.changePassword"), user.username);
 
 		StatelessForm<Void> form = new StatelessForm<Void>("passwordForm") {
 
diff --git a/src/com/gitblit/wicket/pages/EditTeamPage.java b/src/com/gitblit/wicket/pages/EditTeamPage.java
index 1991c02..8344d38 100644
--- a/src/com/gitblit/wicket/pages/EditTeamPage.java
+++ b/src/com/gitblit/wicket/pages/EditTeamPage.java
@@ -212,7 +212,7 @@
 		form.add(new SimpleAttributeModifier("autocomplete", "off"));
 
 		// not all user services support manipulating team memberships
-		boolean editMemberships = GitBlit.self().supportsTeamMembershipChanges();
+		boolean editMemberships = GitBlit.self().supportsTeamMembershipChanges(null);
 		
 		// field names reflective match TeamModel fields
 		form.add(new TextField<String>("name"));
diff --git a/src/com/gitblit/wicket/pages/EditUserPage.java b/src/com/gitblit/wicket/pages/EditUserPage.java
index 7a01fb6..4939e97 100644
--- a/src/com/gitblit/wicket/pages/EditUserPage.java
+++ b/src/com/gitblit/wicket/pages/EditUserPage.java
@@ -55,7 +55,7 @@
 	public EditUserPage() {
 		// create constructor
 		super();
-		if (!GitBlit.self().supportsCredentialChanges()) {
+		if (!GitBlit.self().supportsAddUser()) {
 			error(MessageFormat.format(getString("gb.userServiceDoesNotPermitAddUser"),
 					GitBlit.getString(Keys.realm.userService, "users.conf")), true);
 		}
@@ -134,7 +134,7 @@
 				}
 				boolean rename = !StringUtils.isEmpty(oldName)
 						&& !oldName.equalsIgnoreCase(username);
-				if (GitBlit.self().supportsCredentialChanges()) {
+				if (GitBlit.self().supportsCredentialChanges(userModel)) {
 					if (!userModel.password.equals(confirmPassword.getObject())) {
 						error(getString("gb.passwordsDoNotMatch"));
 						return;
@@ -210,16 +210,16 @@
 		form.add(new SimpleAttributeModifier("autocomplete", "off"));
 		
 		// not all user services support manipulating username and password
-		boolean editCredentials = GitBlit.self().supportsCredentialChanges();
+		boolean editCredentials = GitBlit.self().supportsCredentialChanges(userModel);
 		
 		// not all user services support manipulating display name
-		boolean editDisplayName = GitBlit.self().supportsDisplayNameChanges();
+		boolean editDisplayName = GitBlit.self().supportsDisplayNameChanges(userModel);
 
 		// not all user services support manipulating email address
-		boolean editEmailAddress = GitBlit.self().supportsEmailAddressChanges();
+		boolean editEmailAddress = GitBlit.self().supportsEmailAddressChanges(userModel);
 
 		// not all user services support manipulating team memberships
-		boolean editTeams = GitBlit.self().supportsTeamMembershipChanges();
+		boolean editTeams = GitBlit.self().supportsTeamMembershipChanges(userModel);
 
 		// field names reflective match UserModel fields
 		form.add(new TextField<String>("username").setEnabled(editCredentials));
diff --git a/src/com/gitblit/wicket/panels/TeamsPanel.java b/src/com/gitblit/wicket/panels/TeamsPanel.java
index cc37c51..b76388b 100644
--- a/src/com/gitblit/wicket/panels/TeamsPanel.java
+++ b/src/com/gitblit/wicket/panels/TeamsPanel.java
@@ -40,7 +40,7 @@
 
 		Fragment adminLinks = new Fragment("adminPanel", "adminLinks", this);
 		adminLinks.add(new BookmarkablePageLink<Void>("newTeam", EditTeamPage.class));
-		add(adminLinks.setVisible(showAdmin && GitBlit.self().supportsTeamMembershipChanges()));
+		add(adminLinks.setVisible(showAdmin && GitBlit.self().supportsTeamMembershipChanges(null)));
 
 		final List<TeamModel> teams = GitBlit.self().getAllTeams();
 		DataView<TeamModel> teamsView = new DataView<TeamModel>("teamRow",
diff --git a/src/com/gitblit/wicket/panels/UsersPanel.html b/src/com/gitblit/wicket/panels/UsersPanel.html
index aed985c..8015961 100644
--- a/src/com/gitblit/wicket/panels/UsersPanel.html
+++ b/src/com/gitblit/wicket/panels/UsersPanel.html
@@ -17,7 +17,7 @@
 			</th>
 			<th class="hidden-phone hidden-tablet left"><wicket:message key="gb.displayName">[display name]</wicket:message></th>
 			<th class="hidden-phone hidden-tablet left"><wicket:message key="gb.emailAddress">[email address]</wicket:message></th>
-			<th class="hidden-phone" style="width:120px;"><wicket:message key="gb.accessLevel">[access level]</wicket:message></th>
+			<th class="hidden-phone" style="width:140px;"><wicket:message key="gb.type">[type]</wicket:message></th>
 			<th class="hidden-phone" style="width:140px;"><wicket:message key="gb.teamMemberships">[team memberships]</wicket:message></th>
 			<th class="hidden-phone" style="width:100px;"><wicket:message key="gb.repositories">[repositories]</wicket:message></th>
 			<th style="width:80px;" class="right"></th>
@@ -27,7 +27,7 @@
        			<td class="left" ><span class="list" wicket:id="username">[username]</span></td>
        			<td class="hidden-phone hidden-tablet left" ><span class="list" wicket:id="displayName">[display name]</span></td>
        			<td class="hidden-phone hidden-tablet left" ><span class="list" wicket:id="emailAddress">[email address]</span></td>
-       			<td class="hidden-phone left" ><span class="list" wicket:id="accesslevel">[access level]</span></td>
+       			<td class="hidden-phone left" ><span style="font-size: 0.8em;" wicket:id="accountType">[account type]</span></td>
        			<td class="hidden-phone left" ><span class="list" wicket:id="teams">[team memberships]</span></td>
        			<td class="hidden-phone left" ><span class="list" wicket:id="repositories">[repositories]</span></td>
        			<td class="rightAlign"><span wicket:id="userLinks"></span></td>      			
diff --git a/src/com/gitblit/wicket/panels/UsersPanel.java b/src/com/gitblit/wicket/panels/UsersPanel.java
index 46c502e..f5b95e2 100644
--- a/src/com/gitblit/wicket/panels/UsersPanel.java
+++ b/src/com/gitblit/wicket/panels/UsersPanel.java
@@ -41,7 +41,7 @@
 
 		Fragment adminLinks = new Fragment("adminPanel", "adminLinks", this);
 		adminLinks.add(new BookmarkablePageLink<Void>("newUser", EditUserPage.class)
-				.setVisible(GitBlit.self().supportsCredentialChanges()));
+				.setVisible(GitBlit.self().supportsAddUser()));
 		add(adminLinks.setVisible(showAdmin));
 
 		final List<UserModel> users = GitBlit.self().getAllUsers();
@@ -81,7 +81,7 @@
 					item.add(editLink);
 				}
 
-				item.add(new Label("accesslevel", entry.canAdmin() ? "administrator" : ""));
+				item.add(new Label("accountType", entry.accountType.name() + (entry.canAdmin() ? ", admin":"")));
 				item.add(new Label("teams", entry.teams.size() > 0 ? ("" + entry.teams.size()) : ""));
 				item.add(new Label("repositories",
 						entry.permissions.size() > 0 ? ("" + entry.permissions.size()) : ""));
diff --git a/tests/com/gitblit/tests/LdapUserServiceTest.java b/tests/com/gitblit/tests/LdapUserServiceTest.java
index ffe8264..a928f4a 100644
--- a/tests/com/gitblit/tests/LdapUserServiceTest.java
+++ b/tests/com/gitblit/tests/LdapUserServiceTest.java
@@ -31,6 +31,7 @@
 import com.gitblit.LdapUserService;
 import com.gitblit.models.UserModel;
 import com.gitblit.tests.mock.MemorySettings;
+import com.gitblit.utils.StringUtils;
 import com.unboundid.ldap.listener.InMemoryDirectoryServer;
 import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
 import com.unboundid.ldap.listener.InMemoryListenerConfig;
@@ -154,5 +155,20 @@
 		UserModel userOneModel = ldapUserService.authenticate("*)(userPassword=userOnePassword", "userOnePassword".toCharArray());
 		assertNull(userOneModel);
 	}
+	
+	@Test
+	public void testLocalAccount() {
+		UserModel localAccount = new UserModel("bruce");
+		localAccount.displayName = "Bruce Campbell";
+		localAccount.password = StringUtils.MD5_TYPE + StringUtils.getMD5("gimmesomesugar");
+		ldapUserService.deleteUser(localAccount.username);
+		assertTrue("Failed to add local account",
+				ldapUserService.updateUserModel(localAccount));
+		assertEquals("Accounts are not equal!", 
+				localAccount, 
+				ldapUserService.authenticate(localAccount.username, "gimmesomesugar".toCharArray()));
+		assertTrue("Failed to delete local account!",
+				ldapUserService.deleteUser(localAccount.username));
+	}
 
 }
diff --git a/tests/com/gitblit/tests/RedmineUserServiceTest.java b/tests/com/gitblit/tests/RedmineUserServiceTest.java
index 30a8fb2..0e12542 100644
--- a/tests/com/gitblit/tests/RedmineUserServiceTest.java
+++ b/tests/com/gitblit/tests/RedmineUserServiceTest.java
@@ -1,9 +1,10 @@
 package com.gitblit.tests;
 
 import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
 
 import java.util.HashMap;
 
@@ -12,6 +13,7 @@
 import com.gitblit.RedmineUserService;
 import com.gitblit.models.UserModel;
 import com.gitblit.tests.mock.MemorySettings;
+import com.gitblit.utils.StringUtils;
 
 public class RedmineUserServiceTest {
 
@@ -29,7 +31,7 @@
         redmineUserService.setup(new MemorySettings(new HashMap<String, Object>()));
         redmineUserService.setTestingCurrentUserAsJson(JSON);
         UserModel userModel = redmineUserService.authenticate("RedmineUserId", "RedmineAPIKey".toCharArray());
-        assertThat(userModel.getName(), is("RedmineUserId"));
+        assertThat(userModel.getName(), is("redmineuserid"));
         assertThat(userModel.getDisplayName(), is("baz foo"));
         assertThat(userModel.emailAddress, is("baz@example.com"));
         assertNotNull(userModel.cookie);
@@ -48,5 +50,23 @@
         assertNotNull(userModel.cookie);
         assertThat(userModel.canAdmin, is(false));
     }
+    
+    @Test
+	public void testLocalAccount() {
+        RedmineUserService redmineUserService = new RedmineUserService();
+        redmineUserService.setup(new MemorySettings(new HashMap<String, Object>()));
+
+		UserModel localAccount = new UserModel("bruce");
+		localAccount.displayName = "Bruce Campbell";
+		localAccount.password = StringUtils.MD5_TYPE + StringUtils.getMD5("gimmesomesugar");
+		redmineUserService.deleteUser(localAccount.username);
+		assertTrue("Failed to add local account",
+				redmineUserService.updateUserModel(localAccount));
+		assertEquals("Accounts are not equal!", 
+				localAccount, 
+				redmineUserService.authenticate(localAccount.username, "gimmesomesugar".toCharArray()));
+		assertTrue("Failed to delete local account!",
+				redmineUserService.deleteUser(localAccount.username));
+	}
 
 }

--
Gitblit v1.9.1