mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-09 06:55:47 +01:00
Use bearer tokens to authenticate hg hook callbacks
This commit is contained in:
@@ -9,23 +9,21 @@ import java.io.File;
|
||||
import java.io.IOException;
|
||||
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 final File poolDirectory;
|
||||
|
||||
private final CloneProvider<T, C> cloneProvider;
|
||||
private final Repository repository;
|
||||
|
||||
public SimpleWorkdirFactory(Repository repository, CloneProvider<T, C> cloneProvider) {
|
||||
this(new File(System.getProperty("java.io.tmpdir"), "scmm-work-pool"), repository, cloneProvider);
|
||||
public SimpleWorkdirFactory(CloneProvider<T, C> 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.cloneProvider = cloneProvider;
|
||||
this.repository = repository;
|
||||
poolDirectory.mkdirs();
|
||||
}
|
||||
|
||||
@@ -35,10 +33,12 @@ public class SimpleWorkdirFactory<T extends AutoCloseable, C> {
|
||||
T clone = cloneProvider.cloneRepository(context, directory);
|
||||
return new WorkingCopy<>(clone, this::close, directory);
|
||||
} 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 {
|
||||
return Files.createTempDirectory(poolDirectory.toPath(),"workdir").toFile();
|
||||
}
|
||||
|
||||
@@ -4,26 +4,20 @@ import org.eclipse.jgit.api.Git;
|
||||
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||
import org.eclipse.jgit.lib.Repository;
|
||||
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.InternalRepositoryException;
|
||||
import sonia.scm.repository.util.SimpleWorkdirFactory;
|
||||
import sonia.scm.repository.util.WorkingCopy;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
|
||||
public class SimpleGitWorkdirFactory extends SimpleWorkdirFactory<Repository, GitContext> implements GitWorkdirFactory {
|
||||
|
||||
public SimpleGitWorkdirFactory() {
|
||||
super(null, new GitCloneProvider());
|
||||
super(new GitCloneProvider());
|
||||
}
|
||||
|
||||
public SimpleGitWorkdirFactory(File poolDirectory) {
|
||||
super(poolDirectory, null, new GitCloneProvider());
|
||||
super(poolDirectory, new GitCloneProvider());
|
||||
}
|
||||
|
||||
private static class GitCloneProvider implements CloneProvider<Repository, GitContext> {
|
||||
@@ -45,4 +39,9 @@ public class SimpleGitWorkdirFactory extends SimpleWorkdirFactory<Repository, Gi
|
||||
return ScmTransportProtocol.NAME + "://" + bareRepository.getAbsolutePath();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected sonia.scm.repository.Repository getRepository(GitContext context) {
|
||||
return context.getRepository();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,11 @@
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt</artifactId>
|
||||
<version>0.4</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
|
||||
@@ -35,15 +35,9 @@ package sonia.scm.repository;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
|
||||
import org.apache.shiro.codec.Base64;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import sonia.scm.security.AccessToken;
|
||||
import sonia.scm.security.AccessTokenBuilderFactory;
|
||||
import sonia.scm.security.CipherUtil;
|
||||
import sonia.scm.util.HttpUtil;
|
||||
import sonia.scm.web.HgUtil;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
@@ -68,14 +62,7 @@ public final class HgEnvironment
|
||||
/** Field description */
|
||||
private static final String ENV_URL = "SCM_URL";
|
||||
|
||||
/** Field description */
|
||||
private static final String SCM_CREDENTIALS = "SCM_CREDENTIALS";
|
||||
|
||||
/**
|
||||
* the logger for HgEnvironment
|
||||
*/
|
||||
private static final Logger logger =
|
||||
LoggerFactory.getLogger(HgEnvironment.class);
|
||||
private static final String SCM_BEARER_TOKEN = "SCM_BEARER_TOKEN";
|
||||
|
||||
//~--- constructors ---------------------------------------------------------
|
||||
|
||||
@@ -98,14 +85,14 @@ public final class HgEnvironment
|
||||
*/
|
||||
public static void prepareEnvironment(Map<String, String> environment,
|
||||
HgRepositoryHandler handler, HgHookManager hookManager,
|
||||
HttpServletRequest request)
|
||||
HttpServletRequest request, AccessTokenBuilderFactory accessTokenBuilderFactory)
|
||||
{
|
||||
String hookUrl;
|
||||
|
||||
if (request != null)
|
||||
{
|
||||
hookUrl = hookManager.createUrl(request);
|
||||
environment.put(SCM_CREDENTIALS, getCredentials(request));
|
||||
environment.put(SCM_BEARER_TOKEN, getCredentials(accessTokenBuilderFactory));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -126,9 +113,9 @@ public final class HgEnvironment
|
||||
* @param hookManager
|
||||
*/
|
||||
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 ----------------------------------------------------------
|
||||
@@ -139,31 +126,10 @@ public final class HgEnvironment
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private static String getCredentials(HttpServletRequest request)
|
||||
private static String getCredentials(AccessTokenBuilderFactory accessTokenBuilderFactory)
|
||||
{
|
||||
String credentials = null;
|
||||
String header = request.getHeader(HttpUtil.HEADER_AUTHORIZATION);
|
||||
AccessToken accessToken = accessTokenBuilderFactory.create().build();
|
||||
|
||||
if (!Strings.isNullOrEmpty(header))
|
||||
{
|
||||
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);
|
||||
return CipherUtil.getInstance().encode(accessToken.compact());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,15 +32,12 @@ package sonia.scm.repository.spi;
|
||||
|
||||
import com.aragost.javahg.Changeset;
|
||||
import com.aragost.javahg.commands.CommitCommand;
|
||||
import com.aragost.javahg.commands.PushCommand;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.repository.Branch;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.util.WorkingCopy;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Mercurial implementation of the {@link BranchCommand}.
|
||||
* Note that this creates an empty commit to "persist" the new branch.
|
||||
@@ -57,7 +54,7 @@ public class HgBranchCommand extends AbstractCommand implements BranchCommand {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Branch branch(String name) throws IOException {
|
||||
public Branch branch(String name) {
|
||||
try (WorkingCopy<RepositoryCloseableWrapper> workingCopy = workdirFactory.createWorkingCopy(getContext())) {
|
||||
com.aragost.javahg.Repository repository = workingCopy.get().get();
|
||||
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)
|
||||
.execute();
|
||||
|
||||
PushCommand pushCommand = PushCommand
|
||||
.on(repository)
|
||||
.branch(name)
|
||||
.newBranch();
|
||||
pushCommand
|
||||
.cmdAppend("--config", "");
|
||||
pushCommand .execute();
|
||||
|
||||
LOG.debug("Created new branch '{}' in repository {} with changeset {}",
|
||||
name, getRepository().getNamespaceAndName(), emptyChangeset.getNode());
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ import com.google.common.base.Strings;
|
||||
import sonia.scm.repository.HgConfig;
|
||||
import sonia.scm.repository.HgHookManager;
|
||||
import sonia.scm.repository.HgRepositoryHandler;
|
||||
import sonia.scm.security.AccessTokenBuilderFactory;
|
||||
import sonia.scm.web.HgUtil;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
@@ -49,6 +50,8 @@ import sonia.scm.web.HgUtil;
|
||||
import java.io.Closeable;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -66,44 +69,46 @@ public class HgCommandContext implements Closeable
|
||||
* Constructs ...
|
||||
*
|
||||
*
|
||||
*
|
||||
* @param hookManager
|
||||
* @param handler
|
||||
* @param repository
|
||||
* @param directory
|
||||
* @param accessTokenBuilderFactory
|
||||
*/
|
||||
public HgCommandContext(HgHookManager hookManager,
|
||||
HgRepositoryHandler handler, sonia.scm.repository.Repository repository,
|
||||
File directory)
|
||||
File directory, AccessTokenBuilderFactory accessTokenBuilderFactory)
|
||||
{
|
||||
this(hookManager, handler, repository, directory,
|
||||
handler.getHgContext().isPending());
|
||||
handler.getHgContext().isPending(), accessTokenBuilderFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs ...
|
||||
*
|
||||
*
|
||||
*
|
||||
* @param hookManager
|
||||
* @param hanlder
|
||||
* @param handler
|
||||
* @param repository
|
||||
* @param directory
|
||||
* @param pending
|
||||
* @param accessTokenBuilderFactory
|
||||
*/
|
||||
public HgCommandContext(HgHookManager hookManager,
|
||||
HgRepositoryHandler hanlder, sonia.scm.repository.Repository repository,
|
||||
File directory, boolean pending)
|
||||
HgRepositoryHandler handler, sonia.scm.repository.Repository repository,
|
||||
File directory, boolean pending, AccessTokenBuilderFactory accessTokenBuilderFactory)
|
||||
{
|
||||
this.hookManager = hookManager;
|
||||
this.hanlder = hanlder;
|
||||
this.handler = handler;
|
||||
this.directory = directory;
|
||||
this.scmRepository = repository;
|
||||
this.encoding = repository.getProperty(PROPERTY_ENCODING);
|
||||
this.pending = pending;
|
||||
this.accessTokenBuilderFactory = accessTokenBuilderFactory;
|
||||
|
||||
if (Strings.isNullOrEmpty(encoding))
|
||||
{
|
||||
encoding = hanlder.getConfig().getEncoding();
|
||||
encoding = handler.getConfig().getEncoding();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,13 +139,19 @@ public class HgCommandContext implements Closeable
|
||||
{
|
||||
if (repository == null)
|
||||
{
|
||||
repository = HgUtil.open(hanlder, hookManager, directory, encoding,
|
||||
pending);
|
||||
repository = HgUtil.open(handler, hookManager, directory, encoding,
|
||||
pending, accessTokenBuilderFactory);
|
||||
}
|
||||
|
||||
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 ----------------------------------------------------------
|
||||
|
||||
/**
|
||||
@@ -151,7 +162,7 @@ public class HgCommandContext implements Closeable
|
||||
*/
|
||||
public HgConfig getConfig()
|
||||
{
|
||||
return hanlder.getConfig();
|
||||
return handler.getConfig();
|
||||
}
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
@@ -163,7 +174,7 @@ public class HgCommandContext implements Closeable
|
||||
private String encoding;
|
||||
|
||||
/** Field description */
|
||||
private HgRepositoryHandler hanlder;
|
||||
private HgRepositoryHandler handler;
|
||||
|
||||
/** Field description */
|
||||
private HgHookManager hookManager;
|
||||
@@ -173,4 +184,8 @@ public class HgCommandContext implements Closeable
|
||||
|
||||
/** Field description */
|
||||
private Repository repository;
|
||||
|
||||
private final sonia.scm.repository.Repository scmRepository;
|
||||
|
||||
private final AccessTokenBuilderFactory accessTokenBuilderFactory;
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ import sonia.scm.repository.HgHookManager;
|
||||
import sonia.scm.repository.HgRepositoryHandler;
|
||||
import sonia.scm.repository.RepositoryHookType;
|
||||
import sonia.scm.repository.spi.javahg.HgLogChangesetCommand;
|
||||
import sonia.scm.security.AccessTokenBuilderFactory;
|
||||
import sonia.scm.web.HgUtil;
|
||||
|
||||
import java.io.File;
|
||||
@@ -63,13 +64,14 @@ public class HgHookChangesetProvider implements HookChangesetProvider
|
||||
|
||||
public HgHookChangesetProvider(HgRepositoryHandler handler,
|
||||
File repositoryDirectory, HgHookManager hookManager, String startRev,
|
||||
RepositoryHookType type)
|
||||
RepositoryHookType type, AccessTokenBuilderFactory accessTokenBuilderFactory)
|
||||
{
|
||||
this.handler = handler;
|
||||
this.repositoryDirectory = repositoryDirectory;
|
||||
this.hookManager = hookManager;
|
||||
this.startRev = startRev;
|
||||
this.type = type;
|
||||
this.accessTokenBuilderFactory = accessTokenBuilderFactory;
|
||||
}
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
@@ -128,7 +130,7 @@ public class HgHookChangesetProvider implements HookChangesetProvider
|
||||
|
||||
// TODO get repository encoding
|
||||
return HgUtil.open(handler, hookManager, repositoryDirectory, null,
|
||||
pending);
|
||||
pending, accessTokenBuilderFactory);
|
||||
}
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
@@ -150,4 +152,6 @@ public class HgHookChangesetProvider implements HookChangesetProvider
|
||||
|
||||
/** Field description */
|
||||
private RepositoryHookType type;
|
||||
|
||||
private final AccessTokenBuilderFactory accessTokenBuilderFactory;
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ import sonia.scm.repository.api.HookBranchProvider;
|
||||
import sonia.scm.repository.api.HookFeature;
|
||||
import sonia.scm.repository.api.HookMessageProvider;
|
||||
import sonia.scm.repository.api.HookTagProvider;
|
||||
import sonia.scm.security.AccessTokenBuilderFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.EnumSet;
|
||||
@@ -75,9 +76,9 @@ public class HgHookContextProvider extends HookContextProvider
|
||||
*/
|
||||
public HgHookContextProvider(HgRepositoryHandler handler,
|
||||
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 ----------------------------------------------------------
|
||||
|
||||
@@ -40,6 +40,7 @@ import sonia.scm.repository.HgRepositoryHandler;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.api.Command;
|
||||
import sonia.scm.repository.api.CommandNotSupportedException;
|
||||
import sonia.scm.security.AccessTokenBuilderFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@@ -77,13 +78,13 @@ public class HgRepositoryServiceProvider extends RepositoryServiceProvider
|
||||
//~--- constructors ---------------------------------------------------------
|
||||
|
||||
HgRepositoryServiceProvider(HgRepositoryHandler handler,
|
||||
HgHookManager hookManager, Repository repository)
|
||||
HgHookManager hookManager, Repository repository, AccessTokenBuilderFactory accessTokenBuilderFactory)
|
||||
{
|
||||
this.repository = repository;
|
||||
this.handler = handler;
|
||||
this.repositoryDirectory = handler.getDirectory(repository.getId());
|
||||
this.context = new HgCommandContext(hookManager, handler, repository,
|
||||
repositoryDirectory);
|
||||
repositoryDirectory, accessTokenBuilderFactory);
|
||||
}
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
@@ -38,6 +38,7 @@ import sonia.scm.plugin.Extension;
|
||||
import sonia.scm.repository.HgHookManager;
|
||||
import sonia.scm.repository.HgRepositoryHandler;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.security.AccessTokenBuilderFactory;
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -47,15 +48,17 @@ import sonia.scm.repository.Repository;
|
||||
public class HgRepositoryServiceResolver implements RepositoryServiceResolver
|
||||
{
|
||||
|
||||
private HgRepositoryHandler handler;
|
||||
private HgHookManager hookManager;
|
||||
private final HgRepositoryHandler handler;
|
||||
private final HgHookManager hookManager;
|
||||
private final AccessTokenBuilderFactory accessTokenBuilderFactory;
|
||||
|
||||
@Inject
|
||||
public HgRepositoryServiceResolver(HgRepositoryHandler handler,
|
||||
HgHookManager hookManager)
|
||||
HgHookManager hookManager, AccessTokenBuilderFactory accessTokenBuilderFactory)
|
||||
{
|
||||
this.handler = handler;
|
||||
this.hookManager = hookManager;
|
||||
this.accessTokenBuilderFactory = accessTokenBuilderFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -63,7 +66,7 @@ public class HgRepositoryServiceResolver implements RepositoryServiceResolver
|
||||
HgRepositoryServiceProvider provider = null;
|
||||
|
||||
if (HgRepositoryHandler.TYPE_NAME.equalsIgnoreCase(repository.getType())) {
|
||||
provider = new HgRepositoryServiceProvider(handler, hookManager, repository);
|
||||
provider = new HgRepositoryServiceProvider(handler, hookManager, repository, accessTokenBuilderFactory);
|
||||
}
|
||||
|
||||
return provider;
|
||||
|
||||
@@ -2,35 +2,65 @@ package sonia.scm.repository.spi;
|
||||
|
||||
import com.aragost.javahg.Repository;
|
||||
import com.aragost.javahg.commands.CloneCommand;
|
||||
import com.aragost.javahg.commands.PullCommand;
|
||||
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.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 SimpleHgWorkdirFactory() {
|
||||
super(null, new HgCloneProvider());
|
||||
|
||||
@Inject
|
||||
public SimpleHgWorkdirFactory(Provider<HgRepositoryEnvironmentBuilder> hgRepositoryEnvironmentBuilder) {
|
||||
this(hgRepositoryEnvironmentBuilder, new HookConfigurer());
|
||||
}
|
||||
|
||||
public SimpleHgWorkdirFactory(File poolDirectory) {
|
||||
super(poolDirectory, null, new HgCloneProvider());
|
||||
SimpleHgWorkdirFactory(Provider<HgRepositoryEnvironmentBuilder> hgRepositoryEnvironmentBuilder, Consumer<PullCommand> hookConfigurer) {
|
||||
super(new HgCloneProvider(hgRepositoryEnvironmentBuilder, hookConfigurer));
|
||||
}
|
||||
|
||||
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
|
||||
public RepositoryCloseableWrapper cloneRepository(HgCommandContext context, File target) throws IOException {
|
||||
String execute = CloneCommand.on(context.open()).execute(target.getAbsolutePath());
|
||||
return new RepositoryCloseableWrapper(Repository.open(target));
|
||||
BiConsumer<sonia.scm.repository.Repository, Map<String, String>> repositoryMapBiConsumer = (repository, environment) -> {
|
||||
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 {
|
||||
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.centralRepository = centralRepository;
|
||||
this.hookConfigurer = hookConfigurer;
|
||||
}
|
||||
|
||||
Repository get() {
|
||||
@@ -39,5 +69,21 @@ class RepositoryCloseableWrapper implements AutoCloseable {
|
||||
|
||||
@Override
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,7 +201,7 @@ public class HgCGIServlet extends HttpServlet implements ScmProviderHttpServlet
|
||||
executor.setExceptionHandler(exceptionHandler);
|
||||
executor.setStatusCodeHandler(exceptionHandler);
|
||||
executor.setContentLengthWorkaround(true);
|
||||
hgRepositoryEnvironmentBuilder.buildFor(repository, request, executor.getEnvironment());
|
||||
hgRepositoryEnvironmentBuilder.buildFor(repository, request, executor.getEnvironment().asMutableMap());
|
||||
|
||||
// unused ???
|
||||
HttpSession session = request.getSession(false);
|
||||
|
||||
@@ -42,6 +42,7 @@ import com.google.inject.Inject;
|
||||
import com.google.inject.Provider;
|
||||
import com.google.inject.Singleton;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.apache.shiro.authc.AuthenticationToken;
|
||||
import org.apache.shiro.subject.Subject;
|
||||
import org.slf4j.Logger;
|
||||
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.spi.HgHookContextProvider;
|
||||
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.Tokens;
|
||||
import sonia.scm.util.HttpUtil;
|
||||
import sonia.scm.util.Util;
|
||||
|
||||
@@ -93,7 +95,7 @@ public class HgHookCallbackServlet extends HttpServlet
|
||||
private static final String PARAM_CHALLENGE = "challenge";
|
||||
|
||||
/** Field description */
|
||||
private static final String PARAM_CREDENTIALS = "credentials";
|
||||
private static final String PARAM_TOKEN = "token";
|
||||
|
||||
/** Field description */
|
||||
private static final String PARAM_NODE = "node";
|
||||
@@ -117,12 +119,13 @@ public class HgHookCallbackServlet extends HttpServlet
|
||||
@Inject
|
||||
public HgHookCallbackServlet(HookEventFacade hookEventFacade,
|
||||
HgRepositoryHandler handler, HgHookManager hookManager,
|
||||
Provider<HgContext> contextProvider)
|
||||
Provider<HgContext> contextProvider, AccessTokenBuilderFactory accessTokenBuilderFactory)
|
||||
{
|
||||
this.hookEventFacade = hookEventFacade;
|
||||
this.handler = handler;
|
||||
this.hookManager = hookManager;
|
||||
this.contextProvider = contextProvider;
|
||||
this.accessTokenBuilderFactory = accessTokenBuilderFactory;
|
||||
}
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
@@ -179,11 +182,11 @@ public class HgHookCallbackServlet extends HttpServlet
|
||||
|
||||
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);
|
||||
@@ -209,34 +212,20 @@ public class HgHookCallbackServlet extends HttpServlet
|
||||
}
|
||||
}
|
||||
|
||||
private void authenticate(HttpServletRequest request, String credentials)
|
||||
private void authenticate(String token)
|
||||
{
|
||||
try
|
||||
{
|
||||
credentials = CipherUtil.getInstance().decode(credentials);
|
||||
token = CipherUtil.getInstance().decode(token);
|
||||
|
||||
if (Util.isNotEmpty(credentials))
|
||||
{
|
||||
int index = credentials.indexOf(':');
|
||||
|
||||
if (index > 0 && index < credentials.length())
|
||||
if (Util.isNotEmpty(token))
|
||||
{
|
||||
Subject subject = SecurityUtils.getSubject();
|
||||
|
||||
AuthenticationToken accessToken = createToken(token);
|
||||
|
||||
//J-
|
||||
subject.login(
|
||||
Tokens.createAuthenticationToken(
|
||||
request,
|
||||
credentials.substring(0, index),
|
||||
credentials.substring(index + 1)
|
||||
)
|
||||
);
|
||||
//J+
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.error("could not find delimiter");
|
||||
}
|
||||
subject.login(accessToken);
|
||||
}
|
||||
}
|
||||
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)
|
||||
throws IOException
|
||||
{
|
||||
@@ -259,7 +253,7 @@ public class HgHookCallbackServlet extends HttpServlet
|
||||
|
||||
File repositoryDirectory = handler.getDirectory(repositoryId);
|
||||
context = new HgHookContextProvider(handler, repositoryDirectory, hookManager,
|
||||
node, type);
|
||||
node, type, accessTokenBuilderFactory);
|
||||
|
||||
hookEventFacade.handle(repositoryId).fireHookEvent(type, context);
|
||||
|
||||
@@ -460,4 +454,6 @@ public class HgHookCallbackServlet extends HttpServlet
|
||||
|
||||
/** Field description */
|
||||
private final HgHookManager hookManager;
|
||||
|
||||
private final AccessTokenBuilderFactory accessTokenBuilderFactory;
|
||||
}
|
||||
|
||||
@@ -1,96 +1,73 @@
|
||||
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.HgHookManager;
|
||||
import sonia.scm.repository.HgRepositoryHandler;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.security.AccessToken;
|
||||
import sonia.scm.security.AccessTokenBuilderFactory;
|
||||
import sonia.scm.security.CipherUtil;
|
||||
import sonia.scm.util.HttpUtil;
|
||||
import sonia.scm.web.cgi.EnvList;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.io.File;
|
||||
import java.util.Base64;
|
||||
import java.util.Map;
|
||||
|
||||
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_PATH = "SCM_REPOSITORY_PATH";
|
||||
private static final String ENV_REPOSITORY_ID = "SCM_REPOSITORY_ID";
|
||||
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 SCM_CREDENTIALS = "SCM_CREDENTIALS";
|
||||
private static final String SCM_BEARER_TOKEN = "SCM_BEARER_TOKEN";
|
||||
|
||||
private final HgRepositoryHandler handler;
|
||||
private final HgHookManager hookManager;
|
||||
private final AccessTokenBuilderFactory accessTokenBuilderFactory;
|
||||
|
||||
@Inject
|
||||
public HgRepositoryEnvironmentBuilder(HgRepositoryHandler handler, HgHookManager hookManager) {
|
||||
public HgRepositoryEnvironmentBuilder(HgRepositoryHandler handler, HgHookManager hookManager, AccessTokenBuilderFactory accessTokenBuilderFactory) {
|
||||
this.handler = handler;
|
||||
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());
|
||||
|
||||
environment.set(ENV_REPOSITORY_NAME, repository.getNamespace() + "/" + repository.getName());
|
||||
environment.set(ENV_REPOSITORY_ID, repository.getId());
|
||||
environment.set(ENV_REPOSITORY_PATH,
|
||||
environment.put(ENV_REPOSITORY_NAME, repository.getNamespace() + "/" + repository.getName());
|
||||
environment.put(ENV_REPOSITORY_ID, repository.getId());
|
||||
environment.put(ENV_REPOSITORY_PATH,
|
||||
directory.getAbsolutePath());
|
||||
|
||||
// add hook environment
|
||||
Map<String, String> environmentMap = environment.asMutableMap();
|
||||
if (handler.getConfig().isDisableHookSSLValidation()) {
|
||||
// disable ssl validation
|
||||
// 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
|
||||
// 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(
|
||||
environmentMap,
|
||||
environment,
|
||||
handler,
|
||||
hookManager,
|
||||
request
|
||||
request,
|
||||
accessTokenBuilderFactory
|
||||
);
|
||||
|
||||
addCredentials(environment, request);
|
||||
addCredentials(environment);
|
||||
}
|
||||
|
||||
private void addCredentials(EnvList env, HttpServletRequest request)
|
||||
{
|
||||
String authorization = request.getHeader(HttpUtil.HEADER_AUTHORIZATION);
|
||||
private void addCredentials(Map<String, String> env) {
|
||||
|
||||
if (!Strings.isNullOrEmpty(authorization))
|
||||
{
|
||||
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));
|
||||
AccessToken accessToken = accessTokenBuilderFactory.create().build();
|
||||
|
||||
env.set(SCM_CREDENTIALS, CipherUtil.getInstance().encode(userInfo));
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG.warn("unknown authentication scheme used");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG.trace("no authorization header found");
|
||||
}
|
||||
String encodedToken = CipherUtil.getInstance().encode(accessToken.compact());
|
||||
env.put(SCM_BEARER_TOKEN, encodedToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ import sonia.scm.repository.HgHookManager;
|
||||
import sonia.scm.repository.HgPythonScript;
|
||||
import sonia.scm.repository.HgRepositoryHandler;
|
||||
import sonia.scm.repository.spi.javahg.HgFileviewExtension;
|
||||
import sonia.scm.security.AccessTokenBuilderFactory;
|
||||
import sonia.scm.util.HttpUtil;
|
||||
import sonia.scm.util.Util;
|
||||
|
||||
@@ -58,6 +59,8 @@ import sonia.scm.util.Util;
|
||||
import java.io.File;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
@@ -102,7 +105,21 @@ public final class HgUtil
|
||||
* @return
|
||||
*/
|
||||
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;
|
||||
|
||||
@@ -113,8 +130,7 @@ public final class HgUtil
|
||||
|
||||
RepositoryConfiguration repoConfiguration = RepositoryConfiguration.DEFAULT;
|
||||
|
||||
HgEnvironment.prepareEnvironment(repoConfiguration.getEnvironment(),
|
||||
handler, hookManager);
|
||||
prepareEnvironment.accept(repoConfiguration.getEnvironment());
|
||||
|
||||
repoConfiguration.addExtension(HgFileviewExtension.class);
|
||||
repoConfiguration.setEnablePendingChangesets(pending);
|
||||
|
||||
@@ -40,7 +40,7 @@ import os, urllib, urllib2
|
||||
|
||||
baseUrl = os.environ['SCM_URL']
|
||||
challenge = os.environ['SCM_CHALLENGE']
|
||||
credentials = os.environ['SCM_CREDENTIALS']
|
||||
token = os.environ['SCM_BEARER_TOKEN']
|
||||
repositoryId = os.environ['SCM_REPOSITORY_ID']
|
||||
|
||||
def printMessages(ui, msgs):
|
||||
@@ -54,13 +54,13 @@ def callHookUrl(ui, repo, hooktype, node):
|
||||
try:
|
||||
url = baseUrl + hooktype
|
||||
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
|
||||
proxy_handler = urllib2.ProxyHandler({})
|
||||
opener = urllib2.build_opener(proxy_handler)
|
||||
req = urllib2.Request(url, data)
|
||||
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" )
|
||||
printMessages(ui, conn)
|
||||
abort = False
|
||||
|
||||
@@ -83,7 +83,7 @@ public class AbstractHgCommandTestBase extends ZippedRepositoryTestBase
|
||||
HgTestUtil.checkForSkip(handler);
|
||||
|
||||
cmdContext = new HgCommandContext(HgTestUtil.createHookManager(), handler,
|
||||
RepositoryTestData.createHeartOfGold(), repositoryDirectory);
|
||||
RepositoryTestData.createHeartOfGold(), repositoryDirectory, null);
|
||||
}
|
||||
|
||||
//~--- set methods ----------------------------------------------------------
|
||||
|
||||
@@ -1,18 +1,41 @@
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import com.google.inject.util.Providers;
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.junit.Test;
|
||||
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 static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class HgBranchCommandTest extends AbstractHgCommandTestBase {
|
||||
@Test
|
||||
public void x() throws IOException {
|
||||
public void shouldCreateBranch() {
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -129,6 +129,6 @@ public class HgIncomingCommandTest extends IncomingOutgoingTestBase
|
||||
return new HgIncomingCommand(
|
||||
new HgCommandContext(
|
||||
HgTestUtil.createHookManager(), handler, incomingRepository,
|
||||
incomingDirectory), incomingRepository, handler);
|
||||
incomingDirectory, null), incomingRepository, handler);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ public class HgModificationsCommandTest extends IncomingOutgoingTestBase {
|
||||
|
||||
@Before
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -125,6 +125,6 @@ public class HgOutgoingCommandTest extends IncomingOutgoingTestBase
|
||||
return new HgOutgoingCommand(
|
||||
new HgCommandContext(
|
||||
HgTestUtil.createHookManager(), handler, outgoingRepository,
|
||||
outgoingDirectory), outgoingRepository, handler);
|
||||
outgoingDirectory, null), outgoingRepository, handler);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ public class HgHookCallbackServletTest {
|
||||
@Test
|
||||
public void shouldExtractCorrectRepositoryId() throws ServletException, IOException {
|
||||
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);
|
||||
HttpServletResponse response = mock(HttpServletResponse.class);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user