From 997c16d6826cfa1bef33ba08e15055cc407b9398 Mon Sep 17 00:00:00 2001
From: James Moger <james.moger@gitblit.com>
Date: Tue, 13 Dec 2011 17:36:58 -0500
Subject: [PATCH] Federation support for Teams

---
 src/com/gitblit/FederationServlet.java       |   20 ++++++
 docs/02_federation.mkd                       |   10 ++-
 src/com/gitblit/FederationPullExecutor.java  |   32 ++++++++++
 tests/com/gitblit/tests/FederationTests.java |   84 ++++++++++++++++++++++++---
 src/com/gitblit/models/UserModel.java        |   12 ++++
 src/com/gitblit/Constants.java               |    2 
 src/com/gitblit/utils/FederationUtils.java   |   18 ++++++
 7 files changed, 160 insertions(+), 18 deletions(-)

diff --git a/docs/02_federation.mkd b/docs/02_federation.mkd
index a592c1e..6525000 100644
--- a/docs/02_federation.mkd
+++ b/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.  
diff --git a/src/com/gitblit/Constants.java b/src/com/gitblit/Constants.java
index 3279980..c2d5eb2 100644
--- a/src/com/gitblit/Constants.java
+++ b/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()) {
diff --git a/src/com/gitblit/FederationPullExecutor.java b/src/com/gitblit/FederationPullExecutor.java
index 20fd67c..c84761b 100644
--- a/src/com/gitblit/FederationPullExecutor.java
+++ b/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);
+								}
+							}
+						}
 					}
 				}
 			}
diff --git a/src/com/gitblit/FederationServlet.java b/src/com/gitblit/FederationServlet.java
index 0be1066..f2ed903 100644
--- a/src/com/gitblit/FederationServlet.java
+++ b/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;
@@ -90,7 +91,7 @@
 			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 @@
 					}
 				}
 				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;
 			}
 		}
 
diff --git a/src/com/gitblit/models/UserModel.java b/src/com/gitblit/models/UserModel.java
index bd8974d..ecb97cf 100644
--- a/src/com/gitblit/models/UserModel.java
+++ b/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;
diff --git a/src/com/gitblit/utils/FederationUtils.java b/src/com/gitblit/utils/FederationUtils.java
index 324aa67..8207962 100644
--- a/src/com/gitblit/utils/FederationUtils.java
+++ b/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.
 	 * 
diff --git a/tests/com/gitblit/tests/FederationTests.java b/tests/com/gitblit/tests/FederationTests.java
index ed65100..499c610 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.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 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<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);
 	}
 }

--
Gitblit v1.9.1