James Moger
2013-01-04 4e3c152fa7e97200855ba0d2716362dbe7976920
Support local accounts with LdapUserService and RedmineUserService (issue-183)
19 files modified
267 ■■■■ changed files
docs/04_releases.mkd 1 ●●●● patch | view | raw | blame | history
src/com/gitblit/ConfigUserService.java 4 ●●●● patch | view | raw | blame | history
src/com/gitblit/Constants.java 8 ●●●●● patch | view | raw | blame | history
src/com/gitblit/GitBlit.java 32 ●●●● patch | view | raw | blame | history
src/com/gitblit/GitblitUserService.java 49 ●●●● patch | view | raw | blame | history
src/com/gitblit/LdapUserService.java 18 ●●●● patch | view | raw | blame | history
src/com/gitblit/RedmineUserService.java 51 ●●●● patch | view | raw | blame | history
src/com/gitblit/client/UsersPanel.java 4 ●●●● patch | view | raw | blame | history
src/com/gitblit/client/UsersTableModel.java 21 ●●●●● patch | view | raw | blame | history
src/com/gitblit/models/UserModel.java 8 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/BasePage.java 2 ●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/ChangePasswordPage.java 5 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/EditTeamPage.java 2 ●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/EditUserPage.java 12 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/panels/TeamsPanel.java 2 ●●● patch | view | raw | blame | history
src/com/gitblit/wicket/panels/UsersPanel.html 4 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/panels/UsersPanel.java 4 ●●●● patch | view | raw | blame | history
tests/com/gitblit/tests/LdapUserServiceTest.java 16 ●●●●● patch | view | raw | blame | history
tests/com/gitblit/tests/RedmineUserServiceTest.java 24 ●●●●● patch | view | raw | blame | history
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
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);
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)
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
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;
    }
}
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, "");        
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;
    }
}
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());
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());
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();
    }
    /**
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
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") {
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"));
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));
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",
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>                  
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()) : ""));
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));
    }
}
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));
    }
}