This commit is contained in:
Mohamed Karray
2018-11-15 11:28:33 +01:00
381 changed files with 12331 additions and 11177 deletions

View File

@@ -30,19 +30,19 @@ public class ManagerDaoAdapter<T extends ModelObject> {
afterUpdate.handle(notModified);
} else {
throw new NotFoundException();
throw new NotFoundException(object.getClass(), object.getId());
}
}
public T create(T newObject, Supplier<PermissionCheck> permissionCheck, AroundHandler<T> beforeCreate, AroundHandler<T> afterCreate) throws AlreadyExistsException {
public T create(T newObject, Supplier<PermissionCheck> permissionCheck, AroundHandler<T> beforeCreate, AroundHandler<T> afterCreate) {
return create(newObject, permissionCheck, beforeCreate, afterCreate, dao::contains);
}
public T create(T newObject, Supplier<PermissionCheck> permissionCheck, AroundHandler<T> beforeCreate, AroundHandler<T> afterCreate, Predicate<T> existsCheck) throws AlreadyExistsException {
public T create(T newObject, Supplier<PermissionCheck> permissionCheck, AroundHandler<T> beforeCreate, AroundHandler<T> afterCreate, Predicate<T> existsCheck) {
permissionCheck.get().check();
AssertUtil.assertIsValid(newObject);
if (existsCheck.test(newObject)) {
throw new AlreadyExistsException();
throw new AlreadyExistsException(newObject);
}
newObject.setCreationDate(System.currentTimeMillis());
beforeCreate.handle(newObject);
@@ -58,7 +58,7 @@ public class ManagerDaoAdapter<T extends ModelObject> {
dao.delete(toDelete);
afterDelete.handle(toDelete);
} else {
throw new NotFoundException();
throw new NotFoundException(toDelete.getClass(), toDelete.getId());
}
}

View File

@@ -0,0 +1,20 @@
package sonia.scm;
import com.google.common.collect.ImmutableMap;
import com.google.inject.servlet.ServletModule;
import org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher;
import org.jboss.resteasy.plugins.server.servlet.ResteasyContextParameters;
import javax.inject.Singleton;
import java.util.Map;
public class ResteasyModule extends ServletModule {
@Override
protected void configureServlets() {
bind(HttpServletDispatcher.class).in(Singleton.class);
Map<String, String> initParams = ImmutableMap.of(ResteasyContextParameters.RESTEASY_SERVLET_MAPPING_PREFIX, "/api");
serve("/api/*").with(HttpServletDispatcher.class, initParams);
}
}

View File

@@ -126,6 +126,7 @@ public class ScmContextListener extends GuiceResteasyBootstrapServletContextList
ClassOverrides overrides = ClassOverrides.findOverrides(pluginLoader.getUberClassLoader());
List<Module> moduleList = Lists.newArrayList();
moduleList.add(new ResteasyModule());
moduleList.add(new ScmInitializerModule());
moduleList.add(new ScmEventBusModule());
moduleList.add(new EagerSingletonModule());

View File

@@ -57,5 +57,9 @@ public class TemplatingPushStateDispatcher implements PushStateDispatcher {
return request.getContextPath();
}
public String getLiveReloadURL() {
return System.getProperty("livereload.url");
}
}
}

View File

@@ -0,0 +1,43 @@
package sonia.scm.api;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import sonia.scm.api.v2.resources.ErrorDto;
import sonia.scm.api.v2.resources.ExceptionWithContextToErrorDtoMapper;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
import java.util.Collections;
@Provider
public class FallbackExceptionMapper implements ExceptionMapper<Exception> {
private static final Logger logger = LoggerFactory.getLogger(FallbackExceptionMapper.class);
private static final String ERROR_CODE = "CmR8GCJb31";
private final ExceptionWithContextToErrorDtoMapper mapper;
@Inject
public FallbackExceptionMapper(ExceptionWithContextToErrorDtoMapper mapper) {
this.mapper = mapper;
}
@Override
public Response toResponse(Exception exception) {
logger.debug("map {} to status code 500", exception);
ErrorDto errorDto = new ErrorDto();
errorDto.setMessage("internal server error");
errorDto.setContext(Collections.emptyList());
errorDto.setErrorCode(ERROR_CODE);
errorDto.setTransactionId(MDC.get("transaction_id"));
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(errorDto)
.type(VndMediaType.ERROR_TYPE)
.build();
}
}

View File

@@ -1,18 +1,16 @@
package sonia.scm.api.rest;
import sonia.scm.AlreadyExistsException;
import sonia.scm.api.v2.resources.ExceptionWithContextToErrorDtoMapper;
import javax.ws.rs.core.Response;
import javax.inject.Inject;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
@Provider
public class AlreadyExistsExceptionMapper implements ExceptionMapper<AlreadyExistsException> {
@Override
public Response toResponse(AlreadyExistsException exception) {
return Response.status(Status.CONFLICT)
.entity(exception.getMessage())
.build();
public class AlreadyExistsExceptionMapper extends ContextualExceptionMapper<AlreadyExistsException> {
@Inject
public AlreadyExistsExceptionMapper(ExceptionWithContextToErrorDtoMapper mapper) {
super(AlreadyExistsException.class, Status.CONFLICT, mapper);
}
}

View File

@@ -0,0 +1,13 @@
package sonia.scm.api.rest;
import org.apache.shiro.authc.AuthenticationException;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.Provider;
@Provider
public class AuthenticationExceptionMapper extends StatusExceptionMapper<AuthenticationException> {
public AuthenticationExceptionMapper() {
super(AuthenticationException.class, Response.Status.UNAUTHORIZED);
}
}

View File

@@ -1,15 +1,16 @@
package sonia.scm.api.rest;
import sonia.scm.ConcurrentModificationException;
import sonia.scm.api.v2.resources.ExceptionWithContextToErrorDtoMapper;
import javax.inject.Inject;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
@Provider
public class ConcurrentModificationExceptionMapper implements ExceptionMapper<ConcurrentModificationException> {
@Override
public Response toResponse(ConcurrentModificationException exception) {
return Response.status(Response.Status.CONFLICT).build();
public class ConcurrentModificationExceptionMapper extends ContextualExceptionMapper<ConcurrentModificationException> {
@Inject
public ConcurrentModificationExceptionMapper(ExceptionWithContextToErrorDtoMapper mapper) {
super(ConcurrentModificationException.class, Response.Status.CONFLICT, mapper);
}
}

View File

@@ -0,0 +1,39 @@
package sonia.scm.api.rest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.ExceptionWithContext;
import sonia.scm.api.v2.resources.ExceptionWithContextToErrorDtoMapper;
import sonia.scm.web.VndMediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
public class ContextualExceptionMapper<E extends ExceptionWithContext> implements ExceptionMapper<E> {
private static final Logger logger = LoggerFactory.getLogger(ContextualExceptionMapper.class);
private final ExceptionWithContextToErrorDtoMapper mapper;
private final Response.Status status;
private final Class<E> type;
public ContextualExceptionMapper(Class<E> type, Response.Status status, ExceptionWithContextToErrorDtoMapper mapper) {
this.mapper = mapper;
this.type = type;
this.status = status;
}
@Override
public Response toResponse(E exception) {
if (logger.isTraceEnabled()) {
logger.trace("map {} to status code {}", type.getSimpleName(), status.getStatusCode(), exception);
} else {
logger.debug("map {} to status code {} with message '{}'", type.getSimpleName(), status.getStatusCode(), exception.getMessage());
}
return Response.status(status)
.entity(mapper.map(exception))
.type(VndMediaType.ERROR_TYPE)
.build();
}
}

View File

@@ -1,68 +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.api.rest;
//~--- JDK imports ------------------------------------------------------------
import lombok.extern.slf4j.Slf4j;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
/**
*
* @author Sebastian Sdorra
* @since 1.36
*/
@Provider
@Slf4j
public class IllegalArgumentExceptionMapper
implements ExceptionMapper<IllegalArgumentException>
{
/**
* Method description
*
*
* @param exception
*
* @return
*/
@Override
public Response toResponse(IllegalArgumentException exception)
{
log.info("caught IllegalArgumentException -- mapping to bad request", exception);
return Response.status(Status.BAD_REQUEST).entity(exception.getMessage()).build();
}
}

View File

@@ -0,0 +1,13 @@
package sonia.scm.api.rest;
import javax.ws.rs.NotAuthorizedException;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.Provider;
@Provider
public class NotAuthorizedExceptionMapper extends StatusExceptionMapper<NotAuthorizedException> {
public NotAuthorizedExceptionMapper()
{
super(NotAuthorizedException.class, Response.Status.UNAUTHORIZED);
}
}

View File

@@ -2,8 +2,6 @@ package sonia.scm.api.rest.resources;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.repository.PathNotFoundException;
import sonia.scm.repository.RevisionNotFoundException;
import sonia.scm.repository.api.CatCommandBuilder;
import sonia.scm.repository.api.RepositoryService;
import sonia.scm.util.IOUtil;
@@ -34,18 +32,6 @@ public class BrowserStreamingOutput implements StreamingOutput {
public void write(OutputStream output) throws IOException {
try {
builder.retriveContent(output, path);
} catch (PathNotFoundException ex) {
if (logger.isWarnEnabled()) {
logger.warn("could not find path {}", ex.getPath());
}
throw new WebApplicationException(Response.Status.NOT_FOUND);
} catch (RevisionNotFoundException ex) {
if (logger.isWarnEnabled()) {
logger.warn("could not find revision {}", ex.getRevision());
}
throw new WebApplicationException(Response.Status.NOT_FOUND);
} finally {
IOUtil.close(repositoryService);
}

View File

@@ -44,8 +44,6 @@ import org.apache.shiro.authc.credential.PasswordService;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.ConcurrentModificationException;
import sonia.scm.NotFoundException;
import sonia.scm.api.rest.RestActionResult;
import sonia.scm.security.Role;
import sonia.scm.security.ScmSecurityException;

View File

@@ -1,148 +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.api.rest.resources;
//~--- non-JDK imports --------------------------------------------------------
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.security.Role;
import sonia.scm.security.ScmSecurityException;
import sonia.scm.util.ScmConfigurationUtil;
//~--- JDK imports ------------------------------------------------------------
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
/**
*
* @author Sebastian Sdorra
*/
@Singleton
@Path("config")
public class ConfigurationResource
{
/**
* Constructs ...
*
*
* @param configuration
* @param securityContextProvider
*/
@Inject
public ConfigurationResource(ScmConfiguration configuration)
{
this.configuration = configuration;
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @return
*/
@GET
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public Response getConfiguration()
{
Response response = null;
if (SecurityUtils.getSubject().hasRole(Role.ADMIN))
{
response = Response.ok(configuration).build();
}
else
{
response = Response.status(Response.Status.FORBIDDEN).build();
}
return response;
}
//~--- set methods ----------------------------------------------------------
/**
* Method description
*
*
* @param uriInfo
* @param newConfig
*
* @return
*/
@POST
@Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public Response setConfig(@Context UriInfo uriInfo,
ScmConfiguration newConfig)
{
// TODO replace by checkRole
Subject subject = SecurityUtils.getSubject();
if (!subject.hasRole(Role.ADMIN))
{
throw new ScmSecurityException("admin privileges required");
}
configuration.load(newConfig);
synchronized (ScmConfiguration.class)
{
ScmConfigurationUtil.getInstance().store(configuration);
}
return Response.created(uriInfo.getRequestUri()).build();
}
//~--- fields ---------------------------------------------------------------
/** Field description */
public ScmConfiguration configuration;
}

View File

@@ -37,7 +37,6 @@ package sonia.scm.api.rest.resources;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.repository.RevisionNotFoundException;
import sonia.scm.repository.api.DiffCommandBuilder;
import sonia.scm.repository.api.RepositoryService;
import sonia.scm.util.IOUtil;
@@ -93,24 +92,8 @@ public class DiffStreamingOutput implements StreamingOutput
public void write(OutputStream output) throws IOException {
try
{
builder.retriveContent(output);
builder.retrieveContent(output);
}
catch (RevisionNotFoundException ex)
{
if (logger.isWarnEnabled())
{
logger.warn("could not find revision {}", ex.getRevision());
}
throw new WebApplicationException(Response.Status.NOT_FOUND);
}
// catch (RepositoryException ex)
// {
// logger.error("could not write content to page", ex);
//
// throw new WebApplicationException(ex,
// Response.Status.INTERNAL_SERVER_ERROR);
// }
finally
{
IOUtil.close(repositoryService);

View File

@@ -1,248 +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.api.rest.resources;
//~--- non-JDK imports --------------------------------------------------------
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
import com.webcohesion.enunciate.metadata.rs.ResponseHeader;
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import com.webcohesion.enunciate.metadata.rs.TypeHint;
import org.apache.shiro.SecurityUtils;
import sonia.scm.group.Group;
import sonia.scm.group.GroupManager;
import sonia.scm.security.Role;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
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.Context;
import javax.ws.rs.core.GenericEntity;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Request;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import java.util.Collection;
//~--- JDK imports ------------------------------------------------------------
/**
* RESTful Web Service Resource to manage groups and their members.
*
* @author Sebastian Sdorra
*/
@Path("groups")
@Singleton
public class GroupResource extends AbstractManagerResource<Group> {
/** Field description */
public static final String PATH_PART = "groups";
//~--- constructors ---------------------------------------------------------
@Inject
public GroupResource(GroupManager groupManager)
{
super(groupManager, Group.class);
}
//~--- methods --------------------------------------------------------------
/**
* Creates a new group. <strong>Note:</strong> This method requires admin privileges.
*
* @param uriInfo current uri informations
* @param group the group to be created
*
* @return
*/
@POST
@StatusCodes({
@ResponseCode(code = 201, condition = "create success", additionalHeaders = {
@ResponseHeader(name = "Location", description = "uri to the created group")
}),
@ResponseCode(code = 403, condition = "forbidden, the current user has no admin privileges"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(TypeHint.NO_CONTENT.class)
@Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
@Override
public Response create(@Context UriInfo uriInfo, Group group)
{
return super.create(uriInfo, group);
}
/**
* Deletes a group. <strong>Note:</strong> This method requires admin privileges.
*
* @param name the name of the group to delete.
*
* @return
*/
@DELETE
@Path("{id}")
@StatusCodes({
@ResponseCode(code = 204, condition = "delete success"),
@ResponseCode(code = 403, condition = "forbidden, the current user has no admin privileges"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(TypeHint.NO_CONTENT.class)
@Override
public Response delete(@PathParam("id") String name)
{
return super.delete(name);
}
/**
* Modifies the given group. <strong>Note:</strong> This method requires admin privileges.
*
* @param name name of the group to be modified
* @param group group object to modify
*
* @return
*/
@PUT
@Path("{id}")
@StatusCodes({
@ResponseCode(code = 204, condition = "update success"),
@ResponseCode(code = 403, condition = "forbidden, the current user has no admin privileges"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(TypeHint.NO_CONTENT.class)
@Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
@Override
public Response update(@PathParam("id") String name, Group group)
{
return super.update(name, group);
}
//~--- get methods ----------------------------------------------------------
/**
* Fetches a group by its name or id. <strong>Note:</strong> This method requires admin privileges.
*
* @param request the current request
* @param id the id/name of the group
*
* @return the {@link Group} with the specified id
*/
@GET
@Path("{id}")
@TypeHint(Group.class)
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 403, condition = "forbidden, the current user has no admin privileges"),
@ResponseCode(code = 404, condition = "not found, no group with the specified id/name available"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
@Override
public Response get(@Context Request request, @PathParam("id") String id)
{
Response response = null;
if (SecurityUtils.getSubject().hasRole(Role.ADMIN))
{
response = super.get(request, id);
}
else
{
response = Response.status(Response.Status.FORBIDDEN).build();
}
return response;
}
/**
* Returns all groups. <strong>Note:</strong> This method requires admin privileges.
*
* @param request the current request
* @param start the start value for paging
* @param limit the limit value for paging
* @param sortby sort parameter
* @param desc sort direction desc or aesc
*
* @return
*/
@GET
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
@TypeHint(Group[].class)
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 403, condition = "forbidden, the current user has no admin privileges"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Override
public Response getAll(@Context Request request, @DefaultValue("0")
@QueryParam("start") int start, @DefaultValue("-1")
@QueryParam("limit") int limit, @QueryParam("sortby") String sortby,
@DefaultValue("false")
@QueryParam("desc") boolean desc)
{
return super.getAll(request, start, limit, sortby, desc);
}
//~--- methods --------------------------------------------------------------
@Override
protected GenericEntity<Collection<Group>> createGenericEntity(
Collection<Group> items)
{
return new GenericEntity<Collection<Group>>(items) {}
;
}
//~--- get methods ----------------------------------------------------------
@Override
protected String getId(Group group)
{
return group.getName();
}
@Override
protected String getPathPart()
{
return PATH_PART;
}
}

View File

@@ -1,362 +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.api.rest.resources;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.collect.Lists;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.api.rest.RestActionResult;
import sonia.scm.api.rest.RestActionUploadResult;
import sonia.scm.plugin.OverviewPluginPredicate;
import sonia.scm.plugin.PluginConditionFailedException;
import sonia.scm.plugin.PluginInformation;
import sonia.scm.plugin.PluginInformationComparator;
import sonia.scm.plugin.PluginManager;
//~--- JDK imports ------------------------------------------------------------
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import com.webcohesion.enunciate.metadata.rs.TypeHint;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import javax.ws.rs.Consumes;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
/**
* RESTful Web Service Endpoint to manage plugins.
*
* @author Sebastian Sdorra
*/
@Singleton
@Path("plugins")
public class PluginResource
{
/**
* the logger for PluginResource
*/
private static final Logger logger =
LoggerFactory.getLogger(PluginResource.class);
//~--- constructors ---------------------------------------------------------
/**
* Constructs ...
*
*
* @param pluginManager
*/
@Inject
public PluginResource(PluginManager pluginManager)
{
this.pluginManager = pluginManager;
}
//~--- methods --------------------------------------------------------------
/**
* Installs a plugin from a package.
*
* @param uploadedInputStream
*
* @return
*
* @throws IOException
*/
@POST
@Path("install-package")
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 412, condition = "precondition failed"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public Response install(
/*@FormParam("package")*/ InputStream uploadedInputStream)
throws IOException
{
Response response = null;
try
{
pluginManager.installPackage(uploadedInputStream);
response = Response.ok(new RestActionUploadResult(true)).build();
}
catch (PluginConditionFailedException ex)
{
logger.warn(
"could not install plugin package, because the condition failed", ex);
response = Response.status(Status.PRECONDITION_FAILED).entity(
new RestActionResult(false)).build();
}
catch (Exception ex)
{
logger.warn("plugin installation failed", ex);
response =
Response.serverError().entity(new RestActionResult(false)).build();
}
return response;
}
/**
* Installs a plugin.
*
* @param id id of the plugin to be installed
*
* @return
*/
@POST
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(TypeHint.NO_CONTENT.class)
@Path("install/{id}")
public Response install(@PathParam("id") String id)
{
pluginManager.install(id);
// TODO should return 204 no content
return Response.ok().build();
}
/**
* Installs a plugin from a package. This method is a workaround for ExtJS
* file upload, which requires text/html as content-type.
*
* @param uploadedInputStream
* @return
*
* @throws IOException
*/
@POST
@Path("install-package.html")
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 412, condition = "precondition failed"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Produces(MediaType.TEXT_HTML)
public Response installFromUI(
/*@FormParam("package")*/ InputStream uploadedInputStream)
throws IOException
{
return install(uploadedInputStream);
}
/**
* Uninstalls a plugin.
*
* @param id id of the plugin to be uninstalled
*
* @return
*/
@POST
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Path("uninstall/{id}")
public Response uninstall(@PathParam("id") String id)
{
pluginManager.uninstall(id);
// TODO should return 204 content
// consider to do a uninstall with a delete
return Response.ok().build();
}
/**
* Updates a plugin.
*
* @param id id of the plugin to be updated
*
* @return
*/
@POST
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Path("update/{id}")
public Response update(@PathParam("id") String id)
{
pluginManager.update(id);
// TODO should return 204 content
// consider to do an update with a put
return Response.ok().build();
}
//~--- get methods ----------------------------------------------------------
/**
* Returns all plugins.
*
* @return all plugins
*/
@GET
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public Collection<PluginInformation> getAll()
{
return pluginManager.getAll();
}
/**
* Returns all available plugins.
*
* @return all available plugins
*/
@GET
@Path("available")
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public Collection<PluginInformation> getAvailable()
{
return pluginManager.getAvailable();
}
/**
* Returns all plugins which are available for update.
*
* @return all plugins which are available for update
*/
@GET
@Path("updates")
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public Collection<PluginInformation> getAvailableUpdates()
{
return pluginManager.getAvailableUpdates();
}
/**
* Returns all installed plugins.
*
* @return all installed plugins
*/
@GET
@Path("installed")
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
public Collection<PluginInformation> getInstalled()
{
return pluginManager.getInstalled();
}
/**
* Returns all plugins for the overview.
*
* @return all plugins for the overview
*/
@GET
@Path("overview")
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public Collection<PluginInformation> getOverview()
{
//J-
List<PluginInformation> plugins = Lists.newArrayList(
pluginManager.get(OverviewPluginPredicate.INSTANCE)
);
//J+
Collections.sort(plugins, PluginInformationComparator.INSTANCE);
Iterator<PluginInformation> it = plugins.iterator();
String last = null;
while (it.hasNext())
{
PluginInformation pi = it.next();
String id = pi.getId(false);
if ((last != null) && id.equals(last))
{
it.remove();
}
last = id;
}
return plugins;
}
//~--- fields ---------------------------------------------------------------
/** plugin manager */
private final PluginManager pluginManager;
}

View File

@@ -45,11 +45,11 @@ import com.webcohesion.enunciate.metadata.rs.TypeHint;
import org.apache.shiro.SecurityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.AlreadyExistsException;
import sonia.scm.NotFoundException;
import sonia.scm.NotSupportedFeatuerException;
import sonia.scm.NotSupportedFeatureException;
import sonia.scm.Type;
import sonia.scm.api.rest.RestActionUploadResult;
import sonia.scm.api.v2.resources.RepositoryResource;
import sonia.scm.repository.*;
import sonia.scm.repository.api.Command;
import sonia.scm.repository.api.RepositoryService;
@@ -394,7 +394,7 @@ public class RepositoryImportResource
response = Response.ok(result).build();
}
catch (NotSupportedFeatuerException ex)
catch (NotSupportedFeatureException ex)
{
logger
.warn(
@@ -515,13 +515,6 @@ public class RepositoryImportResource
// repository = new Repository(null, type, name);
manager.create(repository);
}
catch (AlreadyExistsException ex)
{
logger.warn("a {} repository with the name {} already exists", type,
name);
throw new WebApplicationException(Response.Status.CONFLICT);
}
catch (InternalRepositoryException ex)
{
handleGenericCreationFailure(ex, type, name);
@@ -616,7 +609,7 @@ public class RepositoryImportResource
types.add(t);
}
}
catch (NotSupportedFeatuerException ex)
catch (NotSupportedFeatureException ex)
{
if (logger.isTraceEnabled())
{
@@ -718,7 +711,7 @@ public class RepositoryImportResource
}
}
}
catch (NotSupportedFeatuerException ex)
catch (NotSupportedFeatureException ex)
{
throw new WebApplicationException(ex, Response.Status.BAD_REQUEST);
}

View File

@@ -1,319 +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.api.rest.resources;
//~--- non-JDK imports --------------------------------------------------------
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
import com.webcohesion.enunciate.metadata.rs.ResponseHeader;
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.security.Role;
import sonia.scm.user.User;
import sonia.scm.user.UserManager;
import sonia.scm.util.AssertUtil;
import sonia.scm.util.Util;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
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.Context;
import javax.ws.rs.core.GenericEntity;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Request;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import java.util.Collection;
//~--- JDK imports ------------------------------------------------------------
/**
* RESTful Web Service Resource to manage users.
*
* @author Sebastian Sdorra
*/
@Singleton
@Path("users")
public class UserResource extends AbstractManagerResource<User>
{
/** Field description */
public static final String DUMMY_PASSWORT = "__dummypassword__";
/** Field description */
public static final String PATH_PART = "users";
//~--- constructors ---------------------------------------------------------
/**
* Constructs ...
*
*
* @param userManager
* @param passwordService
*/
@Inject
public UserResource(UserManager userManager, PasswordService passwordService)
{
super(userManager, User.class);
this.passwordService = passwordService;
}
//~--- methods --------------------------------------------------------------
/**
* Creates a new user. <strong>Note:</strong> This method requires admin privileges.
*
* @param uriInfo current uri informations
* @param user the user to be created
*
* @return
*/
@POST
@StatusCodes({
@ResponseCode(code = 201, condition = "create success", additionalHeaders = {
@ResponseHeader(name = "Location", description = "uri to the created group")
}),
@ResponseCode(code = 403, condition = "forbidden, the current user has no admin privileges"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(TypeHint.NO_CONTENT.class)
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
@Override
public Response create(@Context UriInfo uriInfo, User user)
{
return super.create(uriInfo, user);
}
/**
* Deletes a user. <strong>Note:</strong> This method requires admin privileges.
*
* @param name the name of the user to delete.
*
* @return
*/
@DELETE
@Path("{id}")
@StatusCodes({
@ResponseCode(code = 204, condition = "delete success"),
@ResponseCode(code = 403, condition = "forbidden, the current user has no admin privileges"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(TypeHint.NO_CONTENT.class)
@Override
public Response delete(@PathParam("id") String name)
{
return super.delete(name);
}
/**
* Modifies the given user. <strong>Note:</strong> This method requires admin privileges.
*
* @param name name of the user to be modified
* @param user user object to modify
*
* @return
*/
@PUT
@Path("{id}")
@StatusCodes({
@ResponseCode(code = 204, condition = "update success"),
@ResponseCode(code = 403, condition = "forbidden, the current user has no admin privileges"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(TypeHint.NO_CONTENT.class)
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
@Override
public Response update(@PathParam("id") String name, User user)
{
return super.update(name, user);
}
//~--- get methods ----------------------------------------------------------
/**
* Returns a user. <strong>Note:</strong> This method requires admin privileges.
*
* @param request the current request
* @param id the id/name of the user
*
* @return the {@link User} with the specified id
*/
@GET
@Path("{id}")
@TypeHint(User.class)
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 403, condition = "forbidden, the current user has no admin privileges"),
@ResponseCode(code = 404, condition = "not found, no group with the specified id/name available"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
@Override
public Response get(@Context Request request, @PathParam("id") String id)
{
Response response = null;
if (SecurityUtils.getSubject().hasRole(Role.ADMIN))
{
response = super.get(request, id);
}
else
{
response = Response.status(Response.Status.FORBIDDEN).build();
}
return response;
}
/**
* Returns all users. <strong>Note:</strong> This method requires admin privileges.
*
* @param request the current request
* @param start the start value for paging
* @param limit the limit value for paging
* @param sortby sort parameter
* @param desc sort direction desc or aesc
*
* @return
*/
@GET
@TypeHint(User[].class)
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 403, condition = "forbidden, the current user has no admin privileges"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
@Override
public Response getAll(@Context Request request, @DefaultValue("0")
@QueryParam("start") int start, @DefaultValue("-1")
@QueryParam("limit") int limit, @QueryParam("sortby") String sortby,
@DefaultValue("false")
@QueryParam("desc") boolean desc)
{
return super.getAll(request, start, limit, sortby, desc);
}
//~--- methods --------------------------------------------------------------
@Override
protected GenericEntity<Collection<User>> createGenericEntity(
Collection<User> items)
{
return new GenericEntity<Collection<User>>(items) {}
;
}
@Override
protected void preCreate(User user)
{
encryptPassword(user);
}
@Override
protected void preUpdate(User user)
{
if (DUMMY_PASSWORT.equals(user.getPassword()))
{
User o = manager.get(user.getName());
AssertUtil.assertIsNotNull(o);
user.setPassword(o.getPassword());
}
else
{
encryptPassword(user);
}
}
@Override
protected Collection<User> prepareForReturn(Collection<User> users)
{
if (Util.isNotEmpty(users))
{
for (User u : users)
{
u.setPassword(DUMMY_PASSWORT);
}
}
return users;
}
@Override
protected User prepareForReturn(User user)
{
user.setPassword(DUMMY_PASSWORT);
return user;
}
@Override
protected String getId(User user)
{
return user.getName();
}
@Override
protected String getPathPart()
{
return PATH_PART;
}
private void encryptPassword(User user)
{
String password = user.getPassword();
if (Util.isNotEmpty(password))
{
user.setPassword(passwordService.encryptPassword(password));
}
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private PasswordService passwordService;
}

View File

@@ -28,12 +28,14 @@
*/
package sonia.scm.api.v2.resources;
package sonia.scm.api.v2;
import sonia.scm.NotFoundException;
import sonia.scm.api.rest.StatusExceptionMapper;
import sonia.scm.api.rest.ContextualExceptionMapper;
import sonia.scm.api.v2.resources.ExceptionWithContextToErrorDtoMapper;
import javax.inject.Inject;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.Provider;
@@ -41,9 +43,9 @@ import javax.ws.rs.ext.Provider;
* @since 2.0.0
*/
@Provider
public class NotFoundExceptionMapper extends StatusExceptionMapper<NotFoundException> {
public NotFoundExceptionMapper() {
super(NotFoundException.class, Response.Status.NOT_FOUND);
public class NotFoundExceptionMapper extends ContextualExceptionMapper<NotFoundException> {
@Inject
public NotFoundExceptionMapper(ExceptionWithContextToErrorDtoMapper mapper) {
super(NotFoundException.class, Response.Status.NOT_FOUND, mapper);
}
}

View File

@@ -0,0 +1,17 @@
package sonia.scm.api.v2;
import sonia.scm.NotSupportedFeatureException;
import sonia.scm.api.rest.ContextualExceptionMapper;
import sonia.scm.api.v2.resources.ExceptionWithContextToErrorDtoMapper;
import javax.inject.Inject;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.Provider;
@Provider
public class NotSupportedFeatureExceptionMapper extends ContextualExceptionMapper<NotSupportedFeatureException> {
@Inject
public NotSupportedFeatureExceptionMapper(ExceptionWithContextToErrorDtoMapper mapper) {
super(NotSupportedFeatureException.class, Response.Status.BAD_REQUEST, mapper);
}
}

View File

@@ -1,58 +1,30 @@
package sonia.scm.api.v2;
import lombok.Getter;
import org.jboss.resteasy.api.validation.ResteasyViolationException;
import sonia.scm.api.v2.resources.ViolationExceptionToErrorDtoMapper;
import javax.validation.ConstraintViolation;
import javax.inject.Inject;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlRootElement;
import java.util.List;
import java.util.stream.Collectors;
@Provider
public class ValidationExceptionMapper implements ExceptionMapper<ResteasyViolationException> {
private final ViolationExceptionToErrorDtoMapper mapper;
@Inject
public ValidationExceptionMapper(ViolationExceptionToErrorDtoMapper mapper) {
this.mapper = mapper;
}
@Override
public Response toResponse(ResteasyViolationException exception) {
List<ConstraintViolationBean> violations =
exception.getConstraintViolations()
.stream()
.map(ConstraintViolationBean::new)
.collect(Collectors.toList());
return Response
.status(Response.Status.BAD_REQUEST)
.type(MediaType.APPLICATION_JSON_TYPE)
.entity(new ValidationError(violations))
.entity(mapper.map(exception))
.build();
}
@Getter
public static class ValidationError {
@XmlElement(name = "violation")
@XmlElementWrapper(name = "violations")
private List<ConstraintViolationBean> violations;
public ValidationError(List<ConstraintViolationBean> violations) {
this.violations = violations;
}
}
@XmlRootElement(name = "violation")
@Getter
public static class ConstraintViolationBean {
private String path;
private String message;
public ConstraintViolationBean(ConstraintViolation<?> violation) {
message = violation.getMessage();
path = violation.getPropertyPath().toString();
}
}
}

View File

@@ -18,6 +18,7 @@ import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
@Path(AuthenticationResource.PATH)
@AllowAnonymousAccess
public class AuthenticationResource {
private static final Logger LOG = LoggerFactory.getLogger(AuthenticationResource.class);

View File

@@ -5,12 +5,12 @@ 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.Branch;
import sonia.scm.repository.Branches;
import sonia.scm.repository.Changeset;
import sonia.scm.repository.ChangesetPagingResult;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryNotFoundException;
import sonia.scm.repository.RepositoryPermissions;
import sonia.scm.repository.api.CommandNotSupportedException;
import sonia.scm.repository.api.RepositoryService;
@@ -26,6 +26,10 @@ import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.util.List;
import static sonia.scm.ContextEntry.ContextBuilder.entity;
import static sonia.scm.NotFoundException.notFound;
public class BranchRootResource {
@@ -77,7 +81,7 @@ public class BranchRootResource {
.build();
} catch (CommandNotSupportedException ex) {
return Response.status(Response.Status.BAD_REQUEST).build();
} catch (RepositoryNotFoundException e) {
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND).build();
}
}
@@ -97,7 +101,7 @@ public class BranchRootResource {
@PathParam("name") String name,
@PathParam("branch") String branchName,
@DefaultValue("0") @QueryParam("page") int page,
@DefaultValue("10") @QueryParam("pageSize") int pageSize) throws Exception {
@DefaultValue("10") @QueryParam("pageSize") int pageSize) throws IOException {
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
boolean branchExists = repositoryService.getBranchesCommand()
.getBranches()
@@ -105,7 +109,7 @@ public class BranchRootResource {
.stream()
.anyMatch(branch -> branchName.equals(branch.getName()));
if (!branchExists){
throw new NotFoundException("branch", branchName);
throw notFound(entity(Branch.class, branchName).in(Repository.class, namespace + "/" + name));
}
Repository repository = repositoryService.getRepository();
RepositoryPermissions.read(repository).check();
@@ -124,6 +128,49 @@ public class BranchRootResource {
}
}
@Path("{branch}/diffchangesets/{otherBranchName}")
@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 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 changesetDiff(@PathParam("namespace") String namespace,
@PathParam("name") String name,
@PathParam("branch") String branchName,
@PathParam("otherBranchName") String otherBranchName,
@DefaultValue("0") @QueryParam("page") int page,
@DefaultValue("10") @QueryParam("pageSize") int pageSize) throws Exception {
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
List<Branch> allBranches = repositoryService.getBranchesCommand().getBranches().getBranches();
if (allBranches.stream().noneMatch(branch -> branchName.equals(branch.getName()))) {
throw new NotFoundException("branch", branchName);
}
if (allBranches.stream().noneMatch(branch -> otherBranchName.equals(branch.getName()))) {
throw new NotFoundException("branch", otherBranchName);
}
Repository repository = repositoryService.getRepository();
RepositoryPermissions.read(repository).check();
ChangesetPagingResult changesets = new PagedLogCommandBuilder(repositoryService)
.page(page)
.pageSize(pageSize)
.create()
.setBranch(branchName)
.setAncestorChangeset(otherBranchName)
.getChangesets();
if (changesets != null && changesets.getChangesets() != null) {
PageResult<Changeset> pageResult = new PageResult<>(changesets.getChangesets(), changesets.getTotal());
return Response.ok(branchChangesetCollectionToDtoMapper.map(page, pageSize, pageResult, repository, branchName)).build();
} else {
return Response.ok().build();
}
}
}
/**
* Returns the branches for a repository.
*
@@ -150,8 +197,6 @@ public class BranchRootResource {
return Response.ok(branchCollectionToDtoMapper.map(namespace, name, branches.getBranches())).build();
} catch (CommandNotSupportedException ex) {
return Response.status(Response.Status.BAD_REQUEST).build();
} catch (RepositoryNotFoundException e) {
return Response.status(Response.Status.NOT_FOUND).build();
}
}
}

View File

@@ -28,6 +28,7 @@ public abstract class BranchToBranchDtoMapper {
Links.Builder linksBuilder = linkingTo()
.self(resourceLinks.branch().self(namespaceAndName, target.getName()))
.single(linkBuilder("history", resourceLinks.branch().history(namespaceAndName, target.getName())).build())
.single(linkBuilder("changesetDiff", resourceLinks.branch().changesetDiff(namespaceAndName, target.getName())).build())
.single(linkBuilder("changeset", resourceLinks.changeset().changeset(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getRevision())).build())
.single(linkBuilder("source", resourceLinks.source().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getRevision())).build());
target.add(linksBuilder.build());

View File

@@ -1,17 +1,17 @@
package sonia.scm.api.v2.resources;
import sonia.scm.api.rest.ContextualExceptionMapper;
import sonia.scm.user.ChangePasswordNotAllowedException;
import sonia.scm.user.InvalidPasswordException;
import javax.inject.Inject;
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();
public class ChangePasswordNotAllowedExceptionMapper extends ContextualExceptionMapper<ChangePasswordNotAllowedException> {
@Inject
public ChangePasswordNotAllowedExceptionMapper(ExceptionWithContextToErrorDtoMapper mapper) {
super(ChangePasswordNotAllowedException.class, Response.Status.BAD_REQUEST, mapper);
}
}

View File

@@ -9,10 +9,7 @@ import sonia.scm.repository.Changeset;
import sonia.scm.repository.ChangesetPagingResult;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryNotFoundException;
import sonia.scm.repository.RepositoryPermissions;
import sonia.scm.repository.RevisionNotFoundException;
import sonia.scm.repository.api.LogCommandBuilder;
import sonia.scm.repository.api.RepositoryService;
import sonia.scm.repository.api.RepositoryServiceFactory;
import sonia.scm.web.VndMediaType;
@@ -26,6 +23,7 @@ import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.util.Optional;
@Slf4j
@@ -56,7 +54,7 @@ public class ChangesetRootResource {
@Produces(VndMediaType.CHANGESET_COLLECTION)
@TypeHint(CollectionDto.class)
public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name, @DefaultValue("0") @QueryParam("page") int page,
@DefaultValue("10") @QueryParam("pageSize") int pageSize) throws IOException, RevisionNotFoundException, RepositoryNotFoundException {
@DefaultValue("10") @QueryParam("pageSize") int pageSize) throws IOException {
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
Repository repository = repositoryService.getRepository();
RepositoryPermissions.read(repository).check();
@@ -89,7 +87,7 @@ public class ChangesetRootResource {
@Produces(VndMediaType.CHANGESET)
@TypeHint(ChangesetDto.class)
@Path("{id}")
public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("id") String id) throws IOException, RevisionNotFoundException, RepositoryNotFoundException {
public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("id") String id) throws IOException {
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
Repository repository = repositoryService.getRepository();
RepositoryPermissions.read(repository).check();
@@ -97,8 +95,12 @@ public class ChangesetRootResource {
.setStartChangeset(id)
.setEndChangeset(id)
.getChangesets();
if (changesets != null && changesets.getChangesets() != null && changesets.getChangesets().size() == 1) {
return Response.ok(changesetToChangesetDtoMapper.map(changesets.getChangesets().get(0), repository)).build();
if (changesets != null && changesets.getChangesets() != null && !changesets.getChangesets().isEmpty()) {
Optional<Changeset> changeset = changesets.getChangesets().stream().filter(ch -> ch.getId().equals(id)).findFirst();
if (!changeset.isPresent()) {
return Response.status(Response.Status.NOT_FOUND).build();
}
return Response.ok(changesetToChangesetDtoMapper.map(changeset.get(), repository)).build();
} else {
return Response.status(Response.Status.NOT_FOUND).build();
}

View File

@@ -1,7 +1,6 @@
package sonia.scm.api.v2.resources;
import de.otto.edison.hal.HalRepresentation;
import sonia.scm.AlreadyExistsException;
import sonia.scm.Manager;
import sonia.scm.ModelObject;
import sonia.scm.PageResult;
@@ -47,7 +46,7 @@ class CollectionResourceManagerAdapter<MODEL_OBJECT extends ModelObject,
* Creates a model object for the given dto and returns a corresponding http response.
* This handles all corner cases, eg. no conflicts or missing privileges.
*/
public Response create(DTO dto, Supplier<MODEL_OBJECT> modelObjectSupplier, Function<MODEL_OBJECT, String> uriCreator) throws AlreadyExistsException {
public Response create(DTO dto, Supplier<MODEL_OBJECT> modelObjectSupplier, Function<MODEL_OBJECT, String> uriCreator) {
if (dto == null) {
return Response.status(BAD_REQUEST).build();
}

View File

@@ -6,10 +6,8 @@ import com.webcohesion.enunciate.metadata.rs.ResponseCode;
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.NotFoundException;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.PathNotFoundException;
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.util.IOUtil;
@@ -64,8 +62,8 @@ public class ContentResource {
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
Response.ResponseBuilder responseBuilder = Response.ok(stream);
return createContentHeader(namespace, name, revision, path, repositoryService, responseBuilder);
} catch (RepositoryNotFoundException e) {
LOG.debug("path '{}' not found in repository {}/{}", path, namespace, name, e);
} catch (NotFoundException e) {
LOG.debug(e.getMessage());
return Response.status(Status.NOT_FOUND).build();
}
}
@@ -75,14 +73,8 @@ public class ContentResource {
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
repositoryService.getCatCommand().setRevision(revision).retriveContent(os, path);
os.close();
} catch (RepositoryNotFoundException e) {
LOG.debug("repository {}/{} not found", path, namespace, name, e);
throw new WebApplicationException(Status.NOT_FOUND);
} catch (PathNotFoundException e) {
LOG.debug("path '{}' not found in repository {}/{}", path, namespace, name, e);
throw new WebApplicationException(Status.NOT_FOUND);
} catch (RevisionNotFoundException e) {
LOG.debug("revision '{}' not found in repository {}/{}", revision, namespace, name, e);
} catch (NotFoundException e) {
LOG.debug(e.getMessage());
throw new WebApplicationException(Status.NOT_FOUND);
}
};
@@ -111,8 +103,8 @@ public class ContentResource {
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
Response.ResponseBuilder responseBuilder = Response.ok();
return createContentHeader(namespace, name, revision, path, repositoryService, responseBuilder);
} catch (RepositoryNotFoundException e) {
LOG.debug("path '{}' not found in repository {}/{}", path, namespace, name, e);
} catch (NotFoundException e) {
LOG.debug(e.getMessage());
return Response.status(Status.NOT_FOUND).build();
}
}
@@ -120,12 +112,6 @@ public class ContentResource {
private Response createContentHeader(String namespace, String name, String revision, String path, RepositoryService repositoryService, Response.ResponseBuilder responseBuilder) {
try {
appendContentHeader(path, getHead(revision, path, repositoryService), responseBuilder);
} catch (PathNotFoundException e) {
LOG.debug("path '{}' not found in repository {}/{}", path, namespace, name, e);
return Response.status(Status.NOT_FOUND).build();
} catch (RevisionNotFoundException e) {
LOG.debug("revision '{}' not found in repository {}/{}", revision, namespace, name, e);
return Response.status(Status.NOT_FOUND).build();
} catch (IOException e) {
LOG.info("error reading repository resource {} from {}/{}", path, namespace, name, e);
return Response.status(Status.INTERNAL_SERVER_ERROR).entity(e.getMessage()).build();
@@ -136,10 +122,10 @@ public class ContentResource {
private void appendContentHeader(String path, byte[] head, Response.ResponseBuilder responseBuilder) {
ContentType contentType = ContentTypes.detect(path, head);
responseBuilder.header("Content-Type", contentType.getRaw());
contentType.getLanguage().ifPresent(language -> responseBuilder.header("Language", language));
contentType.getLanguage().ifPresent(language -> responseBuilder.header("X-Programming-Language", language));
}
private byte[] getHead(String revision, String path, RepositoryService repositoryService) throws IOException, PathNotFoundException, RevisionNotFoundException {
private byte[] getHead(String revision, String path, RepositoryService repositoryService) throws IOException {
InputStream stream = repositoryService.getCatCommand().setRevision(revision).getStream(path);
try {
byte[] buffer = new byte[HEAD_BUFFER_SIZE];

View File

@@ -4,24 +4,29 @@ import com.webcohesion.enunciate.metadata.rs.ResponseCode;
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import sonia.scm.NotFoundException;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.RevisionNotFoundException;
import sonia.scm.repository.api.DiffFormat;
import sonia.scm.repository.api.RepositoryService;
import sonia.scm.repository.api.RepositoryServiceFactory;
import sonia.scm.util.HttpUtil;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.validation.constraints.Pattern;
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.WebApplicationException;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.StreamingOutput;
public class DiffRootResource {
public static final String HEADER_CONTENT_DISPOSITION = "Content-Disposition";
private static final String DIFF_FORMAT_VALUES_REGEX = "NATIVE|GIT|UNIFIED";
private final RepositoryServiceFactory serviceFactory;
@Inject
@@ -50,17 +55,15 @@ public class DiffRootResource {
@ResponseCode(code = 404, condition = "not found, no revision with the specified param for the repository available or repository not found"),
@ResponseCode(code = 500, condition = "internal server error")
})
public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision){
public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision , @Pattern(regexp = DIFF_FORMAT_VALUES_REGEX) @DefaultValue("NATIVE") @QueryParam("format") String format ){
HttpUtil.checkForCRLFInjection(revision);
DiffFormat diffFormat = DiffFormat.valueOf(format);
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
StreamingOutput responseEntry = output -> {
try {
repositoryService.getDiffCommand()
.setRevision(revision)
.retriveContent(output);
} catch (RevisionNotFoundException e) {
throw new WebApplicationException(Response.Status.NOT_FOUND);
}
repositoryService.getDiffCommand()
.setRevision(revision)
.setFormat(diffFormat)
.retrieveContent(output);
};
return Response.ok(responseEntry)
.header(HEADER_CONTENT_DISPOSITION, HttpUtil.createContentDispositionAttachmentHeader(String.format("%s-%s.diff", name, revision)))

View File

@@ -0,0 +1,33 @@
package sonia.scm.api.v2.resources;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Getter;
import lombok.Setter;
import sonia.scm.ContextEntry;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlRootElement;
import java.util.List;
@Getter @Setter
public class ErrorDto {
private String transactionId;
private String errorCode;
private List<ContextEntry> context;
private String message;
@JsonInclude(JsonInclude.Include.NON_NULL)
@XmlElementWrapper(name = "violations")
private List<ConstraintViolationDto> violations;
@JsonInclude(JsonInclude.Include.NON_NULL)
private String url;
@XmlRootElement(name = "violation")
@Getter @Setter
public static class ConstraintViolationDto {
private String path;
private String message;
}
}

View File

@@ -0,0 +1,23 @@
package sonia.scm.api.v2.resources;
import org.mapstruct.AfterMapping;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.MappingTarget;
import org.slf4j.MDC;
import sonia.scm.ExceptionWithContext;
@Mapper
public abstract class ExceptionWithContextToErrorDtoMapper {
@Mapping(target = "errorCode", source = "code")
@Mapping(target = "transactionId", ignore = true)
@Mapping(target = "violations", ignore = true)
@Mapping(target = "url", ignore = true)
public abstract ErrorDto map(ExceptionWithContext exception);
@AfterMapping
void setTransactionId(@MappingTarget ErrorDto dto) {
dto.setTransactionId(MDC.get("transaction_id"));
}
}

View File

@@ -7,11 +7,8 @@ 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;
@@ -26,6 +23,9 @@ import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response;
import java.io.IOException;
import static sonia.scm.ContextEntry.ContextBuilder.entity;
import static sonia.scm.NotFoundException.notFound;
@Slf4j
public class FileHistoryRootResource {
@@ -51,8 +51,6 @@ public class FileHistoryRootResource {
* @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: .*}")
@@ -69,8 +67,9 @@ public class FileHistoryRootResource {
@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))) {
@DefaultValue("10") @QueryParam("pageSize") int pageSize) throws IOException {
NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name);
try (RepositoryService repositoryService = serviceFactory.create(namespaceAndName)) {
log.info("Get changesets of the file {} and revision {}", path, revision);
Repository repository = repositoryService.getRepository();
ChangesetPagingResult changesets = new PagedLogCommandBuilder(repositoryService)
@@ -84,9 +83,9 @@ public class FileHistoryRootResource {
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);
String message = String.format("for the revision %s and the file %s there are no changesets", revision, path);
log.error(message);
throw new InternalRepositoryException(message);
throw notFound(entity("path", path).in("revision", revision).in(namespaceAndName));
}
}
}

View File

@@ -5,12 +5,12 @@ 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 sonia.scm.AlreadyExistsException;
import sonia.scm.group.Group;
import sonia.scm.group.GroupManager;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.inject.Named;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.DefaultValue;
@@ -71,7 +71,7 @@ public class GroupCollectionResource {
/**
* Creates a new group.
* @param groupDto The group to be created.
* @param group The group to be created.
* @return A response with the link to the new group (if created successfully).
*/
@POST
@@ -86,9 +86,9 @@ public class GroupCollectionResource {
})
@TypeHint(TypeHint.NO_CONTENT.class)
@ResponseHeaders(@ResponseHeader(name = "Location", description = "uri to the created group"))
public Response create(@Valid GroupDto groupDto) throws AlreadyExistsException {
return adapter.create(groupDto,
() -> dtoToGroupMapper.map(groupDto),
group -> resourceLinks.group().self(group.getName()));
public Response create(@Valid GroupDto group) {
return adapter.create(group,
() -> dtoToGroupMapper.map(group),
g -> resourceLinks.group().self(g.getName()));
}
}

View File

@@ -3,13 +3,12 @@ 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.ConcurrentModificationException;
import sonia.scm.NotFoundException;
import sonia.scm.group.Group;
import sonia.scm.group.GroupManager;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.inject.Named;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
@@ -84,7 +83,7 @@ public class GroupResource {
* <strong>Note:</strong> This method requires "group" privilege.
*
* @param name name of the group to be modified
* @param groupDto group object to modify
* @param group group object to modify
*/
@PUT
@Path("")
@@ -98,7 +97,7 @@ public class GroupResource {
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(TypeHint.NO_CONTENT.class)
public Response update(@PathParam("id") String name, @Valid GroupDto groupDto) throws ConcurrentModificationException {
return adapter.update(name, existing -> dtoToGroupMapper.map(groupDto));
public Response update(@PathParam("id") String name, @Valid GroupDto group) {
return adapter.update(name, existing -> dtoToGroupMapper.map(group));
}
}

View File

@@ -1,10 +1,9 @@
package sonia.scm.api.v2.resources;
import de.otto.edison.hal.HalRepresentation;
import sonia.scm.AlreadyExistsException;
import sonia.scm.ConcurrentModificationException;
import sonia.scm.Manager;
import sonia.scm.ModelObject;
import sonia.scm.NotFoundException;
import sonia.scm.PageResult;
import javax.ws.rs.core.Response;
@@ -22,6 +21,7 @@ class IdResourceManagerAdapter<MODEL_OBJECT extends ModelObject,
DTO extends HalRepresentation> {
private final Manager<MODEL_OBJECT> manager;
private final String type;
private final SingleResourceManagerAdapter<MODEL_OBJECT, DTO> singleAdapter;
private final CollectionResourceManagerAdapter<MODEL_OBJECT, DTO> collectionAdapter;
@@ -30,13 +30,14 @@ class IdResourceManagerAdapter<MODEL_OBJECT extends ModelObject,
this.manager = manager;
singleAdapter = new SingleResourceManagerAdapter<>(manager, type);
collectionAdapter = new CollectionResourceManagerAdapter<>(manager, type);
this.type = type.getSimpleName();
}
Response get(String id, Function<MODEL_OBJECT, DTO> mapToDto) {
return singleAdapter.get(loadBy(id), mapToDto);
}
public Response update(String id, Function<MODEL_OBJECT, MODEL_OBJECT> applyChanges) throws ConcurrentModificationException {
public Response update(String id, Function<MODEL_OBJECT, MODEL_OBJECT> applyChanges) {
return singleAdapter.update(
loadBy(id),
applyChanges,
@@ -48,7 +49,7 @@ class IdResourceManagerAdapter<MODEL_OBJECT extends ModelObject,
return collectionAdapter.getAll(page, pageSize, sortBy, desc, mapToDto);
}
public Response create(DTO dto, Supplier<MODEL_OBJECT> modelObjectSupplier, Function<MODEL_OBJECT, String> uriCreator) throws AlreadyExistsException {
public Response create(DTO dto, Supplier<MODEL_OBJECT> modelObjectSupplier, Function<MODEL_OBJECT, String> uriCreator) {
return collectionAdapter.create(dto, modelObjectSupplier, uriCreator);
}
@@ -56,8 +57,8 @@ class IdResourceManagerAdapter<MODEL_OBJECT extends ModelObject,
return singleAdapter.delete(id);
}
private Supplier<Optional<MODEL_OBJECT>> loadBy(String id) {
return () -> Optional.ofNullable(manager.get(id));
private Supplier<MODEL_OBJECT> loadBy(String id) {
return () -> Optional.ofNullable(manager.get(id)).orElseThrow(() -> new NotFoundException(type, id));
}
private Predicate<MODEL_OBJECT> idStaysTheSame(String id) {

View File

@@ -1,6 +1,7 @@
package sonia.scm.api.v2.resources;
import com.webcohesion.enunciate.metadata.rs.TypeHint;
import sonia.scm.security.AllowAnonymousAccess;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
@@ -9,6 +10,7 @@ import javax.ws.rs.Path;
import javax.ws.rs.Produces;
@Path(IndexResource.INDEX_PATH_V2)
@AllowAnonymousAccess
public class IndexResource {
public static final String INDEX_PATH_V2 = "v2/";

View File

@@ -1,15 +1,17 @@
package sonia.scm.api.v2.resources;
import sonia.scm.api.rest.StatusExceptionMapper;
import sonia.scm.api.rest.ContextualExceptionMapper;
import sonia.scm.repository.InternalRepositoryException;
import javax.inject.Inject;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.Provider;
@Provider
public class InternalRepositoryExceptionMapper extends StatusExceptionMapper<InternalRepositoryException> {
public class InternalRepositoryExceptionMapper extends ContextualExceptionMapper<InternalRepositoryException> {
public InternalRepositoryExceptionMapper() {
super(InternalRepositoryException.class, Response.Status.INTERNAL_SERVER_ERROR);
@Inject
public InternalRepositoryExceptionMapper(ExceptionWithContextToErrorDtoMapper mapper) {
super(InternalRepositoryException.class, Response.Status.INTERNAL_SERVER_ERROR, mapper);
}
}

View File

@@ -1,17 +1,17 @@
package sonia.scm.api.v2.resources;
import sonia.scm.api.rest.ContextualExceptionMapper;
import sonia.scm.user.InvalidPasswordException;
import javax.inject.Inject;
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();
public class InvalidPasswordExceptionMapper extends ContextualExceptionMapper<InvalidPasswordException> {
@Inject
public InvalidPasswordExceptionMapper(ExceptionWithContextToErrorDtoMapper mapper) {
super(InvalidPasswordException.class, Response.Status.BAD_REQUEST, mapper);
}
}

View File

@@ -39,6 +39,9 @@ public class MapperModule extends AbstractModule {
bind(ReducedObjectModelToDtoMapper.class).to(Mappers.getMapper(ReducedObjectModelToDtoMapper.class).getClass());
bind(ViolationExceptionToErrorDtoMapper.class).to(Mappers.getMapper(ViolationExceptionToErrorDtoMapper.class).getClass());
bind(ExceptionWithContextToErrorDtoMapper.class).to(Mappers.getMapper(ExceptionWithContextToErrorDtoMapper.class).getClass());
// no mapstruct required
bind(UIPluginDtoMapper.class);
bind(UIPluginDtoCollectionMapper.class);

View File

@@ -10,6 +10,7 @@ import sonia.scm.user.UserManager;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.inject.Named;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
@@ -73,8 +74,8 @@ public class MeResource {
})
@TypeHint(TypeHint.NO_CONTENT.class)
@Consumes(VndMediaType.PASSWORD_CHANGE)
public Response changePassword(@Valid PasswordChangeDto passwordChangeDto) {
userManager.changePasswordForLoggedInUser(passwordService.encryptPassword(passwordChangeDto.getOldPassword()), passwordService.encryptPassword(passwordChangeDto.getNewPassword()));
public Response changePassword(@Valid PasswordChangeDto passwordChange) {
userManager.changePasswordForLoggedInUser(passwordService.encryptPassword(passwordChange.getOldPassword()), passwordService.encryptPassword(passwordChange.getNewPassword()));
return Response.noContent().build();
}
}

View File

@@ -3,11 +3,8 @@ 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;
@@ -46,7 +43,7 @@ public class ModificationsRootResource {
@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 {
public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision) throws IOException {
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
Modifications modifications = repositoryService.getModificationsCommand()
.revision(revision)

View File

@@ -11,11 +11,11 @@ import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Permission;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryManager;
import sonia.scm.repository.RepositoryNotFoundException;
import sonia.scm.repository.RepositoryPermissions;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.inject.Named;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
@@ -30,6 +30,9 @@ import java.net.URI;
import java.util.Optional;
import java.util.function.Predicate;
import static sonia.scm.AlreadyExistsException.alreadyExists;
import static sonia.scm.ContextEntry.ContextBuilder.entity;
import static sonia.scm.NotFoundException.notFound;
import static sonia.scm.api.v2.resources.PermissionDto.GROUP_PREFIX;
@Slf4j
@@ -71,12 +74,12 @@ public class PermissionRootResource {
@TypeHint(TypeHint.NO_CONTENT.class)
@Consumes(VndMediaType.PERMISSION)
@Path("")
public Response create(@PathParam("namespace") String namespace, @PathParam("name") String name,@Valid PermissionDto permission) throws AlreadyExistsException, NotFoundException {
public Response create(@PathParam("namespace") String namespace, @PathParam("name") String name,@Valid PermissionDto permission) {
log.info("try to add new permission: {}", permission);
Repository repository = load(namespace, name);
RepositoryPermissions.permissionWrite(repository).check();
checkPermissionAlreadyExists(permission, repository);
repository.getPermissions().add(dtoToModelMapper.map(permission));
repository.addPermission(dtoToModelMapper.map(permission));
manager.modify(repository);
String urlPermissionName = modelToDtoMapper.getUrlPermissionName(permission);
return Response.created(URI.create(resourceLinks.permission().self(namespace, name, urlPermissionName))).build();
@@ -109,7 +112,7 @@ public class PermissionRootResource {
.filter(filterPermission(permissionName))
.map(permission -> modelToDtoMapper.map(permission, repository))
.findFirst()
.orElseThrow(NotFoundException::new)
.orElseThrow(() -> notFound(entity(Permission.class, namespace).in(Repository.class, namespace + "/" + name)))
).build();
}
@@ -131,7 +134,7 @@ public class PermissionRootResource {
@Produces(VndMediaType.PERMISSION)
@TypeHint(PermissionDto.class)
@Path("")
public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name) throws RepositoryNotFoundException {
public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name) {
Repository repository = load(namespace, name);
RepositoryPermissions.permissionRead(repository).check();
return Response.ok(permissionCollectionToDtoMapper.map(repository)).build();
@@ -158,23 +161,23 @@ public class PermissionRootResource {
public Response update(@PathParam("namespace") String namespace,
@PathParam("name") String name,
@PathParam("permission-name") String permissionName,
@Valid PermissionDto permission) throws AlreadyExistsException {
@Valid PermissionDto permission) {
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("permission", extractedPermissionName);
throw notFound(entity(Permission.class, namespace).in(Repository.class, namespace + "/" + name));
}
permission.setGroupPermission(isGroupPermission(permissionName));
if (!extractedPermissionName.equals(permission.getName())) {
checkPermissionAlreadyExists(permission, repository, "target permission " + permission.getName() + " already exists");
checkPermissionAlreadyExists(permission, repository);
}
Permission existingPermission = repository.getPermissions()
.stream()
.filter(filterPermission(permissionName))
.findFirst()
.orElseThrow(NotFoundException::new);
.orElseThrow(() -> notFound(entity(Permission.class, namespace).in(Repository.class, namespace + "/" + name)));
dtoToModelMapper.modify(existingPermission, permission);
manager.modify(repository);
log.info("the permission with name: {} is updated.", permissionName);
@@ -206,7 +209,7 @@ public class PermissionRootResource {
.stream()
.filter(filterPermission(permissionName))
.findFirst()
.ifPresent(p -> repository.getPermissions().remove(p))
.ifPresent(repository::removePermission)
;
manager.modify(repository);
log.info("the permission with name: {} is updated.", permissionName);
@@ -237,12 +240,12 @@ public class PermissionRootResource {
* @param namespace the repository namespace
* @param name the repository name
* @return the repository if the user is permitted
* @throws RepositoryNotFoundException if the repository does not exists
* @throws NotFoundException if the repository does not exists
*/
private Repository load(String namespace, String name) throws RepositoryNotFoundException {
private Repository load(String namespace, String name) {
NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name);
return Optional.ofNullable(manager.get(namespaceAndName))
.orElseThrow(() -> new RepositoryNotFoundException(namespaceAndName));
.orElseThrow(() -> notFound(entity(namespaceAndName)));
}
/**
@@ -250,12 +253,11 @@ public class PermissionRootResource {
*
* @param permission the searched permission
* @param repository the repository to be inspected
* @param errorMessage error message
* @throws AlreadyExistsException if the permission already exists in the repository
*/
private void checkPermissionAlreadyExists(PermissionDto permission, Repository repository, String errorMessage) throws AlreadyExistsException {
private void checkPermissionAlreadyExists(PermissionDto permission, Repository repository) {
if (isPermissionExist(permission, repository)) {
throw new AlreadyExistsException(errorMessage);
throw alreadyExists(entity("permission", permission.getName()).in(repository));
}
}
@@ -264,10 +266,6 @@ public class PermissionRootResource {
.stream()
.anyMatch(p -> p.getName().equals(permission.getName()) && p.isGroupPermission() == permission.isGroupPermission());
}
private void checkPermissionAlreadyExists(PermissionDto permission, Repository repository) throws AlreadyExistsException {
checkPermissionAlreadyExists(permission, repository, "the permission " + permission.getName() + " already exist.");
}
}

View File

@@ -5,9 +5,12 @@ 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 sonia.scm.AlreadyExistsException;
import org.apache.shiro.SecurityUtils;
import sonia.scm.repository.Permission;
import sonia.scm.repository.PermissionType;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryManager;
import sonia.scm.user.User;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
@@ -21,6 +24,8 @@ import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response;
import static java.util.Collections.singletonList;
public class RepositoryCollectionResource {
private static final int DEFAULT_PAGE_SIZE = 10;
@@ -72,7 +77,7 @@ public class RepositoryCollectionResource {
* <strong>Note:</strong> This method requires "repository" privilege. The namespace of the given repository will
* be ignored and set by the configured namespace strategy.
*
* @param repositoryDto The repository to be created.
* @param repository The repository to be created.
* @return A response with the link to the new repository (if created successfully).
*/
@POST
@@ -87,9 +92,19 @@ public class RepositoryCollectionResource {
})
@TypeHint(TypeHint.NO_CONTENT.class)
@ResponseHeaders(@ResponseHeader(name = "Location", description = "uri to the created repository"))
public Response create(@Valid RepositoryDto repositoryDto) throws AlreadyExistsException {
return adapter.create(repositoryDto,
() -> dtoToRepositoryMapper.map(repositoryDto, null),
repository -> resourceLinks.repository().self(repository.getNamespace(), repository.getName()));
public Response create(@Valid RepositoryDto repository) {
return adapter.create(repository,
() -> createModelObjectFromDto(repository),
r -> resourceLinks.repository().self(r.getNamespace(), r.getName()));
}
private Repository createModelObjectFromDto(@Valid RepositoryDto repositoryDto) {
Repository repository = dtoToRepositoryMapper.map(repositoryDto, null);
repository.setPermissions(singletonList(new Permission(currentUser(), PermissionType.OWNER)));
return repository;
}
private String currentUser() {
return SecurityUtils.getSubject().getPrincipals().oneByType(User.class).getName();
}
}

View File

@@ -3,8 +3,6 @@ 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.ConcurrentModificationException;
import sonia.scm.NotFoundException;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryIsNotArchivedException;
@@ -12,6 +10,7 @@ import sonia.scm.repository.RepositoryManager;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Provider;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
@@ -26,6 +25,9 @@ import java.util.Optional;
import java.util.function.Predicate;
import java.util.function.Supplier;
import static sonia.scm.ContextEntry.ContextBuilder.entity;
import static sonia.scm.NotFoundException.notFound;
public class RepositoryResource {
private final RepositoryToRepositoryDtoMapper repositoryToDtoMapper;
@@ -124,7 +126,7 @@ public class RepositoryResource {
*
* @param namespace the namespace of the repository to be modified
* @param name the name of the repository to be modified
* @param repositoryDto repository object to modify
* @param repository repository object to modify
*/
@PUT
@Path("")
@@ -138,10 +140,10 @@ public class RepositoryResource {
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(TypeHint.NO_CONTENT.class)
public Response update(@PathParam("namespace") String namespace, @PathParam("name") String name, @Valid RepositoryDto repositoryDto) throws ConcurrentModificationException {
public Response update(@PathParam("namespace") String namespace, @PathParam("name") String name, @Valid RepositoryDto repository) {
return adapter.update(
loadBy(namespace, name),
existing -> processUpdate(repositoryDto, existing),
existing -> processUpdate(repository, existing),
nameAndNamespaceStaysTheSame(namespace, name)
);
}
@@ -203,8 +205,9 @@ public class RepositoryResource {
}
}
private Supplier<Optional<Repository>> loadBy(String namespace, String name) {
return () -> Optional.ofNullable(manager.get(new NamespaceAndName(namespace, name)));
private Supplier<Repository> loadBy(String namespace, String name) {
NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name);
return () -> Optional.ofNullable(manager.get(namespaceAndName)).orElseThrow(() -> notFound(entity(namespaceAndName)));
}
private Predicate<Repository> nameAndNamespaceStaysTheSame(String namespace, String name) {

View File

@@ -322,6 +322,10 @@ class ResourceLinks {
public String history(NamespaceAndName namespaceAndName, String branch) {
return branchLinkBuilder.method("getRepositoryResource").parameters(namespaceAndName.getNamespace(), namespaceAndName.getName()).method("branches").parameters().method("history").parameters(branch).href();
}
public String changesetDiff(NamespaceAndName namespaceAndName, String branch) {
return branchLinkBuilder.method("getRepositoryResource").parameters(namespaceAndName.getNamespace(), namespaceAndName.getName()).method("branches").parameters().method("changesetDiff").parameters(branch, "").href() + "{otherBranch}";
}
}
public BranchCollectionLinks branchCollection() {

View File

@@ -11,7 +11,6 @@ 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;
@@ -33,45 +32,41 @@ class SingleResourceManagerAdapter<MODEL_OBJECT extends ModelObject,
DTO extends HalRepresentation> extends AbstractManagerResource<MODEL_OBJECT> {
private final Function<Throwable, Optional<Response>> errorHandler;
private final Class<MODEL_OBJECT> type;
SingleResourceManagerAdapter(Manager<MODEL_OBJECT> manager, Class<MODEL_OBJECT> type) {
this(manager, type, e -> Optional.empty());
}
SingleResourceManagerAdapter(Manager<MODEL_OBJECT> manager, Class<MODEL_OBJECT> type, Function<Throwable, Optional<Response>> errorHandler) {
SingleResourceManagerAdapter(
Manager<MODEL_OBJECT> manager,
Class<MODEL_OBJECT> type,
Function<Throwable, Optional<Response>> errorHandler) {
super(manager, type);
this.errorHandler = errorHandler;
this.type = type;
}
/**
* Reads the model object for the given id, transforms it to a dto and returns a corresponding http response.
* This handles all corner cases, eg. no matching object for the id or missing privileges.
*/
Response get(Supplier<Optional<MODEL_OBJECT>> reader, Function<MODEL_OBJECT, DTO> mapToDto) {
return reader.get()
.map(mapToDto)
.map(Response::ok)
.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);
Response get(Supplier<MODEL_OBJECT> reader, Function<MODEL_OBJECT, DTO> mapToDto) {
return Response.ok(mapToDto.apply(reader.get())).build();
}
/**
* Update the model object for the given id according to the given function and returns a corresponding http response.
* This handles all corner cases, eg. no matching object for the id or missing privileges.
*/
public Response update(Supplier<Optional<MODEL_OBJECT>> reader, Function<MODEL_OBJECT, MODEL_OBJECT> applyChanges, Predicate<MODEL_OBJECT> hasSameKey) throws NotFoundException, ConcurrentModificationException {
MODEL_OBJECT existingModelObject = reader.get().orElseThrow(NotFoundException::new);
Response update(Supplier<MODEL_OBJECT> reader, Function<MODEL_OBJECT, MODEL_OBJECT> applyChanges, Predicate<MODEL_OBJECT> hasSameKey) {
MODEL_OBJECT existingModelObject = reader.get();
MODEL_OBJECT changedModelObject = applyChanges.apply(existingModelObject);
if (!hasSameKey.test(changedModelObject)) {
return Response.status(BAD_REQUEST).entity("illegal change of id").build();
}
else if (modelObjectWasModifiedConcurrently(existingModelObject, changedModelObject)) {
throw new ConcurrentModificationException();
throw new ConcurrentModificationException(type, existingModelObject.getId());
}
return update(getId(existingModelObject), changedModelObject);
}
@@ -81,11 +76,13 @@ class SingleResourceManagerAdapter<MODEL_OBJECT extends ModelObject,
&& (updated.getLastModified() == null || existing.getLastModified() > updated.getLastModified());
}
public Response delete(Supplier<Optional<MODEL_OBJECT>> reader) {
return reader.get()
.map(MODEL_OBJECT::getId)
.map(this::delete)
.orElse(null);
public Response delete(Supplier<MODEL_OBJECT> reader) {
try {
return delete(reader.get().getId());
} catch (NotFoundException e) {
// due to idempotency of delete this does not matter here.
return null;
}
}
@Override

View File

@@ -1,6 +1,5 @@
package sonia.scm.api.v2.resources;
import sonia.scm.NotFoundException;
import sonia.scm.repository.BrowserResult;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.api.BrowseCommandBuilder;
@@ -31,14 +30,14 @@ public class SourceRootResource {
@GET
@Produces(VndMediaType.SOURCE)
@Path("")
public Response getAllWithoutRevision(@PathParam("namespace") String namespace, @PathParam("name") String name) throws NotFoundException, IOException {
public Response getAllWithoutRevision(@PathParam("namespace") String namespace, @PathParam("name") String name) throws IOException {
return getSource(namespace, name, "/", null);
}
@GET
@Produces(VndMediaType.SOURCE)
@Path("{revision}")
public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision) throws NotFoundException, IOException {
public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision) throws IOException {
return getSource(namespace, name, "/", revision);
}
@@ -49,7 +48,7 @@ public class SourceRootResource {
return getSource(namespace, name, path, revision);
}
private Response getSource(String namespace, String repoName, String path, String revision) throws IOException, NotFoundException {
private Response getSource(String namespace, String repoName, String path, String revision) throws IOException {
NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, repoName);
try (RepositoryService repositoryService = serviceFactory.create(namespaceAndName)) {
BrowseCommandBuilder browseCommand = repositoryService.getBrowseCommand();

View File

@@ -1,7 +0,0 @@
package sonia.scm.api.v2.resources;
import sonia.scm.NotFoundException;
public class TagNotFoundException extends NotFoundException {
}

View File

@@ -3,9 +3,9 @@ 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.repository.NamespaceAndName;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryNotFoundException;
import sonia.scm.repository.RepositoryPermissions;
import sonia.scm.repository.Tag;
import sonia.scm.repository.Tags;
@@ -21,6 +21,9 @@ import javax.ws.rs.Produces;
import javax.ws.rs.core.Response;
import java.io.IOException;
import static sonia.scm.ContextEntry.ContextBuilder.entity;
import static sonia.scm.NotFoundException.notFound;
public class TagRootResource {
private final RepositoryServiceFactory serviceFactory;
@@ -47,7 +50,7 @@ public class TagRootResource {
})
@Produces(VndMediaType.TAG_COLLECTION)
@TypeHint(CollectionDto.class)
public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name) throws IOException, RepositoryNotFoundException {
public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name) throws IOException {
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
Tags tags = getTags(repositoryService);
if (tags != null && tags.getTags() != null) {
@@ -72,7 +75,7 @@ public class TagRootResource {
@Produces(VndMediaType.TAG)
@TypeHint(TagDto.class)
@Path("{tagName}")
public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("tagName") String tagName) throws IOException, RepositoryNotFoundException, TagNotFoundException {
public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("tagName") String tagName) throws IOException {
NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name);
try (RepositoryService repositoryService = serviceFactory.create(namespaceAndName)) {
Tags tags = getTags(repositoryService);
@@ -80,7 +83,7 @@ public class TagRootResource {
Tag tag = tags.getTags().stream()
.filter(t -> tagName.equals(t.getName()))
.findFirst()
.orElseThrow(TagNotFoundException::new);
.orElseThrow(() -> createNotFoundException(namespace, name, tagName));
return Response.ok(tagToTagDtoMapper.map(tag, namespaceAndName)).build();
} else {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
@@ -90,6 +93,10 @@ public class TagRootResource {
}
}
private NotFoundException createNotFoundException(String namespace, String name, String tagName) {
return notFound(entity("Tag", tagName).in("Repository", namespace + "/" + name));
}
private Tags getTags(RepositoryService repositoryService) throws IOException {
Repository repository = repositoryService.getRepository();
RepositoryPermissions.read(repository).check();

View File

@@ -5,6 +5,7 @@ import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import com.webcohesion.enunciate.metadata.rs.TypeHint;
import sonia.scm.plugin.PluginLoader;
import sonia.scm.plugin.PluginWrapper;
import sonia.scm.security.AllowAnonymousAccess;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
@@ -17,6 +18,7 @@ import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@AllowAnonymousAccess
public class UIPluginResource {
private final PluginLoader pluginLoader;

View File

@@ -6,12 +6,12 @@ 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;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.inject.Named;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.DefaultValue;
@@ -76,7 +76,7 @@ public class UserCollectionResource {
*
* <strong>Note:</strong> This method requires "user" privilege.
*
* @param userDto The user to be created.
* @param user The user to be created.
* @return A response with the link to the new user (if created successfully).
*/
@POST
@@ -91,7 +91,7 @@ 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, passwordService.encryptPassword(userDto.getPassword())), user -> resourceLinks.user().self(user.getName()));
public Response create(@Valid UserDto user) {
return adapter.create(user, () -> dtoToUserMapper.map(user, passwordService.encryptPassword(user.getPassword())), u -> resourceLinks.user().self(u.getName()));
}
}

View File

@@ -4,12 +4,12 @@ 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.user.User;
import sonia.scm.user.UserManager;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.inject.Named;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
@@ -87,7 +87,7 @@ public class UserResource {
* <strong>Note:</strong> This method requires "user" privilege.
*
* @param name name of the user to be modified
* @param userDto user object to modify
* @param user user object to modify
*/
@PUT
@Path("")
@@ -101,8 +101,8 @@ public class UserResource {
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(TypeHint.NO_CONTENT.class)
public Response update(@PathParam("id") String name, @Valid UserDto userDto) throws ConcurrentModificationException {
return adapter.update(name, existing -> dtoToUserMapper.map(userDto, existing.getPassword()));
public Response update(@PathParam("id") String name, @Valid UserDto user) {
return adapter.update(name, existing -> dtoToUserMapper.map(user, existing.getPassword()));
}
/**
@@ -114,7 +114,7 @@ public class UserResource {
* <strong>Note:</strong> This method requires "user:changeOwnPassword" privilege to modify the own password.
*
* @param name name of the user to be modified
* @param passwordOverwriteDto change password object to modify password. the old password is here not required
* @param passwordOverwrite change password object to modify password. the old password is here not required
*/
@PUT
@Path("password")
@@ -128,8 +128,8 @@ public class UserResource {
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(TypeHint.NO_CONTENT.class)
public Response overwritePassword(@PathParam("id") String name, @Valid PasswordOverwriteDto passwordOverwriteDto) {
userManager.overwritePassword(name, passwordService.encryptPassword(passwordOverwriteDto.getNewPassword()));
public Response overwritePassword(@PathParam("id") String name, @Valid PasswordOverwriteDto passwordOverwrite) {
userManager.overwritePassword(name, passwordService.encryptPassword(passwordOverwrite.getNewPassword()));
return Response.noContent().build();
}
}

View File

@@ -0,0 +1,54 @@
package sonia.scm.api.v2.resources;
import org.jboss.resteasy.api.validation.ResteasyViolationException;
import org.mapstruct.AfterMapping;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.MappingTarget;
import org.slf4j.MDC;
import javax.validation.ConstraintViolation;
import java.util.List;
import java.util.stream.Collectors;
@Mapper
public abstract class ViolationExceptionToErrorDtoMapper {
@Mapping(target = "errorCode", ignore = true)
@Mapping(target = "transactionId", ignore = true)
@Mapping(target = "context", ignore = true)
@Mapping(target = "url", ignore = true)
public abstract ErrorDto map(ResteasyViolationException exception);
@AfterMapping
void setTransactionId(@MappingTarget ErrorDto dto) {
dto.setTransactionId(MDC.get("transaction_id"));
}
@AfterMapping
void mapViolations(ResteasyViolationException exception, @MappingTarget ErrorDto dto) {
List<ErrorDto.ConstraintViolationDto> violations =
exception.getConstraintViolations()
.stream()
.map(this::createViolationDto)
.collect(Collectors.toList());
dto.setViolations(violations);
}
private ErrorDto.ConstraintViolationDto createViolationDto(ConstraintViolation<?> violation) {
ErrorDto.ConstraintViolationDto constraintViolationDto = new ErrorDto.ConstraintViolationDto();
constraintViolationDto.setMessage(violation.getMessage());
constraintViolationDto.setPath(violation.getPropertyPath().toString());
return constraintViolationDto;
}
@AfterMapping
void setErrorCode(@MappingTarget ErrorDto dto) {
dto.setErrorCode("1wR7ZBe7H1");
}
@AfterMapping
void setMessage(@MappingTarget ErrorDto dto) {
dto.setMessage("input violates conditions (see violation list)");
}
}

View File

@@ -34,22 +34,19 @@ package sonia.scm.boot;
//~--- non-JDK imports --------------------------------------------------------
import com.github.legman.Subscribe;
import com.google.inject.servlet.GuiceFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.SCMContext;
import sonia.scm.Stage;
import sonia.scm.event.ScmEventBus;
//~--- JDK imports ------------------------------------------------------------
import javax.servlet.FilterConfig;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletException;
//~--- JDK imports ------------------------------------------------------------
/**
*
* @author Sebastian Sdorra
@@ -65,6 +62,8 @@ public class BootstrapContextFilter extends GuiceFilter
//~--- methods --------------------------------------------------------------
private final BootstrapContextListener listener = new BootstrapContextListener();
/**
* Restart the whole webapp context.
*
@@ -85,29 +84,20 @@ public class BootstrapContextFilter extends GuiceFilter
}
else
{
logger.warn(
"destroy filter pipeline, because of a received restart event");
logger.warn("destroy filter pipeline, because of a received restart event");
destroy();
logger.warn(
"reinitialize filter pipeline, because of a received restart event");
super.init(filterConfig);
logger.warn("reinitialize filter pipeline, because of a received restart event");
initGuice();
}
}
/**
* Method description
*
*
* @param filterConfig
*
* @throws ServletException
*/
@Override
public void init(FilterConfig filterConfig) throws ServletException
{
this.filterConfig = filterConfig;
super.init(filterConfig);
initGuice();
if (SCMContext.getContext().getStage() == Stage.DEVELOPMENT)
{
@@ -116,6 +106,19 @@ public class BootstrapContextFilter extends GuiceFilter
}
}
public void initGuice() throws ServletException {
super.init(filterConfig);
listener.contextInitialized(new ServletContextEvent(filterConfig.getServletContext()));
}
@Override
public void destroy() {
super.destroy();
listener.contextDestroyed(new ServletContextEvent(filterConfig.getServletContext()));
ServletContextCleaner.cleanup(filterConfig.getServletContext());
}
//~--- fields ---------------------------------------------------------------
/** Field description */

View File

@@ -148,13 +148,15 @@ public class BootstrapContextListener implements ServletContextListener
{
context = sce.getServletContext();
PluginIndex index = readCorePluginIndex(context);
File pluginDirectory = getPluginDirectory();
try
{
extractCorePlugins(context, pluginDirectory, index);
if (!isCorePluginExtractionDisabled()) {
extractCorePlugins(context, pluginDirectory);
} else {
logger.info("core plugin extraction is disabled");
}
ClassLoader cl =
ClassLoaders.getContextClassLoader(BootstrapContextListener.class);
@@ -181,31 +183,8 @@ public class BootstrapContextListener implements ServletContextListener
}
}
/**
* Restart the whole webapp context.
*
*
* @param event restart event
*/
@Subscribe
public void handleRestartEvent(RestartEvent event)
{
logger.warn("received restart event from {} with reason: {}",
event.getCause(), event.getReason());
if (context == null)
{
logger.error("context is null, scm-manager is not initialized");
}
else
{
ServletContextEvent sce = new ServletContextEvent(context);
logger.warn("destroy context, because of a received restart event");
contextDestroyed(sce);
logger.warn("reinitialize context, because of a received restart event");
contextInitialized(sce);
}
private boolean isCorePluginExtractionDisabled() {
return Boolean.getBoolean("sonia.scm.boot.disable-core-plugin-extraction");
}
/**
@@ -214,7 +193,6 @@ public class BootstrapContextListener implements ServletContextListener
*
* @param context
* @param pluginDirectory
* @param name
* @param entry
*
* @throws IOException
@@ -269,17 +247,15 @@ public class BootstrapContextListener implements ServletContextListener
*
* @param context
* @param pluginDirectory
* @param lines
* @param index
*
* @throws IOException
*/
private void extractCorePlugins(ServletContext context, File pluginDirectory,
PluginIndex index)
throws IOException
private void extractCorePlugins(ServletContext context, File pluginDirectory) throws IOException
{
IOUtil.mkdirs(pluginDirectory);
PluginIndex index = readCorePluginIndex(context);
for (PluginIndexEntry entry : index)
{
extractCorePlugin(context, pluginDirectory, entry);

View File

@@ -0,0 +1,97 @@
package sonia.scm.boot;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.Priority;
import sonia.scm.SCMContext;
import sonia.scm.Stage;
import sonia.scm.event.ScmEventBus;
import sonia.scm.filter.WebElement;
import javax.inject.Inject;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* This servlet sends a {@link RestartEvent} to the {@link ScmEventBus} which causes scm-manager to restart the context.
* The {@link RestartServlet} can be used for reloading java code or for installing plugins without a complete restart.
* At the moment the Servlet accepts only request, if scm-manager was started in the {@link Stage#DEVELOPMENT} stage.
*
* @since 2.0.0
*/
@Priority(0)
@WebElement("/restart")
public class RestartServlet extends HttpServlet {
private static final Logger LOG = LoggerFactory.getLogger(RestartServlet.class);
private final ObjectMapper objectMapper = new ObjectMapper();
private final AtomicBoolean restarting = new AtomicBoolean();
private final ScmEventBus eventBus;
private final Stage stage;
@Inject
public RestartServlet() {
this(ScmEventBus.getInstance(), SCMContext.getContext().getStage());
}
RestartServlet(ScmEventBus eventBus, Stage stage) {
this.eventBus = eventBus;
this.stage = stage;
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
LOG.info("received sendRestartEvent request");
if (isRestartAllowed()) {
try (InputStream requestInput = req.getInputStream()) {
Reason reason = objectMapper.readValue(requestInput, Reason.class);
sendRestartEvent(resp, reason);
} catch (IOException ex) {
LOG.warn("failed to trigger sendRestartEvent event", ex);
resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
} else {
LOG.debug("received restart event in non development stage");
resp.setStatus(HttpServletResponse.SC_NOT_ACCEPTABLE);
}
}
private boolean isRestartAllowed() {
return stage == Stage.DEVELOPMENT;
}
private void sendRestartEvent(HttpServletResponse response, Reason reason) {
if ( restarting.compareAndSet(false, true) ) {
LOG.info("trigger sendRestartEvent, because of {}", reason.getMessage());
eventBus.post(new RestartEvent(RestartServlet.class, reason.getMessage()));
response.setStatus(HttpServletResponse.SC_ACCEPTED);
} else {
LOG.warn("scm-manager restarts already");
response.setStatus(HttpServletResponse.SC_CONFLICT);
}
}
public static class Reason {
private String message;
public void setMessage(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}
}

View File

@@ -0,0 +1,59 @@
package sonia.scm.boot;
import com.google.common.collect.ImmutableSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.ServletContext;
import java.util.Enumeration;
import java.util.Set;
/**
* Remove cached resources from {@link ServletContext} to allow a clean restart of scm-manager without stale or
* duplicated data.
*
* @since 2.0.0
*/
final class ServletContextCleaner {
private static final Logger LOG = LoggerFactory.getLogger(ServletContextCleaner.class);
private static final Set<String> REMOVE_PREFIX = ImmutableSet.of(
"org.jboss.resteasy",
"resteasy",
"org.apache.shiro",
"sonia.scm"
);
private ServletContextCleaner() {
}
/**
* Remove cached attributes from {@link ServletContext}.
*
* @param servletContext servlet context
*/
static void cleanup(ServletContext servletContext) {
LOG.info("remove cached attributes from context");
Enumeration<String> attributeNames = servletContext.getAttributeNames();
while( attributeNames.hasMoreElements()) {
String name = attributeNames.nextElement();
if (shouldRemove(name)) {
LOG.info("remove attribute {} from servlet context", name);
servletContext.removeAttribute(name);
} else {
LOG.info("keep attribute {} in servlet context", name);
}
}
}
private static boolean shouldRemove(String name) {
for (String prefix : REMOVE_PREFIX) {
if (name.startsWith(prefix)) {
return true;
}
}
return false;
}
}

View File

@@ -1,90 +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.filter;
//~--- non-JDK imports --------------------------------------------------------
import com.google.inject.Inject;
import org.apache.shiro.subject.Subject;
import sonia.scm.Priority;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.security.Role;
/**
* Security filter which allow only administrators to access the underlying
* resources.
*
* @author Sebastian Sdorra
*/
// TODO before releasing v2, delete this filter (we use Permission objects now)
@WebElement(
value = Filters.PATTERN_CONFIG,
morePatterns = {
Filters.PATTERN_USERS,
Filters.PATTERN_GROUPS,
Filters.PATTERN_PLUGINS
}
)
@Priority(Filters.PRIORITY_AUTHORIZATION + 1)
public class AdminSecurityFilter extends SecurityFilter
{
/**
* Constructs a new instance.
*
* @param configuration scm-manager main configuration
*/
@Inject
public AdminSecurityFilter(ScmConfiguration configuration)
{
super(configuration);
}
//~--- get methods ----------------------------------------------------------
/**
* Returns {@code true} if the subject has the admin role.
*
* @param subject subject
*
* @return {@code true} if the subject has the admin role
*/
@Override
protected boolean hasPermission(Subject subject)
{
return subject.hasRole(Role.ADMIN);
}
}

View File

@@ -34,7 +34,6 @@ package sonia.scm.filter;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.annotations.VisibleForTesting;
import com.google.inject.Singleton;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
@@ -42,6 +41,8 @@ import org.apache.shiro.subject.Subject;
import org.slf4j.MDC;
import sonia.scm.SCMContext;
import sonia.scm.security.DefaultKeyGenerator;
import sonia.scm.security.KeyGenerator;
import sonia.scm.web.filter.HttpFilter;
//~--- JDK imports ------------------------------------------------------------
@@ -62,27 +63,26 @@ import sonia.scm.Priority;
@WebElement(Filters.PATTERN_ALL)
public class MDCFilter extends HttpFilter
{
private static final DefaultKeyGenerator TRANSACTION_KEY_GENERATOR = new DefaultKeyGenerator();
/** Field description */
@VisibleForTesting
static final String MDC_CLIEN_HOST = "client_host";
static final String MDC_CLIENT_HOST = "client_host";
/** Field description */
@VisibleForTesting
static final String MDC_CLIEN_IP = "client_ip";
/** url of the current request */
static final String MDC_CLIENT_IP = "client_ip";
@VisibleForTesting
static final String MDC_REQUEST_URI = "request_uri";
/** request method */
@VisibleForTesting
static final String MDC_REQUEST_METHOD = "request_method";
/** Field description */
@VisibleForTesting
static final String MDC_USERNAME = "username";
@VisibleForTesting
static final String MDC_TRANSACTION_ID = "transaction_id";
//~--- methods --------------------------------------------------------------
/**
@@ -102,10 +102,11 @@ public class MDCFilter extends HttpFilter
throws IOException, ServletException
{
MDC.put(MDC_USERNAME, getUsername());
MDC.put(MDC_CLIEN_IP, request.getRemoteAddr());
MDC.put(MDC_CLIEN_HOST, request.getRemoteHost());
MDC.put(MDC_CLIENT_IP, request.getRemoteAddr());
MDC.put(MDC_CLIENT_HOST, request.getRemoteHost());
MDC.put(MDC_REQUEST_METHOD, request.getMethod());
MDC.put(MDC_REQUEST_URI, request.getRequestURI());
MDC.put(MDC_TRANSACTION_ID, TRANSACTION_KEY_GENERATOR.createKey());
try
{
@@ -114,10 +115,11 @@ public class MDCFilter extends HttpFilter
finally
{
MDC.remove(MDC_USERNAME);
MDC.remove(MDC_CLIEN_IP);
MDC.remove(MDC_CLIEN_HOST);
MDC.remove(MDC_CLIENT_IP);
MDC.remove(MDC_CLIENT_HOST);
MDC.remove(MDC_REQUEST_METHOD);
MDC.remove(MDC_REQUEST_URI);
MDC.remove(MDC_TRANSACTION_ID);
}
}

View File

@@ -42,9 +42,8 @@ import org.apache.shiro.subject.Subject;
import sonia.scm.Priority;
import sonia.scm.SCMContext;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.security.SecurityRequests;
import sonia.scm.web.filter.HttpFilter;
import sonia.scm.web.filter.SecurityHttpServletRequestWrapper;
import sonia.scm.web.filter.PropagatePrincipleServletRequestWrapper;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
@@ -61,10 +60,7 @@ import static sonia.scm.api.v2.resources.ScmPathInfo.REST_API_PATH;
* @author Sebastian Sdorra
*/
@Priority(Filters.PRIORITY_AUTHORIZATION)
// TODO find a better way for unprotected resources
@WebElement(value = REST_API_PATH + "" +
"/(?!v2/ui).*", regex = true)
public class SecurityFilter extends HttpFilter
public class PropagatePrincipleFilter extends HttpFilter
{
/** name of request attribute for the primary principal */
@@ -74,7 +70,7 @@ public class SecurityFilter extends HttpFilter
private final ScmConfiguration configuration;
@Inject
public SecurityFilter(ScmConfiguration configuration)
public PropagatePrincipleFilter(ScmConfiguration configuration)
{
this.configuration = configuration;
}
@@ -84,31 +80,16 @@ public class SecurityFilter extends HttpFilter
HttpServletResponse response, FilterChain chain)
throws IOException, ServletException
{
if (!SecurityRequests.isAuthenticationRequest(request) && !SecurityRequests.isIndexRequest(request))
Subject subject = SecurityUtils.getSubject();
if (hasPermission(subject))
{
Subject subject = SecurityUtils.getSubject();
if (hasPermission(subject))
{
// add primary principal as request attribute
// see https://goo.gl/JRjNmf
String username = getUsername(subject);
request.setAttribute(ATTRIBUTE_REMOTE_USER, username);
// add primary principal as request attribute
// see https://goo.gl/JRjNmf
String username = getUsername(subject);
request.setAttribute(ATTRIBUTE_REMOTE_USER, username);
// wrap servlet request to provide authentication informations
chain.doFilter(new SecurityHttpServletRequestWrapper(request, username), response);
}
else if (subject.isAuthenticated() || subject.isRemembered())
{
response.sendError(HttpServletResponse.SC_FORBIDDEN);
}
else if (configuration.isAnonymousAccessEnabled())
{
response.sendError(HttpServletResponse.SC_FORBIDDEN);
}
else
{
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
// wrap servlet request to provide authentication information
chain.doFilter(new PropagatePrincipleServletRequestWrapper(request, username), response);
}
else
{
@@ -116,7 +97,7 @@ public class SecurityFilter extends HttpFilter
}
}
protected boolean hasPermission(Subject subject)
private boolean hasPermission(Subject subject)
{
return ((configuration != null)
&& configuration.isAnonymousAccessEnabled()) || subject.isAuthenticated()
@@ -139,5 +120,4 @@ public class SecurityFilter extends HttpFilter
return username;
}
}

View File

@@ -42,7 +42,6 @@ import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.AlreadyExistsException;
import sonia.scm.HandlerEventType;
import sonia.scm.ManagerDaoAdapter;
import sonia.scm.NotFoundException;
@@ -106,7 +105,7 @@ public class DefaultGroupManager extends AbstractGroupManager
}
@Override
public Group create(Group group) throws AlreadyExistsException {
public Group create(Group group) {
String type = group.getType();
if (Util.isEmpty(type)) {
group.setType(groupDAO.getType());
@@ -172,7 +171,7 @@ public class DefaultGroupManager extends AbstractGroupManager
if (fresh == null)
{
throw new NotFoundException("group", group.getId());
throw new NotFoundException(Group.class, group.getId());
}
fresh.copyProperties(group);

View File

@@ -41,6 +41,8 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableList.Builder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.SCMContext;
import sonia.scm.Stage;
import javax.servlet.ServletContext;
import java.net.MalformedURLException;
@@ -69,19 +71,21 @@ public class DefaultUberWebResourceLoader implements UberWebResourceLoader
//~--- constructors ---------------------------------------------------------
/**
* Constructs ...
*
*
* @param servletContext
* @param plugins
*/
public DefaultUberWebResourceLoader(ServletContext servletContext,
Iterable<PluginWrapper> plugins)
{
public DefaultUberWebResourceLoader(ServletContext servletContext, Iterable<PluginWrapper> plugins) {
this(servletContext, plugins, SCMContext.getContext().getStage());
}
public DefaultUberWebResourceLoader(ServletContext servletContext, Iterable<PluginWrapper> plugins, Stage stage) {
this.servletContext = servletContext;
this.plugins = plugins;
this.cache = CacheBuilder.newBuilder().build();
this.cache = createCache(stage);
}
private Cache<String, URL> createCache(Stage stage) {
if (stage == Stage.DEVELOPMENT) {
return CacheBuilder.newBuilder().maximumSize(0).build(); // Disable caching
}
return CacheBuilder.newBuilder().build();
}
//~--- get methods ----------------------------------------------------------
@@ -97,7 +101,7 @@ public class DefaultUberWebResourceLoader implements UberWebResourceLoader
@Override
public URL getResource(String path)
{
URL resource = cache.getIfPresent(path);
URL resource = getFromCache(path);
if (resource == null)
{
@@ -105,7 +109,7 @@ public class DefaultUberWebResourceLoader implements UberWebResourceLoader
if (resource != null)
{
cache.put(path, resource);
addToCache(path, resource);
}
}
else
@@ -116,6 +120,14 @@ public class DefaultUberWebResourceLoader implements UberWebResourceLoader
return resource;
}
private URL getFromCache(String path) {
return cache.getIfPresent(path);
}
private void addToCache(String path, URL url) {
cache.put(path, url);
}
/**
* Method description
*

View File

@@ -65,6 +65,9 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import static sonia.scm.ContextEntry.ContextBuilder.entity;
import static sonia.scm.NotFoundException.notFound;
/**
* Default implementation of {@link RepositoryManager}.
*
@@ -122,11 +125,11 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager {
}
@Override
public Repository create(Repository repository) throws AlreadyExistsException {
public Repository create(Repository repository) {
return create(repository, true);
}
public Repository create(Repository repository, boolean initRepository) throws AlreadyExistsException {
public Repository create(Repository repository, boolean initRepository) {
repository.setId(keyGenerator.createKey());
repository.setNamespace(namespaceStrategy.createNamespace(repository));
@@ -140,7 +143,7 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager {
try {
getHandler(newRepository).create(newRepository);
} catch (AlreadyExistsException e) {
throw new InternalRepositoryException("directory for repository does already exist", e);
throw new InternalRepositoryException(repository, "directory for repository does already exist", e);
}
}
fireEvent(HandlerEventType.BEFORE_CREATE, newRepository);
@@ -170,7 +173,7 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager {
}
@Override
public void importRepository(Repository repository) throws AlreadyExistsException {
public void importRepository(Repository repository) {
create(repository, false);
}
@@ -198,7 +201,7 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager {
}
@Override
public void refresh(Repository repository) throws RepositoryNotFoundException {
public void refresh(Repository repository) {
AssertUtil.assertIsNotNull(repository);
RepositoryPermissions.read(repository).check();
@@ -207,7 +210,7 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager {
if (fresh != null) {
fresh.copyProperties(repository);
} else {
throw new RepositoryNotFoundException(repository);
throw notFound(entity(repository));
}
}
@@ -350,9 +353,9 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager {
RepositoryHandler handler = handlerMap.get(type);
if (handler == null) {
throw new InternalRepositoryException("could not find handler for " + type);
throw new InternalRepositoryException(entity(repository), "could not find handler for " + type);
} else if (!handler.isConfigured()) {
throw new InternalRepositoryException("handler is not configured for type " + type);
throw new InternalRepositoryException(entity(repository), "handler is not configured for type " + type);
}
return handler;

View File

@@ -33,7 +33,6 @@ import com.google.common.collect.ImmutableList;
import com.google.inject.Inject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.ConcurrentModificationException;
import sonia.scm.NotFoundException;
import java.util.Set;
@@ -61,7 +60,7 @@ public final class HealthChecker {
Repository repository = repositoryManager.get(id);
if (repository == null) {
throw new RepositoryNotFoundException(id);
throw new NotFoundException(Repository.class, id);
}
doCheck(repository);

View File

@@ -167,7 +167,7 @@ public class AuthorizationChangedEventProducer {
private boolean isAuthorizationDataModified(Repository repository, Repository beforeModification) {
return repository.isArchived() != beforeModification.isArchived()
|| repository.isPublicReadable() != beforeModification.isPublicReadable()
|| ! repository.getPermissions().equals(beforeModification.getPermissions());
|| !(repository.getPermissions().containsAll(beforeModification.getPermissions()) && beforeModification.getPermissions().containsAll(repository.getPermissions()));
}
private void fireEventForEveryUser() {

View File

@@ -54,13 +54,14 @@ import sonia.scm.cache.CacheManager;
import sonia.scm.group.GroupNames;
import sonia.scm.group.GroupPermissions;
import sonia.scm.plugin.Extension;
import sonia.scm.repository.Permission;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryDAO;
import sonia.scm.repository.RepositoryPermissions;
import sonia.scm.user.User;
import sonia.scm.user.UserPermissions;
import sonia.scm.util.Util;
import java.util.Collection;
import java.util.List;
import java.util.Set;
@@ -198,7 +199,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
private void collectRepositoryPermissions(Builder<String> builder,
Repository repository, User user, GroupNames groups)
{
List<sonia.scm.repository.Permission> repositoryPermissions
Collection<Permission> repositoryPermissions
= repository.getPermissions();
if (Util.isNotEmpty(repositoryPermissions))

View File

@@ -0,0 +1,45 @@
package sonia.scm.security;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.container.ResourceInfo;
import javax.ws.rs.core.Context;
import javax.ws.rs.ext.Provider;
import java.lang.reflect.Method;
@Provider
public class SecurityRequestFilter implements ContainerRequestFilter {
private static final Logger LOG = LoggerFactory.getLogger(SecurityRequestFilter.class);
@Context
private ResourceInfo resourceInfo;
@Override
public void filter(ContainerRequestContext requestContext) {
Method resourceMethod = resourceInfo.getResourceMethod();
if (hasPermission() || anonymousAccessIsAllowed(resourceMethod)) {
LOG.debug("allowed unauthenticated request to method {}", resourceMethod);
// nothing further to do
} else {
LOG.debug("blocked unauthenticated request to method {}", resourceMethod);
throw new AuthenticationException();
}
}
private boolean anonymousAccessIsAllowed(Method method) {
return method.isAnnotationPresent(AllowAnonymousAccess.class)
|| method.getDeclaringClass().isAnnotationPresent(AllowAnonymousAccess.class);
}
private boolean hasPermission() {
Subject subject = SecurityUtils.getSubject();
return subject.isAuthenticated() || subject.isRemembered();
}
}

View File

@@ -39,7 +39,7 @@ import com.google.inject.Singleton;
import org.apache.shiro.SecurityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.AlreadyExistsException;
import sonia.scm.ContextEntry;
import sonia.scm.EagerSingleton;
import sonia.scm.HandlerEventType;
import sonia.scm.ManagerDaoAdapter;
@@ -137,7 +137,7 @@ public class DefaultUserManager extends AbstractUserManager
* @throws IOException
*/
@Override
public User create(User user) throws AlreadyExistsException {
public User create(User user) {
String type = user.getType();
if (Util.isEmpty(type)) {
user.setType(userDAO.getType());
@@ -219,7 +219,7 @@ public class DefaultUserManager extends AbstractUserManager
if (fresh == null)
{
throw new NotFoundException();
throw new NotFoundException(User.class, user.getName());
}
fresh.copyProperties(user);
@@ -403,7 +403,7 @@ public class DefaultUserManager extends AbstractUserManager
User user = get((String) SecurityUtils.getSubject().getPrincipals().getPrimaryPrincipal());
if (!user.getPassword().equals(oldPassword)) {
throw new InvalidPasswordException();
throw new InvalidPasswordException(ContextEntry.ContextBuilder.entity("passwordChange", "-").in(User.class, user.getName()));
}
user.setPassword(newPassword);
@@ -419,10 +419,10 @@ public class DefaultUserManager extends AbstractUserManager
public void overwritePassword(String userId, String newPassword) {
User user = get(userId);
if (user == null) {
throw new NotFoundException();
throw new NotFoundException(User.class, userId);
}
if (!isTypeDefault(user)) {
throw new ChangePasswordNotAllowedException(user.getType());
throw new ChangePasswordNotAllowedException(ContextEntry.ContextBuilder.entity("passwordChange", "-").in(User.class, user.getName()), user.getType());
}
user.setPassword(newPassword);
this.modify(user);

View File

@@ -30,6 +30,9 @@ import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.function.Function;
import static sonia.scm.ContextEntry.ContextBuilder.entity;
import static sonia.scm.NotFoundException.notFound;
/**
* Collect the plugin translations.
@@ -69,7 +72,7 @@ public class I18nServlet extends HttpServlet {
createdFile.ifPresent(map -> createdJsonFileConsumer.accept(path, map));
return createdFile.orElse(null);
}
)).orElseThrow(NotFoundException::new);
)).orElseThrow(() -> notFound(entity("jsonprovider", path)));
}
@VisibleForTesting

View File

@@ -4,11 +4,11 @@ import com.google.inject.Inject;
import com.google.inject.Singleton;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpStatus;
import sonia.scm.NotFoundException;
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;
@@ -71,8 +71,8 @@ public class HttpProtocolServlet extends HttpServlet {
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);
} catch (NotFoundException e) {
log.debug(e.getMessage());
resp.setStatus(HttpStatus.SC_NOT_FOUND);
}
}