@@ -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. |
@@ -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()) { |
@@ -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<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 @@ 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); | |||
} | |||
} | |||
} | |||
} | |||
} | |||
} |
@@ -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<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; | |||
} | |||
} | |||
@@ -96,6 +96,18 @@ public class UserModel implements Principal, Serializable, Comparable<UserModel> | |||
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; |
@@ -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<Collection<UserModel>>() { | |||
}.getType(); | |||
private static final Type TEAMS_TYPE = new TypeToken<Collection<TeamModel>>() { | |||
}.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<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. |
@@ -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<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); | |||
} | |||
} |