Auto-update secure.config OAuth token when needed.

GitHub OAuth can decide to expire an existing token
and generate a new one, based on time and requested
scope change: this may impact existing credentials
stored in secure.config and then block replication
to GitHub repos.

With this enhancement the secure.config is checked
at login and updated if needed.

NOTE: we could potentially have concurrency issues
(even if the update is a synchronised method) because
of replication auto-config with concurrent OAuth token 
updates. FileBasedConfig already takes care of File-level
locking on save.

Change-Id: I41fe6eb8020c2fdecad90232ddd27af5f8a995d7
diff --git a/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/ConcurrentFileBasedConfigWriteException.java b/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/ConcurrentFileBasedConfigWriteException.java
new file mode 100644
index 0000000..c620429
--- /dev/null
+++ b/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/ConcurrentFileBasedConfigWriteException.java
@@ -0,0 +1,24 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.googlesource.gerrit.plugins.github.oauth;
+
+import java.io.IOException;
+
+public class ConcurrentFileBasedConfigWriteException extends IOException {
+  private static final long serialVersionUID = -4019991986934953417L;
+
+  public ConcurrentFileBasedConfigWriteException(String message) {
+    super(message);
+  }
+}
diff --git a/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/GitHubOAuthConfig.java b/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/GitHubOAuthConfig.java
index 6645b69..a5a12ef 100644
--- a/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/GitHubOAuthConfig.java
+++ b/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/GitHubOAuthConfig.java
@@ -51,6 +51,8 @@
   public final String gitHubOAuthAccessTokenUrl;
   public final boolean enabled;
   public final List<OAuthProtocol.Scope> scopes;
+  public final int fileUpdateMaxRetryCount;
+  public final int fileUpdateMaxRetryIntervalMsec;
 
   @Inject
   public GitHubOAuthConfig(@GerritServerConfig Config config)
@@ -73,6 +75,9 @@
         config.getString("auth", null, "type").equalsIgnoreCase(
             AuthType.HTTP.toString());
     scopes = parseScopes(config.getString(CONF_SECTION, null, "scopes"));
+
+    fileUpdateMaxRetryCount = config.getInt(CONF_SECTION, "fileUpdateMaxRetryCount", 3);
+    fileUpdateMaxRetryIntervalMsec = config.getInt(CONF_SECTION, "fileUpdateMaxRetryIntervalMsec", 3000);
   }
 
   private String dropTrailingSlash(String url) {
@@ -87,7 +92,7 @@
         scopes.add(Enum.valueOf(Scope.class, scope));
       }
     }
-    
+
     return scopes;
   }
 
diff --git a/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/OAuthFilter.java b/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/OAuthFilter.java
index ac4340b..548847b 100644
--- a/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/OAuthFilter.java
+++ b/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/OAuthFilter.java
@@ -15,6 +15,10 @@
 
 import java.io.IOException;
 import java.net.HttpURLConnection;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Random;
+import java.util.Set;
 
 import javax.servlet.Filter;
 import javax.servlet.FilterChain;
@@ -28,10 +32,15 @@
 import javax.servlet.http.HttpSession;
 
 import org.apache.http.client.HttpClient;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
 import org.kohsuke.github.GHMyself;
+import org.kohsuke.github.GitHub;
 import org.slf4j.LoggerFactory;
 
 import com.google.common.base.Strings;
+import com.google.gerrit.server.config.SitePaths;
 import com.google.gson.Gson;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -45,14 +54,17 @@
   private final GitHubOAuthConfig config;
   private final OAuthCookieProvider cookieProvider;
   private final OAuthProtocol oauth;
+  private final Random retryRandom = new Random(System.currentTimeMillis());
+  private SitePaths sites;
 
   @Inject
-  public OAuthFilter(GitHubOAuthConfig config) {
+  public OAuthFilter(GitHubOAuthConfig config, SitePaths sites) {
     this.config = config;
     this.cookieProvider = new OAuthCookieProvider(new TokenCipher());
     HttpClient httpClient;
     httpClient = new GitHubHttpProvider().get();
     this.oauth = new OAuthProtocol(config, httpClient, new Gson());
+    this.sites = sites;
   }
 
   @Override
@@ -85,14 +97,19 @@
           || (authCookie == null && gerritCookie == null)) {
         if (oauth.isOAuthFinal(httpRequest)) {
 
+          GitHubLogin hubLogin = oauth.loginPhase2(httpRequest, httpResponse);
+          GitHub hub = hubLogin.hub;
           GHMyself myself =
-              oauth.loginPhase2(httpRequest, httpResponse).hub.getMyself();
+              hub.getMyself();
           String user = myself.getLogin();
           String email = myself.getEmail();
           String fullName =
               Strings.emptyToNull(myself.getName()) == null ? user : myself
                   .getName();
 
+          updateSecureConfigWithRetry(hub.getMyOrganizations().keySet(), user,
+              hubLogin.token.access_token);
+
           if (user != null) {
             OAuthCookie userCookie =
                 cookieProvider.getFromUser(user, email, fullName);
@@ -148,6 +165,80 @@
     }
   }
 
+  private void updateSecureConfigWithRetry(Set<String> organisations,
+      String user, String access_token) {
+    int retryCount = 0;
+
+    while (retryCount < config.fileUpdateMaxRetryCount) {
+      try {
+        updateSecureConfig(organisations, user, access_token);
+        return;
+      } catch (IOException e) {
+        retryCount++;
+        int retryInterval =
+            retryRandom.nextInt(config.fileUpdateMaxRetryIntervalMsec);
+        log.warn("Error whilst trying to update " + sites.secure_config
+            + (retryCount < config.fileUpdateMaxRetryCount ? ": attempt #" + retryCount + " will be retried after " + retryInterval + " msecs":""), e);
+        try {
+          Thread.sleep(retryInterval);
+        } catch (InterruptedException e1) {
+          log.error("Thread has been cancelled before retrying to save "
+              + sites.secure_config);
+          return;
+        }
+      } catch (ConfigInvalidException e) {
+        log.error("Cannot update " + sites.secure_config
+            + " as the file is corrupted", e);
+        return;
+      }
+    }
+  }
+
+  private synchronized void updateSecureConfig(Set<String> organisations,
+      String user, String access_token) throws IOException,
+      ConfigInvalidException {
+    FileBasedConfig currentSecureConfig =
+        new FileBasedConfig(sites.secure_config, FS.DETECTED);
+    long currentSecureConfigUpdateTs = sites.secure_config.lastModified();
+    currentSecureConfig.load();
+
+    boolean configUpdate = updateConfigSection(currentSecureConfig, user, user, access_token);
+    for (String organisation : organisations) {
+      configUpdate |= updateConfigSection(currentSecureConfig, organisation, user, access_token);
+    }
+
+    if(!configUpdate) {
+      return;
+    }
+
+    log.info("Updating " + sites.secure_config + " credentials for user " + user);
+
+    if (sites.secure_config.lastModified() != currentSecureConfigUpdateTs) {
+      throw new ConcurrentFileBasedConfigWriteException("File "
+          + sites.secure_config + " was written at "
+          + formatTS(sites.secure_config.lastModified())
+          + " while was trying to update security for user " + user);
+    }
+    currentSecureConfig.save();
+  }
+
+  private String formatTS(long ts) {
+    return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(ts));
+  }
+
+  private boolean updateConfigSection(FileBasedConfig config,
+      String section, String user, String password) {
+    String configUser = config.getString("remote", section, "username");
+    String configPassword = config.getString("remote", section, "password");
+    if(configUser == null || !configUser.equals(user) || configPassword.equals(password)) {
+      return false;
+    }
+
+    config.setString("remote", section, "username", user);
+    config.setString("remote", section, "password", password);
+    return true;
+  }
+
   private Cookie getGerritCookie(HttpServletRequest httpRequest) {
     for (Cookie cookie : httpRequest.getCookies()) {
       if (cookie.getName().equalsIgnoreCase(GERRIT_COOKIE_NAME)) {