mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-12 00:15:44 +01:00
Merged in feature/lfs_over_ssh (pull request #336)
Feature LFS over ssh
This commit is contained in:
@@ -40,6 +40,12 @@
|
|||||||
<version>2.6</version>
|
<version>2.6</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jboss.resteasy</groupId>
|
||||||
|
<artifactId>resteasy-jackson2-provider</artifactId>
|
||||||
|
<version>${resteasy.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package sonia.scm.web.lfs;
|
||||||
|
|
||||||
|
import org.eclipse.jgit.lfs.server.Response;
|
||||||
|
import sonia.scm.security.AccessToken;
|
||||||
|
|
||||||
|
import java.text.DateFormat;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.TimeZone;
|
||||||
|
|
||||||
|
class ExpiringAction extends Response.Action {
|
||||||
|
|
||||||
|
@SuppressWarnings({"squid:S00116"})
|
||||||
|
// This class is used for json serialization, only
|
||||||
|
public final String expires_at;
|
||||||
|
|
||||||
|
ExpiringAction(String href, AccessToken accessToken) {
|
||||||
|
this.expires_at = createDateFormat().format(accessToken.getExpiration());
|
||||||
|
this.href = href;
|
||||||
|
this.header = Collections.singletonMap("Authorization", "Bearer " + accessToken.compact());
|
||||||
|
}
|
||||||
|
|
||||||
|
private DateFormat createDateFormat() {
|
||||||
|
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
|
||||||
|
dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
|
||||||
|
return dateFormat;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package sonia.scm.web.lfs;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.google.common.base.Charsets;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import sonia.scm.config.ScmConfiguration;
|
||||||
|
import sonia.scm.plugin.Extension;
|
||||||
|
import sonia.scm.protocolcommand.CommandInterpreter;
|
||||||
|
import sonia.scm.protocolcommand.CommandInterpreterFactory;
|
||||||
|
import sonia.scm.protocolcommand.RepositoryContext;
|
||||||
|
import sonia.scm.protocolcommand.RepositoryContextResolver;
|
||||||
|
import sonia.scm.protocolcommand.ScmCommandProtocol;
|
||||||
|
import sonia.scm.protocolcommand.git.GitRepositoryContextResolver;
|
||||||
|
import sonia.scm.repository.Repository;
|
||||||
|
import sonia.scm.security.AccessToken;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static java.lang.String.format;
|
||||||
|
|
||||||
|
@Extension
|
||||||
|
public class LFSAuthCommand implements CommandInterpreterFactory {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(LFSAuthCommand.class);
|
||||||
|
|
||||||
|
private static final String LFS_INFO_URL_PATTERN = "%s/repo/%s/%s.git/info/lfs/";
|
||||||
|
|
||||||
|
private final LfsAccessTokenFactory tokenFactory;
|
||||||
|
private final GitRepositoryContextResolver gitRepositoryContextResolver;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
private final ScmConfiguration configuration;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public LFSAuthCommand(LfsAccessTokenFactory tokenFactory, GitRepositoryContextResolver gitRepositoryContextResolver, ScmConfiguration configuration) {
|
||||||
|
this.tokenFactory = tokenFactory;
|
||||||
|
this.gitRepositoryContextResolver = gitRepositoryContextResolver;
|
||||||
|
|
||||||
|
objectMapper = new ObjectMapper();
|
||||||
|
this.configuration = configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<CommandInterpreter> canHandle(String command) {
|
||||||
|
if (command.startsWith("git-lfs-authenticate")) {
|
||||||
|
LOG.trace("create command for input: {}", command);
|
||||||
|
return Optional.of(new LfsAuthCommandInterpreter(command));
|
||||||
|
} else {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class LfsAuthCommandInterpreter implements CommandInterpreter {
|
||||||
|
|
||||||
|
private final String command;
|
||||||
|
|
||||||
|
LfsAuthCommandInterpreter(String command) {
|
||||||
|
this.command = command;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String[] getParsedArgs() {
|
||||||
|
// we are interested only in the 'repo' argument, so we discard the rest
|
||||||
|
return new String[]{command.split("\\s+")[1]};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ScmCommandProtocol getProtocolHandler() {
|
||||||
|
return (context, repositoryContext) -> {
|
||||||
|
ExpiringAction response = createResponseObject(repositoryContext);
|
||||||
|
// we buffer the response and write it with a single write,
|
||||||
|
// because otherwise the ssh connection is not closed
|
||||||
|
String buffer = serializeResponse(response);
|
||||||
|
context.getOutputStream().write(buffer.getBytes(Charsets.UTF_8));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RepositoryContextResolver getRepositoryContextResolver() {
|
||||||
|
return gitRepositoryContextResolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ExpiringAction createResponseObject(RepositoryContext repositoryContext) {
|
||||||
|
Repository repository = repositoryContext.getRepository();
|
||||||
|
|
||||||
|
String url = format(LFS_INFO_URL_PATTERN, configuration.getBaseUrl(), repository.getNamespace(), repository.getName());
|
||||||
|
AccessToken accessToken = tokenFactory.createReadAccessToken(repository);
|
||||||
|
|
||||||
|
return new ExpiringAction(url, accessToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String serializeResponse(ExpiringAction response) throws IOException {
|
||||||
|
return objectMapper.writeValueAsString(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package sonia.scm.web.lfs;
|
||||||
|
|
||||||
|
import com.github.sdorra.ssp.PermissionCheck;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import sonia.scm.repository.Repository;
|
||||||
|
import sonia.scm.repository.RepositoryPermissions;
|
||||||
|
import sonia.scm.security.AccessToken;
|
||||||
|
import sonia.scm.security.AccessTokenBuilderFactory;
|
||||||
|
import sonia.scm.security.Scope;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
public class LfsAccessTokenFactory {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(LfsAccessTokenFactory.class);
|
||||||
|
|
||||||
|
private final AccessTokenBuilderFactory tokenBuilderFactory;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
LfsAccessTokenFactory(AccessTokenBuilderFactory tokenBuilderFactory) {
|
||||||
|
this.tokenBuilderFactory = tokenBuilderFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
AccessToken createReadAccessToken(Repository repository) {
|
||||||
|
PermissionCheck read = RepositoryPermissions.read(repository);
|
||||||
|
read.check();
|
||||||
|
|
||||||
|
PermissionCheck pull = RepositoryPermissions.pull(repository);
|
||||||
|
pull.check();
|
||||||
|
|
||||||
|
List<String> permissions = new ArrayList<>();
|
||||||
|
permissions.add(read.asShiroString());
|
||||||
|
permissions.add(pull.asShiroString());
|
||||||
|
|
||||||
|
PermissionCheck push = RepositoryPermissions.push(repository);
|
||||||
|
if (push.isPermitted()) {
|
||||||
|
// we have to add push permissions,
|
||||||
|
// because this token is also used to obtain the write access token
|
||||||
|
permissions.add(push.asShiroString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return createToken(Scope.valueOf(permissions));
|
||||||
|
}
|
||||||
|
|
||||||
|
AccessToken createWriteAccessToken(Repository repository) {
|
||||||
|
PermissionCheck read = RepositoryPermissions.read(repository);
|
||||||
|
read.check();
|
||||||
|
|
||||||
|
PermissionCheck pull = RepositoryPermissions.pull(repository);
|
||||||
|
pull.check();
|
||||||
|
|
||||||
|
PermissionCheck push = RepositoryPermissions.push(repository);
|
||||||
|
push.check();
|
||||||
|
|
||||||
|
return createToken(Scope.valueOf(read.asShiroString(), pull.asShiroString(), push.asShiroString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private AccessToken createToken(Scope scope) {
|
||||||
|
LOG.trace("create access token with scope: {}", scope);
|
||||||
|
return tokenBuilderFactory
|
||||||
|
.create()
|
||||||
|
.expiresIn(5, TimeUnit.MINUTES)
|
||||||
|
.scope(scope)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,12 +2,13 @@ package sonia.scm.web.lfs;
|
|||||||
|
|
||||||
import org.eclipse.jgit.lfs.lib.AnyLongObjectId;
|
import org.eclipse.jgit.lfs.lib.AnyLongObjectId;
|
||||||
import org.eclipse.jgit.lfs.server.LargeFileRepository;
|
import org.eclipse.jgit.lfs.server.LargeFileRepository;
|
||||||
import org.eclipse.jgit.lfs.server.Response;
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import sonia.scm.repository.Repository;
|
||||||
|
import sonia.scm.security.AccessToken;
|
||||||
import sonia.scm.store.Blob;
|
import sonia.scm.store.Blob;
|
||||||
import sonia.scm.store.BlobStore;
|
import sonia.scm.store.BlobStore;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This LargeFileRepository is used for jGit-Servlet implementation. Under the jgit LFS Servlet hood, the
|
* This LargeFileRepository is used for jGit-Servlet implementation. Under the jgit LFS Servlet hood, the
|
||||||
* SCM-Repository API is used to implement the Repository.
|
* SCM-Repository API is used to implement the Repository.
|
||||||
@@ -17,49 +18,67 @@ import java.io.IOException;
|
|||||||
*/
|
*/
|
||||||
public class ScmBlobLfsRepository implements LargeFileRepository {
|
public class ScmBlobLfsRepository implements LargeFileRepository {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(ScmBlobLfsRepository.class);
|
||||||
|
|
||||||
private final BlobStore blobStore;
|
private final BlobStore blobStore;
|
||||||
|
private final LfsAccessTokenFactory tokenFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This URI is used to determine the actual URI for Upload / Download. Must be full URI (or rewritable by reverse
|
* This URI is used to determine the actual URI for Upload / Download. Must be full URI (or rewritable by reverse
|
||||||
* proxy).
|
* proxy).
|
||||||
*/
|
*/
|
||||||
private final String baseUri;
|
private final String baseUri;
|
||||||
|
private final Repository repository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link ScmBlobLfsRepository} is created for either download or upload, not both. Therefore we can cache the
|
||||||
|
* access token and do not have to create them anew for each action.
|
||||||
|
*/
|
||||||
|
private AccessToken accessToken;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a {@link ScmBlobLfsRepository} for the provided repository.
|
* Creates a {@link ScmBlobLfsRepository} for the provided repository.
|
||||||
*
|
*
|
||||||
|
* @param repository The current scm repository this LFS repository is used for.
|
||||||
* @param blobStore The SCM Blobstore used for this @{@link LargeFileRepository}.
|
* @param blobStore The SCM Blobstore used for this @{@link LargeFileRepository}.
|
||||||
|
* @param tokenFactory The token builder for subsequent LFS requests.
|
||||||
* @param baseUri This URI is used to determine the actual URI for Upload / Download. Must be full URI (or
|
* @param baseUri This URI is used to determine the actual URI for Upload / Download. Must be full URI (or
|
||||||
* rewritable by reverse proxy).
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public ScmBlobLfsRepository(BlobStore blobStore, String baseUri) {
|
public ScmBlobLfsRepository(Repository repository, BlobStore blobStore, LfsAccessTokenFactory tokenFactory, String baseUri) {
|
||||||
|
this.repository = repository;
|
||||||
this.blobStore = blobStore;
|
this.blobStore = blobStore;
|
||||||
|
this.tokenFactory = tokenFactory;
|
||||||
this.baseUri = baseUri;
|
this.baseUri = baseUri;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Response.Action getDownloadAction(AnyLongObjectId id) {
|
public ExpiringAction getDownloadAction(AnyLongObjectId id) {
|
||||||
|
if (accessToken == null) {
|
||||||
return getAction(id);
|
LOG.trace("create access token to download lfs object {} from repository {}", id, repository.getNamespaceAndName());
|
||||||
|
accessToken = tokenFactory.createReadAccessToken(repository);
|
||||||
|
}
|
||||||
|
return getAction(id, accessToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Response.Action getUploadAction(AnyLongObjectId id, long size) {
|
public ExpiringAction getUploadAction(AnyLongObjectId id, long size) {
|
||||||
|
if (accessToken == null) {
|
||||||
return getAction(id);
|
LOG.trace("create access token to upload lfs object {} to repository {}", id, repository.getNamespaceAndName());
|
||||||
|
accessToken = tokenFactory.createWriteAccessToken(repository);
|
||||||
|
}
|
||||||
|
return getAction(id, accessToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Response.Action getVerifyAction(AnyLongObjectId id) {
|
public ExpiringAction getVerifyAction(AnyLongObjectId id) {
|
||||||
|
|
||||||
//validation is optional. We do not support it.
|
//validation is optional. We do not support it.
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getSize(AnyLongObjectId id) throws IOException {
|
public long getSize(AnyLongObjectId id) {
|
||||||
|
|
||||||
//this needs to be size of what is will be written into the response of the download. Clients are likely to
|
//this needs to be size of what is will be written into the response of the download. Clients are likely to
|
||||||
// verify it.
|
// verify it.
|
||||||
@@ -77,14 +96,11 @@ public class ScmBlobLfsRepository implements LargeFileRepository {
|
|||||||
/**
|
/**
|
||||||
* Constructs the Download / Upload actions to be supplied to the client.
|
* Constructs the Download / Upload actions to be supplied to the client.
|
||||||
*/
|
*/
|
||||||
private Response.Action getAction(AnyLongObjectId id) {
|
private ExpiringAction getAction(AnyLongObjectId id, AccessToken token) {
|
||||||
|
|
||||||
//LFS protocol has to provide the information on where to put or get the actual content, i. e.
|
//LFS protocol has to provide the information on where to put or get the actual content, i. e.
|
||||||
//the actual URI for up- and download.
|
//the actual URI for up- and download.
|
||||||
|
|
||||||
Response.Action a = new Response.Action();
|
return new ExpiringAction(baseUri + id.getName(), token);
|
||||||
a.href = baseUri + id.getName();
|
|
||||||
|
|
||||||
return a;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import org.slf4j.LoggerFactory;
|
|||||||
import sonia.scm.repository.Repository;
|
import sonia.scm.repository.Repository;
|
||||||
import sonia.scm.store.BlobStore;
|
import sonia.scm.store.BlobStore;
|
||||||
import sonia.scm.util.HttpUtil;
|
import sonia.scm.util.HttpUtil;
|
||||||
|
import sonia.scm.web.lfs.LfsAccessTokenFactory;
|
||||||
import sonia.scm.web.lfs.LfsBlobStoreFactory;
|
import sonia.scm.web.lfs.LfsBlobStoreFactory;
|
||||||
import sonia.scm.web.lfs.ScmBlobLfsRepository;
|
import sonia.scm.web.lfs.ScmBlobLfsRepository;
|
||||||
|
|
||||||
@@ -27,13 +28,15 @@ import javax.servlet.http.HttpServletRequest;
|
|||||||
@Singleton
|
@Singleton
|
||||||
public class LfsServletFactory {
|
public class LfsServletFactory {
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(LfsServletFactory.class);
|
private static final Logger LOG = LoggerFactory.getLogger(LfsServletFactory.class);
|
||||||
|
|
||||||
private final LfsBlobStoreFactory lfsBlobStoreFactory;
|
private final LfsBlobStoreFactory lfsBlobStoreFactory;
|
||||||
|
private final LfsAccessTokenFactory tokenFactory;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public LfsServletFactory(LfsBlobStoreFactory lfsBlobStoreFactory) {
|
public LfsServletFactory(LfsBlobStoreFactory lfsBlobStoreFactory, LfsAccessTokenFactory tokenFactory) {
|
||||||
this.lfsBlobStoreFactory = lfsBlobStoreFactory;
|
this.lfsBlobStoreFactory = lfsBlobStoreFactory;
|
||||||
|
this.tokenFactory = tokenFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -44,10 +47,11 @@ public class LfsServletFactory {
|
|||||||
* @return The {@link LfsProtocolServlet} to provide the LFS Batch API for a SCM Repository.
|
* @return The {@link LfsProtocolServlet} to provide the LFS Batch API for a SCM Repository.
|
||||||
*/
|
*/
|
||||||
public LfsProtocolServlet createProtocolServletFor(Repository repository, HttpServletRequest request) {
|
public LfsProtocolServlet createProtocolServletFor(Repository repository, HttpServletRequest request) {
|
||||||
|
LOG.trace("create lfs protocol servlet for repository {}", repository.getNamespaceAndName());
|
||||||
BlobStore blobStore = lfsBlobStoreFactory.getLfsBlobStore(repository);
|
BlobStore blobStore = lfsBlobStoreFactory.getLfsBlobStore(repository);
|
||||||
String baseUri = buildBaseUri(repository, request);
|
String baseUri = buildBaseUri(repository, request);
|
||||||
|
|
||||||
LargeFileRepository largeFileRepository = new ScmBlobLfsRepository(blobStore, baseUri);
|
LargeFileRepository largeFileRepository = new ScmBlobLfsRepository(repository, blobStore, tokenFactory, baseUri);
|
||||||
return new ScmLfsProtocolServlet(largeFileRepository);
|
return new ScmLfsProtocolServlet(largeFileRepository);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,6 +63,7 @@ public class LfsServletFactory {
|
|||||||
* @return The {@link FileLfsServlet} to provide the LFS Upload / Download API for a SCM Repository.
|
* @return The {@link FileLfsServlet} to provide the LFS Upload / Download API for a SCM Repository.
|
||||||
*/
|
*/
|
||||||
public HttpServlet createFileLfsServletFor(Repository repository, HttpServletRequest request) {
|
public HttpServlet createFileLfsServletFor(Repository repository, HttpServletRequest request) {
|
||||||
|
LOG.trace("create lfs file servlet for repository {}", repository.getNamespaceAndName());
|
||||||
return new ScmFileTransferServlet(lfsBlobStoreFactory.getLfsBlobStore(repository));
|
return new ScmFileTransferServlet(lfsBlobStoreFactory.getLfsBlobStore(repository));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
package sonia.scm.web.lfs;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import sonia.scm.config.ScmConfiguration;
|
||||||
|
import sonia.scm.protocolcommand.CommandContext;
|
||||||
|
import sonia.scm.protocolcommand.CommandInterpreter;
|
||||||
|
import sonia.scm.protocolcommand.RepositoryContext;
|
||||||
|
import sonia.scm.protocolcommand.git.GitRepositoryContextResolver;
|
||||||
|
import sonia.scm.repository.Repository;
|
||||||
|
import sonia.scm.security.AccessToken;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static java.time.Instant.parse;
|
||||||
|
import static java.util.Date.from;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.Mockito.lenient;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class LFSAuthCommandTest {
|
||||||
|
|
||||||
|
static final Repository REPOSITORY = new Repository("1", "git", "space", "X");
|
||||||
|
static final Date EXPIRATION = from(parse("2007-05-03T10:15:30.00Z"));
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
LfsAccessTokenFactory tokenFactory;
|
||||||
|
@Mock
|
||||||
|
GitRepositoryContextResolver gitRepositoryContextResolver;
|
||||||
|
@Mock
|
||||||
|
ScmConfiguration configuration;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
LFSAuthCommand lfsAuthCommand;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void initAuthorizationToken() {
|
||||||
|
AccessToken accessToken = mock(AccessToken.class);
|
||||||
|
lenient().when(this.tokenFactory.createReadAccessToken(REPOSITORY)).thenReturn(accessToken);
|
||||||
|
lenient().when(this.tokenFactory.createWriteAccessToken(REPOSITORY)).thenReturn(accessToken);
|
||||||
|
lenient().when(accessToken.getExpiration()).thenReturn(EXPIRATION);
|
||||||
|
lenient().when(accessToken.compact()).thenReturn("ACCESS_TOKEN");
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void initConfig() {
|
||||||
|
lenient().when(configuration.getBaseUrl()).thenReturn("http://example.com");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldHandleGitLfsAuthenticate() {
|
||||||
|
Optional<CommandInterpreter> commandInterpreter = lfsAuthCommand.canHandle("git-lfs-authenticate repo/space/X upload");
|
||||||
|
assertThat(commandInterpreter).isPresent();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotHandleOtherCommands() {
|
||||||
|
Optional<CommandInterpreter> commandInterpreter = lfsAuthCommand.canHandle("git-lfs-something repo/space/X upload");
|
||||||
|
assertThat(commandInterpreter).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldExtractRepositoryArgument() {
|
||||||
|
CommandInterpreter commandInterpreter = lfsAuthCommand.canHandle("git-lfs-authenticate repo/space/X\t upload").get();
|
||||||
|
assertThat(commandInterpreter.getParsedArgs()).containsOnly("repo/space/X");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCreateJsonResponse() throws IOException {
|
||||||
|
CommandInterpreter commandInterpreter = lfsAuthCommand.canHandle("git-lfs-authenticate repo/space/X\t upload").get();
|
||||||
|
CommandContext commandContext = createCommandContext();
|
||||||
|
commandInterpreter.getProtocolHandler().handle(commandContext, createRepositoryContext());
|
||||||
|
assertThat(commandContext.getOutputStream().toString())
|
||||||
|
.isEqualTo("{\"href\":\"http://example.com/repo/space/X.git/info/lfs/\",\"header\":{\"Authorization\":\"Bearer ACCESS_TOKEN\"},\"expires_at\":\"2007-05-03T10:15:30Z\"}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private CommandContext createCommandContext() {
|
||||||
|
return new CommandContext(null, null, null, new ByteArrayOutputStream(), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private RepositoryContext createRepositoryContext() {
|
||||||
|
return new RepositoryContext(REPOSITORY, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package sonia.scm.web.lfs;
|
||||||
|
|
||||||
|
import org.eclipse.jgit.lfs.lib.LongObjectId;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import sonia.scm.repository.Repository;
|
||||||
|
import sonia.scm.security.AccessToken;
|
||||||
|
import sonia.scm.store.BlobStore;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import static java.time.Instant.parse;
|
||||||
|
import static java.util.Date.from;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.eclipse.jgit.lfs.lib.LongObjectId.fromString;
|
||||||
|
import static org.mockito.Mockito.lenient;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class ScmBlobLfsRepositoryTest {
|
||||||
|
|
||||||
|
static final Repository REPOSITORY = new Repository("1", "git", "space", "X");
|
||||||
|
static final Date EXPIRATION = from(parse("2007-05-03T10:15:30.00Z"));
|
||||||
|
static final LongObjectId OBJECT_ID = fromString("976ed944c37cc5d1606af316937edb9d286ecf6c606af316937edb9d286ecf6c");
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
BlobStore blobStore;
|
||||||
|
@Mock
|
||||||
|
LfsAccessTokenFactory tokenFactory;
|
||||||
|
|
||||||
|
ScmBlobLfsRepository lfsRepository;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void initializeLfsRepository() {
|
||||||
|
lfsRepository = new ScmBlobLfsRepository(REPOSITORY, blobStore, tokenFactory, "http://scm.org/");
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void initAuthorizationToken() {
|
||||||
|
AccessToken readToken = createToken("READ_TOKEN");
|
||||||
|
lenient().when(this.tokenFactory.createReadAccessToken(REPOSITORY))
|
||||||
|
.thenReturn(readToken);
|
||||||
|
AccessToken writeToken = createToken("WRITE_TOKEN");
|
||||||
|
lenient().when(this.tokenFactory.createWriteAccessToken(REPOSITORY))
|
||||||
|
.thenReturn(writeToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
AccessToken createToken(String mockedValue) {
|
||||||
|
AccessToken accessToken = mock(AccessToken.class);
|
||||||
|
lenient().when(accessToken.getExpiration()).thenReturn(EXPIRATION);
|
||||||
|
lenient().when(accessToken.compact()).thenReturn(mockedValue);
|
||||||
|
return accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldTakeExpirationFromToken() {
|
||||||
|
ExpiringAction downloadAction = lfsRepository.getDownloadAction(OBJECT_ID);
|
||||||
|
assertThat(downloadAction.expires_at).isEqualTo("2007-05-03T10:15:30Z");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldContainReadTokenForDownlo() {
|
||||||
|
ExpiringAction downloadAction = lfsRepository.getDownloadAction(OBJECT_ID);
|
||||||
|
assertThat(downloadAction.header.get("Authorization")).isEqualTo("Bearer READ_TOKEN");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldContainWriteTokenForUpload() {
|
||||||
|
ExpiringAction downloadAction = lfsRepository.getUploadAction(OBJECT_ID, 42L);
|
||||||
|
assertThat(downloadAction.header.get("Authorization")).isEqualTo("Bearer WRITE_TOKEN");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldContainUrl() {
|
||||||
|
ExpiringAction downloadAction = lfsRepository.getDownloadAction(OBJECT_ID);
|
||||||
|
assertThat(downloadAction.href).isEqualTo("http://scm.org/976ed944c37cc5d1606af316937edb9d286ecf6c606af316937edb9d286ecf6c");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCreateTokenForDownloadActionOnlyOnce() {
|
||||||
|
lfsRepository.getDownloadAction(OBJECT_ID);
|
||||||
|
lfsRepository.getDownloadAction(OBJECT_ID);
|
||||||
|
verify(tokenFactory, times(1)).createReadAccessToken(REPOSITORY);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCreateTokenForUploadActionOnlyOnce() {
|
||||||
|
lfsRepository.getUploadAction(OBJECT_ID, 42L);
|
||||||
|
lfsRepository.getUploadAction(OBJECT_ID, 42L);
|
||||||
|
verify(tokenFactory, times(1)).createWriteAccessToken(REPOSITORY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -11,41 +11,26 @@ import static org.junit.Assert.assertThat;
|
|||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
/**
|
|
||||||
* Created by omilke on 18.05.2017.
|
|
||||||
*/
|
|
||||||
public class LfsServletFactoryTest {
|
public class LfsServletFactoryTest {
|
||||||
|
|
||||||
|
private static final String NAMESPACE = "space";
|
||||||
|
private static final String NAME = "git-lfs-demo";
|
||||||
|
private static final Repository REPOSITORY = new Repository("", "GIT", NAMESPACE, NAME);
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void buildBaseUri() {
|
public void shouldBuildBaseUri() {
|
||||||
|
String result = LfsServletFactory.buildBaseUri(REPOSITORY, requestWithUri("git-lfs-demo"));
|
||||||
String repositoryNamespace = "space";
|
|
||||||
String repositoryName = "git-lfs-demo";
|
|
||||||
|
|
||||||
String result = LfsServletFactory.buildBaseUri(new Repository("", "GIT", repositoryNamespace, repositoryName), RequestWithUri(repositoryName, true));
|
|
||||||
assertThat(result, is(equalTo("http://localhost:8081/scm/repo/space/git-lfs-demo.git/info/lfs/objects/")));
|
|
||||||
|
|
||||||
|
|
||||||
//result will be with dot-git suffix, ide
|
|
||||||
result = LfsServletFactory.buildBaseUri(new Repository("", "GIT", repositoryNamespace, repositoryName), RequestWithUri(repositoryName, false));
|
|
||||||
assertThat(result, is(equalTo("http://localhost:8081/scm/repo/space/git-lfs-demo.git/info/lfs/objects/")));
|
assertThat(result, is(equalTo("http://localhost:8081/scm/repo/space/git-lfs-demo.git/info/lfs/objects/")));
|
||||||
}
|
}
|
||||||
|
|
||||||
private HttpServletRequest RequestWithUri(String repositoryName, boolean withDotGitSuffix) {
|
private HttpServletRequest requestWithUri(String repositoryName) {
|
||||||
|
|
||||||
HttpServletRequest mockedRequest = mock(HttpServletRequest.class);
|
HttpServletRequest mockedRequest = mock(HttpServletRequest.class);
|
||||||
|
|
||||||
final String suffix;
|
|
||||||
if (withDotGitSuffix) {
|
|
||||||
suffix = ".git";
|
|
||||||
} else {
|
|
||||||
suffix = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
//build from valid live request data
|
//build from valid live request data
|
||||||
when(mockedRequest.getRequestURL()).thenReturn(
|
when(mockedRequest.getRequestURL()).thenReturn(
|
||||||
new StringBuffer(String.format("http://localhost:8081/scm/repo/%s%s/info/lfs/objects/batch", repositoryName, suffix)));
|
new StringBuffer(String.format("http://localhost:8081/scm/repo/%s/info/lfs/objects/batch", repositoryName)));
|
||||||
when(mockedRequest.getRequestURI()).thenReturn(String.format("/scm/repo/%s%s/info/lfs/objects/batch", repositoryName, suffix));
|
when(mockedRequest.getRequestURI()).thenReturn(String.format("/scm/repo/%s/info/lfs/objects/batch", repositoryName));
|
||||||
when(mockedRequest.getContextPath()).thenReturn("/scm");
|
when(mockedRequest.getContextPath()).thenReturn("/scm");
|
||||||
|
|
||||||
return mockedRequest;
|
return mockedRequest;
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2014, Sebastian Sdorra
|
* Copyright (c) 2014, Sebastian Sdorra
|
||||||
* All rights reserved.
|
* All rights reserved.
|
||||||
*
|
* <p>
|
||||||
* Redistribution and use in source and binary forms, with or without
|
* Redistribution and use in source and binary forms, with or without
|
||||||
* modification, are permitted provided that the following conditions are met:
|
* modification, are permitted provided that the following conditions are met:
|
||||||
*
|
* <p>
|
||||||
* 1. Redistributions of source code must retain the above copyright notice,
|
* 1. Redistributions of source code must retain the above copyright notice,
|
||||||
* this list of conditions and the following disclaimer.
|
* this list of conditions and the following disclaimer.
|
||||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
* 3. Neither the name of SCM-Manager; nor the names of its
|
* 3. Neither the name of SCM-Manager; nor the names of its
|
||||||
* contributors may be used to endorse or promote products derived from this
|
* contributors may be used to endorse or promote products derived from this
|
||||||
* software without specific prior written permission.
|
* software without specific prior written permission.
|
||||||
*
|
* <p>
|
||||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
@@ -24,17 +24,20 @@
|
|||||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
* 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
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
*
|
* <p>
|
||||||
* http://bitbucket.org/sdorra/scm-manager
|
* http://bitbucket.org/sdorra/scm-manager
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
package sonia.scm.security;
|
package sonia.scm.security;
|
||||||
|
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Provider;
|
import javax.inject.Provider;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
|
import com.google.inject.OutOfScopeException;
|
||||||
|
import com.google.inject.ProvisionException;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import sonia.scm.config.ScmConfiguration;
|
import sonia.scm.config.ScmConfiguration;
|
||||||
@@ -47,8 +50,8 @@ import sonia.scm.util.HttpUtil;
|
|||||||
* enabled. The xsrf field will be validated on every request by the {@link XsrfAccessTokenValidator}. Xsrf protection
|
* enabled. The xsrf field will be validated on every request by the {@link XsrfAccessTokenValidator}. Xsrf protection
|
||||||
* can be disabled with {@link ScmConfiguration#setEnabledXsrfProtection(boolean)}.
|
* can be disabled with {@link ScmConfiguration#setEnabledXsrfProtection(boolean)}.
|
||||||
*
|
*
|
||||||
* @see <a href="https://goo.gl/s67xO3">Issue 793</a>
|
|
||||||
* @author Sebastian Sdorra
|
* @author Sebastian Sdorra
|
||||||
|
* @see <a href="https://goo.gl/s67xO3">Issue 793</a>
|
||||||
* @since 2.0.0
|
* @since 2.0.0
|
||||||
*/
|
*/
|
||||||
@Extension
|
@Extension
|
||||||
@@ -77,8 +80,7 @@ public class XsrfAccessTokenEnricher implements AccessTokenEnricher {
|
|||||||
@Override
|
@Override
|
||||||
public void enrich(AccessTokenBuilder builder) {
|
public void enrich(AccessTokenBuilder builder) {
|
||||||
if (configuration.isEnabledXsrfProtection()) {
|
if (configuration.isEnabledXsrfProtection()) {
|
||||||
if (HttpUtil.isWUIRequest(requestProvider.get())) {
|
if (isEnrichable()) {
|
||||||
LOG.debug("received wui token claim, enrich jwt with xsrf key");
|
|
||||||
builder.custom(Xsrf.TOKEN_KEY, createToken());
|
builder.custom(Xsrf.TOKEN_KEY, createToken());
|
||||||
} else {
|
} else {
|
||||||
LOG.trace("skip xsrf enrichment, because jwt session is started from a non wui client");
|
LOG.trace("skip xsrf enrichment, because jwt session is started from a non wui client");
|
||||||
@@ -88,6 +90,25 @@ public class XsrfAccessTokenEnricher implements AccessTokenEnricher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isEnrichable() {
|
||||||
|
try {
|
||||||
|
HttpServletRequest request = requestProvider.get();
|
||||||
|
if (HttpUtil.isWUIRequest(request)) {
|
||||||
|
LOG.debug("received wui token claim, enrich jwt with xsrf key");
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
LOG.trace("skip xsrf enrichment, because jwt session is started from a non wui client");
|
||||||
|
}
|
||||||
|
} catch (ProvisionException ex) {
|
||||||
|
if (ex.getCause() instanceof OutOfScopeException) {
|
||||||
|
LOG.trace("skip xsrf enrichment, because no request scope is available");
|
||||||
|
} else {
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
String createToken() {
|
String createToken() {
|
||||||
// TODO create interface and use a better method
|
// TODO create interface and use a better method
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2014, Sebastian Sdorra
|
* Copyright (c) 2014, Sebastian Sdorra
|
||||||
* All rights reserved.
|
* All rights reserved.
|
||||||
*
|
* <p>
|
||||||
* Redistribution and use in source and binary forms, with or without
|
* Redistribution and use in source and binary forms, with or without
|
||||||
* modification, are permitted provided that the following conditions are met:
|
* modification, are permitted provided that the following conditions are met:
|
||||||
*
|
* <p>
|
||||||
* 1. Redistributions of source code must retain the above copyright notice,
|
* 1. Redistributions of source code must retain the above copyright notice,
|
||||||
* this list of conditions and the following disclaimer.
|
* this list of conditions and the following disclaimer.
|
||||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
* 3. Neither the name of SCM-Manager; nor the names of its
|
* 3. Neither the name of SCM-Manager; nor the names of its
|
||||||
* contributors may be used to endorse or promote products derived from this
|
* contributors may be used to endorse or promote products derived from this
|
||||||
* software without specific prior written permission.
|
* software without specific prior written permission.
|
||||||
*
|
* <p>
|
||||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
@@ -24,34 +24,36 @@
|
|||||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
* 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
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
*
|
* <p>
|
||||||
* http://bitbucket.org/sdorra/scm-manager
|
* http://bitbucket.org/sdorra/scm-manager
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package sonia.scm.security;
|
package sonia.scm.security;
|
||||||
|
|
||||||
import org.junit.Before;
|
import com.google.inject.OutOfScopeException;
|
||||||
import org.junit.Test;
|
import com.google.inject.ProvisionException;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.MockitoJUnitRunner;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
import sonia.scm.config.ScmConfiguration;
|
import sonia.scm.config.ScmConfiguration;
|
||||||
import sonia.scm.util.HttpUtil;
|
import sonia.scm.util.HttpUtil;
|
||||||
|
|
||||||
|
import javax.inject.Provider;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
import static org.mockito.Mockito.never;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.*;
|
||||||
import static org.mockito.Mockito.when;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unit tests for {@link XsrfAccessTokenEnricher}.
|
* Unit tests for {@link XsrfAccessTokenEnricher}.
|
||||||
*
|
*
|
||||||
* @author Sebastian Sdorra
|
* @author Sebastian Sdorra
|
||||||
*/
|
*/
|
||||||
@RunWith(MockitoJUnitRunner.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
public class XsrfAccessTokenEnricherTest {
|
class XsrfAccessTokenEnricherTest {
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private HttpServletRequest request;
|
private HttpServletRequest request;
|
||||||
@@ -63,13 +65,42 @@ public class XsrfAccessTokenEnricherTest {
|
|||||||
|
|
||||||
private XsrfAccessTokenEnricher enricher;
|
private XsrfAccessTokenEnricher enricher;
|
||||||
|
|
||||||
/**
|
@BeforeEach
|
||||||
* Prepare object under test.
|
void createConfiguration() {
|
||||||
*/
|
|
||||||
@Before
|
|
||||||
public void prepareObjectUnderTest() {
|
|
||||||
configuration = new ScmConfiguration();
|
configuration = new ScmConfiguration();
|
||||||
enricher = new XsrfAccessTokenEnricher(configuration, () -> request) {
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
void testWithoutRequestScope() {
|
||||||
|
// prepare
|
||||||
|
Provider<HttpServletRequest> requestProvider = mock(Provider.class);
|
||||||
|
when(requestProvider.get()).thenThrow(new ProvisionException("failed to provision", new OutOfScopeException("no request scope is available")));
|
||||||
|
configuration.setEnabledXsrfProtection(true);
|
||||||
|
XsrfAccessTokenEnricher enricher = createEnricher(requestProvider);
|
||||||
|
|
||||||
|
// execute
|
||||||
|
enricher.enrich(builder);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
verify(builder, never()).custom(Xsrf.TOKEN_KEY, "42");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
void testWithProvisionException() {
|
||||||
|
// prepare
|
||||||
|
Provider<HttpServletRequest> requestProvider = mock(Provider.class);
|
||||||
|
when(requestProvider.get()).thenThrow(new ProvisionException("failed to provision"));
|
||||||
|
configuration.setEnabledXsrfProtection(true);
|
||||||
|
XsrfAccessTokenEnricher enricher = createEnricher(requestProvider);
|
||||||
|
|
||||||
|
// execute
|
||||||
|
assertThrows(ProvisionException.class, () -> enricher.enrich(builder));
|
||||||
|
}
|
||||||
|
|
||||||
|
private XsrfAccessTokenEnricher createEnricher(Provider<HttpServletRequest> requestProvider) {
|
||||||
|
return new XsrfAccessTokenEnricher(configuration, requestProvider) {
|
||||||
@Override
|
@Override
|
||||||
String createToken() {
|
String createToken() {
|
||||||
return "42";
|
return "42";
|
||||||
@@ -77,11 +108,16 @@ public class XsrfAccessTokenEnricherTest {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Nested
|
||||||
* Tests {@link XsrfAccessTokenEnricher#enrich(java.util.Map)}.
|
class WithRequestMock {
|
||||||
*/
|
|
||||||
|
@BeforeEach
|
||||||
|
void setupEnricher() {
|
||||||
|
enricher = createEnricher(() -> request);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testEnrich() {
|
void testEnrich() {
|
||||||
// prepare
|
// prepare
|
||||||
configuration.setEnabledXsrfProtection(true);
|
configuration.setEnabledXsrfProtection(true);
|
||||||
when(request.getHeader(HttpUtil.HEADER_SCM_CLIENT)).thenReturn(HttpUtil.SCM_CLIENT_WUI);
|
when(request.getHeader(HttpUtil.HEADER_SCM_CLIENT)).thenReturn(HttpUtil.SCM_CLIENT_WUI);
|
||||||
@@ -93,11 +129,8 @@ public class XsrfAccessTokenEnricherTest {
|
|||||||
verify(builder).custom(Xsrf.TOKEN_KEY, "42");
|
verify(builder).custom(Xsrf.TOKEN_KEY, "42");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Tests {@link XsrfAccessTokenEnricher#enrich(java.util.Map)} with disabled xsrf protection.
|
|
||||||
*/
|
|
||||||
@Test
|
@Test
|
||||||
public void testEnrichWithDisabledXsrf() {
|
void testEnrichWithDisabledXsrf() {
|
||||||
// prepare
|
// prepare
|
||||||
configuration.setEnabledXsrfProtection(false);
|
configuration.setEnabledXsrfProtection(false);
|
||||||
|
|
||||||
@@ -108,11 +141,8 @@ public class XsrfAccessTokenEnricherTest {
|
|||||||
verify(builder, never()).custom(Xsrf.TOKEN_KEY, "42");
|
verify(builder, never()).custom(Xsrf.TOKEN_KEY, "42");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Tests {@link XsrfAccessTokenEnricher#enrich(java.util.Map)} with disabled xsrf protection.
|
|
||||||
*/
|
|
||||||
@Test
|
@Test
|
||||||
public void testEnrichWithNonWuiClient() {
|
void testEnrichWithNonWuiClient() {
|
||||||
// prepare
|
// prepare
|
||||||
configuration.setEnabledXsrfProtection(true);
|
configuration.setEnabledXsrfProtection(true);
|
||||||
|
|
||||||
@@ -122,5 +152,5 @@ public class XsrfAccessTokenEnricherTest {
|
|||||||
// assert
|
// assert
|
||||||
verify(builder, never()).custom(Xsrf.TOKEN_KEY, "42");
|
verify(builder, never()).custom(Xsrf.TOKEN_KEY, "42");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user