mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-10 15:35:49 +01:00
merge branch m2.0.0-3
This commit is contained in:
@@ -1,24 +0,0 @@
|
||||
package sonia.scm;
|
||||
|
||||
import javax.servlet.RequestDispatcher;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* This dispatcher forwards every request to the index.html of the application.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class ForwardingPushStateDispatcher implements PushStateDispatcher {
|
||||
@Override
|
||||
public void dispatch(HttpServletRequest request, HttpServletResponse response, String uri) throws IOException {
|
||||
RequestDispatcher dispatcher = request.getRequestDispatcher("/index.html");
|
||||
try {
|
||||
dispatcher.forward(request, response);
|
||||
} catch (ServletException e) {
|
||||
throw new IOException("failed to forward request", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,13 @@ package sonia.scm;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.base.Strings;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Provider;
|
||||
|
||||
/**
|
||||
* Injection Provider for the {@link PushStateDispatcher}. The provider will return a {@link ProxyPushStateDispatcher}
|
||||
* if the system property {@code PushStateDispatcherProvider#PROPERTY_TARGET} is set to a proxy target url, otherwise
|
||||
* a {@link ForwardingPushStateDispatcher} is used.
|
||||
* a {@link TemplatingPushStateDispatcher} is used.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@@ -17,11 +18,18 @@ public class PushStateDispatcherProvider implements Provider<PushStateDispatcher
|
||||
@VisibleForTesting
|
||||
static final String PROPERTY_TARGET = "sonia.scm.ui.proxy";
|
||||
|
||||
private Provider<TemplatingPushStateDispatcher> templatingPushStateDispatcherProvider;
|
||||
|
||||
@Inject
|
||||
public PushStateDispatcherProvider(Provider<TemplatingPushStateDispatcher> templatingPushStateDispatcherProvider) {
|
||||
this.templatingPushStateDispatcherProvider = templatingPushStateDispatcherProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PushStateDispatcher get() {
|
||||
String target = System.getProperty(PROPERTY_TARGET);
|
||||
if (Strings.isNullOrEmpty(target)) {
|
||||
return new ForwardingPushStateDispatcher();
|
||||
return templatingPushStateDispatcherProvider.get();
|
||||
}
|
||||
return new ProxyPushStateDispatcher(target);
|
||||
}
|
||||
|
||||
@@ -33,8 +33,6 @@
|
||||
|
||||
package sonia.scm;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import com.google.common.base.Throwables;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.inject.Injector;
|
||||
@@ -63,8 +61,6 @@ import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
@@ -135,7 +131,7 @@ public class ScmContextListener extends GuiceResteasyBootstrapServletContextList
|
||||
moduleList.add(new EagerSingletonModule());
|
||||
moduleList.add(ShiroWebModule.guiceFilterModule());
|
||||
moduleList.add(new WebElementModule(pluginLoader));
|
||||
moduleList.add(new ScmServletModule(context, pluginLoader, overrides, pluginLoader.getExtensionProcessor()));
|
||||
moduleList.add(new ScmServletModule(context, pluginLoader, overrides));
|
||||
moduleList.add(
|
||||
new ScmSecurityModule(context, pluginLoader.getExtensionProcessor())
|
||||
);
|
||||
|
||||
@@ -33,8 +33,6 @@
|
||||
|
||||
package sonia.scm;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.google.inject.Provider;
|
||||
import com.google.inject.multibindings.Multibinder;
|
||||
@@ -56,17 +54,48 @@ import sonia.scm.group.xml.XmlGroupDAO;
|
||||
import sonia.scm.io.DefaultFileSystem;
|
||||
import sonia.scm.io.FileSystem;
|
||||
import sonia.scm.net.SSLContextProvider;
|
||||
import sonia.scm.net.ahc.*;
|
||||
import sonia.scm.plugin.*;
|
||||
import sonia.scm.repository.*;
|
||||
import sonia.scm.net.ahc.AdvancedHttpClient;
|
||||
import sonia.scm.net.ahc.ContentTransformer;
|
||||
import sonia.scm.net.ahc.DefaultAdvancedHttpClient;
|
||||
import sonia.scm.net.ahc.JsonContentTransformer;
|
||||
import sonia.scm.net.ahc.XmlContentTransformer;
|
||||
import sonia.scm.plugin.DefaultPluginLoader;
|
||||
import sonia.scm.plugin.DefaultPluginManager;
|
||||
import sonia.scm.plugin.PluginLoader;
|
||||
import sonia.scm.plugin.PluginManager;
|
||||
import sonia.scm.repository.DefaultRepositoryManager;
|
||||
import sonia.scm.repository.DefaultRepositoryProvider;
|
||||
import sonia.scm.repository.HealthCheckContextListener;
|
||||
import sonia.scm.repository.NamespaceStrategy;
|
||||
import sonia.scm.repository.NamespaceStrategyProvider;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryDAO;
|
||||
import sonia.scm.repository.RepositoryManager;
|
||||
import sonia.scm.repository.RepositoryManagerProvider;
|
||||
import sonia.scm.repository.RepositoryProvider;
|
||||
import sonia.scm.repository.api.HookContextFactory;
|
||||
import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||
import sonia.scm.repository.spi.HookEventFacade;
|
||||
import sonia.scm.repository.xml.XmlRepositoryDAO;
|
||||
import sonia.scm.schedule.QuartzScheduler;
|
||||
import sonia.scm.schedule.Scheduler;
|
||||
import sonia.scm.security.*;
|
||||
import sonia.scm.store.*;
|
||||
import sonia.scm.security.AuthorizationChangedEventProducer;
|
||||
import sonia.scm.security.CipherHandler;
|
||||
import sonia.scm.security.CipherUtil;
|
||||
import sonia.scm.security.ConfigurableLoginAttemptHandler;
|
||||
import sonia.scm.security.DefaultKeyGenerator;
|
||||
import sonia.scm.security.DefaultSecuritySystem;
|
||||
import sonia.scm.security.KeyGenerator;
|
||||
import sonia.scm.security.LoginAttemptHandler;
|
||||
import sonia.scm.security.SecuritySystem;
|
||||
import sonia.scm.store.BlobStoreFactory;
|
||||
import sonia.scm.store.ConfigurationEntryStoreFactory;
|
||||
import sonia.scm.store.ConfigurationStoreFactory;
|
||||
import sonia.scm.store.DataStoreFactory;
|
||||
import sonia.scm.store.FileBlobStoreFactory;
|
||||
import sonia.scm.store.JAXBConfigurationEntryStoreFactory;
|
||||
import sonia.scm.store.JAXBConfigurationStoreFactory;
|
||||
import sonia.scm.store.JAXBDataStoreFactory;
|
||||
import sonia.scm.template.MustacheTemplateEngine;
|
||||
import sonia.scm.template.TemplateEngine;
|
||||
import sonia.scm.template.TemplateEngineFactory;
|
||||
@@ -81,14 +110,16 @@ import sonia.scm.util.ScmConfigurationUtil;
|
||||
import sonia.scm.web.UserAgentParser;
|
||||
import sonia.scm.web.cgi.CGIExecutorFactory;
|
||||
import sonia.scm.web.cgi.DefaultCGIExecutorFactory;
|
||||
import sonia.scm.web.filter.AuthenticationFilter;
|
||||
import sonia.scm.web.filter.LoggingFilter;
|
||||
import sonia.scm.web.protocol.HttpProtocolServlet;
|
||||
import sonia.scm.web.security.AdministrationContext;
|
||||
import sonia.scm.web.security.DefaultAdministrationContext;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.servlet.ServletContext;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
import static sonia.scm.api.v2.resources.ScmPathInfo.REST_API_PATH;
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -99,14 +130,14 @@ public class ScmServletModule extends ServletModule
|
||||
|
||||
/** Field description */
|
||||
public static final String[] PATTERN_ADMIN = new String[] {
|
||||
"/api/rest/groups*",
|
||||
"/api/rest/users*", "/api/rest/plguins*" };
|
||||
REST_API_PATH + "/groups*",
|
||||
REST_API_PATH + "/users*", REST_API_PATH + "/plguins*" };
|
||||
|
||||
/** Field description */
|
||||
public static final String PATTERN_ALL = "/*";
|
||||
|
||||
/** Field description */
|
||||
public static final String PATTERN_CONFIG = "/api/rest/config*";
|
||||
public static final String PATTERN_CONFIG = REST_API_PATH + "/config*";
|
||||
|
||||
/** Field description */
|
||||
public static final String PATTERN_DEBUG = "/debug.html";
|
||||
@@ -155,22 +186,11 @@ public class ScmServletModule extends ServletModule
|
||||
|
||||
//~--- constructors ---------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Constructs ...
|
||||
*
|
||||
*
|
||||
* @param servletContext
|
||||
* @param pluginLoader
|
||||
* @param overrides
|
||||
* @param extensionProcessor
|
||||
*/
|
||||
ScmServletModule(ServletContext servletContext,
|
||||
DefaultPluginLoader pluginLoader, ClassOverrides overrides, ExtensionProcessor extensionProcessor)
|
||||
ScmServletModule(ServletContext servletContext, DefaultPluginLoader pluginLoader, ClassOverrides overrides)
|
||||
{
|
||||
this.servletContext = servletContext;
|
||||
this.pluginLoader = pluginLoader;
|
||||
this.overrides = overrides;
|
||||
this.extensionProcessor = extensionProcessor;
|
||||
}
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
@@ -293,6 +313,8 @@ public class ScmServletModule extends ServletModule
|
||||
bind(TemplateEngineFactory.class);
|
||||
bind(ObjectMapper.class).toProvider(ObjectMapperProvider.class);
|
||||
|
||||
filter(HttpProtocolServlet.PATTERN).through(AuthenticationFilter.class);
|
||||
|
||||
// bind events
|
||||
// bind(LastModifiedUpdateListener.class);
|
||||
|
||||
@@ -389,11 +411,6 @@ public class ScmServletModule extends ServletModule
|
||||
|
||||
/**
|
||||
* Load ScmConfiguration with JAXB
|
||||
*
|
||||
*
|
||||
* @param context
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private ScmConfiguration getScmConfiguration()
|
||||
{
|
||||
@@ -414,6 +431,4 @@ public class ScmServletModule extends ServletModule
|
||||
|
||||
/** Field description */
|
||||
private final ServletContext servletContext;
|
||||
|
||||
private final ExtensionProcessor extensionProcessor;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package sonia.scm;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import sonia.scm.template.Template;
|
||||
import sonia.scm.template.TemplateEngine;
|
||||
import sonia.scm.template.TemplateEngineFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.io.Writer;
|
||||
|
||||
/**
|
||||
* This dispatcher renders the /index.mustache template, which is merged in from the scm-ui package.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class TemplatingPushStateDispatcher implements PushStateDispatcher {
|
||||
|
||||
@VisibleForTesting
|
||||
static final String TEMPLATE = "/index.mustache";
|
||||
|
||||
private final TemplateEngine templateEngine;
|
||||
|
||||
@Inject
|
||||
public TemplatingPushStateDispatcher(TemplateEngineFactory templateEngineFactory) {
|
||||
this(templateEngineFactory.getDefaultEngine());
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
TemplatingPushStateDispatcher(TemplateEngine templateEngine) {
|
||||
this.templateEngine = templateEngine;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispatch(HttpServletRequest request, HttpServletResponse response, String uri) throws IOException {
|
||||
response.setContentType("text/html");
|
||||
response.setCharacterEncoding("UTF-8");
|
||||
|
||||
Template template = templateEngine.getTemplate(TEMPLATE);
|
||||
try (Writer writer = response.getWriter()) {
|
||||
template.execute(writer, new IndexHtmlModel(request));
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static class IndexHtmlModel {
|
||||
|
||||
private final HttpServletRequest request;
|
||||
|
||||
private IndexHtmlModel(HttpServletRequest request) {
|
||||
this.request = request;
|
||||
}
|
||||
|
||||
public String getContextPath() {
|
||||
return request.getContextPath();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package sonia.scm;
|
||||
|
||||
import com.github.sdorra.webresources.WebResourceSender;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.io.Resources;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.filter.WebElement;
|
||||
@@ -15,7 +15,6 @@ import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URL;
|
||||
|
||||
/**
|
||||
@@ -27,16 +26,22 @@ import java.net.URL;
|
||||
@WebElement(value = WebResourceServlet.PATTERN, regex = true)
|
||||
public class WebResourceServlet extends HttpServlet {
|
||||
|
||||
|
||||
/**
|
||||
* exclude api requests and the old frontend servlets.
|
||||
*
|
||||
* TODO remove old protocol servlets and hook. Move /hook/hg to api?
|
||||
*/
|
||||
@VisibleForTesting
|
||||
static final String PATTERN = "/(?!api/|git/|hg/|svn/|hook/).*";
|
||||
static final String PATTERN = "/(?!api/|git/|hg/|svn/|hook/|repo/).*";
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(WebResourceServlet.class);
|
||||
|
||||
private final WebResourceSender sender = WebResourceSender.create()
|
||||
.withGZIP()
|
||||
.withGZIPMinLength(512)
|
||||
.withBufferSize(16384);
|
||||
|
||||
private final UberWebResourceLoader webResourceLoader;
|
||||
private final PushStateDispatcher pushStateDispatcher;
|
||||
|
||||
@@ -53,7 +58,7 @@ public class WebResourceServlet extends HttpServlet {
|
||||
LOG.trace("try to load {}", uri);
|
||||
URL url = webResourceLoader.getResource(uri);
|
||||
if (url != null) {
|
||||
serveResource(response, url);
|
||||
serveResource(request, response, url);
|
||||
} else {
|
||||
dispatch(request, response, uri);
|
||||
}
|
||||
@@ -72,10 +77,9 @@ public class WebResourceServlet extends HttpServlet {
|
||||
return HttpUtil.getStrippedURI(request);
|
||||
}
|
||||
|
||||
private void serveResource(HttpServletResponse response, URL url) {
|
||||
// TODO lastModifiedDate, if-... ???
|
||||
try (OutputStream output = response.getOutputStream()) {
|
||||
Resources.copy(url, output);
|
||||
private void serveResource(HttpServletRequest request, HttpServletResponse response, URL url) {
|
||||
try {
|
||||
sender.resource(url).send(request, response);
|
||||
} catch (IOException ex) {
|
||||
LOG.warn("failed to serve resource: {}", url);
|
||||
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
package sonia.scm.api.rest;
|
||||
|
||||
import sonia.scm.api.v2.resources.UriInfoStore;
|
||||
import sonia.scm.api.v2.resources.ScmPathInfoStore;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.ws.rs.container.ContainerRequestContext;
|
||||
import javax.ws.rs.container.ContainerRequestFilter;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
import javax.ws.rs.ext.Provider;
|
||||
|
||||
@Provider
|
||||
public class UriInfoFilter implements ContainerRequestFilter {
|
||||
|
||||
private final javax.inject.Provider<UriInfoStore> storeProvider;
|
||||
private final javax.inject.Provider<ScmPathInfoStore> storeProvider;
|
||||
|
||||
@Inject
|
||||
public UriInfoFilter(javax.inject.Provider<UriInfoStore> storeProvider) {
|
||||
public UriInfoFilter(javax.inject.Provider<ScmPathInfoStore> storeProvider) {
|
||||
this.storeProvider = storeProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void filter(ContainerRequestContext requestContext) {
|
||||
storeProvider.get().set(requestContext.getUriInfo());
|
||||
UriInfo uriInfo = requestContext.getUriInfo();
|
||||
storeProvider.get().set(uriInfo::getBaseUri);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,37 +33,28 @@
|
||||
|
||||
package sonia.scm.api.rest.resources;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import com.google.common.base.Function;
|
||||
import com.google.common.collect.Collections2;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Ordering;
|
||||
import com.google.inject.Inject;
|
||||
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryManager;
|
||||
import sonia.scm.repository.RepositoryTypePredicate;
|
||||
import sonia.scm.util.HttpUtil;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import sonia.scm.template.Viewable;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import sonia.scm.template.Viewable;
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -106,13 +97,12 @@ public class RepositoryRootResource
|
||||
@Produces(MediaType.TEXT_HTML)
|
||||
public Viewable renderRepositoriesRoot(@Context HttpServletRequest request, @PathParam("type") final String type)
|
||||
{
|
||||
String baseUrl = HttpUtil.getCompleteUrl(request);
|
||||
//J-
|
||||
Collection<RepositoryTemplateElement> unsortedRepositories =
|
||||
Collections2.transform(
|
||||
Collections2.filter(
|
||||
repositoryManager.getAll(), new RepositoryTypePredicate(type))
|
||||
, new RepositoryTransformFunction(baseUrl)
|
||||
, new RepositoryTransformFunction()
|
||||
);
|
||||
|
||||
List<RepositoryTemplateElement> repositories = Ordering.from(
|
||||
@@ -138,17 +128,9 @@ public class RepositoryRootResource
|
||||
public static class RepositoryTemplateElement
|
||||
{
|
||||
|
||||
/**
|
||||
* Constructs ...
|
||||
*
|
||||
*
|
||||
* @param repository
|
||||
* @param baseUrl
|
||||
*/
|
||||
public RepositoryTemplateElement(Repository repository, String baseUrl)
|
||||
public RepositoryTemplateElement(Repository repository)
|
||||
{
|
||||
this.repository = repository;
|
||||
this.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
//~--- get methods --------------------------------------------------------
|
||||
@@ -175,22 +157,8 @@ public class RepositoryRootResource
|
||||
return repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public String getUrl()
|
||||
{
|
||||
return repository.createUrl(baseUrl);
|
||||
}
|
||||
|
||||
//~--- fields -------------------------------------------------------------
|
||||
|
||||
/** Field description */
|
||||
private String baseUrl;
|
||||
|
||||
/** Field description */
|
||||
private Repository repository;
|
||||
|
||||
@@ -236,31 +204,10 @@ public class RepositoryRootResource
|
||||
private static class RepositoryTransformFunction
|
||||
implements Function<Repository, RepositoryTemplateElement>
|
||||
{
|
||||
|
||||
public RepositoryTransformFunction(String baseUrl)
|
||||
{
|
||||
this.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
//~--- methods ------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param repository
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public RepositoryTemplateElement apply(Repository repository)
|
||||
{
|
||||
return new RepositoryTemplateElement(repository, baseUrl);
|
||||
return new RepositoryTemplateElement(repository);
|
||||
}
|
||||
|
||||
//~--- fields -------------------------------------------------------------
|
||||
|
||||
/** Field description */
|
||||
private String baseUrl;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package sonia.scm.api.v2;
|
||||
|
||||
public final class ValidationConstraints {
|
||||
|
||||
private ValidationConstraints() {}
|
||||
|
||||
/**
|
||||
* A user or group name should not start with <code>@</code> or a whitespace
|
||||
* and it not contains whitespaces
|
||||
* and the characters: . - _ @ are allowed
|
||||
*/
|
||||
public static final String USER_GROUP_PATTERN = "^[A-Za-z0-9\\.\\-_][A-Za-z0-9\\.\\-_@]*$";
|
||||
|
||||
}
|
||||
@@ -1,73 +1,21 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import de.otto.edison.hal.Embedded;
|
||||
import de.otto.edison.hal.HalRepresentation;
|
||||
import de.otto.edison.hal.Links;
|
||||
import de.otto.edison.hal.paging.NumberedPaging;
|
||||
import de.otto.edison.hal.paging.PagingRel;
|
||||
import sonia.scm.ModelObject;
|
||||
import sonia.scm.PageResult;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static com.damnhandy.uri.template.UriTemplate.fromTemplate;
|
||||
import static de.otto.edison.hal.Embedded.embeddedBuilder;
|
||||
import static de.otto.edison.hal.Link.link;
|
||||
import static de.otto.edison.hal.Links.linkingTo;
|
||||
import static de.otto.edison.hal.paging.NumberedPaging.zeroBasedNumberedPaging;
|
||||
import static java.util.stream.Collectors.toList;
|
||||
|
||||
abstract class BasicCollectionToDtoMapper<E extends ModelObject, D extends HalRepresentation, M extends BaseMapper<E, D>> {
|
||||
|
||||
private final String collectionName;
|
||||
public class BasicCollectionToDtoMapper<E extends ModelObject, D extends HalRepresentation, M extends BaseMapper<E, D>> extends PagedCollectionToDtoMapper<E, D> {
|
||||
|
||||
private final M entityToDtoMapper;
|
||||
|
||||
@Inject
|
||||
public BasicCollectionToDtoMapper(String collectionName, M entityToDtoMapper) {
|
||||
this.collectionName = collectionName;
|
||||
super(collectionName);
|
||||
this.entityToDtoMapper = entityToDtoMapper;
|
||||
}
|
||||
|
||||
CollectionDto map(int pageNumber, int pageSize, PageResult<E> pageResult, String selfLink, Optional<String> createLink) {
|
||||
return map(pageNumber, pageSize, pageResult, selfLink, createLink, entityToDtoMapper::map);
|
||||
}
|
||||
|
||||
CollectionDto map(int pageNumber, int pageSize, PageResult<E> pageResult, String selfLink, Optional<String> createLink, Function<E, ? extends HalRepresentation> mapper) {
|
||||
NumberedPaging paging = zeroBasedNumberedPaging(pageNumber, pageSize, pageResult.getOverallCount());
|
||||
List<HalRepresentation> dtos = pageResult.getEntities().stream().map(mapper).collect(toList());
|
||||
CollectionDto collectionDto = new CollectionDto(
|
||||
createLinks(paging, selfLink, createLink),
|
||||
embedDtos(dtos));
|
||||
collectionDto.setPage(pageNumber);
|
||||
collectionDto.setPageTotal(computePageTotal(pageSize, pageResult));
|
||||
return collectionDto;
|
||||
}
|
||||
|
||||
private int computePageTotal(int pageSize, PageResult<E> pageResult) {
|
||||
if (pageResult.getOverallCount() % pageSize > 0) {
|
||||
return pageResult.getOverallCount() / pageSize + 1;
|
||||
} else {
|
||||
return pageResult.getOverallCount() / pageSize;
|
||||
}
|
||||
}
|
||||
|
||||
private Links createLinks(NumberedPaging page, String selfLink, Optional<String> createLink) {
|
||||
Links.Builder linksBuilder = linkingTo()
|
||||
.with(page.links(
|
||||
fromTemplate(selfLink + "{?page,pageSize}"),
|
||||
EnumSet.allOf(PagingRel.class)));
|
||||
createLink.ifPresent(link -> linksBuilder.single(link("create", link)));
|
||||
return linksBuilder.build();
|
||||
}
|
||||
|
||||
private Embedded embedDtos(List<HalRepresentation> dtos) {
|
||||
return embeddedBuilder()
|
||||
.with(collectionName, dtos)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import sonia.scm.PageResult;
|
||||
import sonia.scm.repository.Changeset;
|
||||
import sonia.scm.repository.Repository;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
public class BranchChangesetCollectionToDtoMapper extends ChangesetCollectionToDtoMapperBase {
|
||||
|
||||
private final ResourceLinks resourceLinks;
|
||||
|
||||
@Inject
|
||||
public BranchChangesetCollectionToDtoMapper(ChangesetToChangesetDtoMapper changesetToChangesetDtoMapper, ResourceLinks resourceLinks) {
|
||||
super(changesetToChangesetDtoMapper);
|
||||
this.resourceLinks = resourceLinks;
|
||||
}
|
||||
|
||||
public CollectionDto map(int pageNumber, int pageSize, PageResult<Changeset> pageResult, Repository repository, String branch) {
|
||||
return this.map(pageNumber, pageSize, pageResult, repository, () -> createSelfLink(repository, branch));
|
||||
}
|
||||
|
||||
private String createSelfLink(Repository repository, String branch) {
|
||||
return resourceLinks.branch().history(repository.getNamespaceAndName(), branch);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package sonia.scm.api.v2.resources;
|
||||
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
|
||||
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
|
||||
import com.webcohesion.enunciate.metadata.rs.TypeHint;
|
||||
import sonia.scm.NotFoundException;
|
||||
import sonia.scm.PageResult;
|
||||
import sonia.scm.repository.Branches;
|
||||
import sonia.scm.repository.Changeset;
|
||||
@@ -32,14 +33,14 @@ public class BranchRootResource {
|
||||
private final BranchToBranchDtoMapper branchToDtoMapper;
|
||||
private final BranchCollectionToDtoMapper branchCollectionToDtoMapper;
|
||||
|
||||
private final ChangesetCollectionToDtoMapper changesetCollectionToDtoMapper;
|
||||
private final BranchChangesetCollectionToDtoMapper branchChangesetCollectionToDtoMapper;
|
||||
|
||||
@Inject
|
||||
public BranchRootResource(RepositoryServiceFactory serviceFactory, BranchToBranchDtoMapper branchToDtoMapper, BranchCollectionToDtoMapper branchCollectionToDtoMapper, ChangesetCollectionToDtoMapper changesetCollectionToDtoMapper) {
|
||||
public BranchRootResource(RepositoryServiceFactory serviceFactory, BranchToBranchDtoMapper branchToDtoMapper, BranchCollectionToDtoMapper branchCollectionToDtoMapper, BranchChangesetCollectionToDtoMapper changesetCollectionToDtoMapper) {
|
||||
this.serviceFactory = serviceFactory;
|
||||
this.branchToDtoMapper = branchToDtoMapper;
|
||||
this.branchCollectionToDtoMapper = branchCollectionToDtoMapper;
|
||||
this.changesetCollectionToDtoMapper = changesetCollectionToDtoMapper;
|
||||
this.branchChangesetCollectionToDtoMapper = changesetCollectionToDtoMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -98,6 +99,14 @@ public class BranchRootResource {
|
||||
@DefaultValue("0") @QueryParam("page") int page,
|
||||
@DefaultValue("10") @QueryParam("pageSize") int pageSize) throws Exception {
|
||||
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
|
||||
boolean branchExists = repositoryService.getBranchesCommand()
|
||||
.getBranches()
|
||||
.getBranches()
|
||||
.stream()
|
||||
.anyMatch(branch -> branchName.equals(branch.getName()));
|
||||
if (!branchExists){
|
||||
throw new NotFoundException("branch", branchName);
|
||||
}
|
||||
Repository repository = repositoryService.getRepository();
|
||||
RepositoryPermissions.read(repository).check();
|
||||
ChangesetPagingResult changesets = repositoryService.getLogCommand()
|
||||
@@ -107,7 +116,7 @@ public class BranchRootResource {
|
||||
.getChangesets();
|
||||
if (changesets != null && changesets.getChangesets() != null) {
|
||||
PageResult<Changeset> pageResult = new PageResult<>(changesets.getChangesets(), changesets.getTotal());
|
||||
return Response.ok(changesetCollectionToDtoMapper.map(page, pageSize, pageResult, repository)).build();
|
||||
return Response.ok(branchChangesetCollectionToDtoMapper.map(page, pageSize, pageResult, repository, branchName)).build();
|
||||
} else {
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import sonia.scm.user.ChangePasswordNotAllowedException;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.ext.ExceptionMapper;
|
||||
import javax.ws.rs.ext.Provider;
|
||||
|
||||
@Provider
|
||||
public class ChangePasswordNotAllowedExceptionMapper implements ExceptionMapper<ChangePasswordNotAllowedException> {
|
||||
@Override
|
||||
public Response toResponse(ChangePasswordNotAllowedException exception) {
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(exception.getMessage())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -5,22 +5,19 @@ import sonia.scm.repository.Changeset;
|
||||
import sonia.scm.repository.Repository;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.util.Optional;
|
||||
|
||||
public class ChangesetCollectionToDtoMapper extends BasicCollectionToDtoMapper<Changeset, ChangesetDto, ChangesetToChangesetDtoMapper> {
|
||||
public class ChangesetCollectionToDtoMapper extends ChangesetCollectionToDtoMapperBase {
|
||||
|
||||
private final ChangesetToChangesetDtoMapper changesetToChangesetDtoMapper;
|
||||
private final ResourceLinks resourceLinks;
|
||||
|
||||
@Inject
|
||||
public ChangesetCollectionToDtoMapper(ChangesetToChangesetDtoMapper changesetToChangesetDtoMapper, ResourceLinks resourceLinks) {
|
||||
super("changesets", changesetToChangesetDtoMapper);
|
||||
this.changesetToChangesetDtoMapper = changesetToChangesetDtoMapper;
|
||||
super(changesetToChangesetDtoMapper);
|
||||
this.resourceLinks = resourceLinks;
|
||||
}
|
||||
|
||||
public CollectionDto map(int pageNumber, int pageSize, PageResult<Changeset> pageResult, Repository repository) {
|
||||
return super.map(pageNumber, pageSize, pageResult, createSelfLink(repository), Optional.empty(), changeset -> changesetToChangesetDtoMapper.map(changeset, repository));
|
||||
return super.map(pageNumber, pageSize, pageResult, repository, () -> createSelfLink(repository));
|
||||
}
|
||||
|
||||
private String createSelfLink(Repository repository) {
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import sonia.scm.PageResult;
|
||||
import sonia.scm.repository.Changeset;
|
||||
import sonia.scm.repository.Repository;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
class ChangesetCollectionToDtoMapperBase extends PagedCollectionToDtoMapper<Changeset, ChangesetDto> {
|
||||
|
||||
private final ChangesetToChangesetDtoMapper changesetToChangesetDtoMapper;
|
||||
|
||||
ChangesetCollectionToDtoMapperBase(ChangesetToChangesetDtoMapper changesetToChangesetDtoMapper) {
|
||||
super("changesets");
|
||||
this.changesetToChangesetDtoMapper = changesetToChangesetDtoMapper;
|
||||
}
|
||||
|
||||
CollectionDto map(int pageNumber, int pageSize, PageResult<Changeset> pageResult, Repository repository, Supplier<String> selfLinkSupplier) {
|
||||
return super.map(pageNumber, pageSize, pageResult, selfLinkSupplier.get(), Optional.empty(), changeset -> changesetToChangesetDtoMapper.map(changeset, repository));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import static de.otto.edison.hal.Link.link;
|
||||
import static de.otto.edison.hal.Links.linkingTo;
|
||||
|
||||
@Mapper
|
||||
public abstract class ChangesetToChangesetDtoMapper extends BaseMapper<Changeset, ChangesetDto> {
|
||||
public abstract class ChangesetToChangesetDtoMapper implements InstantAttributeMapper {
|
||||
|
||||
@Inject
|
||||
private RepositoryServiceFactory serviceFactory;
|
||||
@@ -65,7 +65,8 @@ public abstract class ChangesetToChangesetDtoMapper extends BaseMapper<Changeset
|
||||
|
||||
Links.Builder linksBuilder = linkingTo()
|
||||
.self(resourceLinks.changeset().self(repository.getNamespace(), repository.getName(), target.getId()))
|
||||
.single(link("diff", resourceLinks.diff().self(namespace, name, target.getId())));
|
||||
.single(link("diff", resourceLinks.diff().self(namespace, name, target.getId())))
|
||||
.single(link("modifications", resourceLinks.modifications().self(namespace, name, target.getId())));
|
||||
target.add(linksBuilder.build());
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import static de.otto.edison.hal.Link.link;
|
||||
import static de.otto.edison.hal.Links.linkingTo;
|
||||
|
||||
@Mapper
|
||||
public abstract class ChangesetToParentDtoMapper extends BaseMapper<Changeset, ParentChangesetDto> {
|
||||
public abstract class ChangesetToParentDtoMapper {
|
||||
|
||||
@Inject
|
||||
private ResourceLinks resourceLinks;
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import sonia.scm.PageResult;
|
||||
import sonia.scm.repository.Changeset;
|
||||
import sonia.scm.repository.Repository;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
public class FileHistoryCollectionToDtoMapper extends ChangesetCollectionToDtoMapperBase {
|
||||
|
||||
|
||||
private final ResourceLinks resourceLinks;
|
||||
|
||||
@Inject
|
||||
public FileHistoryCollectionToDtoMapper(ChangesetToChangesetDtoMapper changesetToChangesetDtoMapper, ResourceLinks resourceLinks) {
|
||||
super(changesetToChangesetDtoMapper);
|
||||
this.resourceLinks = resourceLinks;
|
||||
}
|
||||
|
||||
public CollectionDto map(int pageNumber, int pageSize, PageResult<Changeset> pageResult, Repository repository, String revision, String path) {
|
||||
return super.map(pageNumber, pageSize, pageResult, repository, () -> createSelfLink(repository, revision, path));
|
||||
}
|
||||
|
||||
private String createSelfLink(Repository repository, String revision, String path) {
|
||||
return resourceLinks.fileHistory().self(repository.getNamespace(), repository.getName(), revision, path);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
|
||||
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
|
||||
import com.webcohesion.enunciate.metadata.rs.TypeHint;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import sonia.scm.PageResult;
|
||||
import sonia.scm.repository.Changeset;
|
||||
import sonia.scm.repository.ChangesetPagingResult;
|
||||
import sonia.scm.repository.InternalRepositoryException;
|
||||
import sonia.scm.repository.NamespaceAndName;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryNotFoundException;
|
||||
import sonia.scm.repository.RevisionNotFoundException;
|
||||
import sonia.scm.repository.api.RepositoryService;
|
||||
import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.ws.rs.DefaultValue;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.io.IOException;
|
||||
|
||||
@Slf4j
|
||||
public class FileHistoryRootResource {
|
||||
|
||||
private final RepositoryServiceFactory serviceFactory;
|
||||
|
||||
private final FileHistoryCollectionToDtoMapper fileHistoryCollectionToDtoMapper;
|
||||
|
||||
|
||||
@Inject
|
||||
public FileHistoryRootResource(RepositoryServiceFactory serviceFactory, FileHistoryCollectionToDtoMapper fileHistoryCollectionToDtoMapper) {
|
||||
this.serviceFactory = serviceFactory;
|
||||
this.fileHistoryCollectionToDtoMapper = fileHistoryCollectionToDtoMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all changesets related to the given file starting with the given revision
|
||||
*
|
||||
* @param namespace the repository namespace
|
||||
* @param name the repository name
|
||||
* @param revision the revision
|
||||
* @param path the path of the file
|
||||
* @param page pagination
|
||||
* @param pageSize pagination
|
||||
* @return all changesets related to the given file starting with the given revision
|
||||
* @throws IOException on io error
|
||||
* @throws RevisionNotFoundException on missing revision
|
||||
* @throws RepositoryNotFoundException on missing repository
|
||||
*/
|
||||
@GET
|
||||
@Path("{revision}/{path: .*}")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
|
||||
@ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the changeset"),
|
||||
@ResponseCode(code = 404, condition = "not found, no changesets available in the repository"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Produces(VndMediaType.CHANGESET_COLLECTION)
|
||||
@TypeHint(CollectionDto.class)
|
||||
public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name,
|
||||
@PathParam("revision") String revision,
|
||||
@PathParam("path") String path,
|
||||
@DefaultValue("0") @QueryParam("page") int page,
|
||||
@DefaultValue("10") @QueryParam("pageSize") int pageSize) throws IOException, RevisionNotFoundException, RepositoryNotFoundException {
|
||||
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
|
||||
log.info("Get changesets of the file {} and revision {}", path, revision);
|
||||
Repository repository = repositoryService.getRepository();
|
||||
ChangesetPagingResult changesets = repositoryService.getLogCommand()
|
||||
.setPagingStart(page)
|
||||
.setPagingLimit(pageSize)
|
||||
.setPath(path)
|
||||
.setStartChangeset(revision)
|
||||
.getChangesets();
|
||||
if (changesets != null && changesets.getChangesets() != null) {
|
||||
PageResult<Changeset> pageResult = new PageResult<>(changesets.getChangesets(), changesets.getTotal());
|
||||
return Response.ok(fileHistoryCollectionToDtoMapper.map(page, pageSize, pageResult, repository, revision, path)).build();
|
||||
} else {
|
||||
String message = String.format("for the revision %s and the file %s there is no changesets", revision, path);
|
||||
log.error(message);
|
||||
throw new InternalRepositoryException(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import de.otto.edison.hal.Links;
|
||||
import org.mapstruct.AfterMapping;
|
||||
import org.mapstruct.Context;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
import org.mapstruct.MappingTarget;
|
||||
import sonia.scm.repository.FileObject;
|
||||
import sonia.scm.repository.NamespaceAndName;
|
||||
@@ -11,12 +12,15 @@ import sonia.scm.repository.SubRepository;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static de.otto.edison.hal.Link.link;
|
||||
|
||||
@Mapper
|
||||
public abstract class FileObjectToFileObjectDtoMapper extends BaseMapper<FileObject, FileObjectDto> {
|
||||
public abstract class FileObjectToFileObjectDtoMapper implements InstantAttributeMapper {
|
||||
|
||||
@Inject
|
||||
private ResourceLinks resourceLinks;
|
||||
|
||||
@Mapping(target = "attributes", ignore = true) // We do not map HAL attributes
|
||||
protected abstract FileObjectDto map(FileObject fileObject, @Context NamespaceAndName namespaceAndName, @Context String revision);
|
||||
|
||||
abstract SubRepositoryDto mapSubrepository(SubRepository subRepository);
|
||||
@@ -29,6 +33,7 @@ public abstract class FileObjectToFileObjectDtoMapper extends BaseMapper<FileObj
|
||||
links.self(resourceLinks.source().sourceWithPath(namespaceAndName.getNamespace(), namespaceAndName.getName(), revision, path));
|
||||
} else {
|
||||
links.self(resourceLinks.source().content(namespaceAndName.getNamespace(), namespaceAndName.getName(), revision, path));
|
||||
links.single(link("history", resourceLinks.fileHistory().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), revision, path)));
|
||||
}
|
||||
|
||||
dto.add(links.build());
|
||||
|
||||
@@ -13,6 +13,8 @@ import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static sonia.scm.api.v2.ValidationConstraints.USER_GROUP_PATTERN;
|
||||
|
||||
@Getter @Setter @NoArgsConstructor
|
||||
public class GroupDto extends HalRepresentation {
|
||||
|
||||
@@ -20,7 +22,7 @@ public class GroupDto extends HalRepresentation {
|
||||
private String description;
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
private Instant lastModified;
|
||||
@Pattern(regexp = "^[A-z0-9\\.\\-_@]|[^ ]([A-z0-9\\.\\-_@ ]*[A-z0-9\\.\\-_@]|[^ ])?$")
|
||||
@Pattern(regexp = USER_GROUP_PATTERN)
|
||||
private String name;
|
||||
@NotEmpty
|
||||
private String type;
|
||||
|
||||
@@ -10,6 +10,7 @@ import sonia.scm.PageResult;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.Supplier;
|
||||
@@ -37,6 +38,15 @@ class IdResourceManagerAdapter<MODEL_OBJECT extends ModelObject,
|
||||
return singleAdapter.get(loadBy(id), mapToDto);
|
||||
}
|
||||
|
||||
public Response update(String id, Function<MODEL_OBJECT, MODEL_OBJECT> applyChanges, Consumer<MODEL_OBJECT> checker) throws NotFoundException, ConcurrentModificationException {
|
||||
return singleAdapter.update(
|
||||
loadBy(id),
|
||||
applyChanges,
|
||||
idStaysTheSame(id),
|
||||
checker
|
||||
);
|
||||
}
|
||||
|
||||
public Response update(String id, Function<MODEL_OBJECT, MODEL_OBJECT> applyChanges) throws NotFoundException, ConcurrentModificationException {
|
||||
return singleAdapter.update(
|
||||
loadBy(id),
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import de.otto.edison.hal.HalRepresentation;
|
||||
import de.otto.edison.hal.Links;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public class IndexDto extends HalRepresentation {
|
||||
|
||||
private final String version;
|
||||
|
||||
IndexDto(String version, Links links) {
|
||||
super(links);
|
||||
this.version = version;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import de.otto.edison.hal.Links;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.config.ConfigurationPermissions;
|
||||
import sonia.scm.group.GroupPermissions;
|
||||
import sonia.scm.user.UserPermissions;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static de.otto.edison.hal.Link.link;
|
||||
|
||||
public class IndexDtoGenerator {
|
||||
|
||||
private final ResourceLinks resourceLinks;
|
||||
private final SCMContextProvider scmContextProvider;
|
||||
|
||||
@Inject
|
||||
public IndexDtoGenerator(ResourceLinks resourceLinks, SCMContextProvider scmContextProvider) {
|
||||
this.resourceLinks = resourceLinks;
|
||||
this.scmContextProvider = scmContextProvider;
|
||||
}
|
||||
|
||||
public IndexDto generate() {
|
||||
Links.Builder builder = Links.linkingTo();
|
||||
builder.self(resourceLinks.index().self());
|
||||
builder.single(link("uiPlugins", resourceLinks.uiPluginCollection().self()));
|
||||
if (SecurityUtils.getSubject().isAuthenticated()) {
|
||||
builder.single(
|
||||
link("me", resourceLinks.me().self()),
|
||||
link("logout", resourceLinks.authentication().logout())
|
||||
);
|
||||
if (UserPermissions.list().isPermitted()) {
|
||||
builder.single(link("users", resourceLinks.userCollection().self()));
|
||||
}
|
||||
if (GroupPermissions.list().isPermitted()) {
|
||||
builder.single(link("groups", resourceLinks.groupCollection().self()));
|
||||
}
|
||||
if (ConfigurationPermissions.list().isPermitted()) {
|
||||
builder.single(link("config", resourceLinks.config().self()));
|
||||
}
|
||||
builder.single(link("repositories", resourceLinks.repositoryCollection().self()));
|
||||
} else {
|
||||
builder.single(link("login", resourceLinks.authentication().jsonLogin()));
|
||||
}
|
||||
|
||||
return new IndexDto(scmContextProvider.getVersion(), builder.build());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.webcohesion.enunciate.metadata.rs.TypeHint;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
|
||||
@Path(IndexResource.INDEX_PATH_V2)
|
||||
public class IndexResource {
|
||||
public static final String INDEX_PATH_V2 = "v2/";
|
||||
|
||||
private final IndexDtoGenerator indexDtoGenerator;
|
||||
|
||||
@Inject
|
||||
public IndexResource(IndexDtoGenerator indexDtoGenerator) {
|
||||
this.indexDtoGenerator = indexDtoGenerator;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("")
|
||||
@Produces(VndMediaType.INDEX)
|
||||
@TypeHint(IndexDto.class)
|
||||
public IndexDto getIndex() {
|
||||
return indexDtoGenerator.generate();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import sonia.scm.api.rest.StatusExceptionMapper;
|
||||
import sonia.scm.repository.InternalRepositoryException;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.ext.Provider;
|
||||
|
||||
@Provider
|
||||
public class InternalRepositoryExceptionMapper extends StatusExceptionMapper<InternalRepositoryException> {
|
||||
|
||||
public InternalRepositoryExceptionMapper() {
|
||||
super(InternalRepositoryException.class, Response.Status.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import sonia.scm.user.InvalidPasswordException;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.ext.ExceptionMapper;
|
||||
import javax.ws.rs.ext.Provider;
|
||||
|
||||
@Provider
|
||||
public class InvalidPasswordExceptionMapper implements ExceptionMapper<InvalidPasswordException> {
|
||||
@Override
|
||||
public Response toResponse(InvalidPasswordException exception) {
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(exception.getMessage())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ public class MapperModule extends AbstractModule {
|
||||
@Override
|
||||
protected void configure() {
|
||||
bind(UserDtoToUserMapper.class).to(Mappers.getMapper(UserDtoToUserMapper.class).getClass());
|
||||
bind(MeToUserDtoMapper.class).to(Mappers.getMapper(MeToUserDtoMapper.class).getClass());
|
||||
bind(UserToUserDtoMapper.class).to(Mappers.getMapper(UserToUserDtoMapper.class).getClass());
|
||||
bind(UserCollectionToDtoMapper.class);
|
||||
|
||||
@@ -34,11 +35,12 @@ public class MapperModule extends AbstractModule {
|
||||
bind(TagToTagDtoMapper.class).to(Mappers.getMapper(TagToTagDtoMapper.class).getClass());
|
||||
|
||||
bind(FileObjectToFileObjectDtoMapper.class).to(Mappers.getMapper(FileObjectToFileObjectDtoMapper.class).getClass());
|
||||
bind(ModificationsToDtoMapper.class).to(Mappers.getMapper(ModificationsToDtoMapper.class).getClass());
|
||||
|
||||
// no mapstruct required
|
||||
bind(UIPluginDtoMapper.class);
|
||||
bind(UIPluginDtoCollectionMapper.class);
|
||||
|
||||
bind(UriInfoStore.class).in(ServletScopes.REQUEST);
|
||||
bind(ScmPathInfoStore.class).in(ServletScopes.REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,19 +4,27 @@ import com.webcohesion.enunciate.metadata.rs.ResponseCode;
|
||||
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
|
||||
import com.webcohesion.enunciate.metadata.rs.TypeHint;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.apache.shiro.authc.credential.PasswordService;
|
||||
import sonia.scm.ConcurrentModificationException;
|
||||
import sonia.scm.NotFoundException;
|
||||
import sonia.scm.user.InvalidPasswordException;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.user.UserManager;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.PUT;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.Request;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static sonia.scm.user.InvalidPasswordException.INVALID_MATCHING;
|
||||
|
||||
|
||||
/**
|
||||
@@ -24,15 +32,20 @@ import javax.ws.rs.core.UriInfo;
|
||||
*/
|
||||
@Path(MeResource.ME_PATH_V2)
|
||||
public class MeResource {
|
||||
static final String ME_PATH_V2 = "v2/me/";
|
||||
public static final String ME_PATH_V2 = "v2/me/";
|
||||
|
||||
private final UserToUserDtoMapper userToDtoMapper;
|
||||
private final MeToUserDtoMapper meToUserDtoMapper;
|
||||
|
||||
private final IdResourceManagerAdapter<User, UserDto> adapter;
|
||||
private final PasswordService passwordService;
|
||||
private final UserManager userManager;
|
||||
|
||||
@Inject
|
||||
public MeResource(UserToUserDtoMapper userToDtoMapper, UserManager manager) {
|
||||
this.userToDtoMapper = userToDtoMapper;
|
||||
public MeResource(MeToUserDtoMapper meToUserDtoMapper, UserManager manager, PasswordService passwordService) {
|
||||
this.meToUserDtoMapper = meToUserDtoMapper;
|
||||
this.adapter = new IdResourceManagerAdapter<>(manager, User.class);
|
||||
this.passwordService = passwordService;
|
||||
this.userManager = manager;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -50,6 +63,34 @@ public class MeResource {
|
||||
public Response get(@Context Request request, @Context UriInfo uriInfo) throws NotFoundException {
|
||||
|
||||
String id = (String) SecurityUtils.getSubject().getPrincipals().getPrimaryPrincipal();
|
||||
return adapter.get(id, userToDtoMapper::map);
|
||||
return adapter.get(id, meToUserDtoMapper::map);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change password of the current user
|
||||
*/
|
||||
@PUT
|
||||
@Path("password")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 204, condition = "update success"),
|
||||
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(TypeHint.NO_CONTENT.class)
|
||||
@Consumes(VndMediaType.PASSWORD_CHANGE)
|
||||
public Response changePassword(PasswordChangeDto passwordChangeDto) throws NotFoundException, ConcurrentModificationException {
|
||||
String name = (String) SecurityUtils.getSubject().getPrincipals().getPrimaryPrincipal();
|
||||
return adapter.update(name, user -> user.changePassword(passwordService.encryptPassword(passwordChangeDto.getNewPassword())), userManager.getUserTypeChecker().andThen(getOldOriginalPasswordChecker(passwordChangeDto.getOldPassword())));
|
||||
}
|
||||
|
||||
/**
|
||||
* Match given old password from the dto with the stored password before updating
|
||||
*/
|
||||
private Consumer<User> getOldOriginalPasswordChecker(String oldPassword) {
|
||||
return user -> {
|
||||
if (!user.getPassword().equals(passwordService.encryptPassword(oldPassword))) {
|
||||
throw new InvalidPasswordException(INVALID_MATCHING);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import de.otto.edison.hal.Links;
|
||||
import org.mapstruct.AfterMapping;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.MappingTarget;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.user.UserManager;
|
||||
import sonia.scm.user.UserPermissions;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static de.otto.edison.hal.Link.link;
|
||||
import static de.otto.edison.hal.Links.linkingTo;
|
||||
|
||||
@Mapper
|
||||
public abstract class MeToUserDtoMapper extends UserToUserDtoMapper{
|
||||
|
||||
@Inject
|
||||
private UserManager userManager;
|
||||
|
||||
@Inject
|
||||
private ResourceLinks resourceLinks;
|
||||
|
||||
|
||||
@Override
|
||||
@AfterMapping
|
||||
protected void appendLinks(User user, @MappingTarget UserDto target) {
|
||||
Links.Builder linksBuilder = linkingTo().self(resourceLinks.me().self());
|
||||
if (UserPermissions.delete(user).isPermitted()) {
|
||||
linksBuilder.single(link("delete", resourceLinks.me().delete(target.getName())));
|
||||
}
|
||||
if (UserPermissions.modify(user).isPermitted()) {
|
||||
linksBuilder.single(link("update", resourceLinks.me().update(target.getName())));
|
||||
}
|
||||
if (userManager.isTypeDefault(user)) {
|
||||
linksBuilder.single(link("password", resourceLinks.me().passwordChange()));
|
||||
}
|
||||
target.add(linksBuilder.build());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import de.otto.edison.hal.HalRepresentation;
|
||||
import de.otto.edison.hal.Links;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public class ModificationsDto extends HalRepresentation {
|
||||
|
||||
|
||||
private String revision;
|
||||
/**
|
||||
* list of added files
|
||||
*/
|
||||
private List<String> added;
|
||||
|
||||
/**
|
||||
* list of modified files
|
||||
*/
|
||||
private List<String> modified;
|
||||
|
||||
/**
|
||||
* list of removed files
|
||||
*/
|
||||
private List<String> removed;
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("squid:S1185") // We want to have this method available in this package
|
||||
protected HalRepresentation add(Links links) {
|
||||
return super.add(links);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
|
||||
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
|
||||
import com.webcohesion.enunciate.metadata.rs.TypeHint;
|
||||
import sonia.scm.repository.InternalRepositoryException;
|
||||
import sonia.scm.repository.Modifications;
|
||||
import sonia.scm.repository.NamespaceAndName;
|
||||
import sonia.scm.repository.RepositoryNotFoundException;
|
||||
import sonia.scm.repository.RevisionNotFoundException;
|
||||
import sonia.scm.repository.api.RepositoryService;
|
||||
import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.io.IOException;
|
||||
|
||||
public class ModificationsRootResource {
|
||||
|
||||
private final RepositoryServiceFactory serviceFactory;
|
||||
private final ModificationsToDtoMapper modificationsToDtoMapper;
|
||||
|
||||
@Inject
|
||||
public ModificationsRootResource(RepositoryServiceFactory serviceFactory, ModificationsToDtoMapper modificationsToDtoMapper) {
|
||||
this.serviceFactory = serviceFactory;
|
||||
this.modificationsToDtoMapper = modificationsToDtoMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file modifications related to a revision.
|
||||
* file modifications are for example: Modified, Added or Removed.
|
||||
*/
|
||||
@GET
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
|
||||
@ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the modifications"),
|
||||
@ResponseCode(code = 404, condition = "not found, no changeset with the specified id is available in the repository"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Produces(VndMediaType.MODIFICATIONS)
|
||||
@TypeHint(ModificationsDto.class)
|
||||
@Path("{revision}")
|
||||
public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision) throws IOException, RevisionNotFoundException, RepositoryNotFoundException , InternalRepositoryException {
|
||||
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
|
||||
Modifications modifications = repositoryService.getModificationsCommand()
|
||||
.revision(revision)
|
||||
.getModifications();
|
||||
ModificationsDto output = modificationsToDtoMapper.map(modifications, repositoryService.getRepository());
|
||||
if (modifications != null ) {
|
||||
return Response.ok(output).build();
|
||||
} else {
|
||||
return Response.status(Response.Status.NOT_FOUND).build();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import de.otto.edison.hal.Links;
|
||||
import org.mapstruct.AfterMapping;
|
||||
import org.mapstruct.Context;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
import org.mapstruct.MappingTarget;
|
||||
import sonia.scm.repository.Modifications;
|
||||
import sonia.scm.repository.Repository;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static de.otto.edison.hal.Links.linkingTo;
|
||||
|
||||
@Mapper
|
||||
public abstract class ModificationsToDtoMapper {
|
||||
|
||||
@Inject
|
||||
private ResourceLinks resourceLinks;
|
||||
|
||||
@Mapping(target = "attributes", ignore = true) // We do not map HAL attributes
|
||||
public abstract ModificationsDto map(Modifications modifications, @Context Repository repository);
|
||||
|
||||
@AfterMapping
|
||||
void appendLinks(@MappingTarget ModificationsDto target, @Context Repository repository) {
|
||||
Links.Builder linksBuilder = linkingTo()
|
||||
.self(resourceLinks.modifications().self(repository.getNamespace(), repository.getName(), target.getRevision()));
|
||||
target.add(linksBuilder.build());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import de.otto.edison.hal.Embedded;
|
||||
import de.otto.edison.hal.HalRepresentation;
|
||||
import de.otto.edison.hal.Links;
|
||||
import de.otto.edison.hal.paging.NumberedPaging;
|
||||
import de.otto.edison.hal.paging.PagingRel;
|
||||
import sonia.scm.ModelObject;
|
||||
import sonia.scm.PageResult;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static com.damnhandy.uri.template.UriTemplate.fromTemplate;
|
||||
import static de.otto.edison.hal.Embedded.embeddedBuilder;
|
||||
import static de.otto.edison.hal.Link.link;
|
||||
import static de.otto.edison.hal.Links.linkingTo;
|
||||
import static de.otto.edison.hal.paging.NumberedPaging.zeroBasedNumberedPaging;
|
||||
import static java.util.stream.Collectors.toList;
|
||||
|
||||
abstract class PagedCollectionToDtoMapper<E extends ModelObject, D extends HalRepresentation> {
|
||||
|
||||
private final String collectionName;
|
||||
|
||||
PagedCollectionToDtoMapper(String collectionName) {
|
||||
this.collectionName = collectionName;
|
||||
}
|
||||
|
||||
CollectionDto map(int pageNumber, int pageSize, PageResult<E> pageResult, String selfLink, Optional<String> createLink, Function<E, ? extends HalRepresentation> mapper) {
|
||||
NumberedPaging paging = zeroBasedNumberedPaging(pageNumber, pageSize, pageResult.getOverallCount());
|
||||
List<HalRepresentation> dtos = pageResult.getEntities().stream().map(mapper).collect(toList());
|
||||
CollectionDto collectionDto = new CollectionDto(
|
||||
createLinks(paging, selfLink, createLink),
|
||||
embedDtos(dtos));
|
||||
collectionDto.setPage(pageNumber);
|
||||
collectionDto.setPageTotal(computePageTotal(pageSize, pageResult));
|
||||
return collectionDto;
|
||||
}
|
||||
|
||||
private int computePageTotal(int pageSize, PageResult<E> pageResult) {
|
||||
if (pageResult.getOverallCount() % pageSize > 0) {
|
||||
return pageResult.getOverallCount() / pageSize + 1;
|
||||
} else {
|
||||
return pageResult.getOverallCount() / pageSize;
|
||||
}
|
||||
}
|
||||
|
||||
private Links createLinks(NumberedPaging page, String selfLink, Optional<String> createLink) {
|
||||
Links.Builder linksBuilder = linkingTo()
|
||||
.with(page.links(
|
||||
fromTemplate(selfLink + "{?page,pageSize}"),
|
||||
EnumSet.allOf(PagingRel.class)));
|
||||
createLink.ifPresent(link -> linksBuilder.single(link("create", link)));
|
||||
return linksBuilder.build();
|
||||
}
|
||||
|
||||
private Embedded embedDtos(List<HalRepresentation> dtos) {
|
||||
return embeddedBuilder()
|
||||
.with(collectionName, dtos)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
import org.hibernate.validator.constraints.NotEmpty;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
public class PasswordChangeDto {
|
||||
|
||||
private String oldPassword;
|
||||
|
||||
@NotEmpty
|
||||
private String newPassword;
|
||||
}
|
||||
@@ -4,15 +4,20 @@ import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import de.otto.edison.hal.HalRepresentation;
|
||||
import de.otto.edison.hal.Links;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
@Getter @Setter @ToString
|
||||
import javax.validation.constraints.Pattern;
|
||||
|
||||
import static sonia.scm.api.v2.ValidationConstraints.USER_GROUP_PATTERN;
|
||||
|
||||
@Getter @Setter @ToString @NoArgsConstructor
|
||||
public class PermissionDto extends HalRepresentation {
|
||||
|
||||
public static final String GROUP_PREFIX = "@";
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
@Pattern(regexp = USER_GROUP_PATTERN)
|
||||
private String name;
|
||||
|
||||
/**
|
||||
@@ -28,9 +33,6 @@ public class PermissionDto extends HalRepresentation {
|
||||
|
||||
private boolean groupPermission = false;
|
||||
|
||||
public PermissionDto() {
|
||||
}
|
||||
|
||||
public PermissionDto(String permissionName, boolean groupPermission) {
|
||||
name = permissionName;
|
||||
this.groupPermission = groupPermission;
|
||||
|
||||
@@ -16,6 +16,7 @@ import sonia.scm.repository.RepositoryPermissions;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.validation.Valid;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.DELETE;
|
||||
import javax.ws.rs.GET;
|
||||
@@ -70,7 +71,7 @@ public class PermissionRootResource {
|
||||
@TypeHint(TypeHint.NO_CONTENT.class)
|
||||
@Consumes(VndMediaType.PERMISSION)
|
||||
@Path("")
|
||||
public Response create(@PathParam("namespace") String namespace, @PathParam("name") String name, PermissionDto permission) throws AlreadyExistsException, NotFoundException {
|
||||
public Response create(@PathParam("namespace") String namespace, @PathParam("name") String name,@Valid PermissionDto permission) throws AlreadyExistsException, NotFoundException {
|
||||
log.info("try to add new permission: {}", permission);
|
||||
Repository repository = load(namespace, name);
|
||||
RepositoryPermissions.permissionWrite(repository).check();
|
||||
@@ -157,13 +158,13 @@ public class PermissionRootResource {
|
||||
public Response update(@PathParam("namespace") String namespace,
|
||||
@PathParam("name") String name,
|
||||
@PathParam("permission-name") String permissionName,
|
||||
PermissionDto permission) throws NotFoundException, AlreadyExistsException {
|
||||
@Valid PermissionDto permission) throws NotFoundException, AlreadyExistsException {
|
||||
log.info("try to update the permission with name: {}. the modified permission is: {}", permissionName, permission);
|
||||
Repository repository = load(namespace, name);
|
||||
RepositoryPermissions.permissionWrite(repository).check();
|
||||
String extractedPermissionName = getPermissionName(permissionName);
|
||||
if (!isPermissionExist(new PermissionDto(extractedPermissionName, isGroupPermission(permissionName)), repository)) {
|
||||
throw new NotFoundException("the permission " + extractedPermissionName + " does not exist");
|
||||
throw new NotFoundException("permission", extractedPermissionName);
|
||||
}
|
||||
permission.setGroupPermission(isGroupPermission(permissionName));
|
||||
if (!extractedPermissionName.equals(permission.getName())) {
|
||||
@@ -239,8 +240,9 @@ public class PermissionRootResource {
|
||||
* @throws RepositoryNotFoundException if the repository does not exists
|
||||
*/
|
||||
private Repository load(String namespace, String name) throws RepositoryNotFoundException {
|
||||
return Optional.ofNullable(manager.get(new NamespaceAndName(namespace, name)))
|
||||
.orElseThrow(() -> new RepositoryNotFoundException(name));
|
||||
NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name);
|
||||
return Optional.ofNullable(manager.get(namespaceAndName))
|
||||
.orElseThrow(() -> new RepositoryNotFoundException(namespaceAndName));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,8 +10,6 @@ import java.util.Optional;
|
||||
import static java.util.Optional.empty;
|
||||
import static java.util.Optional.of;
|
||||
|
||||
// Mapstruct does not support parameterized (i.e. non-default) constructors. Thus, we need to use field injection.
|
||||
@SuppressWarnings("squid:S3306")
|
||||
public class RepositoryCollectionToDtoMapper extends BasicCollectionToDtoMapper<Repository, RepositoryDto, RepositoryToRepositoryDtoMapper> {
|
||||
|
||||
private final ResourceLinks resourceLinks;
|
||||
|
||||
@@ -24,7 +24,7 @@ public class RepositoryDto extends HalRepresentation {
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
private Instant lastModified;
|
||||
private String namespace;
|
||||
@Pattern(regexp = "(?!^\\.\\.$)(?!^\\.$)(?!.*[\\\\\\[\\]])^[A-z0-9\\.][A-z0-9\\.\\-_/]*$")
|
||||
@Pattern(regexp = "^[A-z0-9\\-_]+$")
|
||||
private String name;
|
||||
private boolean archived = false;
|
||||
@NotEmpty
|
||||
|
||||
@@ -40,6 +40,8 @@ public class RepositoryResource {
|
||||
private final Provider<ContentResource> contentResource;
|
||||
private final Provider<PermissionRootResource> permissionRootResource;
|
||||
private final Provider<DiffRootResource> diffRootResource;
|
||||
private final Provider<ModificationsRootResource> modificationsRootResource;
|
||||
private final Provider<FileHistoryRootResource> fileHistoryRootResource;
|
||||
|
||||
@Inject
|
||||
public RepositoryResource(
|
||||
@@ -50,7 +52,10 @@ public class RepositoryResource {
|
||||
Provider<ChangesetRootResource> changesetRootResource,
|
||||
Provider<SourceRootResource> sourceRootResource, Provider<ContentResource> contentResource,
|
||||
Provider<PermissionRootResource> permissionRootResource,
|
||||
Provider<DiffRootResource> diffRootResource) {
|
||||
Provider<DiffRootResource> diffRootResource,
|
||||
Provider<ModificationsRootResource> modificationsRootResource,
|
||||
Provider<FileHistoryRootResource> fileHistoryRootResource
|
||||
) {
|
||||
this.dtoToRepositoryMapper = dtoToRepositoryMapper;
|
||||
this.manager = manager;
|
||||
this.repositoryToDtoMapper = repositoryToDtoMapper;
|
||||
@@ -62,6 +67,8 @@ public class RepositoryResource {
|
||||
this.contentResource = contentResource;
|
||||
this.permissionRootResource = permissionRootResource;
|
||||
this.diffRootResource = diffRootResource;
|
||||
this.modificationsRootResource = modificationsRootResource;
|
||||
this.fileHistoryRootResource = fileHistoryRootResource;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -165,6 +172,11 @@ public class RepositoryResource {
|
||||
return changesetRootResource.get();
|
||||
}
|
||||
|
||||
@Path("history/")
|
||||
public FileHistoryRootResource history() {
|
||||
return fileHistoryRootResource.get();
|
||||
}
|
||||
|
||||
@Path("sources/")
|
||||
public SourceRootResource sources() {
|
||||
return sourceRootResource.get();
|
||||
@@ -180,6 +192,9 @@ public class RepositoryResource {
|
||||
return permissionRootResource.get();
|
||||
}
|
||||
|
||||
@Path("modifications/")
|
||||
public ModificationsRootResource modifications() {return modificationsRootResource.get(); }
|
||||
|
||||
private Optional<Response> handleNotArchived(Throwable throwable) {
|
||||
if (throwable instanceof RepositoryIsNotArchivedException) {
|
||||
return Optional.of(Response.status(Response.Status.PRECONDITION_FAILED).build());
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import de.otto.edison.hal.Link;
|
||||
import de.otto.edison.hal.Links;
|
||||
import org.mapstruct.AfterMapping;
|
||||
import org.mapstruct.Mapper;
|
||||
@@ -11,9 +12,13 @@ import sonia.scm.repository.RepositoryPermissions;
|
||||
import sonia.scm.repository.api.Command;
|
||||
import sonia.scm.repository.api.RepositoryService;
|
||||
import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||
import sonia.scm.repository.api.ScmProtocol;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static de.otto.edison.hal.Link.link;
|
||||
import static de.otto.edison.hal.Links.linkingTo;
|
||||
import static java.util.stream.Collectors.toList;
|
||||
|
||||
// Mapstruct does not support parameterized (i.e. non-default) constructors. Thus, we need to use field injection.
|
||||
@SuppressWarnings("squid:S3306")
|
||||
@@ -30,7 +35,6 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper<Reposit
|
||||
@AfterMapping
|
||||
void appendLinks(Repository repository, @MappingTarget RepositoryDto target) {
|
||||
Links.Builder linksBuilder = linkingTo().self(resourceLinks.repository().self(target.getNamespace(), target.getName()));
|
||||
linksBuilder.single(link("httpProtocol", resourceLinks.repository().clone(target.getType(), target.getNamespace(), target.getName())));
|
||||
if (RepositoryPermissions.delete(repository).isPermitted()) {
|
||||
linksBuilder.single(link("delete", resourceLinks.repository().delete(target.getNamespace(), target.getName())));
|
||||
}
|
||||
@@ -39,6 +43,12 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper<Reposit
|
||||
linksBuilder.single(link("permissions", resourceLinks.permission().all(target.getNamespace(), target.getName())));
|
||||
}
|
||||
try (RepositoryService repositoryService = serviceFactory.create(repository)) {
|
||||
if (RepositoryPermissions.pull(repository).isPermitted()) {
|
||||
List<Link> protocolLinks = repositoryService.getSupportedProtocols()
|
||||
.map(this::createProtocolLink)
|
||||
.collect(toList());
|
||||
linksBuilder.array(protocolLinks);
|
||||
}
|
||||
if (repositoryService.isSupported(Command.TAGS)) {
|
||||
linksBuilder.single(link("tags", resourceLinks.tag().all(target.getNamespace(), target.getName())));
|
||||
}
|
||||
@@ -50,4 +60,8 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper<Reposit
|
||||
linksBuilder.single(link("sources", resourceLinks.source().selfWithoutRevision(target.getNamespace(), target.getName())));
|
||||
target.add(linksBuilder.build());
|
||||
}
|
||||
|
||||
private Link createProtocolLink(ScmProtocol protocol) {
|
||||
return Link.linkBuilder("protocol", protocol.getUrl()).withName(protocol.getType()).build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,28 +3,31 @@ package sonia.scm.api.v2.resources;
|
||||
import sonia.scm.repository.NamespaceAndName;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
import java.net.URI;
|
||||
|
||||
class ResourceLinks {
|
||||
|
||||
private final UriInfoStore uriInfoStore;
|
||||
private final ScmPathInfoStore scmPathInfoStore;
|
||||
|
||||
@Inject
|
||||
ResourceLinks(UriInfoStore uriInfoStore) {
|
||||
this.uriInfoStore = uriInfoStore;
|
||||
ResourceLinks(ScmPathInfoStore scmPathInfoStore) {
|
||||
this.scmPathInfoStore = scmPathInfoStore;
|
||||
}
|
||||
|
||||
// we have to add the file path using URI, so that path separators (aka '/') will not be encoded as '%2F'
|
||||
private static String addPath(String sourceWithPath, String path) {
|
||||
return URI.create(sourceWithPath).resolve(path).toASCIIString();
|
||||
}
|
||||
|
||||
GroupLinks group() {
|
||||
return new GroupLinks(uriInfoStore.get());
|
||||
return new GroupLinks(scmPathInfoStore.get());
|
||||
}
|
||||
|
||||
static class GroupLinks {
|
||||
private final LinkBuilder groupLinkBuilder;
|
||||
|
||||
GroupLinks(UriInfo uriInfo) {
|
||||
groupLinkBuilder = new LinkBuilder(uriInfo, GroupRootResource.class, GroupResource.class);
|
||||
GroupLinks(ScmPathInfo pathInfo) {
|
||||
groupLinkBuilder = new LinkBuilder(pathInfo, GroupRootResource.class, GroupResource.class);
|
||||
}
|
||||
|
||||
String self(String name) {
|
||||
@@ -41,14 +44,14 @@ class ResourceLinks {
|
||||
}
|
||||
|
||||
GroupCollectionLinks groupCollection() {
|
||||
return new GroupCollectionLinks(uriInfoStore.get());
|
||||
return new GroupCollectionLinks(scmPathInfoStore.get());
|
||||
}
|
||||
|
||||
static class GroupCollectionLinks {
|
||||
private final LinkBuilder collectionLinkBuilder;
|
||||
|
||||
GroupCollectionLinks(UriInfo uriInfo) {
|
||||
collectionLinkBuilder = new LinkBuilder(uriInfo, GroupRootResource.class, GroupCollectionResource.class);
|
||||
GroupCollectionLinks(ScmPathInfo pathInfo) {
|
||||
collectionLinkBuilder = new LinkBuilder(pathInfo, GroupRootResource.class, GroupCollectionResource.class);
|
||||
}
|
||||
|
||||
String self() {
|
||||
@@ -61,14 +64,14 @@ class ResourceLinks {
|
||||
}
|
||||
|
||||
UserLinks user() {
|
||||
return new UserLinks(uriInfoStore.get());
|
||||
return new UserLinks(scmPathInfoStore.get());
|
||||
}
|
||||
|
||||
static class UserLinks {
|
||||
private final LinkBuilder userLinkBuilder;
|
||||
|
||||
UserLinks(UriInfo uriInfo) {
|
||||
userLinkBuilder = new LinkBuilder(uriInfo, UserRootResource.class, UserResource.class);
|
||||
UserLinks(ScmPathInfo pathInfo) {
|
||||
userLinkBuilder = new LinkBuilder(pathInfo, UserRootResource.class, UserResource.class);
|
||||
}
|
||||
|
||||
String self(String name) {
|
||||
@@ -82,17 +85,52 @@ class ResourceLinks {
|
||||
String update(String name) {
|
||||
return userLinkBuilder.method("getUserResource").parameters(name).method("update").parameters().href();
|
||||
}
|
||||
|
||||
public String passwordChange(String name) {
|
||||
return userLinkBuilder.method("getUserResource").parameters(name).method("changePassword").parameters().href();
|
||||
}
|
||||
}
|
||||
|
||||
MeLinks me() {
|
||||
return new MeLinks(scmPathInfoStore.get(), this.user());
|
||||
}
|
||||
|
||||
static class MeLinks {
|
||||
private final LinkBuilder meLinkBuilder;
|
||||
private UserLinks userLinks;
|
||||
|
||||
MeLinks(ScmPathInfo pathInfo, UserLinks user) {
|
||||
meLinkBuilder = new LinkBuilder(pathInfo, MeResource.class);
|
||||
userLinks = user;
|
||||
}
|
||||
|
||||
String self() {
|
||||
return meLinkBuilder.method("get").parameters().href();
|
||||
}
|
||||
|
||||
String delete(String name) {
|
||||
return userLinks.delete(name);
|
||||
}
|
||||
|
||||
String update(String name) {
|
||||
return userLinks.update(name);
|
||||
}
|
||||
|
||||
public String passwordChange() {
|
||||
return meLinkBuilder.method("changePassword").parameters().href();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
UserCollectionLinks userCollection() {
|
||||
return new UserCollectionLinks(uriInfoStore.get());
|
||||
return new UserCollectionLinks(scmPathInfoStore.get());
|
||||
}
|
||||
|
||||
static class UserCollectionLinks {
|
||||
private final LinkBuilder collectionLinkBuilder;
|
||||
|
||||
UserCollectionLinks(UriInfo uriInfo) {
|
||||
collectionLinkBuilder = new LinkBuilder(uriInfo, UserRootResource.class, UserCollectionResource.class);
|
||||
UserCollectionLinks(ScmPathInfo pathInfo) {
|
||||
collectionLinkBuilder = new LinkBuilder(pathInfo, UserRootResource.class, UserCollectionResource.class);
|
||||
}
|
||||
|
||||
String self() {
|
||||
@@ -105,14 +143,14 @@ class ResourceLinks {
|
||||
}
|
||||
|
||||
ConfigLinks config() {
|
||||
return new ConfigLinks(uriInfoStore.get());
|
||||
return new ConfigLinks(scmPathInfoStore.get());
|
||||
}
|
||||
|
||||
static class ConfigLinks {
|
||||
private final LinkBuilder configLinkBuilder;
|
||||
|
||||
ConfigLinks(UriInfo uriInfo) {
|
||||
configLinkBuilder = new LinkBuilder(uriInfo, ConfigResource.class);
|
||||
ConfigLinks(ScmPathInfo pathInfo) {
|
||||
configLinkBuilder = new LinkBuilder(pathInfo, ConfigResource.class);
|
||||
}
|
||||
|
||||
String self() {
|
||||
@@ -125,26 +163,20 @@ class ResourceLinks {
|
||||
}
|
||||
|
||||
public RepositoryLinks repository() {
|
||||
return new RepositoryLinks(uriInfoStore.get());
|
||||
return new RepositoryLinks(scmPathInfoStore.get());
|
||||
}
|
||||
|
||||
static class RepositoryLinks {
|
||||
private final LinkBuilder repositoryLinkBuilder;
|
||||
private final UriInfo uriInfo;
|
||||
|
||||
RepositoryLinks(UriInfo uriInfo) {
|
||||
repositoryLinkBuilder = new LinkBuilder(uriInfo, RepositoryRootResource.class, RepositoryResource.class);
|
||||
this.uriInfo = uriInfo;
|
||||
RepositoryLinks(ScmPathInfo pathInfo) {
|
||||
repositoryLinkBuilder = new LinkBuilder(pathInfo, RepositoryRootResource.class, RepositoryResource.class);
|
||||
}
|
||||
|
||||
String self(String namespace, String name) {
|
||||
return repositoryLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("get").parameters().href();
|
||||
}
|
||||
|
||||
String clone(String type, String namespace, String name) {
|
||||
return uriInfo.getBaseUri().resolve(URI.create("../../" + type + "/" + namespace + "/" + name)).toASCIIString();
|
||||
}
|
||||
|
||||
String delete(String namespace, String name) {
|
||||
return repositoryLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("delete").parameters().href();
|
||||
}
|
||||
@@ -155,14 +187,14 @@ class ResourceLinks {
|
||||
}
|
||||
|
||||
RepositoryCollectionLinks repositoryCollection() {
|
||||
return new RepositoryCollectionLinks(uriInfoStore.get());
|
||||
return new RepositoryCollectionLinks(scmPathInfoStore.get());
|
||||
}
|
||||
|
||||
static class RepositoryCollectionLinks {
|
||||
private final LinkBuilder collectionLinkBuilder;
|
||||
|
||||
RepositoryCollectionLinks(UriInfo uriInfo) {
|
||||
collectionLinkBuilder = new LinkBuilder(uriInfo, RepositoryRootResource.class, RepositoryCollectionResource.class);
|
||||
RepositoryCollectionLinks(ScmPathInfo pathInfo) {
|
||||
collectionLinkBuilder = new LinkBuilder(pathInfo, RepositoryRootResource.class, RepositoryCollectionResource.class);
|
||||
}
|
||||
|
||||
String self() {
|
||||
@@ -175,14 +207,14 @@ class ResourceLinks {
|
||||
}
|
||||
|
||||
public RepositoryTypeLinks repositoryType() {
|
||||
return new RepositoryTypeLinks(uriInfoStore.get());
|
||||
return new RepositoryTypeLinks(scmPathInfoStore.get());
|
||||
}
|
||||
|
||||
static class RepositoryTypeLinks {
|
||||
private final LinkBuilder repositoryTypeLinkBuilder;
|
||||
|
||||
RepositoryTypeLinks(UriInfo uriInfo) {
|
||||
repositoryTypeLinkBuilder = new LinkBuilder(uriInfo, RepositoryTypeRootResource.class, RepositoryTypeResource.class);
|
||||
RepositoryTypeLinks(ScmPathInfo pathInfo) {
|
||||
repositoryTypeLinkBuilder = new LinkBuilder(pathInfo, RepositoryTypeRootResource.class, RepositoryTypeResource.class);
|
||||
}
|
||||
|
||||
String self(String name) {
|
||||
@@ -191,14 +223,14 @@ class ResourceLinks {
|
||||
}
|
||||
|
||||
public RepositoryTypeCollectionLinks repositoryTypeCollection() {
|
||||
return new RepositoryTypeCollectionLinks(uriInfoStore.get());
|
||||
return new RepositoryTypeCollectionLinks(scmPathInfoStore.get());
|
||||
}
|
||||
|
||||
static class RepositoryTypeCollectionLinks {
|
||||
private final LinkBuilder collectionLinkBuilder;
|
||||
|
||||
RepositoryTypeCollectionLinks(UriInfo uriInfo) {
|
||||
collectionLinkBuilder = new LinkBuilder(uriInfo, RepositoryTypeRootResource.class, RepositoryTypeCollectionResource.class);
|
||||
RepositoryTypeCollectionLinks(ScmPathInfo pathInfo) {
|
||||
collectionLinkBuilder = new LinkBuilder(pathInfo, RepositoryTypeRootResource.class, RepositoryTypeCollectionResource.class);
|
||||
}
|
||||
|
||||
String self() {
|
||||
@@ -208,14 +240,14 @@ class ResourceLinks {
|
||||
|
||||
|
||||
public TagCollectionLinks tag() {
|
||||
return new TagCollectionLinks(uriInfoStore.get());
|
||||
return new TagCollectionLinks(scmPathInfoStore.get());
|
||||
}
|
||||
|
||||
static class TagCollectionLinks {
|
||||
private final LinkBuilder tagLinkBuilder;
|
||||
|
||||
TagCollectionLinks(UriInfo uriInfo) {
|
||||
tagLinkBuilder = new LinkBuilder(uriInfo, RepositoryRootResource.class, RepositoryResource.class, TagRootResource.class);
|
||||
TagCollectionLinks(ScmPathInfo pathInfo) {
|
||||
tagLinkBuilder = new LinkBuilder(pathInfo, RepositoryRootResource.class, RepositoryResource.class, TagRootResource.class);
|
||||
}
|
||||
|
||||
String self(String namespace, String name, String tagName) {
|
||||
@@ -228,14 +260,14 @@ class ResourceLinks {
|
||||
}
|
||||
|
||||
public DiffLinks diff() {
|
||||
return new DiffLinks(uriInfoStore.get());
|
||||
return new DiffLinks(scmPathInfoStore.get());
|
||||
}
|
||||
|
||||
static class DiffLinks {
|
||||
private final LinkBuilder diffLinkBuilder;
|
||||
|
||||
DiffLinks(UriInfo uriInfo) {
|
||||
diffLinkBuilder = new LinkBuilder(uriInfo, RepositoryRootResource.class, RepositoryResource.class, DiffRootResource.class);
|
||||
DiffLinks(ScmPathInfo pathInfo) {
|
||||
diffLinkBuilder = new LinkBuilder(pathInfo, RepositoryRootResource.class, RepositoryResource.class, DiffRootResource.class);
|
||||
}
|
||||
|
||||
String self(String namespace, String name, String id) {
|
||||
@@ -248,14 +280,14 @@ class ResourceLinks {
|
||||
}
|
||||
|
||||
public BranchLinks branch() {
|
||||
return new BranchLinks(uriInfoStore.get());
|
||||
return new BranchLinks(scmPathInfoStore.get());
|
||||
}
|
||||
|
||||
static class BranchLinks {
|
||||
private final LinkBuilder branchLinkBuilder;
|
||||
|
||||
BranchLinks(UriInfo uriInfo) {
|
||||
branchLinkBuilder = new LinkBuilder(uriInfo, RepositoryRootResource.class, RepositoryResource.class, BranchRootResource.class);
|
||||
BranchLinks(ScmPathInfo pathInfo) {
|
||||
branchLinkBuilder = new LinkBuilder(pathInfo, RepositoryRootResource.class, RepositoryResource.class, BranchRootResource.class);
|
||||
}
|
||||
|
||||
String self(NamespaceAndName namespaceAndName, String branch) {
|
||||
@@ -268,14 +300,14 @@ class ResourceLinks {
|
||||
}
|
||||
|
||||
public BranchCollectionLinks branchCollection() {
|
||||
return new BranchCollectionLinks(uriInfoStore.get());
|
||||
return new BranchCollectionLinks(scmPathInfoStore.get());
|
||||
}
|
||||
|
||||
static class BranchCollectionLinks {
|
||||
private final LinkBuilder branchLinkBuilder;
|
||||
|
||||
BranchCollectionLinks(UriInfo uriInfo) {
|
||||
branchLinkBuilder = new LinkBuilder(uriInfo, RepositoryRootResource.class, RepositoryResource.class, BranchRootResource.class);
|
||||
BranchCollectionLinks(ScmPathInfo pathInfo) {
|
||||
branchLinkBuilder = new LinkBuilder(pathInfo, RepositoryRootResource.class, RepositoryResource.class, BranchRootResource.class);
|
||||
}
|
||||
|
||||
String self(String namespace, String name) {
|
||||
@@ -284,14 +316,14 @@ class ResourceLinks {
|
||||
}
|
||||
|
||||
public ChangesetLinks changeset() {
|
||||
return new ChangesetLinks(uriInfoStore.get());
|
||||
return new ChangesetLinks(scmPathInfoStore.get());
|
||||
}
|
||||
|
||||
static class ChangesetLinks {
|
||||
private final LinkBuilder changesetLinkBuilder;
|
||||
|
||||
ChangesetLinks(UriInfo uriInfo) {
|
||||
changesetLinkBuilder = new LinkBuilder(uriInfo, RepositoryRootResource.class, RepositoryResource.class, ChangesetRootResource.class);
|
||||
ChangesetLinks(ScmPathInfo pathInfo) {
|
||||
changesetLinkBuilder = new LinkBuilder(pathInfo, RepositoryRootResource.class, RepositoryResource.class, ChangesetRootResource.class);
|
||||
}
|
||||
|
||||
String self(String namespace, String name, String changesetId) {
|
||||
@@ -307,15 +339,47 @@ class ResourceLinks {
|
||||
}
|
||||
}
|
||||
|
||||
public ModificationsLinks modifications() {
|
||||
return new ModificationsLinks(scmPathInfoStore.get());
|
||||
}
|
||||
|
||||
static class ModificationsLinks {
|
||||
private final LinkBuilder modificationsLinkBuilder;
|
||||
|
||||
ModificationsLinks(ScmPathInfo pathInfo) {
|
||||
modificationsLinkBuilder = new LinkBuilder(pathInfo, RepositoryRootResource.class, RepositoryResource.class, ModificationsRootResource.class);
|
||||
}
|
||||
String self(String namespace, String name, String revision) {
|
||||
return modificationsLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("modifications").parameters().method("get").parameters(revision).href();
|
||||
}
|
||||
}
|
||||
|
||||
public FileHistoryLinks fileHistory() {
|
||||
return new FileHistoryLinks(scmPathInfoStore.get());
|
||||
}
|
||||
|
||||
static class FileHistoryLinks {
|
||||
private final LinkBuilder fileHistoryLinkBuilder;
|
||||
|
||||
FileHistoryLinks(ScmPathInfo pathInfo) {
|
||||
fileHistoryLinkBuilder = new LinkBuilder(pathInfo, RepositoryRootResource.class, RepositoryResource.class, FileHistoryRootResource.class);
|
||||
}
|
||||
|
||||
String self(String namespace, String name, String changesetId, String path) {
|
||||
return addPath(fileHistoryLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("history").parameters().method("getAll").parameters(changesetId, "").href(), path);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public SourceLinks source() {
|
||||
return new SourceLinks(uriInfoStore.get());
|
||||
return new SourceLinks(scmPathInfoStore.get());
|
||||
}
|
||||
|
||||
static class SourceLinks {
|
||||
private final LinkBuilder sourceLinkBuilder;
|
||||
|
||||
SourceLinks(UriInfo uriInfo) {
|
||||
sourceLinkBuilder = new LinkBuilder(uriInfo, RepositoryRootResource.class, RepositoryResource.class, SourceRootResource.class);
|
||||
SourceLinks(ScmPathInfo pathInfo) {
|
||||
sourceLinkBuilder = new LinkBuilder(pathInfo, RepositoryRootResource.class, RepositoryResource.class, SourceRootResource.class);
|
||||
}
|
||||
|
||||
String self(String namespace, String name, String revision) {
|
||||
@@ -338,20 +402,17 @@ class ResourceLinks {
|
||||
return addPath(sourceLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("content").parameters().method("get").parameters(revision, "").href(), path);
|
||||
}
|
||||
|
||||
// we have to add the file path using URI, so that path separators (aka '/') will not be encoded as '%2F'
|
||||
private String addPath(String sourceWithPath, String path) {
|
||||
return URI.create(sourceWithPath).resolve(path).toASCIIString();
|
||||
}
|
||||
|
||||
}
|
||||
public PermissionLinks permission() {
|
||||
return new PermissionLinks(uriInfoStore.get());
|
||||
return new PermissionLinks(scmPathInfoStore.get());
|
||||
}
|
||||
|
||||
static class PermissionLinks {
|
||||
private final LinkBuilder permissionLinkBuilder;
|
||||
|
||||
PermissionLinks(UriInfo uriInfo) {
|
||||
permissionLinkBuilder = new LinkBuilder(uriInfo, RepositoryRootResource.class, RepositoryResource.class, PermissionRootResource.class);
|
||||
PermissionLinks(ScmPathInfo pathInfo) {
|
||||
permissionLinkBuilder = new LinkBuilder(pathInfo, RepositoryRootResource.class, RepositoryResource.class, PermissionRootResource.class);
|
||||
}
|
||||
|
||||
String all(String namespace, String name) {
|
||||
@@ -379,16 +440,15 @@ class ResourceLinks {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public UIPluginLinks uiPlugin() {
|
||||
return new UIPluginLinks(uriInfoStore.get());
|
||||
return new UIPluginLinks(scmPathInfoStore.get());
|
||||
}
|
||||
|
||||
static class UIPluginLinks {
|
||||
private final LinkBuilder uiPluginLinkBuilder;
|
||||
|
||||
UIPluginLinks(UriInfo uriInfo) {
|
||||
uiPluginLinkBuilder = new LinkBuilder(uriInfo, UIRootResource.class, UIPluginResource.class);
|
||||
UIPluginLinks(ScmPathInfo pathInfo) {
|
||||
uiPluginLinkBuilder = new LinkBuilder(pathInfo, UIRootResource.class, UIPluginResource.class);
|
||||
}
|
||||
|
||||
String self(String id) {
|
||||
@@ -397,18 +457,59 @@ class ResourceLinks {
|
||||
}
|
||||
|
||||
public UIPluginCollectionLinks uiPluginCollection() {
|
||||
return new UIPluginCollectionLinks(uriInfoStore.get());
|
||||
return new UIPluginCollectionLinks(scmPathInfoStore.get());
|
||||
}
|
||||
|
||||
static class UIPluginCollectionLinks {
|
||||
private final LinkBuilder uiPluginCollectionLinkBuilder;
|
||||
|
||||
UIPluginCollectionLinks(UriInfo uriInfo) {
|
||||
uiPluginCollectionLinkBuilder = new LinkBuilder(uriInfo, UIRootResource.class, UIPluginResource.class);
|
||||
UIPluginCollectionLinks(ScmPathInfo pathInfo) {
|
||||
uiPluginCollectionLinkBuilder = new LinkBuilder(pathInfo, UIRootResource.class, UIPluginResource.class);
|
||||
}
|
||||
|
||||
String self() {
|
||||
return uiPluginCollectionLinkBuilder.method("plugins").parameters().method("getInstalledPlugins").parameters().href();
|
||||
}
|
||||
}
|
||||
|
||||
public AuthenticationLinks authentication() {
|
||||
return new AuthenticationLinks(scmPathInfoStore.get());
|
||||
}
|
||||
|
||||
static class AuthenticationLinks {
|
||||
private final LinkBuilder loginLinkBuilder;
|
||||
|
||||
AuthenticationLinks(ScmPathInfo pathInfo) {
|
||||
this.loginLinkBuilder = new LinkBuilder(pathInfo, AuthenticationResource.class);
|
||||
}
|
||||
|
||||
String formLogin() {
|
||||
return loginLinkBuilder.method("authenticateViaForm").parameters().href();
|
||||
}
|
||||
|
||||
String jsonLogin() {
|
||||
return loginLinkBuilder.method("authenticateViaJSONBody").parameters().href();
|
||||
}
|
||||
|
||||
String logout() {
|
||||
return loginLinkBuilder.method("logout").parameters().href();
|
||||
}
|
||||
}
|
||||
|
||||
public IndexLinks index() {
|
||||
return new IndexLinks(scmPathInfoStore.get());
|
||||
}
|
||||
|
||||
static class IndexLinks {
|
||||
private final LinkBuilder indexLinkBuilder;
|
||||
|
||||
IndexLinks(ScmPathInfo pathInfo) {
|
||||
indexLinkBuilder = new LinkBuilder(pathInfo, IndexResource.class);
|
||||
}
|
||||
|
||||
String self() {
|
||||
return indexLinkBuilder.method("getIndex").parameters().href();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import javax.ws.rs.core.GenericEntity;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.util.Collection;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.Supplier;
|
||||
@@ -53,6 +54,11 @@ class SingleResourceManagerAdapter<MODEL_OBJECT extends ModelObject,
|
||||
.map(Response.ResponseBuilder::build)
|
||||
.orElseThrow(NotFoundException::new);
|
||||
}
|
||||
public Response update(Supplier<Optional<MODEL_OBJECT>> reader, Function<MODEL_OBJECT, MODEL_OBJECT> applyChanges, Predicate<MODEL_OBJECT> hasSameKey, Consumer<MODEL_OBJECT> checker) throws NotFoundException, ConcurrentModificationException {
|
||||
MODEL_OBJECT existingModelObject = reader.get().orElseThrow(NotFoundException::new);
|
||||
checker.accept(existingModelObject);
|
||||
return update(reader,applyChanges,hasSameKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the model object for the given id according to the given function and returns a corresponding http response.
|
||||
|
||||
@@ -28,7 +28,7 @@ public abstract class TagToTagDtoMapper {
|
||||
Links.Builder linksBuilder = linkingTo()
|
||||
.self(resourceLinks.tag().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getName()))
|
||||
.single(link("sources", resourceLinks.source().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getRevision())))
|
||||
.single(link("changesets", resourceLinks.changeset().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getRevision())));
|
||||
.single(link("changeset", resourceLinks.changeset().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getRevision())));
|
||||
target.add(linksBuilder.build());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.webcohesion.enunciate.metadata.rs.ResponseHeader;
|
||||
import com.webcohesion.enunciate.metadata.rs.ResponseHeaders;
|
||||
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
|
||||
import com.webcohesion.enunciate.metadata.rs.TypeHint;
|
||||
import org.apache.shiro.authc.credential.PasswordService;
|
||||
import sonia.scm.AlreadyExistsException;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.user.UserManager;
|
||||
@@ -29,14 +30,16 @@ public class UserCollectionResource {
|
||||
private final ResourceLinks resourceLinks;
|
||||
|
||||
private final IdResourceManagerAdapter<User, UserDto> adapter;
|
||||
private final PasswordService passwordService;
|
||||
|
||||
@Inject
|
||||
public UserCollectionResource(UserManager manager, UserDtoToUserMapper dtoToUserMapper,
|
||||
UserCollectionToDtoMapper userCollectionToDtoMapper, ResourceLinks resourceLinks) {
|
||||
UserCollectionToDtoMapper userCollectionToDtoMapper, ResourceLinks resourceLinks, PasswordService passwordService) {
|
||||
this.dtoToUserMapper = dtoToUserMapper;
|
||||
this.userCollectionToDtoMapper = userCollectionToDtoMapper;
|
||||
this.adapter = new IdResourceManagerAdapter<>(manager, User.class);
|
||||
this.resourceLinks = resourceLinks;
|
||||
this.passwordService = passwordService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -89,8 +92,6 @@ public class UserCollectionResource {
|
||||
@TypeHint(TypeHint.NO_CONTENT.class)
|
||||
@ResponseHeaders(@ResponseHeader(name = "Location", description = "uri to the created user"))
|
||||
public Response create(@Valid UserDto userDto) throws AlreadyExistsException {
|
||||
return adapter.create(userDto,
|
||||
() -> dtoToUserMapper.map(userDto, ""),
|
||||
user -> resourceLinks.user().self(user.getName()));
|
||||
return adapter.create(userDto, () -> dtoToUserMapper.map(userDto, passwordService.encryptPassword(userDto.getPassword())), user -> resourceLinks.user().self(user.getName()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,6 @@ import java.util.Optional;
|
||||
import static java.util.Optional.empty;
|
||||
import static java.util.Optional.of;
|
||||
|
||||
// Mapstruct does not support parameterized (i.e. non-default) constructors. Thus, we need to use field injection.
|
||||
@SuppressWarnings("squid:S3306")
|
||||
public class UserCollectionToDtoMapper extends BasicCollectionToDtoMapper<User, UserDto, UserToUserDtoMapper> {
|
||||
|
||||
private final ResourceLinks resourceLinks;
|
||||
|
||||
@@ -13,6 +13,8 @@ import javax.validation.constraints.Pattern;
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
|
||||
import static sonia.scm.api.v2.ValidationConstraints.USER_GROUP_PATTERN;
|
||||
|
||||
@NoArgsConstructor @Getter @Setter
|
||||
public class UserDto extends HalRepresentation {
|
||||
private boolean active;
|
||||
@@ -24,8 +26,9 @@ public class UserDto extends HalRepresentation {
|
||||
private Instant lastModified;
|
||||
@NotEmpty @Email
|
||||
private String mail;
|
||||
@Pattern(regexp = "^[A-z0-9\\.\\-_@]|[^ ]([A-z0-9\\.\\-_@ ]*[A-z0-9\\.\\-_@]|[^ ])?$")
|
||||
@Pattern(regexp = USER_GROUP_PATTERN)
|
||||
private String name;
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
private String password;
|
||||
private String type;
|
||||
private Map<String, String> properties;
|
||||
|
||||
@@ -1,37 +1,35 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import org.apache.shiro.authc.credential.PasswordService;
|
||||
import org.mapstruct.AfterMapping;
|
||||
import org.mapstruct.Context;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
import org.mapstruct.Named;
|
||||
import org.mapstruct.MappingTarget;
|
||||
import sonia.scm.user.User;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import static sonia.scm.api.rest.resources.UserResource.DUMMY_PASSWORT;
|
||||
|
||||
// Mapstruct does not support parameterized (i.e. non-default) constructors. Thus, we need to use field injection.
|
||||
@SuppressWarnings("squid:S3306")
|
||||
@Mapper
|
||||
public abstract class UserDtoToUserMapper extends BaseDtoMapper {
|
||||
|
||||
@Inject
|
||||
private PasswordService passwordService;
|
||||
|
||||
@Mapping(source = "password", target = "password", qualifiedByName = "encrypt")
|
||||
@Mapping(target = "creationDate", ignore = true)
|
||||
public abstract User map(UserDto userDto, @Context String originalPassword);
|
||||
public abstract User map(UserDto userDto, @Context String usedPassword);
|
||||
|
||||
@Named("encrypt")
|
||||
String encrypt(String password, @Context String originalPassword) {
|
||||
|
||||
if (DUMMY_PASSWORT.equals(password)) {
|
||||
return originalPassword;
|
||||
} else {
|
||||
return passwordService.encryptPassword(password);
|
||||
}
|
||||
/**
|
||||
* depends on the use case the right password will be mapped.
|
||||
* The given Password in the context parameter will be set.
|
||||
* The mapper consumer have the control of what password should be set.
|
||||
* </p>
|
||||
* eg. for update user action the password will be set to the original password
|
||||
* for create user and change password actions the password is the user input
|
||||
*
|
||||
* @param usedPassword the password to be set
|
||||
* @param user the target
|
||||
*/
|
||||
@AfterMapping
|
||||
void overridePassword(@MappingTarget User user, @Context String usedPassword) {
|
||||
user.setPassword(usedPassword);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package sonia.scm.api.v2.resources;
|
||||
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
|
||||
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
|
||||
import com.webcohesion.enunciate.metadata.rs.TypeHint;
|
||||
import org.apache.shiro.authc.credential.PasswordService;
|
||||
import sonia.scm.ConcurrentModificationException;
|
||||
import sonia.scm.NotFoundException;
|
||||
import sonia.scm.user.User;
|
||||
@@ -26,12 +27,16 @@ public class UserResource {
|
||||
private final UserToUserDtoMapper userToDtoMapper;
|
||||
|
||||
private final IdResourceManagerAdapter<User, UserDto> adapter;
|
||||
private final UserManager userManager;
|
||||
private final PasswordService passwordService;
|
||||
|
||||
@Inject
|
||||
public UserResource(UserDtoToUserMapper dtoToUserMapper, UserToUserDtoMapper userToDtoMapper, UserManager manager) {
|
||||
public UserResource(UserDtoToUserMapper dtoToUserMapper, UserToUserDtoMapper userToDtoMapper, UserManager manager, PasswordService passwordService) {
|
||||
this.dtoToUserMapper = dtoToUserMapper;
|
||||
this.userToDtoMapper = userToDtoMapper;
|
||||
this.adapter = new IdResourceManagerAdapter<>(manager, User.class);
|
||||
this.userManager = manager;
|
||||
this.passwordService = passwordService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -40,7 +45,6 @@ public class UserResource {
|
||||
* <strong>Note:</strong> This method requires "user" privilege.
|
||||
*
|
||||
* @param id the id/name of the user
|
||||
*
|
||||
*/
|
||||
@GET
|
||||
@Path("")
|
||||
@@ -63,7 +67,6 @@ public class UserResource {
|
||||
* <strong>Note:</strong> This method requires "user" privilege.
|
||||
*
|
||||
* @param name the name of the user to delete.
|
||||
*
|
||||
*/
|
||||
@DELETE
|
||||
@Path("")
|
||||
@@ -80,10 +83,11 @@ public class UserResource {
|
||||
|
||||
/**
|
||||
* Modifies the given user.
|
||||
* The given Password in the payload will be ignored. To Change Password use the changePassword endpoint
|
||||
*
|
||||
* <strong>Note:</strong> This method requires "user" privilege.
|
||||
*
|
||||
* @param name name of the user to be modified
|
||||
* @param name name of the user to be modified
|
||||
* @param userDto user object to modify
|
||||
*/
|
||||
@PUT
|
||||
@@ -101,4 +105,30 @@ public class UserResource {
|
||||
public Response update(@PathParam("id") String name, @Valid UserDto userDto) throws NotFoundException, ConcurrentModificationException {
|
||||
return adapter.update(name, existing -> dtoToUserMapper.map(userDto, existing.getPassword()));
|
||||
}
|
||||
|
||||
/**
|
||||
* This Endpoint is for Admin user to modify a user password.
|
||||
* The oldPassword property of the DTO is not needed here. it will be ignored.
|
||||
* The oldPassword property is needed in the MeResources when the actual user change the own password.
|
||||
*
|
||||
* <strong>Note:</strong> This method requires "user:modify" privilege.
|
||||
* @param name name of the user to be modified
|
||||
* @param passwordChangeDto change password object to modify password. the old password is here not required
|
||||
*/
|
||||
@PUT
|
||||
@Path("password")
|
||||
@Consumes(VndMediaType.PASSWORD_CHANGE)
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 204, condition = "update success"),
|
||||
@ResponseCode(code = 400, condition = "Invalid body, e.g. the user type is not xml or the given oldPassword do not match the stored one"),
|
||||
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
|
||||
@ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"user\" privilege"),
|
||||
@ResponseCode(code = 404, condition = "not found, no user with the specified id/name available"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(TypeHint.NO_CONTENT.class)
|
||||
public Response changePassword(@PathParam("id") String name, @Valid PasswordChangeDto passwordChangeDto) throws NotFoundException, ConcurrentModificationException {
|
||||
return adapter.update(name, user -> user.changePassword(passwordService.encryptPassword(passwordChangeDto.getNewPassword())), userManager.getUserTypeChecker());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,9 +4,10 @@ import com.google.common.annotations.VisibleForTesting;
|
||||
import de.otto.edison.hal.Links;
|
||||
import org.mapstruct.AfterMapping;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
import org.mapstruct.MappingTarget;
|
||||
import sonia.scm.api.rest.resources.UserResource;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.user.UserManager;
|
||||
import sonia.scm.user.UserPermissions;
|
||||
|
||||
import javax.inject.Inject;
|
||||
@@ -19,21 +20,19 @@ import static de.otto.edison.hal.Links.linkingTo;
|
||||
@Mapper
|
||||
public abstract class UserToUserDtoMapper extends BaseMapper<User, UserDto> {
|
||||
|
||||
@Inject
|
||||
private UserManager userManager;
|
||||
|
||||
@Override
|
||||
@Mapping(target = "attributes", ignore = true)
|
||||
@Mapping(target = "password", ignore = true)
|
||||
public abstract UserDto map(User modelObject);
|
||||
|
||||
@Inject
|
||||
private ResourceLinks resourceLinks;
|
||||
|
||||
@VisibleForTesting
|
||||
void setResourceLinks(ResourceLinks resourceLinks) {
|
||||
this.resourceLinks = resourceLinks;
|
||||
}
|
||||
|
||||
@AfterMapping
|
||||
void removePassword(@MappingTarget UserDto target) {
|
||||
target.setPassword(UserResource.DUMMY_PASSWORT);
|
||||
}
|
||||
|
||||
@AfterMapping
|
||||
void appendLinks(User user, @MappingTarget UserDto target) {
|
||||
protected void appendLinks(User user, @MappingTarget UserDto target) {
|
||||
Links.Builder linksBuilder = linkingTo().self(resourceLinks.user().self(target.getName()));
|
||||
if (UserPermissions.delete(user).isPermitted()) {
|
||||
linksBuilder.single(link("delete", resourceLinks.user().delete(target.getName())));
|
||||
@@ -41,6 +40,9 @@ public abstract class UserToUserDtoMapper extends BaseMapper<User, UserDto> {
|
||||
if (UserPermissions.modify(user).isPermitted()) {
|
||||
linksBuilder.single(link("update", resourceLinks.user().update(target.getName())));
|
||||
}
|
||||
if (userManager.isTypeDefault(user)) {
|
||||
linksBuilder.single(link("password", resourceLinks.user().passwordChange(target.getName())));
|
||||
}
|
||||
target.add(linksBuilder.build());
|
||||
}
|
||||
|
||||
|
||||
@@ -37,10 +37,8 @@ package sonia.scm.filter;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.inject.Inject;
|
||||
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.apache.shiro.subject.Subject;
|
||||
|
||||
import sonia.scm.Priority;
|
||||
import sonia.scm.SCMContext;
|
||||
import sonia.scm.config.ScmConfiguration;
|
||||
@@ -48,14 +46,15 @@ import sonia.scm.security.SecurityRequests;
|
||||
import sonia.scm.web.filter.HttpFilter;
|
||||
import sonia.scm.web.filter.SecurityHttpServletRequestWrapper;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
import static sonia.scm.api.v2.resources.ScmPathInfo.REST_API_PATH;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -63,7 +62,8 @@ import javax.servlet.http.HttpServletResponse;
|
||||
*/
|
||||
@Priority(Filters.PRIORITY_AUTHORIZATION)
|
||||
// TODO find a better way for unprotected resources
|
||||
@WebElement(value = "/api/rest/(?!v2/ui).*", regex = true)
|
||||
@WebElement(value = REST_API_PATH + "" +
|
||||
"/(?!v2/ui).*", regex = true)
|
||||
public class SecurityFilter extends HttpFilter
|
||||
{
|
||||
|
||||
@@ -84,7 +84,7 @@ public class SecurityFilter extends HttpFilter
|
||||
HttpServletResponse response, FilterChain chain)
|
||||
throws IOException, ServletException
|
||||
{
|
||||
if (!SecurityRequests.isAuthenticationRequest(request))
|
||||
if (!SecurityRequests.isAuthenticationRequest(request) && !SecurityRequests.isIndexRequest(request))
|
||||
{
|
||||
Subject subject = SecurityUtils.getSubject();
|
||||
if (hasPermission(subject))
|
||||
|
||||
@@ -31,18 +31,11 @@
|
||||
|
||||
package sonia.scm.plugin;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
@@ -55,47 +48,27 @@ import java.nio.file.Path;
|
||||
public class PathWebResourceLoader implements WebResourceLoader
|
||||
{
|
||||
|
||||
/** Field description */
|
||||
private static final String DEFAULT_SEPARATOR = "/";
|
||||
private static final String SEPARATOR = "/";
|
||||
|
||||
/**
|
||||
* the logger for PathWebResourceLoader
|
||||
*/
|
||||
private static final Logger logger =
|
||||
private static final Logger LOG =
|
||||
LoggerFactory.getLogger(PathWebResourceLoader.class);
|
||||
|
||||
//~--- constructors ---------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Constructs ...
|
||||
*
|
||||
*
|
||||
* @param directory
|
||||
*/
|
||||
public PathWebResourceLoader(Path directory)
|
||||
{
|
||||
this.directory = directory;
|
||||
}
|
||||
|
||||
//~--- get methods ----------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param path
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public URL getResource(String path)
|
||||
{
|
||||
public URL getResource(String path) {
|
||||
URL resource = null;
|
||||
Path file = directory.resolve(filePath(path));
|
||||
|
||||
if (Files.exists(file) && ! Files.isDirectory(file))
|
||||
{
|
||||
logger.trace("found path {} at {}", path, file);
|
||||
LOG.trace("found path {} at {}", path, file);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -103,56 +76,20 @@ public class PathWebResourceLoader implements WebResourceLoader
|
||||
}
|
||||
catch (MalformedURLException ex)
|
||||
{
|
||||
logger.error("could not transform path to url", ex);
|
||||
LOG.error("could not transform path to url", ex);
|
||||
}
|
||||
} else {
|
||||
LOG.trace("could not find file {}", file);
|
||||
}
|
||||
|
||||
return resource;
|
||||
}
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param path
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private String filePath(String path)
|
||||
{
|
||||
|
||||
// TODO handle illegal path parts, such as ..
|
||||
String filePath = filePath(DEFAULT_SEPARATOR, path);
|
||||
|
||||
if (!DEFAULT_SEPARATOR.equals(File.separator))
|
||||
{
|
||||
filePath = filePath(File.separator, path);
|
||||
private String filePath(String path) {
|
||||
if (path.startsWith(SEPARATOR)) {
|
||||
return path.substring(1);
|
||||
}
|
||||
|
||||
return filePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param separator
|
||||
* @param path
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private String filePath(String separator, String path)
|
||||
{
|
||||
String filePath = path;
|
||||
|
||||
if (filePath.startsWith(separator))
|
||||
{
|
||||
filePath = filePath.substring(separator.length());
|
||||
}
|
||||
|
||||
return filePath;
|
||||
return path;
|
||||
}
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
|
||||
@@ -31,10 +31,7 @@
|
||||
|
||||
package sonia.scm.repository;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import com.github.sdorra.ssp.PermissionActionCheck;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||
import com.google.inject.Inject;
|
||||
@@ -43,7 +40,6 @@ import org.apache.shiro.concurrent.SubjectAwareExecutorService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.AlreadyExistsException;
|
||||
import sonia.scm.ArgumentIsInvalidException;
|
||||
import sonia.scm.ConfigurationException;
|
||||
import sonia.scm.HandlerEventType;
|
||||
import sonia.scm.ManagerDaoAdapter;
|
||||
@@ -54,11 +50,9 @@ import sonia.scm.config.ScmConfiguration;
|
||||
import sonia.scm.security.KeyGenerator;
|
||||
import sonia.scm.util.AssertUtil;
|
||||
import sonia.scm.util.CollectionAppender;
|
||||
import sonia.scm.util.HttpUtil;
|
||||
import sonia.scm.util.IOUtil;
|
||||
import sonia.scm.util.Util;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
@@ -71,8 +65,6 @@ import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Default implementation of {@link RepositoryManager}.
|
||||
*
|
||||
@@ -90,7 +82,6 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager {
|
||||
private final KeyGenerator keyGenerator;
|
||||
private final RepositoryDAO repositoryDAO;
|
||||
private final Set<Type> types;
|
||||
private RepositoryMatcher repositoryMatcher;
|
||||
private NamespaceStrategy namespaceStrategy;
|
||||
private final ManagerDaoAdapter<Repository> managerDaoAdapter;
|
||||
|
||||
@@ -99,12 +90,10 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager {
|
||||
public DefaultRepositoryManager(ScmConfiguration configuration,
|
||||
SCMContextProvider contextProvider, KeyGenerator keyGenerator,
|
||||
RepositoryDAO repositoryDAO, Set<RepositoryHandler> handlerSet,
|
||||
RepositoryMatcher repositoryMatcher,
|
||||
NamespaceStrategy namespaceStrategy) {
|
||||
this.configuration = configuration;
|
||||
this.keyGenerator = keyGenerator;
|
||||
this.repositoryDAO = repositoryDAO;
|
||||
this.repositoryMatcher = repositoryMatcher;
|
||||
this.namespaceStrategy = namespaceStrategy;
|
||||
|
||||
ThreadFactory factory = new ThreadFactoryBuilder()
|
||||
@@ -317,71 +306,6 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager {
|
||||
return validTypes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Repository getFromRequest(HttpServletRequest request) {
|
||||
AssertUtil.assertIsNotNull(request);
|
||||
|
||||
return getFromUri(HttpUtil.getStrippedURI(request));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Repository getFromUri(String uri) {
|
||||
AssertUtil.assertIsNotEmpty(uri);
|
||||
|
||||
if (uri.startsWith(HttpUtil.SEPARATOR_PATH)) {
|
||||
uri = uri.substring(1);
|
||||
}
|
||||
|
||||
int typeSeparator = uri.indexOf(HttpUtil.SEPARATOR_PATH);
|
||||
Repository repository = null;
|
||||
|
||||
if (typeSeparator > 0) {
|
||||
String type = uri.substring(0, typeSeparator);
|
||||
|
||||
uri = uri.substring(typeSeparator + 1);
|
||||
repository = getFromTypeAndUri(type, uri);
|
||||
}
|
||||
|
||||
return repository;
|
||||
}
|
||||
|
||||
private Repository getFromTypeAndUri(String type, String uri) {
|
||||
if (Strings.isNullOrEmpty(type)) {
|
||||
throw new ArgumentIsInvalidException("argument type is required");
|
||||
}
|
||||
|
||||
if (Strings.isNullOrEmpty(uri)) {
|
||||
throw new ArgumentIsInvalidException("argument uri is required");
|
||||
}
|
||||
|
||||
// remove ;jsessionid, jetty bug?
|
||||
uri = HttpUtil.removeMatrixParameter(uri);
|
||||
|
||||
Repository repository = null;
|
||||
|
||||
if (handlerMap.containsKey(type)) {
|
||||
Collection<Repository> repositories = repositoryDAO.getAll();
|
||||
|
||||
PermissionActionCheck<Repository> check = RepositoryPermissions.read();
|
||||
|
||||
for (Repository r : repositories) {
|
||||
if (repositoryMatcher.matches(r, type, uri)) {
|
||||
check.check(r);
|
||||
repository = r.clone();
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((repository == null) && logger.isDebugEnabled()) {
|
||||
logger.debug("could not find repository with type {} and uri {}", type,
|
||||
uri);
|
||||
}
|
||||
|
||||
return repository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RepositoryHandler getHandler(String type) {
|
||||
return handlerMap.get(type);
|
||||
|
||||
@@ -33,86 +33,32 @@
|
||||
|
||||
package sonia.scm.repository;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Provider;
|
||||
import com.google.inject.servlet.RequestScoped;
|
||||
|
||||
import sonia.scm.security.ScmSecurityException;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
@RequestScoped
|
||||
public class DefaultRepositoryProvider implements RepositoryProvider
|
||||
{
|
||||
public class DefaultRepositoryProvider implements RepositoryProvider {
|
||||
|
||||
/** Field description */
|
||||
public static final String ATTRIBUTE_NAME = "scm.request.repository";
|
||||
|
||||
//~--- constructors ---------------------------------------------------------
|
||||
private final Provider<HttpServletRequest> requestProvider;
|
||||
|
||||
/**
|
||||
* Constructs ...
|
||||
*
|
||||
*
|
||||
* @param requestProvider
|
||||
* @param manager
|
||||
*/
|
||||
@Inject
|
||||
public DefaultRepositoryProvider(
|
||||
Provider<HttpServletRequest> requestProvider,
|
||||
RepositoryManager manager)
|
||||
{
|
||||
public DefaultRepositoryProvider(Provider<HttpServletRequest> requestProvider) {
|
||||
this.requestProvider = requestProvider;
|
||||
this.manager = manager;
|
||||
}
|
||||
|
||||
//~--- get methods ----------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @return
|
||||
*
|
||||
* @throws ScmSecurityException
|
||||
*/
|
||||
@Override
|
||||
public Repository get() throws ScmSecurityException
|
||||
{
|
||||
Repository repository = null;
|
||||
public Repository get() {
|
||||
HttpServletRequest request = requestProvider.get();
|
||||
|
||||
if (request != null)
|
||||
{
|
||||
repository = (Repository) request.getAttribute(ATTRIBUTE_NAME);
|
||||
|
||||
if (repository == null)
|
||||
{
|
||||
repository = manager.getFromRequest(request);
|
||||
|
||||
if (repository != null)
|
||||
{
|
||||
request.setAttribute(ATTRIBUTE_NAME, repository);
|
||||
}
|
||||
}
|
||||
if (request != null) {
|
||||
return (Repository) request.getAttribute(ATTRIBUTE_NAME);
|
||||
}
|
||||
|
||||
return repository;
|
||||
throw new IllegalStateException("request not found");
|
||||
}
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
|
||||
/** Field description */
|
||||
private final RepositoryManager manager;
|
||||
|
||||
/** Field description */
|
||||
private final Provider<HttpServletRequest> requestProvider;
|
||||
}
|
||||
|
||||
@@ -61,8 +61,7 @@ public final class HealthChecker {
|
||||
Repository repository = repositoryManager.get(id);
|
||||
|
||||
if (repository == null) {
|
||||
throw new RepositoryNotFoundException(
|
||||
"could not find repository with id ".concat(id));
|
||||
throw new RepositoryNotFoundException(id);
|
||||
}
|
||||
|
||||
doCheck(repository);
|
||||
|
||||
@@ -56,6 +56,7 @@ import sonia.scm.plugin.Extension;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryDAO;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.user.UserPermissions;
|
||||
import sonia.scm.util.Util;
|
||||
|
||||
import java.util.List;
|
||||
@@ -74,7 +75,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
|
||||
|
||||
// TODO move to util class
|
||||
private static final String SEPARATOR = System.getProperty("line.separator", "\n");
|
||||
|
||||
|
||||
/** Field description */
|
||||
private static final String ADMIN_PERMISSION = "*";
|
||||
|
||||
@@ -88,7 +89,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
|
||||
LoggerFactory.getLogger(DefaultAuthorizationCollector.class);
|
||||
|
||||
//~--- constructors ---------------------------------------------------------
|
||||
|
||||
|
||||
/**
|
||||
* Constructs ...
|
||||
*
|
||||
@@ -209,7 +210,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
|
||||
String perm = permission.getType().getPermissionPrefix().concat(repository.getId());
|
||||
if (logger.isTraceEnabled())
|
||||
{
|
||||
logger.trace("add repository permission {} for user {} at repository {}",
|
||||
logger.trace("add repository permission {} for user {} at repository {}",
|
||||
perm, user.getName(), repository.getName());
|
||||
}
|
||||
|
||||
@@ -254,6 +255,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
|
||||
|
||||
collectGlobalPermissions(builder, user, groups);
|
||||
collectRepositoryPermissions(builder, user, groups);
|
||||
builder.add(canReadOwnUser(user));
|
||||
permissions = builder.build();
|
||||
}
|
||||
|
||||
@@ -262,6 +264,10 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
|
||||
return info;
|
||||
}
|
||||
|
||||
private String canReadOwnUser(User user) {
|
||||
return UserPermissions.read(user.getName()).asShiroString();
|
||||
}
|
||||
|
||||
//~--- get methods ----------------------------------------------------------
|
||||
|
||||
private boolean isUserPermitted(User user, GroupNames groups,
|
||||
@@ -272,7 +278,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
|
||||
|| ((!perm.isGroupPermission()) && user.getName().equals(perm.getName()));
|
||||
//J+
|
||||
}
|
||||
|
||||
|
||||
@Subscribe
|
||||
public void invalidateCache(AuthorizationChangedEvent event) {
|
||||
if (event.isEveryUserAffected()) {
|
||||
@@ -281,12 +287,12 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
|
||||
invalidateCache();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void invalidateUserCache(final String username) {
|
||||
logger.info("invalidate cache for user {}, because of a received authorization event", username);
|
||||
cache.removeAll((CacheKey item) -> username.equalsIgnoreCase(item.username));
|
||||
}
|
||||
|
||||
|
||||
private void invalidateCache() {
|
||||
logger.info("invalidate cache, because of a received authorization event");
|
||||
cache.clear();
|
||||
|
||||
@@ -3,12 +3,15 @@ package sonia.scm.security;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static sonia.scm.api.v2.resources.ScmPathInfo.REST_API_PATH;
|
||||
|
||||
/**
|
||||
* Created by masuewer on 04.07.18.
|
||||
*/
|
||||
public final class SecurityRequests {
|
||||
|
||||
private static final Pattern URI_LOGIN_PATTERN = Pattern.compile("/api/rest(?:/v2)?/auth/access_token");
|
||||
private static final Pattern URI_LOGIN_PATTERN = Pattern.compile(REST_API_PATH + "(?:/v2)?/auth/access_token");
|
||||
private static final Pattern URI_INDEX_PATTERN = Pattern.compile(REST_API_PATH + "/v2/?");
|
||||
|
||||
private SecurityRequests() {}
|
||||
|
||||
@@ -21,4 +24,13 @@ public final class SecurityRequests {
|
||||
return URI_LOGIN_PATTERN.matcher(uri).matches();
|
||||
}
|
||||
|
||||
public static boolean isIndexRequest(HttpServletRequest request) {
|
||||
String uri = request.getRequestURI().substring(request.getContextPath().length());
|
||||
return isIndexRequest(uri);
|
||||
}
|
||||
|
||||
public static boolean isIndexRequest(String uri) {
|
||||
return URI_INDEX_PATTERN.matcher(uri).matches();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2010, Sebastian Sdorra
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
* 3. Neither the name of SCM-Manager; nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from this
|
||||
* software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* http://bitbucket.org/sdorra/scm-manager
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
|
||||
package sonia.scm.util;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import sonia.scm.DecoratorFactory;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
public final class Decorators
|
||||
{
|
||||
|
||||
/**
|
||||
* the logger for Decorators
|
||||
*/
|
||||
private static final Logger logger =
|
||||
LoggerFactory.getLogger(Decorators.class);
|
||||
|
||||
//~--- constructors ---------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Constructs ...
|
||||
*
|
||||
*/
|
||||
private Decorators() {}
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param object
|
||||
* @param decoratorFactories
|
||||
* @param <T>
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static <T> T decorate(T object,
|
||||
Iterable<? extends DecoratorFactory<T>> decoratorFactories)
|
||||
{
|
||||
if (decoratorFactories != null)
|
||||
{
|
||||
for (DecoratorFactory<T> decoratorFactory : decoratorFactories)
|
||||
{
|
||||
if (logger.isDebugEnabled())
|
||||
{
|
||||
logger.debug("decorate {} with {}", object.getClass(),
|
||||
decoratorFactory.getClass());
|
||||
}
|
||||
|
||||
object = decoratorFactory.createDecorator(object);
|
||||
}
|
||||
}
|
||||
else if (logger.isDebugEnabled())
|
||||
{
|
||||
logger.debug("no decorators found for {}", object.getClass());
|
||||
}
|
||||
|
||||
return object;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package sonia.scm.web.protocol;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.http.HttpStatus;
|
||||
import sonia.scm.PushStateDispatcher;
|
||||
import sonia.scm.filter.WebElement;
|
||||
import sonia.scm.repository.DefaultRepositoryProvider;
|
||||
import sonia.scm.repository.NamespaceAndName;
|
||||
import sonia.scm.repository.RepositoryNotFoundException;
|
||||
import sonia.scm.repository.api.RepositoryService;
|
||||
import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||
import sonia.scm.repository.spi.HttpScmProtocol;
|
||||
import sonia.scm.web.UserAgent;
|
||||
import sonia.scm.web.UserAgentParser;
|
||||
|
||||
import javax.inject.Provider;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
|
||||
@Singleton
|
||||
@WebElement(value = HttpProtocolServlet.PATTERN)
|
||||
@Slf4j
|
||||
public class HttpProtocolServlet extends HttpServlet {
|
||||
|
||||
public static final String PATH = "/repo";
|
||||
public static final String PATTERN = PATH + "/*";
|
||||
|
||||
private final RepositoryServiceFactory serviceFactory;
|
||||
|
||||
private final Provider<HttpServletRequest> requestProvider;
|
||||
|
||||
private final PushStateDispatcher dispatcher;
|
||||
private final UserAgentParser userAgentParser;
|
||||
|
||||
|
||||
@Inject
|
||||
public HttpProtocolServlet(RepositoryServiceFactory serviceFactory, Provider<HttpServletRequest> requestProvider, PushStateDispatcher dispatcher, UserAgentParser userAgentParser) {
|
||||
this.serviceFactory = serviceFactory;
|
||||
this.requestProvider = requestProvider;
|
||||
this.dispatcher = dispatcher;
|
||||
this.userAgentParser = userAgentParser;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
|
||||
UserAgent userAgent = userAgentParser.parse(request);
|
||||
if (userAgent.isBrowser()) {
|
||||
log.trace("dispatch browser request for user agent {}", userAgent);
|
||||
dispatcher.dispatch(request, response, request.getRequestURI());
|
||||
} else {
|
||||
|
||||
String pathInfo = request.getPathInfo();
|
||||
Optional<NamespaceAndName> namespaceAndName = NamespaceAndNameFromPathExtractor.fromUri(pathInfo);
|
||||
if (namespaceAndName.isPresent()) {
|
||||
service(request, response, namespaceAndName.get());
|
||||
} else {
|
||||
log.debug("namespace and name not found in request path {}", pathInfo);
|
||||
response.setStatus(HttpStatus.SC_BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void service(HttpServletRequest req, HttpServletResponse resp, NamespaceAndName namespaceAndName) throws IOException, ServletException {
|
||||
try (RepositoryService repositoryService = serviceFactory.create(namespaceAndName)) {
|
||||
requestProvider.get().setAttribute(DefaultRepositoryProvider.ATTRIBUTE_NAME, repositoryService.getRepository());
|
||||
HttpScmProtocol protocol = repositoryService.getProtocol(HttpScmProtocol.class);
|
||||
protocol.serve(req, resp, getServletConfig());
|
||||
} catch (RepositoryNotFoundException e) {
|
||||
log.debug("Repository not found for namespace and name {}", namespaceAndName, e);
|
||||
resp.setStatus(HttpStatus.SC_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package sonia.scm.web.protocol;
|
||||
|
||||
import sonia.scm.repository.NamespaceAndName;
|
||||
import sonia.scm.util.HttpUtil;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import static java.util.Optional.empty;
|
||||
import static java.util.Optional.of;
|
||||
|
||||
final class NamespaceAndNameFromPathExtractor {
|
||||
|
||||
private NamespaceAndNameFromPathExtractor() {}
|
||||
|
||||
static Optional<NamespaceAndName> fromUri(String uri) {
|
||||
if (uri.startsWith(HttpUtil.SEPARATOR_PATH)) {
|
||||
uri = uri.substring(1);
|
||||
}
|
||||
|
||||
int endOfNamespace = uri.indexOf(HttpUtil.SEPARATOR_PATH);
|
||||
if (endOfNamespace < 1) {
|
||||
return empty();
|
||||
}
|
||||
|
||||
String namespace = uri.substring(0, endOfNamespace);
|
||||
int nameSeparatorIndex = uri.indexOf(HttpUtil.SEPARATOR_PATH, endOfNamespace + 1);
|
||||
int nameIndex = nameSeparatorIndex > 0 ? nameSeparatorIndex : uri.length();
|
||||
if (nameIndex == endOfNamespace + 1) {
|
||||
return empty();
|
||||
}
|
||||
|
||||
String name = uri.substring(endOfNamespace + 1, nameIndex);
|
||||
|
||||
int nameDotIndex = name.indexOf('.');
|
||||
if (nameDotIndex >= 0) {
|
||||
return of(new NamespaceAndName(namespace, name.substring(0, nameDotIndex)));
|
||||
} else {
|
||||
return of(new NamespaceAndName(namespace, name));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -99,7 +99,7 @@ public class ApiAuthenticationFilter extends AuthenticationFilter
|
||||
throws IOException, ServletException
|
||||
{
|
||||
// skip filter on login resource
|
||||
if (SecurityRequests.isAuthenticationRequest(request))
|
||||
if (SecurityRequests.isAuthenticationRequest(request) )
|
||||
{
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user