Merge branch 'stable-2.13'
* stable-2.13:
Introduce authorization token for SSH requests
Authorize Git LFS HTTP requests
Refactor auth token generation and handling
Change token data delimiter to `~`
Fix Amazon S3 backend links in configuration doc
LfsSshAuth: Prevent repeated slash in response href
Change-Id: Iab1bf1cb394b05ca922df1c4dbd804ad4725e851
diff --git a/src/main/java/com/googlesource/gerrit/plugins/lfs/AuthInfo.java b/src/main/java/com/googlesource/gerrit/plugins/lfs/AuthInfo.java
new file mode 100644
index 0000000..187ce6f
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/AuthInfo.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2017 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.lfs;
+
+public class AuthInfo {
+ public final String authToken;
+ public final String expiresAt;
+
+ public AuthInfo(String authToken, String expiresAt) {
+ this.authToken = authToken;
+ this.expiresAt = expiresAt;
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/lfs/ExpiringAction.java b/src/main/java/com/googlesource/gerrit/plugins/lfs/ExpiringAction.java
new file mode 100644
index 0000000..48bf7d7
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/ExpiringAction.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2017 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.lfs;
+
+import static org.eclipse.jgit.util.HttpSupport.HDR_AUTHORIZATION;
+
+import org.eclipse.jgit.lfs.server.Response;
+
+import java.util.Collections;
+
+public class ExpiringAction extends Response.Action {
+ public final String expiresAt;
+
+ public ExpiringAction(String href, AuthInfo info) {
+ this.href = href;
+ this.header = Collections.singletonMap(HDR_AUTHORIZATION, info.authToken);
+ this.expiresAt = info.expiresAt;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/googlesource/gerrit/plugins/lfs/LfsApiServlet.java b/src/main/java/com/googlesource/gerrit/plugins/lfs/LfsApiServlet.java
index dfc7f58..67fe512 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/lfs/LfsApiServlet.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/LfsApiServlet.java
@@ -18,9 +18,13 @@
import static com.google.gerrit.extensions.client.ProjectState.READ_ONLY;
import static com.google.gerrit.httpd.plugins.LfsPluginServlet.URL_REGEX;
+import com.google.common.base.Strings;
import com.google.gerrit.common.ProjectUtil;
+import com.google.gerrit.common.data.Capable;
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.google.inject.Inject;
import com.google.inject.Singleton;
@@ -28,50 +32,59 @@
import org.eclipse.jgit.lfs.errors.LfsException;
import org.eclipse.jgit.lfs.errors.LfsRepositoryNotFound;
import org.eclipse.jgit.lfs.errors.LfsRepositoryReadOnly;
+import org.eclipse.jgit.lfs.errors.LfsUnauthorized;
import org.eclipse.jgit.lfs.errors.LfsUnavailable;
import org.eclipse.jgit.lfs.errors.LfsValidationError;
import org.eclipse.jgit.lfs.server.LargeFileRepository;
+import org.eclipse.jgit.lfs.server.LfsGerritProtocolServlet;
import org.eclipse.jgit.lfs.server.LfsObject;
-import org.eclipse.jgit.lfs.server.LfsProtocolServlet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Singleton
-public class LfsApiServlet extends LfsProtocolServlet {
+public class LfsApiServlet extends LfsGerritProtocolServlet {
private static final long serialVersionUID = 1L;
private static final Pattern URL_PATTERN = Pattern.compile(URL_REGEX);
+ private static final String DOWNLOAD = "download";
+ private static final String UPLOAD = "upload";
private final ProjectCache projectCache;
private final LfsConfigurationFactory lfsConfigFactory;
private final LfsRepositoryResolver repoResolver;
+ private final LfsAuthUserProvider userProvider;
@Inject
LfsApiServlet(ProjectCache projectCache,
LfsConfigurationFactory lfsConfigFactory,
- LfsRepositoryResolver repoResolver) {
+ LfsRepositoryResolver repoResolver,
+ LfsAuthUserProvider userProvider) {
this.projectCache = projectCache;
this.lfsConfigFactory = lfsConfigFactory;
this.repoResolver = repoResolver;
+ this.userProvider = userProvider;
}
@Override
protected LargeFileRepository getLargeFileRepository(
- LfsRequest request, String path) throws LfsException {
+ LfsRequest request, String path, String auth)
+ throws LfsException {
String pathInfo = path.startsWith("/") ? path : "/" + path;
Matcher matcher = URL_PATTERN.matcher(pathInfo);
if (!matcher.matches()) {
throw new LfsException("no repository at " + pathInfo);
}
+ String projName = matcher.group(1);
Project.NameKey project = Project.NameKey.parse(
- ProjectUtil.stripGitSuffix(matcher.group(1)));
+ ProjectUtil.stripGitSuffix(projName));
ProjectState state = projectCache.get(project);
-
if (state == null || state.getProject().getState() == HIDDEN) {
throw new LfsRepositoryNotFound(project.get());
}
+ authorizeUser(userProvider.getUser(auth, projName, request.getOperation()),
+ state, request.getOperation());
- if (request.getOperation().equals("upload")
+ if (request.getOperation().equals(UPLOAD)
&& state.getProject().getState() == READ_ONLY) {
throw new LfsRepositoryReadOnly(project.get());
}
@@ -82,7 +95,7 @@
// No config means we default to "not enabled".
if (config != null && config.isEnabled()) {
// For uploads, check object sizes against limit if configured
- if (request.getOperation().equals("upload")) {
+ if (request.getOperation().equals(UPLOAD)) {
if (config.isReadOnly()) {
throw new LfsRepositoryReadOnly(project.get());
}
@@ -104,4 +117,17 @@
throw new LfsUnavailable(project.get());
}
+
+ private void authorizeUser(CurrentUser user, ProjectState state,
+ String operation) throws LfsUnauthorized {
+ ProjectControl control = state.controlFor(user);
+ if ((operation.equals(DOWNLOAD) && !control.isReadable()) ||
+ (operation.equals(UPLOAD) && Capable.OK != control.canPushToAtLeastOneRef())) {
+ throw new LfsUnauthorized(
+ String.format("User %s is not authorized to perform %s operation",
+ Strings.isNullOrEmpty(user.getUserName())
+ ? "anonymous" : user.getUserName(),
+ operation.toLowerCase()));
+ }
+ }
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/lfs/LfsAuthToken.java b/src/main/java/com/googlesource/gerrit/plugins/lfs/LfsAuthToken.java
new file mode 100644
index 0000000..1ab35c4
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/LfsAuthToken.java
@@ -0,0 +1,93 @@
+//Copyright (C) 2017 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.lfs;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Optional;
+import com.google.common.base.Splitter;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.joda.time.format.DateTimeFormatter;
+import org.joda.time.format.ISODateTimeFormat;
+
+import java.util.List;
+
+public abstract class LfsAuthToken {
+ public static abstract class Processor<T extends LfsAuthToken> {
+ private static final char DELIMETER = '~';
+
+ protected final LfsCipher cipher;
+
+ protected Processor(LfsCipher cipher) {
+ this.cipher = cipher;
+ }
+
+ public String serialize(T token) {
+ return cipher.encrypt(Joiner.on(DELIMETER).join(getValues(token)));
+ }
+
+ public Optional<T> deserialize(String input) {
+ Optional<String> decrypted = cipher.decrypt(input);
+ if (!decrypted.isPresent()) {
+ return Optional.absent();
+ }
+
+ return createToken(Splitter.on(DELIMETER).splitToList(decrypted.get()));
+ }
+
+ protected abstract List<String> getValues(T token);
+ protected abstract Optional<T> createToken(List<String> values);
+ }
+
+ public static abstract class Verifier<T extends LfsAuthToken> {
+ protected final T token;
+
+ protected Verifier(T token) {
+ this.token = token;
+ }
+
+ public boolean verify() {
+ return onTime(token.expiresAt)
+ && verifyTokenValues();
+ }
+
+ protected abstract boolean verifyTokenValues();
+
+ static boolean onTime(String dateTime) {
+ String now = LfsAuthToken.ISO.print(now());
+ return now.compareTo(dateTime) <= 0;
+ }
+ }
+
+ static final DateTimeFormatter ISO = ISODateTimeFormat.dateTime();
+ public final String expiresAt;
+
+ protected LfsAuthToken(int expirationSeconds) {
+ this(timeout(expirationSeconds));
+ }
+
+ protected LfsAuthToken(String expiresAt) {
+ this.expiresAt = expiresAt;
+ }
+
+ static String timeout(int expirationSeconds) {
+ return LfsAuthToken.ISO.print(now().plusSeconds(expirationSeconds));
+ }
+
+ static DateTime now() {
+ return DateTime.now().toDateTime(DateTimeZone.UTC);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/googlesource/gerrit/plugins/lfs/LfsAuthUserProvider.java b/src/main/java/com/googlesource/gerrit/plugins/lfs/LfsAuthUserProvider.java
new file mode 100644
index 0000000..d1b3207
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/LfsAuthUserProvider.java
@@ -0,0 +1,76 @@
+// Copyright (C) 2017 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.lfs;
+
+import static com.googlesource.gerrit.plugins.lfs.LfsSshRequestAuthorizer.SSH_AUTH_PREFIX;
+
+import com.google.common.base.Optional;
+import com.google.common.base.Strings;
+import com.google.gerrit.server.AnonymousUser;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.config.AuthConfig;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+@Singleton
+class LfsAuthUserProvider {
+ private static final String BASIC_AUTH_PREFIX = "Basic ";
+
+ private final Provider<AnonymousUser> anonymous;
+ private final Provider<CurrentUser> user;
+ private final AuthConfig authCfg;
+ private final LfsSshRequestAuthorizer sshAuth;
+ private final AccountCache accounts;
+ private final IdentifiedUser.GenericFactory userFactory;
+
+ @Inject
+ LfsAuthUserProvider(Provider<AnonymousUser> anonymous,
+ Provider<CurrentUser> user,
+ AuthConfig authCfg,
+ LfsSshRequestAuthorizer sshAuth,
+ AccountCache accounts,
+ IdentifiedUser.GenericFactory userFactory) {
+ this.anonymous = anonymous;
+ this.user = user;
+ this.authCfg = authCfg;
+ this.sshAuth = sshAuth;
+ this.accounts = accounts;
+ this.userFactory = userFactory;
+ }
+
+ CurrentUser getUser(String auth, String project, String operation) {
+ if (!Strings.isNullOrEmpty(auth)) {
+ if (auth.startsWith(BASIC_AUTH_PREFIX) && authCfg.isGitBasicAuth()) {
+ return user.get();
+ }
+
+ if (auth.startsWith(SSH_AUTH_PREFIX)) {
+ Optional<String> user = sshAuth.getUserFromValidToken(
+ auth.substring(SSH_AUTH_PREFIX.length()), project, operation);
+ if (user.isPresent()) {
+ AccountState acc = accounts.getByUsername(user.get());
+ if (acc != null) {
+ return userFactory.create(acc);
+ }
+ }
+ }
+ }
+ return anonymous.get();
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/lfs/LfsCipher.java b/src/main/java/com/googlesource/gerrit/plugins/lfs/LfsCipher.java
new file mode 100644
index 0000000..25238da
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/LfsCipher.java
@@ -0,0 +1,108 @@
+// Copyright (C) 2017 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.lfs;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.base.Optional;
+import com.google.common.base.Strings;
+import com.google.common.primitives.Bytes;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.util.Base64;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.security.AlgorithmParameters;
+import java.security.GeneralSecurityException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Arrays;
+
+import javax.crypto.Cipher;
+import javax.crypto.KeyGenerator;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.IvParameterSpec;
+
+@Singleton
+public class LfsCipher {
+ private static final Logger log =
+ LoggerFactory.getLogger(LfsCipher.class);
+ private static final int IV_LENGTH = 16;
+ private static final String ALGORITHM = "AES";
+ private static final String CIPHER_TYPE = ALGORITHM + "/CBC/PKCS5PADDING";
+ private static final int KEY_SIZE = 128;
+
+ private final SecureRandom random;
+ private final SecretKey key;
+
+ public LfsCipher() {
+ this.random = new SecureRandom();
+ this.key = generateKey();
+ }
+
+ public String encrypt(String input) {
+ try {
+ byte[] initVector = new byte[IV_LENGTH];
+ random.nextBytes(initVector);
+ Cipher cipher = cipher(initVector, Cipher.ENCRYPT_MODE);
+ return Base64.encodeBytes(
+ Bytes.concat(initVector, cipher.doFinal(input.getBytes(UTF_8))));
+ } catch (GeneralSecurityException e) {
+ log.error("Token generation failed with error", e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ public Optional<String> decrypt(String input) {
+ if (Strings.isNullOrEmpty(input)) {
+ return Optional.absent();
+ }
+
+ byte[] bytes = Base64.decode(input);
+ byte[] initVector = Arrays.copyOf(bytes, IV_LENGTH);
+ try {
+ Cipher cipher = cipher(initVector, Cipher.DECRYPT_MODE);
+ return Optional.of(new String(
+ cipher.doFinal(Arrays.copyOfRange(bytes, IV_LENGTH, bytes.length)),
+ UTF_8));
+ } catch (GeneralSecurityException e) {
+ log.error("Exception was thrown during token verification", e);
+ }
+
+ return Optional.absent();
+ }
+
+ private Cipher cipher(byte[] initVector, int mode)
+ throws GeneralSecurityException {
+ IvParameterSpec spec = new IvParameterSpec(initVector);
+ AlgorithmParameters params = AlgorithmParameters.getInstance(ALGORITHM);
+ params.init(spec);
+ Cipher cipher = Cipher.getInstance(CIPHER_TYPE);
+ cipher.init(mode, key, params);
+ return cipher;
+ }
+
+ private SecretKey generateKey() {
+ try {
+ KeyGenerator generator = KeyGenerator.getInstance(ALGORITHM);
+ generator.init(KEY_SIZE, random);
+ return generator.generateKey();
+ } catch (NoSuchAlgorithmException e) {
+ log.error("Generating key failed with error", e);
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/lfs/LfsSshAuth.java b/src/main/java/com/googlesource/gerrit/plugins/lfs/LfsSshAuth.java
index d82abff..22d7236 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/lfs/LfsSshAuth.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/LfsSshAuth.java
@@ -14,8 +14,6 @@
package com.googlesource.gerrit.plugins.lfs;
-import static org.eclipse.jgit.util.HttpSupport.HDR_AUTHORIZATION;
-
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.config.CanonicalWebUrl;
import com.google.gerrit.sshd.BaseCommand.Failure;
@@ -28,20 +26,20 @@
import com.google.inject.Provider;
import com.google.inject.Singleton;
-import org.eclipse.jgit.lfs.server.Response;
-
import java.net.MalformedURLException;
import java.net.URL;
-import java.util.Collections;
import java.util.List;
@Singleton
public class LfsSshAuth implements LfsPluginAuthCommand.LfsSshPluginAuth {
+ private final LfsSshRequestAuthorizer auth;
private final String canonicalWebUrl;
private final Gson gson;
@Inject
- LfsSshAuth(@CanonicalWebUrl Provider<String> canonicalWebUrl) {
+ LfsSshAuth(LfsSshRequestAuthorizer auth,
+ @CanonicalWebUrl Provider<String> canonicalWebUrl) {
+ this.auth = auth;
this.canonicalWebUrl = canonicalWebUrl.get();
this.gson = new GsonBuilder()
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
@@ -54,15 +52,20 @@
throws UnloggedFailure, Failure {
try {
URL url = new URL(canonicalWebUrl);
- String href = url.getProtocol() + "://" + url.getAuthority()
- + url.getPath() + "/" + args.get(0) + "/info/lfs";
- Response.Action response = new Response.Action();
- response.href = href;
- response.header =
- Collections.singletonMap(HDR_AUTHORIZATION, "not:required");
-
- return gson.toJson(response);
-
+ String path = url.getPath();
+ String project = args.get(0);
+ String operation = args.get(1);
+ StringBuilder href = new StringBuilder(url.getProtocol())
+ .append("://")
+ .append(url.getAuthority())
+ .append(path)
+ .append(path.endsWith("/") ? "" : "/")
+ .append(project)
+ .append("/info/lfs");
+ LfsSshRequestAuthorizer.SshAuthInfo info =
+ auth.generateAuthInfo(user, project, operation);
+ ExpiringAction action = new ExpiringAction(href.toString(), info);
+ return gson.toJson(action);
} catch (MalformedURLException e) {
throw new Failure(1, "Server configuration error: "
+ "forming Git LFS endpoint URL from canonicalWebUrl ["
diff --git a/src/main/java/com/googlesource/gerrit/plugins/lfs/LfsSshRequestAuthorizer.java b/src/main/java/com/googlesource/gerrit/plugins/lfs/LfsSshRequestAuthorizer.java
new file mode 100644
index 0000000..208f30e
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/LfsSshRequestAuthorizer.java
@@ -0,0 +1,148 @@
+// Copyright (C) 2017 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.lfs;
+
+import com.google.common.base.Optional;
+import com.google.gerrit.server.CurrentUser;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Singleton
+class LfsSshRequestAuthorizer {
+ static class SshAuthInfo extends AuthInfo {
+ SshAuthInfo(String authToken, String expiresAt) {
+ super(SSH_AUTH_PREFIX + authToken, expiresAt);
+ }
+ }
+
+ private static final Logger log =
+ LoggerFactory.getLogger(LfsSshRequestAuthorizer.class);
+ private static final int DEFAULT_SSH_TIMEOUT = 10;
+ static final String SSH_AUTH_PREFIX = "Ssh: ";
+
+ private final Processor processor;
+ private final int expirationSeconds;
+
+ @Inject
+ LfsSshRequestAuthorizer(Processor processor,
+ LfsConfigurationFactory configFactory) {
+ this.processor = processor;
+ int timeout = DEFAULT_SSH_TIMEOUT;
+ try {
+ timeout = configFactory.getGlobalConfig().getInt("auth",
+ null, "sshExpirationSeconds", DEFAULT_SSH_TIMEOUT);
+ } catch (IllegalArgumentException e) {
+ log.warn("Reading expiration timeout failed with error."
+ + " Falling back to default {}", DEFAULT_SSH_TIMEOUT, e);
+ }
+ this.expirationSeconds = timeout;
+ }
+
+ SshAuthInfo generateAuthInfo(CurrentUser user, String project,
+ String operation) {
+ LfsSshAuthToken token = new LfsSshAuthToken(user.getUserName(), project,
+ operation, expirationSeconds);
+ return new SshAuthInfo(processor.serialize(token), token.expiresAt);
+ }
+
+ Optional<String> getUserFromValidToken(String authToken,
+ String project, String operation) {
+ Optional<LfsSshAuthToken> token = processor.deserialize(authToken);
+ if (!token.isPresent()) {
+ return Optional.absent();
+ }
+
+ Verifier verifier = new Verifier(token.get(), project, operation);
+ if (!verifier.verify()) {
+ log.error("Invalid data was provided with auth token {}.", authToken);
+ return Optional.absent();
+ }
+
+ return Optional.of(token.get().user);
+ }
+
+ static class Processor extends LfsAuthToken.Processor<LfsSshAuthToken> {
+ @Inject
+ protected Processor(LfsCipher cipher) {
+ super(cipher);
+ }
+
+ @Override
+ protected List<String> getValues(LfsSshAuthToken token) {
+ List<String> values = new ArrayList<>(4);
+ values.add(token.user);
+ values.add(token.project);
+ values.add(token.operation);
+ values.add(token.expiresAt);
+ return values;
+ }
+
+ @Override
+ protected Optional<LfsSshAuthToken> createToken(List<String> values) {
+ if (values.size() != 4) {
+ return Optional.absent();
+ }
+
+ return Optional.of(new LfsSshAuthToken(values.get(0), values.get(1),
+ values.get(2), values.get(3)));
+ }
+ }
+
+ private static class Verifier extends LfsAuthToken.Verifier<LfsSshAuthToken> {
+ private final String project;
+ private final String operation;
+
+ protected Verifier(LfsSshAuthToken token, String project,
+ String operation) {
+ super(token);
+ this.project = project;
+ this.operation = operation;
+ }
+
+ @Override
+ protected boolean verifyTokenValues() {
+ return project.equals(token.project)
+ && operation.equals(token.operation);
+ }
+ }
+
+ private static class LfsSshAuthToken extends LfsAuthToken {
+ private final String user;
+ private final String project;
+ private final String operation;
+
+ LfsSshAuthToken(String user, String project, String operation,
+ int expirationSeconds) {
+ super(expirationSeconds);
+ this.user = user;
+ this.project = project;
+ this.operation = operation;
+ }
+
+ LfsSshAuthToken(String user, String project, String operation,
+ String expiresAt) {
+ super(expiresAt);
+ this.user = user;
+ this.project = project;
+ this.operation = operation;
+ }
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/lfs/fs/LfsFsContentServlet.java b/src/main/java/com/googlesource/gerrit/plugins/lfs/fs/LfsFsContentServlet.java
index 445a148..810fe60 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/lfs/fs/LfsFsContentServlet.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/fs/LfsFsContentServlet.java
@@ -70,7 +70,7 @@
return;
}
- if (!authorizer.verifyAgainstToken(req.getHeader(HDR_AUTHORIZATION),
+ if (!authorizer.verifyAuthInfo(req.getHeader(HDR_AUTHORIZATION),
DOWNLOAD, obj)) {
sendError(rsp, HttpStatus.SC_UNAUTHORIZED, MessageFormat.format(
LfsServerText.get().failedToCalcSignature, "Invalid authorization token"));
@@ -91,7 +91,7 @@
return;
}
- if (!authorizer.verifyAgainstToken(
+ if (!authorizer.verifyAuthInfo(
req.getHeader(HDR_AUTHORIZATION), UPLOAD, id)) {
sendError(rsp, HttpStatus.SC_UNAUTHORIZED,
MessageFormat.format(LfsServerText.get().failedToCalcSignature,
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 8b67591..87eb349 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
@@ -14,130 +14,104 @@
package com.googlesource.gerrit.plugins.lfs.fs;
-import com.google.common.base.Strings;
-import com.google.common.primitives.Bytes;
+import com.google.common.base.Optional;
import com.google.inject.Inject;
import com.google.inject.Singleton;
+import com.googlesource.gerrit.plugins.lfs.AuthInfo;
+import com.googlesource.gerrit.plugins.lfs.LfsAuthToken;
+import com.googlesource.gerrit.plugins.lfs.LfsCipher;
+
import org.eclipse.jgit.lfs.lib.AnyLongObjectId;
-import org.eclipse.jgit.util.Base64;
-import org.joda.time.DateTime;
-import org.joda.time.DateTimeZone;
-import org.joda.time.format.DateTimeFormat;
-import org.joda.time.format.DateTimeFormatter;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import org.eclipse.jgit.lfs.lib.LongObjectId;
-import java.nio.charset.StandardCharsets;
-import java.security.AlgorithmParameters;
-import java.security.GeneralSecurityException;
-import java.security.InvalidAlgorithmParameterException;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-import java.security.SecureRandom;
-import java.security.spec.InvalidParameterSpecException;
-import java.util.Arrays;
-
-import javax.crypto.Cipher;
-import javax.crypto.KeyGenerator;
-import javax.crypto.NoSuchPaddingException;
-import javax.crypto.SecretKey;
-import javax.crypto.spec.IvParameterSpec;
+import java.util.ArrayList;
+import java.util.List;
@Singleton
public class LfsFsRequestAuthorizer {
- private static final Logger log = LoggerFactory.getLogger(LfsFsRequestAuthorizer.class);
- private static final int IV_LENGTH = 16;
- private static final String ALGORITHM = "AES";
- static final DateTimeFormatter DATE_TIME =
- DateTimeFormat.forPattern("YYYYMMDDHHmmss");
-
- private final SecureRandom rndm;
- private final SecretKey key;
+ private final Processor processor;
@Inject
- LfsFsRequestAuthorizer() {
- this.rndm = new SecureRandom();
- this.key = generateKey();
+ LfsFsRequestAuthorizer(Processor processor) {
+ this.processor = processor;
}
- public String generateToken(String operation, AnyLongObjectId id,
+ public AuthInfo generateAuthInfo(String operation, AnyLongObjectId id,
int expirationSeconds) {
- try {
- 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(expirationSeconds))
- .getBytes(StandardCharsets.UTF_8))));
- } catch (GeneralSecurityException e) {
- log.error("Token generation failed with error", e);
- throw new RuntimeException(e);
- }
+ LfsFsAuthToken token = new LfsFsAuthToken(operation, id, expirationSeconds);
+ return new AuthInfo(processor.serialize(token), token.expiresAt);
}
- public boolean verifyAgainstToken(String token, String operation,
+ public boolean verifyAuthInfo(String authToken, String operation,
AnyLongObjectId id) {
- if (Strings.isNullOrEmpty(token)) {
+ Optional<LfsFsAuthToken> token = processor.deserialize(authToken);
+ if (!token.isPresent()) {
return false;
}
- byte[] bytes = Base64.decode(token);
- byte[] initVector = Arrays.copyOf(bytes, IV_LENGTH);
- try {
- Cipher cipher = cipher(initVector, Cipher.DECRYPT_MODE);
- String data = new String(
- cipher.doFinal(Arrays.copyOfRange(bytes, IV_LENGTH, bytes.length)),
- StandardCharsets.UTF_8);
- String oid = id.name();
- String prefix = String.format("%s-%s-", operation, oid);
- return data.startsWith(prefix)
- && onTime(data.substring(prefix.length()), operation, oid);
- } catch (GeneralSecurityException e) {
- log.error("Exception was thrown during token verification", e);
+ return new Verifier(token.get(), operation, id).verify();
+ }
+
+ static class Processor extends LfsAuthToken.Processor<LfsFsAuthToken> {
+ @Inject
+ protected Processor(LfsCipher cipher) {
+ super(cipher);
}
- return false;
- }
-
- boolean onTime(String dateTime, String operation, String id) {
- String now = DATE_TIME.print(now());
- if (now.compareTo(dateTime) > 0) {
- log.info("Operation {} on id {} timed out", operation, id);
- return false;
+ @Override
+ protected List<String> getValues(LfsFsAuthToken token) {
+ List<String> values = new ArrayList<>(3);
+ values.add(token.operation);
+ values.add(token.id.getName());
+ values.add(token.expiresAt);
+ return values;
}
- return true;
+ @Override
+ protected Optional<LfsFsAuthToken> createToken(List<String> values) {
+ if (values.size() != 3) {
+ return Optional.absent();
+ }
+
+ return Optional.of(new LfsFsAuthToken(values.get(0),
+ LongObjectId.fromString(values.get(1)), values.get(2)));
+ }
}
- private String timeout(int expirationSeconds) {
- return DATE_TIME.print(now().plusSeconds(expirationSeconds));
+ private static class Verifier extends LfsAuthToken.Verifier<LfsFsAuthToken> {
+ private final String operation;
+ private final AnyLongObjectId id;
+
+ protected Verifier(LfsFsAuthToken token,
+ String operation, AnyLongObjectId id) {
+ super(token);
+ this.operation = operation;
+ this.id = id;
+ }
+
+ @Override
+ protected boolean verifyTokenValues() {
+ return operation.equals(token.operation)
+ && id.getName().equals(token.id.getName());
+ }
}
- private DateTime now() {
- return DateTime.now().toDateTime(DateTimeZone.UTC);
- }
+ private static class LfsFsAuthToken extends LfsAuthToken {
+ private final String operation;
+ private final AnyLongObjectId id;
- private Cipher cipher(byte[] initVector, int mode) throws NoSuchAlgorithmException,
- NoSuchPaddingException, InvalidParameterSpecException,
- InvalidKeyException, InvalidAlgorithmParameterException {
- IvParameterSpec spec = new IvParameterSpec(initVector);
- Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
- AlgorithmParameters params = AlgorithmParameters.getInstance(ALGORITHM);
- params.init(spec);
- cipher.init(mode, key, params);
- return cipher;
- }
+ LfsFsAuthToken(String operation, AnyLongObjectId id,
+ int expirationSeconds) {
+ super(expirationSeconds);
+ this.operation = operation;
+ this.id = id;
+ }
- private SecretKey generateKey() {
- try {
- KeyGenerator generator = KeyGenerator.getInstance(ALGORITHM);
- generator.init(128, rndm);
- return generator.generateKey();
- } catch (NoSuchAlgorithmException e) {
- log.error("Generating key failed with error", e);
- throw new RuntimeException(e);
+ LfsFsAuthToken(String operation, AnyLongObjectId id, String expiresAt) {
+ super(expiresAt);
+ this.operation = operation;
+ this.id = id;
}
}
}
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 a810943..71a51af 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
@@ -15,7 +15,6 @@
package com.googlesource.gerrit.plugins.lfs.fs;
import static com.googlesource.gerrit.plugins.lfs.LfsBackend.DEFAULT;
-import static org.eclipse.jgit.util.HttpSupport.HDR_AUTHORIZATION;
import com.google.common.base.Strings;
import com.google.gerrit.extensions.annotations.PluginCanonicalWebUrl;
@@ -23,6 +22,8 @@
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
+import com.googlesource.gerrit.plugins.lfs.AuthInfo;
+import com.googlesource.gerrit.plugins.lfs.ExpiringAction;
import com.googlesource.gerrit.plugins.lfs.LfsBackend;
import com.googlesource.gerrit.plugins.lfs.LfsConfigurationFactory;
import com.googlesource.gerrit.plugins.lfs.LfsGlobalConfig;
@@ -35,7 +36,6 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
-import java.util.Collections;
public class LocalLargeFileRepository extends FileLfsRepository {
public interface Factory {
@@ -74,17 +74,17 @@
@Override
public Response.Action getDownloadAction(AnyLongObjectId id) {
Response.Action action = super.getDownloadAction(id);
- action.header = Collections.singletonMap(HDR_AUTHORIZATION,
- authorizer.generateToken(DOWNLOAD, id, expirationSeconds));
- return action;
+ AuthInfo authInfo =
+ authorizer.generateAuthInfo(DOWNLOAD, id, expirationSeconds);
+ return new ExpiringAction(action.href, authInfo);
}
@Override
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, expirationSeconds));
- return action;
+ AuthInfo authInfo =
+ authorizer.generateAuthInfo(UPLOAD, id, expirationSeconds);
+ return new ExpiringAction(action.href, authInfo);
}
private static String getContentUrl(String url, LfsBackend backend) {
diff --git a/src/main/java/org/eclipse/jgit/lfs/errors/LfsUnauthorized.java b/src/main/java/org/eclipse/jgit/lfs/errors/LfsUnauthorized.java
new file mode 100644
index 0000000..896eb9f
--- /dev/null
+++ b/src/main/java/org/eclipse/jgit/lfs/errors/LfsUnauthorized.java
@@ -0,0 +1,24 @@
+// Copyright (C) 2017 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 org.eclipse.jgit.lfs.errors;
+
+
+public class LfsUnauthorized extends LfsException {
+ private static final long serialVersionUID = 1L;
+
+ public LfsUnauthorized(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/org/eclipse/jgit/lfs/server/LfsGerritProtocolServlet.java b/src/main/java/org/eclipse/jgit/lfs/server/LfsGerritProtocolServlet.java
new file mode 100644
index 0000000..849d789
--- /dev/null
+++ b/src/main/java/org/eclipse/jgit/lfs/server/LfsGerritProtocolServlet.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright (C) 2015, Sasa Zivkov <sasa.zivkov@sap.com>
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.lfs.server;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.apache.http.HttpStatus.SC_FORBIDDEN;
+import static org.apache.http.HttpStatus.SC_INSUFFICIENT_STORAGE;
+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_OK;
+import static org.apache.http.HttpStatus.SC_SERVICE_UNAVAILABLE;
+import static org.apache.http.HttpStatus.SC_UNAUTHORIZED;
+import static org.apache.http.HttpStatus.SC_UNPROCESSABLE_ENTITY;
+import static org.eclipse.jgit.util.HttpSupport.HDR_AUTHORIZATION;
+
+import com.google.gson.FieldNamingPolicy;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+import org.eclipse.jgit.lfs.errors.LfsBandwidthLimitExceeded;
+import org.eclipse.jgit.lfs.errors.LfsException;
+import org.eclipse.jgit.lfs.errors.LfsInsufficientStorage;
+import org.eclipse.jgit.lfs.errors.LfsRateLimitExceeded;
+import org.eclipse.jgit.lfs.errors.LfsRepositoryNotFound;
+import org.eclipse.jgit.lfs.errors.LfsRepositoryReadOnly;
+import org.eclipse.jgit.lfs.errors.LfsUnauthorized;
+import org.eclipse.jgit.lfs.errors.LfsUnavailable;
+import org.eclipse.jgit.lfs.errors.LfsValidationError;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.io.Writer;
+import java.util.List;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * LFS protocol handler implementing the LFS batch API [1]
+ *
+ * [1] https://github.com/github/git-lfs/blob/master/docs/api/v1/http-v1-batch.md
+ *
+ * @since 4.3
+ */
+// TODO this is copy of org.eclipse.jgit.lfs.server.LfsProtocolServlet with small improvements
+// that allows user's auth - donate it back to JGit and get rid of it once Gerrit moves to it
+public abstract class LfsGerritProtocolServlet extends HttpServlet {
+
+ private static final long serialVersionUID = 1L;
+
+ private static final String CONTENTTYPE_VND_GIT_LFS_JSON =
+ "application/vnd.git-lfs+json; charset=utf-8"; //$NON-NLS-1$
+
+ private static final int SC_RATE_LIMIT_EXCEEDED = 429;
+
+ private static final int SC_BANDWIDTH_LIMIT_EXCEEDED = 509;
+
+ private Gson gson = createGson();
+
+ /**
+ * Get the large file repository for the given request and path.
+ *
+ * @param request
+ * the request
+ * @param path
+ * the path
+ * @param auth
+ * the authorization info
+ *
+ * @return the large file repository storing large files.
+ * @throws LfsException
+ * implementations should throw more specific exceptions to
+ * signal which type of error occurred:
+ * <dl>
+ * <dt>{@link LfsValidationError}</dt>
+ * <dd>when there is a validation error with one or more of the
+ * objects in the request</dd>
+ * <dt>{@link LfsRepositoryNotFound}</dt>
+ * <dd>when the repository does not exist for the user</dd>
+ * <dt>{@link LfsRepositoryReadOnly}</dt>
+ * <dd>when the user has read, but not write access. Only
+ * applicable when the operation in the request is "upload"</dd>
+ * <dt>{@link LfsRateLimitExceeded}</dt>
+ * <dd>when the user has hit a rate limit with the server</dd>
+ * <dt>{@link LfsBandwidthLimitExceeded}</dt>
+ * <dd>when the bandwidth limit for the user or repository has
+ * been exceeded</dd>
+ * <dt>{@link LfsInsufficientStorage}</dt>
+ * <dd>when there is insufficient storage on the server</dd>
+ * <dt>{@link LfsUnauthorized}</dt>
+ * <dd>when user is not authorized to perform LFS operation</dd>
+ * <dt>{@link LfsUnavailable}</dt>
+ * <dd>when LFS is not available</dd>
+ * <dt>{@link LfsException}</dt>
+ * <dd>when an unexpected internal server error occurred</dd>
+ * </dl>
+ * @since 4.5
+ */
+ protected abstract LargeFileRepository getLargeFileRepository(
+ LfsRequest request, String path, String auth) throws LfsException;
+
+ /**
+ * LFS request.
+ *
+ * @since 4.5
+ */
+ protected static class LfsRequest {
+ private String operation;
+
+ private List<LfsObject> objects;
+
+ /**
+ * Get the LFS operation.
+ *
+ * @return the operation
+ */
+ public String getOperation() {
+ return operation;
+ }
+
+ /**
+ * Get the LFS objects.
+ *
+ * @return the objects
+ */
+ public List<LfsObject> getObjects() {
+ return objects;
+ }
+ }
+
+ @Override
+ protected void doPost(HttpServletRequest req, HttpServletResponse res)
+ throws ServletException, IOException {
+ Writer w = new BufferedWriter(
+ new OutputStreamWriter(res.getOutputStream(), UTF_8));
+
+ Reader r = new BufferedReader(
+ new InputStreamReader(req.getInputStream(), UTF_8));
+ LfsRequest request = gson.fromJson(r, LfsRequest.class);
+ String path = req.getPathInfo();
+
+ res.setContentType(CONTENTTYPE_VND_GIT_LFS_JSON);
+ LargeFileRepository repo = null;
+ try {
+ repo = getLargeFileRepository(request, path,
+ req.getHeader(HDR_AUTHORIZATION));
+ if (repo == null) {
+ throw new LfsException("unexpected error"); //$NON-NLS-1$
+ }
+ res.setStatus(SC_OK);
+ TransferHandler handler = TransferHandler
+ .forOperation(request.operation, repo, request.objects);
+ gson.toJson(handler.process(), w);
+ } catch (LfsValidationError e) {
+ sendError(res, w, SC_UNPROCESSABLE_ENTITY, e.getMessage());
+ } catch (LfsRepositoryNotFound e) {
+ sendError(res, w, SC_NOT_FOUND, e.getMessage());
+ } catch (LfsRepositoryReadOnly e) {
+ sendError(res, w, SC_FORBIDDEN, e.getMessage());
+ } catch (LfsRateLimitExceeded e) {
+ sendError(res, w, SC_RATE_LIMIT_EXCEEDED, e.getMessage());
+ } catch (LfsBandwidthLimitExceeded e) {
+ sendError(res, w, SC_BANDWIDTH_LIMIT_EXCEEDED, e.getMessage());
+ } catch (LfsInsufficientStorage e) {
+ sendError(res, w, SC_INSUFFICIENT_STORAGE, e.getMessage());
+ } catch (LfsUnavailable e) {
+ sendError(res, w, SC_SERVICE_UNAVAILABLE, e.getMessage());
+ } catch (LfsUnauthorized e) {
+ sendError(res, w, SC_UNAUTHORIZED, e.getMessage());
+ } catch (LfsException e) {
+ sendError(res, w, SC_INTERNAL_SERVER_ERROR, e.getMessage());
+ } finally {
+ w.flush();
+ }
+ }
+
+ static class Error {
+ String message;
+
+ Error(String m) {
+ this.message = m;
+ }
+ }
+
+ private void sendError(HttpServletResponse rsp, Writer writer, int status,
+ String message) {
+ rsp.setStatus(status);
+ gson.toJson(new Error(message), writer);
+ }
+
+ private Gson createGson() {
+ return new GsonBuilder()
+ .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
+ .disableHtmlEscaping()
+ .create();
+ }
+}
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index 7cf8603..d0b57b8 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -2,13 +2,21 @@
## Core Gerrit Settings
-The following option must be set in `$GERRIT_SITE/etc/gerrit.config`.
+The following options must be set in `$GERRIT_SITE/etc/gerrit.config`.
### Section `lfs`
lfs.plugin = @PLUGIN@
: With this option set LFS requests are forwarded to the @PLUGIN@ plugin.
+### Section `auth`
+
+auth.gitBasicAuth = true
+: Git LFS client uses Basic HTTP auth with LFS requests. When this option
+is not enabled (not set or equals to `false`) Git LFS HTTP requests are treated
+as anonymous requests. Therefore requests will be successfully authorized only
+for projects that allows anonymous to perform requested operation.
+
## Per Project Settings
The following options can be configured in `@PLUGIN@.config` on the
@@ -93,6 +101,15 @@
The following options can be configured in `$GERRIT_SITE/etc/@PLUGIN@.config`
and `$GERRIT_SITE/etc/@PLUGIN@.secure.config.`
+### Section `auth`
+
+auth.sshExpirationSeconds
+: Validity, in seconds, of authentication token for SSH requests.
+[Git LFS Authentication](https://github.com/git-lfs/git-lfs/blob/master/docs/api/authentication.md)
+specifies that SSH might be used to authenticate the user. Successful authentication
+provides token that is later used for Git LFS requests.
+: Default is `10` seconds.
+
### Section `storage`
storage.backend
@@ -120,16 +137,16 @@
The following configuration options are only used when the backend is `s3`.
s3.region
-: link:http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html#concepts-available-regions
-[Amazon region] the S3 storage bucket is residing in.
+: [Amazon region](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html#concepts-available-regions)
+the S3 storage bucket is residing in.
s3.bucket
-: Name of the link:http://docs.aws.amazon.com/AmazonS3/latest/UG/CreatingaBucket.html
-[Amazon S3 storage bucket] which will store large objects.
+: Name of the [Amazon S3 storage bucket](http://docs.aws.amazon.com/AmazonS3/latest/UG/CreatingaBucket.html)
+ which will store large objects.
s3.storageClass
-: link:http://docs.aws.amazon.com/AmazonS3/latest/dev/storage-class-intro.html
-[Amazon S3 storage class] used for storing large objects.
+: [Amazon S3 storage class](http://docs.aws.amazon.com/AmazonS3/latest/dev/storage-class-intro.html)
+ used for storing large objects.
: Default is `REDUCED_REDUNDANCY`
s3.expirationSeconds
@@ -146,13 +163,13 @@
: Default is `false`.
s3.accessKey
-: The link:http://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html
-[Amazon IAM accessKey] for authenticating to S3. It is recommended to place this
+: The [Amazon IAM accessKey](http://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html)
+for authenticating to S3. It is recommended to place this
setting in `$GERRIT_SITE/etc/@PLUGIN@.secure.config`.
s3.secretKey
-: The link:http://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html
-[Amazon IAM secretKey] for authenticating to S3. It is recommended to place this
+: The [Amazon IAM secretKey](http://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html)
+ for authenticating to S3. It is recommended to place this
setting in `$GERRIT_SITE/etc/@PLUGIN@.secure.config`.
### Multiple LFS backends
diff --git a/src/test/java/com/googlesource/gerrit/plugins/lfs/LfsAuthTokenTest.java b/src/test/java/com/googlesource/gerrit/plugins/lfs/LfsAuthTokenTest.java
new file mode 100644
index 0000000..5b5c532
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/lfs/LfsAuthTokenTest.java
@@ -0,0 +1,116 @@
+// Copyright (C) 2017 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.lfs;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.googlesource.gerrit.plugins.lfs.LfsAuthToken.ISO;
+import static com.googlesource.gerrit.plugins.lfs.LfsAuthToken.Verifier.onTime;
+
+import com.google.common.base.Optional;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class LfsAuthTokenTest {
+ private final LfsCipher cipher = new LfsCipher();
+
+ @Test
+ public void testExpiredTime() throws Exception {
+ DateTime now = now();
+ // test that even 1ms expiration is enough
+ assertThat(onTime(ISO.print(now.minusMillis(1)))).isFalse();
+ }
+
+ @Test
+ public void testOnTime() throws Exception {
+ DateTime now = now();
+ // if there is at least 1ms before there is no timeout
+ assertThat(onTime(ISO.print(now.plusMillis(1)))).isTrue();
+ }
+
+ @Test
+ public void testTokenSerializationDeserialization() throws Exception {
+ TestTokenProessor processor = new TestTokenProessor(cipher);
+ TestToken token = new TestToken(0);
+ String serialized = processor.serialize(token);
+
+ assertThat(serialized).isNotEmpty();
+
+ Optional<TestToken> deserialized = processor.deserialize(serialized);
+ assertThat(deserialized.isPresent()).isTrue();
+ assertThat(token.expiresAt).isEqualTo(deserialized.get().expiresAt);
+ }
+
+ @Test
+ public void testTokenOnTime() throws Exception {
+ TestToken token = new TestToken(1);
+ TestTokenVerifier verifier = new TestTokenVerifier(token);
+ assertThat(verifier.verify()).isTrue();
+ }
+
+ @Test
+ public void testTokenExpired() throws Exception {
+ TestToken token = new TestToken(-1);
+ TestTokenVerifier verifier = new TestTokenVerifier(token);
+ assertThat(verifier.verify()).isFalse();
+ }
+
+ private DateTime now() {
+ return DateTime.now().toDateTime(DateTimeZone.UTC);
+ }
+
+ private class TestToken extends LfsAuthToken {
+ TestToken(int expirationSeconds) {
+ super(expirationSeconds);
+ }
+
+ TestToken(String expiresAt) {
+ super(expiresAt);
+ }
+ }
+
+ private class TestTokenProessor extends LfsAuthToken.Processor<TestToken> {
+ TestTokenProessor(LfsCipher cipher) {
+ super(cipher);
+ }
+
+ @Override
+ protected List<String> getValues(TestToken token) {
+ List<String> values = new ArrayList<>(2);
+ values.add(token.expiresAt);
+ return values;
+ }
+
+ @Override
+ protected Optional<TestToken> createToken(List<String> values) {
+ return Optional.of(new TestToken(values.get(0)));
+ }
+ }
+
+ private class TestTokenVerifier extends LfsAuthToken.Verifier<TestToken> {
+ protected TestTokenVerifier(TestToken token) {
+ super(token);
+ }
+
+ @Override
+ protected boolean verifyTokenValues() {
+ return true;
+ }
+ }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/lfs/LfsCipherTest.java b/src/test/java/com/googlesource/gerrit/plugins/lfs/LfsCipherTest.java
new file mode 100644
index 0000000..5f51f4a
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/lfs/LfsCipherTest.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2017 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.lfs;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.base.Optional;
+
+import org.junit.Test;
+
+public class LfsCipherTest {
+ private final LfsCipher cipher = new LfsCipher();
+
+ @Test
+ public void testCipherTextIsDifferentThanInput() throws Exception {
+ String plain = "plain text";
+ String encrypted = cipher.encrypt(plain);
+ assertThat(encrypted).isNotEmpty();
+ assertThat(encrypted).isNotEqualTo(plain);
+ }
+
+ @Test
+ public void testVerifyDecodeAgainstEncodedInput() throws Exception {
+ String plain = "plain text";
+ String encrypted = cipher.encrypt(plain);
+ Optional<String> decrypted = cipher.decrypt(encrypted);
+ assertThat(decrypted.isPresent()).isTrue();
+ assertThat(decrypted.get()).isEqualTo(plain);
+ }
+
+ @Test
+ public void testVerifyDecodeAgainstInvalidInput() throws Exception {
+ String plain = "plain text";
+ String encrypted = cipher.encrypt(plain);
+ // there is a chance that two first chars in token are the same
+ // in such case re-generate the token
+ while(encrypted.charAt(0) == encrypted.charAt(1)) {
+ encrypted = cipher.encrypt(plain);
+ }
+
+ Optional<String> decrypted = cipher.decrypt(encrypted.substring(1, 2)
+ + encrypted.substring(0, 1) + encrypted.substring(2));
+ assertThat(decrypted.isPresent()).isTrue();
+ assertThat(decrypted.get()).isNotEqualTo(plain);
+ }
+}
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 8cab1a3..8513efb 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
@@ -15,74 +15,36 @@
package com.googlesource.gerrit.plugins.lfs.fs;
import static com.google.common.truth.Truth.assertThat;
-import static com.googlesource.gerrit.plugins.lfs.fs.LfsFsRequestAuthorizer.DATE_TIME;
import static org.eclipse.jgit.lfs.lib.LongObjectId.zeroId;
+import com.googlesource.gerrit.plugins.lfs.AuthInfo;
+import com.googlesource.gerrit.plugins.lfs.LfsCipher;
+import com.googlesource.gerrit.plugins.lfs.fs.LfsFsRequestAuthorizer.Processor;
+
import org.eclipse.jgit.lfs.lib.LongObjectId;
-import org.joda.time.DateTime;
-import org.joda.time.DateTimeZone;
import org.junit.Test;
public class LfsFsRequestAuthorizerTest {
- private final LfsFsRequestAuthorizer auth = new LfsFsRequestAuthorizer();
+ private final LfsFsRequestAuthorizer auth =
+ new LfsFsRequestAuthorizer(new Processor(new LfsCipher()));
@Test
- public void testExpiredTime() throws Exception {
- DateTime now = DateTime.now().toDateTime(DateTimeZone.UTC);
- // test that even 1s expiration is enough
- assertThat(auth.onTime(DATE_TIME.print(now.minusSeconds(1)), "o", "id"))
- .isFalse();
- }
-
- @Test
- public void testOnTime() throws Exception {
- DateTime now = DateTime.now().toDateTime(DateTimeZone.UTC);
- // if there is at least 1s before there is no timeout
- assertThat(auth.onTime(DATE_TIME.print(now.plusSeconds(1)), "o", "id"))
- .isTrue();
- }
-
- @Test
- public void testVerifyAgainstMissingToken() throws Exception {
- assertThat(auth.verifyAgainstToken("", "o", zeroId())).isFalse();
- assertThat(auth.verifyAgainstToken(null, "o", zeroId())).isFalse();
- }
-
- @Test
- public void testVerifyAgainstToken() throws Exception {
- 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(), 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",
- zeroId())).isFalse();
+ public void testVerifyAuthInfo() throws Exception {
+ AuthInfo info = auth.generateAuthInfo("o", zeroId(), 1);
+ assertThat(auth.verifyAuthInfo(info.authToken, "o", zeroId())).isTrue();
}
@Test
public void testVerifyAgainstDifferentOperation() throws Exception {
- String token = auth.generateToken("o", zeroId(), 1);
- assertThat(auth.verifyAgainstToken(token, "p", zeroId())).isFalse();
+ AuthInfo info = auth.generateAuthInfo("o", zeroId(), 1);
+ assertThat(auth.verifyAuthInfo(info.authToken, "p", zeroId())).isFalse();
}
@Test
public void testVerifyAgainstDifferentObjectId() throws Exception {
- 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();
+ AuthInfo info = auth.generateAuthInfo("o", zeroId(), 1);
+ assertThat(auth.verifyAuthInfo(info.authToken, "o",
+ LongObjectId.fromString("123456789012345678901234567890"
+ + "123456789012345678901234567890" + "1234"))).isFalse();
}
}