\r
#### additions\r
\r
+- Support for locally and remotely authenticated accounts in LdapUserService and RedmineUserService (issue 183)\r
- Added Dutch translation (github/kwoot)\r
\r
#### changes\r
// Read realm file\r
read();\r
UserModel model = users.remove(username.toLowerCase());\r
+ if (model == null) {\r
+ // user does not exist\r
+ return false;\r
+ }\r
// remove user from team\r
for (TeamModel team : model.teams) {\r
TeamModel t = teams.get(team.name);\r
return ordinal() <= COOKIE.ordinal();\r
}\r
}\r
+ \r
+ public static enum AccountType {\r
+ LOCAL, LDAP, REDMINE;\r
+ \r
+ public boolean isLocal() {\r
+ return this == LOCAL;\r
+ }\r
+ }\r
\r
@Documented\r
@Retention(RetentionPolicy.RUNTIME)\r
this.userService.setup(settings);\r
}\r
\r
+ public boolean supportsAddUser() {\r
+ return supportsCredentialChanges(new UserModel(""));\r
+ }\r
+ \r
/**\r
+ * Returns true if the user's credentials can be changed.\r
* \r
+ * @param user\r
* @return true if the user service supports credential changes\r
*/\r
- public boolean supportsCredentialChanges() {\r
- return userService.supportsCredentialChanges();\r
+ public boolean supportsCredentialChanges(UserModel user) {\r
+ return (user != null && user.isLocalAccount()) || userService.supportsCredentialChanges();\r
}\r
\r
/**\r
+ * Returns true if the user's display name can be changed.\r
* \r
+ * @param user\r
* @return true if the user service supports display name changes\r
*/\r
- public boolean supportsDisplayNameChanges() {\r
- return userService.supportsDisplayNameChanges();\r
+ public boolean supportsDisplayNameChanges(UserModel user) {\r
+ return (user != null && user.isLocalAccount()) || userService.supportsDisplayNameChanges();\r
}\r
\r
/**\r
+ * Returns true if the user's email address can be changed.\r
* \r
+ * @param user\r
* @return true if the user service supports email address changes\r
*/\r
- public boolean supportsEmailAddressChanges() {\r
- return userService.supportsEmailAddressChanges();\r
+ public boolean supportsEmailAddressChanges(UserModel user) {\r
+ return (user != null && user.isLocalAccount()) || userService.supportsEmailAddressChanges();\r
}\r
\r
/**\r
+ * Returns true if the user's team memberships can be changed.\r
* \r
+ * @param user\r
* @return true if the user service supports team membership changes\r
*/\r
- public boolean supportsTeamMembershipChanges() {\r
- return userService.supportsTeamMembershipChanges();\r
+ public boolean supportsTeamMembershipChanges(UserModel user) {\r
+ return (user != null && user.isLocalAccount()) || userService.supportsTeamMembershipChanges();\r
}\r
\r
/**\r
* @return the effective list of permissions for the user\r
*/\r
public List<RegistrantAccessPermission> getUserAccessPermissions(UserModel user) {\r
+ if (StringUtils.isEmpty(user.username)) {\r
+ // new user\r
+ return new ArrayList<RegistrantAccessPermission>();\r
+ }\r
Set<RegistrantAccessPermission> set = new LinkedHashSet<RegistrantAccessPermission>();\r
set.addAll(user.getRepositoryPermissions());\r
// Flag missing repositories\r
import org.slf4j.Logger;\r
import org.slf4j.LoggerFactory;\r
\r
+import com.gitblit.Constants.AccountType;\r
import com.gitblit.models.TeamModel;\r
import com.gitblit.models.UserModel;\r
import com.gitblit.utils.DeepCopier;\r
+import com.gitblit.utils.StringUtils;\r
\r
/**\r
* This class wraps the default user service and is recommended as the starting\r
public class GitblitUserService implements IUserService {\r
\r
protected IUserService serviceImpl;\r
+ \r
+ protected final String ExternalAccount = "#externalAccount";\r
\r
private final Logger logger = LoggerFactory.getLogger(GitblitUserService.class);\r
\r
\r
@Override\r
public UserModel authenticate(char[] cookie) {\r
- return serviceImpl.authenticate(cookie);\r
+ UserModel user = serviceImpl.authenticate(cookie);\r
+ setAccountType(user);\r
+ return user;\r
}\r
\r
@Override\r
public UserModel authenticate(String username, char[] password) {\r
- return serviceImpl.authenticate(username, password);\r
+ UserModel user = serviceImpl.authenticate(username, password);\r
+ setAccountType(user);\r
+ return user;\r
}\r
\r
@Override\r
\r
@Override\r
public UserModel getUserModel(String username) {\r
- return serviceImpl.getUserModel(username);\r
+ UserModel user = serviceImpl.getUserModel(username);\r
+ setAccountType(user);\r
+ return user;\r
}\r
\r
@Override\r
\r
@Override\r
public boolean updateUserModel(String username, UserModel model) {\r
- if (supportsCredentialChanges()) {\r
- if (!supportsTeamMembershipChanges()) {\r
+ if (model.isLocalAccount() || supportsCredentialChanges()) {\r
+ if (!model.isLocalAccount() && !supportsTeamMembershipChanges()) {\r
// teams are externally controlled - copy from original model\r
UserModel existingModel = getUserModel(username);\r
\r
if (model.username.equals(username)) {\r
// passwords are not persisted by the backing user service\r
model.password = null;\r
- if (!supportsTeamMembershipChanges()) {\r
+ if (!model.isLocalAccount() && !supportsTeamMembershipChanges()) {\r
// teams are externally controlled- copy from original model\r
UserModel existingModel = getUserModel(username);\r
\r
\r
@Override\r
public List<UserModel> getAllUsers() {\r
- return serviceImpl.getAllUsers();\r
+ List<UserModel> users = serviceImpl.getAllUsers();\r
+ for (UserModel user : users) {\r
+ setAccountType(user);\r
+ }\r
+ return users; \r
}\r
\r
@Override\r
public boolean deleteRepositoryRole(String role) {\r
return serviceImpl.deleteRepositoryRole(role);\r
}\r
+ \r
+ protected boolean isLocalAccount(String username) {\r
+ UserModel user = getUserModel(username);\r
+ return user != null && user.isLocalAccount();\r
+ }\r
+ \r
+ protected void setAccountType(UserModel user) {\r
+ if (user != null) {\r
+ if (!StringUtils.isEmpty(user.password)\r
+ && !ExternalAccount.equalsIgnoreCase(user.password)\r
+ && !"StoredInLDAP".equalsIgnoreCase(user.password)) {\r
+ user.accountType = AccountType.LOCAL;\r
+ } else {\r
+ user.accountType = getAccountType();\r
+ }\r
+ }\r
+ }\r
+ \r
+ protected AccountType getAccountType() {\r
+ return AccountType.LOCAL;\r
+ }\r
}\r
import org.slf4j.Logger;\r
import org.slf4j.LoggerFactory;\r
\r
+import com.gitblit.Constants.AccountType;\r
import com.gitblit.models.TeamModel;\r
import com.gitblit.models.UserModel;\r
import com.gitblit.utils.ArrayUtils;\r
public class LdapUserService extends GitblitUserService {\r
\r
public static final Logger logger = LoggerFactory.getLogger(LdapUserService.class);\r
- \r
- private IStoredSettings settings;\r
\r
+ private IStoredSettings settings;\r
+ \r
public LdapUserService() {\r
super();\r
}\r
public boolean supportsTeamMembershipChanges() {\r
return !settings.getBoolean(Keys.realm.ldap.maintainTeams, false);\r
}\r
+ \r
+ @Override\r
+ protected AccountType getAccountType() {\r
+ return AccountType.LDAP;\r
+ }\r
\r
@Override\r
public UserModel authenticate(String username, char[] password) {\r
+ if (isLocalAccount(username)) {\r
+ // local account, bypass LDAP authentication\r
+ return super.authenticate(username, password);\r
+ }\r
+ \r
String simpleUsername = getSimpleUsername(username);\r
\r
LDAPConnection ldapConnection = getLdapConnection();\r
setAdminAttribute(user);\r
\r
// Don't want visibility into the real password, make up a dummy\r
- user.password = "StoredInLDAP";\r
+ user.password = ExternalAccount;\r
+ user.accountType = getAccountType();\r
\r
// Get full name Attribute\r
String displayName = settings.getString(Keys.realm.ldap.displayName, ""); \r
import org.slf4j.Logger;\r
import org.slf4j.LoggerFactory;\r
\r
+import com.gitblit.Constants.AccountType;\r
import com.gitblit.models.UserModel;\r
+import com.gitblit.utils.ArrayUtils;\r
import com.gitblit.utils.ConnectionUtils;\r
import com.gitblit.utils.StringUtils;\r
import com.google.gson.Gson;\r
public boolean supportsTeamMembershipChanges() {\r
return false;\r
}\r
+ \r
+ @Override\r
+ protected AccountType getAccountType() {\r
+ return AccountType.REDMINE;\r
+ }\r
\r
@Override\r
public UserModel authenticate(String username, char[] password) {\r
+ if (isLocalAccount(username)) {\r
+ // local account, bypass Redmine authentication\r
+ return super.authenticate(username, password);\r
+ }\r
+\r
String urlText = this.settings.getString(Keys.realm.redmine.url, "");\r
if (!urlText.endsWith("/")) {\r
urlText.concat("/");\r
String login = current.user.login;\r
\r
boolean canAdmin = true;\r
- // non admin user can not get login name\r
if (StringUtils.isEmpty(login)) {\r
- canAdmin = false;\r
login = current.user.mail;\r
+ \r
+ // non admin user can not get login name\r
+ // TODO review this assumption, if it is true, it is undocumented\r
+ canAdmin = false;\r
}\r
-\r
- UserModel userModel = new UserModel(login);\r
- userModel.canAdmin = canAdmin;\r
- userModel.displayName = current.user.firstname + " " + current.user.lastname;\r
- userModel.emailAddress = current.user.mail;\r
- userModel.cookie = StringUtils.getSHA1(userModel.username + new String(password));\r
-\r
- return userModel;\r
+ \r
+ UserModel user = getUserModel(login);\r
+ if (user == null) // create user object for new authenticated user\r
+ user = new UserModel(login);\r
+ \r
+ // create a user cookie\r
+ if (StringUtils.isEmpty(user.cookie) && !ArrayUtils.isEmpty(password)) {\r
+ user.cookie = StringUtils.getSHA1(user.username + new String(password));\r
+ }\r
+ \r
+ // update user attributes from Redmine\r
+ user.accountType = getAccountType();\r
+ user.canAdmin = canAdmin;\r
+ user.displayName = current.user.firstname + " " + current.user.lastname;\r
+ user.emailAddress = current.user.mail;\r
+ user.password = ExternalAccount;\r
+ \r
+ // TODO Redmine group mapping for administration & teams\r
+ // http://www.redmine.org/projects/redmine/wiki/Rest_Users\r
+ \r
+ // push the changes to the backing user service\r
+ super.updateUserModel(user);\r
+ \r
+ return user;\r
} catch (IOException e) {\r
logger.error("authenticate", e);\r
}\r
public void setTestingCurrentUserAsJson(String json) {\r
this.testingJson = json;\r
}\r
-\r
}\r
String name = table.getColumnName(UsersTableModel.Columns.Name.ordinal());\r
table.getColumn(name).setCellRenderer(nameRenderer);\r
\r
- int w = 125;\r
- name = table.getColumnName(UsersTableModel.Columns.AccessLevel.ordinal());\r
+ int w = 130;\r
+ name = table.getColumnName(UsersTableModel.Columns.Type.ordinal());\r
table.getColumn(name).setMinWidth(w);\r
table.getColumn(name).setMaxWidth(w);\r
name = table.getColumnName(UsersTableModel.Columns.Teams.ordinal());\r
List<UserModel> list;\r
\r
enum Columns {\r
- Name, Display_Name, AccessLevel, Teams, Repositories;\r
+ Name, Display_Name, Type, Teams, Repositories;\r
\r
@Override\r
public String toString() {\r
return Translation.get("gb.name");\r
case Display_Name:\r
return Translation.get("gb.displayName");\r
- case AccessLevel:\r
- return Translation.get("gb.accessLevel");\r
+ case Type:\r
+ return Translation.get("gb.type");\r
case Teams:\r
return Translation.get("gb.teamMemberships");\r
case Repositories:\r
return model.username;\r
case Display_Name:\r
return model.displayName;\r
- case AccessLevel:\r
+ case Type:\r
+ StringBuilder sb = new StringBuilder();\r
+ if (model.accountType != null) {\r
+ sb.append(model.accountType.name());\r
+ }\r
if (model.canAdmin()) {\r
- return "administrator";\r
+ if (sb.length() > 0) {\r
+ sb.append(", ");\r
+ }\r
+ sb.append("admin");\r
}\r
- return "";\r
+ return sb.toString();\r
case Teams:\r
return (model.teams == null || model.teams.size() == 0) ? "" : String\r
.valueOf(model.teams.size());\r
\r
import com.gitblit.Constants.AccessPermission;\r
import com.gitblit.Constants.AccessRestrictionType;\r
+import com.gitblit.Constants.AccountType;\r
import com.gitblit.Constants.AuthorizationControl;\r
import com.gitblit.Constants.PermissionType;\r
import com.gitblit.Constants.RegistrantType;\r
\r
// non-persisted fields\r
public boolean isAuthenticated;\r
+ public AccountType accountType;\r
\r
public UserModel(String username) {\r
this.username = username;\r
this.isAuthenticated = true;\r
+ this.accountType = AccountType.LOCAL;\r
}\r
\r
private UserModel() {\r
this.username = "$anonymous";\r
this.isAuthenticated = false;\r
+ this.accountType = AccountType.LOCAL;\r
+ }\r
+ \r
+ public boolean isLocalAccount() {\r
+ return accountType.isLocal();\r
}\r
\r
/**\r
GitBlitWebSession session = GitBlitWebSession.get();\r
if (session.isLoggedIn()) { \r
UserModel user = session.getUser();\r
- boolean editCredentials = GitBlit.self().supportsCredentialChanges();\r
+ boolean editCredentials = GitBlit.self().supportsCredentialChanges(user);\r
boolean standardLogin = session.authenticationType.isStandard();\r
\r
// username, logout, and change password\r
throw new RestartResponseException(getApplication().getHomePage());\r
}\r
\r
- if (!GitBlit.self().supportsCredentialChanges()) {\r
+ UserModel user = GitBlitWebSession.get().getUser(); \r
+ if (!GitBlit.self().supportsCredentialChanges(user)) {\r
error(MessageFormat.format(getString("gb.userServiceDoesNotPermitPasswordChanges"),\r
GitBlit.getString(Keys.realm.userService, "users.conf")), true);\r
}\r
\r
- setupPage(getString("gb.changePassword"), GitBlitWebSession.get().getUsername());\r
+ setupPage(getString("gb.changePassword"), user.username);\r
\r
StatelessForm<Void> form = new StatelessForm<Void>("passwordForm") {\r
\r
form.add(new SimpleAttributeModifier("autocomplete", "off"));\r
\r
// not all user services support manipulating team memberships\r
- boolean editMemberships = GitBlit.self().supportsTeamMembershipChanges();\r
+ boolean editMemberships = GitBlit.self().supportsTeamMembershipChanges(null);\r
\r
// field names reflective match TeamModel fields\r
form.add(new TextField<String>("name"));\r
public EditUserPage() {\r
// create constructor\r
super();\r
- if (!GitBlit.self().supportsCredentialChanges()) {\r
+ if (!GitBlit.self().supportsAddUser()) {\r
error(MessageFormat.format(getString("gb.userServiceDoesNotPermitAddUser"),\r
GitBlit.getString(Keys.realm.userService, "users.conf")), true);\r
}\r
}\r
boolean rename = !StringUtils.isEmpty(oldName)\r
&& !oldName.equalsIgnoreCase(username);\r
- if (GitBlit.self().supportsCredentialChanges()) {\r
+ if (GitBlit.self().supportsCredentialChanges(userModel)) {\r
if (!userModel.password.equals(confirmPassword.getObject())) {\r
error(getString("gb.passwordsDoNotMatch"));\r
return;\r
form.add(new SimpleAttributeModifier("autocomplete", "off"));\r
\r
// not all user services support manipulating username and password\r
- boolean editCredentials = GitBlit.self().supportsCredentialChanges();\r
+ boolean editCredentials = GitBlit.self().supportsCredentialChanges(userModel);\r
\r
// not all user services support manipulating display name\r
- boolean editDisplayName = GitBlit.self().supportsDisplayNameChanges();\r
+ boolean editDisplayName = GitBlit.self().supportsDisplayNameChanges(userModel);\r
\r
// not all user services support manipulating email address\r
- boolean editEmailAddress = GitBlit.self().supportsEmailAddressChanges();\r
+ boolean editEmailAddress = GitBlit.self().supportsEmailAddressChanges(userModel);\r
\r
// not all user services support manipulating team memberships\r
- boolean editTeams = GitBlit.self().supportsTeamMembershipChanges();\r
+ boolean editTeams = GitBlit.self().supportsTeamMembershipChanges(userModel);\r
\r
// field names reflective match UserModel fields\r
form.add(new TextField<String>("username").setEnabled(editCredentials));\r
\r
Fragment adminLinks = new Fragment("adminPanel", "adminLinks", this);\r
adminLinks.add(new BookmarkablePageLink<Void>("newTeam", EditTeamPage.class));\r
- add(adminLinks.setVisible(showAdmin && GitBlit.self().supportsTeamMembershipChanges()));\r
+ add(adminLinks.setVisible(showAdmin && GitBlit.self().supportsTeamMembershipChanges(null)));\r
\r
final List<TeamModel> teams = GitBlit.self().getAllTeams();\r
DataView<TeamModel> teamsView = new DataView<TeamModel>("teamRow",\r
</th>\r
<th class="hidden-phone hidden-tablet left"><wicket:message key="gb.displayName">[display name]</wicket:message></th>\r
<th class="hidden-phone hidden-tablet left"><wicket:message key="gb.emailAddress">[email address]</wicket:message></th>\r
- <th class="hidden-phone" style="width:120px;"><wicket:message key="gb.accessLevel">[access level]</wicket:message></th>\r
+ <th class="hidden-phone" style="width:140px;"><wicket:message key="gb.type">[type]</wicket:message></th>\r
<th class="hidden-phone" style="width:140px;"><wicket:message key="gb.teamMemberships">[team memberships]</wicket:message></th>\r
<th class="hidden-phone" style="width:100px;"><wicket:message key="gb.repositories">[repositories]</wicket:message></th>\r
<th style="width:80px;" class="right"></th>\r
<td class="left" ><span class="list" wicket:id="username">[username]</span></td>\r
<td class="hidden-phone hidden-tablet left" ><span class="list" wicket:id="displayName">[display name]</span></td>\r
<td class="hidden-phone hidden-tablet left" ><span class="list" wicket:id="emailAddress">[email address]</span></td>\r
- <td class="hidden-phone left" ><span class="list" wicket:id="accesslevel">[access level]</span></td>\r
+ <td class="hidden-phone left" ><span style="font-size: 0.8em;" wicket:id="accountType">[account type]</span></td>\r
<td class="hidden-phone left" ><span class="list" wicket:id="teams">[team memberships]</span></td>\r
<td class="hidden-phone left" ><span class="list" wicket:id="repositories">[repositories]</span></td>\r
<td class="rightAlign"><span wicket:id="userLinks"></span></td> \r
\r
Fragment adminLinks = new Fragment("adminPanel", "adminLinks", this);\r
adminLinks.add(new BookmarkablePageLink<Void>("newUser", EditUserPage.class)\r
- .setVisible(GitBlit.self().supportsCredentialChanges()));\r
+ .setVisible(GitBlit.self().supportsAddUser()));\r
add(adminLinks.setVisible(showAdmin));\r
\r
final List<UserModel> users = GitBlit.self().getAllUsers();\r
item.add(editLink);\r
}\r
\r
- item.add(new Label("accesslevel", entry.canAdmin() ? "administrator" : ""));\r
+ item.add(new Label("accountType", entry.accountType.name() + (entry.canAdmin() ? ", admin":"")));\r
item.add(new Label("teams", entry.teams.size() > 0 ? ("" + entry.teams.size()) : ""));\r
item.add(new Label("repositories",\r
entry.permissions.size() > 0 ? ("" + entry.permissions.size()) : ""));\r
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;
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));
+ }
}
package com.gitblit.tests;\r
\r
import static org.hamcrest.CoreMatchers.is;\r
+import static org.junit.Assert.assertEquals;\r
import static org.junit.Assert.assertNotNull;\r
-import static org.junit.Assert.assertNull;\r
import static org.junit.Assert.assertThat;\r
+import static org.junit.Assert.assertTrue;\r
\r
import java.util.HashMap;\r
\r
import com.gitblit.RedmineUserService;\r
import com.gitblit.models.UserModel;\r
import com.gitblit.tests.mock.MemorySettings;\r
+import com.gitblit.utils.StringUtils;\r
\r
public class RedmineUserServiceTest {\r
\r
redmineUserService.setup(new MemorySettings(new HashMap<String, Object>()));\r
redmineUserService.setTestingCurrentUserAsJson(JSON);\r
UserModel userModel = redmineUserService.authenticate("RedmineUserId", "RedmineAPIKey".toCharArray());\r
- assertThat(userModel.getName(), is("RedmineUserId"));\r
+ assertThat(userModel.getName(), is("redmineuserid"));\r
assertThat(userModel.getDisplayName(), is("baz foo"));\r
assertThat(userModel.emailAddress, is("baz@example.com"));\r
assertNotNull(userModel.cookie);\r
assertNotNull(userModel.cookie);\r
assertThat(userModel.canAdmin, is(false));\r
}\r
+ \r
+ @Test\r
+ public void testLocalAccount() {\r
+ RedmineUserService redmineUserService = new RedmineUserService();\r
+ redmineUserService.setup(new MemorySettings(new HashMap<String, Object>()));\r
+\r
+ UserModel localAccount = new UserModel("bruce");\r
+ localAccount.displayName = "Bruce Campbell";\r
+ localAccount.password = StringUtils.MD5_TYPE + StringUtils.getMD5("gimmesomesugar");\r
+ redmineUserService.deleteUser(localAccount.username);\r
+ assertTrue("Failed to add local account",\r
+ redmineUserService.updateUserModel(localAccount));\r
+ assertEquals("Accounts are not equal!", \r
+ localAccount, \r
+ redmineUserService.authenticate(localAccount.username, "gimmesomesugar".toCharArray()));\r
+ assertTrue("Failed to delete local account!",\r
+ redmineUserService.deleteUser(localAccount.username));\r
+ }\r
\r
}\r