summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJames Moger <james.moger@gitblit.com>2011-12-07 19:33:10 -0500
committerJames Moger <james.moger@gitblit.com>2011-12-07 19:33:10 -0500
commitfe24a0be919653d9e502f7729d9a804f2e28435d (patch)
treea63d2b07ce300843ae061d435c8891e8e5a930dc
parent7e8873a14ccc2cb25213489d7d7ba97f09673831 (diff)
downloadgitblit-fe24a0be919653d9e502f7729d9a804f2e28435d.tar.gz
gitblit-fe24a0be919653d9e502f7729d9a804f2e28435d.zip
Teams support.
Teams simplify the management of user-repository access permissions. Teams have a list of restricted repositories. Users are also added to teams and that grants them access to those repositories. Federation and RPC support are still in-progress.
-rw-r--r--docs/01_features.mkd1
-rw-r--r--docs/01_setup.mkd179
-rw-r--r--docs/04_releases.mkd1
-rw-r--r--resources/users_16x16.pngbin0 -> 918 bytes
-rw-r--r--src/com/gitblit/ConfigUserService.java307
-rw-r--r--src/com/gitblit/FileUserService.java323
-rw-r--r--src/com/gitblit/GitBlit.java81
-rw-r--r--src/com/gitblit/IUserService.java79
-rw-r--r--src/com/gitblit/models/TeamModel.java88
-rw-r--r--src/com/gitblit/models/UserModel.java25
-rw-r--r--src/com/gitblit/utils/DeepCopier.java135
-rw-r--r--src/com/gitblit/wicket/GitBlitWebApp.properties8
-rw-r--r--src/com/gitblit/wicket/WicketUtils.java8
-rw-r--r--src/com/gitblit/wicket/pages/EditRepositoryPage.html3
-rw-r--r--src/com/gitblit/wicket/pages/EditRepositoryPage.java19
-rw-r--r--src/com/gitblit/wicket/pages/EditTeamPage.html24
-rw-r--r--src/com/gitblit/wicket/pages/EditTeamPage.java169
-rw-r--r--src/com/gitblit/wicket/pages/EditUserPage.html5
-rw-r--r--src/com/gitblit/wicket/pages/EditUserPage.java26
-rw-r--r--src/com/gitblit/wicket/pages/UsersPage.html2
-rw-r--r--src/com/gitblit/wicket/pages/UsersPage.java3
-rw-r--r--src/com/gitblit/wicket/panels/TeamsPanel.html44
-rw-r--r--src/com/gitblit/wicket/panels/TeamsPanel.java89
-rw-r--r--src/com/gitblit/wicket/panels/UsersPanel.html2
-rw-r--r--tests/com/gitblit/tests/UserServiceTest.java107
25 files changed, 1547 insertions, 181 deletions
diff --git a/docs/01_features.mkd b/docs/01_features.mkd
index b43ea46f..4172f4e1 100644
--- a/docs/01_features.mkd
+++ b/docs/01_features.mkd
@@ -13,6 +13,7 @@
- Gitweb inspired web UI
- Administrators may create, edit, rename, or delete repositories through the web UI or RPC interface
- Administrators may create, edit, rename, or delete users through the web UI or RPC interface
+- Administrators may create, edit, rename, or delete teams through the web UI or RPC interface
- Repository Owners may edit repositories through the web UI
- Git-notes display support
- Branch metrics (uses Google Charts)
diff --git a/docs/01_setup.mkd b/docs/01_setup.mkd
index 468421d3..3256d1b3 100644
--- a/docs/01_setup.mkd
+++ b/docs/01_setup.mkd
@@ -157,8 +157,13 @@ All repositories created with Gitblit are *bare* and will automatically have *.g
#### Repository Owner
The *Repository Owner* has the special permission of being able to edit a repository through the web UI. The Repository Owner is not permitted to rename the repository, delete the repository, or reassign ownership to another user.
-### Administering Users (Gitblit v0.8.0+)
-All users are stored in the `users.conf` file or in the file you specified in `gitblit.properties`.<br/>
+### Teams
+
+Since v0.8.0, Gitblit supports *teams* for the original `users.properties` user service and the current default user service `users.conf`. Teams have assigned users and assigned repositories. A user can be a member of multiple teams and a repository may belong to multiple teams. This allows the administrator to quickly add a user to a team without having to keep track of all the appropriate repositories.
+
+### Administering Users (users.conf, Gitblit v0.8.0+)
+All users are stored in the `users.conf` file or in the file you specified in `gitblit.properties`. Your file extension must be *.conf* in order to use this user service.
+
The `users.conf` file uses a Git-style configuration format:
[user "admin"]
@@ -167,14 +172,35 @@ The `users.conf` file uses a Git-style configuration format:
role = "#notfederated"
repository = repo1.git
repository = repo2.git
+
+ [user "hannibal"]
+ password = bossman
+
+ [user "faceman"]
+ password = vanity
+
+ [user "murdock"]
+ password = crazy
+
+ [user "babaracus"]
+ password = grrrr
+
+ [team "ateam"]
+ user = hannibal
+ user = faceman
+ user = murdock
+ user = babaracus
+ repository = topsecret.git
The `users.conf` file allows flexibility for adding new fields to a UserModel object that the original `users.properties` file does not afford without imposing the complexity of relying on an embedded SQL database.
-### Administering Users (Gitblit v0.5.0 - v0.7.0)
-All users are stored in the `users.properties` file or in the file you specified in `gitblit.properties`.<br/>
+### Administering Users (users.properties, Gitblit v0.5.0 - v0.7.0)
+All users are stored in the `users.properties` file or in the file you specified in `gitblit.properties`. Your file extension must be *.properties* in order to use this user service.
+
The format of `users.properties` follows Jetty's convention for HashRealms:
username,password,role1,role2,role3...
+ @teamname,!username1,!username2,!username3,repository1,repository2,repository3...
#### Usernames
Usernames must be unique and are case-insensitive.
@@ -191,149 +217,8 @@ Instead of maintaining a `users.conf` or `users.properties` file, you may want t
You may use your own custom *com.gitblit.IUserService* implementation by specifying its fully qualified classname in the *realm.userService* setting.
-Your user service class must be on Gitblit's classpath and must have a public default constructor.
-
-%BEGINCODE%
-public interface IUserService {
-
- /**
- * Setup the user service.
- *
- * @param settings
- * @since 0.7.0
- */
- @Override
- public void setup(IStoredSettings settings) {
- }
-
- /**
- * Does the user service support cookie authentication?
- *
- * @return true or false
- */
- boolean supportsCookies();
-
- /**
- * Returns the cookie value for the specified user.
- *
- * @param model
- * @return cookie value
- */
- char[] getCookie(UserModel model);
-
- /**
- * Authenticate a user based on their cookie.
- *
- * @param cookie
- * @return a user object or null
- */
- UserModel authenticate(char[] cookie);
-
- /**
- * Authenticate a user based on a username and password.
- *
- * @param username
- * @param password
- * @return a user object or null
- */
- UserModel authenticate(String username, char[] password);
-
- /**
- * Retrieve the user object for the specified username.
- *
- * @param username
- * @return a user object or null
- */
- UserModel getUserModel(String username);
-
- /**
- * Updates/writes a complete user object.
- *
- * @param model
- * @return true if update is successful
- */
- boolean updateUserModel(UserModel model);
-
- /**
- * 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
- */
- boolean updateUserModel(String username, UserModel model);
-
- /**
- * Deletes the user object from the user service.
- *
- * @param model
- * @return true if successful
- */
- boolean deleteUserModel(UserModel model);
-
- /**
- * Delete the user object with the specified username
- *
- * @param username
- * @return true if successful
- */
- boolean deleteUser(String username);
-
- /**
- * Returns the list of all users available to the login service.
- *
- * @return list of all usernames
- */
- List<String> getAllUsernames();
-
- /**
- * 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
- */
- List<String> getUsernamesForRepositoryRole(String role);
-
- /**
- * Sets the list of all uses who are allowed to bypass the access
- * restriction placed on the specified repository.
- *
- * @param role
- * the repository name
- * @param usernames
- * @return true if successful
- */
- boolean setUsernamesForRepositoryRole(String role, List<String> usernames);
-
- /**
- * Renames a repository role.
- *
- * @param oldRole
- * @param newRole
- * @return true if successful
- */
- boolean renameRepositoryRole(String oldRole, String newRole);
-
- /**
- * Removes a repository role from all users.
- *
- * @param role
- * @return true if successful
- */
- boolean deleteRepositoryRole(String role);
-
- /**
- * @See java.lang.Object.toString();
- * @return string representation of the login service
- */
- String toString();
-}
-%ENDCODE%
+Your user service class must be on Gitblit's classpath and must have a public default constructor.
+Please see the following interface definition [com.gitblit.IUserService](https://github.com/gitblit/gitblit/blob/master/src/com/gitblit/IUserService.java).
## Client Setup and Configuration
### Https with Self-Signed Certificates
diff --git a/docs/04_releases.mkd b/docs/04_releases.mkd
index 7a0cb094..343f794c 100644
--- a/docs/04_releases.mkd
+++ b/docs/04_releases.mkd
@@ -6,6 +6,7 @@
- added: new default user service implementation: com.gitblit.ConfigUserService (users.conf)
This user service implementation allows for serialization and deserialization of more sophisticated Gitblit User objects and will open the door for more advanced Gitblit features. For upgrading installations, a `users.conf` file will automatically be created for you from your existing `users.properties` file on your first launch of Gitblit. You will have to manually set *realm.userService=users.conf* to switch to the new user service. The original `users.properties` file and it's corresponding implementation are deprecated.
**New:** *realm.userService = users.conf*
+- added: Teams for specifying user-repository access
- added: Gitblit Express bundle to get started running Gitblit on RedHat's OpenShift cloud
- added: optional Gravatar integration
**New:** *web.allowGravatar = true*
diff --git a/resources/users_16x16.png b/resources/users_16x16.png
new file mode 100644
index 00000000..247af645
--- /dev/null
+++ b/resources/users_16x16.png
Binary files differ
diff --git a/src/com/gitblit/ConfigUserService.java b/src/com/gitblit/ConfigUserService.java
index 28a16c50..a0a38e6a 100644
--- a/src/com/gitblit/ConfigUserService.java
+++ b/src/com/gitblit/ConfigUserService.java
@@ -32,7 +32,9 @@ import org.eclipse.jgit.util.FS;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import com.gitblit.models.TeamModel;
import com.gitblit.models.UserModel;
+import com.gitblit.utils.DeepCopier;
import com.gitblit.utils.StringUtils;
/**
@@ -51,6 +53,16 @@ import com.gitblit.utils.StringUtils;
*/
public class ConfigUserService implements IUserService {
+ private static final String TEAM = "team";
+
+ private static final String USER = "user";
+
+ private static final String PASSWORD = "password";
+
+ private static final String REPOSITORY = "repository";
+
+ private static final String ROLE = "role";
+
private final File realmFile;
private final Logger logger = LoggerFactory.getLogger(ConfigUserService.class);
@@ -59,13 +71,7 @@ public class ConfigUserService implements IUserService {
private final Map<String, UserModel> cookies = new ConcurrentHashMap<String, UserModel>();
- private final String userSection = "user";
-
- private final String passwordField = "password";
-
- private final String repositoryField = "repository";
-
- private final String roleField = "role";
+ private final Map<String, TeamModel> teams = new ConcurrentHashMap<String, TeamModel>();
private volatile long lastModified;
@@ -77,7 +83,7 @@ public class ConfigUserService implements IUserService {
* Setup the user service.
*
* @param settings
- * @since 0.6.1
+ * @since 0.7.0
*/
@Override
public void setup(IStoredSettings settings) {
@@ -172,6 +178,11 @@ public class ConfigUserService implements IUserService {
public UserModel getUserModel(String username) {
read();
UserModel model = users.get(username.toLowerCase());
+ if (model != null) {
+ // clone the model, otherwise all changes to this object are
+ // live and unpersisted
+ model = DeepCopier.copy(model);
+ }
return model;
}
@@ -200,8 +211,34 @@ public class ConfigUserService implements IUserService {
public boolean updateUserModel(String username, UserModel model) {
try {
read();
- users.remove(username.toLowerCase());
+ UserModel oldUser = users.remove(username.toLowerCase());
users.put(model.username.toLowerCase(), model);
+ // null check on "final" teams because JSON-sourced UserModel
+ // can have a null teams object
+ if (model.teams != null) {
+ for (TeamModel team : model.teams) {
+ TeamModel t = teams.get(team.name.toLowerCase());
+ if (t == null) {
+ // new team
+ team.addUser(username);
+ teams.put(team.name.toLowerCase(), team);
+ } else {
+ // do not clobber existing team definition
+ // maybe because this is a federated user
+ t.removeUser(username);
+ t.addUser(model.username);
+ }
+ }
+
+ // check for implicit team removal
+ if (oldUser != null) {
+ for (TeamModel team : oldUser.teams) {
+ if (!model.isTeamMember(team.name)) {
+ team.removeUser(username);
+ }
+ }
+ }
+ }
write();
return true;
} catch (Throwable t) {
@@ -233,7 +270,19 @@ public class ConfigUserService implements IUserService {
try {
// Read realm file
read();
- users.remove(username.toLowerCase());
+ UserModel model = users.remove(username.toLowerCase());
+ // remove user from team
+ for (TeamModel team : model.teams) {
+ TeamModel t = teams.get(team.name);
+ if (t == null) {
+ // new team
+ team.removeUser(username);
+ teams.put(team.name.toLowerCase(), team);
+ } else {
+ // existing team
+ t.removeUser(username);
+ }
+ }
write();
return true;
} catch (Throwable t) {
@@ -243,6 +292,172 @@ public class ConfigUserService implements IUserService {
}
/**
+ * 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() {
+ read();
+ List<String> list = new ArrayList<String>(teams.keySet());
+ return list;
+ }
+
+ /**
+ * 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
+ */
+ @Override
+ public List<String> getTeamnamesForRepositoryRole(String role) {
+ List<String> list = new ArrayList<String>();
+ try {
+ read();
+ for (Map.Entry<String, TeamModel> entry : teams.entrySet()) {
+ TeamModel model = entry.getValue();
+ if (model.hasRepository(role)) {
+ list.add(model.name);
+ }
+ }
+ } catch (Throwable t) {
+ logger.error(MessageFormat.format("Failed to get teamnames for role {0}!", role), t);
+ }
+ return list;
+ }
+
+ /**
+ * Sets the list of all teams who are allowed to bypass the access
+ * restriction placed on the specified repository.
+ *
+ * @param role
+ * the repository name
+ * @param teamnames
+ * @return true if successful
+ */
+ @Override
+ public boolean setTeamnamesForRepositoryRole(String role, List<String> teamnames) {
+ try {
+ Set<String> specifiedTeams = new HashSet<String>();
+ for (String teamname : teamnames) {
+ specifiedTeams.add(teamname.toLowerCase());
+ }
+
+ read();
+
+ // identify teams which require add or remove role
+ for (TeamModel team : teams.values()) {
+ // team has role, check against revised team list
+ if (specifiedTeams.contains(team.name.toLowerCase())) {
+ team.addRepository(role);
+ } else {
+ // remove role from team
+ team.removeRepository(role);
+ }
+ }
+
+ // persist changes
+ write();
+ return true;
+ } catch (Throwable t) {
+ logger.error(MessageFormat.format("Failed to set teams for role {0}!", role), t);
+ }
+ return false;
+ }
+
+ /**
+ * 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) {
+ read();
+ TeamModel model = teams.get(teamname.toLowerCase());
+ if (model != null) {
+ // clone the model, otherwise all changes to this object are
+ // live and unpersisted
+ model = DeepCopier.copy(model);
+ }
+ return model;
+ }
+
+ /**
+ * 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 updateTeamModel(model.name, model);
+ }
+
+ /**
+ * 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) {
+ try {
+ read();
+ teams.remove(teamname.toLowerCase());
+ teams.put(model.name.toLowerCase(), model);
+ write();
+ return true;
+ } catch (Throwable t) {
+ logger.error(MessageFormat.format("Failed to update team model {0}!", model.name), t);
+ }
+ return false;
+ }
+
+ /**
+ * 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 deleteTeam(model.name);
+ }
+
+ /**
+ * Delete the team object with the specified teamname
+ *
+ * @param teamname
+ * @return true if successful
+ * @since 0.8.0
+ */
+ @Override
+ public boolean deleteTeam(String teamname) {
+ try {
+ // Read realm file
+ read();
+ teams.remove(teamname.toLowerCase());
+ write();
+ return true;
+ } catch (Throwable t) {
+ logger.error(MessageFormat.format("Failed to delete team {0}!", teamname), t);
+ }
+ return false;
+ }
+
+ /**
* Returns the list of all users available to the login service.
*
* @return list of all usernames
@@ -337,6 +552,13 @@ public class ConfigUserService implements IUserService {
}
}
+ // identify teams which require role rename
+ for (TeamModel model : teams.values()) {
+ if (model.hasRepository(oldRole)) {
+ model.removeRepository(oldRole);
+ model.addRepository(newRole);
+ }
+ }
// persist changes
write();
return true;
@@ -363,6 +585,11 @@ public class ConfigUserService implements IUserService {
user.removeRepository(role);
}
+ // identify teams which require role rename
+ for (TeamModel team : teams.values()) {
+ team.removeRepository(role);
+ }
+
// persist changes
write();
return true;
@@ -383,8 +610,10 @@ public class ConfigUserService implements IUserService {
File realmFileCopy = new File(realmFile.getAbsolutePath() + ".tmp");
StoredConfig config = new FileBasedConfig(realmFileCopy, FS.detect());
+
+ // write users
for (UserModel model : users.values()) {
- config.setString(userSection, model.username, passwordField, model.password);
+ config.setString(USER, model.username, PASSWORD, model.password);
// user roles
List<String> roles = new ArrayList<String>();
@@ -394,12 +623,33 @@ public class ConfigUserService implements IUserService {
if (model.excludeFromFederation) {
roles.add(Constants.NOT_FEDERATED_ROLE);
}
- config.setStringList(userSection, model.username, roleField, roles);
+ config.setStringList(USER, model.username, ROLE, roles);
// repository memberships
- config.setStringList(userSection, model.username, repositoryField,
- new ArrayList<String>(model.repositories));
+ // null check on "final" repositories because JSON-sourced UserModel
+ // can have a null repositories object
+ if (model.repositories != null) {
+ config.setStringList(USER, model.username, REPOSITORY, new ArrayList<String>(
+ model.repositories));
+ }
+ }
+
+ // write teams
+ for (TeamModel model : teams.values()) {
+ // null check on "final" repositories because JSON-sourced TeamModel
+ // can have a null repositories object
+ if (model.repositories != null) {
+ config.setStringList(TEAM, model.name, REPOSITORY, new ArrayList<String>(
+ model.repositories));
+ }
+
+ // null check on "final" users because JSON-sourced TeamModel
+ // can have a null users object
+ if (model.users != null) {
+ config.setStringList(TEAM, model.name, USER, new ArrayList<String>(model.users));
+ }
}
+
config.save();
// If the write is successful, delete the current file and rename
@@ -429,23 +679,25 @@ public class ConfigUserService implements IUserService {
lastModified = realmFile.lastModified();
users.clear();
cookies.clear();
+ teams.clear();
+
try {
StoredConfig config = new FileBasedConfig(realmFile, FS.detect());
config.load();
- Set<String> usernames = config.getSubsections(userSection);
+ Set<String> usernames = config.getSubsections(USER);
for (String username : usernames) {
UserModel user = new UserModel(username);
- user.password = config.getString(userSection, username, passwordField);
+ user.password = config.getString(USER, username, PASSWORD);
// user roles
Set<String> roles = new HashSet<String>(Arrays.asList(config.getStringList(
- userSection, username, roleField)));
+ USER, username, ROLE)));
user.canAdmin = roles.contains(Constants.ADMIN_ROLE);
user.excludeFromFederation = roles.contains(Constants.NOT_FEDERATED_ROLE);
// repository memberships
Set<String> repositories = new HashSet<String>(Arrays.asList(config
- .getStringList(userSection, username, repositoryField)));
+ .getStringList(USER, username, REPOSITORY)));
for (String repository : repositories) {
user.addRepository(repository);
}
@@ -454,6 +706,25 @@ public class ConfigUserService implements IUserService {
users.put(username, user);
cookies.put(StringUtils.getSHA1(username + user.password), user);
}
+
+ // load the teams
+ Set<String> teamnames = config.getSubsections(TEAM);
+ for (String teamname : teamnames) {
+ TeamModel team = new TeamModel(teamname);
+ team.addRepositories(Arrays.asList(config.getStringList(TEAM, teamname,
+ REPOSITORY)));
+ team.addUsers(Arrays.asList(config.getStringList(TEAM, teamname, USER)));
+
+ teams.put(team.name.toLowerCase(), team);
+
+ // set the teams on the users
+ for (String user : team.users) {
+ UserModel model = users.get(user);
+ if (model != null) {
+ model.teams.add(team);
+ }
+ }
+ }
} catch (Exception e) {
logger.error(MessageFormat.format("Failed to read {0}", realmFile), e);
}
diff --git a/src/com/gitblit/FileUserService.java b/src/com/gitblit/FileUserService.java
index a98e4175..880ca7b8 100644
--- a/src/com/gitblit/FileUserService.java
+++ b/src/com/gitblit/FileUserService.java
@@ -30,7 +30,9 @@ import java.util.concurrent.ConcurrentHashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import com.gitblit.models.TeamModel;
import com.gitblit.models.UserModel;
+import com.gitblit.utils.DeepCopier;
import com.gitblit.utils.StringUtils;
/**
@@ -53,6 +55,8 @@ public class FileUserService extends FileSettings implements IUserService {
private final Map<String, String> cookies = new ConcurrentHashMap<String, String>();
+ private final Map<String, TeamModel> teams = new ConcurrentHashMap<String, TeamModel>();
+
public FileUserService(File realmFile) {
super(realmFile.getAbsolutePath());
}
@@ -61,7 +65,7 @@ public class FileUserService extends FileSettings implements IUserService {
* Setup the user service.
*
* @param settings
- * @since 0.6.1
+ * @since 0.7.0
*/
@Override
public void setup(IStoredSettings settings) {
@@ -181,6 +185,12 @@ public class FileUserService extends FileSettings implements IUserService {
model.addRepository(role);
}
}
+ // set the teams for the user
+ for (TeamModel team : teams.values()) {
+ if (team.hasUser(username)) {
+ model.teams.add(DeepCopier.copy(team));
+ }
+ }
return model;
}
@@ -209,6 +219,7 @@ public class FileUserService extends FileSettings implements IUserService {
public boolean updateUserModel(String username, UserModel model) {
try {
Properties allUsers = read();
+ UserModel oldUser = getUserModel(username);
ArrayList<String> roles = new ArrayList<String>(model.repositories);
// Permissions
@@ -231,6 +242,32 @@ public class FileUserService extends FileSettings implements IUserService {
allUsers.remove(username);
allUsers.put(model.username, sb.toString());
+ // null check on "final" teams because JSON-sourced UserModel
+ // can have a null teams object
+ if (model.teams != null) {
+ // update team cache
+ for (TeamModel team : model.teams) {
+ TeamModel t = getTeamModel(team.name);
+ if (t == null) {
+ // new team
+ t = team;
+ }
+ t.removeUser(username);
+ t.addUser(model.username);
+ updateTeamCache(allUsers, t.name, t);
+ }
+
+ // check for implicit team removal
+ if (oldUser != null) {
+ for (TeamModel team : oldUser.teams) {
+ if (!model.isTeamMember(team.name)) {
+ team.removeUser(username);
+ updateTeamCache(allUsers, team.name, team);
+ }
+ }
+ }
+ }
+
write(allUsers);
return true;
} catch (Throwable t) {
@@ -262,7 +299,17 @@ public class FileUserService extends FileSettings implements IUserService {
try {
// Read realm file
Properties allUsers = read();
+ UserModel user = getUserModel(username);
allUsers.remove(username);
+ for (TeamModel team : user.teams) {
+ TeamModel t = getTeamModel(team.name);
+ if (t == null) {
+ // new team
+ t = team;
+ }
+ t.removeUser(username);
+ updateTeamCache(allUsers, t.name, t);
+ }
write(allUsers);
return true;
} catch (Throwable t) {
@@ -279,7 +326,14 @@ public class FileUserService extends FileSettings implements IUserService {
@Override
public List<String> getAllUsernames() {
Properties allUsers = read();
- List<String> list = new ArrayList<String>(allUsers.stringPropertyNames());
+ List<String> list = new ArrayList<String>();
+ for (String user : allUsers.stringPropertyNames()) {
+ if (user.charAt(0) == '@') {
+ // skip team user definitions
+ continue;
+ }
+ list.add(user);
+ }
return list;
}
@@ -297,6 +351,9 @@ public class FileUserService extends FileSettings implements IUserService {
try {
Properties allUsers = read();
for (String username : allUsers.stringPropertyNames()) {
+ if (username.charAt(0) == '@') {
+ continue;
+ }
String value = allUsers.getProperty(username);
String[] values = value.split(",");
// skip first value (password)
@@ -315,7 +372,7 @@ public class FileUserService extends FileSettings implements IUserService {
}
/**
- * Sets the list of all uses who are allowed to bypass the access
+ * Sets the list of all users who are allowed to bypass the access
* restriction placed on the specified repository.
*
* @param role
@@ -426,7 +483,7 @@ public class FileUserService extends FileSettings implements IUserService {
sb.append(',');
sb.append(newRole);
sb.append(',');
-
+
// skip first value (password)
for (int i = 1; i < values.length; i++) {
String value = values[i];
@@ -520,7 +577,7 @@ public class FileUserService extends FileSettings implements IUserService {
FileWriter writer = new FileWriter(realmFileCopy);
properties
.store(writer,
- "# Gitblit realm file format: username=password,\\#permission,repository1,repository2...");
+ " Gitblit realm file format:\n username=password,\\#permission,repository1,repository2...\n @teamname=!username1,!username2,!username3,repository1,repository2...");
writer.close();
// If the write is successful, delete the current file and rename
// the temporary copy to the original filename.
@@ -551,11 +608,31 @@ public class FileUserService extends FileSettings implements IUserService {
if (lastRead != lastModified()) {
// reload hash cache
cookies.clear();
+ teams.clear();
+
for (String username : allUsers.stringPropertyNames()) {
String value = allUsers.getProperty(username);
String[] roles = value.split(",");
- String password = roles[0];
- cookies.put(StringUtils.getSHA1(username + password), username);
+ if (username.charAt(0) == '@') {
+ // team definition
+ TeamModel team = new TeamModel(username.substring(1));
+ List<String> repositories = new ArrayList<String>();
+ List<String> users = new ArrayList<String>();
+ for (String role : roles) {
+ if (role.charAt(0) == '!') {
+ users.add(role.substring(1));
+ } else {
+ repositories.add(role);
+ }
+ }
+ team.addRepositories(repositories);
+ team.addUsers(users);
+ teams.put(team.name.toLowerCase(), team);
+ } else {
+ // user definition
+ String password = roles[0];
+ cookies.put(StringUtils.getSHA1(username + password), username);
+ }
}
}
return allUsers;
@@ -565,4 +642,236 @@ public class FileUserService extends FileSettings implements IUserService {
public String toString() {
return getClass().getSimpleName() + "(" + propertiesFile.getAbsolutePath() + ")";
}
+
+ /**
+ * 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() {
+ List<String> list = new ArrayList<String>(teams.keySet());
+ return list;
+ }
+
+ /**
+ * 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 teamnames that can bypass the access restriction
+ */
+ @Override
+ public List<String> getTeamnamesForRepositoryRole(String role) {
+ List<String> list = new ArrayList<String>();
+ try {
+ Properties allUsers = read();
+ for (String team : allUsers.stringPropertyNames()) {
+ if (team.charAt(0) != '@') {
+ // skip users
+ continue;
+ }
+ String value = allUsers.getProperty(team);
+ String[] values = value.split(",");
+ for (int i = 0; i < values.length; i++) {
+ String r = values[i];
+ if (r.equalsIgnoreCase(role)) {
+ // strip leading @
+ list.add(team.substring(1));
+ break;
+ }
+ }
+ }
+ } catch (Throwable t) {
+ logger.error(MessageFormat.format("Failed to get teamnames for role {0}!", role), t);
+ }
+ return list;
+ }
+
+ /**
+ * Sets the list of all teams who are allowed to bypass the access
+ * restriction placed on the specified repository.
+ *
+ * @param role
+ * the repository name
+ * @param teamnames
+ * @return true if successful
+ */
+ @Override
+ public boolean setTeamnamesForRepositoryRole(String role, List<String> teamnames) {
+ try {
+ Set<String> specifiedTeams = new HashSet<String>(teamnames);
+ Set<String> needsAddRole = new HashSet<String>(specifiedTeams);
+ Set<String> needsRemoveRole = new HashSet<String>();
+
+ // identify teams which require add and remove role
+ Properties allUsers = read();
+ for (String team : allUsers.stringPropertyNames()) {
+ if (team.charAt(0) != '@') {
+ // skip users
+ continue;
+ }
+ String name = team.substring(1);
+ String value = allUsers.getProperty(team);
+ String[] values = value.split(",");
+ for (int i = 0; i < values.length; i++) {
+ String r = values[i];
+ if (r.equalsIgnoreCase(role)) {
+ // team has role, check against revised team list
+ if (specifiedTeams.contains(name)) {
+ needsAddRole.remove(name);
+ } else {
+ // remove role from team
+ needsRemoveRole.add(name);
+ }
+ break;
+ }
+ }
+ }
+
+ // add roles to teams
+ for (String name : needsAddRole) {
+ String team = "@" + name;
+ String teamValues = allUsers.getProperty(team);
+ teamValues += "," + role;
+ allUsers.put(team, teamValues);
+ }
+
+ // remove role from team
+ for (String name : needsRemoveRole) {
+ String team = "@" + name;
+ String[] values = allUsers.getProperty(team).split(",");
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < values.length; i++) {
+ String value = values[i];
+ if (!value.equalsIgnoreCase(role)) {
+ sb.append(value);
+ sb.append(',');
+ }
+ }
+ sb.setLength(sb.length() - 1);
+
+ // update properties
+ allUsers.put(team, sb.toString());
+ }
+
+ // persist changes
+ write(allUsers);
+ return true;
+ } catch (Throwable t) {
+ logger.error(MessageFormat.format("Failed to set teamnames for role {0}!", role), t);
+ }
+ return false;
+ }
+
+ /**
+ * 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) {
+ read();
+ TeamModel team = teams.get(teamname.toLowerCase());
+ if (team != null) {
+ // clone the model, otherwise all changes to this object are
+ // live and unpersisted
+ team = DeepCopier.copy(team);
+ }
+ return team;
+ }
+
+ /**
+ * 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 updateTeamModel(model.name, model);
+ }
+
+ /**
+ * 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) {
+ try {
+ Properties allUsers = read();
+ updateTeamCache(allUsers, teamname, model);
+ write(allUsers);
+ return true;
+ } catch (Throwable t) {
+ logger.error(MessageFormat.format("Failed to update team model {0}!", model.name), t);
+ }
+ return false;
+ }
+
+ private void updateTeamCache(Properties allUsers, String teamname, TeamModel model) {
+ StringBuilder sb = new StringBuilder();
+ for (String repository : model.repositories) {
+ sb.append(repository);
+ sb.append(',');
+ }
+ for (String user : model.users) {
+ sb.append('!');
+ sb.append(user);
+ sb.append(',');
+ }
+ // trim trailing comma
+ sb.setLength(sb.length() - 1);
+ allUsers.remove("@" + teamname);
+ allUsers.put("@" + model.name, sb.toString());
+
+ // update team cache
+ teams.remove(teamname.toLowerCase());
+ teams.put(model.name.toLowerCase(), 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 deleteTeam(model.name);
+ }
+
+ /**
+ * Delete the team object with the specified teamname
+ *
+ * @param teamname
+ * @return true if successful
+ * @since 0.8.0
+ */
+ @Override
+ public boolean deleteTeam(String teamname) {
+ Properties allUsers = read();
+ teams.remove(teamname.toLowerCase());
+ allUsers.remove("@" + teamname);
+ try {
+ write(allUsers);
+ return true;
+ } catch (Throwable t) {
+ logger.error(MessageFormat.format("Failed to delete team {0}!", teamname), t);
+ }
+ return false;
+ }
}
diff --git a/src/com/gitblit/GitBlit.java b/src/com/gitblit/GitBlit.java
index 60a96e66..13dc3fa5 100644
--- a/src/com/gitblit/GitBlit.java
+++ b/src/com/gitblit/GitBlit.java
@@ -69,6 +69,7 @@ import com.gitblit.models.RepositoryModel;
import com.gitblit.models.ServerSettings;
import com.gitblit.models.ServerStatus;
import com.gitblit.models.SettingModel;
+import com.gitblit.models.TeamModel;
import com.gitblit.models.UserModel;
import com.gitblit.utils.ByteFormat;
import com.gitblit.utils.FederationUtils;
@@ -511,6 +512,83 @@ public class GitBlit implements ServletContextListener {
}
/**
+ * 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());
+ Collections.sort(teams);
+ return teams;
+ }
+
+ /**
+ * Returns the TeamModel object for the specified name.
+ *
+ * @param teamname
+ * @return a TeamModel object or null
+ */
+ public TeamModel getTeamModel(String teamname) {
+ return userService.getTeamModel(teamname);
+ }
+
+ /**
+ * Returns the list of all teams who are allowed to bypass the access
+ * restriction placed on the specified repository.
+ *
+ * @see IUserService.getTeamnamesForRepositoryRole(String)
+ * @param repository
+ * @return list of all teamnames that can bypass the access restriction
+ */
+ public List<String> getRepositoryTeams(RepositoryModel repository) {
+ return userService.getTeamnamesForRepositoryRole(repository.name);
+ }
+
+ /**
+ * Sets the list of all uses who are allowed to bypass the access
+ * restriction placed on the specified repository.
+ *
+ * @see IUserService.setTeamnamesForRepositoryRole(String, List<String>)
+ * @param repository
+ * @param teamnames
+ * @return true if successful
+ */
+ public boolean setRepositoryTeams(RepositoryModel repository, List<String> repositoryTeams) {
+ return userService.setTeamnamesForRepositoryRole(repository.name, repositoryTeams);
+ }
+ /**
+ * Updates the TeamModel object for the specified name.
+ *
+ * @param teamname
+ * @param team
+ * @param isCreate
+ */
+ public void updateTeamModel(String teamname, TeamModel team, boolean isCreate) throws GitBlitException {
+ if (!teamname.equalsIgnoreCase(team.name)) {
+ if (userService.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)) {
+ 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
+ */
+ public boolean deleteTeam(String teamname) {
+ return userService.deleteTeam(teamname);
+ }
+
+ /**
* Clears all the cached data for the specified repository.
*
* @param repositoryName
@@ -1115,6 +1193,7 @@ public class GitBlit implements ServletContextListener {
case PULL_REPOSITORIES:
return token.equals(all) || token.equals(unr) || token.equals(jur);
case PULL_USERS:
+ case PULL_TEAMS:
return token.equals(all) || token.equals(unr);
case PULL_SETTINGS:
return token.equals(all);
@@ -1473,7 +1552,7 @@ public class GitBlit implements ServletContextListener {
configService.updateUserModel(userModel);
}
}
-
+
// issue suggestion about switching to users.conf
logger.warn("Please consider using \"users.conf\" instead of the deprecated \"users.properties\" file");
} else if (realmFile.getName().toLowerCase().endsWith(".conf")) {
diff --git a/src/com/gitblit/IUserService.java b/src/com/gitblit/IUserService.java
index e143c794..98dbf0d6 100644
--- a/src/com/gitblit/IUserService.java
+++ b/src/com/gitblit/IUserService.java
@@ -17,6 +17,7 @@ package com.gitblit;
import java.util.List;
+import com.gitblit.models.TeamModel;
import com.gitblit.models.UserModel;
/**
@@ -122,6 +123,84 @@ public interface IUserService {
List<String> getAllUsernames();
/**
+ * Returns the list of all teams available to the login service.
+ *
+ * @return list of all teams
+ * @since 0.8.0
+ */
+ List<String> getAllTeamNames();
+
+ /**
+ * 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
+ */
+ List<String> getTeamnamesForRepositoryRole(String role);
+
+ /**
+ * Sets the list of all teams who are allowed to bypass the access
+ * restriction placed on the specified repository.
+ *
+ * @param role
+ * the repository name
+ * @param teamnames
+ * @return true if successful
+ */
+ boolean setTeamnamesForRepositoryRole(String role, List<String> teamnames);
+
+ /**
+ * Retrieve the team object for the specified team name.
+ *
+ * @param teamname
+ * @return a team object or null
+ * @since 0.8.0
+ */
+ TeamModel getTeamModel(String teamname);
+
+ /**
+ * Updates/writes a complete team object.
+ *
+ * @param model
+ * @return true if update is successful
+ * @since 0.8.0
+ */
+ boolean updateTeamModel(TeamModel model);
+
+ /**
+ * 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
+ */
+ boolean updateTeamModel(String teamname, TeamModel model);
+
+ /**
+ * Deletes the team object from the user service.
+ *
+ * @param model
+ * @return true if successful
+ * @since 0.8.0
+ */
+ boolean deleteTeamModel(TeamModel model);
+
+ /**
+ * Delete the team object with the specified teamname
+ *
+ * @param teamname
+ * @return true if successful
+ * @since 0.8.0
+ */
+ boolean deleteTeam(String teamname);
+
+ /**
* Returns the list of all users who are allowed to bypass the access
* restriction placed on the specified repository.
*
diff --git a/src/com/gitblit/models/TeamModel.java b/src/com/gitblit/models/TeamModel.java
new file mode 100644
index 00000000..195b9d5c
--- /dev/null
+++ b/src/com/gitblit/models/TeamModel.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2011 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.models;
+
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * TeamModel is a serializable model class that represents a group of users and
+ * a list of accessible repositories.
+ *
+ * @author James Moger
+ *
+ */
+public class TeamModel implements Serializable, Comparable<TeamModel> {
+
+ private static final long serialVersionUID = 1L;
+
+ // field names are reflectively mapped in EditTeam page
+ public String name;
+ public final Set<String> users = new HashSet<String>();
+ public final Set<String> repositories = new HashSet<String>();
+
+ public TeamModel(String name) {
+ this.name = name;
+ }
+
+ public boolean hasRepository(String name) {
+ return repositories.contains(name.toLowerCase());
+ }
+
+ public void addRepository(String name) {
+ repositories.add(name.toLowerCase());
+ }
+
+ public void addRepositories(Collection<String> names) {
+ for (String name:names) {
+ repositories.add(name.toLowerCase());
+ }
+ }
+
+ public void removeRepository(String name) {
+ repositories.remove(name.toLowerCase());
+ }
+
+ public boolean hasUser(String name) {
+ return users.contains(name.toLowerCase());
+ }
+
+ public void addUser(String name) {
+ users.add(name.toLowerCase());
+ }
+
+ public void addUsers(Collection<String> names) {
+ for (String name:names) {
+ users.add(name.toLowerCase());
+ }
+ }
+
+ public void removeUser(String name) {
+ users.remove(name.toLowerCase());
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+
+ @Override
+ public int compareTo(TeamModel o) {
+ return name.compareTo(o.name);
+ }
+}
diff --git a/src/com/gitblit/models/UserModel.java b/src/com/gitblit/models/UserModel.java
index 8c99512b..bd8974d7 100644
--- a/src/com/gitblit/models/UserModel.java
+++ b/src/com/gitblit/models/UserModel.java
@@ -40,6 +40,7 @@ public class UserModel implements Principal, Serializable, Comparable<UserModel>
public boolean canAdmin;
public boolean excludeFromFederation;
public final Set<String> repositories = new HashSet<String>();
+ public final Set<TeamModel> teams = new HashSet<TeamModel>();
public UserModel(String username) {
this.username = username;
@@ -54,13 +55,24 @@ public class UserModel implements Principal, Serializable, Comparable<UserModel>
*/
@Deprecated
public boolean canAccessRepository(String repositoryName) {
- return canAdmin || repositories.contains(repositoryName.toLowerCase());
+ return canAdmin || repositories.contains(repositoryName.toLowerCase())
+ || hasTeamAccess(repositoryName);
}
public boolean canAccessRepository(RepositoryModel repository) {
boolean isOwner = !StringUtils.isEmpty(repository.owner)
&& repository.owner.equals(username);
- return canAdmin || isOwner || repositories.contains(repository.name.toLowerCase());
+ return canAdmin || isOwner || repositories.contains(repository.name.toLowerCase())
+ || hasTeamAccess(repository.name);
+ }
+
+ public boolean hasTeamAccess(String repositoryName) {
+ for (TeamModel team : teams) {
+ if (team.hasRepository(repositoryName)) {
+ return true;
+ }
+ }
+ return false;
}
public boolean hasRepository(String name) {
@@ -75,6 +87,15 @@ public class UserModel implements Principal, Serializable, Comparable<UserModel>
repositories.remove(name.toLowerCase());
}
+ public boolean isTeamMember(String teamname) {
+ for (TeamModel team : teams) {
+ if (team.name.equalsIgnoreCase(teamname)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
@Override
public String getName() {
return username;
diff --git a/src/com/gitblit/utils/DeepCopier.java b/src/com/gitblit/utils/DeepCopier.java
new file mode 100644
index 00000000..5df30623
--- /dev/null
+++ b/src/com/gitblit/utils/DeepCopier.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2011 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.utils;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+
+public class DeepCopier {
+
+ /**
+ * Produce a deep copy of the given object. Serializes the entire object to
+ * a byte array in memory. Recommended for relatively small objects.
+ */
+ @SuppressWarnings("unchecked")
+ public static <T> T copy(T original) {
+ T o = null;
+ try {
+ ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
+ ObjectOutputStream oos = new ObjectOutputStream(byteOut);
+ oos.writeObject(original);
+ ByteArrayInputStream byteIn = new ByteArrayInputStream(byteOut.toByteArray());
+ ObjectInputStream ois = new ObjectInputStream(byteIn);
+ try {
+ o = (T) ois.readObject();
+ } catch (ClassNotFoundException cex) {
+ // actually can not happen in this instance
+ }
+ } catch (IOException iox) {
+ // doesn't seem likely to happen as these streams are in memory
+ throw new RuntimeException(iox);
+ }
+ return o;
+ }
+
+ /**
+ * This conserves heap memory!!!!! Produce a deep copy of the given object.
+ * Serializes the object through a pipe between two threads. Recommended for
+ * very large objects. The current thread is used for serializing the
+ * original object in order to respect any synchronization the caller may
+ * have around it, and a new thread is used for deserializing the copy.
+ *
+ */
+ public static <T> T copyParallel(T original) {
+ try {
+ PipedOutputStream outputStream = new PipedOutputStream();
+ PipedInputStream inputStream = new PipedInputStream(outputStream);
+ ObjectOutputStream ois = new ObjectOutputStream(outputStream);
+ Receiver<T> receiver = new Receiver<T>(inputStream);
+ try {
+ ois.writeObject(original);
+ } finally {
+ ois.close();
+ }
+ return receiver.getResult();
+ } catch (IOException iox) {
+ // doesn't seem likely to happen as these streams are in memory
+ throw new RuntimeException(iox);
+ }
+ }
+
+ private static class Receiver<T> extends Thread {
+
+ private final InputStream inputStream;
+ private volatile T result;
+ private volatile Throwable throwable;
+
+ public Receiver(InputStream inputStream) {
+ this.inputStream = inputStream;
+ start();
+ }
+
+ @SuppressWarnings("unchecked")
+ public void run() {
+
+ try {
+ ObjectInputStream ois = new ObjectInputStream(inputStream);
+ try {
+ result = (T) ois.readObject();
+ try {
+ // Some serializers may write more than they actually
+ // need to deserialize the object, but if we don't
+ // read it all the PipedOutputStream will choke.
+ while (inputStream.read() != -1) {
+ }
+ } catch (IOException e) {
+ // The object has been successfully deserialized, so
+ // ignore problems at this point (for example, the
+ // serializer may have explicitly closed the inputStream
+ // itself, causing this read to fail).
+ }
+ } finally {
+ ois.close();
+ }
+ } catch (Throwable t) {
+ throwable = t;
+ }
+ }
+
+ public T getResult() throws IOException {
+ try {
+ join();
+ } catch (InterruptedException e) {
+ throw new RuntimeException("Unexpected InterruptedException", e);
+ }
+ // join() guarantees that all shared memory is synchronized between
+ // the two threads
+ if (throwable != null) {
+ if (throwable instanceof ClassNotFoundException) {
+ // actually can not happen in this instance
+ }
+ throw new RuntimeException(throwable);
+ }
+ return result;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/com/gitblit/wicket/GitBlitWebApp.properties b/src/com/gitblit/wicket/GitBlitWebApp.properties
index 7ffdb596..6f321687 100644
--- a/src/com/gitblit/wicket/GitBlitWebApp.properties
+++ b/src/com/gitblit/wicket/GitBlitWebApp.properties
@@ -187,4 +187,10 @@ gb.recentActivityNone = last {0} days / none
gb.dailyActivity = daily activity
gb.activeRepositories = active repositories
gb.activeAuthors = active authors
-gb.commits = commits \ No newline at end of file
+gb.commits = commits
+gb.teams = teams
+gb.teamName = team name
+gb.teamMembers = team members
+gb.teamMemberships = team memberships
+gb.newTeam = new team
+gb.permittedTeams = permitted teams \ No newline at end of file
diff --git a/src/com/gitblit/wicket/WicketUtils.java b/src/com/gitblit/wicket/WicketUtils.java
index e8c5e154..dbeb47f7 100644
--- a/src/com/gitblit/wicket/WicketUtils.java
+++ b/src/com/gitblit/wicket/WicketUtils.java
@@ -259,6 +259,10 @@ public class WicketUtils {
return new PageParameters("user=" + username);
}
+ public static PageParameters newTeamnameParameter(String teamname) {
+ return new PageParameters("team=" + teamname);
+ }
+
public static PageParameters newRepositoryParameter(String repositoryName) {
return new PageParameters("r=" + repositoryName);
}
@@ -377,6 +381,10 @@ public class WicketUtils {
return params.getString("user", "");
}
+ public static String getTeamname(PageParameters params) {
+ return params.getString("team", "");
+ }
+
public static String getToken(PageParameters params) {
return params.getString("t", "");
}
diff --git a/src/com/gitblit/wicket/pages/EditRepositoryPage.html b/src/com/gitblit/wicket/pages/EditRepositoryPage.html
index 27a5448b..9e221898 100644
--- a/src/com/gitblit/wicket/pages/EditRepositoryPage.html
+++ b/src/com/gitblit/wicket/pages/EditRepositoryPage.html
@@ -7,7 +7,7 @@
<wicket:extend>
<body onload="document.getElementById('name').focus();">
<!-- Repository Table -->
- <form wicket:id="editForm">
+ <form style="padding-top:5px;" wicket:id="editForm">
<table class="plain">
<tbody>
<tr><th><wicket:message key="gb.name"></wicket:message></th><td class="edit"><input class="span6" type="text" wicket:id="name" id="name" size="40" tabindex="1" /> &nbsp;<i><wicket:message key="gb.nameDescription"></wicket:message></i></td></tr>
@@ -24,6 +24,7 @@
<tr><td colspan="2"><hr></hr></td></tr>
<tr><th><wicket:message key="gb.accessRestriction"></wicket:message></th><td class="edit"><select class="span6" wicket:id="accessRestriction" tabindex="12" /></td></tr>
<tr><th style="vertical-align: top;"><wicket:message key="gb.permittedUsers"></wicket:message></th><td style="padding:2px;"><span wicket:id="users"></span></td></tr>
+ <tr><th style="vertical-align: top;"><wicket:message key="gb.permittedTeams"></wicket:message></th><td style="padding:2px;"><span wicket:id="teams"></span></td></tr>
<tr><td colspan="2"><hr></hr></td></tr>
<tr><th><wicket:message key="gb.federationStrategy"></wicket:message></th><td class="edit"><select class="span6" wicket:id="federationStrategy" tabindex="13" /></td></tr>
<tr><th style="vertical-align: top;"><wicket:message key="gb.federationSets"></wicket:message></th><td style="padding:2px;"><span wicket:id="federationSets"></span></td></tr>
diff --git a/src/com/gitblit/wicket/pages/EditRepositoryPage.java b/src/com/gitblit/wicket/pages/EditRepositoryPage.java
index be88bd5f..1a5ec3dd 100644
--- a/src/com/gitblit/wicket/pages/EditRepositoryPage.java
+++ b/src/com/gitblit/wicket/pages/EditRepositoryPage.java
@@ -75,12 +75,14 @@ public class EditRepositoryPage extends RootSubPage {
List<String> federationSets = new ArrayList<String>();
List<String> repositoryUsers = new ArrayList<String>();
+ List<String> repositoryTeams = new ArrayList<String>();
if (isCreate) {
super.setupPage(getString("gb.newRepository"), "");
} else {
super.setupPage(getString("gb.edit"), repositoryModel.name);
if (repositoryModel.accessRestriction.exceeds(AccessRestrictionType.NONE)) {
repositoryUsers.addAll(GitBlit.self().getRepositoryUsers(repositoryModel));
+ repositoryTeams.addAll(GitBlit.self().getRepositoryTeams(repositoryModel));
Collections.sort(repositoryUsers);
}
federationSets.addAll(repositoryModel.federationSets);
@@ -93,6 +95,11 @@ public class EditRepositoryPage extends RootSubPage {
repositoryUsers), new CollectionModel<String>(GitBlit.self().getAllUsernames()),
new ChoiceRenderer<String>("", ""), 10, false);
+ // teams palette
+ final Palette<String> teamsPalette = new Palette<String>("teams", new ListModel<String>(
+ repositoryTeams), new CollectionModel<String>(GitBlit.self().getAllTeamnames()),
+ new ChoiceRenderer<String>("", ""), 10, false);
+
// federation sets palette
List<String> sets = GitBlit.getStrings(Keys.federation.sets);
final Palette<String> federationSetsPalette = new Palette<String>("federationSets",
@@ -165,8 +172,9 @@ public class EditRepositoryPage extends RootSubPage {
// save the repository
GitBlit.self().updateRepositoryModel(oldName, repositoryModel, isCreate);
- // save the repository access list
+ // repository access
if (repositoryModel.accessRestriction.exceeds(AccessRestrictionType.NONE)) {
+ // save the user access list
Iterator<String> users = usersPalette.getSelectedChoices();
List<String> repositoryUsers = new ArrayList<String>();
while (users.hasNext()) {
@@ -178,6 +186,14 @@ public class EditRepositoryPage extends RootSubPage {
repositoryUsers.add(repositoryModel.owner);
}
GitBlit.self().setRepositoryUsers(repositoryModel, repositoryUsers);
+
+ // save the team access list
+ Iterator<String> teams = teamsPalette.getSelectedChoices();
+ List<String> repositoryTeams = new ArrayList<String>();
+ while (teams.hasNext()) {
+ repositoryTeams.add(teams.next());
+ }
+ GitBlit.self().setRepositoryTeams(repositoryModel, repositoryTeams);
}
} catch (GitBlitException e) {
error(e.getMessage());
@@ -215,6 +231,7 @@ public class EditRepositoryPage extends RootSubPage {
form.add(new CheckBox("skipSizeCalculation"));
form.add(new CheckBox("skipSummaryMetrics"));
form.add(usersPalette);
+ form.add(teamsPalette);
form.add(federationSetsPalette);
form.add(new Button("save"));
diff --git a/src/com/gitblit/wicket/pages/EditTeamPage.html b/src/com/gitblit/wicket/pages/EditTeamPage.html
new file mode 100644
index 00000000..84a53e3c
--- /dev/null
+++ b/src/com/gitblit/wicket/pages/EditTeamPage.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<wicket:extend>
+<body onload="document.getElementById('name').focus();">
+ <!-- User Table -->
+ <form style="padding-top:5px;" wicket:id="editForm">
+ <table class="plain">
+ <tbody>
+ <tr><th><wicket:message key="gb.teamName"></wicket:message></th><td class="edit"><input type="text" wicket:id="name" id="name" size="30" tabindex="1" /></td></tr>
+ <tr><td colspan="2"><hr></hr></td></tr>
+ <tr><th style="vertical-align: top;"><wicket:message key="gb.teamMembers"></wicket:message></th><td style="padding:2px;"><span wicket:id="users"></span></td></tr>
+ <tr><td colspan="2"><hr></hr></td></tr>
+ <tr><th style="vertical-align: top;"><wicket:message key="gb.restrictedRepositories"></wicket:message></th><td style="padding:2px;"><span wicket:id="repositories"></span></td></tr>
+ <tr><th></th><td class="editButton"><input class="btn" type="submit" value="Cancel" wicket:message="value:gb.cancel" wicket:id="cancel" tabindex="3" /> &nbsp; <input class="btn primary" type="submit" value="Save" wicket:message="value:gb.save" wicket:id="save" tabindex="4" /></td></tr>
+ </tbody>
+ </table>
+ </form>
+</body>
+</wicket:extend>
+</html> \ No newline at end of file
diff --git a/src/com/gitblit/wicket/pages/EditTeamPage.java b/src/com/gitblit/wicket/pages/EditTeamPage.java
new file mode 100644
index 00000000..47f3568f
--- /dev/null
+++ b/src/com/gitblit/wicket/pages/EditTeamPage.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright 2011 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.wicket.pages;
+
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.extensions.markup.html.form.palette.Palette;
+import org.apache.wicket.markup.html.form.Button;
+import org.apache.wicket.markup.html.form.ChoiceRenderer;
+import org.apache.wicket.markup.html.form.Form;
+import org.apache.wicket.markup.html.form.TextField;
+import org.apache.wicket.model.CompoundPropertyModel;
+import org.apache.wicket.model.util.CollectionModel;
+import org.apache.wicket.model.util.ListModel;
+
+import com.gitblit.Constants.AccessRestrictionType;
+import com.gitblit.GitBlit;
+import com.gitblit.GitBlitException;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.TeamModel;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.wicket.RequiresAdminRole;
+import com.gitblit.wicket.WicketUtils;
+
+@RequiresAdminRole
+public class EditTeamPage extends RootSubPage {
+
+ private final boolean isCreate;
+
+ public EditTeamPage() {
+ // create constructor
+ super();
+ isCreate = true;
+ setupPage(new TeamModel(""));
+ }
+
+ public EditTeamPage(PageParameters params) {
+ // edit constructor
+ super(params);
+ isCreate = false;
+ String name = WicketUtils.getTeamname(params);
+ TeamModel model = GitBlit.self().getTeamModel(name);
+ setupPage(model);
+ }
+
+ protected void setupPage(final TeamModel teamModel) {
+ if (isCreate) {
+ super.setupPage(getString("gb.newTeam"), "");
+ } else {
+ super.setupPage(getString("gb.edit"), teamModel.name);
+ }
+
+ CompoundPropertyModel<TeamModel> model = new CompoundPropertyModel<TeamModel>(teamModel);
+
+ List<String> repos = new ArrayList<String>();
+ for (String repo : GitBlit.self().getRepositoryList()) {
+ RepositoryModel repositoryModel = GitBlit.self().getRepositoryModel(repo);
+ if (repositoryModel.accessRestriction.exceeds(AccessRestrictionType.NONE)) {
+ repos.add(repo);
+ }
+ }
+ StringUtils.sortRepositorynames(repos);
+
+ List<String> teamUsers = new ArrayList<String>(teamModel.users);
+ Collections.sort(teamUsers);
+
+ final String oldName = teamModel.name;
+ final Palette<String> repositories = new Palette<String>("repositories",
+ new ListModel<String>(new ArrayList<String>(teamModel.repositories)),
+ new CollectionModel<String>(repos), new ChoiceRenderer<String>("", ""), 10, false);
+ final Palette<String> users = new Palette<String>("users", new ListModel<String>(
+ new ArrayList<String>(teamUsers)), new CollectionModel<String>(GitBlit.self()
+ .getAllUsernames()), new ChoiceRenderer<String>("", ""), 10, false);
+ Form<TeamModel> form = new Form<TeamModel>("editForm", model) {
+
+ private static final long serialVersionUID = 1L;
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.apache.wicket.markup.html.form.Form#onSubmit()
+ */
+ @Override
+ protected void onSubmit() {
+ String teamname = teamModel.name;
+ if (StringUtils.isEmpty(teamname)) {
+ error("Please enter a teamname!");
+ return;
+ }
+ if (isCreate) {
+ TeamModel model = GitBlit.self().getTeamModel(teamname);
+ if (model != null) {
+ error(MessageFormat.format("Team name ''{0}'' is unavailable.", teamname));
+ return;
+ }
+ }
+ Iterator<String> selectedRepositories = repositories.getSelectedChoices();
+ List<String> repos = new ArrayList<String>();
+ while (selectedRepositories.hasNext()) {
+ repos.add(selectedRepositories.next().toLowerCase());
+ }
+ teamModel.repositories.clear();
+ teamModel.repositories.addAll(repos);
+
+ Iterator<String> selectedUsers = users.getSelectedChoices();
+ List<String> members = new ArrayList<String>();
+ while (selectedUsers.hasNext()) {
+ members.add(selectedUsers.next().toLowerCase());
+ }
+ teamModel.users.clear();
+ teamModel.users.addAll(members);
+
+ try {
+ GitBlit.self().updateTeamModel(oldName, teamModel, isCreate);
+ } catch (GitBlitException e) {
+ error(e.getMessage());
+ return;
+ }
+ setRedirect(false);
+ if (isCreate) {
+ // create another team
+ info(MessageFormat.format("New team ''{0}'' successfully created.",
+ teamModel.name));
+ setResponsePage(EditTeamPage.class);
+ } else {
+ // back to users page
+ setResponsePage(UsersPage.class);
+ }
+ }
+ };
+
+ // field names reflective match TeamModel fields
+ form.add(new TextField<String>("name"));
+ form.add(repositories);
+ form.add(users);
+
+ form.add(new Button("save"));
+ Button cancel = new Button("cancel") {
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public void onSubmit() {
+ setResponsePage(UsersPage.class);
+ }
+ };
+ cancel.setDefaultFormProcessing(false);
+ form.add(cancel);
+
+ add(form);
+ }
+}
diff --git a/src/com/gitblit/wicket/pages/EditUserPage.html b/src/com/gitblit/wicket/pages/EditUserPage.html
index ceda3cbf..978393bb 100644
--- a/src/com/gitblit/wicket/pages/EditUserPage.html
+++ b/src/com/gitblit/wicket/pages/EditUserPage.html
@@ -7,7 +7,7 @@
<wicket:extend>
<body onload="document.getElementById('username').focus();">
<!-- User Table -->
- <form wicket:id="editForm">
+ <form style="padding-top:5px;" wicket:id="editForm">
<table class="plain">
<tbody>
<tr><th><wicket:message key="gb.username"></wicket:message></th><td class="edit"><input type="text" wicket:id="username" id="username" size="30" tabindex="1" /></td></tr>
@@ -15,6 +15,9 @@
<tr><th><wicket:message key="gb.confirmPassword"></wicket:message></th><td class="edit"><input type="password" wicket:id="confirmPassword" size="30" tabindex="3" /></td></tr>
<tr><th><wicket:message key="gb.canAdmin"></wicket:message></th><td class="edit"><input type="checkbox" wicket:id="canAdmin" tabindex="6" /> &nbsp;<i><wicket:message key="gb.canAdminDescription"></wicket:message></i></td></tr>
<tr><th><wicket:message key="gb.excludeFromFederation"></wicket:message></th><td class="edit"><input type="checkbox" wicket:id="excludeFromFederation" tabindex="7" /> &nbsp;<i><wicket:message key="gb.excludeFromFederationDescription"></wicket:message></i></td></tr>
+ <tr><td colspan="2"><hr></hr></td></tr>
+ <tr><th style="vertical-align: top;"><wicket:message key="gb.teamMemberships"></wicket:message></th><td style="padding:2px;"><span wicket:id="teams"></span></td></tr>
+ <tr><td colspan="2"><hr></hr></td></tr>
<tr><th style="vertical-align: top;"><wicket:message key="gb.restrictedRepositories"></wicket:message></th><td style="padding:2px;"><span wicket:id="repositories"></span></td></tr>
<tr><th></th><td class="editButton"><input class="btn" type="submit" value="Cancel" wicket:message="value:gb.cancel" wicket:id="cancel" tabindex="8" /> &nbsp; <input class="btn primary" type="submit" value="Save" wicket:message="value:gb.save" wicket:id="save" tabindex="9" /></td></tr>
</tbody>
diff --git a/src/com/gitblit/wicket/pages/EditUserPage.java b/src/com/gitblit/wicket/pages/EditUserPage.java
index 8955e222..799cf01d 100644
--- a/src/com/gitblit/wicket/pages/EditUserPage.java
+++ b/src/com/gitblit/wicket/pages/EditUserPage.java
@@ -17,6 +17,7 @@ package com.gitblit.wicket.pages;
import java.text.MessageFormat;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.Iterator;
import java.util.List;
@@ -38,6 +39,7 @@ import com.gitblit.GitBlit;
import com.gitblit.GitBlitException;
import com.gitblit.Keys;
import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.TeamModel;
import com.gitblit.models.UserModel;
import com.gitblit.utils.StringUtils;
import com.gitblit.wicket.RequiresAdminRole;
@@ -82,10 +84,19 @@ public class EditUserPage extends RootSubPage {
repos.add(repo);
}
}
+ List<String> userTeams = new ArrayList<String>();
+ for (TeamModel team : userModel.teams) {
+ userTeams.add(team.name);
+ }
+ Collections.sort(userTeams);
+
final String oldName = userModel.username;
final Palette<String> repositories = new Palette<String>("repositories",
new ListModel<String>(new ArrayList<String>(userModel.repositories)),
new CollectionModel<String>(repos), new ChoiceRenderer<String>("", ""), 10, false);
+ final Palette<String> teams = new Palette<String>("teams", new ListModel<String>(
+ new ArrayList<String>(userTeams)), new CollectionModel<String>(GitBlit.self()
+ .getAllTeamnames()), new ChoiceRenderer<String>("", ""), 10, false);
Form<UserModel> form = new Form<UserModel>("editForm", model) {
private static final long serialVersionUID = 1L;
@@ -109,7 +120,8 @@ public class EditUserPage extends RootSubPage {
return;
}
}
- boolean rename = !StringUtils.isEmpty(oldName) && !oldName.equalsIgnoreCase(username);
+ boolean rename = !StringUtils.isEmpty(oldName)
+ && !oldName.equalsIgnoreCase(username);
if (!userModel.password.equals(confirmPassword.getObject())) {
error("Passwords do not match!");
return;
@@ -154,6 +166,17 @@ public class EditUserPage extends RootSubPage {
}
userModel.repositories.clear();
userModel.repositories.addAll(repos);
+
+ Iterator<String> selectedTeams = teams.getSelectedChoices();
+ userModel.teams.clear();
+ while (selectedTeams.hasNext()) {
+ TeamModel team = GitBlit.self().getTeamModel(selectedTeams.next());
+ if (team == null) {
+ continue;
+ }
+ userModel.teams.add(team);
+ }
+
try {
GitBlit.self().updateUserModel(oldName, userModel, isCreate);
} catch (GitBlitException e) {
@@ -185,6 +208,7 @@ public class EditUserPage extends RootSubPage {
form.add(new CheckBox("canAdmin"));
form.add(new CheckBox("excludeFromFederation"));
form.add(repositories);
+ form.add(teams);
form.add(new Button("save"));
Button cancel = new Button("cancel") {
diff --git a/src/com/gitblit/wicket/pages/UsersPage.html b/src/com/gitblit/wicket/pages/UsersPage.html
index 4d14496d..edb85f7d 100644
--- a/src/com/gitblit/wicket/pages/UsersPage.html
+++ b/src/com/gitblit/wicket/pages/UsersPage.html
@@ -5,6 +5,8 @@
lang="en">
<body>
<wicket:extend>
+ <div wicket:id="teamsPanel">[teams panel]</div>
+
<div wicket:id="usersPanel">[users panel]</div>
</wicket:extend>
</body>
diff --git a/src/com/gitblit/wicket/pages/UsersPage.java b/src/com/gitblit/wicket/pages/UsersPage.java
index b54b968d..9526deae 100644
--- a/src/com/gitblit/wicket/pages/UsersPage.java
+++ b/src/com/gitblit/wicket/pages/UsersPage.java
@@ -16,6 +16,7 @@
package com.gitblit.wicket.pages;
import com.gitblit.wicket.RequiresAdminRole;
+import com.gitblit.wicket.panels.TeamsPanel;
import com.gitblit.wicket.panels.UsersPanel;
@RequiresAdminRole
@@ -25,6 +26,8 @@ public class UsersPage extends RootPage {
super();
setupPage("", "");
+ add(new TeamsPanel("teamsPanel", showAdmin).setVisible(showAdmin));
+
add(new UsersPanel("usersPanel", showAdmin).setVisible(showAdmin));
}
}
diff --git a/src/com/gitblit/wicket/panels/TeamsPanel.html b/src/com/gitblit/wicket/panels/TeamsPanel.html
new file mode 100644
index 00000000..af1d56de
--- /dev/null
+++ b/src/com/gitblit/wicket/panels/TeamsPanel.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<body>
+<wicket:panel>
+
+ <div wicket:id="adminPanel">[admin links]</div>
+
+ <table class="repositories">
+ <tr>
+ <th class="left">
+ <img style="vertical-align: middle; border: 1px solid #888; background-color: white;" src="users_16x16.png"/>
+ <wicket:message key="gb.teams">[teams]</wicket:message>
+ </th>
+ <th class="right"></th>
+ </tr>
+ <tbody>
+ <tr wicket:id="teamRow">
+ <td class="left" ><div class="list" wicket:id="teamname">[teamname]</div></td>
+ <td class="rightAlign"><span wicket:id="teamLinks"></span></td>
+ </tr>
+ </tbody>
+ </table>
+
+ <wicket:fragment wicket:id="adminLinks">
+ <!-- page nav links -->
+ <div class="admin_nav">
+ <img style="vertical-align: middle;" src="add_16x16.png"/>
+ <a wicket:id="newTeam">
+ <wicket:message key="gb.newTeam"></wicket:message>
+ </a>
+ </div>
+ </wicket:fragment>
+
+ <wicket:fragment wicket:id="teamAdminLinks">
+ <span class="link"><a wicket:id="editTeam"><wicket:message key="gb.edit">[edit]</wicket:message></a> | <a wicket:id="deleteTeam"><wicket:message key="gb.delete">[delete]</wicket:message></a></span>
+ </wicket:fragment>
+
+</wicket:panel>
+</body>
+</html> \ No newline at end of file
diff --git a/src/com/gitblit/wicket/panels/TeamsPanel.java b/src/com/gitblit/wicket/panels/TeamsPanel.java
new file mode 100644
index 00000000..33afb514
--- /dev/null
+++ b/src/com/gitblit/wicket/panels/TeamsPanel.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2011 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.wicket.panels;
+
+import java.text.MessageFormat;
+import java.util.List;
+
+import org.apache.wicket.markup.html.link.BookmarkablePageLink;
+import org.apache.wicket.markup.html.link.Link;
+import org.apache.wicket.markup.html.panel.Fragment;
+import org.apache.wicket.markup.repeater.Item;
+import org.apache.wicket.markup.repeater.data.DataView;
+import org.apache.wicket.markup.repeater.data.ListDataProvider;
+
+import com.gitblit.GitBlit;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.pages.EditTeamPage;
+
+public class TeamsPanel extends BasePanel {
+
+ private static final long serialVersionUID = 1L;
+
+ public TeamsPanel(String wicketId, final boolean showAdmin) {
+ super(wicketId);
+
+ Fragment adminLinks = new Fragment("adminPanel", "adminLinks", this);
+ adminLinks.add(new BookmarkablePageLink<Void>("newTeam", EditTeamPage.class));
+ add(adminLinks.setVisible(showAdmin));
+
+ final List<String> teamnames = GitBlit.self().getAllTeamnames();
+ DataView<String> teamsView = new DataView<String>("teamRow", new ListDataProvider<String>(
+ teamnames)) {
+ private static final long serialVersionUID = 1L;
+ private int counter;
+
+ @Override
+ protected void onBeforeRender() {
+ super.onBeforeRender();
+ counter = 0;
+ }
+
+ public void populateItem(final Item<String> item) {
+ final String entry = item.getModelObject();
+ LinkPanel editLink = new LinkPanel("teamname", "list", entry, EditTeamPage.class,
+ WicketUtils.newTeamnameParameter(entry));
+ WicketUtils.setHtmlTooltip(editLink, getString("gb.edit") + " " + entry);
+ item.add(editLink);
+ Fragment teamLinks = new Fragment("teamLinks", "teamAdminLinks", this);
+ teamLinks.add(new BookmarkablePageLink<Void>("editTeam", EditTeamPage.class,
+ WicketUtils.newTeamnameParameter(entry)));
+ Link<Void> deleteLink = new Link<Void>("deleteTeam") {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public void onClick() {
+ if (GitBlit.self().deleteTeam(entry)) {
+ teamnames.remove(entry);
+ info(MessageFormat.format("Team ''{0}'' deleted.", entry));
+ } else {
+ error(MessageFormat.format("Failed to delete team ''{0}''!", entry));
+ }
+ }
+ };
+ deleteLink.add(new JavascriptEventConfirmation("onclick", MessageFormat.format(
+ "Delete team \"{0}\"?", entry)));
+ teamLinks.add(deleteLink);
+ item.add(teamLinks);
+
+ WicketUtils.setAlternatingBackground(item, counter);
+ counter++;
+ }
+ };
+ add(teamsView.setVisible(showAdmin));
+ }
+}
diff --git a/src/com/gitblit/wicket/panels/UsersPanel.html b/src/com/gitblit/wicket/panels/UsersPanel.html
index c81a3fd9..1cc19ee5 100644
--- a/src/com/gitblit/wicket/panels/UsersPanel.html
+++ b/src/com/gitblit/wicket/panels/UsersPanel.html
@@ -13,7 +13,7 @@
<tr>
<th class="left">
<img style="vertical-align: middle; border: 1px solid #888; background-color: white;" src="user_16x16.png"/>
- <wicket:message key="gb.username">[username]</wicket:message>
+ <wicket:message key="gb.users">[users]</wicket:message>
</th>
<th class="right"></th>
</tr>
diff --git a/tests/com/gitblit/tests/UserServiceTest.java b/tests/com/gitblit/tests/UserServiceTest.java
index 3f410fa5..93e7f606 100644
--- a/tests/com/gitblit/tests/UserServiceTest.java
+++ b/tests/com/gitblit/tests/UserServiceTest.java
@@ -38,6 +38,7 @@ public class UserServiceTest {
file.delete();
IUserService service = new FileUserService(file);
testUsers(service);
+ testTeams(service);
file.delete();
}
@@ -47,6 +48,7 @@ public class UserServiceTest {
file.delete();
IUserService service = new ConfigUserService(file);
testUsers(service);
+ testTeams(service);
file.delete();
}
@@ -106,4 +108,109 @@ public class UserServiceTest {
testUser = service.getUserModel("test");
assertTrue(testUser.hasRepository("newrepo1"));
}
+
+ protected void testTeams(IUserService service) {
+
+ // confirm we have no teams
+ assertEquals(0, service.getAllTeamNames().size());
+
+ // remove newrepo1 from test user
+ // now test user has no repositories
+ UserModel user = service.getUserModel("test");
+ user.repositories.clear();
+ service.updateUserModel(user);
+ user = service.getUserModel("test");
+ assertEquals(0, user.repositories.size());
+ assertFalse(user.canAccessRepository("newrepo1"));
+ assertFalse(user.canAccessRepository("NEWREPO1"));
+
+ // create test team and add test user and newrepo1
+ TeamModel team = new TeamModel("testteam");
+ team.addUser("test");
+ team.addRepository("newrepo1");
+ service.updateTeamModel(team);
+
+ // confirm 1 user and 1 repo
+ team = service.getTeamModel("testteam");
+ assertEquals(1, team.repositories.size());
+ assertEquals(1, team.users.size());
+
+ // confirm team membership
+ user = service.getUserModel("test");
+ assertEquals(0, user.repositories.size());
+ assertEquals(1, user.teams.size());
+
+ // confirm team access
+ assertTrue(team.hasRepository("newrepo1"));
+ assertTrue(user.hasTeamAccess("newrepo1"));
+ assertTrue(team.hasRepository("NEWREPO1"));
+ assertTrue(user.hasTeamAccess("NEWREPO1"));
+
+ // rename the team and add new repository
+ team.addRepository("newrepo2");
+ team.name = "testteam2";
+ service.updateTeamModel("testteam", team);
+
+ team = service.getTeamModel("testteam2");
+ user = service.getUserModel("test");
+
+ // confirm user and team can access newrepo2
+ assertEquals(2, team.repositories.size());
+ assertTrue(team.hasRepository("newrepo2"));
+ assertTrue(user.hasTeamAccess("newrepo2"));
+ assertTrue(team.hasRepository("NEWREPO2"));
+ assertTrue(user.hasTeamAccess("NEWREPO2"));
+
+ // delete testteam2
+ service.deleteTeam("testteam2");
+ team = service.getTeamModel("testteam2");
+ user = service.getUserModel("test");
+
+ // confirm team does not exist and user can not access newrepo1 and 2
+ assertEquals(null, team);
+ assertFalse(user.canAccessRepository("newrepo1"));
+ assertFalse(user.canAccessRepository("newrepo2"));
+
+ // create new team and add it to user
+ // this tests the inverse team creation/team addition
+ team = new TeamModel("testteam");
+ team.addRepository("NEWREPO1");
+ team.addRepository("NEWREPO2");
+ user.teams.add(team);
+ service.updateUserModel(user);
+
+ // confirm the inverted team addition
+ user = service.getUserModel("test");
+ team = service.getTeamModel("testteam");
+ assertTrue(user.hasTeamAccess("newrepo1"));
+ assertTrue(user.hasTeamAccess("newrepo2"));
+ assertTrue(team.hasUser("test"));
+
+ // drop testteam from user and add nextteam to user
+ team = new TeamModel("nextteam");
+ team.addRepository("NEWREPO1");
+ team.addRepository("NEWREPO2");
+ user.teams.clear();
+ user.teams.add(team);
+ service.updateUserModel(user);
+
+ // confirm implicit drop
+ user = service.getUserModel("test");
+ team = service.getTeamModel("testteam");
+ assertTrue(user.hasTeamAccess("newrepo1"));
+ assertTrue(user.hasTeamAccess("newrepo2"));
+ assertFalse(team.hasUser("test"));
+ team = service.getTeamModel("nextteam");
+ assertTrue(team.hasUser("test"));
+
+ // delete the user and confirm team no longer has user
+ service.deleteUser("test");
+ team = service.getTeamModel("testteam");
+ assertFalse(team.hasUser("test"));
+
+ // delete both teams
+ service.deleteTeam("testteam");
+ service.deleteTeam("nextteam");
+ assertEquals(0, service.getAllTeamNames().size());
+ }
} \ No newline at end of file