Introduce FS backend expirationSeconds configuration parameter

By default auth token generated for LFS over FS operation times out
after 10s. Introduce configuration parameter so that one can adjust it
to particular needs.

Change-Id: Ibaf3bc9dfcd529f3f1901802fac8b1978e9d4db2
Signed-off-by: Jacek Centkowski <geminica.programs@gmail.com>
diff --git a/src/main/java/com/googlesource/gerrit/plugins/lfs/fs/LfsFsRequestAuthorizer.java b/src/main/java/com/googlesource/gerrit/plugins/lfs/fs/LfsFsRequestAuthorizer.java
index 41bb374..8b67591 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/lfs/fs/LfsFsRequestAuthorizer.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/fs/LfsFsRequestAuthorizer.java
@@ -51,27 +51,26 @@
   private static final String ALGORITHM = "AES";
   static final DateTimeFormatter DATE_TIME =
       DateTimeFormat.forPattern("YYYYMMDDHHmmss");
-  static final int TOKEN_TIMEOUT = 10;
 
   private final SecureRandom rndm;
   private final SecretKey key;
-  private final int timeout;
 
   @Inject
   LfsFsRequestAuthorizer() {
     this.rndm = new SecureRandom();
     this.key = generateKey();
-    this.timeout = TOKEN_TIMEOUT;
   }
 
-  public String generateToken(String operation, AnyLongObjectId id) {
+  public String generateToken(String operation, AnyLongObjectId id,
+      int expirationSeconds) {
     try {
-      byte [] initVector = new byte[IV_LENGTH];
+      byte[] initVector = new byte[IV_LENGTH];
       rndm.nextBytes(initVector);
       Cipher cipher = cipher(initVector, Cipher.ENCRYPT_MODE);
       return Base64.encodeBytes(Bytes.concat(initVector,
           cipher.doFinal(String.format("%s-%s-%s", operation,
-              id.name(), timeout()).getBytes(StandardCharsets.UTF_8))));
+              id.name(), timeout(expirationSeconds))
+              .getBytes(StandardCharsets.UTF_8))));
     } catch (GeneralSecurityException e) {
       log.error("Token generation failed with error", e);
       throw new RuntimeException(e);
@@ -112,8 +111,8 @@
     return true;
   }
 
-  private String timeout() {
-    return DATE_TIME.print(now().plusSeconds(timeout));
+  private String timeout(int expirationSeconds) {
+    return DATE_TIME.print(now().plusSeconds(expirationSeconds));
   }
 
   private DateTime now() {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/lfs/fs/LocalLargeFileRepository.java b/src/main/java/com/googlesource/gerrit/plugins/lfs/fs/LocalLargeFileRepository.java
index 2e9cd00..a810943 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/lfs/fs/LocalLargeFileRepository.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/fs/LocalLargeFileRepository.java
@@ -45,9 +45,11 @@
   public static final String CONTENT_PATH = "content";
   public static final String UPLOAD = "upload";
   public static final String DOWNLOAD = "download";
+  private static final int DEFAULT_TIMEOUT = 10; //in seconds
 
   private final String servletUrlPattern;
   private final LfsFsRequestAuthorizer authorizer;
+  private final int expirationSeconds;
 
   @Inject
   LocalLargeFileRepository(LfsConfigurationFactory configFactory,
@@ -60,6 +62,9 @@
             backend, defaultDataDir));
     this.authorizer = authorizer;
     this.servletUrlPattern = "/" + getContentPath(backend) + "*";
+    this.expirationSeconds = configFactory.getGlobalConfig()
+        .getInt(backend.type.name(), backend.name, "expirationSeconds",
+            DEFAULT_TIMEOUT);
   }
 
   public String getServletUrlPattern() {
@@ -70,7 +75,7 @@
   public Response.Action getDownloadAction(AnyLongObjectId id) {
     Response.Action action = super.getDownloadAction(id);
     action.header = Collections.singletonMap(HDR_AUTHORIZATION,
-        authorizer.generateToken(DOWNLOAD, id));
+        authorizer.generateToken(DOWNLOAD, id, expirationSeconds));
     return action;
   }
 
@@ -78,7 +83,7 @@
   public Response.Action getUploadAction(AnyLongObjectId id, long size) {
     Response.Action action = super.getUploadAction(id, size);
     action.header = Collections.singletonMap(HDR_AUTHORIZATION,
-        authorizer.generateToken(UPLOAD, id));
+        authorizer.generateToken(UPLOAD, id, expirationSeconds));
     return action;
   }
 
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index f6ece33..7cf8603 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -107,6 +107,14 @@
 : The directory in which to store data files. If not specified, defaults to
 the plugin's data folder: `$GERRIT_SITE/data/@PLUGIN@`.
 
+fs.expirationSeconds
+: Validity, in seconds, of authentication token for signed requests.
+Gerrit's LFS protocol handler signs requests to be issued by the git-lfs
+extension. This way the git-lfs extension doesn't need any credentials to
+access objects in the FS bucket. Validity of these request signatures expires
+after this period.
+: Default is `10` seconds.
+
 ### <a id="lfs-s3-backend"></a>Section `s3` - default S3 backend
 
 The following configuration options are only used when the backend is `s3`.
diff --git a/src/test/java/com/googlesource/gerrit/plugins/lfs/fs/LfsFsRequestAuthorizerTest.java b/src/test/java/com/googlesource/gerrit/plugins/lfs/fs/LfsFsRequestAuthorizerTest.java
index 75e9a56..8cab1a3 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/lfs/fs/LfsFsRequestAuthorizerTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/lfs/fs/LfsFsRequestAuthorizerTest.java
@@ -50,13 +50,13 @@
 
   @Test
   public void testVerifyAgainstToken() throws Exception {
-    String token = auth.generateToken("o", zeroId());
+    String token = auth.generateToken("o", zeroId(), 1);
     assertThat(auth.verifyAgainstToken(token, "o", zeroId())).isTrue();
   }
 
   @Test
   public void testVerifyAgainstInvalidToken() throws Exception {
-    String token = auth.generateToken("o", zeroId());
+    String token = auth.generateToken("o", zeroId(), 1);
     // replace 1st and 2nd token letters with each other
     assertThat(auth.verifyAgainstToken(
         token.substring(1, 2) + token.substring(0, 1) + token.substring(2), "o",
@@ -65,17 +65,24 @@
 
   @Test
   public void testVerifyAgainstDifferentOperation() throws Exception {
-    String token = auth.generateToken("o", zeroId());
+    String token = auth.generateToken("o", zeroId(), 1);
     assertThat(auth.verifyAgainstToken(token, "p", zeroId())).isFalse();
   }
 
   @Test
   public void testVerifyAgainstDifferentObjectId() throws Exception {
-    String token = auth.generateToken("o", zeroId());
+    String token = auth.generateToken("o", zeroId(), 1);
     assertThat(auth.verifyAgainstToken(token, "o",
         LongObjectId.fromString(
             "123456789012345678901234567890"
             + "123456789012345678901234567890"
             + "1234"))).isFalse();
   }
+
+  @Test
+  public void testVerifyAgainstExpiredToken() throws Exception {
+    // generate already expired token
+    String token = auth.generateToken("o", zeroId(), -1);
+    assertThat(auth.verifyAgainstToken(token, "o", zeroId())).isFalse();
+  }
 }