Merged 2.0.0-m3

This commit is contained in:
Philipp Czora
2018-11-23 16:25:12 +01:00
111 changed files with 2400 additions and 3541 deletions

View File

@@ -741,7 +741,7 @@
<properties>
<!-- test libraries -->
<mockito.version>2.10.0</mockito.version>
<mockito.version>2.23.0</mockito.version>
<hamcrest.version>1.3</hamcrest.version>
<junit.version>5.2.0</junit.version>

View File

@@ -0,0 +1,15 @@
package sonia.scm.security;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Use this annotation to mark REST resource methods that may be accessed <b>without</b> authentication.
* To mark all methods of a complete class you can annotate the class instead.
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AllowAnonymousAccess {
}

View File

@@ -45,28 +45,12 @@ public final class Filters
/** Field description */
public static final String PATTERN_ALL = "/*";
/** Field description */
public static final String PATTERN_CONFIG = REST_API_PATH + "/config*";
/** Field description */
public static final String PATTERN_DEBUG = "/debug.html";
/** Field description */
public static final String PATTERN_GROUPS = REST_API_PATH + "/groups*";
/** Field description */
public static final String PATTERN_PLUGINS = REST_API_PATH + "/plugins*";
/** Field description */
public static final String PATTERN_RESOURCE_REGEX =
"^/(?:resources|api|plugins|index)[\\./].*(?:html|\\.css|\\.js|\\.xml|\\.json|\\.txt)";
/** Field description */
public static final String PATTERN_RESTAPI = REST_API_PATH + "/*";
/** Field description */
public static final String PATTERN_USERS = REST_API_PATH + "/users*";
/** authentication priority */
public static final int PRIORITY_AUTHENTICATION = 5000;

View File

@@ -37,7 +37,6 @@ import com.github.sdorra.ssp.PermissionObject;
import com.github.sdorra.ssp.StaticPermissions;
import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import com.google.common.collect.Lists;
import sonia.scm.BasicPropertiesAware;
import sonia.scm.ModelObject;
import sonia.scm.util.Util;
@@ -50,8 +49,11 @@ import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlTransient;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Source code repository.
@@ -79,7 +81,7 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
private Long lastModified;
private String namespace;
private String name;
private List<Permission> permissions;
private final Set<Permission> permissions = new HashSet<>();
@XmlElement(name = "public")
private boolean publicReadable = false;
private boolean archived = false;
@@ -127,7 +129,6 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
this.name = name;
this.contact = contact;
this.description = description;
this.permissions = Lists.newArrayList();
if (Util.isNotEmpty(permissions)) {
this.permissions.addAll(Arrays.asList(permissions));
@@ -200,12 +201,8 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
return new NamespaceAndName(getNamespace(), getName());
}
public List<Permission> getPermissions() {
if (permissions == null) {
permissions = Lists.newArrayList();
}
return permissions;
public Collection<Permission> getPermissions() {
return Collections.unmodifiableCollection(permissions);
}
/**
@@ -300,8 +297,17 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
this.name = name;
}
public void setPermissions(List<Permission> permissions) {
this.permissions = permissions;
public void setPermissions(Collection<Permission> permissions) {
this.permissions.clear();
this.permissions.addAll(permissions);
}
public void addPermission(Permission newPermission) {
this.permissions.add(newPermission);
}
public void removePermission(Permission permission) {
this.permissions.remove(permission);
}
public void setPublicReadable(boolean publicReadable) {

View File

@@ -66,6 +66,10 @@ public enum Command
/**
* @since 2.0
*/
MODIFICATIONS
MODIFICATIONS,
/**
* @since 2.0
*/
MERGE
}

View File

@@ -0,0 +1,143 @@
package sonia.scm.repository.api;
import com.google.common.base.Preconditions;
import sonia.scm.repository.Person;
import sonia.scm.repository.spi.MergeCommand;
import sonia.scm.repository.spi.MergeCommandRequest;
/**
* Use this {@link MergeCommandBuilder} to merge two branches of a repository ({@link #executeMerge()}) or to check if
* the branches could be merged without conflicts ({@link #dryRun()}). To do so, you have to specify the name of
* the target branch ({@link #setTargetBranch(String)}) and the name of the branch that should be merged
* ({@link #setBranchToMerge(String)}). Additionally you can specify an author that should be used for the commit
* ({@link #setAuthor(Person)}) and a message template ({@link #setMessageTemplate(String)}) if you are not doing a dry
* run only. If no author is specified, the logged in user and a default message will be used instead.
*
* To actually merge <code>feature_branch</code> into <code>integration_branch</code> do this:
* <pre><code>
* repositoryService.gerMergeCommand()
* .setBranchToMerge("feature_branch")
* .setTargetBranch("integration_branch")
* .executeMerge();
* </code></pre>
*
* If the merge is successful, the result will look like this:
* <pre><code>
* O <- Merge result (new head of integration_branch)
* |\
* | \
* old integration_branch -> O O <- feature_branch
* | |
* O O
* </code></pre>
*
* To check whether they can be merged without conflicts beforehand do this:
* <pre><code>
* repositoryService.gerMergeCommand()
* .setBranchToMerge("feature_branch")
* .setTargetBranch("integration_branch")
* .dryRun()
* .isMergeable();
* </code></pre>
*
* Keep in mind that you should <em>always</em> check the result of a merge even though you may have done a dry run
* beforehand, because the branches can change between the dry run and the actual merge.
*
* @since 2.0.0
*/
public class MergeCommandBuilder {
private final MergeCommand mergeCommand;
private final MergeCommandRequest request = new MergeCommandRequest();
MergeCommandBuilder(MergeCommand mergeCommand) {
this.mergeCommand = mergeCommand;
}
/**
* Use this to set the branch that should be merged into the target branch.
*
* <b>This is mandatory.</b>
*
* @return This builder instance.
*/
public MergeCommandBuilder setBranchToMerge(String branchToMerge) {
request.setBranchToMerge(branchToMerge);
return this;
}
/**
* Use this to set the target branch the other branch should be merged into.
*
* <b>This is mandatory.</b>
*
* @return This builder instance.
*/
public MergeCommandBuilder setTargetBranch(String targetBranch) {
request.setTargetBranch(targetBranch);
return this;
}
/**
* Use this to set the author of the merge commit manually. If this is omitted, the currently logged in user will be
* used instead.
*
* This is optional and for {@link #executeMerge()} only.
*
* @return This builder instance.
*/
public MergeCommandBuilder setAuthor(Person author) {
request.setAuthor(author);
return this;
}
/**
* Use this to set a template for the commit message. If no message is set, a default message will be used.
*
* You can use the placeholder <code>{0}</code> for the branch to be merged and <code>{1}</code> for the target
* branch, eg.:
*
* <pre><code>
* ...setMessageTemplate("Merge of {0} into {1}")...
* </code></pre>
*
* This is optional and for {@link #executeMerge()} only.
*
* @return This builder instance.
*/
public MergeCommandBuilder setMessageTemplate(String messageTemplate) {
request.setMessageTemplate(messageTemplate);
return this;
}
/**
* Use this to reset the command.
* @return This builder instance.
*/
public MergeCommandBuilder reset() {
request.reset();
return this;
}
/**
* Use this to actually do the merge. If an automatic merge is not possible, {@link MergeCommandResult#isSuccess()}
* will return <code>false</code>.
*
* @return The result of the merge.
*/
public MergeCommandResult executeMerge() {
Preconditions.checkArgument(request.isValid(), "revision to merge and target revision is required");
return mergeCommand.merge(request);
}
/**
* Use this to check whether the given branches can be merged autmatically. If this is possible,
* {@link MergeDryRunCommandResult#isMergeable()} will return <code>true</code>.
*
* @return The result whether the given branches can be merged automatically.
*/
public MergeDryRunCommandResult dryRun() {
Preconditions.checkArgument(request.isValid(), "revision to merge and target revision is required");
return mergeCommand.dryRun(request);
}
}

View File

@@ -0,0 +1,44 @@
package sonia.scm.repository.api;
import java.util.Collection;
import java.util.HashSet;
import static java.util.Collections.emptyList;
import static java.util.Collections.unmodifiableCollection;
/**
* This class keeps the result of a merge of branches. Use {@link #isSuccess()} to check whether the merge was
* sucessfully executed. If the result is <code>false</code> the merge could not be done without conflicts. In this
* case you can use {@link #getFilesWithConflict()} to get a list of files with merge conflicts.
*/
public class MergeCommandResult {
private final Collection<String> filesWithConflict;
private MergeCommandResult(Collection<String> filesWithConflict) {
this.filesWithConflict = filesWithConflict;
}
public static MergeCommandResult success() {
return new MergeCommandResult(emptyList());
}
public static MergeCommandResult failure(Collection<String> filesWithConflict) {
return new MergeCommandResult(new HashSet<>(filesWithConflict));
}
/**
* If this returns <code>true</code>, the merge was successfull. If this returns <code>false</code> there were
* merge conflicts. In this case you can use {@link #getFilesWithConflict()} to check what files could not be merged.
*/
public boolean isSuccess() {
return filesWithConflict.isEmpty();
}
/**
* If the merge was not successful ({@link #isSuccess()} returns <code>false</code>) this will give you a list of
* file paths that could not be merged automatically.
*/
public Collection<String> getFilesWithConflict() {
return unmodifiableCollection(filesWithConflict);
}
}

View File

@@ -0,0 +1,22 @@
package sonia.scm.repository.api;
/**
* This class keeps the result of a merge dry run. Use {@link #isMergeable()} to check whether an automatic merge is
* possible or not.
*/
public class MergeDryRunCommandResult {
private final boolean mergeable;
public MergeDryRunCommandResult(boolean mergeable) {
this.mergeable = mergeable;
}
/**
* This will return <code>true</code>, when an automatic merge is possible <em>at the moment</em>; <code>false</code>
* otherwise.
*/
public boolean isMergeable() {
return mergeable;
}
}

View File

@@ -79,6 +79,7 @@ import java.util.stream.Stream;
* @apiviz.uses sonia.scm.repository.api.PushCommandBuilder
* @apiviz.uses sonia.scm.repository.api.BundleCommandBuilder
* @apiviz.uses sonia.scm.repository.api.UnbundleCommandBuilder
* @apiviz.uses sonia.scm.repository.api.MergeCommandBuilder
* @since 1.17
*/
@Slf4j
@@ -353,6 +354,22 @@ public final class RepositoryService implements Closeable {
repository);
}
/**
* The merge command executes a merge of two branches. It is possible to do a dry run to check, whether the given
* branches can be merged without conflicts.
*
* @return instance of {@link MergeCommandBuilder}
* @throws CommandNotSupportedException if the command is not supported
* by the implementation of the repository service provider.
* @since 2.0.0
*/
public MergeCommandBuilder gerMergeCommand() {
logger.debug("create unbundle command for repository {}",
repository.getNamespaceAndName());
return new MergeCommandBuilder(provider.getMergeCommand());
}
/**
* Returns true if the command is supported by the repository service.
*

View File

@@ -0,0 +1,10 @@
package sonia.scm.repository.spi;
import sonia.scm.repository.api.MergeCommandResult;
import sonia.scm.repository.api.MergeDryRunCommandResult;
public interface MergeCommand {
MergeCommandResult merge(MergeCommandRequest request);
MergeDryRunCommandResult dryRun(MergeCommandRequest request);
}

View File

@@ -0,0 +1,93 @@
package sonia.scm.repository.spi;
import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import com.google.common.base.Strings;
import sonia.scm.Validateable;
import sonia.scm.repository.Person;
import sonia.scm.util.Util;
import java.io.Serializable;
public class MergeCommandRequest implements Validateable, Resetable, Serializable, Cloneable {
private static final long serialVersionUID = -2650236557922431528L;
private String branchToMerge;
private String targetBranch;
private Person author;
private String messageTemplate;
public String getBranchToMerge() {
return branchToMerge;
}
public void setBranchToMerge(String branchToMerge) {
this.branchToMerge = branchToMerge;
}
public String getTargetBranch() {
return targetBranch;
}
public void setTargetBranch(String targetBranch) {
this.targetBranch = targetBranch;
}
public Person getAuthor() {
return author;
}
public void setAuthor(Person author) {
this.author = author;
}
public String getMessageTemplate() {
return messageTemplate;
}
public void setMessageTemplate(String messageTemplate) {
this.messageTemplate = messageTemplate;
}
public boolean isValid() {
return !Strings.isNullOrEmpty(getBranchToMerge())
&& !Strings.isNullOrEmpty(getTargetBranch());
}
public void reset() {
this.setBranchToMerge(null);
this.setTargetBranch(null);
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final MergeCommandRequest other = (MergeCommandRequest) obj;
return Objects.equal(branchToMerge, other.branchToMerge)
&& Objects.equal(targetBranch, other.targetBranch)
&& Objects.equal(author, other.author);
}
@Override
public int hashCode() {
return Objects.hashCode(branchToMerge, targetBranch, author);
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("branchToMerge", branchToMerge)
.add("targetBranch", targetBranch)
.add("author", author)
.toString();
}
}

View File

@@ -251,4 +251,12 @@ public abstract class RepositoryServiceProvider implements Closeable
{
throw new CommandNotSupportedException(Command.UNBUNDLE);
}
/**
* @since 2.0
*/
public MergeCommand getMergeCommand()
{
throw new CommandNotSupportedException(Command.MERGE);
}
}

View File

@@ -301,7 +301,7 @@ public class AuthenticationFilter extends HttpFilter
}
}
chain.doFilter(new SecurityHttpServletRequestWrapper(request, username),
chain.doFilter(new PropagatePrincipleServletRequestWrapper(request, username),
response);
}

View File

@@ -38,37 +38,17 @@ package sonia.scm.web.filter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
/**
*
* @author Sebastian Sdorra
*/
public class SecurityHttpServletRequestWrapper extends HttpServletRequestWrapper
{
public class PropagatePrincipleServletRequestWrapper extends HttpServletRequestWrapper {
/**
* Constructs ...
*
*
* @param request
* @param principal
*/
public SecurityHttpServletRequestWrapper(HttpServletRequest request,
String principal)
{
private final String principal;
public PropagatePrincipleServletRequestWrapper(HttpServletRequest request, String principal) {
super(request);
this.principal = principal;
}
//~--- get methods ----------------------------------------------------------
@Override
public String getRemoteUser()
{
public String getRemoteUser() {
return principal;
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private final String principal;
}

View File

@@ -0,0 +1,26 @@
package sonia.scm.it;
import io.restassured.RestAssured;
import org.junit.Test;
import sonia.scm.it.utils.RestUtil;
import sonia.scm.it.utils.ScmRequests;
import static org.junit.Assert.assertEquals;
public class AnonymousAccessITCase {
@Test
public void shouldAccessIndexResourceWithoutAuthentication() {
ScmRequests.start()
.requestIndexResource()
.assertStatusCode(200);
}
@Test
public void shouldRejectUserResourceWithoutAuthentication() {
assertEquals(401, RestAssured.given()
.when()
.get(RestUtil.REST_BASE_URL.resolve("users/"))
.statusCode());
}
}

View File

@@ -32,6 +32,7 @@
package sonia.scm.it;
import org.apache.http.HttpStatus;
import org.assertj.core.api.Assertions;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -49,8 +50,10 @@ import sonia.scm.web.VndMediaType;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import static java.util.Arrays.asList;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static sonia.scm.it.utils.RepositoryUtil.addAndCommitRandomFile;
@@ -72,7 +75,7 @@ public class PermissionsITCase {
public TemporaryFolder temporaryFolder = new TemporaryFolder();
private final String repositoryType;
private int createdPermissions;
private Collection<String> createdPermissions;
public PermissionsITCase(String repositoryType) {
@@ -94,7 +97,7 @@ public class PermissionsITCase {
TestData.createNotAdminUser(USER_OWNER, USER_PASS);
TestData.createUserPermission(USER_OWNER, PermissionType.OWNER, repositoryType);
TestData.createNotAdminUser(USER_OTHER, USER_PASS);
createdPermissions = 3;
createdPermissions = asList(USER_READ, USER_WRITE, USER_OWNER);
}
@Test
@@ -131,8 +134,8 @@ public class PermissionsITCase {
@Test
public void ownerShouldSeePermissions() {
List<Object> userPermissions = TestData.getUserPermissions(USER_OWNER, USER_PASS, repositoryType);
assertEquals(userPermissions.size(), createdPermissions);
List<Map> userPermissions = TestData.getUserPermissions(USER_OWNER, USER_PASS, repositoryType);
Assertions.assertThat(userPermissions).extracting(e -> e.get("name")).containsAll(createdPermissions);
}
@Test

View File

@@ -38,6 +38,10 @@ public class ScmRequests {
return new ScmRequests();
}
public IndexResponse requestIndexResource() {
return new IndexResponse(applyGETRequest(RestUtil.REST_BASE_URL.toString()));
}
public IndexResponse requestIndexResource(String username, String password) {
setUsername(username);
setPassword(password);

View File

@@ -99,10 +99,10 @@ public class TestData {
;
}
public static List<Object> getUserPermissions(String username, String password, String repositoryType) {
public static List<Map> getUserPermissions(String username, String password, String repositoryType) {
return callUserPermissions(username, password, repositoryType, HttpStatus.SC_OK)
.extract()
.body().jsonPath().getList("_embedded.permissions");
.body().jsonPath().<Map>getList("_embedded.permissions");
}
public static ValidatableResponse callUserPermissions(String username, String password, String repositoryType, int expectedStatusCode) {

View File

@@ -115,12 +115,6 @@
<artifactId>smp-maven-plugin</artifactId>
<version>1.0.0-alpha-3</version>
<extensions>true</extensions>
<configuration>
<links>
<link>@scm-manager/ui-types</link>
<link>@scm-manager/ui-components</link>
</links>
</configuration>
</plugin>
<plugin>

View File

@@ -52,6 +52,10 @@
<extensions>true</extensions>
<configuration>
<corePlugin>true</corePlugin>
<links>
<link>@scm-manager/ui-types</link>
<link>@scm-manager/ui-components</link>
</links>
</configuration>
</plugin>

View File

@@ -1,124 +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 sonia.scm.repository.GitConfig;
import sonia.scm.repository.GitRepositoryHandler;
//~--- 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/repositories/git")
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public class GitConfigResource
{
/**
* Constructs ...
*
*
*
* @param repositoryHandler
*/
@Inject
public GitConfigResource(GitRepositoryHandler repositoryHandler)
{
this.repositoryHandler = repositoryHandler;
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @return
*/
@GET
public GitConfig getConfig()
{
GitConfig config = repositoryHandler.getConfig();
if (config == null)
{
config = new GitConfig();
repositoryHandler.setConfig(config);
}
return config;
}
//~--- set methods ----------------------------------------------------------
/**
* Method description
*
*
* @param uriInfo
* @param config
*
* @return
*/
@POST
@Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public Response setConfig(@Context UriInfo uriInfo, GitConfig config)
{
repositoryHandler.setConfig(config);
repositoryHandler.storeConfig();
return Response.created(uriInfo.getRequestUri()).build();
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private GitRepositoryHandler repositoryHandler;
}

View File

@@ -0,0 +1,25 @@
package sonia.scm.repository;
import java.util.function.Consumer;
public class CloseableWrapper<C> implements AutoCloseable {
private final C wrapped;
private final Consumer<C> cleanup;
public CloseableWrapper(C wrapped, Consumer<C> cleanup) {
this.wrapped = wrapped;
this.cleanup = cleanup;
}
public C get() { return wrapped; }
@Override
public void close() {
try {
cleanup.accept(wrapped);
} catch (Throwable t) {
throw new RuntimeException(t);
}
}
}

View File

@@ -91,6 +91,8 @@ public class GitRepositoryHandler
private final Scheduler scheduler;
private final GitWorkdirFactory workdirFactory;
private Task task;
//~--- constructors ---------------------------------------------------------
@@ -104,10 +106,11 @@ public class GitRepositoryHandler
* @param scheduler
*/
@Inject
public GitRepositoryHandler(ConfigurationStoreFactory storeFactory, FileSystem fileSystem, Scheduler scheduler)
public GitRepositoryHandler(ConfigurationStoreFactory storeFactory, FileSystem fileSystem, Scheduler scheduler, GitWorkdirFactory workdirFactory)
{
super(storeFactory, fileSystem);
this.scheduler = scheduler;
this.workdirFactory = workdirFactory;
}
//~--- get methods ----------------------------------------------------------
@@ -234,4 +237,8 @@ public class GitRepositoryHandler
{
return new File(directory, DIRECTORY_REFS).exists();
}
public GitWorkdirFactory getWorkdirFactory() {
return workdirFactory;
}
}

View File

@@ -0,0 +1,8 @@
package sonia.scm.repository;
import sonia.scm.repository.spi.GitContext;
import sonia.scm.repository.spi.WorkingCopy;
public interface GitWorkdirFactory {
WorkingCopy createWorkingCopy(GitContext gitContext);
}

View File

@@ -38,6 +38,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.repository.GitUtil;
import sonia.scm.repository.Repository;
//~--- JDK imports ------------------------------------------------------------
@@ -65,10 +66,12 @@ public class GitContext implements Closeable
*
*
* @param directory
* @param repository
*/
public GitContext(File directory)
public GitContext(File directory, Repository repository)
{
this.directory = directory;
this.repository = repository;
}
//~--- methods --------------------------------------------------------------
@@ -82,8 +85,8 @@ public class GitContext implements Closeable
{
logger.trace("close git repository {}", directory);
GitUtil.close(repository);
repository = null;
GitUtil.close(gitRepository);
gitRepository = null;
}
/**
@@ -96,21 +99,30 @@ public class GitContext implements Closeable
*/
public org.eclipse.jgit.lib.Repository open() throws IOException
{
if (repository == null)
if (gitRepository == null)
{
logger.trace("open git repository {}", directory);
repository = GitUtil.open(directory);
gitRepository = GitUtil.open(directory);
}
return gitRepository;
}
Repository getRepository() {
return repository;
}
File getDirectory() {
return directory;
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private final File directory;
private final Repository repository;
/** Field description */
private org.eclipse.jgit.lib.Repository repository;
private org.eclipse.jgit.lib.Repository gitRepository;
}

View File

@@ -0,0 +1,169 @@
package sonia.scm.repository.spi;
import com.google.common.base.Strings;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.MergeResult;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.merge.MergeStrategy;
import org.eclipse.jgit.merge.ResolveMerger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.repository.GitWorkdirFactory;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.Person;
import sonia.scm.repository.api.MergeCommandResult;
import sonia.scm.repository.api.MergeDryRunCommandResult;
import sonia.scm.user.User;
import java.io.IOException;
import java.text.MessageFormat;
public class GitMergeCommand extends AbstractGitCommand implements MergeCommand {
private static final Logger logger = LoggerFactory.getLogger(GitMergeCommand.class);
private static final String MERGE_COMMIT_MESSAGE_TEMPLATE = String.join("\n",
"Merge of branch {0} into {1}",
"",
"Automatic merge by SCM-Manager.");
private final GitWorkdirFactory workdirFactory;
GitMergeCommand(GitContext context, sonia.scm.repository.Repository repository, GitWorkdirFactory workdirFactory) {
super(context, repository);
this.workdirFactory = workdirFactory;
}
@Override
public MergeCommandResult merge(MergeCommandRequest request) {
try (WorkingCopy workingCopy = workdirFactory.createWorkingCopy(context)) {
Repository repository = workingCopy.get();
logger.debug("cloned repository to folder {}", repository.getWorkTree());
return new MergeWorker(repository, request).merge();
} catch (IOException e) {
throw new InternalRepositoryException(context.getRepository(), "could not clone repository for merge", e);
}
}
@Override
public MergeDryRunCommandResult dryRun(MergeCommandRequest request) {
try {
Repository repository = context.open();
ResolveMerger merger = (ResolveMerger) MergeStrategy.RECURSIVE.newMerger(repository, true);
return new MergeDryRunCommandResult(merger.merge(repository.resolve(request.getBranchToMerge()), repository.resolve(request.getTargetBranch())));
} catch (IOException e) {
throw new InternalRepositoryException(context.getRepository(), "could not clone repository for merge", e);
}
}
private class MergeWorker {
private final String target;
private final String toMerge;
private final Person author;
private final Git clone;
private final String messageTemplate;
private MergeWorker(Repository clone, MergeCommandRequest request) {
this.target = request.getTargetBranch();
this.toMerge = request.getBranchToMerge();
this.author = request.getAuthor();
this.messageTemplate = request.getMessageTemplate();
this.clone = new Git(clone);
}
private MergeCommandResult merge() throws IOException {
checkOutTargetBranch();
MergeResult result = doMergeInClone();
if (result.getMergeStatus().isSuccessful()) {
doCommit();
push();
return MergeCommandResult.success();
} else {
return analyseFailure(result);
}
}
private void checkOutTargetBranch() {
try {
clone.checkout().setName(target).call();
} catch (GitAPIException e) {
throw new InternalRepositoryException(context.getRepository(), "could not checkout target branch for merge: " + target, e);
}
}
private MergeResult doMergeInClone() throws IOException {
MergeResult result;
try {
result = clone.merge()
.setCommit(false) // we want to set the author manually
.include(toMerge, resolveRevision(toMerge))
.call();
} catch (GitAPIException e) {
throw new InternalRepositoryException(context.getRepository(), "could not merge branch " + toMerge + " into " + target, e);
}
return result;
}
private void doCommit() {
logger.debug("merged branch {} into {}", toMerge, target);
Person authorToUse = determineAuthor();
try {
clone.commit()
.setAuthor(authorToUse.getName(), authorToUse.getMail())
.setMessage(MessageFormat.format(determineMessageTemplate(), toMerge, target))
.call();
} catch (GitAPIException e) {
throw new InternalRepositoryException(context.getRepository(), "could not commit merge between branch " + toMerge + " and " + target, e);
}
}
private String determineMessageTemplate() {
if (Strings.isNullOrEmpty(messageTemplate)) {
return MERGE_COMMIT_MESSAGE_TEMPLATE;
} else {
return messageTemplate;
}
}
private Person determineAuthor() {
if (author == null) {
Subject subject = SecurityUtils.getSubject();
User user = subject.getPrincipals().oneByType(User.class);
String name = user.getDisplayName();
String email = user.getMail();
logger.debug("no author set; using logged in user: {} <{}>", name, email);
return new Person(name, email);
} else {
return author;
}
}
private void push() {
try {
clone.push().call();
} catch (GitAPIException e) {
throw new InternalRepositoryException(context.getRepository(), "could not push merged branch " + toMerge + " to origin", e);
}
logger.debug("pushed merged branch {}", target);
}
private MergeCommandResult analyseFailure(MergeResult result) {
logger.info("could not merged branch {} into {} due to conflict in paths {}", toMerge, target, result.getConflicts().keySet());
return MergeCommandResult.failure(result.getConflicts().keySet());
}
private ObjectId resolveRevision(String branchToMerge) throws IOException {
ObjectId resolved = clone.getRepository().resolve(branchToMerge);
if (resolved == null) {
return clone.getRepository().resolve("origin/" + branchToMerge);
} else {
return resolved;
}
}
}
}

View File

@@ -77,7 +77,6 @@ public class GitOutgoingCommand extends AbstractGitIncomingOutgoingCommand
* @return
*
* @throws IOException
* @throws RepositoryException
*/
@Override
public ChangesetPagingResult getOutgoingChangesets(

View File

@@ -101,7 +101,6 @@ public class GitPullCommand extends AbstractGitPushOrPullCommand
* @return
*
* @throws IOException
* @throws RepositoryException
*/
@Override
public PullResponse pull(PullCommandRequest request)

View File

@@ -85,7 +85,6 @@ public class GitPushCommand extends AbstractGitPushOrPullCommand
* @return
*
* @throws IOException
* @throws RepositoryException
*/
@Override
public PushResponse push(PushCommandRequest request)

View File

@@ -63,7 +63,8 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider
Command.INCOMING,
Command.OUTGOING,
Command.PUSH,
Command.PULL
Command.PULL,
Command.MERGE
);
//J+
@@ -72,7 +73,7 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider
public GitRepositoryServiceProvider(GitRepositoryHandler handler, Repository repository) {
this.handler = handler;
this.repository = repository;
this.context = new GitContext(handler.getDirectory(repository));
this.context = new GitContext(handler.getDirectory(repository), repository);
}
//~--- methods --------------------------------------------------------------
@@ -240,7 +241,12 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider
return new GitTagsCommand(context, repository);
}
//~--- fields ---------------------------------------------------------------
@Override
public MergeCommand getMergeCommand() {
return new GitMergeCommand(context, repository, handler.getWorkdirFactory());
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private GitContext context;

View File

@@ -0,0 +1,62 @@
package sonia.scm.repository.spi;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.util.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.repository.GitWorkdirFactory;
import sonia.scm.repository.InternalRepositoryException;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
public class SimpleGitWorkdirFactory implements GitWorkdirFactory {
private static final Logger logger = LoggerFactory.getLogger(SimpleGitWorkdirFactory.class);
private final File poolDirectory;
public SimpleGitWorkdirFactory() {
this(new File(System.getProperty("java.io.tmpdir"), "scmm-git-pool"));
}
public SimpleGitWorkdirFactory(File poolDirectory) {
this.poolDirectory = poolDirectory;
poolDirectory.mkdirs();
}
public WorkingCopy createWorkingCopy(GitContext gitContext) {
try {
Repository clone = cloneRepository(gitContext.getDirectory(), createNewWorkdir());
return new WorkingCopy(clone, this::close);
} catch (GitAPIException e) {
throw new InternalRepositoryException(gitContext.getRepository(), "could not clone working copy of repository", e);
} catch (IOException e) {
throw new InternalRepositoryException(gitContext.getRepository(), "could not create temporary directory for clone of repository", e);
}
}
private File createNewWorkdir() throws IOException {
return Files.createTempDirectory(poolDirectory.toPath(),"workdir").toFile();
}
protected Repository cloneRepository(File bareRepository, File target) throws GitAPIException {
return Git.cloneRepository()
.setURI(bareRepository.getAbsolutePath())
.setDirectory(target)
.call()
.getRepository();
}
private void close(Repository repository) {
repository.close();
try {
FileUtils.delete(repository.getWorkTree(), FileUtils.RECURSIVE);
} catch (IOException e) {
logger.warn("could not delete temporary git workdir '{}'", repository.getWorkTree(), e);
}
}
}

View File

@@ -0,0 +1,12 @@
package sonia.scm.repository.spi;
import org.eclipse.jgit.lib.Repository;
import sonia.scm.repository.CloseableWrapper;
import java.util.function.Consumer;
public class WorkingCopy extends CloseableWrapper<Repository> {
WorkingCopy(Repository wrapped, Consumer<Repository> cleanup) {
super(wrapped, cleanup);
}
}

View File

@@ -151,7 +151,6 @@ public class GitRepositoryViewer
* @return
*
* @throws IOException
* @throws RepositoryException
*/
private BranchesModel createBranchesModel(Repository repository)
throws IOException

View File

@@ -41,6 +41,8 @@ import org.mapstruct.factory.Mappers;
import sonia.scm.api.v2.resources.GitConfigDtoToGitConfigMapper;
import sonia.scm.api.v2.resources.GitConfigToGitConfigDtoMapper;
import sonia.scm.plugin.Extension;
import sonia.scm.repository.GitWorkdirFactory;
import sonia.scm.repository.spi.SimpleGitWorkdirFactory;
import sonia.scm.web.lfs.LfsBlobStoreFactory;
/**
@@ -63,5 +65,7 @@ public class GitServletModule extends ServletModule
bind(GitConfigDtoToGitConfigMapper.class).to(Mappers.getMapper(GitConfigDtoToGitConfigMapper.class).getClass());
bind(GitConfigToGitConfigDtoMapper.class).to(Mappers.getMapper(GitConfigToGitConfigDtoMapper.class).getClass());
bind(GitWorkdirFactory.class).to(SimpleGitWorkdirFactory.class);
}
}

View File

@@ -0,0 +1,29 @@
package sonia.scm.repository;
import org.junit.Test;
import java.util.function.Consumer;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
public class CloseableWrapperTest {
@Test
public void shouldExecuteGivenMethodAtClose() {
Consumer<String> wrapped = new Consumer<String>() {
// no this cannot be replaced with a lambda because otherwise we could not use Mockito#spy
@Override
public void accept(String s) {
}
};
Consumer<String> closer = spy(wrapped);
try (CloseableWrapper<String> wrapper = new CloseableWrapper<>("test", closer)) {
// nothing to do here
}
verify(closer).accept("test");
}
}

View File

@@ -61,6 +61,9 @@ public class GitRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
@Mock
private ConfigurationStoreFactory factory;
@Mock
private GitWorkdirFactory gitWorkdirFactory;
@Override
protected void checkDirectory(File directory) {
File head = new File(directory, "HEAD");
@@ -84,7 +87,7 @@ public class GitRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
protected RepositoryHandler createRepositoryHandler(ConfigurationStoreFactory factory,
File directory) {
GitRepositoryHandler repositoryHandler = new GitRepositoryHandler(factory,
new DefaultFileSystem(), scheduler);
new DefaultFileSystem(), scheduler, gitWorkdirFactory);
repositoryHandler.init(contextProvider);
@@ -100,7 +103,7 @@ public class GitRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
@Test
public void getDirectory() {
GitRepositoryHandler repositoryHandler = new GitRepositoryHandler(factory,
new DefaultFileSystem(), scheduler);
new DefaultFileSystem(), scheduler, gitWorkdirFactory);
GitConfig gitConfig = new GitConfig();
gitConfig.setRepositoryDirectory(new File("/path"));

View File

@@ -50,8 +50,10 @@ public class AbstractGitCommandTestBase extends ZippedRepositoryTestBase
@After
public void close()
{
if (context != null) {
context.close();
}
}
/**
* Method description
@@ -63,7 +65,7 @@ public class AbstractGitCommandTestBase extends ZippedRepositoryTestBase
{
if (context == null)
{
context = new GitContext(repositoryDirectory);
context = new GitContext(repositoryDirectory, repository);
}
return context;

View File

@@ -85,7 +85,6 @@ public class GitBlameCommandTest extends AbstractGitCommandTestBase
*
*
* @throws IOException
* @throws RepositoryException
*/
@Test
public void testGetBlameResult() throws IOException
@@ -119,7 +118,6 @@ public class GitBlameCommandTest extends AbstractGitCommandTestBase
*
*
* @throws IOException
* @throws RepositoryException
*/
@Test
public void testGetBlameResultWithRevision()

View File

@@ -61,7 +61,6 @@ public class GitIncomingCommandTest
*
* @throws GitAPIException
* @throws IOException
* @throws RepositoryException
*/
@Test
public void testGetIncomingChangesets()
@@ -95,7 +94,6 @@ public class GitIncomingCommandTest
*
* @throws GitAPIException
* @throws IOException
* @throws RepositoryException
*/
@Test
public void testGetIncomingChangesetsWithAllreadyPullChangesets()
@@ -105,7 +103,7 @@ public class GitIncomingCommandTest
commit(outgoing, "added a");
GitPullCommand pull = new GitPullCommand(handler, new GitContext(incomingDirectory), incomingRepository);
GitPullCommand pull = new GitPullCommand(handler, new GitContext(incomingDirectory, null), incomingRepository);
PullCommandRequest req = new PullCommandRequest();
req.setRemoteRepository(outgoingRepository);
pull.pull(req);
@@ -132,7 +130,6 @@ public class GitIncomingCommandTest
*
*
* @throws IOException
* @throws RepositoryException
*/
@Test
public void testGetIncomingChangesetsWithEmptyRepository()
@@ -156,7 +153,6 @@ public class GitIncomingCommandTest
*
* @throws GitAPIException
* @throws IOException
* @throws RepositoryException
*/
@Test
@Ignore
@@ -191,7 +187,7 @@ public class GitIncomingCommandTest
*/
private GitIncomingCommand createCommand()
{
return new GitIncomingCommand(handler, new GitContext(incomingDirectory),
return new GitIncomingCommand(handler, new GitContext(incomingDirectory, null),
incomingRepository);
}
}

View File

@@ -0,0 +1,139 @@
package sonia.scm.repository.spi;
import com.github.sdorra.shiro.ShiroRule;
import com.github.sdorra.shiro.SubjectAware;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.junit.Rule;
import org.junit.Test;
import sonia.scm.repository.Person;
import sonia.scm.repository.api.MergeCommandResult;
import sonia.scm.user.User;
import java.io.IOException;
import static org.assertj.core.api.Assertions.assertThat;
@SubjectAware(configuration = "classpath:sonia/scm/configuration/shiro.ini")
public class GitMergeCommandTest extends AbstractGitCommandTestBase {
private static final String REALM = "AdminRealm";
@Rule
public ShiroRule shiro = new ShiroRule();
@Test
public void shouldDetectMergeableBranches() {
GitMergeCommand command = createCommand();
MergeCommandRequest request = new MergeCommandRequest();
request.setBranchToMerge("mergeable");
request.setTargetBranch("master");
boolean mergeable = command.dryRun(request).isMergeable();
assertThat(mergeable).isTrue();
}
@Test
public void shouldDetectNotMergeableBranches() {
GitMergeCommand command = createCommand();
MergeCommandRequest request = new MergeCommandRequest();
request.setBranchToMerge("test-branch");
request.setTargetBranch("master");
boolean mergeable = command.dryRun(request).isMergeable();
assertThat(mergeable).isFalse();
}
@Test
public void shouldMergeMergeableBranches() throws IOException, GitAPIException {
GitMergeCommand command = createCommand();
MergeCommandRequest request = new MergeCommandRequest();
request.setTargetBranch("master");
request.setBranchToMerge("mergeable");
request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det"));
MergeCommandResult mergeCommandResult = command.merge(request);
assertThat(mergeCommandResult.isSuccess()).isTrue();
Repository repository = createContext().open();
Iterable<RevCommit> commits = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call();
RevCommit mergeCommit = commits.iterator().next();
PersonIdent mergeAuthor = mergeCommit.getAuthorIdent();
String message = mergeCommit.getFullMessage();
assertThat(mergeAuthor.getName()).isEqualTo("Dirk Gently");
assertThat(mergeAuthor.getEmailAddress()).isEqualTo("dirk@holistic.det");
assertThat(message).contains("master", "mergeable");
// We expect the merge result of file b.txt here by looking up the sha hash of its content.
// If the file is missing (aka not merged correctly) this will throw a MissingObjectException:
byte[] contentOfFileB = repository.open(repository.resolve("9513e9c76e73f3e562fd8e4c909d0607113c77c6")).getBytes();
assertThat(new String(contentOfFileB)).isEqualTo("b\ncontent from branch\n");
}
@Test
public void shouldUseConfiguredCommitMessageTemplate() throws IOException, GitAPIException {
GitMergeCommand command = createCommand();
MergeCommandRequest request = new MergeCommandRequest();
request.setTargetBranch("master");
request.setBranchToMerge("mergeable");
request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det"));
request.setMessageTemplate("simple");
MergeCommandResult mergeCommandResult = command.merge(request);
assertThat(mergeCommandResult.isSuccess()).isTrue();
Repository repository = createContext().open();
Iterable<RevCommit> commits = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call();
RevCommit mergeCommit = commits.iterator().next();
String message = mergeCommit.getFullMessage();
assertThat(message).isEqualTo("simple");
}
@Test
public void shouldNotMergeConflictingBranches() {
GitMergeCommand command = createCommand();
MergeCommandRequest request = new MergeCommandRequest();
request.setBranchToMerge("test-branch");
request.setTargetBranch("master");
MergeCommandResult mergeCommandResult = command.merge(request);
assertThat(mergeCommandResult.isSuccess()).isFalse();
assertThat(mergeCommandResult.getFilesWithConflict()).containsExactly("a.txt");
}
@Test
@SubjectAware(username = "admin", password = "secret")
public void shouldTakeAuthorFromSubjectIfNotSet() throws IOException, GitAPIException {
shiro.setSubject(
new Subject.Builder()
.principals(new SimplePrincipalCollection(new User("dirk", "Dirk Gently", "dirk@holistic.det"), REALM))
.buildSubject());
GitMergeCommand command = createCommand();
MergeCommandRequest request = new MergeCommandRequest();
request.setTargetBranch("master");
request.setBranchToMerge("mergeable");
MergeCommandResult mergeCommandResult = command.merge(request);
assertThat(mergeCommandResult.isSuccess()).isTrue();
Repository repository = createContext().open();
Iterable<RevCommit> mergeCommit = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call();
PersonIdent mergeAuthor = mergeCommit.iterator().next().getAuthorIdent();
assertThat(mergeAuthor.getName()).isEqualTo("Dirk Gently");
assertThat(mergeAuthor.getEmailAddress()).isEqualTo("dirk@holistic.det");
}
private GitMergeCommand createCommand() {
return new GitMergeCommand(createContext(), repository, new SimpleGitWorkdirFactory());
}
}

View File

@@ -18,8 +18,8 @@ public class GitModificationsCommandTest extends AbstractRemoteCommandTestBase {
@Before
public void init() {
incomingModificationsCommand = new GitModificationsCommand(new GitContext(incomingDirectory), incomingRepository);
outgoingModificationsCommand = new GitModificationsCommand(new GitContext(outgoingDirectory), outgoingRepository);
incomingModificationsCommand = new GitModificationsCommand(new GitContext(incomingDirectory, null), incomingRepository);
outgoingModificationsCommand = new GitModificationsCommand(new GitContext(outgoingDirectory, null), outgoingRepository);
}
@Test
@@ -63,12 +63,12 @@ public class GitModificationsCommandTest extends AbstractRemoteCommandTestBase {
}
void pushOutgoingAndPullIncoming() throws IOException {
GitPushCommand cmd = new GitPushCommand(handler, new GitContext(outgoingDirectory),
GitPushCommand cmd = new GitPushCommand(handler, new GitContext(outgoingDirectory, null),
outgoingRepository);
PushCommandRequest request = new PushCommandRequest();
request.setRemoteRepository(incomingRepository);
cmd.push(request);
GitPullCommand pullCommand = new GitPullCommand(handler, new GitContext(incomingDirectory),
GitPullCommand pullCommand = new GitPullCommand(handler, new GitContext(incomingDirectory, null),
incomingRepository);
PullCommandRequest pullRequest = new PullCommandRequest();
pullRequest.setRemoteRepository(incomingRepository);

View File

@@ -61,7 +61,6 @@ public class GitOutgoingCommandTest extends AbstractRemoteCommandTestBase
*
* @throws GitAPIException
* @throws IOException
* @throws RepositoryException
*/
@Test
public void testGetOutgoingChangesets()
@@ -95,7 +94,6 @@ public class GitOutgoingCommandTest extends AbstractRemoteCommandTestBase
*
* @throws GitAPIException
* @throws IOException
* @throws RepositoryException
*/
@Test
public void testGetOutgoingChangesetsWithAlreadyPushedChanges()
@@ -106,7 +104,7 @@ public class GitOutgoingCommandTest extends AbstractRemoteCommandTestBase
commit(outgoing, "added a");
GitPushCommand push = new GitPushCommand(handler,
new GitContext(outgoingDirectory),
new GitContext(outgoingDirectory, null),
outgoingRepository);
PushCommandRequest req = new PushCommandRequest();
@@ -135,7 +133,6 @@ public class GitOutgoingCommandTest extends AbstractRemoteCommandTestBase
*
*
* @throws IOException
* @throws RepositoryException
*/
@Test
public void testGetOutgoingChangesetsWithEmptyRepository()
@@ -161,7 +158,7 @@ public class GitOutgoingCommandTest extends AbstractRemoteCommandTestBase
*/
private GitOutgoingCommand createCommand()
{
return new GitOutgoingCommand(handler, new GitContext(outgoingDirectory),
return new GitOutgoingCommand(handler, new GitContext(outgoingDirectory, null),
outgoingRepository);
}
}

View File

@@ -61,7 +61,6 @@ public class GitPushCommandTest extends AbstractRemoteCommandTestBase
*
* @throws GitAPIException
* @throws IOException
* @throws RepositoryException
*/
@Test
public void testPush()
@@ -99,7 +98,7 @@ public class GitPushCommandTest extends AbstractRemoteCommandTestBase
*/
private GitPushCommand createCommand()
{
return new GitPushCommand(handler, new GitContext(outgoingDirectory),
return new GitPushCommand(handler, new GitContext(outgoingDirectory, null),
outgoingRepository);
}
}

View File

@@ -0,0 +1,87 @@
package sonia.scm.repository.spi;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.Repository;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.File;
import java.io.IOException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
public class SimpleGitWorkdirFactoryTest extends AbstractGitCommandTestBase {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void emptyPoolShouldCreateNewWorkdir() throws IOException {
SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(temporaryFolder.newFolder());
File masterRepo = createRepositoryDirectory();
try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext())) {
assertThat(workingCopy.get().getDirectory())
.exists()
.isNotEqualTo(masterRepo)
.isDirectory();
assertThat(new File(workingCopy.get().getWorkTree(), "a.txt"))
.exists()
.isFile()
.hasContent("a\nline for blame");
}
}
@Test
public void cloneFromPoolShouldBeClosed() throws IOException {
PoolWithSpy factory = new PoolWithSpy(temporaryFolder.newFolder());
try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext())) {
assertThat(workingCopy).isNotNull();
}
verify(factory.createdClone).close();
}
@Test
public void cloneFromPoolShouldNotBeReused() throws IOException {
SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(temporaryFolder.newFolder());
File firstDirectory;
try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext())) {
firstDirectory = workingCopy.get().getDirectory();
}
try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext())) {
File secondDirectory = workingCopy.get().getDirectory();
assertThat(secondDirectory).isNotEqualTo(firstDirectory);
}
}
@Test
public void cloneFromPoolShouldBeDeletedOnClose() throws IOException {
SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(temporaryFolder.newFolder());
File directory;
try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext())) {
directory = workingCopy.get().getWorkTree();
}
assertThat(directory).doesNotExist();
}
private static class PoolWithSpy extends SimpleGitWorkdirFactory {
PoolWithSpy(File poolDirectory) {
super(poolDirectory);
}
Repository createdClone;
@Override
protected Repository cloneRepository(File bareRepository, File destination) throws GitAPIException {
createdClone = spy(super.cloneRepository(bareRepository, destination));
return createdClone;
}
}
}

View File

@@ -59,6 +59,10 @@
<extensions>true</extensions>
<configuration>
<corePlugin>true</corePlugin>
<links>
<link>@scm-manager/ui-types</link>
<link>@scm-manager/ui-components</link>
</links>
</configuration>
</plugin>

View File

@@ -1,348 +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 sonia.scm.SCMContext;
import sonia.scm.installer.HgInstallerFactory;
import sonia.scm.installer.HgPackage;
import sonia.scm.installer.HgPackageReader;
import sonia.scm.installer.HgPackages;
import sonia.scm.net.ahc.AdvancedHttpClient;
import sonia.scm.repository.HgConfig;
import sonia.scm.repository.HgRepositoryHandler;
//~--- JDK imports ------------------------------------------------------------
import java.io.IOException;
import java.util.List;
import javax.ws.rs.Consumes;
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.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
/**
*
* @author Sebastian Sdorra
*/
@Singleton
@Path("config/repositories/hg")
public class HgConfigResource
{
/**
* Constructs ...
*
*
*
*
* @param client
* @param handler
* @param pkgReader
*/
@Inject
public HgConfigResource(AdvancedHttpClient client,
HgRepositoryHandler handler, HgPackageReader pkgReader)
{
this.client = client;
this.handler = handler;
this.pkgReader = pkgReader;
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param uriInfo
*
* @return
*/
@POST
@Path("auto-configuration")
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public HgConfig autoConfiguration(@Context UriInfo uriInfo)
{
return autoConfiguration(uriInfo, null);
}
/**
* Method description
*
*
* @param uriInfo
* @param config
*
* @return
*/
@POST
@Path("auto-configuration")
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
@Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public HgConfig autoConfiguration(@Context UriInfo uriInfo, HgConfig config)
{
if (config == null)
{
config = new HgConfig();
}
handler.doAutoConfiguration(config);
return handler.getConfig();
}
/**
* Method description
*
*
*
* @param id
* @return
*/
@POST
@Path("packages/{pkgId}")
public Response installPackage(@PathParam("pkgId") String id)
{
Response response = null;
HgPackage pkg = pkgReader.getPackage(id);
if (pkg != null)
{
if (HgInstallerFactory.createInstaller().installPackage(client, handler,
SCMContext.getContext().getBaseDirectory(), pkg))
{
response = Response.noContent().build();
}
else
{
response =
Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
}
}
else
{
response = Response.status(Response.Status.NOT_FOUND).build();
}
return response;
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @return
*/
@GET
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public HgConfig getConfig()
{
HgConfig config = handler.getConfig();
if (config == null)
{
config = new HgConfig();
}
return config;
}
/**
* Method description
*
*
* @return
*/
@GET
@Path("installations/hg")
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public InstallationsResponse getHgInstallations()
{
List<String> installations =
HgInstallerFactory.createInstaller().getHgInstallations();
return new InstallationsResponse(installations);
}
/**
* Method description
*
*
* @return
*/
@GET
@Path("packages")
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public HgPackages getPackages()
{
return pkgReader.getPackages();
}
/**
* Method description
*
*
* @return
*/
@GET
@Path("installations/python")
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public InstallationsResponse getPythonInstallations()
{
List<String> installations =
HgInstallerFactory.createInstaller().getPythonInstallations();
return new InstallationsResponse(installations);
}
//~--- set methods ----------------------------------------------------------
/**
* Method description
*
*
* @param uriInfo
* @param config
*
* @return
*
* @throws IOException
*/
@POST
@Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public Response setConfig(@Context UriInfo uriInfo, HgConfig config)
throws IOException
{
handler.setConfig(config);
handler.storeConfig();
return Response.created(uriInfo.getRequestUri()).build();
}
//~--- inner classes --------------------------------------------------------
/**
* Class description
*
*
* @version Enter version here..., 11/04/25
* @author Enter your name here...
*/
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "installations")
public static class InstallationsResponse
{
/**
* Constructs ...
*
*/
public InstallationsResponse() {}
/**
* Constructs ...
*
*
* @param paths
*/
public InstallationsResponse(List<String> paths)
{
this.paths = paths;
}
//~--- get methods --------------------------------------------------------
/**
* Method description
*
*
* @return
*/
public List<String> getPaths()
{
return paths;
}
//~--- set methods --------------------------------------------------------
/**
* Method description
*
*
* @param paths
*/
public void setPaths(List<String> paths)
{
this.paths = paths;
}
//~--- fields -------------------------------------------------------------
/** Field description */
@XmlElement(name = "path")
private List<String> paths;
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private AdvancedHttpClient client;
/** Field description */
private HgRepositoryHandler handler;
/** Field description */
private HgPackageReader pkgReader;
}

View File

@@ -45,6 +45,10 @@
<artifactId>smp-maven-plugin</artifactId>
<configuration>
<corePlugin>true</corePlugin>
<links>
<link>@scm-manager/ui-types</link>
<link>@scm-manager/ui-components</link>
</links>
</configuration>
</plugin>

View File

@@ -1,125 +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 sonia.scm.repository.RepositoryManager;
import sonia.scm.repository.SvnConfig;
import sonia.scm.repository.SvnRepositoryHandler;
//~--- 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/repositories/svn")
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public class SvnConfigResource
{
/**
* Constructs ...
*
*
* @param repositoryManager
*/
@Inject
public SvnConfigResource(RepositoryManager repositoryManager)
{
repositoryHandler = (SvnRepositoryHandler) repositoryManager.getHandler(
SvnRepositoryHandler.TYPE_NAME);
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @return
*/
@GET
public SvnConfig getConfig()
{
SvnConfig config = repositoryHandler.getConfig();
if (config == null)
{
config = new SvnConfig();
repositoryHandler.setConfig(config);
}
return config;
}
//~--- set methods ----------------------------------------------------------
/**
* Method description
*
*
* @param uriInfo
* @param config
*
* @return
*/
@POST
@Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public Response setConfig(@Context UriInfo uriInfo, SvnConfig config)
{
repositoryHandler.setConfig(config);
repositoryHandler.storeConfig();
return Response.created(uriInfo.getRequestUri()).build();
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private SvnRepositoryHandler repositoryHandler;
}

View File

@@ -1,14 +1,16 @@
{
"name": "scm-ui-components",
"version": "0.0.3",
"description": "Lerna root for SCM-Manager UI Components",
"private": true,
"scripts": {
"bootstrap": "lerna bootstrap",
"link": "lerna exec -- yarn link",
"unlink": "lerna exec --no-bail -- yarn unlink || true"
"unlink": "lerna exec --no-bail -- yarn unlink || true",
"deploy": "node ./scripts/publish.js"
},
"devDependencies": {
"lerna": "^3.2.1"
},
"dependencies": {}
"lerna": "^3.4.3",
"xml2js": "^0.4.19"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@scm-manager/ui-components",
"version": "0.0.1",
"version": "2.0.0-SNAPSHOT",
"description": "UI Components for SCM-Manager and its plugins",
"main": "src/index.js",
"files": [
@@ -26,7 +26,7 @@
},
"dependencies": {
"@scm-manager/ui-extensions": "^0.1.1",
"@scm-manager/ui-types": "0.0.1",
"@scm-manager/ui-types": "2.0.0-SNAPSHOT",
"classnames": "^2.2.6",
"moment": "^2.22.2",
"react": "^16.5.2",

View File

@@ -1,39 +1,33 @@
//@flow
import React from "react";
import injectSheet from "react-jss";
import classNames from "classnames";
import Tooltip from './Tooltip';
import HelpIcon from './HelpIcon';
const styles = {
img: {
display: "block"
},
q: {
float: "left",
paddingLeft: "3px",
float: "right"
tooltip: {
display: "inline-block",
paddingLeft: "3px"
}
};
type Props = {
message: string,
classes: any
};
}
class Help extends React.Component<Props> {
render() {
const { message, classes } = this.props;
const multiline = message.length > 60 ? "is-tooltip-multiline" : "";
return (
<div
className={classNames("tooltip is-tooltip-right", multiline, classes.q)}
data-tooltip={message}
>
<i
className={classNames("fa fa-question has-text-info", classes.img)}
/>
</div>
<Tooltip className={classes.tooltip} message={message}>
<HelpIcon />
</Tooltip>
);
}
}
export default injectSheet(styles)(Help);

View File

@@ -0,0 +1,14 @@
//@flow
import React from "react";
import classNames from "classnames";
type Props = {
};
class HelpIcon extends React.Component<Props> {
render() {
return <i className={classNames("fa fa-question has-text-info")} />
}
}
export default HelpIcon;

View File

@@ -1,46 +0,0 @@
//@flow
import React from "react";
import { Help } from "./index";
type Props = {
label: string,
helpText?: string
};
class LabelWithHelpIcon extends React.Component<Props> {
renderLabel = () => {
const label = this.props.label;
if (label) {
return <label className="label">{label}</label>;
}
return "";
};
renderHelp = () => {
const helpText = this.props.helpText;
if (helpText) {
return (
<div className="control columns is-vcentered">
<Help message={helpText} />
</div>
);
} else return null;
};
renderLabelWithHelpIcon = () => {
if (this.props.label) {
return (
<div className="field is-grouped">
<div className="control">{this.renderLabel()}</div>
{this.renderHelp()}
</div>
);
} else return null;
};
render() {
return this.renderLabelWithHelpIcon();
}
}
export default LabelWithHelpIcon;

View File

@@ -1,10 +1,14 @@
//@flow
import React from "react";
import { translate } from "react-i18next";
import injectSheet from "react-jss";
import Image from "./Image";
const styles = {
minHeightContainer: {
minHeight: "256px"
},
wrapper: {
position: "relative"
},
@@ -34,6 +38,7 @@ class Loading extends React.Component<Props> {
render() {
const { message, t, classes } = this.props;
return (
<div className={classes.minHeightContainer}>
<div className={classes.wrapper}>
<div className={classes.loading}>
<Image
@@ -44,6 +49,7 @@ class Loading extends React.Component<Props> {
<p className="has-text-centered">{message}</p>
</div>
</div>
</div>
);
}
}

View File

@@ -0,0 +1,26 @@
//@flow
import * as React from "react";
import classNames from "classnames";
type Props = {
message: string,
className: string,
children: React.Node
};
class Tooltip extends React.Component<Props> {
render() {
const { className, message, children } = this.props;
const multiline = message.length > 60 ? "is-tooltip-multiline" : "";
return (
<div
className={classNames("tooltip", "is-tooltip-right", multiline, className)}
data-tooltip={message}
>
{children}
</div>
);
}
}
export default Tooltip;

View File

@@ -10,7 +10,9 @@ type Props = {
disabled?: boolean,
helpText?: string
};
class Checkbox extends React.Component<Props> {
onCheckboxChange = (event: SyntheticInputEvent<HTMLInputElement>) => {
if (this.props.onChange) {
this.props.onChange(event.target.checked, this.props.name);
@@ -20,12 +22,8 @@ class Checkbox extends React.Component<Props> {
renderHelp = () => {
const helpText = this.props.helpText;
if (helpText) {
return (
<div className="control columns is-vcentered">
<Help message={helpText} />
</div>
);
} else return null;
return <Help message={helpText} />;
}
};
render() {
@@ -39,10 +37,11 @@ class Checkbox extends React.Component<Props> {
onChange={this.onCheckboxChange}
disabled={this.props.disabled}
/>
{" "}
{this.props.label}
{this.renderHelp()}
</label>
</div>
{this.renderHelp()}
</div>
);
}

View File

@@ -1,7 +1,7 @@
//@flow
import React from "react";
import classNames from "classnames";
import { LabelWithHelpIcon } from "../index";
import LabelWithHelpIcon from "./LabelWithHelpIcon";
type Props = {
label?: string,

View File

@@ -0,0 +1,37 @@
//@flow
import React from "react";
import Help from "../Help.js";
type Props = {
label?: string,
helpText?: string
};
class LabelWithHelpIcon extends React.Component<Props> {
renderHelp() {
const { helpText } = this.props;
if (helpText) {
return (
<Help message={helpText} />
);
}
}
render() {
const {label } = this.props;
if (label) {
const help = this.renderHelp();
return (
<label className="label">
{label} { help }
</label>
);
}
return "";
}
}
export default LabelWithHelpIcon;

View File

@@ -1,7 +1,7 @@
//@flow
import React from "react";
import classNames from "classnames";
import { LabelWithHelpIcon } from "../index";
import LabelWithHelpIcon from "./LabelWithHelpIcon";
export type SelectItem = {
value: string,

View File

@@ -1,6 +1,6 @@
//@flow
import React from "react";
import { LabelWithHelpIcon } from "../index";
import LabelWithHelpIcon from "./LabelWithHelpIcon";
export type SelectItem = {
value: string,
@@ -8,10 +8,11 @@ export type SelectItem = {
};
type Props = {
name?: string,
label?: string,
placeholder?: SelectItem[],
value?: string,
onChange: string => void,
onChange: (value: string, name?: string) => void,
helpText?: string
};
@@ -19,7 +20,7 @@ class Textarea extends React.Component<Props> {
field: ?HTMLTextAreaElement;
handleInput = (event: SyntheticInputEvent<HTMLTextAreaElement>) => {
this.props.onChange(event.target.value);
this.props.onChange(event.target.value, this.props.name);
};
render() {

View File

@@ -6,4 +6,5 @@ export { default as Checkbox } from "./Checkbox.js";
export { default as InputField } from "./InputField.js";
export { default as Select } from "./Select.js";
export { default as Textarea } from "./Textarea.js";
export { default as LabelWithHelpIcon } from "./LabelWithHelpIcon";

View File

@@ -17,8 +17,9 @@ export { default as Notification } from "./Notification.js";
export { default as Paginator } from "./Paginator.js";
export { default as LinkPaginator } from "./LinkPaginator.js";
export { default as ProtectedRoute } from "./ProtectedRoute.js";
export { default as Help } from "./Help.js";
export { default as LabelWithHelpIcon } from "./LabelWithHelpIcon.js";
export { default as Help } from "./Help";
export { default as HelpIcon } from "./HelpIcon";
export { default as Tooltip } from "./Tooltip";
export { getPageFromMatch } from "./urls";
export { default as Autocomplete} from "./Autocomplete";

View File

@@ -1,6 +1,6 @@
{
"name": "@scm-manager/ui-types",
"version": "0.0.1",
"version": "2.0.0-SNAPSHOT",
"description": "Flow types for SCM-Manager related Objects",
"main": "src/index.js",
"files": [
@@ -8,7 +8,7 @@
],
"repository": "https://bitbucket.org/sdorra/scm-manager",
"author": "Sebastian Sdorra <sebastian.sdorra@cloudogu.com>",
"license" : "BSD-3-Clause",
"license": "BSD-3-Clause",
"scripts": {
"lint": "ui-bunder lint",
"check": "flow check"
@@ -21,8 +21,14 @@
[
"babelify",
{
"plugins": ["@babel/plugin-proposal-class-properties"],
"presets": ["@babel/preset-env", "@babel/preset-flow", "@babel/preset-react"]
"plugins": [
"@babel/plugin-proposal-class-properties"
],
"presets": [
"@babel/preset-env",
"@babel/preset-flow",
"@babel/preset-react"
]
}
]
]

View File

@@ -1,52 +0,0 @@
Arguments:
/usr/bin/node /home/ssdorra/.yarn/bin/yarn.js add --dev ui-bundler
PATH:
/home/ssdorra/.yarn/bin:/usr/local/go/bin:/home/ssdorra/Projects/go/bin:/home/ssdorra/.local/bin:/usr/local/google-cloud-sdk/bin:/usr/local/go/bin:/home/ssdorra/.sdkman/candidates/maven/current/bin:/home/ssdorra/.sdkman/candidates/groovy/current/bin:/home/ssdorra/bin:/home/ssdorra/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/usr/lib/jvm/java-8-oracle/bin:/usr/lib/jvm/java-8-oracle/db/bin:/usr/lib/jvm/java-8-oracle/jre/bin:/home/ssdorra/Projects/go/bin
Yarn version:
1.9.2
Node version:
8.11.4
Platform:
linux x64
Trace:
Error: https://registry.yarnpkg.com/ui-bundler: Not found
at Request.params.callback [as _callback] (/home/ssdorra/.yarn/lib/cli.js:64150:18)
at Request.self.callback (/home/ssdorra/.yarn/lib/cli.js:137416:22)
at emitTwo (events.js:126:13)
at Request.emit (events.js:214:7)
at Request.<anonymous> (/home/ssdorra/.yarn/lib/cli.js:138388:10)
at emitOne (events.js:116:13)
at Request.emit (events.js:211:7)
at IncomingMessage.<anonymous> (/home/ssdorra/.yarn/lib/cli.js:138310:12)
at Object.onceWrapper (events.js:313:30)
at emitNone (events.js:111:20)
npm manifest:
{
"name": "@scm-manager/ui-types",
"version": "0.0.1",
"description": "Flow types for SCM-Manager related Objects",
"main": "src/index.js",
"files": [
"src"
],
"repository": "https://bitbucket.org/sdorra/scm-manager",
"author": "Sebastian Sdorra <sebastian.sdorra@cloudogu.com>",
"license": "MIT",
"scripts": {
"check": "flow check"
},
"devDependencies": {}
}
yarn manifest:
No manifest
Lockfile:
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1

View File

@@ -68,6 +68,16 @@
<script>link</script>
</configuration>
</execution>
<execution>
<id>deploy</id>
<phase>deploy</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<script>deploy</script>
</configuration>
</execution>
</executions>
</plugin>

View File

@@ -0,0 +1,140 @@
const fs = require("fs");
const path = require("path");
const {spawn} = require("child_process");
const parseString = require('xml2js').parseString;
function isSnapshot(version) {
return version.indexOf("SNAPSHOT") > 0;
}
function createSnapshotVersion(version) {
const date = new Date();
const year = date.getFullYear();
const month = date.getMonth().toString().padStart(2, "0");
const day = date.getDate().toString().padStart(2, "0");
const hours = date.getHours().toString().padStart(2, "0");
const minutes = date.getMinutes().toString().padStart(2, "0");
const seconds = date.getSeconds().toString().padStart(2, "0");
return version.replace("SNAPSHOT", `${year}${month}${day}-${hours}${minutes}${seconds}`);
}
function createVersionForPublishing(version) {
if (isSnapshot(version)) {
return createSnapshotVersion(version);
}
return version;
}
function publishPackages(version) {
console.log(`publish ${version} of all packages`);
return new Promise((resolve, reject) => {
const lerna = spawn("lerna", [
"exec", "--", "yarn", "publish", "--new-version", version, "--access", "public"
], {
stdio: [
process.stdin,
process.stdout,
process.stderr
]
});
lerna.on('close', (code) => {
if (code === 0) {
resolve();
} else {
reject(new Error("publishing of packages failed with status code: " + code));
}
});
});
}
function setVersion(package, packages, newVersion) {
return new Promise((resolve, reject) => {
const packageJsonPath = path.join(package, "package.json");
fs.readFile(packageJsonPath, "utf-8" , (err, content) => {
if (err) {
reject(err);
} else {
const packageJson = JSON.parse(content);
packageJson.version = newVersion;
for (let dep in packageJson.dependencies) {
if (packages.indexOf(dep) >= 0) {
packageJson.dependencies[ dep ] = newVersion;
}
}
fs.writeFile( packageJsonPath, JSON.stringify(packageJson, null, 2), (err) => {
if (err) {
reject(err)
} else {
console.log("modified", packageJsonPath);
resolve();
}
});
}
});
});
}
function setVersions(newVersion) {
console.log("set versions of packages to", newVersion);
return new Promise((resolve, reject) => {
fs.readdir("packages", (err, packages) => {
if ( err ) {
reject(err);
} else {
const actions = [];
const packagesWithOrg = packages.map((name) => `@scm-manager/${name}`);
for (let pkg of packages) {
const action = setVersion(path.join("packages", pkg), packagesWithOrg, newVersion);
actions.push(action);
}
resolve(Promise.all(actions));
}
});
});
}
function getVersion() {
return new Promise((resolve, reject) => {
fs.readFile("pom.xml", "utf8", (err, xml) => {
if (err) {
reject(err);
} else {
parseString(xml, function (err, json) {
if (err) {
reject(err)
} else {
const project = json.project;
let version = project.version;
if (!version) {
version = project.parent.version;
}
version = version[0];
resolve(version)
}
});
}
});
});
}
getVersion()
.then(version => {
const publishVersion = createVersionForPublishing(version);
return setVersions(publishVersion)
.then(() => publishPackages(publishVersion))
.then(() => setVersions(version));
})
.catch((err) => {
throw err;
});

File diff suppressed because it is too large Load Diff

View File

@@ -27,13 +27,32 @@ type Props = {
t: string => string
};
class Index extends Component<Props> {
type State = {
pluginsLoaded: boolean
};
class Index extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
pluginsLoaded: false
};
}
componentDidMount() {
this.props.fetchIndexResources();
}
pluginLoaderCallback = () => {
this.setState({
pluginsLoaded: true
});
};
render() {
const { indexResources, loading, error, t } = this.props;
const { pluginsLoaded } = this.state;
if (error) {
return (
@@ -47,7 +66,7 @@ class Index extends Component<Props> {
return <Loading />;
} else {
return (
<PluginLoader>
<PluginLoader loaded={ pluginsLoaded } callback={ this.pluginLoaderCallback }>
<App />
</PluginLoader>
);

View File

@@ -5,12 +5,13 @@ import { getUiPluginsLink } from "../modules/indexResource";
import { connect } from "react-redux";
type Props = {
loaded: boolean,
children: React.Node,
link: string
link: string,
callback: () => void
};
type State = {
finished: boolean,
message: string
};
@@ -23,18 +24,20 @@ class PluginLoader extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
finished: false,
message: "booting"
};
}
componentDidMount() {
const { loaded } = this.props;
if (!loaded) {
this.setState({
message: "loading plugin information"
});
this.getPlugins(this.props.link);
}
}
getPlugins = (link: string): Promise<any> => {
return apiClient
@@ -43,11 +46,7 @@ class PluginLoader extends React.Component<Props, State> {
.then(JSON.parse)
.then(pluginCollection => pluginCollection._embedded.plugins)
.then(this.loadPlugins)
.then(() => {
this.setState({
finished: true
});
});
.then(this.props.callback);
};
loadPlugins = (plugins: Plugin[]) => {
@@ -87,8 +86,9 @@ class PluginLoader extends React.Component<Props, State> {
};
render() {
const { message, finished } = this.state;
if (finished) {
const { loaded } = this.props;
const { message } = this.state;
if (loaded) {
return <div>{this.props.children}</div>;
}
return <Loading message={message} />;

View File

@@ -15,11 +15,14 @@ i18n
.init({
fallbackLng: "en",
// try to load only "en" and not "en_US"
load: "languageOnly",
// have a common namespace used around the full app
ns: ["commons"],
defaultNS: "commons",
debug: true,
debug: false,
interpolation: {
escapeValue: false // not needed for react!!

View File

@@ -124,7 +124,7 @@ class RepositoryForm extends React.Component<Props, State> {
const { repositoryTypes, t } = this.props;
const repository = this.state.repository;
return (
<div>
<>
<InputField
label={t("repository.name")}
onChange={this.handleNameChange}
@@ -140,7 +140,7 @@ class RepositoryForm extends React.Component<Props, State> {
options={this.createSelectOptions(repositoryTypes)}
helpText={t("help.typeHelpText")}
/>
</div>
</>
);
}

View File

@@ -35,6 +35,7 @@ import PermissionsNavLink from "../components/PermissionsNavLink";
import Sources from "../sources/containers/Sources";
import RepositoryNavLink from "../components/RepositoryNavLink";
import { getRepositoriesLink } from "../../modules/indexResource";
import {ExtensionPoint} from '@scm-manager/ui-extensions';
type Props = {
namespace: string,
@@ -104,6 +105,12 @@ class RepositoryRoot extends React.Component<Props> {
}
const url = this.matchedUrl();
const extensionProps = {
repository,
url
};
return (
<Page title={repository.namespace + "/" + repository.name}>
<div className="columns">
@@ -165,6 +172,10 @@ class RepositoryRoot extends React.Component<Props> {
/>
)}
/>
<ExtensionPoint name="repository.route"
props={extensionProps}
renderAll={true}
/>
</Switch>
</div>
<div className="column">
@@ -186,11 +197,15 @@ class RepositoryRoot extends React.Component<Props> {
label={t("repository-root.sources")}
activeOnlyWhenExact={false}
/>
<EditNavLink repository={repository} editUrl={`${url}/edit`} />
<ExtensionPoint name="repository.navigation"
props={extensionProps}
renderAll={true}
/>
<PermissionsNavLink
permissionUrl={`${url}/permissions`}
repository={repository}
/>
<EditNavLink repository={repository} editUrl={`${url}/edit`} />
</Section>
<Section label={t("repository-root.actions-label")}>
<DeleteNavAction repository={repository} delete={this.delete} />

View File

@@ -38,7 +38,7 @@ class UserForm extends React.Component<Props, State> {
mail: "",
password: "",
admin: false,
active: false,
active: true,
_links: {}
},
mailValidationError: false,
@@ -73,7 +73,8 @@ class UserForm extends React.Component<Props, State> {
this.state.passwordConfirmationError ||
this.state.displayNameValidationError ||
this.isFalsy(user.name) ||
this.isFalsy(user.displayName)
this.isFalsy(user.displayName) ||
this.isFalsy(user.mail)
);
};

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

@@ -26,7 +26,11 @@ public class ContextualExceptionMapper<E extends ExceptionWithContext> implement
@Override
public Response toResponse(E exception) {
logger.debug("map {} to status code {}", type.getSimpleName(), status.getStatusCode(), 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)

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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -79,7 +79,7 @@ public class PermissionRootResource {
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();
@@ -209,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);

View File

@@ -5,12 +5,15 @@ import com.webcohesion.enunciate.metadata.rs.ResponseHeader;
import com.webcohesion.enunciate.metadata.rs.ResponseHeaders;
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import com.webcohesion.enunciate.metadata.rs.TypeHint;
import org.apache.shiro.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;
import javax.inject.Named;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.DefaultValue;
@@ -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;
@@ -89,7 +94,17 @@ public class RepositoryCollectionResource {
@ResponseHeaders(@ResponseHeader(name = "Location", description = "uri to the created repository"))
public Response create(@Valid RepositoryDto repository) {
return adapter.create(repository,
() -> dtoToRepositoryMapper.map(repository, null),
() -> 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

@@ -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

@@ -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

@@ -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;
}
@@ -83,8 +79,6 @@ public class SecurityFilter extends HttpFilter
protected void doFilter(HttpServletRequest request,
HttpServletResponse response, FilterChain chain)
throws IOException, ServletException
{
if (!SecurityRequests.isAuthenticationRequest(request) && !SecurityRequests.isIndexRequest(request))
{
Subject subject = SecurityUtils.getSubject();
if (hasPermission(subject))
@@ -94,21 +88,8 @@ public class SecurityFilter extends HttpFilter
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

@@ -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

@@ -19,8 +19,6 @@ import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.NotFoundException;
import sonia.scm.api.rest.AuthorizationExceptionMapper;
import sonia.scm.api.rest.ContextualExceptionMapper;
import sonia.scm.api.rest.IllegalArgumentExceptionMapper;
import sonia.scm.api.v2.NotFoundExceptionMapper;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Repository;
@@ -78,7 +76,6 @@ public class DiffResourceTest extends RepositoryTestBase {
dispatcher.getProviderFactory().register(new NotFoundExceptionMapper(mapper));
dispatcher.getProviderFactory().registerProvider(AuthorizationExceptionMapper.class);
dispatcher.getProviderFactory().registerProvider(CRLFInjectionExceptionMapper.class);
dispatcher.getProviderFactory().registerProvider(IllegalArgumentExceptionMapper.class);
when(service.getDiffCommand()).thenReturn(diffCommandBuilder);
subjectThreadState.bind();
ThreadContext.bind(subject);

View File

@@ -5,7 +5,6 @@ import org.jboss.resteasy.mock.MockDispatcherFactory;
import sonia.scm.api.rest.AlreadyExistsExceptionMapper;
import sonia.scm.api.rest.AuthorizationExceptionMapper;
import sonia.scm.api.rest.ConcurrentModificationExceptionMapper;
import sonia.scm.api.rest.IllegalArgumentExceptionMapper;
import sonia.scm.api.v2.NotFoundExceptionMapper;
import sonia.scm.api.v2.NotSupportedFeatureExceptionMapper;
@@ -21,7 +20,6 @@ public class DispatcherMock {
dispatcher.getProviderFactory().register(new InternalRepositoryExceptionMapper(mapper));
dispatcher.getProviderFactory().register(new ChangePasswordNotAllowedExceptionMapper(mapper));
dispatcher.getProviderFactory().register(new InvalidPasswordExceptionMapper(mapper));
dispatcher.getProviderFactory().registerProvider(IllegalArgumentExceptionMapper.class);
dispatcher.getProviderFactory().register(new NotSupportedFeatureExceptionMapper(mapper));
return dispatcher;
}

View File

@@ -402,18 +402,17 @@ public class PermissionRootResourceTest extends RepositoryTestBase {
}
private Repository createUserWithRepository(String userPermission) {
Repository mockRepository = mock(Repository.class);
when(mockRepository.getId()).thenReturn(REPOSITORY_NAME);
when(mockRepository.getNamespace()).thenReturn(REPOSITORY_NAMESPACE);
when(mockRepository.getName()).thenReturn(REPOSITORY_NAME);
when(mockRepository.getNamespaceAndName()).thenReturn(new NamespaceAndName(REPOSITORY_NAMESPACE, REPOSITORY_NAME));
Repository mockRepository = new Repository();
mockRepository.setId(REPOSITORY_NAME);
mockRepository.setNamespace(REPOSITORY_NAMESPACE);
mockRepository.setName(REPOSITORY_NAME);
when(repositoryManager.get(any(NamespaceAndName.class))).thenReturn(mockRepository);
when(subject.isPermitted(userPermission != null ? eq(userPermission) : any(String.class))).thenReturn(true);
return mockRepository;
}
private void createUserWithRepositoryAndPermissions(ArrayList<Permission> permissions, String userPermission) {
when(createUserWithRepository(userPermission).getPermissions()).thenReturn(permissions);
createUserWithRepository(userPermission).setPermissions(permissions);
}
private Stream<DynamicTest> createDynamicTestsToAssertResponses(ExpectedRequest... expectedRequests) {
@@ -421,10 +420,9 @@ public class PermissionRootResourceTest extends RepositoryTestBase {
.map(entry -> dynamicTest("the endpoint " + entry.description + " should return the status code " + entry.expectedResponseStatus, () -> assertExpectedRequest(entry)));
}
private MockHttpResponse assertExpectedRequest(ExpectedRequest entry) throws URISyntaxException {
private void assertExpectedRequest(ExpectedRequest entry) throws URISyntaxException {
MockHttpResponse response = new MockHttpResponse();
HttpRequest request = null;
request = MockHttpRequest
HttpRequest request = MockHttpRequest
.create(entry.method, "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + entry.path)
.content(entry.content)
.contentType(VndMediaType.PERMISSION);
@@ -436,7 +434,6 @@ public class PermissionRootResourceTest extends RepositoryTestBase {
if (entry.responseValidator != null) {
entry.responseValidator.accept(response);
}
return response;
}
@ToString
@@ -470,12 +467,12 @@ public class PermissionRootResourceTest extends RepositoryTestBase {
return this;
}
public ExpectedRequest expectedResponseStatus(int expectedResponseStatus) {
ExpectedRequest expectedResponseStatus(int expectedResponseStatus) {
this.expectedResponseStatus = expectedResponseStatus;
return this;
}
public ExpectedRequest responseValidator(Consumer<MockHttpResponse> responseValidator) {
ExpectedRequest responseValidator(Consumer<MockHttpResponse> responseValidator) {
this.responseValidator = responseValidator;
return this;
}

View File

@@ -4,6 +4,9 @@ import com.github.sdorra.shiro.ShiroRule;
import com.github.sdorra.shiro.SubjectAware;
import com.google.common.io.Resources;
import com.google.inject.util.Providers;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.assertj.core.api.Assertions;
import org.jboss.resteasy.core.Dispatcher;
import org.jboss.resteasy.mock.MockHttpRequest;
import org.jboss.resteasy.mock.MockHttpResponse;
@@ -22,6 +25,7 @@ import sonia.scm.repository.RepositoryIsNotArchivedException;
import sonia.scm.repository.RepositoryManager;
import sonia.scm.repository.api.RepositoryService;
import sonia.scm.repository.api.RepositoryServiceFactory;
import sonia.scm.user.User;
import sonia.scm.web.VndMediaType;
import javax.servlet.http.HttpServletResponse;
@@ -37,6 +41,7 @@ import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
import static javax.servlet.http.HttpServletResponse.SC_OK;
import static javax.servlet.http.HttpServletResponse.SC_PRECONDITION_FAILED;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@@ -59,6 +64,8 @@ import static sonia.scm.api.v2.resources.DispatcherMock.createDispatcher;
)
public class RepositoryRootResourceTest extends RepositoryTestBase {
private static final String REALM = "AdminRealm";
private Dispatcher dispatcher;
@Rule
@@ -96,6 +103,13 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
when(serviceFactory.create(any(Repository.class))).thenReturn(service);
when(scmPathInfoStore.get()).thenReturn(uriInfo);
when(uriInfo.getApiRestUri()).thenReturn(URI.create("/x/y"));
SimplePrincipalCollection trillian = new SimplePrincipalCollection("trillian", REALM);
trillian.add(new User("trillian"), REALM);
shiro.setSubject(
new Subject.Builder()
.principals(trillian)
.authenticated(true)
.buildSubject());
}
@Test
@@ -257,6 +271,34 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
verify(repositoryManager).create(any(Repository.class));
}
@Test
public void shouldSetCurrentUserAsOwner() throws Exception {
ArgumentCaptor<Repository> createCaptor = ArgumentCaptor.forClass(Repository.class);
when(repositoryManager.create(createCaptor.capture())).thenAnswer(invocation -> {
Repository repository = (Repository) invocation.getArguments()[0];
repository.setNamespace("otherspace");
return repository;
});
URL url = Resources.getResource("sonia/scm/api/v2/repository-test-update.json");
byte[] repositoryJson = Resources.toByteArray(url);
MockHttpRequest request = MockHttpRequest
.post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2)
.contentType(VndMediaType.REPOSITORY)
.content(repositoryJson);
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
Assertions.assertThat(createCaptor.getValue().getPermissions())
.hasSize(1)
.allSatisfy(p -> {
assertThat(p.getName()).isEqualTo("trillian");
assertThat(p.getType()).isEqualTo(PermissionType.OWNER);
});
}
@Test
public void shouldNotOverwriteExistingPermissionsOnUpdate() throws Exception {
Repository existingRepository = mockRepository("space", "repo");

View File

@@ -103,7 +103,7 @@ public class UserRootResourceTest {
@Test
public void shouldGet400OnCreatingNewUserWithNotAllowedCharacters() throws URISyntaxException {
// the @ character at the begin of the name is not allowed
String userJson = "{ \"name\": \"@user\", \"type\": \"db\" }";
String userJson = "{ \"name\": \"@user\",\"active\": true,\"admin\": false,\"displayName\": \"someone\",\"mail\": \"x@example.com\",\"type\": \"db\" }";
MockHttpRequest request = MockHttpRequest
.post("/" + UserRootResource.USERS_PATH_V2)
.contentType(VndMediaType.USER)
@@ -115,7 +115,7 @@ public class UserRootResourceTest {
assertEquals(400, response.getStatus());
// the whitespace at the begin opf the name is not allowed
userJson = "{ \"name\": \" user\", \"type\": \"db\" }";
userJson = "{ \"name\": \" user\",\"active\": true,\"admin\": false,\"displayName\": \"someone\",\"mail\": \"x@example.com\",\"type\": \"db\" }";
request = MockHttpRequest
.post("/" + UserRootResource.USERS_PATH_V2)
.contentType(VndMediaType.USER)

View File

@@ -1,85 +0,0 @@
/**
* Copyright (c) 2014, 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;
import com.github.sdorra.shiro.ShiroRule;
import com.github.sdorra.shiro.SubjectAware;
import org.apache.shiro.SecurityUtils;
import org.junit.Test;
import static org.junit.Assert.*;
import org.junit.Before;
import org.junit.Rule;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.config.ScmConfiguration;
/**
* Unit tests for {@link AdminSecurityFilter}.
*
* @author Sebastian Sdorra
*/
@RunWith(MockitoJUnitRunner.class)
@SubjectAware(configuration = "classpath:sonia/scm/shiro-001.ini")
public class AdminSecurityFilterTest {
private AdminSecurityFilter securityFilter;
@Rule
public ShiroRule shiro = new ShiroRule();
/**
* Prepare object under test and mocks.
*/
@Before
public void setUp(){
this.securityFilter = new AdminSecurityFilter(new ScmConfiguration());
}
/**
* Tests {@link AdminSecurityFilter#hasPermission(org.apache.shiro.subject.Subject)} as administrator.
*/
@Test
@SubjectAware(username = "dent", password = "secret")
public void testHasPermissionAsAdministrator() {
assertTrue(securityFilter.hasPermission(SecurityUtils.getSubject()));
}
/**
* Tests {@link AdminSecurityFilter#hasPermission(org.apache.shiro.subject.Subject)} as user.
*/
@Test
@SubjectAware(username = "trillian", password = "secret")
public void testHasPermissionAsUser() {
assertFalse(securityFilter.hasPermission(SecurityUtils.getSubject()));
}
}

View File

@@ -33,38 +33,38 @@ package sonia.scm.filter;
import com.github.sdorra.shiro.ShiroRule;
import com.github.sdorra.shiro.SubjectAware;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.junit.Test;
import static org.junit.Assert.*;
import static org.hamcrest.Matchers.*;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.Mockito;
import static org.mockito.Mockito.*;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.SCMContext;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.user.User;
import sonia.scm.user.UserTestData;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.verify;
/**
* Unit tests for {@link SecurityFilter}.
* Unit tests for {@link PropagatePrincipleFilter}.
*
* @author Sebastian Sdorra
*/
@RunWith(MockitoJUnitRunner.class)
@SubjectAware(configuration = "classpath:sonia/scm/shiro-001.ini")
public class SecurityFilterTest {
public class PropagatePrincipleFilterTest {
@Mock
private HttpServletRequest request;
@@ -83,7 +83,7 @@ public class SecurityFilterTest {
private ScmConfiguration configuration;
private SecurityFilter securityFilter;
private PropagatePrincipleFilter propagatePrincipleFilter;
@Rule
public ShiroRule shiro = new ShiroRule();
@@ -94,38 +94,7 @@ public class SecurityFilterTest {
@Before
public void setUp(){
this.configuration = new ScmConfiguration();
this.securityFilter = new SecurityFilter(configuration);
when(request.getContextPath()).thenReturn("/scm");
}
/**
* Tests filter on authentication endpoint v1.
*
* @throws IOException
* @throws ServletException
*/
@Test
public void testDoOnAuthenticationUrlV1() throws IOException, ServletException {
checkIfAuthenticationUrlIsPassedThrough("/scm/api/auth/access_token");
}
/**
* Tests filter on authentication endpoint v2.
*
* @throws IOException
* @throws ServletException
*/
@Test
public void testDoOnAuthenticationUrlV2() throws IOException, ServletException {
checkIfAuthenticationUrlIsPassedThrough("/scm/api/v2/auth/access_token");
}
private void checkIfAuthenticationUrlIsPassedThrough(String uri) throws IOException, ServletException {
when(request.getRequestURI()).thenReturn(uri);
securityFilter.doFilter(request, response, chain);
verify(request, never()).setAttribute(Mockito.anyString(), Mockito.any());
verify(chain).doFilter(request, response);
this.propagatePrincipleFilter = new PropagatePrincipleFilter(configuration);
}
/**
@@ -136,8 +105,7 @@ public class SecurityFilterTest {
*/
@Test
public void testAnonymous() throws IOException, ServletException {
when(request.getRequestURI()).thenReturn("/scm/api");
securityFilter.doFilter(request, response, chain);
propagatePrincipleFilter.doFilter(request, response, chain);
response.sendError(HttpServletResponse.SC_FORBIDDEN);
}
@@ -149,14 +117,13 @@ public class SecurityFilterTest {
*/
@Test
public void testAnonymousWithAccessEnabled() throws IOException, ServletException {
when(request.getRequestURI()).thenReturn("/scm/api");
configuration.setAnonymousAccessEnabled(true);
// execute
securityFilter.doFilter(request, response, chain);
propagatePrincipleFilter.doFilter(request, response, chain);
// verify and capture
verify(request).setAttribute(SecurityFilter.ATTRIBUTE_REMOTE_USER, SCMContext.USER_ANONYMOUS);
verify(request).setAttribute(PropagatePrincipleFilter.ATTRIBUTE_REMOTE_USER, SCMContext.USER_ANONYMOUS);
verify(chain).doFilter(requestCaptor.capture(), responseCaptor.capture());
// assert
@@ -173,13 +140,12 @@ public class SecurityFilterTest {
@Test
public void testAuthenticated() throws IOException, ServletException {
authenticateUser(UserTestData.createTrillian());
when(request.getRequestURI()).thenReturn("/scm/api");
// execute
securityFilter.doFilter(request, response, chain);
propagatePrincipleFilter.doFilter(request, response, chain);
// verify and capture
verify(request).setAttribute(SecurityFilter.ATTRIBUTE_REMOTE_USER, "trillian");
verify(request).setAttribute(PropagatePrincipleFilter.ATTRIBUTE_REMOTE_USER, "trillian");
verify(chain).doFilter(requestCaptor.capture(), responseCaptor.capture());
// assert
@@ -187,42 +153,6 @@ public class SecurityFilterTest {
assertEquals("trillian", captured.getRemoteUser());
}
/**
* Tests filter without permissions.
*
* @throws IOException
* @throws ServletException
*/
@Test
public void testForbidden() throws IOException, ServletException {
authenticateUser(UserTestData.createTrillian());
when(request.getRequestURI()).thenReturn("/scm/api");
// execute
securityFilter = new AccessForbiddenSecurityFilter(configuration);
securityFilter.doFilter(request, response, chain);
// assert
verify(response).sendError(HttpServletResponse.SC_FORBIDDEN);
}
/**
* Tests filter unauthenticated and without permissions.
*
* @throws IOException
* @throws ServletException
*/
@Test
public void testUnauthorized() throws IOException, ServletException {
when(request.getRequestURI()).thenReturn("/scm/api");
// execute
securityFilter = new AccessForbiddenSecurityFilter(configuration);
securityFilter.doFilter(request, response, chain);
// assert
verify(response).sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
private void authenticateUser(User user) {
SimplePrincipalCollection spc = new SimplePrincipalCollection();
@@ -236,18 +166,4 @@ public class SecurityFilterTest {
shiro.setSubject(subject);
}
private static class AccessForbiddenSecurityFilter extends SecurityFilter {
private AccessForbiddenSecurityFilter(ScmConfiguration configuration) {
super(configuration);
}
@Override
protected boolean hasPermission(Subject subject) {
return false;
}
}
}

View File

@@ -184,7 +184,7 @@ private long calculateAverage(List<Long> times) {
private Repository createTestRepository(int number) {
Repository repository = new Repository(keyGenerator.createKey(), REPOSITORY_TYPE, "namespace", "repo-" + number);
repository.getPermissions().add(new Permission("trillian", PermissionType.READ));
repository.addPermission(new Permission("trillian", PermissionType.READ));
return repository;
}

Some files were not shown because too many files have changed in this diff Show More