From 831469ba89ea8bca3bfbd1d662dbdd2c9f233798 Mon Sep 17 00:00:00 2001 From: James Moger Date: Mon, 12 Sep 2011 15:37:55 -0400 Subject: Largely completed, uber-cool federation feature. --- src/com/gitblit/GitBlit.java | 447 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 432 insertions(+), 15 deletions(-) (limited to 'src/com/gitblit/GitBlit.java') diff --git a/src/com/gitblit/GitBlit.java b/src/com/gitblit/GitBlit.java index c54fbe14..7c499698 100644 --- a/src/com/gitblit/GitBlit.java +++ b/src/com/gitblit/GitBlit.java @@ -16,6 +16,7 @@ package com.gitblit; import java.io.File; +import java.io.FileFilter; import java.io.IOException; import java.lang.reflect.Field; import java.text.MessageFormat; @@ -25,8 +26,14 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import javax.mail.Message; +import javax.mail.MessagingException; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; import javax.servlet.http.Cookie; @@ -46,10 +53,17 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.gitblit.Constants.AccessRestrictionType; +import com.gitblit.Constants.FederationRequest; +import com.gitblit.Constants.FederationStrategy; +import com.gitblit.Constants.FederationToken; +import com.gitblit.models.FederationModel; +import com.gitblit.models.FederationProposal; import com.gitblit.models.RepositoryModel; import com.gitblit.models.UserModel; import com.gitblit.utils.JGitUtils; import com.gitblit.utils.StringUtils; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; /** * GitBlit is the servlet context listener singleton that acts as the core for @@ -73,6 +87,13 @@ public class GitBlit implements ServletContextListener { private final Logger logger = LoggerFactory.getLogger(GitBlit.class); + private final ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(5); + + private final List federationRegistrations = Collections + .synchronizedList(new ArrayList()); + + private final Map federationPullResults = new ConcurrentHashMap(); + private RepositoryResolver repositoryResolver; private File repositoriesFolder; @@ -83,6 +104,8 @@ public class GitBlit implements ServletContextListener { private IStoredSettings settings; + private MailExecutor mailExecutor; + public GitBlit() { if (gitblit == null) { // set the static singleton reference @@ -102,6 +125,15 @@ public class GitBlit implements ServletContextListener { return gitblit; } + /** + * Determine if this is the GO variant of Gitblit. + * + * @return true if this is the GO variant of Gitblit. + */ + public static boolean isGO() { + return self().settings instanceof FileSettings; + } + /** * Returns the boolean value for the specified key. If the key does not * exist or the value for the key can not be interpreted as a boolean, the @@ -226,6 +258,30 @@ public class GitBlit implements ServletContextListener { * @return a user object or null */ public UserModel authenticate(String username, char[] password) { + if (StringUtils.isEmpty(username)) { + // can not authenticate empty username + return null; + } + String pw = new String(password); + if (StringUtils.isEmpty(pw)) { + // can not authenticate empty password + return null; + } + + // check to see if this is the federation user + if (canFederate()) { + if (username.equalsIgnoreCase(Constants.FEDERATION_USER)) { + List tokens = getFederationTokens(); + if (tokens.contains(pw)) { + // the federation user is an administrator + UserModel federationUser = new UserModel(Constants.FEDERATION_USER); + federationUser.canAdmin = true; + return federationUser; + } + } + } + + // delegate authentication to the user service if (userService == null) { return null; } @@ -463,6 +519,10 @@ public class GitBlit implements ServletContextListener { model.showRemoteBranches = getConfig(config, "showRemoteBranches", false); model.isFrozen = getConfig(config, "isFrozen", false); model.showReadme = getConfig(config, "showReadme", false); + model.federationStrategy = FederationStrategy.fromName(getConfig(config, + "federationStrategy", null)); + model.isFederated = getConfig(config, "isFederated", false); + model.origin = config.getString("remote", "origin", "url"); } r.close(); return model; @@ -614,25 +674,39 @@ public class GitBlit implements ServletContextListener { // update settings if (r != null) { - StoredConfig config = JGitUtils.readConfig(r); - config.setString("gitblit", null, "description", repository.description); - config.setString("gitblit", null, "owner", repository.owner); - config.setBoolean("gitblit", null, "useTickets", repository.useTickets); - config.setBoolean("gitblit", null, "useDocs", repository.useDocs); - config.setString("gitblit", null, "accessRestriction", - repository.accessRestriction.name()); - config.setBoolean("gitblit", null, "showRemoteBranches", repository.showRemoteBranches); - config.setBoolean("gitblit", null, "isFrozen", repository.isFrozen); - config.setBoolean("gitblit", null, "showReadme", repository.showReadme); - try { - config.save(); - } catch (IOException e) { - logger.error("Failed to save repository config!", e); - } + updateConfiguration(r, repository); r.close(); } } + /** + * Updates the Gitblit configuration for the specified repository. + * + * @param r + * the Git repository + * @param repository + * the Gitblit repository model + */ + public void updateConfiguration(Repository r, RepositoryModel repository) { + StoredConfig config = JGitUtils.readConfig(r); + config.setString("gitblit", null, "description", repository.description); + config.setString("gitblit", null, "owner", repository.owner); + config.setBoolean("gitblit", null, "useTickets", repository.useTickets); + config.setBoolean("gitblit", null, "useDocs", repository.useDocs); + config.setString("gitblit", null, "accessRestriction", repository.accessRestriction.name()); + config.setBoolean("gitblit", null, "showRemoteBranches", repository.showRemoteBranches); + config.setBoolean("gitblit", null, "isFrozen", repository.isFrozen); + config.setBoolean("gitblit", null, "showReadme", repository.showReadme); + config.setString("gitblit", null, "federationStrategy", + repository.federationStrategy.name()); + config.setBoolean("gitblit", null, "isFederated", repository.isFederated); + try { + config.save(); + } catch (IOException e) { + logger.error("Failed to save repository config!", e); + } + } + /** * Deletes the repository from the file system and removes the repository * permission from all repository users. @@ -710,6 +784,341 @@ public class GitBlit implements ServletContextListener { return html; } + /** + * Returns Gitblit's scheduled executor service for scheduling tasks. + * + * @return scheduledExecutor + */ + public ScheduledExecutorService executor() { + return scheduledExecutor; + } + + public static boolean canFederate() { + String uuid = getString(Keys.federation.uuid, ""); + return !StringUtils.isEmpty(uuid); + } + + /** + * Configures this Gitblit instance to pull any registered federated gitblit + * instances. + */ + private void configureFederation() { + boolean validUuid = true; + String uuid = settings.getString(Keys.federation.uuid, ""); + if (StringUtils.isEmpty(uuid)) { + logger.warn("Federation UUID is blank! This server can not be PULLED from."); + validUuid = false; + } + if (validUuid) { + for (FederationToken tokenType : FederationToken.values()) { + logger.info(MessageFormat.format("Federation {0} token = {1}", tokenType.name(), + getFederationToken(tokenType))); + } + } + + // Schedule the federation executor + List registrations = getFederationRegistrations(); + if (registrations.size() > 0) { + scheduledExecutor.schedule(new FederationPullExecutor(registrations), 1, + TimeUnit.MINUTES); + } + } + + /** + * Returns the list of federated gitblit instances that this instance will + * try to pull. + * + * @return list of registered gitblit instances + */ + public List getFederationRegistrations() { + if (federationRegistrations.isEmpty()) { + List keys = settings.getAllKeys(Keys.federation._ROOT); + keys.remove(Keys.federation.name); + keys.remove(Keys.federation.uuid); + keys.remove(Keys.federation.allowProposals); + keys.remove(Keys.federation.proposalsFolder); + keys.remove(Keys.federation.defaultFrequency); + Collections.sort(keys); + Map federatedModels = new HashMap(); + for (String key : keys) { + String value = key.substring(Keys.federation._ROOT.length() + 1); + List values = StringUtils.getStringsFromValue(value, "\\."); + String server = values.get(0); + if (!federatedModels.containsKey(server)) { + federatedModels.put(server, new FederationModel(server)); + } + String setting = values.get(1); + if (setting.equals("url")) { + // url of the remote Gitblit instance + federatedModels.get(server).url = settings.getString(key, ""); + } else if (setting.equals("token")) { + // token for the remote Gitblit instance + federatedModels.get(server).token = settings.getString(key, ""); + } else if (setting.equals("frequency")) { + // frequency of the pull operation + federatedModels.get(server).frequency = settings.getString(key, ""); + } else if (setting.equals("folder")) { + // destination folder of the pull operation + federatedModels.get(server).folder = settings.getString(key, ""); + } else if (setting.equals("mergeAccounts")) { + // merge remote accounts into local accounts + federatedModels.get(server).mergeAccounts = settings.getBoolean(key, false); + } else if (setting.equals("sendStatus")) { + // send a status acknowledgment to source Gitblit instance + // at end of git pull + federatedModels.get(server).sendStatus = settings.getBoolean(key, false); + } else if (setting.equals("notifyOnError")) { + // notify administrators on federation pull failures + federatedModels.get(server).notifyOnError = settings.getBoolean(key, false); + } else if (setting.equals("exclude")) { + // excluded repositories + federatedModels.get(server).exclusions = settings.getStrings(key); + } else if (setting.equals("include")) { + // included repositories + federatedModels.get(server).inclusions = settings.getStrings(key); + } + } + + // verify that registrations have a url and a token + for (FederationModel model : federatedModels.values()) { + if (StringUtils.isEmpty(model.url)) { + logger.warn(MessageFormat.format( + "Dropping federation registration {0}. Missing url.", model.name)); + continue; + } + if (StringUtils.isEmpty(model.token)) { + logger.warn(MessageFormat.format( + "Dropping federation registration {0}. Missing token.", model.name)); + continue; + } + // set default frequency if unspecified + if (StringUtils.isEmpty(model.frequency)) { + model.frequency = settings.getString(Keys.federation.defaultFrequency, + "60 mins"); + } + federationRegistrations.add(model); + } + } + return federationRegistrations; + } + + /** + * Retrieve the specified federation registration. + * + * @param name + * the name of the registration + * @return a federation registration + */ + public FederationModel getFederationRegistration(String url, String name) { + // check registrations + for (FederationModel r : getFederationRegistrations()) { + if (r.name.equals(name) && r.url.equals(url)) { + return r; + } + } + + // check the results + for (FederationModel r : getFederationResultRegistrations()) { + if (r.name.equals(name) && r.url.equals(url)) { + return r; + } + } + return null; + } + + /** + * Returns the list of possible federation tokens for this Gitblit instance. + * + * @return list of federation tokens + */ + public List getFederationTokens() { + List tokens = new ArrayList(); + for (FederationToken type : FederationToken.values()) { + tokens.add(getFederationToken(type)); + } + return tokens; + } + + /** + * Returns the specified federation token for this Gitblit instance. + * + * @param type + * @return a federation token + */ + public String getFederationToken(FederationToken type) { + String uuid = settings.getString(Keys.federation.uuid, ""); + return StringUtils.getSHA1(uuid + "-" + type.name()); + } + + /** + * Compares the provided token with this Gitblit instance's tokens and + * determines if the requested permission may be granted to the token. + * + * @param req + * @param token + * @return true if the request can be executed + */ + public boolean validateFederationRequest(FederationRequest req, String token) { + String all = getFederationToken(FederationToken.ALL); + String unr = getFederationToken(FederationToken.USERS_AND_REPOSITORIES); + String jur = getFederationToken(FederationToken.REPOSITORIES); + switch (req) { + case PULL_REPOSITORIES: + return token.equals(all) || token.equals(unr) || token.equals(jur); + case PULL_USERS: + return token.equals(all) || token.equals(unr); + case PULL_SETTINGS: + return token.equals(all); + } + return false; + } + + /** + * Acknowledge and cache the status of a remote Gitblit instance. + * + * @param identification + * the identification of the pulling Gitblit instance + * @param registration + * the registration from the pulling Gitblit instance + * @return true if acknowledged + */ + public boolean acknowledgeFederationStatus(String identification, FederationModel registration) { + // reset the url to the identification of the pulling Gitblit instance + registration.url = identification; + String id = identification; + if (!StringUtils.isEmpty(registration.folder)) { + id += "-" + registration.folder; + } + federationPullResults.put(id, registration); + return true; + } + + /** + * Returns the list of registration results. + * + * @return the list of registration results + */ + public List getFederationResultRegistrations() { + return new ArrayList(federationPullResults.values()); + } + + /** + * Submit a federation proposal. The proposal is cached locally and the + * Gitblit administrator(s) are notified via email. + * + * @param proposal + * the proposal + * @param gitblitUrl + * the url of your gitblit instance + * @return true if the proposal was submitted + */ + public boolean submitFederationProposal(FederationProposal proposal, String gitblitUrl) { + // convert proposal to json + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + String json = gson.toJson(proposal); + + try { + // make the proposals folder + File proposalsFolder = new File(getString(Keys.federation.proposalsFolder, "proposals") + .trim()); + proposalsFolder.mkdirs(); + + // cache json to a file + File file = new File(proposalsFolder, proposal.token + Constants.PROPOSAL_EXT); + com.gitblit.utils.FileUtils.writeContent(file, json); + } catch (Exception e) { + logger.error(MessageFormat.format("Failed to cache proposal from {0}", proposal.url), e); + } + + // send an email, if possible + try { + Message message = mailExecutor.createMessageForAdministrators(); + if (message != null) { + message.setSubject("Federation proposal from " + proposal.url); + message.setText("Please review the proposal @ " + gitblitUrl + "/proposal/" + + proposal.token); + mailExecutor.queue(message); + } + } catch (Throwable t) { + logger.error("Failed to notify administrators of proposal", t); + } + return true; + } + + /** + * Returns the list of pending federation proposals + * + * @return list of federation proposals + */ + public List getPendingFederationProposals() { + List list = new ArrayList(); + File folder = new File(getString(Keys.federation.proposalsFolder, "proposals").trim()); + if (folder.exists()) { + File[] files = folder.listFiles(new FileFilter() { + @Override + public boolean accept(File file) { + return file.isFile() + && file.getName().toLowerCase().endsWith(Constants.PROPOSAL_EXT); + } + }); + Gson gson = new Gson(); + for (File file : files) { + String json = com.gitblit.utils.FileUtils.readContent(file, null); + FederationProposal proposal = gson.fromJson(json, FederationProposal.class); + list.add(proposal); + } + } + return list; + } + + /** + * Returns the proposal identified by the supplied token. + * + * @param token + * @return the specified proposal or null + */ + public FederationProposal getPendingFederationProposal(String token) { + List list = getPendingFederationProposals(); + for (FederationProposal proposal : list) { + if (proposal.token.equals(token)) { + return proposal; + } + } + return null; + } + + /** + * Deletes a pending federation proposal. + * + * @param a + * proposal + * @return true if the proposal was deleted + */ + public boolean deletePendingFederationProposal(FederationProposal proposal) { + File folder = new File(getString(Keys.federation.proposalsFolder, "proposals").trim()); + File file = new File(folder, proposal.token + Constants.PROPOSAL_EXT); + return file.delete(); + } + + /** + * Notify the administrators by email. + * + * @param subject + * @param message + */ + public void notifyAdministrators(String subject, String message) { + try { + Message mail = mailExecutor.createMessageForAdministrators(); + if (mail != null) { + mail.setSubject(subject); + mail.setText(message); + mailExecutor.queue(mail); + } + } catch (MessagingException e) { + logger.error("Messaging error", e); + } + } + /** * Configure the Gitblit singleton with the specified settings source. This * source may be file settings (Gitblit GO) or may be web.xml settings @@ -746,6 +1155,13 @@ public class GitBlit implements ServletContextListener { loginService = new FileUserService(realmFile); } setUserService(loginService); + configureFederation(); + mailExecutor = new MailExecutor(settings); + if (mailExecutor.isReady()) { + scheduledExecutor.scheduleAtFixedRate(mailExecutor, 1, 2, TimeUnit.MINUTES); + } else { + logger.warn("Mail server is not properly configured. Mail services disabled."); + } } /** @@ -770,5 +1186,6 @@ public class GitBlit implements ServletContextListener { @Override public void contextDestroyed(ServletContextEvent contextEvent) { logger.info("Gitblit context destroyed by servlet container."); + scheduledExecutor.shutdownNow(); } } -- cgit v1.2.3