Validate user permission to Git LFS lock operation

Change unifies project and user retrieval and uses common algorithm to
perform lock operation as it is built from the same blocks.

Change-Id: If958127aaeac9c3aafa8b30f20bfbd96c47fd78a
Signed-off-by: Jacek Centkowski <jcentkowski@collab.net>
diff --git a/src/main/java/com/googlesource/gerrit/plugins/lfs/LfsAuthUserProvider.java b/src/main/java/com/googlesource/gerrit/plugins/lfs/LfsAuthUserProvider.java
index aa39abb..ddf46f4 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/lfs/LfsAuthUserProvider.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/LfsAuthUserProvider.java
@@ -28,7 +28,7 @@
 import com.google.inject.Singleton;
 
 @Singleton
-class LfsAuthUserProvider {
+public class LfsAuthUserProvider {
   private static final String BASIC_AUTH_PREFIX = "Basic ";
 
   private final Provider<AnonymousUser> anonymous;
@@ -51,7 +51,7 @@
     this.userFactory = userFactory;
   }
 
-  CurrentUser getUser(String auth, String project, String operation) {
+  public CurrentUser getUser(String auth, String project, String operation) {
     if (!Strings.isNullOrEmpty(auth)) {
       if (auth.startsWith(BASIC_AUTH_PREFIX)) {
         return user.get();
diff --git a/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsGetLocksAction.java b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsGetLocksAction.java
index 4314f77..c08d937 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsGetLocksAction.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsGetLocksAction.java
@@ -19,13 +19,19 @@
 
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.project.ProjectState;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
+import com.googlesource.gerrit.plugins.lfs.LfsAuthUserProvider;
 import java.io.IOException;
 import java.util.Collections;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import org.eclipse.jgit.lfs.errors.LfsException;
+import org.eclipse.jgit.lfs.errors.LfsUnauthorized;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -38,23 +44,37 @@
   private static final Logger log = LoggerFactory.getLogger(LfsGetLocksAction.class);
 
   @Inject
-  LfsGetLocksAction(@Assisted LfsLocksContext context) {
-    super(context);
+  LfsGetLocksAction(
+      ProjectCache projectCache,
+      LfsAuthUserProvider userProvider,
+      @Assisted LfsLocksContext context) {
+    super(projectCache, userProvider, context);
   }
 
   @Override
-  protected void doRun() throws LfsException, IOException {
+  protected String getProjectName() throws LfsException {
     Matcher matcher = LFS_LOCKS_URL_PATTERN.matcher(context.path);
     if (matcher.matches()) {
-      String project = matcher.group(1);
-      listLocks(project);
+      return matcher.group(1);
     }
 
     throw new LfsException("no repository at " + context.path);
   }
 
-  private void listLocks(String project) throws IOException {
-    log.debug("Get list of locks for {} project", project);
+  @Override
+  protected void authorizeUser(ProjectControl control) throws LfsUnauthorized {
+    if (!control.isReadable()) {
+      throwUnauthorizedOp("list locks", control);
+    }
+  }
+
+  @Override
+  protected void doRun(ProjectState project, CurrentUser user) throws LfsException, IOException {
+    listLocks(project);
+  }
+
+  private void listLocks(ProjectState project) throws IOException {
+    log.debug("Get list of locks for {} project", project.getProject().getName());
     //TODO method stub for getting project's locks list
 
     // stub for searching lock by path
diff --git a/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLocksAction.java b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLocksAction.java
index 1408348..4384c6d 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLocksAction.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLocksAction.java
@@ -14,37 +14,101 @@
 
 package com.googlesource.gerrit.plugins.lfs.locks;
 
+import static com.google.gerrit.extensions.client.ProjectState.HIDDEN;
 import static org.apache.http.HttpStatus.SC_INTERNAL_SERVER_ERROR;
+import static org.apache.http.HttpStatus.SC_NOT_FOUND;
+import static org.apache.http.HttpStatus.SC_UNAUTHORIZED;
+import static org.eclipse.jgit.util.HttpSupport.HDR_AUTHORIZATION;
 
+import com.google.common.base.Strings;
+import com.google.gerrit.common.ProjectUtil;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.project.ProjectState;
+import com.googlesource.gerrit.plugins.lfs.LfsAuthUserProvider;
 import java.io.IOException;
 import org.eclipse.jgit.lfs.errors.LfsException;
+import org.eclipse.jgit.lfs.errors.LfsRepositoryNotFound;
+import org.eclipse.jgit.lfs.errors.LfsUnauthorized;
 import org.joda.time.DateTime;
 import org.joda.time.DateTimeZone;
 import org.joda.time.format.DateTimeFormatter;
 import org.joda.time.format.ISODateTimeFormat;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 abstract class LfsLocksAction {
   interface Factory<T extends LfsLocksAction> {
     T create(LfsLocksContext context);
   }
 
+  private static final Logger log = LoggerFactory.getLogger(LfsLocksAction.class);
   private static final DateTimeFormatter ISO = ISODateTimeFormat.dateTime();
+  /** Git LFS client uses 'upload' operation to authorize SSH Lock requests */
+  private static final String LFS_LOCKING_OPERATION = "upload";
 
+  protected final ProjectCache projectCache;
+  protected final LfsAuthUserProvider userProvider;
   protected final LfsLocksContext context;
 
-  protected LfsLocksAction(LfsLocksContext context) {
+  protected LfsLocksAction(
+      ProjectCache projectCache, LfsAuthUserProvider userProvider, LfsLocksContext context) {
+    this.projectCache = projectCache;
+    this.userProvider = userProvider;
     this.context = context;
   }
 
   public void run() throws IOException {
     try {
-      doRun();
+      String name = getProjectName();
+      ProjectState project = getProject(name);
+      CurrentUser user = getUser(name);
+      ProjectControl control = project.controlFor(user);
+      authorizeUser(control);
+      doRun(project, user);
+    } catch (LfsUnauthorized e) {
+      context.sendError(SC_UNAUTHORIZED, e.getMessage());
+    } catch (LfsRepositoryNotFound e) {
+      context.sendError(SC_NOT_FOUND, e.getMessage());
     } catch (LfsException e) {
       context.sendError(SC_INTERNAL_SERVER_ERROR, e.getMessage());
     }
   }
 
-  protected abstract void doRun() throws LfsException, IOException;
+  protected abstract String getProjectName() throws LfsException;
+
+  protected abstract void authorizeUser(ProjectControl control) throws LfsUnauthorized;
+
+  protected abstract void doRun(ProjectState project, CurrentUser user)
+      throws LfsException, IOException;
+
+  protected ProjectState getProject(String name) throws LfsRepositoryNotFound {
+    Project.NameKey project = Project.NameKey.parse(ProjectUtil.stripGitSuffix(name));
+    ProjectState state = projectCache.get(project);
+    if (state == null || state.getProject().getState() == HIDDEN) {
+      throw new LfsRepositoryNotFound(project.get());
+    }
+    return state;
+  }
+
+  protected CurrentUser getUser(String project) {
+    return userProvider.getUser(
+        context.getHeader(HDR_AUTHORIZATION), project, LFS_LOCKING_OPERATION);
+  }
+
+  protected void throwUnauthorizedOp(String op, ProjectControl control) throws LfsUnauthorized {
+    String project = control.getProject().getName();
+    String userName =
+        Strings.isNullOrEmpty(control.getUser().getUserName())
+            ? "anonymous"
+            : control.getUser().getUserName();
+    log.debug(
+        String.format(
+            "operation %s unauthorized for user %s on project %s", op, userName, project));
+    throw new LfsUnauthorized(op, project);
+  }
 
   protected String now() {
     return ISO.print(DateTime.now().toDateTime(DateTimeZone.UTC));
diff --git a/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLocksContext.java b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLocksContext.java
index 309c252..c58b6a7 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLocksContext.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLocksContext.java
@@ -78,6 +78,10 @@
     setLfsResponseType();
   }
 
+  String getHeader(String name) {
+    return req.getHeader(name);
+  }
+
   String getParam(String name) {
     return req.getParameter(name);
   }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsPutLocksAction.java b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsPutLocksAction.java
index c26a75c..e2e28f8 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsPutLocksAction.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsPutLocksAction.java
@@ -19,13 +19,20 @@
 import static com.googlesource.gerrit.plugins.lfs.locks.LfsGetLocksAction.LFS_LOCKS_URL_PATTERN;
 
 import com.google.common.base.Strings;
+import com.google.gerrit.common.data.Capable;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.project.ProjectState;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
+import com.googlesource.gerrit.plugins.lfs.LfsAuthUserProvider;
 import java.io.IOException;
 import java.util.Collections;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import org.eclipse.jgit.lfs.errors.LfsException;
+import org.eclipse.jgit.lfs.errors.LfsUnauthorized;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -36,69 +43,122 @@
   private static final Pattern LFS_VERIFICATION_URL_PATTERN =
       Pattern.compile(String.format(LFS_URL_REGEX_TEMPLATE, LFS_VERIFICATION_PATH));
 
+  protected LockAction action;
+
   @Inject
-  LfsPutLocksAction(@Assisted LfsLocksContext context) {
-    super(context);
+  LfsPutLocksAction(
+      ProjectCache projectCache,
+      LfsAuthUserProvider userProvider,
+      @Assisted LfsLocksContext context) {
+    super(projectCache, userProvider, context);
   }
 
   @Override
-  protected void doRun() throws LfsException, IOException {
+  protected String getProjectName() throws LfsException {
     Matcher matcher = LFS_LOCKS_URL_PATTERN.matcher(context.path);
     if (matcher.matches()) {
       String project = matcher.group(1);
       String lockId = matcher.group(2);
       if (Strings.isNullOrEmpty(lockId)) {
-        createLock(project, context);
+        action = new CreateLock();
       } else {
-        deleteLock(project, lockId, context);
+        action = new DeleteLock(lockId);
       }
-      return;
+      return project;
     }
 
     matcher = LFS_VERIFICATION_URL_PATTERN.matcher(context.path);
     if (matcher.matches()) {
-      verifyLocks(matcher.group(1), context);
-      return;
+      action = new VerifyLock();
+      return matcher.group(1);
     }
 
     throw new LfsException(String.format("Unsupported path %s was provided", context.path));
   }
 
-  private void verifyLocks(String project, LfsLocksContext action) throws IOException {
-    log.debug("Verify list of locks for {} project", project);
-    //TODO method stub for verifying locks
-    action.sendResponse(
-        new LfsVerifyLocksResponse(Collections.emptyList(), Collections.emptyList(), null));
+  @Override
+  protected void authorizeUser(ProjectControl control) throws LfsUnauthorized {
+    // all operations require push permission
+    if (Capable.OK != control.canPushToAtLeastOneRef()) {
+      throwUnauthorizedOp(action.getName(), control);
+    }
   }
 
-  private void deleteLock(String project, String lockId, LfsLocksContext action)
-      throws IOException {
-    LfsDeleteLockInput input = action.input(LfsDeleteLockInput.class);
-    log.debug(
-        "Delete (-f {}) lock for {} in project {}",
-        Boolean.TRUE.equals(input.force),
-        lockId,
-        project);
-    //TODO: this is just the method stub for lock deletion
-    LfsLock lock =
-        new LfsLock(
-            "random_id",
-            "some/path/to/file",
-            now(),
-            new LfsLockOwner("Lock Owner <lock_owner@example.com>"));
-    action.sendResponse(lock);
+  @Override
+  protected void doRun(ProjectState project, CurrentUser user) throws LfsException, IOException {
+    action.run(project, user);
   }
 
-  private void createLock(String project, LfsLocksContext action) throws IOException {
-    LfsCreateLockInput input = action.input(LfsCreateLockInput.class);
-    log.debug("Create lock for {} in project {}", input.path, project);
-    //TODO: this is just the method stub lock creation
-    LfsLock lock =
-        new LfsLock(
-            "random_id",
-            input.path,
-            now(),
-            new LfsLockOwner("Lock Owner <lock_owner@example.com>"));
-    action.sendResponse(lock);
+  private interface LockAction {
+    String getName();
+
+    void run(ProjectState project, CurrentUser user) throws LfsException, IOException;
+  }
+
+  private class CreateLock implements LockAction {
+    @Override
+    public String getName() {
+      return "create lock";
+    }
+
+    @Override
+    public void run(ProjectState project, CurrentUser user) throws LfsException, IOException {
+      LfsCreateLockInput input = context.input(LfsCreateLockInput.class);
+      log.debug("Create lock for {} in project {}", input.path, project);
+      //TODO: this is just the method stub lock creation
+      LfsLock lock =
+          new LfsLock(
+              "random_id",
+              input.path,
+              now(),
+              new LfsLockOwner("Lock Owner <lock_owner@example.com>"));
+      context.sendResponse(lock);
+    }
+  }
+
+  private class DeleteLock implements LockAction {
+    private final String lockId;
+
+    private DeleteLock(String lockId) {
+      this.lockId = lockId;
+    }
+
+    @Override
+    public String getName() {
+      return "delete lock";
+    }
+
+    @Override
+    public void run(ProjectState project, CurrentUser user) throws LfsException, IOException {
+      LfsDeleteLockInput input = context.input(LfsDeleteLockInput.class);
+      log.debug(
+          "Delete (-f {}) lock for {} in project {}",
+          Boolean.TRUE.equals(input.force),
+          lockId,
+          project);
+      //TODO: this is just the method stub for lock deletion
+      LfsLock lock =
+          new LfsLock(
+              lockId,
+              "some/path/to/file",
+              now(),
+              new LfsLockOwner("Lock Owner <lock_owner@example.com>"));
+      context.sendResponse(lock);
+    }
+  }
+
+  private class VerifyLock implements LockAction {
+    @Override
+    public String getName() {
+      return "verify lock";
+    }
+
+    @Override
+    public void run(ProjectState project, CurrentUser user) throws LfsException, IOException {
+      log.debug("Verify list of locks for {} project", project);
+      //TODO method stub for verifying locks
+      context.sendResponse(
+          new LfsVerifyLocksResponse(Collections.emptyList(), Collections.emptyList(), null));
+    }
   }
 }