Use bearer tokens to authenticate hg hook callbacks

This commit is contained in:
René Pfeuffer
2019-03-28 08:40:56 +01:00
parent daaa50f08c
commit cc4bd6ddd1
22 changed files with 232 additions and 191 deletions

View File

@@ -9,23 +9,21 @@ import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
public class SimpleWorkdirFactory<T extends AutoCloseable, C> { public abstract class SimpleWorkdirFactory<T extends AutoCloseable, C> {
private static final Logger logger = LoggerFactory.getLogger(SimpleWorkdirFactory.class); private static final Logger logger = LoggerFactory.getLogger(SimpleWorkdirFactory.class);
private final File poolDirectory; private final File poolDirectory;
private final CloneProvider<T, C> cloneProvider; private final CloneProvider<T, C> cloneProvider;
private final Repository repository;
public SimpleWorkdirFactory(Repository repository, CloneProvider<T, C> cloneProvider) { public SimpleWorkdirFactory(CloneProvider<T, C> cloneProvider) {
this(new File(System.getProperty("java.io.tmpdir"), "scmm-work-pool"), repository, cloneProvider); this(new File(System.getProperty("java.io.tmpdir"), "scmm-work-pool"), cloneProvider);
} }
public SimpleWorkdirFactory(File poolDirectory, Repository repository, CloneProvider<T, C> cloneProvider) { public SimpleWorkdirFactory(File poolDirectory, CloneProvider<T, C> cloneProvider) {
this.poolDirectory = poolDirectory; this.poolDirectory = poolDirectory;
this.cloneProvider = cloneProvider; this.cloneProvider = cloneProvider;
this.repository = repository;
poolDirectory.mkdirs(); poolDirectory.mkdirs();
} }
@@ -35,10 +33,12 @@ public class SimpleWorkdirFactory<T extends AutoCloseable, C> {
T clone = cloneProvider.cloneRepository(context, directory); T clone = cloneProvider.cloneRepository(context, directory);
return new WorkingCopy<>(clone, this::close, directory); return new WorkingCopy<>(clone, this::close, directory);
} catch (IOException e) { } catch (IOException e) {
throw new InternalRepositoryException(repository, "could not create temporary directory for clone of repository", e); throw new InternalRepositoryException(getRepository(context), "could not create temporary directory for clone of repository", e);
} }
} }
protected abstract Repository getRepository(C context);
private File createNewWorkdir() throws IOException { private File createNewWorkdir() throws IOException {
return Files.createTempDirectory(poolDirectory.toPath(),"workdir").toFile(); return Files.createTempDirectory(poolDirectory.toPath(),"workdir").toFile();
} }

View File

@@ -4,26 +4,20 @@ import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.transport.ScmTransportProtocol; import org.eclipse.jgit.transport.ScmTransportProtocol;
import org.eclipse.jgit.util.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.repository.GitWorkdirFactory; import sonia.scm.repository.GitWorkdirFactory;
import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.util.SimpleWorkdirFactory; import sonia.scm.repository.util.SimpleWorkdirFactory;
import sonia.scm.repository.util.WorkingCopy;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
public class SimpleGitWorkdirFactory extends SimpleWorkdirFactory<Repository, GitContext> implements GitWorkdirFactory { public class SimpleGitWorkdirFactory extends SimpleWorkdirFactory<Repository, GitContext> implements GitWorkdirFactory {
public SimpleGitWorkdirFactory() { public SimpleGitWorkdirFactory() {
super(null, new GitCloneProvider()); super(new GitCloneProvider());
} }
public SimpleGitWorkdirFactory(File poolDirectory) { public SimpleGitWorkdirFactory(File poolDirectory) {
super(poolDirectory, null, new GitCloneProvider()); super(poolDirectory, new GitCloneProvider());
} }
private static class GitCloneProvider implements CloneProvider<Repository, GitContext> { private static class GitCloneProvider implements CloneProvider<Repository, GitContext> {
@@ -45,4 +39,9 @@ public class SimpleGitWorkdirFactory extends SimpleWorkdirFactory<Repository, Gi
return ScmTransportProtocol.NAME + "://" + bareRepository.getAbsolutePath(); return ScmTransportProtocol.NAME + "://" + bareRepository.getAbsolutePath();
} }
} }
@Override
protected sonia.scm.repository.Repository getRepository(GitContext context) {
return context.getRepository();
}
} }

View File

@@ -28,6 +28,11 @@
</exclusion> </exclusion>
</exclusions> </exclusions>
</dependency> </dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.4</version>
</dependency>
</dependencies> </dependencies>

View File

@@ -35,15 +35,9 @@ package sonia.scm.repository;
//~--- non-JDK imports -------------------------------------------------------- //~--- non-JDK imports --------------------------------------------------------
import com.google.common.base.Strings; import sonia.scm.security.AccessToken;
import sonia.scm.security.AccessTokenBuilderFactory;
import org.apache.shiro.codec.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.security.CipherUtil; import sonia.scm.security.CipherUtil;
import sonia.scm.util.HttpUtil;
import sonia.scm.web.HgUtil; import sonia.scm.web.HgUtil;
//~--- JDK imports ------------------------------------------------------------ //~--- JDK imports ------------------------------------------------------------
@@ -68,14 +62,7 @@ public final class HgEnvironment
/** Field description */ /** Field description */
private static final String ENV_URL = "SCM_URL"; private static final String ENV_URL = "SCM_URL";
/** Field description */ private static final String SCM_BEARER_TOKEN = "SCM_BEARER_TOKEN";
private static final String SCM_CREDENTIALS = "SCM_CREDENTIALS";
/**
* the logger for HgEnvironment
*/
private static final Logger logger =
LoggerFactory.getLogger(HgEnvironment.class);
//~--- constructors --------------------------------------------------------- //~--- constructors ---------------------------------------------------------
@@ -98,14 +85,14 @@ public final class HgEnvironment
*/ */
public static void prepareEnvironment(Map<String, String> environment, public static void prepareEnvironment(Map<String, String> environment,
HgRepositoryHandler handler, HgHookManager hookManager, HgRepositoryHandler handler, HgHookManager hookManager,
HttpServletRequest request) HttpServletRequest request, AccessTokenBuilderFactory accessTokenBuilderFactory)
{ {
String hookUrl; String hookUrl;
if (request != null) if (request != null)
{ {
hookUrl = hookManager.createUrl(request); hookUrl = hookManager.createUrl(request);
environment.put(SCM_CREDENTIALS, getCredentials(request)); environment.put(SCM_BEARER_TOKEN, getCredentials(accessTokenBuilderFactory));
} }
else else
{ {
@@ -126,9 +113,9 @@ public final class HgEnvironment
* @param hookManager * @param hookManager
*/ */
public static void prepareEnvironment(Map<String, String> environment, public static void prepareEnvironment(Map<String, String> environment,
HgRepositoryHandler handler, HgHookManager hookManager) HgRepositoryHandler handler, HgHookManager hookManager, AccessTokenBuilderFactory accessTokenBuilderFactory)
{ {
prepareEnvironment(environment, handler, hookManager, null); prepareEnvironment(environment, handler, hookManager, null, accessTokenBuilderFactory);
} }
//~--- get methods ---------------------------------------------------------- //~--- get methods ----------------------------------------------------------
@@ -139,31 +126,10 @@ public final class HgEnvironment
* *
* @return * @return
*/ */
private static String getCredentials(HttpServletRequest request) private static String getCredentials(AccessTokenBuilderFactory accessTokenBuilderFactory)
{ {
String credentials = null; AccessToken accessToken = accessTokenBuilderFactory.create().build();
String header = request.getHeader(HttpUtil.HEADER_AUTHORIZATION);
if (!Strings.isNullOrEmpty(header)) return CipherUtil.getInstance().encode(accessToken.compact());
{
String[] parts = header.split("\\s+");
if (parts.length > 0)
{
CipherUtil cu = CipherUtil.getInstance();
credentials = cu.encode(Base64.decodeToString(parts[1]));
}
else
{
logger.warn("invalid basic authentication header");
}
}
else
{
logger.warn("could not find authentication header on request");
}
return Strings.nullToEmpty(credentials);
} }
} }

View File

@@ -32,15 +32,12 @@ package sonia.scm.repository.spi;
import com.aragost.javahg.Changeset; import com.aragost.javahg.Changeset;
import com.aragost.javahg.commands.CommitCommand; import com.aragost.javahg.commands.CommitCommand;
import com.aragost.javahg.commands.PushCommand;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.repository.Branch; import sonia.scm.repository.Branch;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.util.WorkingCopy; import sonia.scm.repository.util.WorkingCopy;
import java.io.IOException;
/** /**
* Mercurial implementation of the {@link BranchCommand}. * Mercurial implementation of the {@link BranchCommand}.
* Note that this creates an empty commit to "persist" the new branch. * Note that this creates an empty commit to "persist" the new branch.
@@ -57,7 +54,7 @@ public class HgBranchCommand extends AbstractCommand implements BranchCommand {
} }
@Override @Override
public Branch branch(String name) throws IOException { public Branch branch(String name) {
try (WorkingCopy<RepositoryCloseableWrapper> workingCopy = workdirFactory.createWorkingCopy(getContext())) { try (WorkingCopy<RepositoryCloseableWrapper> workingCopy = workdirFactory.createWorkingCopy(getContext())) {
com.aragost.javahg.Repository repository = workingCopy.get().get(); com.aragost.javahg.Repository repository = workingCopy.get().get();
com.aragost.javahg.commands.BranchCommand.on(repository).set(name); com.aragost.javahg.commands.BranchCommand.on(repository).set(name);
@@ -68,14 +65,6 @@ public class HgBranchCommand extends AbstractCommand implements BranchCommand {
.message("Create new branch " + name) .message("Create new branch " + name)
.execute(); .execute();
PushCommand pushCommand = PushCommand
.on(repository)
.branch(name)
.newBranch();
pushCommand
.cmdAppend("--config", "");
pushCommand .execute();
LOG.debug("Created new branch '{}' in repository {} with changeset {}", LOG.debug("Created new branch '{}' in repository {} with changeset {}",
name, getRepository().getNamespaceAndName(), emptyChangeset.getNode()); name, getRepository().getNamespaceAndName(), emptyChangeset.getNode());

View File

@@ -42,6 +42,7 @@ import com.google.common.base.Strings;
import sonia.scm.repository.HgConfig; import sonia.scm.repository.HgConfig;
import sonia.scm.repository.HgHookManager; import sonia.scm.repository.HgHookManager;
import sonia.scm.repository.HgRepositoryHandler; import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.security.AccessTokenBuilderFactory;
import sonia.scm.web.HgUtil; import sonia.scm.web.HgUtil;
//~--- JDK imports ------------------------------------------------------------ //~--- JDK imports ------------------------------------------------------------
@@ -49,6 +50,8 @@ import sonia.scm.web.HgUtil;
import java.io.Closeable; import java.io.Closeable;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.Map;
import java.util.function.BiConsumer;
/** /**
* *
@@ -66,44 +69,46 @@ public class HgCommandContext implements Closeable
* Constructs ... * Constructs ...
* *
* *
* * @param hookManager
* @param hookManager
* @param handler * @param handler
* @param repository * @param repository
* @param directory * @param directory
* @param accessTokenBuilderFactory
*/ */
public HgCommandContext(HgHookManager hookManager, public HgCommandContext(HgHookManager hookManager,
HgRepositoryHandler handler, sonia.scm.repository.Repository repository, HgRepositoryHandler handler, sonia.scm.repository.Repository repository,
File directory) File directory, AccessTokenBuilderFactory accessTokenBuilderFactory)
{ {
this(hookManager, handler, repository, directory, this(hookManager, handler, repository, directory,
handler.getHgContext().isPending()); handler.getHgContext().isPending(), accessTokenBuilderFactory);
} }
/** /**
* Constructs ... * Constructs ...
* *
* *
* * @param hookManager
* @param hookManager * @param handler
* @param hanlder
* @param repository * @param repository
* @param directory * @param directory
* @param pending * @param pending
* @param accessTokenBuilderFactory
*/ */
public HgCommandContext(HgHookManager hookManager, public HgCommandContext(HgHookManager hookManager,
HgRepositoryHandler hanlder, sonia.scm.repository.Repository repository, HgRepositoryHandler handler, sonia.scm.repository.Repository repository,
File directory, boolean pending) File directory, boolean pending, AccessTokenBuilderFactory accessTokenBuilderFactory)
{ {
this.hookManager = hookManager; this.hookManager = hookManager;
this.hanlder = hanlder; this.handler = handler;
this.directory = directory; this.directory = directory;
this.scmRepository = repository;
this.encoding = repository.getProperty(PROPERTY_ENCODING); this.encoding = repository.getProperty(PROPERTY_ENCODING);
this.pending = pending; this.pending = pending;
this.accessTokenBuilderFactory = accessTokenBuilderFactory;
if (Strings.isNullOrEmpty(encoding)) if (Strings.isNullOrEmpty(encoding))
{ {
encoding = hanlder.getConfig().getEncoding(); encoding = handler.getConfig().getEncoding();
} }
} }
@@ -134,13 +139,19 @@ public class HgCommandContext implements Closeable
{ {
if (repository == null) if (repository == null)
{ {
repository = HgUtil.open(hanlder, hookManager, directory, encoding, repository = HgUtil.open(handler, hookManager, directory, encoding,
pending); pending, accessTokenBuilderFactory);
} }
return repository; return repository;
} }
public Repository openWithSpecialEnvironment(BiConsumer<sonia.scm.repository.Repository, Map<String, String>> prepareEnvironment)
{
return HgUtil.open(handler, directory, encoding,
pending, environment -> prepareEnvironment.accept(scmRepository, environment));
}
//~--- get methods ---------------------------------------------------------- //~--- get methods ----------------------------------------------------------
/** /**
@@ -151,7 +162,7 @@ public class HgCommandContext implements Closeable
*/ */
public HgConfig getConfig() public HgConfig getConfig()
{ {
return hanlder.getConfig(); return handler.getConfig();
} }
//~--- fields --------------------------------------------------------------- //~--- fields ---------------------------------------------------------------
@@ -163,7 +174,7 @@ public class HgCommandContext implements Closeable
private String encoding; private String encoding;
/** Field description */ /** Field description */
private HgRepositoryHandler hanlder; private HgRepositoryHandler handler;
/** Field description */ /** Field description */
private HgHookManager hookManager; private HgHookManager hookManager;
@@ -173,4 +184,8 @@ public class HgCommandContext implements Closeable
/** Field description */ /** Field description */
private Repository repository; private Repository repository;
private final sonia.scm.repository.Repository scmRepository;
private final AccessTokenBuilderFactory accessTokenBuilderFactory;
} }

View File

@@ -40,6 +40,7 @@ import sonia.scm.repository.HgHookManager;
import sonia.scm.repository.HgRepositoryHandler; import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.repository.RepositoryHookType; import sonia.scm.repository.RepositoryHookType;
import sonia.scm.repository.spi.javahg.HgLogChangesetCommand; import sonia.scm.repository.spi.javahg.HgLogChangesetCommand;
import sonia.scm.security.AccessTokenBuilderFactory;
import sonia.scm.web.HgUtil; import sonia.scm.web.HgUtil;
import java.io.File; import java.io.File;
@@ -62,14 +63,15 @@ public class HgHookChangesetProvider implements HookChangesetProvider
//~--- constructors --------------------------------------------------------- //~--- constructors ---------------------------------------------------------
public HgHookChangesetProvider(HgRepositoryHandler handler, public HgHookChangesetProvider(HgRepositoryHandler handler,
File repositoryDirectory, HgHookManager hookManager, String startRev, File repositoryDirectory, HgHookManager hookManager, String startRev,
RepositoryHookType type) RepositoryHookType type, AccessTokenBuilderFactory accessTokenBuilderFactory)
{ {
this.handler = handler; this.handler = handler;
this.repositoryDirectory = repositoryDirectory; this.repositoryDirectory = repositoryDirectory;
this.hookManager = hookManager; this.hookManager = hookManager;
this.startRev = startRev; this.startRev = startRev;
this.type = type; this.type = type;
this.accessTokenBuilderFactory = accessTokenBuilderFactory;
} }
//~--- methods -------------------------------------------------------------- //~--- methods --------------------------------------------------------------
@@ -128,7 +130,7 @@ public class HgHookChangesetProvider implements HookChangesetProvider
// TODO get repository encoding // TODO get repository encoding
return HgUtil.open(handler, hookManager, repositoryDirectory, null, return HgUtil.open(handler, hookManager, repositoryDirectory, null,
pending); pending, accessTokenBuilderFactory);
} }
//~--- fields --------------------------------------------------------------- //~--- fields ---------------------------------------------------------------
@@ -150,4 +152,6 @@ public class HgHookChangesetProvider implements HookChangesetProvider
/** Field description */ /** Field description */
private RepositoryHookType type; private RepositoryHookType type;
private final AccessTokenBuilderFactory accessTokenBuilderFactory;
} }

View File

@@ -43,6 +43,7 @@ import sonia.scm.repository.api.HookBranchProvider;
import sonia.scm.repository.api.HookFeature; import sonia.scm.repository.api.HookFeature;
import sonia.scm.repository.api.HookMessageProvider; import sonia.scm.repository.api.HookMessageProvider;
import sonia.scm.repository.api.HookTagProvider; import sonia.scm.repository.api.HookTagProvider;
import sonia.scm.security.AccessTokenBuilderFactory;
import java.io.File; import java.io.File;
import java.util.EnumSet; import java.util.EnumSet;
@@ -75,9 +76,9 @@ public class HgHookContextProvider extends HookContextProvider
*/ */
public HgHookContextProvider(HgRepositoryHandler handler, public HgHookContextProvider(HgRepositoryHandler handler,
File repositoryDirectory, HgHookManager hookManager, String startRev, File repositoryDirectory, HgHookManager hookManager, String startRev,
RepositoryHookType type) RepositoryHookType type, AccessTokenBuilderFactory accessTokenBuilderFactory)
{ {
this.hookChangesetProvider = new HgHookChangesetProvider(handler, repositoryDirectory, hookManager, startRev, type); this.hookChangesetProvider = new HgHookChangesetProvider(handler, repositoryDirectory, hookManager, startRev, type, accessTokenBuilderFactory);
} }
//~--- get methods ---------------------------------------------------------- //~--- get methods ----------------------------------------------------------

View File

@@ -40,6 +40,7 @@ import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.api.Command; import sonia.scm.repository.api.Command;
import sonia.scm.repository.api.CommandNotSupportedException; import sonia.scm.repository.api.CommandNotSupportedException;
import sonia.scm.security.AccessTokenBuilderFactory;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@@ -77,13 +78,13 @@ public class HgRepositoryServiceProvider extends RepositoryServiceProvider
//~--- constructors --------------------------------------------------------- //~--- constructors ---------------------------------------------------------
HgRepositoryServiceProvider(HgRepositoryHandler handler, HgRepositoryServiceProvider(HgRepositoryHandler handler,
HgHookManager hookManager, Repository repository) HgHookManager hookManager, Repository repository, AccessTokenBuilderFactory accessTokenBuilderFactory)
{ {
this.repository = repository; this.repository = repository;
this.handler = handler; this.handler = handler;
this.repositoryDirectory = handler.getDirectory(repository.getId()); this.repositoryDirectory = handler.getDirectory(repository.getId());
this.context = new HgCommandContext(hookManager, handler, repository, this.context = new HgCommandContext(hookManager, handler, repository,
repositoryDirectory); repositoryDirectory, accessTokenBuilderFactory);
} }
//~--- methods -------------------------------------------------------------- //~--- methods --------------------------------------------------------------

View File

@@ -38,6 +38,7 @@ import sonia.scm.plugin.Extension;
import sonia.scm.repository.HgHookManager; import sonia.scm.repository.HgHookManager;
import sonia.scm.repository.HgRepositoryHandler; import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.security.AccessTokenBuilderFactory;
/** /**
* *
@@ -47,15 +48,17 @@ import sonia.scm.repository.Repository;
public class HgRepositoryServiceResolver implements RepositoryServiceResolver public class HgRepositoryServiceResolver implements RepositoryServiceResolver
{ {
private HgRepositoryHandler handler; private final HgRepositoryHandler handler;
private HgHookManager hookManager; private final HgHookManager hookManager;
private final AccessTokenBuilderFactory accessTokenBuilderFactory;
@Inject @Inject
public HgRepositoryServiceResolver(HgRepositoryHandler handler, public HgRepositoryServiceResolver(HgRepositoryHandler handler,
HgHookManager hookManager) HgHookManager hookManager, AccessTokenBuilderFactory accessTokenBuilderFactory)
{ {
this.handler = handler; this.handler = handler;
this.hookManager = hookManager; this.hookManager = hookManager;
this.accessTokenBuilderFactory = accessTokenBuilderFactory;
} }
@Override @Override
@@ -63,7 +66,7 @@ public class HgRepositoryServiceResolver implements RepositoryServiceResolver
HgRepositoryServiceProvider provider = null; HgRepositoryServiceProvider provider = null;
if (HgRepositoryHandler.TYPE_NAME.equalsIgnoreCase(repository.getType())) { if (HgRepositoryHandler.TYPE_NAME.equalsIgnoreCase(repository.getType())) {
provider = new HgRepositoryServiceProvider(handler, hookManager, repository); provider = new HgRepositoryServiceProvider(handler, hookManager, repository, accessTokenBuilderFactory);
} }
return provider; return provider;

View File

@@ -2,35 +2,65 @@ package sonia.scm.repository.spi;
import com.aragost.javahg.Repository; import com.aragost.javahg.Repository;
import com.aragost.javahg.commands.CloneCommand; import com.aragost.javahg.commands.CloneCommand;
import com.aragost.javahg.commands.PullCommand;
import sonia.scm.repository.util.SimpleWorkdirFactory; import sonia.scm.repository.util.SimpleWorkdirFactory;
import sonia.scm.web.HgRepositoryEnvironmentBuilder;
import javax.inject.Inject;
import javax.inject.Provider;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
public class SimpleHgWorkdirFactory extends SimpleWorkdirFactory<RepositoryCloseableWrapper, HgCommandContext> implements HgWorkdirFactory { public class SimpleHgWorkdirFactory extends SimpleWorkdirFactory<RepositoryCloseableWrapper, HgCommandContext> implements HgWorkdirFactory {
public SimpleHgWorkdirFactory() {
super(null, new HgCloneProvider()); @Inject
public SimpleHgWorkdirFactory(Provider<HgRepositoryEnvironmentBuilder> hgRepositoryEnvironmentBuilder) {
this(hgRepositoryEnvironmentBuilder, new HookConfigurer());
} }
public SimpleHgWorkdirFactory(File poolDirectory) { SimpleHgWorkdirFactory(Provider<HgRepositoryEnvironmentBuilder> hgRepositoryEnvironmentBuilder, Consumer<PullCommand> hookConfigurer) {
super(poolDirectory, null, new HgCloneProvider()); super(new HgCloneProvider(hgRepositoryEnvironmentBuilder, hookConfigurer));
} }
private static class HgCloneProvider implements CloneProvider<RepositoryCloseableWrapper, HgCommandContext> { private static class HgCloneProvider implements CloneProvider<RepositoryCloseableWrapper, HgCommandContext> {
private final Provider<HgRepositoryEnvironmentBuilder> hgRepositoryEnvironmentBuilder;
private final Consumer<PullCommand> hookConfigurer;
private HgCloneProvider(Provider<HgRepositoryEnvironmentBuilder> hgRepositoryEnvironmentBuilder, Consumer<PullCommand> hookConfigurer) {
this.hgRepositoryEnvironmentBuilder = hgRepositoryEnvironmentBuilder;
this.hookConfigurer = hookConfigurer;
}
@Override @Override
public RepositoryCloseableWrapper cloneRepository(HgCommandContext context, File target) throws IOException { public RepositoryCloseableWrapper cloneRepository(HgCommandContext context, File target) throws IOException {
String execute = CloneCommand.on(context.open()).execute(target.getAbsolutePath()); BiConsumer<sonia.scm.repository.Repository, Map<String, String>> repositoryMapBiConsumer = (repository, environment) -> {
return new RepositoryCloseableWrapper(Repository.open(target)); hgRepositoryEnvironmentBuilder.get().buildFor(repository, null, environment);
};
Repository centralRepository = context.openWithSpecialEnvironment(repositoryMapBiConsumer);
CloneCommand.on(centralRepository).execute(target.getAbsolutePath());
return new RepositoryCloseableWrapper(Repository.open(target), centralRepository, hookConfigurer);
} }
} }
@Override
protected sonia.scm.repository.Repository getRepository(HgCommandContext context) {
return null;
}
} }
class RepositoryCloseableWrapper implements AutoCloseable { class RepositoryCloseableWrapper implements AutoCloseable {
private final Repository delegate; private final Repository delegate;
private final Repository centralRepository;
private final Consumer<PullCommand> hookConfigurer;
RepositoryCloseableWrapper(Repository delegate) { RepositoryCloseableWrapper(Repository delegate, Repository centralRepository, Consumer<PullCommand> hookConfigurer) {
this.delegate = delegate; this.delegate = delegate;
this.centralRepository = centralRepository;
this.hookConfigurer = hookConfigurer;
} }
Repository get() { Repository get() {
@@ -39,5 +69,21 @@ class RepositoryCloseableWrapper implements AutoCloseable {
@Override @Override
public void close() { public void close() {
try {
PullCommand pullCommand = PullCommand.on(centralRepository);
hookConfigurer.accept(pullCommand);
pullCommand.execute(delegate.getDirectory().getAbsolutePath());
} catch (Exception e) {
throw new RuntimeException(e);
}
centralRepository.close();
}
}
class HookConfigurer implements Consumer<PullCommand> {
@Override
public void accept(PullCommand pullCommand) {
pullCommand.cmdAppend("--config", "hooks.changegroup.scm=python:scmhooks.postHook");
pullCommand.cmdAppend("--config", "hooks.pretxnchangegroup.scm=python:scmhooks.preHook");
} }
} }

View File

@@ -201,7 +201,7 @@ public class HgCGIServlet extends HttpServlet implements ScmProviderHttpServlet
executor.setExceptionHandler(exceptionHandler); executor.setExceptionHandler(exceptionHandler);
executor.setStatusCodeHandler(exceptionHandler); executor.setStatusCodeHandler(exceptionHandler);
executor.setContentLengthWorkaround(true); executor.setContentLengthWorkaround(true);
hgRepositoryEnvironmentBuilder.buildFor(repository, request, executor.getEnvironment()); hgRepositoryEnvironmentBuilder.buildFor(repository, request, executor.getEnvironment().asMutableMap());
// unused ??? // unused ???
HttpSession session = request.getSession(false); HttpSession session = request.getSession(false);

View File

@@ -42,6 +42,7 @@ import com.google.inject.Inject;
import com.google.inject.Provider; import com.google.inject.Provider;
import com.google.inject.Singleton; import com.google.inject.Singleton;
import org.apache.shiro.SecurityUtils; import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.subject.Subject; import org.apache.shiro.subject.Subject;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -54,8 +55,9 @@ import sonia.scm.repository.api.HgHookMessage;
import sonia.scm.repository.api.HgHookMessage.Severity; import sonia.scm.repository.api.HgHookMessage.Severity;
import sonia.scm.repository.spi.HgHookContextProvider; import sonia.scm.repository.spi.HgHookContextProvider;
import sonia.scm.repository.spi.HookEventFacade; import sonia.scm.repository.spi.HookEventFacade;
import sonia.scm.security.AccessTokenBuilderFactory;
import sonia.scm.security.BearerToken;
import sonia.scm.security.CipherUtil; import sonia.scm.security.CipherUtil;
import sonia.scm.security.Tokens;
import sonia.scm.util.HttpUtil; import sonia.scm.util.HttpUtil;
import sonia.scm.util.Util; import sonia.scm.util.Util;
@@ -93,7 +95,7 @@ public class HgHookCallbackServlet extends HttpServlet
private static final String PARAM_CHALLENGE = "challenge"; private static final String PARAM_CHALLENGE = "challenge";
/** Field description */ /** Field description */
private static final String PARAM_CREDENTIALS = "credentials"; private static final String PARAM_TOKEN = "token";
/** Field description */ /** Field description */
private static final String PARAM_NODE = "node"; private static final String PARAM_NODE = "node";
@@ -117,12 +119,13 @@ public class HgHookCallbackServlet extends HttpServlet
@Inject @Inject
public HgHookCallbackServlet(HookEventFacade hookEventFacade, public HgHookCallbackServlet(HookEventFacade hookEventFacade,
HgRepositoryHandler handler, HgHookManager hookManager, HgRepositoryHandler handler, HgHookManager hookManager,
Provider<HgContext> contextProvider) Provider<HgContext> contextProvider, AccessTokenBuilderFactory accessTokenBuilderFactory)
{ {
this.hookEventFacade = hookEventFacade; this.hookEventFacade = hookEventFacade;
this.handler = handler; this.handler = handler;
this.hookManager = hookManager; this.hookManager = hookManager;
this.contextProvider = contextProvider; this.contextProvider = contextProvider;
this.accessTokenBuilderFactory = accessTokenBuilderFactory;
} }
//~--- methods -------------------------------------------------------------- //~--- methods --------------------------------------------------------------
@@ -179,11 +182,11 @@ public class HgHookCallbackServlet extends HttpServlet
if (Util.isNotEmpty(node)) if (Util.isNotEmpty(node))
{ {
String credentials = request.getParameter(PARAM_CREDENTIALS); String token = request.getParameter(PARAM_TOKEN);
if (Util.isNotEmpty(credentials)) if (Util.isNotEmpty(token))
{ {
authenticate(request, credentials); authenticate(token);
} }
hookCallback(response, type, repositoryId, challenge, node); hookCallback(response, type, repositoryId, challenge, node);
@@ -209,34 +212,20 @@ public class HgHookCallbackServlet extends HttpServlet
} }
} }
private void authenticate(HttpServletRequest request, String credentials) private void authenticate(String token)
{ {
try try
{ {
credentials = CipherUtil.getInstance().decode(credentials); token = CipherUtil.getInstance().decode(token);
if (Util.isNotEmpty(credentials)) if (Util.isNotEmpty(token))
{ {
int index = credentials.indexOf(':'); Subject subject = SecurityUtils.getSubject();
if (index > 0 && index < credentials.length()) AuthenticationToken accessToken = createToken(token);
{
Subject subject = SecurityUtils.getSubject();
//J- //J-
subject.login( subject.login(accessToken);
Tokens.createAuthenticationToken(
request,
credentials.substring(0, index),
credentials.substring(index + 1)
)
);
//J+
}
else
{
logger.error("could not find delimiter");
}
} }
} }
catch (Exception ex) catch (Exception ex)
@@ -245,6 +234,11 @@ public class HgHookCallbackServlet extends HttpServlet
} }
} }
private AuthenticationToken createToken(String tokenString)
{
return BearerToken.valueOf(tokenString);
}
private void fireHook(HttpServletResponse response, String repositoryId, String node, RepositoryHookType type) private void fireHook(HttpServletResponse response, String repositoryId, String node, RepositoryHookType type)
throws IOException throws IOException
{ {
@@ -259,7 +253,7 @@ public class HgHookCallbackServlet extends HttpServlet
File repositoryDirectory = handler.getDirectory(repositoryId); File repositoryDirectory = handler.getDirectory(repositoryId);
context = new HgHookContextProvider(handler, repositoryDirectory, hookManager, context = new HgHookContextProvider(handler, repositoryDirectory, hookManager,
node, type); node, type, accessTokenBuilderFactory);
hookEventFacade.handle(repositoryId).fireHookEvent(type, context); hookEventFacade.handle(repositoryId).fireHookEvent(type, context);
@@ -460,4 +454,6 @@ public class HgHookCallbackServlet extends HttpServlet
/** Field description */ /** Field description */
private final HgHookManager hookManager; private final HgHookManager hookManager;
private final AccessTokenBuilderFactory accessTokenBuilderFactory;
} }

View File

@@ -1,96 +1,73 @@
package sonia.scm.web; package sonia.scm.web;
import com.google.common.base.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.repository.HgEnvironment; import sonia.scm.repository.HgEnvironment;
import sonia.scm.repository.HgHookManager; import sonia.scm.repository.HgHookManager;
import sonia.scm.repository.HgRepositoryHandler; import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.security.AccessToken;
import sonia.scm.security.AccessTokenBuilderFactory;
import sonia.scm.security.CipherUtil; import sonia.scm.security.CipherUtil;
import sonia.scm.util.HttpUtil;
import sonia.scm.web.cgi.EnvList;
import javax.inject.Inject; import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import java.io.File; import java.io.File;
import java.util.Base64;
import java.util.Map; import java.util.Map;
public class HgRepositoryEnvironmentBuilder { public class HgRepositoryEnvironmentBuilder {
private static final Logger LOG = LoggerFactory.getLogger(HgRepositoryEnvironmentBuilder.class);
private static final String ENV_REPOSITORY_NAME = "REPO_NAME"; private static final String ENV_REPOSITORY_NAME = "REPO_NAME";
private static final String ENV_REPOSITORY_PATH = "SCM_REPOSITORY_PATH"; private static final String ENV_REPOSITORY_PATH = "SCM_REPOSITORY_PATH";
private static final String ENV_REPOSITORY_ID = "SCM_REPOSITORY_ID"; private static final String ENV_REPOSITORY_ID = "SCM_REPOSITORY_ID";
private static final String ENV_PYTHON_HTTPS_VERIFY = "PYTHONHTTPSVERIFY"; private static final String ENV_PYTHON_HTTPS_VERIFY = "PYTHONHTTPSVERIFY";
private static final String ENV_HTTP_POST_ARGS = "SCM_HTTP_POST_ARGS"; private static final String ENV_HTTP_POST_ARGS = "SCM_HTTP_POST_ARGS";
private static final String SCM_CREDENTIALS = "SCM_CREDENTIALS"; private static final String SCM_BEARER_TOKEN = "SCM_BEARER_TOKEN";
private final HgRepositoryHandler handler; private final HgRepositoryHandler handler;
private final HgHookManager hookManager; private final HgHookManager hookManager;
private final AccessTokenBuilderFactory accessTokenBuilderFactory;
@Inject @Inject
public HgRepositoryEnvironmentBuilder(HgRepositoryHandler handler, HgHookManager hookManager) { public HgRepositoryEnvironmentBuilder(HgRepositoryHandler handler, HgHookManager hookManager, AccessTokenBuilderFactory accessTokenBuilderFactory) {
this.handler = handler; this.handler = handler;
this.hookManager = hookManager; this.hookManager = hookManager;
this.accessTokenBuilderFactory = accessTokenBuilderFactory;
} }
void buildFor(Repository repository, HttpServletRequest request, EnvList environment) { public void buildFor(Repository repository, HttpServletRequest request, Map<String, String> environment) {
File directory = handler.getDirectory(repository.getId()); File directory = handler.getDirectory(repository.getId());
environment.set(ENV_REPOSITORY_NAME, repository.getNamespace() + "/" + repository.getName()); environment.put(ENV_REPOSITORY_NAME, repository.getNamespace() + "/" + repository.getName());
environment.set(ENV_REPOSITORY_ID, repository.getId()); environment.put(ENV_REPOSITORY_ID, repository.getId());
environment.set(ENV_REPOSITORY_PATH, environment.put(ENV_REPOSITORY_PATH,
directory.getAbsolutePath()); directory.getAbsolutePath());
// add hook environment // add hook environment
Map<String, String> environmentMap = environment.asMutableMap();
if (handler.getConfig().isDisableHookSSLValidation()) { if (handler.getConfig().isDisableHookSSLValidation()) {
// disable ssl validation // disable ssl validation
// Issue 959: https://goo.gl/zH5eY8 // Issue 959: https://goo.gl/zH5eY8
environmentMap.put(ENV_PYTHON_HTTPS_VERIFY, "0"); environment.put(ENV_PYTHON_HTTPS_VERIFY, "0");
} }
// enable experimental httppostargs protocol of mercurial // enable experimental httppostargs protocol of mercurial
// Issue 970: https://goo.gl/poascp // Issue 970: https://goo.gl/poascp
environmentMap.put(ENV_HTTP_POST_ARGS, String.valueOf(handler.getConfig().isEnableHttpPostArgs())); environment.put(ENV_HTTP_POST_ARGS, String.valueOf(handler.getConfig().isEnableHttpPostArgs()));
HgEnvironment.prepareEnvironment( HgEnvironment.prepareEnvironment(
environmentMap, environment,
handler, handler,
hookManager, hookManager,
request request,
accessTokenBuilderFactory
); );
addCredentials(environment, request); addCredentials(environment);
} }
private void addCredentials(EnvList env, HttpServletRequest request) private void addCredentials(Map<String, String> env) {
{
String authorization = request.getHeader(HttpUtil.HEADER_AUTHORIZATION);
if (!Strings.isNullOrEmpty(authorization)) AccessToken accessToken = accessTokenBuilderFactory.create().build();
{
if (authorization.startsWith(HttpUtil.AUTHORIZATION_SCHEME_BASIC))
{
String encodedUserInfo =
authorization.substring(
HttpUtil.AUTHORIZATION_SCHEME_BASIC.length()).trim();
// TODO check encoding of user-agent ?
String userInfo = new String(Base64.getDecoder().decode(encodedUserInfo));
env.set(SCM_CREDENTIALS, CipherUtil.getInstance().encode(userInfo)); String encodedToken = CipherUtil.getInstance().encode(accessToken.compact());
} env.put(SCM_BEARER_TOKEN, encodedToken);
else
{
LOG.warn("unknown authentication scheme used");
}
}
else
{
LOG.trace("no authorization header found");
}
} }
} }

View File

@@ -50,6 +50,7 @@ import sonia.scm.repository.HgHookManager;
import sonia.scm.repository.HgPythonScript; import sonia.scm.repository.HgPythonScript;
import sonia.scm.repository.HgRepositoryHandler; import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.repository.spi.javahg.HgFileviewExtension; import sonia.scm.repository.spi.javahg.HgFileviewExtension;
import sonia.scm.security.AccessTokenBuilderFactory;
import sonia.scm.util.HttpUtil; import sonia.scm.util.HttpUtil;
import sonia.scm.util.Util; import sonia.scm.util.Util;
@@ -58,6 +59,8 @@ import sonia.scm.util.Util;
import java.io.File; import java.io.File;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.Map;
import java.util.function.Consumer;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
@@ -102,7 +105,21 @@ public final class HgUtil
* @return * @return
*/ */
public static Repository open(HgRepositoryHandler handler, public static Repository open(HgRepositoryHandler handler,
HgHookManager hookManager, File directory, String encoding, boolean pending) HgHookManager hookManager, File directory, String encoding, boolean pending,
AccessTokenBuilderFactory accessTokenBuilderFactory)
{
return open(
handler,
directory,
encoding,
pending,
environment -> HgEnvironment.prepareEnvironment(environment, handler, hookManager, accessTokenBuilderFactory)
);
}
public static Repository open(HgRepositoryHandler handler,
File directory, String encoding, boolean pending,
Consumer<Map<String, String>> prepareEnvironment)
{ {
String enc = encoding; String enc = encoding;
@@ -113,8 +130,7 @@ public final class HgUtil
RepositoryConfiguration repoConfiguration = RepositoryConfiguration.DEFAULT; RepositoryConfiguration repoConfiguration = RepositoryConfiguration.DEFAULT;
HgEnvironment.prepareEnvironment(repoConfiguration.getEnvironment(), prepareEnvironment.accept(repoConfiguration.getEnvironment());
handler, hookManager);
repoConfiguration.addExtension(HgFileviewExtension.class); repoConfiguration.addExtension(HgFileviewExtension.class);
repoConfiguration.setEnablePendingChangesets(pending); repoConfiguration.setEnablePendingChangesets(pending);

View File

@@ -40,7 +40,7 @@ import os, urllib, urllib2
baseUrl = os.environ['SCM_URL'] baseUrl = os.environ['SCM_URL']
challenge = os.environ['SCM_CHALLENGE'] challenge = os.environ['SCM_CHALLENGE']
credentials = os.environ['SCM_CREDENTIALS'] token = os.environ['SCM_BEARER_TOKEN']
repositoryId = os.environ['SCM_REPOSITORY_ID'] repositoryId = os.environ['SCM_REPOSITORY_ID']
def printMessages(ui, msgs): def printMessages(ui, msgs):
@@ -54,13 +54,13 @@ def callHookUrl(ui, repo, hooktype, node):
try: try:
url = baseUrl + hooktype url = baseUrl + hooktype
ui.debug( "send scm-hook to " + url + " and " + node + "\n" ) ui.debug( "send scm-hook to " + url + " and " + node + "\n" )
data = urllib.urlencode({'node': node, 'challenge': challenge, 'credentials': credentials, 'repositoryPath': repo.root, 'repositoryId': repositoryId}) data = urllib.urlencode({'node': node, 'challenge': challenge, 'token': token, 'repositoryPath': repo.root, 'repositoryId': repositoryId})
# open url but ignore proxy settings # open url but ignore proxy settings
proxy_handler = urllib2.ProxyHandler({}) proxy_handler = urllib2.ProxyHandler({})
opener = urllib2.build_opener(proxy_handler) opener = urllib2.build_opener(proxy_handler)
req = urllib2.Request(url, data) req = urllib2.Request(url, data)
conn = opener.open(req) conn = opener.open(req)
if conn.code >= 200 and conn.code < 300: if 200 <= conn.code < 300:
ui.debug( "scm-hook " + hooktype + " success with status code " + str(conn.code) + "\n" ) ui.debug( "scm-hook " + hooktype + " success with status code " + str(conn.code) + "\n" )
printMessages(ui, conn) printMessages(ui, conn)
abort = False abort = False

View File

@@ -83,7 +83,7 @@ public class AbstractHgCommandTestBase extends ZippedRepositoryTestBase
HgTestUtil.checkForSkip(handler); HgTestUtil.checkForSkip(handler);
cmdContext = new HgCommandContext(HgTestUtil.createHookManager(), handler, cmdContext = new HgCommandContext(HgTestUtil.createHookManager(), handler,
RepositoryTestData.createHeartOfGold(), repositoryDirectory); RepositoryTestData.createHeartOfGold(), repositoryDirectory, null);
} }
//~--- set methods ---------------------------------------------------------- //~--- set methods ----------------------------------------------------------

View File

@@ -1,18 +1,41 @@
package sonia.scm.repository.spi; package sonia.scm.repository.spi;
import com.google.inject.util.Providers;
import org.assertj.core.api.Assertions; import org.assertj.core.api.Assertions;
import org.junit.Test; import org.junit.Test;
import sonia.scm.repository.Branch; import sonia.scm.repository.Branch;
import sonia.scm.repository.HgHookManager;
import sonia.scm.security.AccessToken;
import sonia.scm.security.AccessTokenBuilder;
import sonia.scm.security.AccessTokenBuilderFactory;
import sonia.scm.web.HgRepositoryEnvironmentBuilder;
import java.io.IOException;
import java.util.List; import java.util.List;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class HgBranchCommandTest extends AbstractHgCommandTestBase { public class HgBranchCommandTest extends AbstractHgCommandTestBase {
@Test @Test
public void x() throws IOException { public void shouldCreateBranch() {
Assertions.assertThat(readBranches()).filteredOn(b -> b.getName().equals("new_branch")).isEmpty(); Assertions.assertThat(readBranches()).filteredOn(b -> b.getName().equals("new_branch")).isEmpty();
new HgBranchCommand(cmdContext, repository, new SimpleHgWorkdirFactory()).branch("new_branch"); HgHookManager hookManager = mock(HgHookManager.class);
when(hookManager.getChallenge()).thenReturn("1");
when(hookManager.createUrl()).thenReturn("http://example.com/");
AccessTokenBuilderFactory tokenBuilderFactory = mock(AccessTokenBuilderFactory.class);
AccessTokenBuilder tokenBuilder = mock(AccessTokenBuilder.class);
when(tokenBuilderFactory.create()).thenReturn(tokenBuilder);
AccessToken accessToken = mock(AccessToken.class);
when(tokenBuilder.build()).thenReturn(accessToken);
when(accessToken.compact()).thenReturn("");
HgRepositoryEnvironmentBuilder hgRepositoryEnvironmentBuilder =
new HgRepositoryEnvironmentBuilder(handler, hookManager, tokenBuilderFactory);
SimpleHgWorkdirFactory workdirFactory = new SimpleHgWorkdirFactory(Providers.of(hgRepositoryEnvironmentBuilder), pc -> {});
new HgBranchCommand(cmdContext, repository, workdirFactory).branch("new_branch");
Assertions.assertThat(readBranches()).filteredOn(b -> b.getName().equals("new_branch")).isNotEmpty(); Assertions.assertThat(readBranches()).filteredOn(b -> b.getName().equals("new_branch")).isNotEmpty();
} }

View File

@@ -129,6 +129,6 @@ public class HgIncomingCommandTest extends IncomingOutgoingTestBase
return new HgIncomingCommand( return new HgIncomingCommand(
new HgCommandContext( new HgCommandContext(
HgTestUtil.createHookManager(), handler, incomingRepository, HgTestUtil.createHookManager(), handler, incomingRepository,
incomingDirectory), incomingRepository, handler); incomingDirectory, null), incomingRepository, handler);
} }
} }

View File

@@ -19,7 +19,7 @@ public class HgModificationsCommandTest extends IncomingOutgoingTestBase {
@Before @Before
public void init() { public void init() {
HgCommandContext outgoingContext = new HgCommandContext(HgTestUtil.createHookManager(), handler, outgoingRepository, outgoingDirectory); HgCommandContext outgoingContext = new HgCommandContext(HgTestUtil.createHookManager(), handler, outgoingRepository, outgoingDirectory, null);
outgoingModificationsCommand = new HgModificationsCommand(outgoingContext, outgoingRepository); outgoingModificationsCommand = new HgModificationsCommand(outgoingContext, outgoingRepository);
} }

View File

@@ -125,6 +125,6 @@ public class HgOutgoingCommandTest extends IncomingOutgoingTestBase
return new HgOutgoingCommand( return new HgOutgoingCommand(
new HgCommandContext( new HgCommandContext(
HgTestUtil.createHookManager(), handler, outgoingRepository, HgTestUtil.createHookManager(), handler, outgoingRepository,
outgoingDirectory), outgoingRepository, handler); outgoingDirectory, null), outgoingRepository, handler);
} }
} }

View File

@@ -20,7 +20,7 @@ public class HgHookCallbackServletTest {
@Test @Test
public void shouldExtractCorrectRepositoryId() throws ServletException, IOException { public void shouldExtractCorrectRepositoryId() throws ServletException, IOException {
HgRepositoryHandler handler = mock(HgRepositoryHandler.class); HgRepositoryHandler handler = mock(HgRepositoryHandler.class);
HgHookCallbackServlet servlet = new HgHookCallbackServlet(null, handler, null, null); HgHookCallbackServlet servlet = new HgHookCallbackServlet(null, handler, null, null, null);
HttpServletRequest request = mock(HttpServletRequest.class); HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class); HttpServletResponse response = mock(HttpServletResponse.class);