]> source.dussan.org Git - gitblit.git/commitdiff
Teams support.
authorJames Moger <james.moger@gitblit.com>
Thu, 8 Dec 2011 00:33:10 +0000 (19:33 -0500)
committerJames Moger <james.moger@gitblit.com>
Thu, 8 Dec 2011 00:33:10 +0000 (19:33 -0500)
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.

25 files changed:
docs/01_features.mkd
docs/01_setup.mkd
docs/04_releases.mkd
resources/users_16x16.png [new file with mode: 0644]
src/com/gitblit/ConfigUserService.java
src/com/gitblit/FileUserService.java
src/com/gitblit/GitBlit.java
src/com/gitblit/IUserService.java
src/com/gitblit/models/TeamModel.java [new file with mode: 0644]
src/com/gitblit/models/UserModel.java
src/com/gitblit/utils/DeepCopier.java [new file with mode: 0644]
src/com/gitblit/wicket/GitBlitWebApp.properties
src/com/gitblit/wicket/WicketUtils.java
src/com/gitblit/wicket/pages/EditRepositoryPage.html
src/com/gitblit/wicket/pages/EditRepositoryPage.java
src/com/gitblit/wicket/pages/EditTeamPage.html [new file with mode: 0644]
src/com/gitblit/wicket/pages/EditTeamPage.java [new file with mode: 0644]
src/com/gitblit/wicket/pages/EditUserPage.html
src/com/gitblit/wicket/pages/EditUserPage.java
src/com/gitblit/wicket/pages/UsersPage.html
src/com/gitblit/wicket/pages/UsersPage.java
src/com/gitblit/wicket/panels/TeamsPanel.html [new file with mode: 0644]
src/com/gitblit/wicket/panels/TeamsPanel.java [new file with mode: 0644]
src/com/gitblit/wicket/panels/UsersPanel.html
tests/com/gitblit/tests/UserServiceTest.java

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