James Moger
2011-12-13 997c16d6826cfa1bef33ba08e15055cc407b9398
Federation support for Teams
7 files modified
172 ■■■■■ changed files
docs/02_federation.mkd 10 ●●●●● patch | view | raw | blame | history
src/com/gitblit/Constants.java 2 ●●● patch | view | raw | blame | history
src/com/gitblit/FederationPullExecutor.java 32 ●●●●● patch | view | raw | blame | history
src/com/gitblit/FederationServlet.java 18 ●●●●● patch | view | raw | blame | history
src/com/gitblit/models/UserModel.java 12 ●●●●● patch | view | raw | blame | history
src/com/gitblit/utils/FederationUtils.java 18 ●●●●● patch | view | raw | blame | history
tests/com/gitblit/tests/FederationTests.java 80 ●●●●● patch | view | raw | blame | history
docs/02_federation.mkd
@@ -13,7 +13,9 @@
### Important Changes to Note
The Gitblit 0.7.0 federation protocol is incompatible with the 0.6.0 federation protocol because of a change in the way timestamps are formatted.
The *Gitblit 0.8.0* federation protocol adds retrieval of team definitions.  Older clients will not know to request team information.
The *Gitblit 0.7.0* federation protocol is incompatible with the 0.6.0 federation protocol because of a change in the way timestamps are formatted.
Gitblit 0.6.0 uses the default [google-gson](http://google-gson.googlecode.com) timestamp serializer which generates locally formatted timestamps.  Unfortunately, this creates problems for distributed repositories and distributed developers.  Gitblit 0.7.0 corrects this error by serializing dates to the [iso8601](http://en.wikipedia.org/wiki/ISO_8601) standard.  As a result 0.7.0 is not compatible with 0.6.0.  A partial backwards-compatibility fallback was considered but it would only work one direction and since the federation mechanism is bidirectional it was not implemented.
@@ -151,13 +153,13 @@
During a federated pull operation, Gitblit does check that the *origin* of the local repository starts with the url of the federation registration.  
If they do not match, the repository is skipped and this is indicated in the log.
#### User Accounts
#### User Accounts & Teams
By default all user accounts except the *admin* account are automatically pulled when using the *ALL* token or the *USERS_AND_REPOSITORIES* token.  You may exclude a user account from being pulled by a federated Gitblit instance by checking *exclude from federation* in the edit user page.
By default all user accounts and teams (except the *admin* account) are automatically pulled when using the *ALL* token or the *USERS_AND_REPOSITORIES* token.  You may exclude a user account from being pulled by a federated Gitblit instance by checking *exclude from federation* in the edit user page.
The pulling Gitblit instance will store a registration-specific `users.conf` file for the pulled user accounts and their repository permissions. This file is stored in the *federation.N.folder* folder.
If you specify *federation.N.mergeAccounts=true*, then the user accounts from the origin Gitblit instance will be integrated into the `users.conf` file of your Gitblit instance and allow sign-on of those users.
If you specify *federation.N.mergeAccounts=true*, then the user accounts and team definitions from the origin Gitblit instance will be integrated into the `users.conf` file of your Gitblit instance and allow sign-on of those users.
**NOTE:**  
Upgrades from older Gitblit versions will not have the *#notfederated* role assigned to the *admin* account.  Without that role, your admin account WILL be transferred with an *ALL* or *USERS_AND_REPOSITORIES* token.  
src/com/gitblit/Constants.java
@@ -117,7 +117,7 @@
     * Enumeration representing the types of federation requests.
     */
    public static enum FederationRequest {
        POKE, PROPOSAL, PULL_REPOSITORIES, PULL_USERS, PULL_SETTINGS, STATUS;
        POKE, PROPOSAL, PULL_REPOSITORIES, PULL_USERS, PULL_TEAMS, PULL_SETTINGS, STATUS;
        public static FederationRequest fromName(String name) {
            for (FederationRequest type : values()) {
src/com/gitblit/FederationPullExecutor.java
@@ -47,6 +47,7 @@
import com.gitblit.GitBlitException.ForbiddenException;
import com.gitblit.models.FederationModel;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.TeamModel;
import com.gitblit.models.UserModel;
import com.gitblit.utils.FederationUtils;
import com.gitblit.utils.JGitUtils;
@@ -282,10 +283,12 @@
        try {
            // Pull USERS
            // TeamModels are automatically pulled because they are contained
            // within the UserModel. The UserService creates unknown teams
            // and updates existing teams.
            Collection<UserModel> users = FederationUtils.getUsers(registration);
            if (users != null && users.size() > 0) {
                File realmFile = new File(registrationFolderFile, registration.name
                        + "_users.conf");
                File realmFile = new File(registrationFolderFile, registration.name + "_users.conf");
                realmFile.delete();
                ConfigUserService userService = new ConfigUserService(realmFile);
                for (UserModel user : users) {
@@ -318,6 +321,31 @@
                            localUser.canAdmin = user.canAdmin;
                            GitBlit.self().updateUserModel(localUser.username, localUser, false);
                        }
                        for (String teamname : GitBlit.self().getAllTeamnames()) {
                            TeamModel team = GitBlit.self().getTeamModel(teamname);
                            if (user.isTeamMember(teamname) && !team.hasUser(user.username)) {
                                // new team member
                                team.addUser(user.username);
                                GitBlit.self().updateTeamModel(teamname, team, false);
                            } else if (!user.isTeamMember(teamname) && team.hasUser(user.username)) {
                                // remove team member
                                team.removeUser(user.username);
                                GitBlit.self().updateTeamModel(teamname, team, false);
                            }
                            // update team repositories
                            TeamModel remoteTeam = user.getTeam(teamname);
                            if (remoteTeam != null && remoteTeam.repositories != null) {
                                int before = team.repositories.size();
                                team.addRepositories(remoteTeam.repositories);
                                int after = team.repositories.size();
                                if (after > before) {
                                    // repository count changed, update
                                    GitBlit.self().updateTeamModel(teamname, team, false);
                                }
                            }
                        }
                    }
                }
            }
src/com/gitblit/FederationServlet.java
@@ -27,6 +27,7 @@
import com.gitblit.Constants.FederationRequest;
import com.gitblit.models.FederationModel;
import com.gitblit.models.FederationProposal;
import com.gitblit.models.TeamModel;
import com.gitblit.models.UserModel;
import com.gitblit.utils.FederationUtils;
import com.gitblit.utils.HttpUtils;
@@ -198,6 +199,23 @@
                    }
                }
                result = users;
            } else if (FederationRequest.PULL_TEAMS.equals(reqType)) {
                // pull teams
                if (!GitBlit.self().validateFederationRequest(reqType, token)) {
                    // invalid token to pull teams
                    logger.warn(MessageFormat.format(
                            "Federation token from {0} not authorized to pull TEAMS",
                            request.getRemoteAddr()));
                    response.sendError(HttpServletResponse.SC_FORBIDDEN);
                    return;
                }
                List<String> teamnames = GitBlit.self().getAllTeamnames();
                List<TeamModel> teams = new ArrayList<TeamModel>();
                for (String teamname : teamnames) {
                    TeamModel user = GitBlit.self().getTeamModel(teamname);
                    teams.add(user);
                }
                result = teams;
            }
        }
src/com/gitblit/models/UserModel.java
@@ -96,6 +96,18 @@
        return false;
    }
    public TeamModel getTeam(String teamname) {
        if (teams == null) {
            return null;
        }
        for (TeamModel team : teams) {
            if (team.name.equalsIgnoreCase(teamname)) {
                return team;
            }
        }
        return null;
    }
    @Override
    public String getName() {
        return username;
src/com/gitblit/utils/FederationUtils.java
@@ -38,6 +38,7 @@
import com.gitblit.models.FederationModel;
import com.gitblit.models.FederationProposal;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.TeamModel;
import com.gitblit.models.UserModel;
import com.google.gson.reflect.TypeToken;
@@ -56,6 +57,9 @@
    }.getType();
    private static final Type USERS_TYPE = new TypeToken<Collection<UserModel>>() {
    }.getType();
    private static final Type TEAMS_TYPE = new TypeToken<Collection<TeamModel>>() {
    }.getType();
    private static final Logger LOGGER = LoggerFactory.getLogger(FederationUtils.class);
@@ -281,6 +285,20 @@
    }
    /**
     * Tries to pull the gitblit team definitions from the remote gitblit instance.
     *
     * @param registration
     * @return a collection of TeamModel objects
     * @throws Exception
     */
    public static List<TeamModel> getTeams(FederationModel registration) throws Exception {
        String url = asLink(registration.url, registration.token, FederationRequest.PULL_TEAMS);
        Collection<TeamModel> models = JsonUtils.retrieveJson(url, TEAMS_TYPE);
        List<TeamModel> list = new ArrayList<TeamModel>(models);
        return list;
    }
    /**
     * Tries to pull the gitblit server settings from the remote gitblit
     * instance.
     * 
tests/com/gitblit/tests/FederationTests.java
@@ -16,10 +16,12 @@
package com.gitblit.tests;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -31,16 +33,21 @@
import com.gitblit.Constants.FederationProposalResult;
import com.gitblit.Constants.FederationRequest;
import com.gitblit.Constants.FederationToken;
import com.gitblit.models.FederationModel;
import com.gitblit.models.FederationProposal;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.TeamModel;
import com.gitblit.models.UserModel;
import com.gitblit.utils.FederationUtils;
import com.gitblit.utils.JsonUtils;
import com.gitblit.utils.RpcUtils;
public class FederationTests {
    String url = GitBlitSuite.url;
    String account = GitBlitSuite.account;
    String password = GitBlitSuite.password;
    String token = "d7cc58921a80b37e0329a4dae2f9af38bf61ef5c";
    private static final AtomicBoolean started = new AtomicBoolean(false);
@@ -81,15 +88,72 @@
    }
    @Test
    public void testPullRepositories() throws Exception {
        try {
            String requrl = FederationUtils.asLink(url, "d7cc58921a80b37e0329a4dae2f9af38bf61ef5c",
                    FederationRequest.PULL_REPOSITORIES);
    public void testJsonRepositories() throws Exception {
        String requrl = FederationUtils.asLink(url, token, FederationRequest.PULL_REPOSITORIES);
            String json = JsonUtils.retrieveJsonString(requrl, null, null);
        } catch (IOException e) {
            if (!e.getMessage().contains("403")) {
                throw e;
        assertNotNull(json);
            }
    @Test
    public void testJsonUsers() throws Exception {
        String requrl = FederationUtils.asLink(url, token, FederationRequest.PULL_USERS);
        String json = JsonUtils.retrieveJsonString(requrl, null, null);
        assertNotNull(json);
        }
    @Test
    public void testJsonTeams() throws Exception {
        String requrl = FederationUtils.asLink(url, token, FederationRequest.PULL_TEAMS);
        String json = JsonUtils.retrieveJsonString(requrl, null, null);
        assertNotNull(json);
    }
    private FederationModel getRegistration() {
        FederationModel model = new FederationModel("localhost");
        model.url = this.url;
        model.token = this.token;
        return model;
    }
    @Test
    public void testPullRepositories() throws Exception {
        Map<String, RepositoryModel> repos = FederationUtils.getRepositories(getRegistration(),
                false);
        assertNotNull(repos);
        assertTrue(repos.size() > 0);
    }
    @Test
    public void testPullUsers() throws Exception {
        List<UserModel> users = FederationUtils.getUsers(getRegistration());
        assertNotNull(users);
        // admin is excluded
        assertEquals(0, users.size());
        UserModel newUser = new UserModel("test");
        newUser.password = "whocares";
        assertTrue(RpcUtils.createUser(newUser, url, account, password.toCharArray()));
        TeamModel team = new TeamModel("testteam");
        team.addUser("test");
        team.addRepository("helloworld.git");
        assertTrue(RpcUtils.createTeam(team, url, account, password.toCharArray()));
        users = FederationUtils.getUsers(getRegistration());
        assertNotNull(users);
        assertEquals(1, users.size());
        newUser = users.get(0);
        assertTrue(newUser.isTeamMember("testteam"));
        assertTrue(RpcUtils.deleteUser(newUser, url, account, password.toCharArray()));
        assertTrue(RpcUtils.deleteTeam(team, url, account, password.toCharArray()));
    }
    @Test
    public void testPullTeams() throws Exception {
        List<TeamModel> teams = FederationUtils.getTeams(getRegistration());
        assertNotNull(teams);
        assertTrue(teams.size() > 0);
    }
}