From 997c16d6826cfa1bef33ba08e15055cc407b9398 Mon Sep 17 00:00:00 2001 From: James Moger Date: Tue, 13 Dec 2011 17:36:58 -0500 Subject: [PATCH] Federation support for Teams --- docs/02_federation.mkd | 10 ++- src/com/gitblit/Constants.java | 2 +- src/com/gitblit/FederationPullExecutor.java | 32 +++++++- src/com/gitblit/FederationServlet.java | 20 ++++- src/com/gitblit/models/UserModel.java | 12 +++ src/com/gitblit/utils/FederationUtils.java | 18 +++++ tests/com/gitblit/tests/FederationTests.java | 84 +++++++++++++++++--- 7 files changed, 160 insertions(+), 18 deletions(-) diff --git a/docs/02_federation.mkd b/docs/02_federation.mkd index a592c1ed..6525000e 100644 --- a/docs/02_federation.mkd +++ b/docs/02_federation.mkd @@ -13,7 +13,9 @@ Please review all the documentation to understand how it works and its limitatio ### 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 @@ After a repository has been cloned it is flagged as *isFederated* (which identif 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. diff --git a/src/com/gitblit/Constants.java b/src/com/gitblit/Constants.java index 32799808..c2d5eb26 100644 --- a/src/com/gitblit/Constants.java +++ b/src/com/gitblit/Constants.java @@ -117,7 +117,7 @@ public class Constants { * 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()) { diff --git a/src/com/gitblit/FederationPullExecutor.java b/src/com/gitblit/FederationPullExecutor.java index 20fd67c6..c84761b3 100644 --- a/src/com/gitblit/FederationPullExecutor.java +++ b/src/com/gitblit/FederationPullExecutor.java @@ -47,6 +47,7 @@ import com.gitblit.Constants.FederationStrategy; 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 @@ public class FederationPullExecutor implements Runnable { try { // Pull USERS + // TeamModels are automatically pulled because they are contained + // within the UserModel. The UserService creates unknown teams + // and updates existing teams. Collection 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 @@ public class FederationPullExecutor implements Runnable { 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); + } + } + } } } } diff --git a/src/com/gitblit/FederationServlet.java b/src/com/gitblit/FederationServlet.java index 0be1066f..f2ed903c 100644 --- a/src/com/gitblit/FederationServlet.java +++ b/src/com/gitblit/FederationServlet.java @@ -27,6 +27,7 @@ import javax.servlet.http.HttpServletResponse; 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; @@ -90,7 +91,7 @@ public class FederationServlet extends JsonServlet { if (proposal == null) { return; } - + // reject proposal, if not receipt prohibited if (!GitBlit.getBoolean(Keys.federation.allowProposals, false)) { logger.error(MessageFormat.format("Rejected {0} federation proposal from {1}", @@ -198,6 +199,23 @@ public class FederationServlet extends JsonServlet { } } 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 teamnames = GitBlit.self().getAllTeamnames(); + List teams = new ArrayList(); + for (String teamname : teamnames) { + TeamModel user = GitBlit.self().getTeamModel(teamname); + teams.add(user); + } + result = teams; } } diff --git a/src/com/gitblit/models/UserModel.java b/src/com/gitblit/models/UserModel.java index bd8974d7..ecb97cfc 100644 --- a/src/com/gitblit/models/UserModel.java +++ b/src/com/gitblit/models/UserModel.java @@ -96,6 +96,18 @@ public class UserModel implements Principal, Serializable, Comparable 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; diff --git a/src/com/gitblit/utils/FederationUtils.java b/src/com/gitblit/utils/FederationUtils.java index 324aa67e..8207962a 100644 --- a/src/com/gitblit/utils/FederationUtils.java +++ b/src/com/gitblit/utils/FederationUtils.java @@ -38,6 +38,7 @@ import com.gitblit.Keys; 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; @@ -58,6 +59,9 @@ public class FederationUtils { private static final Type USERS_TYPE = new TypeToken>() { }.getType(); + private static final Type TEAMS_TYPE = new TypeToken>() { + }.getType(); + private static final Logger LOGGER = LoggerFactory.getLogger(FederationUtils.class); /** @@ -280,6 +284,20 @@ public class FederationUtils { return list; } + /** + * 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 getTeams(FederationModel registration) throws Exception { + String url = asLink(registration.url, registration.token, FederationRequest.PULL_TEAMS); + Collection models = JsonUtils.retrieveJson(url, TEAMS_TYPE); + List list = new ArrayList(models); + return list; + } + /** * Tries to pull the gitblit server settings from the remote gitblit * instance. diff --git a/tests/com/gitblit/tests/FederationTests.java b/tests/com/gitblit/tests/FederationTests.java index ed65100d..499c6109 100644 --- a/tests/com/gitblit/tests/FederationTests.java +++ b/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.AccessRestrictionType; 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); @@ -80,16 +87,73 @@ public class FederationTests { FederationProposalResult.NO_PROPOSALS); } + @Test + public void testJsonRepositories() throws Exception { + String requrl = FederationUtils.asLink(url, token, FederationRequest.PULL_REPOSITORIES); + String json = JsonUtils.retrieveJsonString(requrl, null, null); + 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 { - try { - String requrl = FederationUtils.asLink(url, "d7cc58921a80b37e0329a4dae2f9af38bf61ef5c", - FederationRequest.PULL_REPOSITORIES); - String json = JsonUtils.retrieveJsonString(requrl, null, null); - } catch (IOException e) { - if (!e.getMessage().contains("403")) { - throw e; - } - } + Map repos = FederationUtils.getRepositories(getRegistration(), + false); + assertNotNull(repos); + assertTrue(repos.size() > 0); + } + + @Test + public void testPullUsers() throws Exception { + List 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 teams = FederationUtils.getTeams(getRegistration()); + assertNotNull(teams); + assertTrue(teams.size() > 0); } } -- 2.39.5