diff --git a/Jenkinsfile b/Jenkinsfile index 0f586bf346..36135cf4a4 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -7,12 +7,15 @@ import com.cloudogu.ces.cesbuildlib.* node('docker') { // Change this as when we go back to default - necessary for proper SonarQube analysis - mainBranch = "2.0.0-m3" + mainBranch = '2.0.0-m3' properties([ // Keep only the last 10 build to preserve space buildDiscarder(logRotator(numToKeepStr: '10')), - disableConcurrentBuilds() + disableConcurrentBuilds(), + parameters([ + string(name: 'dockerTag', trim: true, defaultValue: 'latest', description: 'Extra Docker Tag for cloudogu/scm-manager image') + ]) ]) timeout(activity: true, time: 30, unit: 'MINUTES') { @@ -51,9 +54,9 @@ node('docker') { if (isMainBranch()) { -// stage('Lifecycle') { -// nexusPolicyEvaluation iqApplication: selectedApplication('scm'), iqScanPatterns: [[scanPattern: 'scm-server/target/scm-server-app.zip']], iqStage: 'build' -// } + stage('Lifecycle') { + nexusPolicyEvaluation iqApplication: selectedApplication('scm'), iqScanPatterns: [[scanPattern: 'scm-server/target/scm-server-app.zip']], iqStage: 'build' + } stage('Archive') { archiveArtifacts 'scm-webapp/target/scm-webapp.war' @@ -66,6 +69,13 @@ node('docker') { docker.withRegistry('', 'hub.docker.com-cesmarvin') { image.push(dockerImageTag) image.push('latest') + if (!'latest'.equals(params.dockerTag)) { + image.push(params.dockerTag) + + def newDockerTag = "2.0.0-${commitHash.substring(0,7)}-dev-${params.dockerTag}" + currentBuild.description = newDockerTag + image.push(newDockerTag) + } } } @@ -92,7 +102,7 @@ String mainBranch Maven setupMavenBuild() { // Keep this version number in sync with .mvn/maven-wrapper.properties - Maven mvn = new MavenInDocker(this, "3.5.2-jdk-8") + Maven mvn = new MavenInDocker(this, '3.5.2-jdk-8') if (isMainBranch()) { // Release starts javadoc, which takes very long, so do only for certain branches diff --git a/docs/logo/favicon_16x16px.ico b/docs/logo/favicon_16x16px.ico new file mode 100644 index 0000000000..3436795fdf Binary files /dev/null and b/docs/logo/favicon_16x16px.ico differ diff --git a/docs/logo/favicon_16x16px_transparent.ico b/docs/logo/favicon_16x16px_transparent.ico new file mode 100644 index 0000000000..e5803f340d Binary files /dev/null and b/docs/logo/favicon_16x16px_transparent.ico differ diff --git a/docs/logo/scm-manager_logo.ai b/docs/logo/scm-manager_logo.ai new file mode 100644 index 0000000000..6fe98a15e0 --- /dev/null +++ b/docs/logo/scm-manager_logo.ai @@ -0,0 +1 @@ +%!PS-Adobe-2.0 %%Creator: Adobe Photoshop(TM) Pen Path Export 7.0 %%Title: (scm-manager_logo.ai) %%DocumentNeededResources: procset Adobe_packedarray 2.0 0 %%+ procset Adobe_IllustratorA_AI3 1.0 1 %%ColorUsage: Black&White %%BoundingBox: 0 0 800 275 %%HiResBoundingBox: 0 0 800 275 %AI3_Cropmarks: 0 0 800 275 %%DocumentPreview: None %%EndComments %%BeginProlog %%IncludeResource: procset Adobe_packedarray 2.0 0 Adobe_packedarray /initialize get exec %%IncludeResource: procset Adobe_IllustratorA_AI3 1.0 1 %%EndProlog %%BeginSetup Adobe_IllustratorA_AI3 /initialize get exec n %%EndSetup 0.0 0.0 0.0 1.0 k 0 i 0 J 0 j 1 w 4 M []0 d %%Note: %%Trailer %%EOF \ No newline at end of file diff --git a/docs/logo/scm-manager_logo.jpg b/docs/logo/scm-manager_logo.jpg new file mode 100644 index 0000000000..f518e3f2b2 Binary files /dev/null and b/docs/logo/scm-manager_logo.jpg differ diff --git a/docs/logo/scm-manager_logo.png b/docs/logo/scm-manager_logo.png new file mode 100644 index 0000000000..90a17c7ee4 Binary files /dev/null and b/docs/logo/scm-manager_logo.png differ diff --git a/docs/logo/scm-manager_logo_img.jpg b/docs/logo/scm-manager_logo_img.jpg new file mode 100644 index 0000000000..f2d3b35b66 Binary files /dev/null and b/docs/logo/scm-manager_logo_img.jpg differ diff --git a/docs/logo/scm-manager_logo_img.png b/docs/logo/scm-manager_logo_img.png new file mode 100644 index 0000000000..f349fc5d22 Binary files /dev/null and b/docs/logo/scm-manager_logo_img.png differ diff --git a/docs/logo/scm-manager_logo_img_neg.jpg b/docs/logo/scm-manager_logo_img_neg.jpg new file mode 100644 index 0000000000..1b56b5e627 Binary files /dev/null and b/docs/logo/scm-manager_logo_img_neg.jpg differ diff --git a/docs/logo/scm-manager_logo_img_neg.png b/docs/logo/scm-manager_logo_img_neg.png new file mode 100644 index 0000000000..796a02882c Binary files /dev/null and b/docs/logo/scm-manager_logo_img_neg.png differ diff --git a/docs/logo/scm-manager_logo_neg.jpg b/docs/logo/scm-manager_logo_neg.jpg new file mode 100644 index 0000000000..0d6704d0e3 Binary files /dev/null and b/docs/logo/scm-manager_logo_neg.jpg differ diff --git a/docs/logo/scm-manager_logo_neg.png b/docs/logo/scm-manager_logo_neg.png new file mode 100644 index 0000000000..3eb75aa3ee Binary files /dev/null and b/docs/logo/scm-manager_logo_neg.png differ diff --git a/docs/logo/scm-manager_logo_neg1.jpg b/docs/logo/scm-manager_logo_neg1.jpg new file mode 100644 index 0000000000..d776c6b6e6 Binary files /dev/null and b/docs/logo/scm-manager_logo_neg1.jpg differ diff --git a/docs/logo/scm-manager_logo_neg1.png b/docs/logo/scm-manager_logo_neg1.png new file mode 100644 index 0000000000..817785710b Binary files /dev/null and b/docs/logo/scm-manager_logo_neg1.png differ diff --git a/docs/logo/scm-manager_logo_pos1.jpg b/docs/logo/scm-manager_logo_pos1.jpg new file mode 100644 index 0000000000..fd2533c198 Binary files /dev/null and b/docs/logo/scm-manager_logo_pos1.jpg differ diff --git a/docs/logo/scm-manager_logo_pos1.png b/docs/logo/scm-manager_logo_pos1.png new file mode 100644 index 0000000000..b911417358 Binary files /dev/null and b/docs/logo/scm-manager_logo_pos1.png differ diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/HalAppender.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/HalAppender.java index 6afb542646..b313f68af8 100644 --- a/scm-core/src/main/java/sonia/scm/api/v2/resources/HalAppender.java +++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/HalAppender.java @@ -2,6 +2,8 @@ package sonia.scm.api.v2.resources; import de.otto.edison.hal.HalRepresentation; +import java.util.List; + /** * The {@link HalAppender} can be used within an {@link HalEnricher} to append hateoas links to a json response. * @@ -34,6 +36,14 @@ public interface HalAppender { */ void appendEmbedded(String rel, HalRepresentation embeddedItem); + /** + * Appends a list of embedded objects to the json response. + * + * @param rel name of relation + * @param embeddedItems embedded objects + */ + void appendEmbedded(String rel, List embeddedItems); + /** * Builder for link arrays. */ diff --git a/scm-core/src/main/java/sonia/scm/group/ExternalGroupNames.java b/scm-core/src/main/java/sonia/scm/group/ExternalGroupNames.java deleted file mode 100644 index 179e488236..0000000000 --- a/scm-core/src/main/java/sonia/scm/group/ExternalGroupNames.java +++ /dev/null @@ -1,22 +0,0 @@ -package sonia.scm.group; - -import java.util.Collection; - -/** - * This class represents all associated groups which are provided by external systems for a certain user. - * - * @author Sebastian Sdorra - * @since 2.0.0 - */ -public class ExternalGroupNames extends GroupNames { - public ExternalGroupNames() { - } - - public ExternalGroupNames(String groupName, String... groupNames) { - super(groupName, groupNames); - } - - public ExternalGroupNames(Collection collection) { - super(collection); - } -} diff --git a/scm-core/src/main/java/sonia/scm/group/GroupCollector.java b/scm-core/src/main/java/sonia/scm/group/GroupCollector.java new file mode 100644 index 0000000000..4546db1bc4 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/group/GroupCollector.java @@ -0,0 +1,10 @@ +package sonia.scm.group; + +import java.util.Set; + +public interface GroupCollector { + + String AUTHENTICATED = "_authenticated"; + + Set collect(String principal); +} diff --git a/scm-core/src/main/java/sonia/scm/group/GroupNames.java b/scm-core/src/main/java/sonia/scm/group/GroupNames.java deleted file mode 100644 index c28f9f5ef1..0000000000 --- a/scm-core/src/main/java/sonia/scm/group/GroupNames.java +++ /dev/null @@ -1,187 +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.group; - -//~--- non-JDK imports -------------------------------------------------------- - -import com.google.common.base.Joiner; -import com.google.common.base.Objects; -import com.google.common.collect.Lists; - -import java.io.Serializable; -import java.util.Collection; -import java.util.Collections; -import java.util.Iterator; - -//~--- JDK imports ------------------------------------------------------------ - -/** - * This class represents all associated groups for a user. - * - * @author Sebastian Sdorra - * @since 1.21 - */ -public class GroupNames implements Serializable, Iterable -{ - - /** - * Group for all authenticated users - * @since 1.31 - */ - public static final String AUTHENTICATED = "_authenticated"; - - /** Field description */ - private static final long serialVersionUID = 8615685985213897947L; - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - */ - public GroupNames() - { - this(Collections.emptyList()); - } - - /** - * Constructs ... - * - * - * @param groupName - * @param groupNames - */ - public GroupNames(String groupName, String... groupNames) - { - this(Lists.asList(groupName, groupNames)); - } - - /** - * Constructs ... - * - * - * @param collection - */ - public GroupNames(Collection collection) - { - this.collection = Collections.unmodifiableCollection(collection); - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param groupName - * - * @return - */ - public boolean contains(String groupName) - { - return collection.contains(groupName); - } - - /** - * {@inheritDoc} - */ - @Override - public boolean equals(Object obj) - { - if (obj == null) - { - return false; - } - - if (getClass() != obj.getClass()) - { - return false; - } - - final GroupNames other = (GroupNames) obj; - - return Objects.equal(collection, other.collection); - } - - /** - * {@inheritDoc} - */ - @Override - public int hashCode() - { - return Objects.hashCode(collection); - } - - /** - * Method description - * - * - * @return - */ - @Override - public Iterator iterator() - { - return collection.iterator(); - } - - /** - * Method description - * - * - * @return - */ - @Override - public String toString() - { - return Joiner.on(", ").join(collection); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - public Collection getCollection() - { - return collection; - } - - - //~--- fields --------------------------------------------------------------- - /** Field description */ - private final Collection collection; -} diff --git a/scm-core/src/main/java/sonia/scm/group/GroupResolver.java b/scm-core/src/main/java/sonia/scm/group/GroupResolver.java new file mode 100644 index 0000000000..5aba63c93b --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/group/GroupResolver.java @@ -0,0 +1,10 @@ +package sonia.scm.group; + +import sonia.scm.plugin.ExtensionPoint; + +import java.util.Set; + +@ExtensionPoint +public interface GroupResolver { + Set resolve(String principal); +} diff --git a/scm-core/src/main/java/sonia/scm/migration/MigrationDAO.java b/scm-core/src/main/java/sonia/scm/migration/MigrationDAO.java new file mode 100644 index 0000000000..acc37ce8ee --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/migration/MigrationDAO.java @@ -0,0 +1,7 @@ +package sonia.scm.migration; + +import java.util.Collection; + +public interface MigrationDAO { + Collection getAll(); +} diff --git a/scm-core/src/main/java/sonia/scm/migration/MigrationInfo.java b/scm-core/src/main/java/sonia/scm/migration/MigrationInfo.java new file mode 100644 index 0000000000..19f5d8ba3a --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/migration/MigrationInfo.java @@ -0,0 +1,38 @@ +package sonia.scm.migration; + +public class MigrationInfo { + + private final String id; + private final String protocol; + private final String originalRepositoryName; + private final String namespace; + private final String name; + + public MigrationInfo(String id, String protocol, String originalRepositoryName, String namespace, String name) { + this.id = id; + this.protocol = protocol; + this.originalRepositoryName = originalRepositoryName; + this.namespace = namespace; + this.name = name; + } + + public String getId() { + return id; + } + + public String getProtocol() { + return protocol; + } + + public String getOriginalRepositoryName() { + return originalRepositoryName; + } + + public String getNamespace() { + return namespace; + } + + public String getName() { + return name; + } +} diff --git a/scm-core/src/main/java/sonia/scm/migration/UpdateException.java b/scm-core/src/main/java/sonia/scm/migration/UpdateException.java index 4023620b96..946db59536 100644 --- a/scm-core/src/main/java/sonia/scm/migration/UpdateException.java +++ b/scm-core/src/main/java/sonia/scm/migration/UpdateException.java @@ -1,11 +1,17 @@ package sonia.scm.migration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + public class UpdateException extends RuntimeException { + private static Logger LOG = LoggerFactory.getLogger(UpdateException.class); + public UpdateException(String message) { super(message); } public UpdateException(String message, Throwable cause) { super(message, cause); + LOG.error(message, cause); } } diff --git a/scm-core/src/main/java/sonia/scm/repository/Repository.java b/scm-core/src/main/java/sonia/scm/repository/Repository.java index 5bb50db06f..8c7000c25a 100644 --- a/scm-core/src/main/java/sonia/scm/repository/Repository.java +++ b/scm-core/src/main/java/sonia/scm/repository/Repository.java @@ -82,7 +82,7 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per private String namespace; private String name; @XmlElement(name = "permission") - private final Set permissions = new HashSet<>(); + private Set permissions = new HashSet<>(); @XmlElement(name = "public") private boolean publicReadable = false; private boolean archived = false; @@ -331,6 +331,8 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per try { repository = (Repository) super.clone(); + // fix permission reference on clone + repository.permissions = new HashSet<>(permissions); } catch (CloneNotSupportedException ex) { throw new RuntimeException(ex); } diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryLocationResolver.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryLocationResolver.java index 1b7da51c4c..bdd7a03d62 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryLocationResolver.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryLocationResolver.java @@ -1,5 +1,7 @@ package sonia.scm.repository; +import java.util.function.BiConsumer; + public abstract class RepositoryLocationResolver { public abstract boolean supportsLocationType(Class type); @@ -35,5 +37,12 @@ public abstract class RepositoryLocationResolver { * @throws IllegalStateException when there already is a location for the given repository registered. */ void setLocation(String repositoryId, T location); + + /** + * Iterates all repository locations known to this resolver instance and calls the consumer giving the repository id + * and its location for each repository. + * @param consumer This callback will be called for each repository with the repository id and its location. + */ + void forAllLocations(BiConsumer consumer); } } diff --git a/scm-core/src/main/java/sonia/scm/repository/api/AbstractDiffCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/AbstractDiffCommandBuilder.java new file mode 100644 index 0000000000..b5b2f2a08b --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/api/AbstractDiffCommandBuilder.java @@ -0,0 +1,68 @@ +package sonia.scm.repository.api; + +import sonia.scm.FeatureNotSupportedException; +import sonia.scm.repository.Feature; +import sonia.scm.repository.spi.DiffCommandRequest; + +import java.util.Set; + +abstract class AbstractDiffCommandBuilder { + + + /** request for the diff command implementation */ + final DiffCommandRequest request = new DiffCommandRequest(); + + private final Set supportedFeatures; + + AbstractDiffCommandBuilder(Set supportedFeatures) { + this.supportedFeatures = supportedFeatures; + } + + /** + * Compute the incoming changes of the branch set with {@link #setRevision(String)} in respect to the changeset given + * here. In other words: What changes would be new to the ancestor changeset given here when the branch would + * be merged into it. Requires feature {@link sonia.scm.repository.Feature#INCOMING_REVISION}! + * + * @return {@code this} + */ + public T setAncestorChangeset(String revision) + { + if (!supportedFeatures.contains(Feature.INCOMING_REVISION)) { + throw new FeatureNotSupportedException(Feature.INCOMING_REVISION.name()); + } + request.setAncestorChangeset(revision); + + return self(); + } + + /** + * Show the difference only for the given path. + * + * + * @param path path for difference + * + * @return {@code this} + */ + public T setPath(String path) + { + request.setPath(path); + return self(); + } + + /** + * Show the difference only for the given revision or (using {@link #setAncestorChangeset(String)}) between this + * and another revision. + * + * + * @param revision revision for difference + * + * @return {@code this} + */ + public T setRevision(String revision) + { + request.setRevision(revision); + return self(); + } + + abstract T self(); +} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/Command.java b/scm-core/src/main/java/sonia/scm/repository/api/Command.java index e380727769..3249e54ec3 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/Command.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/Command.java @@ -53,11 +53,6 @@ public enum Command */ BRANCHES, - /** - * @since 2.0 - */ - BRANCH, - /** * @since 1.31 */ @@ -71,10 +66,5 @@ public enum Command /** * @since 2.0 */ - MODIFICATIONS, - - /** - * @since 2.0 - */ - MERGE + MODIFICATIONS, MERGE, DIFF_RESULT, BRANCH; } diff --git a/scm-core/src/main/java/sonia/scm/repository/api/DiffCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/DiffCommandBuilder.java index 9e7094d5bf..18d4e11a7f 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/DiffCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/DiffCommandBuilder.java @@ -38,10 +38,8 @@ package sonia.scm.repository.api; import com.google.common.base.Preconditions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sonia.scm.FeatureNotSupportedException; import sonia.scm.repository.Feature; import sonia.scm.repository.spi.DiffCommand; -import sonia.scm.repository.spi.DiffCommandRequest; import sonia.scm.util.IOUtil; import java.io.ByteArrayOutputStream; @@ -72,7 +70,7 @@ import java.util.Set; * @author Sebastian Sdorra * @since 1.17 */ -public final class DiffCommandBuilder +public final class DiffCommandBuilder extends AbstractDiffCommandBuilder { /** @@ -81,6 +79,9 @@ public final class DiffCommandBuilder private static final Logger logger = LoggerFactory.getLogger(DiffCommandBuilder.class); + /** implementation of the diff command */ + private final DiffCommand diffCommand; + //~--- constructors --------------------------------------------------------- /** @@ -92,8 +93,8 @@ public final class DiffCommandBuilder */ DiffCommandBuilder(DiffCommand diffCommand, Set supportedFeatures) { + super(supportedFeatures); this.diffCommand = diffCommand; - this.supportedFeatures = supportedFeatures; } //~--- methods -------------------------------------------------------------- @@ -162,54 +163,6 @@ public final class DiffCommandBuilder return this; } - - /** - * Show the difference only for the given path. - * - * - * @param path path for difference - * - * @return {@code this} - */ - public DiffCommandBuilder setPath(String path) - { - request.setPath(path); - - return this; - } - - /** - * Show the difference only for the given revision or (using {@link #setAncestorChangeset(String)}) between this - * and another revision. - * - * - * @param revision revision for difference - * - * @return {@code this} - */ - public DiffCommandBuilder setRevision(String revision) - { - request.setRevision(revision); - - return this; - } - /** - * Compute the incoming changes of the branch set with {@link #setRevision(String)} in respect to the changeset given - * here. In other words: What changes would be new to the ancestor changeset given here when the branch would - * be merged into it. Requires feature {@link sonia.scm.repository.Feature#INCOMING_REVISION}! - * - * @return {@code this} - */ - public DiffCommandBuilder setAncestorChangeset(String revision) - { - if (!supportedFeatures.contains(Feature.INCOMING_REVISION)) { - throw new FeatureNotSupportedException(Feature.INCOMING_REVISION.name()); - } - request.setAncestorChangeset(revision); - - return this; - } - //~--- get methods ---------------------------------------------------------- /** @@ -233,12 +186,8 @@ public final class DiffCommandBuilder diffCommand.getDiffResult(request, outputStream); } - //~--- fields --------------------------------------------------------------- - - /** implementation of the diff command */ - private final DiffCommand diffCommand; - private Set supportedFeatures; - - /** request for the diff command implementation */ - private final DiffCommandRequest request = new DiffCommandRequest(); + @Override + DiffCommandBuilder self() { + return this; + } } diff --git a/scm-core/src/main/java/sonia/scm/repository/api/DiffFile.java b/scm-core/src/main/java/sonia/scm/repository/api/DiffFile.java new file mode 100644 index 0000000000..a3b1bafe0b --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/api/DiffFile.java @@ -0,0 +1,12 @@ +package sonia.scm.repository.api; + +public interface DiffFile extends Iterable { + + String getOldRevision(); + + String getNewRevision(); + + String getOldPath(); + + String getNewPath(); +} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/DiffLine.java b/scm-core/src/main/java/sonia/scm/repository/api/DiffLine.java new file mode 100644 index 0000000000..193e5e75d5 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/api/DiffLine.java @@ -0,0 +1,12 @@ +package sonia.scm.repository.api; + +import java.util.OptionalInt; + +public interface DiffLine { + + OptionalInt getOldLineNumber(); + + OptionalInt getNewLineNumber(); + + String getContent(); +} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/DiffResult.java b/scm-core/src/main/java/sonia/scm/repository/api/DiffResult.java new file mode 100644 index 0000000000..b662db4e2d --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/api/DiffResult.java @@ -0,0 +1,8 @@ +package sonia.scm.repository.api; + +public interface DiffResult extends Iterable { + + String getOldRevision(); + + String getNewRevision(); +} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/DiffResultCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/DiffResultCommandBuilder.java new file mode 100644 index 0000000000..7e152f3d0f --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/api/DiffResultCommandBuilder.java @@ -0,0 +1,41 @@ +package sonia.scm.repository.api; + +import com.google.common.base.Preconditions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.repository.Feature; +import sonia.scm.repository.spi.DiffResultCommand; + +import java.io.IOException; +import java.util.Set; + +public class DiffResultCommandBuilder extends AbstractDiffCommandBuilder { + + private static final Logger LOG = LoggerFactory.getLogger(DiffResultCommandBuilder.class); + + private final DiffResultCommand diffResultCommand; + + DiffResultCommandBuilder(DiffResultCommand diffResultCommand, Set supportedFeatures) { + super(supportedFeatures); + this.diffResultCommand = diffResultCommand; + } + + /** + * Returns the content of the difference as parsed objects. + * + * @return content of the difference + */ + public DiffResult getDiffResult() throws IOException { + Preconditions.checkArgument(request.isValid(), + "path and/or revision is required"); + + LOG.debug("create diff result for {}", request); + + return diffResultCommand.getDiffResult(request); + } + + @Override + DiffResultCommandBuilder self() { + return this; + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/Hunk.java b/scm-core/src/main/java/sonia/scm/repository/api/Hunk.java new file mode 100644 index 0000000000..c8a3e1ebca --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/api/Hunk.java @@ -0,0 +1,24 @@ +package sonia.scm.repository.api; + +public interface Hunk extends Iterable { + + default String getRawHeader() { + return String.format("@@ -%s +%s @@", getLineMarker(getOldStart(), getOldLineCount()), getLineMarker(getNewStart(), getNewLineCount())); + } + + default String getLineMarker(int start, int lineCount) { + if (lineCount == 1) { + return Integer.toString(start); + } else { + return String.format("%s,%s", start, lineCount); + } + } + + int getOldStart(); + + int getOldLineCount(); + + int getNewStart(); + + int getNewLineCount(); +} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java index e11afa4be9..5807ffa998 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java @@ -31,7 +31,6 @@ package sonia.scm.repository.api; -import lombok.extern.slf4j.Slf4j; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.cache.CacheManager; @@ -239,6 +238,21 @@ public final class RepositoryService implements Closeable { return new DiffCommandBuilder(provider.getDiffCommand(), provider.getSupportedFeatures()); } + /** + * The diff command shows differences between revisions for a specified file + * or the entire revision. + * + * @return instance of {@link DiffResultCommandBuilder} + * @throws CommandNotSupportedException if the command is not supported + * by the implementation of the repository service provider. + */ + public DiffResultCommandBuilder getDiffResultCommand() { + LOG.debug("create diff result command for repository {}", + repository.getNamespaceAndName()); + + return new DiffResultCommandBuilder(provider.getDiffResultCommand(), provider.getSupportedFeatures()); + } + /** * The incoming command shows new {@link Changeset}s found in a different * repository location. @@ -379,7 +393,6 @@ public final class RepositoryService implements Closeable { * @since 2.0.0 */ public MergeCommandBuilder getMergeCommand() { - RepositoryPermissions.push(getRepository()).check(); LOG.debug("create merge command for repository {}", repository.getNamespaceAndName()); diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/DiffResultCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/DiffResultCommand.java new file mode 100644 index 0000000000..ee50178d76 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/spi/DiffResultCommand.java @@ -0,0 +1,9 @@ +package sonia.scm.repository.spi; + +import sonia.scm.repository.api.DiffResult; + +import java.io.IOException; + +public interface DiffResultCommand { + DiffResult getDiffResult(DiffCommandRequest request) throws IOException; +} diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/RepositoryServiceProvider.java b/scm-core/src/main/java/sonia/scm/repository/spi/RepositoryServiceProvider.java index a82eb7c30a..bf9cdf6a25 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/RepositoryServiceProvider.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/RepositoryServiceProvider.java @@ -158,6 +158,11 @@ public abstract class RepositoryServiceProvider implements Closeable throw new CommandNotSupportedException(Command.DIFF); } + public DiffResultCommand getDiffResultCommand() + { + throw new CommandNotSupportedException(Command.DIFF_RESULT); + } + /** * Method description * diff --git a/scm-core/src/main/java/sonia/scm/security/AccessTokenBuilder.java b/scm-core/src/main/java/sonia/scm/security/AccessTokenBuilder.java index 0924716bd8..afe81ac27f 100644 --- a/scm-core/src/main/java/sonia/scm/security/AccessTokenBuilder.java +++ b/scm-core/src/main/java/sonia/scm/security/AccessTokenBuilder.java @@ -99,15 +99,6 @@ public interface AccessTokenBuilder { */ AccessTokenBuilder scope(Scope scope); - /** - * Define the logged in user as member of the given groups. - * - * @param groups group names - * - * @return {@code this} - */ - AccessTokenBuilder groups(String... groups); - /** * Creates a new {@link AccessToken} with the provided settings. * diff --git a/scm-core/src/main/java/sonia/scm/security/AssignedPermission.java b/scm-core/src/main/java/sonia/scm/security/AssignedPermission.java index c98d81f8ba..cc7ff87534 100644 --- a/scm-core/src/main/java/sonia/scm/security/AssignedPermission.java +++ b/scm-core/src/main/java/sonia/scm/security/AssignedPermission.java @@ -162,7 +162,7 @@ public class AssignedPermission implements PermissionObject, Serializable //J- return MoreObjects.toStringHelper(this) .add("name", name) - .add("groupPermisison", groupPermission) + .add("groupPermission", groupPermission) .add("permission", permission) .toString(); //J+ diff --git a/scm-core/src/main/java/sonia/scm/security/DAORealmHelper.java b/scm-core/src/main/java/sonia/scm/security/DAORealmHelper.java index ea3e7ce9f5..6ec64a67de 100644 --- a/scm-core/src/main/java/sonia/scm/security/DAORealmHelper.java +++ b/scm-core/src/main/java/sonia/scm/security/DAORealmHelper.java @@ -45,7 +45,6 @@ import org.apache.shiro.authc.credential.CredentialsMatcher; import org.apache.shiro.subject.SimplePrincipalCollection; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sonia.scm.group.GroupDAO; import sonia.scm.user.User; import sonia.scm.user.UserDAO; @@ -71,8 +70,6 @@ public final class DAORealmHelper { private final UserDAO userDAO; - private final GroupCollector groupCollector; - private final String realm; //~--- constructors --------------------------------------------------------- @@ -83,14 +80,12 @@ public final class DAORealmHelper { * * @param loginAttemptHandler login attempt handler for wrapping credentials matcher * @param userDAO user dao - * @param groupCollector collect groups for a principal * @param realm name of realm */ - public DAORealmHelper(LoginAttemptHandler loginAttemptHandler, UserDAO userDAO, GroupCollector groupCollector, String realm) { + public DAORealmHelper(LoginAttemptHandler loginAttemptHandler, UserDAO userDAO, String realm) { this.loginAttemptHandler = loginAttemptHandler; this.realm = realm; this.userDAO = userDAO; - this.groupCollector = groupCollector; } //~--- get methods ---------------------------------------------------------- @@ -120,7 +115,7 @@ public final class DAORealmHelper { UsernamePasswordToken upt = (UsernamePasswordToken) token; String principal = upt.getUsername(); - return getAuthenticationInfo(principal, null, null, Collections.emptySet()); + return getAuthenticationInfo(principal, null, null); } /** @@ -135,7 +130,7 @@ public final class DAORealmHelper { } - private AuthenticationInfo getAuthenticationInfo(String principal, String credentials, Scope scope, Iterable groups) { + private AuthenticationInfo getAuthenticationInfo(String principal, String credentials, Scope scope) { checkArgument(!Strings.isNullOrEmpty(principal), "username is required"); LOG.debug("try to authenticate {}", principal); @@ -153,7 +148,6 @@ public final class DAORealmHelper { collection.add(principal, realm); collection.add(user, realm); - collection.add(groupCollector.collect(principal, groups), realm); collection.add(MoreObjects.firstNonNull(scope, Scope.empty()), realm); String creds = credentials; @@ -207,17 +201,17 @@ public final class DAORealmHelper { return this; } - /** - * With groups adds extra groups, besides those which come from the {@link GroupDAO}, to the authentication info. - * - * @param groups extra groups - * - * @return {@code this} - */ - public AuthenticationInfoBuilder withGroups(Iterable groups) { - this.groups = groups; - return this; - } +// /** +// * With groups adds extra groups, besides those which come from the {@link GroupDAO}, to the authentication info. +// * +// * @param groups extra groups +// * +// * @return {@code this} +// */ +// public AuthenticationInfoBuilder withGroups(Iterable groups) { +// this.groups = groups; +// return this; +// } /** * Build creates the authentication info from the given information. @@ -225,7 +219,7 @@ public final class DAORealmHelper { * @return authentication info */ public AuthenticationInfo build() { - return getAuthenticationInfo(principal, credentials, scope, groups); + return getAuthenticationInfo(principal, credentials, scope); } } diff --git a/scm-core/src/main/java/sonia/scm/security/DAORealmHelperFactory.java b/scm-core/src/main/java/sonia/scm/security/DAORealmHelperFactory.java index ee2bf11e21..dd59de2ac8 100644 --- a/scm-core/src/main/java/sonia/scm/security/DAORealmHelperFactory.java +++ b/scm-core/src/main/java/sonia/scm/security/DAORealmHelperFactory.java @@ -30,7 +30,7 @@ */ package sonia.scm.security; -import sonia.scm.group.GroupDAO; +import sonia.scm.cache.CacheManager; import sonia.scm.user.UserDAO; import javax.inject.Inject; @@ -45,20 +45,19 @@ public final class DAORealmHelperFactory { private final LoginAttemptHandler loginAttemptHandler; private final UserDAO userDAO; - private final GroupCollector groupCollector; + private final CacheManager cacheManager; /** * Constructs a new instance. - * * @param loginAttemptHandler login attempt handler * @param userDAO user dao - * @param groupDAO group dao + * @param cacheManager */ @Inject - public DAORealmHelperFactory(LoginAttemptHandler loginAttemptHandler, UserDAO userDAO, GroupDAO groupDAO) { + public DAORealmHelperFactory(LoginAttemptHandler loginAttemptHandler, UserDAO userDAO, CacheManager cacheManager) { this.loginAttemptHandler = loginAttemptHandler; this.userDAO = userDAO; - this.groupCollector = new GroupCollector(groupDAO); + this.cacheManager = cacheManager; } /** @@ -69,7 +68,7 @@ public final class DAORealmHelperFactory { * @return new {@link DAORealmHelper} instance. */ public DAORealmHelper create(String realm) { - return new DAORealmHelper(loginAttemptHandler, userDAO, groupCollector, realm); + return new DAORealmHelper(loginAttemptHandler, userDAO, realm); } } diff --git a/scm-core/src/main/java/sonia/scm/security/GroupCollector.java b/scm-core/src/main/java/sonia/scm/security/GroupCollector.java deleted file mode 100644 index 56687af7ef..0000000000 --- a/scm-core/src/main/java/sonia/scm/security/GroupCollector.java +++ /dev/null @@ -1,43 +0,0 @@ -package sonia.scm.security; - -import com.google.common.collect.ImmutableSet; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import sonia.scm.group.Group; -import sonia.scm.group.GroupDAO; -import sonia.scm.group.GroupNames; - -/** - * Collect groups for a certain principal. - * Warning: The class is only for internal use and should never used directly. - */ -class GroupCollector { - - private static final Logger LOG = LoggerFactory.getLogger(GroupCollector.class); - - private final GroupDAO groupDAO; - - GroupCollector(GroupDAO groupDAO) { - this.groupDAO = groupDAO; - } - - GroupNames collect(String principal, Iterable groupNames) { - ImmutableSet.Builder builder = ImmutableSet.builder(); - - builder.add(GroupNames.AUTHENTICATED); - - for (String group : groupNames) { - builder.add(group); - } - - for (Group group : groupDAO.getAll()) { - if (group.isMember(principal)) { - builder.add(group.getName()); - } - } - - GroupNames groups = new GroupNames(builder.build()); - LOG.debug("collected following groups for principal {}: {}", principal, groups); - return groups; - } -} diff --git a/scm-core/src/main/java/sonia/scm/security/SyncingRealmHelper.java b/scm-core/src/main/java/sonia/scm/security/SyncingRealmHelper.java index d421d33f45..b2175f304a 100644 --- a/scm-core/src/main/java/sonia/scm/security/SyncingRealmHelper.java +++ b/scm-core/src/main/java/sonia/scm/security/SyncingRealmHelper.java @@ -32,24 +32,15 @@ import com.google.inject.Inject; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.subject.SimplePrincipalCollection; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import sonia.scm.AlreadyExistsException; import sonia.scm.NotFoundException; -import sonia.scm.group.ExternalGroupNames; import sonia.scm.group.Group; -import sonia.scm.group.GroupDAO; import sonia.scm.group.GroupManager; import sonia.scm.plugin.Extension; import sonia.scm.user.User; import sonia.scm.user.UserManager; import sonia.scm.web.security.AdministrationContext; -import java.util.Collection; -import java.util.Collections; - -import static java.util.Arrays.asList; - /** * Helper class for syncing realms. The class should simplify the creation of realms, which are syncing authenticated * users with the local database. @@ -60,12 +51,9 @@ import static java.util.Arrays.asList; @Extension public final class SyncingRealmHelper { - private static final Logger LOG = LoggerFactory.getLogger(SyncingRealmHelper.class); - private final AdministrationContext ctx; private final UserManager userManager; private final GroupManager groupManager; - private final GroupCollector groupCollector; /** * Constructs a new SyncingRealmHelper. @@ -73,134 +61,28 @@ public final class SyncingRealmHelper { * @param ctx administration context * @param userManager user manager * @param groupManager group manager - * @param groupDAO group dao */ @Inject - public SyncingRealmHelper(AdministrationContext ctx, UserManager userManager, GroupManager groupManager, GroupDAO groupDAO) { + public SyncingRealmHelper(AdministrationContext ctx, UserManager userManager, GroupManager groupManager) { this.ctx = ctx; this.userManager = userManager; this.groupManager = groupManager; - this.groupCollector = new GroupCollector(groupDAO); } - /** - * Create {@link AuthenticationInfo} from user and groups. - */ - public AuthenticationInfoBuilder.ForRealm authenticationInfo() { - return new AuthenticationInfoBuilder().new ForRealm(); - } - - public class AuthenticationInfoBuilder { - private String realm; - private User user; - private Collection groups = Collections.emptySet(); - private Collection externalGroups = Collections.emptySet(); - - private AuthenticationInfo build() { - return SyncingRealmHelper.this.createAuthenticationInfo(realm, user, groups, externalGroups); - } - - public class ForRealm { - private ForRealm() { - } - - /** - * Sets the realm. - * @param realm name of the realm - */ - public ForUser forRealm(String realm) { - AuthenticationInfoBuilder.this.realm = realm; - return AuthenticationInfoBuilder.this.new ForUser(); - } - } - - public class ForUser { - private ForUser() { - } - - /** - * Sets the user. - * @param user authenticated user - */ - public AuthenticationInfoBuilder.WithGroups andUser(User user) { - AuthenticationInfoBuilder.this.user = user; - return AuthenticationInfoBuilder.this.new WithGroups(); - } - } - - public class WithGroups { - private WithGroups() { - } - - /** - * Set the internal groups for the user. - * @param groups groups of the authenticated user - * @return builder step for groups - */ - public WithGroups withGroups(String... groups) { - return withGroups(asList(groups)); - } - - /** - * Set the internal groups for the user. - * @param groups groups of the authenticated user - * @return builder step for groups - */ - public WithGroups withGroups(Collection groups) { - AuthenticationInfoBuilder.this.groups = groups; - return this; - } - - /** - * Set the external groups for the user. - * @param externalGroups external groups of the authenticated user - * @return builder step for groups - */ - public WithGroups withExternalGroups(String... externalGroups) { - return withExternalGroups(asList(externalGroups)); - } - - /** - * Set the external groups for the user. - * @param externalGroups external groups of the authenticated user - * @return builder step for groups - */ - public WithGroups withExternalGroups(Collection externalGroups) { - AuthenticationInfoBuilder.this.externalGroups = externalGroups; - return this; - } - - /** - * Builds the {@link AuthenticationInfo} from the given options. - * - * @return complete autentication info - */ - public AuthenticationInfo build() { - return AuthenticationInfoBuilder.this.build(); - } - } - } - - //~--- methods -------------------------------------------------------------- - /** * Create {@link AuthenticationInfo} from user and groups. * * * @param realm name of the realm * @param user authenticated user - * @param groups groups of the authenticated user * * @return authentication info */ - private AuthenticationInfo createAuthenticationInfo(String realm, User user, - Collection groups, Collection externalGroups) { + public AuthenticationInfo createAuthenticationInfo(String realm, User user) { SimplePrincipalCollection collection = new SimplePrincipalCollection(); collection.add(user.getId(), realm); collection.add(user, realm); - collection.add(groupCollector.collect(user.getId(), groups), realm); - collection.add(new ExternalGroupNames(externalGroups), realm); return new SimpleAuthenticationInfo(collection, user.getPassword()); } diff --git a/scm-core/src/main/java/sonia/scm/update/BlobDirectoryAccess.java b/scm-core/src/main/java/sonia/scm/update/BlobDirectoryAccess.java new file mode 100644 index 0000000000..35eaa134fd --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/update/BlobDirectoryAccess.java @@ -0,0 +1,15 @@ +package sonia.scm.update; + +import java.io.IOException; +import java.nio.file.Path; + +public interface BlobDirectoryAccess { + + void forBlobDirectories(BlobDirectoryConsumer blobDirectoryConsumer) throws IOException; + + void moveToRepositoryBlobStore(Path blobDirectory, String newDirectoryName, String repositoryId) throws IOException; + + interface BlobDirectoryConsumer { + void accept(Path directory) throws IOException; + } +} diff --git a/scm-core/src/main/java/sonia/scm/update/UpdateStepRepositoryMetadataAccess.java b/scm-core/src/main/java/sonia/scm/update/UpdateStepRepositoryMetadataAccess.java new file mode 100644 index 0000000000..33afe9f76b --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/update/UpdateStepRepositoryMetadataAccess.java @@ -0,0 +1,11 @@ +package sonia.scm.update; + +import sonia.scm.repository.Repository; + +/** + * Use this in {@link sonia.scm.migration.UpdateStep}s only to read repository objects directly from locations given by + * {@link sonia.scm.repository.RepositoryLocationResolver}. + */ +public interface UpdateStepRepositoryMetadataAccess { + Repository read(T location); +} diff --git a/scm-core/src/main/java/sonia/scm/util/IOUtil.java b/scm-core/src/main/java/sonia/scm/util/IOUtil.java index d71f8a36fc..4058c089f4 100644 --- a/scm-core/src/main/java/sonia/scm/util/IOUtil.java +++ b/scm-core/src/main/java/sonia/scm/util/IOUtil.java @@ -37,14 +37,11 @@ package sonia.scm.util; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.io.Command; import sonia.scm.io.CommandResult; import sonia.scm.io.SimpleCommand; import sonia.scm.io.ZipUnArchiver; -//~--- JDK imports ------------------------------------------------------------ - import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.File; @@ -55,12 +52,13 @@ import java.io.InputStream; import java.io.OutputStream; import java.io.Reader; import java.io.Writer; - import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Locale; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -471,8 +469,14 @@ public final class IOUtil { if (!directory.exists() &&!directory.mkdirs()) { - throw new IllegalStateException( - "could not create directory ".concat(directory.getPath())); + // Sometimes, the previous check simply has the wrong result (either the 'exists()' returnes false though the + // directory exists or 'mkdirs()' returns false though the directory was created successfully. + // We therefore have to double check here. Funny though, in these cases a second check with 'directory.exists()' + // still returns false. As it seems, 'directory.getAbsoluteFile().exists()' creates a new object that fixes this + // problem. + if (!directory.getAbsoluteFile().exists()) { + throw new IllegalStateException("could not create directory ".concat(directory.getPath())); + } } } diff --git a/scm-webapp/src/main/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilter.java b/scm-core/src/main/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilterBase.java similarity index 74% rename from scm-webapp/src/main/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilter.java rename to scm-core/src/main/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilterBase.java index 973b3af4cb..8b1868309b 100644 --- a/scm-webapp/src/main/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilter.java +++ b/scm-core/src/main/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilterBase.java @@ -1,16 +1,11 @@ package sonia.scm.web.filter; -import sonia.scm.Priority; import sonia.scm.config.ScmConfiguration; -import sonia.scm.filter.Filters; -import sonia.scm.filter.WebElement; import sonia.scm.util.HttpUtil; import sonia.scm.web.UserAgent; import sonia.scm.web.UserAgentParser; import sonia.scm.web.WebTokenGenerator; -import sonia.scm.web.protocol.HttpProtocolServlet; -import javax.inject.Inject; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; @@ -18,14 +13,11 @@ import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Set; -@Priority(Filters.PRIORITY_AUTHENTICATION) -@WebElement(value = HttpProtocolServlet.PATTERN) -public class HttpProtocolServletAuthenticationFilter extends AuthenticationFilter { +public class HttpProtocolServletAuthenticationFilterBase extends AuthenticationFilter { private final UserAgentParser userAgentParser; - @Inject - public HttpProtocolServletAuthenticationFilter( + protected HttpProtocolServletAuthenticationFilterBase( ScmConfiguration configuration, Set tokenGenerators, UserAgentParser userAgentParser) { diff --git a/scm-core/src/test/java/sonia/scm/repository/RepositoryTest.java b/scm-core/src/test/java/sonia/scm/repository/RepositoryTest.java new file mode 100644 index 0000000000..6053e10ad5 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/repository/RepositoryTest.java @@ -0,0 +1,22 @@ +package sonia.scm.repository; + +import org.junit.jupiter.api.Test; + +import java.util.Arrays; + +import static org.assertj.core.api.Assertions.assertThat; + +class RepositoryTest { + + @Test + void shouldCreateNewPermissionOnClone() { + Repository repository = new Repository(); + repository.setPermissions(Arrays.asList(new RepositoryPermission("one", "role", false))); + + Repository cloned = repository.clone(); + cloned.setPermissions(Arrays.asList(new RepositoryPermission("two", "role", false))); + + assertThat(repository.getPermissions()).extracting(r -> r.getName()).containsOnly("one"); + } + +} diff --git a/scm-core/src/test/java/sonia/scm/repository/api/HunkTest.java b/scm-core/src/test/java/sonia/scm/repository/api/HunkTest.java new file mode 100644 index 0000000000..086df81741 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/repository/api/HunkTest.java @@ -0,0 +1,53 @@ +package sonia.scm.repository.api; + +import org.junit.jupiter.api.Test; + +import java.util.Iterator; + +import static org.assertj.core.api.Assertions.assertThat; + +class HunkTest { + + @Test + void shouldGetComplexHeader() { + String rawHeader = createHunk(2, 3, 4, 5).getRawHeader(); + + assertThat(rawHeader).isEqualTo("@@ -2,3 +4,5 @@"); + } + + @Test + void shouldReturnSingleNumberForOne() { + String rawHeader = createHunk(42, 1, 5, 1).getRawHeader(); + + assertThat(rawHeader).isEqualTo("@@ -42 +5 @@"); + } + + private Hunk createHunk(int oldStart, int oldLineCount, int newStart, int newLineCount) { + return new Hunk() { + @Override + public int getOldStart() { + return oldStart; + } + + @Override + public int getOldLineCount() { + return oldLineCount; + } + + @Override + public int getNewStart() { + return newStart; + } + + @Override + public int getNewLineCount() { + return newLineCount; + } + + @Override + public Iterator iterator() { + return null; + } + }; + } +} diff --git a/scm-core/src/test/java/sonia/scm/security/DAORealmHelperTest.java b/scm-core/src/test/java/sonia/scm/security/DAORealmHelperTest.java index 78dbd4fdd2..0fbcc20ac0 100644 --- a/scm-core/src/test/java/sonia/scm/security/DAORealmHelperTest.java +++ b/scm-core/src/test/java/sonia/scm/security/DAORealmHelperTest.java @@ -1,20 +1,16 @@ package sonia.scm.security; -import com.google.common.collect.ImmutableList; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.DisabledAccountException; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.subject.PrincipalCollection; -import org.junit.Ignore; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import sonia.scm.group.Group; import sonia.scm.group.GroupDAO; -import sonia.scm.group.GroupNames; import sonia.scm.user.User; import sonia.scm.user.UserDAO; @@ -38,7 +34,7 @@ class DAORealmHelperTest { @BeforeEach void setUpObjectUnderTest() { - helper = new DAORealmHelper(loginAttemptHandler, userDAO, new GroupCollector(groupDAO), "hitchhiker"); + helper = new DAORealmHelper(loginAttemptHandler, userDAO, "hitchhiker"); } @Test @@ -73,29 +69,9 @@ class DAORealmHelperTest { AuthenticationInfo authenticationInfo = helper.authenticationInfoBuilder("trillian").build(); PrincipalCollection principals = authenticationInfo.getPrincipals(); assertThat(principals.oneByType(User.class)).isSameAs(user); - assertThat(principals.oneByType(GroupNames.class)).containsOnly("_authenticated"); assertThat(principals.oneByType(Scope.class)).isEmpty(); } - @Test - @Ignore - void shouldReturnAuthenticationInfoWithGroups() { - User user = new User("trillian"); - when(userDAO.get("trillian")).thenReturn(user); - - Group one = new Group("xml", "one", "trillian"); - Group two = new Group("xml", "two", "trillian"); - Group six = new Group("xml", "six", "dent"); - when(groupDAO.getAll()).thenReturn(ImmutableList.of(one, two, six)); - - AuthenticationInfo authenticationInfo = helper.authenticationInfoBuilder("trillian") - .withGroups(ImmutableList.of("three")) - .build(); - - PrincipalCollection principals = authenticationInfo.getPrincipals(); - assertThat(principals.oneByType(GroupNames.class)).containsOnly("_authenticated", "one", "two", "three"); - } - @Test void shouldReturnAuthenticationInfoWithScope() { User user = new User("trillian"); @@ -148,7 +124,6 @@ class DAORealmHelperTest { PrincipalCollection principals = authenticationInfo.getPrincipals(); assertThat(principals.oneByType(User.class)).isSameAs(user); - assertThat(principals.oneByType(GroupNames.class)).containsOnly("_authenticated"); assertThat(principals.oneByType(Scope.class)).isEmpty(); assertThat(authenticationInfo.getCredentials()).isNull(); diff --git a/scm-core/src/test/java/sonia/scm/security/GroupCollectorTest.java b/scm-core/src/test/java/sonia/scm/security/GroupCollectorTest.java deleted file mode 100644 index 3fb59d1614..0000000000 --- a/scm-core/src/test/java/sonia/scm/security/GroupCollectorTest.java +++ /dev/null @@ -1,64 +0,0 @@ -package sonia.scm.security; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.Lists; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import sonia.scm.group.Group; -import sonia.scm.group.GroupDAO; -import sonia.scm.group.GroupNames; - -import java.util.Collections; -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.when; - -@ExtendWith(MockitoExtension.class) -class GroupCollectorTest { - - @Mock - private GroupDAO groupDAO; - - @InjectMocks - private GroupCollector collector; - - @Test - void shouldAlwaysReturnAuthenticatedGroup() { - GroupNames groupNames = collector.collect("trillian", Collections.emptySet()); - assertThat(groupNames).containsOnly("_authenticated"); - } - - @Nested - class WithGroupsFromDao { - - @BeforeEach - void setUpGroupsDao() { - List groups = Lists.newArrayList( - new Group("xml", "heartOfGold", "trillian"), - new Group("xml", "g42", "dent", "prefect"), - new Group("xml", "fjordsOfAfrican", "dent", "trillian") - ); - when(groupDAO.getAll()).thenReturn(groups); - } - - @Test - void shouldReturnGroupsFromDao() { - GroupNames groupNames = collector.collect("trillian", Collections.emptySet()); - assertThat(groupNames).contains("_authenticated", "heartOfGold", "fjordsOfAfrican"); - } - - @Test - void shouldCombineGivenWithDao() { - GroupNames groupNames = collector.collect("trillian", ImmutableList.of("awesome", "incredible")); - assertThat(groupNames).contains("_authenticated", "heartOfGold", "fjordsOfAfrican", "awesome", "incredible"); - } - - } - -} diff --git a/scm-core/src/test/java/sonia/scm/security/SyncingRealmHelperTest.java b/scm-core/src/test/java/sonia/scm/security/SyncingRealmHelperTest.java index 20d1010b57..ca7c2efdc6 100644 --- a/scm-core/src/test/java/sonia/scm/security/SyncingRealmHelperTest.java +++ b/scm-core/src/test/java/sonia/scm/security/SyncingRealmHelperTest.java @@ -36,31 +36,30 @@ package sonia.scm.security; //~--- non-JDK imports -------------------------------------------------------- import com.google.common.base.Throwables; -import com.google.common.collect.Lists; import org.apache.shiro.authc.AuthenticationInfo; -import org.assertj.core.api.Assertions; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.AlreadyExistsException; -import sonia.scm.group.ExternalGroupNames; import sonia.scm.group.Group; -import sonia.scm.group.GroupDAO; import sonia.scm.group.GroupManager; -import sonia.scm.group.GroupNames; import sonia.scm.user.User; import sonia.scm.user.UserManager; import sonia.scm.web.security.AdministrationContext; import sonia.scm.web.security.PrivilegedAction; import java.io.IOException; -import java.util.List; import static org.hamcrest.Matchers.hasItem; -import static org.junit.Assert.*; -import static org.mockito.Mockito.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; //~--- JDK imports ------------------------------------------------------------ @@ -78,9 +77,6 @@ public class SyncingRealmHelperTest { @Mock private UserManager userManager; - @Mock - private GroupDAO groupDAO; - private SyncingRealmHelper helper; /** @@ -106,7 +102,7 @@ public class SyncingRealmHelperTest { } }; - helper = new SyncingRealmHelper(ctx, userManager, groupManager, groupDAO); + helper = new SyncingRealmHelper(ctx, userManager, groupManager); } /** @@ -183,67 +179,15 @@ public class SyncingRealmHelperTest { verify(userManager, times(1)).modify(user); } - @Test - public void builderShouldSetInternalGroups() { - AuthenticationInfo authenticationInfo = helper - .authenticationInfo() - .forRealm("unit-test") - .andUser(new User("ziltoid")) - .withGroups("internal") - .build(); - - GroupNames groupNames = authenticationInfo.getPrincipals().oneByType(GroupNames.class); - Assertions.assertThat(groupNames.getCollection()).contains("_authenticated", "internal"); - } - - @Test - public void builderShouldSetExternalGroups() { - AuthenticationInfo authenticationInfo = helper - .authenticationInfo() - .forRealm("unit-test") - .andUser(new User("ziltoid")) - .withExternalGroups("external") - .build(); - - ExternalGroupNames groupNames = authenticationInfo.getPrincipals().oneByType(ExternalGroupNames.class); - Assertions.assertThat(groupNames.getCollection()).containsOnly("external"); - } @Test public void builderShouldSetValues() { User user = new User("ziltoid"); - AuthenticationInfo authInfo = helper - .authenticationInfo() - .forRealm("unit-test") - .andUser(user) - .build(); + AuthenticationInfo authInfo = helper.createAuthenticationInfo("unit-test", user); assertNotNull(authInfo); assertEquals("ziltoid", authInfo.getPrincipals().getPrimaryPrincipal()); assertThat(authInfo.getPrincipals().getRealmNames(), hasItem("unit-test")); assertEquals(user, authInfo.getPrincipals().oneByType(User.class)); } - - @Test - public void shouldReturnCombinedGroupNames() { - User user = new User("tricia"); - - List groups = Lists.newArrayList(new Group("xml", "heartOfGold", "tricia")); - when(groupDAO.getAll()).thenReturn(groups); - - AuthenticationInfo authInfo = helper - .authenticationInfo() - .forRealm("unit-test") - .andUser(user) - .withGroups("fjordsOfAfrican") - .withExternalGroups("g42") - .build(); - - - GroupNames groupNames = authInfo.getPrincipals().oneByType(GroupNames.class); - Assertions.assertThat(groupNames).contains("_authenticated", "heartOfGold", "fjordsOfAfrican"); - - ExternalGroupNames externalGroupNames = authInfo.getPrincipals().oneByType(ExternalGroupNames.class); - Assertions.assertThat(externalGroupNames).contains("g42"); - } } diff --git a/scm-webapp/src/test/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilterTest.java b/scm-core/src/test/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilterBaseTest.java similarity index 91% rename from scm-webapp/src/test/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilterTest.java rename to scm-core/src/test/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilterBaseTest.java index ff493e2b84..1f9b4fad07 100644 --- a/scm-webapp/src/test/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilterTest.java +++ b/scm-core/src/test/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilterBaseTest.java @@ -23,7 +23,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) -class HttpProtocolServletAuthenticationFilterTest { +class HttpProtocolServletAuthenticationFilterBaseTest { private ScmConfiguration configuration = new ScmConfiguration(); @@ -32,7 +32,7 @@ class HttpProtocolServletAuthenticationFilterTest { @Mock private UserAgentParser userAgentParser; - private HttpProtocolServletAuthenticationFilter authenticationFilter; + private HttpProtocolServletAuthenticationFilterBase authenticationFilter; @Mock private HttpServletRequest request; @@ -48,7 +48,7 @@ class HttpProtocolServletAuthenticationFilterTest { @BeforeEach void setUpObjectUnderTest() { - authenticationFilter = new HttpProtocolServletAuthenticationFilter(configuration, tokenGenerators, userAgentParser); + authenticationFilter = new HttpProtocolServletAuthenticationFilterBase(configuration, tokenGenerators, userAgentParser); } @Test diff --git a/scm-core/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/scm-core/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000000..1f0955d450 --- /dev/null +++ b/scm-core/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/MetadataStore.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/MetadataStore.java index 1f5f0e81b6..f22180ff9a 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/MetadataStore.java +++ b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/MetadataStore.java @@ -5,19 +5,21 @@ import org.slf4j.LoggerFactory; import sonia.scm.ContextEntry; import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.Repository; +import sonia.scm.store.StoreConstants; +import sonia.scm.update.UpdateStepRepositoryMetadataAccess; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; import java.nio.file.Path; -class MetadataStore { +public class MetadataStore implements UpdateStepRepositoryMetadataAccess { private static final Logger LOG = LoggerFactory.getLogger(MetadataStore.class); private final JAXBContext jaxbContext; - MetadataStore() { + public MetadataStore() { try { jaxbContext = JAXBContext.newInstance(Repository.class); } catch (JAXBException ex) { @@ -25,10 +27,10 @@ class MetadataStore { } } - Repository read(Path path) { + public Repository read(Path path) { LOG.trace("read repository metadata from {}", path); try { - return (Repository) jaxbContext.createUnmarshaller().unmarshal(path.toFile()); + return (Repository) jaxbContext.createUnmarshaller().unmarshal(resolveDataPath(path).toFile()); } catch (JAXBException ex) { throw new InternalRepositoryException( ContextEntry.ContextBuilder.entity(Path.class, path.toString()).build(), "failed read repository metadata", ex @@ -41,10 +43,13 @@ class MetadataStore { try { Marshaller marshaller = jaxbContext.createMarshaller(); marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); - marshaller.marshal(repository, path.toFile()); + marshaller.marshal(repository, resolveDataPath(path).toFile()); } catch (JAXBException ex) { throw new InternalRepositoryException(repository, "failed write repository metadata", ex); } } + private Path resolveDataPath(Path repositoryPath) { + return repositoryPath.resolve(StoreConstants.REPOSITORY_METADATA.concat(StoreConstants.FILE_EXTENSION)); + } } diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java index 81cf167071..8f667b0e65 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java +++ b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java @@ -94,6 +94,11 @@ public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocation PathBasedRepositoryLocationResolver.this.setLocation(repositoryId, ((Path) location).toAbsolutePath()); } } + + @Override + public void forAllLocations(BiConsumer consumer) { + pathById.forEach((id, path) -> consumer.accept(id, (T) contextProvider.resolve(path))); + } }; } @@ -115,10 +120,6 @@ public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocation return contextProvider.resolve(removedPath); } - void forAllPaths(BiConsumer consumer) { - pathById.forEach((id, path) -> consumer.accept(id, contextProvider.resolve(path))); - } - void updateModificationDate() { this.writePathDatabase(); } diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/SingleRepositoryUpdateProcessor.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/SingleRepositoryUpdateProcessor.java index eeb95f75b0..f963047624 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/SingleRepositoryUpdateProcessor.java +++ b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/SingleRepositoryUpdateProcessor.java @@ -1,5 +1,7 @@ package sonia.scm.repository.xml; +import sonia.scm.repository.RepositoryLocationResolver; + import javax.inject.Inject; import java.nio.file.Path; import java.util.function.BiConsumer; @@ -7,9 +9,9 @@ import java.util.function.BiConsumer; public class SingleRepositoryUpdateProcessor { @Inject - private PathBasedRepositoryLocationResolver locationResolver; + private RepositoryLocationResolver locationResolver; public void doUpdate(BiConsumer forEachRepository) { - locationResolver.forAllPaths(forEachRepository); + locationResolver.forClass(Path.class).forAllLocations(forEachRepository); } } diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryDAO.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryDAO.java index 151e8f1281..1242c99641 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryDAO.java +++ b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryDAO.java @@ -40,7 +40,7 @@ import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryDAO; -import sonia.scm.store.StoreConstants; +import sonia.scm.repository.RepositoryLocationResolver; import javax.inject.Inject; import java.io.IOException; @@ -76,18 +76,14 @@ public class XmlRepositoryDAO implements RepositoryDAO { } private void init() { - repositoryLocationResolver.forAllPaths((repositoryId, repositoryPath) -> { - Path metadataPath = resolveDataPath(repositoryPath); - Repository repository = metadataStore.read(metadataPath); + RepositoryLocationResolver.RepositoryLocationResolverInstance pathRepositoryLocationResolverInstance = repositoryLocationResolver.create(Path.class); + pathRepositoryLocationResolverInstance.forAllLocations((repositoryId, repositoryPath) -> { + Repository repository = metadataStore.read(repositoryPath); byNamespaceAndName.put(repository.getNamespaceAndName(), repository); byId.put(repositoryId, repository); }); } - private Path resolveDataPath(Path repositoryPath) { - return repositoryPath.resolve(StoreConstants.REPOSITORY_METADATA.concat(StoreConstants.FILE_EXTENSION)); - } - @Override public String getType() { return "xml"; @@ -108,8 +104,7 @@ public class XmlRepositoryDAO implements RepositoryDAO { Path repositoryPath = (Path) location; try { - Path metadataPath = resolveDataPath(repositoryPath); - metadataStore.write(metadataPath, repository); + metadataStore.write(repositoryPath, repository); } catch (Exception e) { repositoryLocationResolver.remove(repository.getId()); throw new InternalRepositoryException(repository, "failed to create filesystem", e); @@ -166,9 +161,8 @@ public class XmlRepositoryDAO implements RepositoryDAO { Path repositoryPath = repositoryLocationResolver .create(Path.class) .getLocation(repository.getId()); - Path metadataPath = resolveDataPath(repositoryPath); repositoryLocationResolver.updateModificationDate(); - metadataStore.write(metadataPath, clone); + metadataStore.write(repositoryPath, clone); } @Override diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/DefaultBlobDirectoryAccess.java b/scm-dao-xml/src/main/java/sonia/scm/store/DefaultBlobDirectoryAccess.java new file mode 100644 index 0000000000..d22a76c79d --- /dev/null +++ b/scm-dao-xml/src/main/java/sonia/scm/store/DefaultBlobDirectoryAccess.java @@ -0,0 +1,68 @@ +package sonia.scm.store; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.SCMContextProvider; +import sonia.scm.repository.RepositoryLocationResolver; +import sonia.scm.update.BlobDirectoryAccess; +import sonia.scm.util.IOUtil; + +import javax.inject.Inject; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Stream; + +public class DefaultBlobDirectoryAccess implements BlobDirectoryAccess { + + private static final Logger LOG = LoggerFactory.getLogger(DefaultBlobDirectoryAccess.class); + + private final SCMContextProvider contextProvider; + private final RepositoryLocationResolver locationResolver; + + @Inject + public DefaultBlobDirectoryAccess(SCMContextProvider contextProvider, RepositoryLocationResolver locationResolver) { + this.contextProvider = contextProvider; + this.locationResolver = locationResolver; + } + + @Override + public void forBlobDirectories(BlobDirectoryConsumer blobDirectoryConsumer) throws IOException { + Path v1blobDir = computeV1BlobDir(); + if (Files.exists(v1blobDir) && Files.isDirectory(v1blobDir)) { + try (Stream fileStream = Files.list(v1blobDir)) { + fileStream.filter(p -> Files.isDirectory(p)).forEach(p -> { + try { + blobDirectoryConsumer.accept(p); + } catch (IOException e) { + throw new RuntimeException("could not call consumer for blob directory " + p, e); + } + }); + } + } + } + + @Override + public void moveToRepositoryBlobStore(Path blobDirectory, String newDirectoryName, String repositoryId) throws IOException { + Path repositoryLocation; + try { + repositoryLocation = locationResolver + .forClass(Path.class) + .getLocation(repositoryId); + } catch (IllegalStateException e) { + LOG.info("ignoring blob directory {} because there is no repository location for repository id {}", blobDirectory, repositoryId); + return; + } + Path target = repositoryLocation + .resolve(Store.BLOB.getRepositoryStoreDirectory()); + IOUtil.mkdirs(target.toFile()); + Path resolvedSourceDirectory = computeV1BlobDir().resolve(blobDirectory); + Path resolvedTargetDirectory = target.resolve(newDirectoryName); + LOG.trace("moving directory {} to {}", resolvedSourceDirectory, resolvedTargetDirectory); + Files.move(resolvedSourceDirectory, resolvedTargetDirectory); + } + + private Path computeV1BlobDir() { + return contextProvider.getBaseDirectory().toPath().resolve("var").resolve("blob"); + } +} diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBPropertyFileAccess.java b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBPropertyFileAccess.java index b8d30bc0c1..ca4aeea468 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBPropertyFileAccess.java +++ b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBPropertyFileAccess.java @@ -17,7 +17,6 @@ public class JAXBPropertyFileAccess implements PropertyFileAccess { private static final Logger LOG = LoggerFactory.getLogger(JAXBPropertyFileAccess.class); - public static final String XML_FILENAME_SUFFIX = ".xml"; private final SCMContextProvider contextProvider; private final RepositoryLocationResolver locationResolver; @@ -31,8 +30,8 @@ public class JAXBPropertyFileAccess implements PropertyFileAccess { public Target renameGlobalConfigurationFrom(String oldName) { return newName -> { Path configDir = contextProvider.getBaseDirectory().toPath().resolve(StoreConstants.CONFIG_DIRECTORY_NAME); - Path oldConfigFile = configDir.resolve(oldName + XML_FILENAME_SUFFIX); - Path newConfigFile = configDir.resolve(newName + XML_FILENAME_SUFFIX); + Path oldConfigFile = configDir.resolve(oldName + StoreConstants.FILE_EXTENSION); + Path newConfigFile = configDir.resolve(newName + StoreConstants.FILE_EXTENSION); Files.move(oldConfigFile, newConfigFile); }; } @@ -45,7 +44,7 @@ public class JAXBPropertyFileAccess implements PropertyFileAccess { Path v1storeDir = computeV1StoreDir(); if (Files.exists(v1storeDir) && Files.isDirectory(v1storeDir)) { try (Stream fileStream = Files.list(v1storeDir)) { - fileStream.filter(p -> p.toString().endsWith(XML_FILENAME_SUFFIX)).forEach(p -> { + fileStream.filter(p -> p.toString().endsWith(StoreConstants.FILE_EXTENSION)).forEach(p -> { try { String storeName = extractStoreName(p); storeFileConsumer.accept(p, storeName); @@ -84,7 +83,7 @@ public class JAXBPropertyFileAccess implements PropertyFileAccess { private String extractStoreName(Path p) { String fileName = p.getFileName().toString(); - return fileName.substring(0, fileName.length() - XML_FILENAME_SUFFIX.length()); + return fileName.substring(0, fileName.length() - StoreConstants.FILE_EXTENSION.length()); } }; } diff --git a/scm-dao-xml/src/test/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolverTest.java b/scm-dao-xml/src/test/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolverTest.java index 941775d6ea..1c5a337abc 100644 --- a/scm-dao-xml/src/test/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolverTest.java +++ b/scm-dao-xml/src/test/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolverTest.java @@ -120,7 +120,7 @@ class PathBasedRepositoryLocationResolverTest { @Test void shouldInitWithExistingData() { Map foundRepositories = new HashMap<>(); - resolverWithExistingData.forAllPaths( + resolverWithExistingData.forClass(Path.class).forAllLocations( foundRepositories::put ); assertThat(foundRepositories) diff --git a/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java b/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java index bdf28310e1..f5571441e7 100644 --- a/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java +++ b/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java @@ -26,15 +26,13 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.Collection; import java.util.function.BiConsumer; +import java.util.function.Consumer; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.fail; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -47,6 +45,7 @@ class XmlRepositoryDAOTest { @Mock private PathBasedRepositoryLocationResolver locationResolver; + private Consumer> triggeredOnForAllLocations = none -> {}; private FileSystem fileSystem = new DefaultFileSystem(); @@ -69,6 +68,11 @@ class XmlRepositoryDAOTest { @Override public void setLocation(String repositoryId, Path location) { } + + @Override + public void forAllLocations(BiConsumer consumer) { + triggeredOnForAllLocations.accept(consumer); + } } ); when(locationResolver.create(anyString())).thenAnswer(invocation -> createMockedRepoPath(basePath, invocation)); @@ -332,11 +336,10 @@ class XmlRepositoryDAOTest { @Test void shouldRefreshWithExistingRepositoriesFromPathDatabase() { // given - doNothing().when(locationResolver).forAllPaths(any()); - XmlRepositoryDAO dao = new XmlRepositoryDAO(locationResolver, fileSystem); - mockExistingPath(); + XmlRepositoryDAO dao = new XmlRepositoryDAO(locationResolver, fileSystem); + // when dao.refresh(); @@ -346,12 +349,7 @@ class XmlRepositoryDAOTest { } private void mockExistingPath() { - doAnswer( - invocation -> { - ((BiConsumer) invocation.getArgument(0)).accept("existing", repositoryPath); - return null; - } - ).when(locationResolver).forAllPaths(any()); + triggeredOnForAllLocations = consumer -> consumer.accept("existing", repositoryPath); } } diff --git a/scm-it/src/test/java/sonia/scm/it/utils/TestData.java b/scm-it/src/test/java/sonia/scm/it/utils/TestData.java index 20de47ffa4..d632f13f60 100644 --- a/scm-it/src/test/java/sonia/scm/it/utils/TestData.java +++ b/scm-it/src/test/java/sonia/scm/it/utils/TestData.java @@ -123,23 +123,6 @@ public class TestData { ; } - public static void createUserPermission(String username, String roleName, String repositoryType) { - String defaultPermissionUrl = TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, repositoryType); - LOG.info("create permission with name {} and role {} using the endpoint: {}", username, roleName, defaultPermissionUrl); - given(VndMediaType.REPOSITORY_PERMISSION) - .when() - .content("{\n" + - "\t\"role\": " + roleName + ",\n" + - "\t\"name\": \"" + username + "\",\n" + - "\t\"groupPermission\": false\n" + - "\t\n" + - "}") - .post(defaultPermissionUrl) - .then() - .statusCode(HttpStatus.SC_CREATED) - ; - } - public static List getUserPermissions(String username, String password, String repositoryType) { return callUserPermissions(username, password, repositoryType, HttpStatus.SC_OK) .extract() diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitConfigHelper.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitConfigHelper.java new file mode 100644 index 0000000000..a978295166 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitConfigHelper.java @@ -0,0 +1,21 @@ +package sonia.scm.repository; + +import org.eclipse.jgit.lib.StoredConfig; + +import java.io.IOException; + +public class GitConfigHelper { + + private static final String CONFIG_SECTION_SCMM = "scmm"; + private static final String CONFIG_KEY_REPOSITORY_ID = "repositoryid"; + + public void createScmmConfig(Repository repository, org.eclipse.jgit.lib.Repository gitRepository) throws IOException { + StoredConfig config = gitRepository.getConfig(); + config.setString(CONFIG_SECTION_SCMM, null, CONFIG_KEY_REPOSITORY_ID, repository.getId()); + config.save(); + } + + public String getRepositoryId(StoredConfig gitConfig) { + return gitConfig.getString(CONFIG_SECTION_SCMM, null, CONFIG_KEY_REPOSITORY_ID); + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryHandler.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryHandler.java index 63800e8a02..e07b3d0d83 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryHandler.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryHandler.java @@ -89,8 +89,6 @@ public class GitRepositoryHandler GitRepositoryServiceProvider.COMMANDS); private static final Object LOCK = new Object(); - private static final String CONFIG_SECTION_SCMM = "scmm"; - private static final String CONFIG_KEY_REPOSITORY_ID = "repositoryid"; private final Scheduler scheduler; @@ -185,7 +183,7 @@ public class GitRepositoryHandler } public String getRepositoryId(StoredConfig gitConfig) { - return gitConfig.getString(GitRepositoryHandler.CONFIG_SECTION_SCMM, null, GitRepositoryHandler.CONFIG_KEY_REPOSITORY_ID); + return new GitConfigHelper().getRepositoryId(gitConfig); } //~--- methods -------------------------------------------------------------- @@ -194,9 +192,7 @@ public class GitRepositoryHandler protected void create(Repository repository, File directory) throws IOException { try (org.eclipse.jgit.lib.Repository gitRepository = build(directory)) { gitRepository.create(true); - StoredConfig config = gitRepository.getConfig(); - config.setString(CONFIG_SECTION_SCMM, null, CONFIG_KEY_REPOSITORY_ID, repository.getId()); - config.save(); + new GitConfigHelper().createScmmConfig(repository, gitRepository); } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java index 7aacdb256a..7175d3b646 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java @@ -43,6 +43,7 @@ import org.eclipse.jgit.api.FetchCommand; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.diff.DiffFormatter; +import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Ref; @@ -441,7 +442,7 @@ public final class GitUtil * * @return */ - public static String getId(ObjectId objectId) + public static String getId(AnyObjectId objectId) { String id = Util.EMPTY_STRING; diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/Differ.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/Differ.java new file mode 100644 index 0000000000..ca417550f4 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/Differ.java @@ -0,0 +1,118 @@ +package sonia.scm.repository.spi; + +import com.google.common.base.Strings; +import org.eclipse.jgit.diff.DiffEntry; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevTree; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.treewalk.EmptyTreeIterator; +import org.eclipse.jgit.treewalk.TreeWalk; +import org.eclipse.jgit.treewalk.filter.PathFilter; +import sonia.scm.repository.GitUtil; +import sonia.scm.util.Util; + +import java.io.IOException; +import java.util.List; + +final class Differ implements AutoCloseable { + + private final RevWalk walk; + private final TreeWalk treeWalk; + private final RevCommit commit; + + private Differ(RevCommit commit, RevWalk walk, TreeWalk treeWalk) { + this.commit = commit; + this.walk = walk; + this.treeWalk = treeWalk; + } + + static Diff diff(Repository repository, DiffCommandRequest request) throws IOException { + try (Differ differ = create(repository, request)) { + return differ.diff(); + } + } + + private static Differ create(Repository repository, DiffCommandRequest request) throws IOException { + RevWalk walk = new RevWalk(repository); + + ObjectId revision = repository.resolve(request.getRevision()); + RevCommit commit = walk.parseCommit(revision); + + walk.markStart(commit); + commit = walk.next(); + TreeWalk treeWalk = new TreeWalk(repository); + treeWalk.reset(); + treeWalk.setRecursive(true); + + if (Util.isNotEmpty(request.getPath())) + { + treeWalk.setFilter(PathFilter.create(request.getPath())); + } + + + if (!Strings.isNullOrEmpty(request.getAncestorChangeset())) + { + ObjectId otherRevision = repository.resolve(request.getAncestorChangeset()); + ObjectId ancestorId = computeCommonAncestor(repository, revision, otherRevision); + RevTree tree = walk.parseCommit(ancestorId).getTree(); + treeWalk.addTree(tree); + } + else if (commit.getParentCount() > 0) + { + RevTree tree = commit.getParent(0).getTree(); + + if (tree != null) + { + treeWalk.addTree(tree); + } + else + { + treeWalk.addTree(new EmptyTreeIterator()); + } + } + else + { + treeWalk.addTree(new EmptyTreeIterator()); + } + + treeWalk.addTree(commit.getTree()); + + return new Differ(commit, walk, treeWalk); + } + + private static ObjectId computeCommonAncestor(org.eclipse.jgit.lib.Repository repository, ObjectId revision1, ObjectId revision2) throws IOException { + return GitUtil.computeCommonAncestor(repository, revision1, revision2); + } + + private Diff diff() throws IOException { + List entries = DiffEntry.scan(treeWalk); + return new Diff(commit, entries); + } + + @Override + public void close() { + GitUtil.release(walk); + GitUtil.release(treeWalk); + } + + public static class Diff { + + private final RevCommit commit; + private final List entries; + + private Diff(RevCommit commit, List entries) { + this.commit = commit; + this.entries = entries; + } + + public RevCommit getCommit() { + return commit; + } + + public List getEntries() { + return entries; + } + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/FileRange.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/FileRange.java new file mode 100644 index 0000000000..8d445e1c44 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/FileRange.java @@ -0,0 +1,20 @@ +package sonia.scm.repository.spi; + +public class FileRange { + + private final int start; + private final int lineCount; + + public FileRange(int start, int lineCount) { + this.start = start; + this.lineCount = lineCount; + } + + public int getStart() { + return start; + } + + public int getLineCount() { + return lineCount; + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffCommand.java index 2d56c8e786..5d5f27806b 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffCommand.java @@ -1,19 +1,19 @@ /** * 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. + * 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. + * 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. - * + * 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 @@ -24,9 +24,8 @@ * 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 - * */ @@ -34,148 +33,41 @@ package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- -import com.google.common.base.Strings; import org.eclipse.jgit.diff.DiffEntry; import org.eclipse.jgit.diff.DiffFormatter; -import org.eclipse.jgit.lib.ObjectId; -import org.eclipse.jgit.revwalk.RevCommit; -import org.eclipse.jgit.revwalk.RevTree; -import org.eclipse.jgit.revwalk.RevWalk; -import org.eclipse.jgit.treewalk.EmptyTreeIterator; -import org.eclipse.jgit.treewalk.TreeWalk; -import org.eclipse.jgit.treewalk.filter.PathFilter; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import sonia.scm.repository.GitUtil; import sonia.scm.repository.Repository; -import sonia.scm.util.Util; import java.io.BufferedOutputStream; import java.io.IOException; import java.io.OutputStream; -import java.util.List; /** * * @author Sebastian Sdorra */ -public class GitDiffCommand extends AbstractGitCommand implements DiffCommand -{ +public class GitDiffCommand extends AbstractGitCommand implements DiffCommand { - /** - * the logger for GitDiffCommand - */ - private static final Logger logger = - LoggerFactory.getLogger(GitDiffCommand.class); - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - * - * - * @param context - * @param repository - */ - public GitDiffCommand(GitContext context, Repository repository) - { + GitDiffCommand(GitContext context, Repository repository) { super(context, repository); } - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param request - * @param output - */ @Override - public void getDiffResult(DiffCommandRequest request, OutputStream output) - { - RevWalk walk = null; - TreeWalk treeWalk = null; - DiffFormatter formatter = null; + public void getDiffResult(DiffCommandRequest request, OutputStream output) throws IOException { + @SuppressWarnings("squid:S2095") // repository will be closed with the RepositoryService + org.eclipse.jgit.lib.Repository repository = open(); + try (DiffFormatter formatter = new DiffFormatter(new BufferedOutputStream(output))) { + formatter.setRepository(repository); - try - { - org.eclipse.jgit.lib.Repository gr = open(); + Differ.Diff diff = Differ.diff(repository, request); - walk = new RevWalk(gr); - - ObjectId revision = gr.resolve(request.getRevision()); - RevCommit commit = walk.parseCommit(revision); - - walk.markStart(commit); - commit = walk.next(); - treeWalk = new TreeWalk(gr); - treeWalk.reset(); - treeWalk.setRecursive(true); - - if (Util.isNotEmpty(request.getPath())) - { - treeWalk.setFilter(PathFilter.create(request.getPath())); - } - - - if (!Strings.isNullOrEmpty(request.getAncestorChangeset())) - { - ObjectId otherRevision = gr.resolve(request.getAncestorChangeset()); - ObjectId ancestorId = computeCommonAncestor(gr, revision, otherRevision); - RevTree tree = walk.parseCommit(ancestorId).getTree(); - treeWalk.addTree(tree); - } - else if (commit.getParentCount() > 0) - { - RevTree tree = commit.getParent(0).getTree(); - - if (tree != null) - { - treeWalk.addTree(tree); - } - else - { - treeWalk.addTree(new EmptyTreeIterator()); - } - } - else - { - treeWalk.addTree(new EmptyTreeIterator()); - } - - treeWalk.addTree(commit.getTree()); - formatter = new DiffFormatter(new BufferedOutputStream(output)); - formatter.setRepository(gr); - - List entries = DiffEntry.scan(treeWalk); - - for (DiffEntry e : entries) - { - if (!e.getOldId().equals(e.getNewId())) - { + for (DiffEntry e : diff.getEntries()) { + if (!e.getOldId().equals(e.getNewId())) { formatter.format(e); } } formatter.flush(); } - catch (Exception ex) - { - // TODO throw exception - logger.error("could not create diff", ex); - } - finally - { - GitUtil.release(walk); - GitUtil.release(treeWalk); - GitUtil.release(formatter); - } - } - - private ObjectId computeCommonAncestor(org.eclipse.jgit.lib.Repository repository, ObjectId revision1, ObjectId revision2) throws IOException { - return GitUtil.computeCommonAncestor(repository, revision1, revision2); } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffResultCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffResultCommand.java new file mode 100644 index 0000000000..e7f26d6885 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffResultCommand.java @@ -0,0 +1,107 @@ +package sonia.scm.repository.spi; + +import org.eclipse.jgit.diff.DiffEntry; +import org.eclipse.jgit.diff.DiffFormatter; +import sonia.scm.repository.GitUtil; +import sonia.scm.repository.InternalRepositoryException; +import sonia.scm.repository.Repository; +import sonia.scm.repository.api.DiffFile; +import sonia.scm.repository.api.DiffResult; +import sonia.scm.repository.api.Hunk; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Iterator; +import java.util.stream.Collectors; + +public class GitDiffResultCommand extends AbstractGitCommand implements DiffResultCommand { + + GitDiffResultCommand(GitContext context, Repository repository) { + super(context, repository); + } + + public DiffResult getDiffResult(DiffCommandRequest diffCommandRequest) throws IOException { + org.eclipse.jgit.lib.Repository repository = open(); + return new GitDiffResult(repository, Differ.diff(repository, diffCommandRequest)); + } + + private class GitDiffResult implements DiffResult { + + private final org.eclipse.jgit.lib.Repository repository; + private final Differ.Diff diff; + + private GitDiffResult(org.eclipse.jgit.lib.Repository repository, Differ.Diff diff) { + this.repository = repository; + this.diff = diff; + } + + @Override + public String getOldRevision() { + return GitUtil.getId(diff.getCommit().getParent(0).getId()); + } + + @Override + public String getNewRevision() { + return GitUtil.getId(diff.getCommit().getId()); + } + + @Override + public Iterator iterator() { + return diff.getEntries() + .stream() + .map(diffEntry -> new GitDiffFile(repository, diffEntry)) + .collect(Collectors.toList()) + .iterator(); + } + } + + private class GitDiffFile implements DiffFile { + + private final org.eclipse.jgit.lib.Repository repository; + private final DiffEntry diffEntry; + + private GitDiffFile(org.eclipse.jgit.lib.Repository repository, DiffEntry diffEntry) { + this.repository = repository; + this.diffEntry = diffEntry; + } + + @Override + public String getOldRevision() { + return GitUtil.getId(diffEntry.getOldId().toObjectId()); + } + + @Override + public String getNewRevision() { + return GitUtil.getId(diffEntry.getNewId().toObjectId()); + } + + @Override + public String getOldPath() { + return diffEntry.getOldPath(); + } + + @Override + public String getNewPath() { + return diffEntry.getNewPath(); + } + + @Override + public Iterator iterator() { + String content = format(repository, diffEntry); + GitHunkParser parser = new GitHunkParser(); + return parser.parse(content).iterator(); + } + + private String format(org.eclipse.jgit.lib.Repository repository, DiffEntry entry) { + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); DiffFormatter formatter = new DiffFormatter(baos)) { + formatter.setRepository(repository); + formatter.format(entry); + return baos.toString(); + } catch (IOException ex) { + throw new InternalRepositoryException(GitDiffResultCommand.this.repository, "failed to format diff entry", ex); + } + } + + } + +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHunk.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHunk.java new file mode 100644 index 0000000000..9a272d7b10 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHunk.java @@ -0,0 +1,48 @@ +package sonia.scm.repository.spi; + +import sonia.scm.repository.api.DiffLine; +import sonia.scm.repository.api.Hunk; + +import java.util.Iterator; +import java.util.List; + +public class GitHunk implements Hunk { + + private final FileRange oldFileRange; + private final FileRange newFileRange; + private List lines; + + public GitHunk(FileRange oldFileRange, FileRange newFileRange) { + this.oldFileRange = oldFileRange; + this.newFileRange = newFileRange; + } + + @Override + public int getOldStart() { + return oldFileRange.getStart(); + } + + @Override + public int getOldLineCount() { + return oldFileRange.getLineCount(); + } + + @Override + public int getNewStart() { + return newFileRange.getStart(); + } + + @Override + public int getNewLineCount() { + return newFileRange.getLineCount(); + } + + @Override + public Iterator iterator() { + return lines.iterator(); + } + + void setLines(List lines) { + this.lines = lines; + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHunkParser.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHunkParser.java new file mode 100644 index 0000000000..b7594bb5d6 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHunkParser.java @@ -0,0 +1,176 @@ +package sonia.scm.repository.spi; + +import sonia.scm.repository.api.DiffLine; +import sonia.scm.repository.api.Hunk; + +import java.util.ArrayList; +import java.util.List; +import java.util.OptionalInt; +import java.util.Scanner; + +import static java.util.OptionalInt.of; + +final class GitHunkParser { + private static final int HEADER_PREFIX_LENGTH = "@@ -".length(); + private static final int HEADER_SUFFIX_LENGTH = " @@".length(); + + private GitHunk currentGitHunk = null; + private List collectedLines = null; + private int oldLineCounter = 0; + private int newLineCounter = 0; + + GitHunkParser() { + } + + public List parse(String content) { + List hunks = new ArrayList<>(); + + try (Scanner scanner = new Scanner(content)) { + while (scanner.hasNextLine()) { + String line = scanner.nextLine(); + if (line.startsWith("@@")) { + parseHeader(hunks, line); + } else if (currentGitHunk != null) { + parseDiffLine(line); + } + } + } + if (currentGitHunk != null) { + currentGitHunk.setLines(collectedLines); + } + + return hunks; + } + + private void parseHeader(List hunks, String line) { + if (currentGitHunk != null) { + currentGitHunk.setLines(collectedLines); + } + String hunkHeader = line.substring(HEADER_PREFIX_LENGTH, line.length() - HEADER_SUFFIX_LENGTH); + String[] split = hunkHeader.split("\\s"); + + FileRange oldFileRange = createFileRange(split[0]); + // TODO merge contains two two block which starts with "-" e.g. -1,3 -2,4 +3,6 + // check if it is relevant for our use case + FileRange newFileRange = createFileRange(split[1]); + + currentGitHunk = new GitHunk(oldFileRange, newFileRange); + hunks.add(currentGitHunk); + + collectedLines = new ArrayList<>(); + oldLineCounter = currentGitHunk.getOldStart(); + newLineCounter = currentGitHunk.getNewStart(); + } + + private void parseDiffLine(String line) { + String content = line.substring(1); + switch (line.charAt(0)) { + case ' ': + collectedLines.add(new UnchangedGitDiffLine(newLineCounter, oldLineCounter, content)); + ++newLineCounter; + ++oldLineCounter; + break; + case '+': + collectedLines.add(new AddedGitDiffLine(newLineCounter, content)); + ++newLineCounter; + break; + case '-': + collectedLines.add(new RemovedGitDiffLine(oldLineCounter, content)); + ++oldLineCounter; + break; + default: + throw new IllegalStateException("cannot handle diff line: " + line); + } + } + + private static class AddedGitDiffLine implements DiffLine { + private final int newLineNumber; + private final String content; + + private AddedGitDiffLine(int newLineNumber, String content) { + this.newLineNumber = newLineNumber; + this.content = content; + } + + @Override + public OptionalInt getOldLineNumber() { + return OptionalInt.empty(); + } + + @Override + public OptionalInt getNewLineNumber() { + return of(newLineNumber); + } + + @Override + public String getContent() { + return content; + } + } + + private static class RemovedGitDiffLine implements DiffLine { + private final int oldLineNumber; + private final String content; + + private RemovedGitDiffLine(int oldLineNumber, String content) { + this.oldLineNumber = oldLineNumber; + this.content = content; + } + + @Override + public OptionalInt getOldLineNumber() { + return of(oldLineNumber); + } + + @Override + public OptionalInt getNewLineNumber() { + return OptionalInt.empty(); + } + + @Override + public String getContent() { + return content; + } + } + + private static class UnchangedGitDiffLine implements DiffLine { + private final int newLineNumber; + private final int oldLineNumber; + private final String content; + + private UnchangedGitDiffLine(int newLineNumber, int oldLineNumber, String content) { + this.newLineNumber = newLineNumber; + this.oldLineNumber = oldLineNumber; + this.content = content; + } + + @Override + public OptionalInt getOldLineNumber() { + return of(oldLineNumber); + } + + @Override + public OptionalInt getNewLineNumber() { + return of(newLineNumber); + } + + @Override + public String getContent() { + return content; + } + } + + private static FileRange createFileRange(String fileRangeString) { + int start; + int lineCount = 1; + int commaIndex = fileRangeString.indexOf(','); + if (commaIndex > 0) { + start = Integer.parseInt(fileRangeString.substring(0, commaIndex)); + lineCount = Integer.parseInt(fileRangeString.substring(commaIndex + 1)); + } else { + start = Integer.parseInt(fileRangeString); + } + + return new FileRange(start, lineCount); + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/update/GitV2UpdateStep.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/update/GitV2UpdateStep.java new file mode 100644 index 0000000000..afcde567e3 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/update/GitV2UpdateStep.java @@ -0,0 +1,70 @@ +package sonia.scm.repository.update; + +import org.eclipse.jgit.storage.file.FileRepositoryBuilder; +import sonia.scm.migration.UpdateException; +import sonia.scm.migration.UpdateStep; +import sonia.scm.plugin.Extension; +import sonia.scm.repository.GitConfigHelper; +import sonia.scm.repository.GitRepositoryHandler; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryLocationResolver; +import sonia.scm.update.UpdateStepRepositoryMetadataAccess; +import sonia.scm.version.Version; + +import javax.inject.Inject; +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; + +import static sonia.scm.version.Version.parse; + +@Extension +public class GitV2UpdateStep implements UpdateStep { + + private final RepositoryLocationResolver locationResolver; + private final UpdateStepRepositoryMetadataAccess repositoryMetadataAccess; + + @Inject + public GitV2UpdateStep(RepositoryLocationResolver locationResolver, UpdateStepRepositoryMetadataAccess repositoryMetadataAccess) { + this.locationResolver = locationResolver; + this.repositoryMetadataAccess = repositoryMetadataAccess; + } + + @Override + public void doUpdate() { + locationResolver.forClass(Path.class).forAllLocations( + (repositoryId, path) -> { + Repository repository = repositoryMetadataAccess.read(path); + if (isGitDirectory(repository)) { + try (org.eclipse.jgit.lib.Repository gitRepository = build(path.resolve("data").toFile())) { + new GitConfigHelper().createScmmConfig(repository, gitRepository); + } catch (IOException e) { + throw new UpdateException("could not update repository with id " + repositoryId + " in path " + path, e); + } + } + } + ); + } + + private org.eclipse.jgit.lib.Repository build(File directory) throws IOException { + return new FileRepositoryBuilder() + .setGitDir(directory) + .readEnvironment() + .findGitDir() + .build(); + } + + private boolean isGitDirectory(Repository repository) { + return GitRepositoryHandler.TYPE_NAME.equals(repository.getType()); + } + + @Override + public Version getTargetVersion() { + return parse("2.0.0"); + } + + @Override + public String getAffectedDataType() { + return "sonia.scm.plugin.git"; + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/LfsV1UpdateStep.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/LfsV1UpdateStep.java new file mode 100644 index 0000000000..2684222c32 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/LfsV1UpdateStep.java @@ -0,0 +1,43 @@ +package sonia.scm.web.lfs; + +import sonia.scm.migration.UpdateStep; +import sonia.scm.plugin.Extension; +import sonia.scm.update.BlobDirectoryAccess; +import sonia.scm.version.Version; + +import javax.inject.Inject; +import java.nio.file.Path; + +@Extension +public class LfsV1UpdateStep implements UpdateStep { + + private final BlobDirectoryAccess blobDirectoryAccess; + + @Inject + public LfsV1UpdateStep(BlobDirectoryAccess blobDirectoryAccess) { + this.blobDirectoryAccess = blobDirectoryAccess; + } + + @Override + public void doUpdate() throws Exception { + blobDirectoryAccess.forBlobDirectories( + f -> { + Path v1Directory = f.getFileName(); + String v1DirectoryName = v1Directory.toString(); + if (v1DirectoryName.endsWith("-git-lfs")) { + blobDirectoryAccess.moveToRepositoryBlobStore(f, v1DirectoryName, v1DirectoryName.substring(0, v1DirectoryName.length() - "-git-lfs".length())); + } + } + ); + } + + @Override + public Version getTargetVersion() { + return Version.parse("2.0.0"); + } + + @Override + public String getAffectedDataType() { + return "sonia.scm.git.lfs"; + } +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommandTest.java index f6e462f968..fd9c45be5c 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommandTest.java @@ -3,6 +3,7 @@ package sonia.scm.repository.spi; import org.junit.Test; import java.io.ByteArrayOutputStream; +import java.io.IOException; import static org.junit.Assert.assertEquals; @@ -38,7 +39,7 @@ public class GitDiffCommandTest extends AbstractGitCommandTestBase { "+f\n"; @Test - public void diffForOneRevisionShouldCreateDiff() { + public void diffForOneRevisionShouldCreateDiff() throws IOException { GitDiffCommand gitDiffCommand = new GitDiffCommand(createContext(), repository); DiffCommandRequest diffCommandRequest = new DiffCommandRequest(); diffCommandRequest.setRevision("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4"); @@ -48,7 +49,7 @@ public class GitDiffCommandTest extends AbstractGitCommandTestBase { } @Test - public void diffForOneBranchShouldCreateDiff() { + public void diffForOneBranchShouldCreateDiff() throws IOException { GitDiffCommand gitDiffCommand = new GitDiffCommand(createContext(), repository); DiffCommandRequest diffCommandRequest = new DiffCommandRequest(); diffCommandRequest.setRevision("test-branch"); @@ -58,7 +59,7 @@ public class GitDiffCommandTest extends AbstractGitCommandTestBase { } @Test - public void diffForPathShouldCreateLimitedDiff() { + public void diffForPathShouldCreateLimitedDiff() throws IOException { GitDiffCommand gitDiffCommand = new GitDiffCommand(createContext(), repository); DiffCommandRequest diffCommandRequest = new DiffCommandRequest(); diffCommandRequest.setRevision("test-branch"); @@ -69,7 +70,7 @@ public class GitDiffCommandTest extends AbstractGitCommandTestBase { } @Test - public void diffBetweenTwoBranchesShouldCreateDiff() { + public void diffBetweenTwoBranchesShouldCreateDiff() throws IOException { GitDiffCommand gitDiffCommand = new GitDiffCommand(createContext(), repository); DiffCommandRequest diffCommandRequest = new DiffCommandRequest(); diffCommandRequest.setRevision("master"); @@ -80,7 +81,7 @@ public class GitDiffCommandTest extends AbstractGitCommandTestBase { } @Test - public void diffBetweenTwoBranchesForPathShouldCreateLimitedDiff() { + public void diffBetweenTwoBranchesForPathShouldCreateLimitedDiff() throws IOException { GitDiffCommand gitDiffCommand = new GitDiffCommand(createContext(), repository); DiffCommandRequest diffCommandRequest = new DiffCommandRequest(); diffCommandRequest.setRevision("master"); diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffResultCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffResultCommandTest.java new file mode 100644 index 0000000000..f359ae987c --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffResultCommandTest.java @@ -0,0 +1,89 @@ +package sonia.scm.repository.spi; + +import org.junit.Test; +import sonia.scm.repository.api.DiffFile; +import sonia.scm.repository.api.DiffResult; +import sonia.scm.repository.api.Hunk; + +import java.io.IOException; +import java.util.Iterator; + +import static org.assertj.core.api.Assertions.assertThat; + +public class GitDiffResultCommandTest extends AbstractGitCommandTestBase { + + @Test + public void shouldReturnOldAndNewRevision() throws IOException { + DiffResult diffResult = createDiffResult("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4"); + + assertThat(diffResult.getNewRevision()).isEqualTo("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4"); + assertThat(diffResult.getOldRevision()).isEqualTo("592d797cd36432e591416e8b2b98154f4f163411"); + } + + @Test + public void shouldReturnFilePaths() throws IOException { + DiffResult diffResult = createDiffResult("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4"); + Iterator iterator = diffResult.iterator(); + DiffFile a = iterator.next(); + assertThat(a.getNewPath()).isEqualTo("a.txt"); + assertThat(a.getOldPath()).isEqualTo("a.txt"); + + DiffFile b = iterator.next(); + assertThat(b.getOldPath()).isEqualTo("b.txt"); + assertThat(b.getNewPath()).isEqualTo("/dev/null"); + } + + @Test + public void shouldReturnFileRevisions() throws IOException { + DiffResult diffResult = createDiffResult("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4"); + Iterator iterator = diffResult.iterator(); + + DiffFile a = iterator.next(); + assertThat(a.getOldRevision()).isEqualTo("78981922613b2afb6025042ff6bd878ac1994e85"); + assertThat(a.getNewRevision()).isEqualTo("1dc60c7504f4326bc83b9b628c384ec8d7e57096"); + + DiffFile b = iterator.next(); + assertThat(b.getOldRevision()).isEqualTo("61780798228d17af2d34fce4cfbdf35556832472"); + assertThat(b.getNewRevision()).isEqualTo("0000000000000000000000000000000000000000"); + } + + @Test + public void shouldReturnFileHunks() throws IOException { + DiffResult diffResult = createDiffResult("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4"); + Iterator iterator = diffResult.iterator(); + + DiffFile a = iterator.next(); + Iterator hunks = a.iterator(); + + Hunk hunk = hunks.next(); + assertThat(hunk.getOldStart()).isEqualTo(1); + assertThat(hunk.getOldLineCount()).isEqualTo(1); + + assertThat(hunk.getNewStart()).isEqualTo(1); + assertThat(hunk.getNewLineCount()).isEqualTo(1); + } + + @Test + public void shouldReturnFileHunksWithFullFileRange() throws IOException { + DiffResult diffResult = createDiffResult("fcd0ef1831e4002ac43ea539f4094334c79ea9ec"); + Iterator iterator = diffResult.iterator(); + + DiffFile a = iterator.next(); + Iterator hunks = a.iterator(); + + Hunk hunk = hunks.next(); + assertThat(hunk.getOldStart()).isEqualTo(1); + assertThat(hunk.getOldLineCount()).isEqualTo(1); + + assertThat(hunk.getNewStart()).isEqualTo(1); + assertThat(hunk.getNewLineCount()).isEqualTo(2); + } + + private DiffResult createDiffResult(String s) throws IOException { + GitDiffResultCommand gitDiffResultCommand = new GitDiffResultCommand(createContext(), repository); + DiffCommandRequest diffCommandRequest = new DiffCommandRequest(); + diffCommandRequest.setRevision(s); + + return gitDiffResultCommand.getDiffResult(diffCommandRequest); + } +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitHunkParserTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitHunkParserTest.java new file mode 100644 index 0000000000..a58fae644d --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitHunkParserTest.java @@ -0,0 +1,138 @@ +package sonia.scm.repository.spi; + +import org.junit.jupiter.api.Test; +import sonia.scm.repository.api.DiffLine; +import sonia.scm.repository.api.Hunk; + +import java.util.Iterator; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class GitHunkParserTest { + + private static final String DIFF_001 = "diff --git a/a.txt b/a.txt\n" + + "index 7898192..2f8bc28 100644\n" + + "--- a/a.txt\n" + + "+++ b/a.txt\n" + + "@@ -1 +1,2 @@\n" + + " a\n" + + "+added line\n"; + + private static final String DIFF_002 = "diff --git a/file b/file\n" + + "index 5e89957..e8823e1 100644\n" + + "--- a/file\n" + + "+++ b/file\n" + + "@@ -2,6 +2,9 @@\n" + + " 2\n" + + " 3\n" + + " 4\n" + + "+5\n" + + "+6\n" + + "+7\n" + + " 8\n" + + " 9\n" + + " 10\n" + + "@@ -15,14 +18,13 @@\n" + + " 18\n" + + " 19\n" + + " 20\n" + + "+21\n" + + "+22\n" + + " 23\n" + + " 24\n" + + " 25\n" + + " 26\n" + + " 27\n" + + "-a\n" + + "-b\n" + + "-c\n" + + " 28\n" + + " 29\n" + + " 30"; + + private static final String DIFF_003 = "diff --git a/a.txt b/a.txt\n" + + "index 7898192..2f8bc28 100644\n" + + "--- a/a.txt\n" + + "+++ b/a.txt\n" + + "@@ -1,2 +1 @@\n" + + " a\n" + + "-removed line\n"; + + private static final String ILLEGAL_DIFF = "diff --git a/a.txt b/a.txt\n" + + "index 7898192..2f8bc28 100644\n" + + "--- a/a.txt\n" + + "+++ b/a.txt\n" + + "@@ -1,2 +1 @@\n" + + " a\n" + + "~illegal line\n"; + + @Test + void shouldParseHunks() { + List hunks = new GitHunkParser().parse(DIFF_001); + assertThat(hunks).hasSize(1); + assertHunk(hunks.get(0), 1, 1, 1, 2); + } + + @Test + void shouldParseMultipleHunks() { + List hunks = new GitHunkParser().parse(DIFF_002); + + assertThat(hunks).hasSize(2); + assertHunk(hunks.get(0), 2, 6, 2, 9); + assertHunk(hunks.get(1), 15, 14, 18, 13); + } + + @Test + void shouldParseAddedHunkLines() { + List hunks = new GitHunkParser().parse(DIFF_001); + + Hunk hunk = hunks.get(0); + + Iterator lines = hunk.iterator(); + + DiffLine line1 = lines.next(); + assertThat(line1.getOldLineNumber()).hasValue(1); + assertThat(line1.getNewLineNumber()).hasValue(1); + assertThat(line1.getContent()).isEqualTo("a"); + + DiffLine line2 = lines.next(); + assertThat(line2.getOldLineNumber()).isEmpty(); + assertThat(line2.getNewLineNumber()).hasValue(2); + assertThat(line2.getContent()).isEqualTo("added line"); + } + + @Test + void shouldParseRemovedHunkLines() { + List hunks = new GitHunkParser().parse(DIFF_003); + + Hunk hunk = hunks.get(0); + + Iterator lines = hunk.iterator(); + + DiffLine line1 = lines.next(); + assertThat(line1.getOldLineNumber()).hasValue(1); + assertThat(line1.getNewLineNumber()).hasValue(1); + assertThat(line1.getContent()).isEqualTo("a"); + + DiffLine line2 = lines.next(); + assertThat(line2.getOldLineNumber()).hasValue(2); + assertThat(line2.getNewLineNumber()).isEmpty(); + assertThat(line2.getContent()).isEqualTo("removed line"); + } + + @Test + void shouldFailForIllegalLine() { + assertThrows(IllegalStateException.class, () -> new GitHunkParser().parse(ILLEGAL_DIFF)); + } + + private void assertHunk(Hunk hunk, int oldStart, int oldLineCount, int newStart, int newLineCount) { + assertThat(hunk.getOldStart()).isEqualTo(oldStart); + assertThat(hunk.getOldLineCount()).isEqualTo(oldLineCount); + + assertThat(hunk.getNewStart()).isEqualTo(newStart); + assertThat(hunk.getNewLineCount()).isEqualTo(newLineCount); + } + +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgEnvironment.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgEnvironment.java index 51ae6d5786..1d227fb54e 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgEnvironment.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgEnvironment.java @@ -40,11 +40,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.web.HgUtil; -//~--- JDK imports ------------------------------------------------------------ - +import javax.servlet.http.HttpServletRequest; import java.util.Map; -import javax.servlet.http.HttpServletRequest; +//~--- JDK imports ------------------------------------------------------------ /** * @@ -118,7 +117,7 @@ public final class HgEnvironment String credentials = hookManager.getCredentials(); environment.put(SCM_BEARER_TOKEN, credentials); } catch (ProvisionException e) { - LOG.debug("could not create bearer token; looks like currently we are not in a request", e); + LOG.debug("could not create bearer token; looks like currently we are not in a request; probably you can ignore the following exception:", e); } environment.put(ENV_PYTHON_PATH, HgUtil.getPythonPath(handler.getConfig())); environment.put(ENV_URL, hookUrl); diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgRepositoryHandler.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgRepositoryHandler.java index 4ccf13e738..0a8dfe065a 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgRepositoryHandler.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgRepositoryHandler.java @@ -41,13 +41,11 @@ import com.google.inject.Singleton; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.ConfigurationException; -import sonia.scm.ContextEntry; import sonia.scm.SCMContextProvider; import sonia.scm.installer.HgInstaller; import sonia.scm.installer.HgInstallerFactory; import sonia.scm.io.ExtendedCommand; import sonia.scm.io.INIConfiguration; -import sonia.scm.io.INIConfigurationReader; import sonia.scm.io.INIConfigurationWriter; import sonia.scm.io.INISection; import sonia.scm.plugin.Extension; @@ -347,14 +345,6 @@ public class HgRepositoryHandler writer.write(hgrc, hgrcFile); } - public String getRepositoryId(File directory) { - try { - return new INIConfigurationReader().read(new File(directory, PATH_HGRC)).getSection(CONFIG_SECTION_SCMM).getParameter(CONFIG_KEY_REPOSITORY_ID); - } catch (IOException e) { - throw new InternalRepositoryException(ContextEntry.ContextBuilder.entity("Directory", directory.toString()), "could not read scm configuration file", e); - } - } - //~--- get methods ---------------------------------------------------------- /** diff --git a/scm-plugins/scm-legacy-plugin/package.json b/scm-plugins/scm-legacy-plugin/package.json new file mode 100644 index 0000000000..84f78f419a --- /dev/null +++ b/scm-plugins/scm-legacy-plugin/package.json @@ -0,0 +1,20 @@ +{ + "name": "@scm-manager/legacy-plugin", + "license": "BSD-3-Clause", + "main": "src/main/js/index.js", + "scripts": { + "build": "ui-bundler plugin", + "watch": "ui-bundler plugin -w", + "lint": "ui-bundler lint", + "flow": "flow check" + }, + "dependencies": { + "@scm-manager/ui-components": "latest", + "@scm-manager/ui-extensions": "^0.1.1", + "react-redux": "^5.0.7", + "@scm-manager/ui-types": "latest" + }, + "devDependencies": { + "@scm-manager/ui-bundler": "^0.0.25" + } +} diff --git a/scm-plugins/scm-legacy-plugin/pom.xml b/scm-plugins/scm-legacy-plugin/pom.xml index 52aff51dd2..6cfa74ea61 100644 --- a/scm-plugins/scm-legacy-plugin/pom.xml +++ b/scm-plugins/scm-legacy-plugin/pom.xml @@ -21,7 +21,13 @@ ${servlet.version} provided - + + javax.ws.rs + jsr311-api + 1.1.1 + compile + + diff --git a/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyIndexHalEnricher.java b/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyIndexHalEnricher.java new file mode 100644 index 0000000000..0914f92e25 --- /dev/null +++ b/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyIndexHalEnricher.java @@ -0,0 +1,38 @@ +package sonia.scm.legacy; + +import com.google.inject.Inject; +import sonia.scm.api.v2.resources.Enrich; +import sonia.scm.api.v2.resources.HalAppender; +import sonia.scm.api.v2.resources.HalEnricher; +import sonia.scm.api.v2.resources.HalEnricherContext; +import sonia.scm.api.v2.resources.Index; +import sonia.scm.api.v2.resources.LinkBuilder; +import sonia.scm.api.v2.resources.ScmPathInfoStore; +import sonia.scm.plugin.Extension; + +import javax.inject.Provider; + +@Extension +@Enrich(Index.class) +public class LegacyIndexHalEnricher implements HalEnricher { + + private Provider scmPathInfoStoreProvider; + + @Inject + public LegacyIndexHalEnricher(Provider scmPathInfoStoreProvider) { + this.scmPathInfoStoreProvider = scmPathInfoStoreProvider; + } + + private String createLink() { + return new LinkBuilder(scmPathInfoStoreProvider.get().get(), LegacyRepositoryService.class) + .method("getNameAndNamespaceForRepositoryId") + .parameters("REPOID") + .href() + .replace("REPOID", "{id}"); + } + + @Override + public void enrich(HalEnricherContext context, HalAppender appender) { + appender.appendLink("nameAndNamespace", createLink()); + } +} diff --git a/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyModule.java b/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyModule.java new file mode 100644 index 0000000000..e8ec437248 --- /dev/null +++ b/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyModule.java @@ -0,0 +1,13 @@ +package sonia.scm.legacy; + +import com.google.inject.servlet.ServletModule; +import sonia.scm.plugin.Extension; + +@Extension +public class LegacyModule extends ServletModule { + + @Override + protected void configureServlets() { + filter("/*").through(RepositoryLegacyProtocolRedirectFilter.class); + } +} diff --git a/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyProtocolServletAuthenticationFilter.java b/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyProtocolServletAuthenticationFilter.java new file mode 100644 index 0000000000..ca6dea0898 --- /dev/null +++ b/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyProtocolServletAuthenticationFilter.java @@ -0,0 +1,22 @@ +package sonia.scm.legacy; + +import sonia.scm.Priority; +import sonia.scm.config.ScmConfiguration; +import sonia.scm.filter.Filters; +import sonia.scm.filter.WebElement; +import sonia.scm.web.UserAgentParser; +import sonia.scm.web.WebTokenGenerator; +import sonia.scm.web.filter.HttpProtocolServletAuthenticationFilterBase; + +import javax.inject.Inject; +import java.util.Set; + +@Priority(Filters.PRIORITY_AUTHENTICATION) +@WebElement(value = "/git/*", morePatterns = {"/hg/*", "/svn/*"}) +public class LegacyProtocolServletAuthenticationFilter extends HttpProtocolServletAuthenticationFilterBase { + + @Inject + public LegacyProtocolServletAuthenticationFilter(ScmConfiguration configuration, Set tokenGenerators, UserAgentParser userAgentParser) { + super(configuration, tokenGenerators, userAgentParser); + } +} diff --git a/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyRepositoryService.java b/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyRepositoryService.java new file mode 100644 index 0000000000..d6b923a927 --- /dev/null +++ b/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyRepositoryService.java @@ -0,0 +1,43 @@ +package sonia.scm.legacy; + +import com.google.inject.Inject; +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; +import sonia.scm.NotFoundException; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryManager; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +@Path("v2/legacy/repository") +public class LegacyRepositoryService { + + private RepositoryManager repositoryManager; + + @Inject + public LegacyRepositoryService(RepositoryManager repositoryManager) { + this.repositoryManager = repositoryManager; + } + + @GET + @Path("{id}") + @Produces(MediaType.APPLICATION_JSON) + @StatusCodes({ + @ResponseCode(code = 200, condition = "success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"repository:read:global\" privilege"), + @ResponseCode(code = 500, condition = "internal server error") + }) + public NamespaceAndNameDto getNameAndNamespaceForRepositoryId(@PathParam("id") String repositoryId) { + Repository repo = repositoryManager.get(repositoryId); + if (repo == null) { + throw new NotFoundException(Repository.class, repositoryId); + } + return new NamespaceAndNameDto(repo.getName(), repo.getNamespace()); + } +} + diff --git a/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/NamespaceAndNameDto.java b/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/NamespaceAndNameDto.java new file mode 100644 index 0000000000..2f7aae2dae --- /dev/null +++ b/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/NamespaceAndNameDto.java @@ -0,0 +1,11 @@ +package sonia.scm.legacy; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public class NamespaceAndNameDto { + private String name; + private String namespace; +} diff --git a/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/RepositoryLegacyProtocolRedirectFilter.java b/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/RepositoryLegacyProtocolRedirectFilter.java new file mode 100644 index 0000000000..6319af04f6 --- /dev/null +++ b/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/RepositoryLegacyProtocolRedirectFilter.java @@ -0,0 +1,180 @@ +package sonia.scm.legacy; + +import sonia.scm.Priority; +import sonia.scm.filter.Filters; +import sonia.scm.migration.MigrationDAO; +import sonia.scm.migration.MigrationInfo; +import sonia.scm.repository.RepositoryDAO; +import sonia.scm.web.filter.HttpFilter; + +import javax.inject.Inject; +import javax.inject.Singleton; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static java.util.Arrays.asList; +import static java.util.Optional.empty; +import static java.util.Optional.of; +import static org.apache.commons.lang.StringUtils.isEmpty; + +@Priority(Filters.PRIORITY_BASEURL) +@Singleton +public class RepositoryLegacyProtocolRedirectFilter extends HttpFilter { + + private final ProtocolBasedLegacyRepositoryInfo info; + private final RepositoryDAO repositoryDao; + + @Inject + public RepositoryLegacyProtocolRedirectFilter(MigrationDAO migrationDAO, RepositoryDAO repositoryDao) { + this.info = load(migrationDAO); + this.repositoryDao = repositoryDao; + } + + private static ProtocolBasedLegacyRepositoryInfo load(MigrationDAO migrationDAO) { + return new ProtocolBasedLegacyRepositoryInfo(migrationDAO.getAll()); + } + + @Override + protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { + new Worker(request, response, chain).doFilter(); + } + + private class Worker { + private final HttpServletRequest request; + private final HttpServletResponse response; + private final FilterChain chain; + + private Worker(HttpServletRequest request, HttpServletResponse response, FilterChain chain) { + this.request = request; + this.response = response; + this.chain = chain; + } + + void doFilter() throws IOException, ServletException { + String servletPath = getServletPathWithoutLeadingSlash(); + String[] pathElements = servletPath.split("/"); + if (pathElements.length > 0) { + checkPathElements(servletPath, pathElements); + } else { + noRedirect(); + } + } + + private void checkPathElements(String servletPath, String[] pathElements) throws IOException, ServletException { + Optional migrationInfo = info.findRepository(asList(pathElements)); + if (migrationInfo.isPresent()) { + doRedirect(servletPath, migrationInfo.get()); + } else { + noRedirect(); + } + } + + private void doRedirect(String servletPath, MigrationInfo migrationInfo) throws IOException, ServletException { + if (repositoryDao.get(migrationInfo.getId()) == null) { + noRedirect(); + } else { + String furtherPath = servletPath.substring(migrationInfo.getProtocol().length() + 1 + migrationInfo.getOriginalRepositoryName().length()); + String queryString = request.getQueryString(); + if (isEmpty(queryString)) { + redirectWithoutQueryParameters(migrationInfo, furtherPath); + } else { + redirectWithQueryParameters(migrationInfo, furtherPath, queryString); + } + } + } + + private void redirectWithoutQueryParameters(MigrationInfo migrationInfo, String furtherPath) throws IOException { + response.sendRedirect(String.format("%s/repo/%s/%s%s", request.getContextPath(), migrationInfo.getNamespace(), migrationInfo.getName(), furtherPath)); + } + + private void redirectWithQueryParameters(MigrationInfo migrationInfo, String furtherPath, String queryString) throws IOException { + response.sendRedirect(String.format("%s/repo/%s/%s%s?%s", request.getContextPath(), migrationInfo.getNamespace(), migrationInfo.getName(), furtherPath, queryString)); + } + + private void noRedirect() throws IOException, ServletException { + chain.doFilter(request, response); + } + + private String getServletPathWithoutLeadingSlash() { + String servletPath = request.getServletPath(); + if (servletPath.startsWith("/")) { + return servletPath.substring(1); + } else { + return servletPath; + } + } + } + + private static class ProtocolBasedLegacyRepositoryInfo { + + private final Map infosForProtocol = new HashMap<>(); + + ProtocolBasedLegacyRepositoryInfo(Collection all) { + all.forEach(this::add); + } + + private void add(MigrationInfo migrationInfo) { + String protocol = migrationInfo.getProtocol(); + LegacyRepositoryInfoCollection legacyRepositoryInfoCollection = infosForProtocol.computeIfAbsent(protocol, x -> new LegacyRepositoryInfoCollection()); + legacyRepositoryInfoCollection.add(migrationInfo); + } + + private Optional findRepository(List pathElements) { + String protocol = pathElements.get(0); + if (!isProtocol(protocol)) { + return empty(); + } + return infosForProtocol.get(protocol).findRepository(removeFirstElement(pathElements)); + } + + boolean isProtocol(String protocol) { + return infosForProtocol.containsKey(protocol); + } + } + + private static class LegacyRepositoryInfoCollection { + + private final Map repositories = new HashMap<>(); + private final Map next = new HashMap<>(); + + Optional findRepository(List pathElements) { + String firstPathElement = pathElements.get(0); + if (repositories.containsKey(firstPathElement)) { + return of(repositories.get(firstPathElement)); + } else if (next.containsKey(firstPathElement)) { + return next.get(firstPathElement).findRepository(removeFirstElement(pathElements)); + } else { + return empty(); + } + } + + private void add(MigrationInfo migrationInfo) { + String originalRepositoryName = migrationInfo.getOriginalRepositoryName(); + List originalRepositoryNameParts = asList(originalRepositoryName.split("/")); + add(migrationInfo, originalRepositoryNameParts); + } + + private void add(MigrationInfo migrationInfo, List originalRepositoryNameParts) { + if (originalRepositoryNameParts.isEmpty()) { + throw new IllegalArgumentException("cannot handle empty name"); + } else if (originalRepositoryNameParts.size() == 1) { + repositories.put(originalRepositoryNameParts.get(0), migrationInfo); + } else { + LegacyRepositoryInfoCollection subCollection = next.computeIfAbsent(originalRepositoryNameParts.get(0), x -> new LegacyRepositoryInfoCollection()); + subCollection.add(migrationInfo, removeFirstElement(originalRepositoryNameParts)); + } + } + } + + private static List removeFirstElement(List originalRepositoryNameParts) { + return originalRepositoryNameParts.subList(1, originalRepositoryNameParts.size()); + } +} diff --git a/scm-plugins/scm-legacy-plugin/src/main/js/DummyComponent.js b/scm-plugins/scm-legacy-plugin/src/main/js/DummyComponent.js new file mode 100644 index 0000000000..6728a93c78 --- /dev/null +++ b/scm-plugins/scm-legacy-plugin/src/main/js/DummyComponent.js @@ -0,0 +1,14 @@ +//@flow +import React from "react"; +import {withRouter} from "react-router-dom"; + +class DummyComponent extends React.Component { + render() { + return ( + <> + + ); + } +} + +export default withRouter(DummyComponent); diff --git a/scm-plugins/scm-legacy-plugin/src/main/js/index.js b/scm-plugins/scm-legacy-plugin/src/main/js/index.js new file mode 100644 index 0000000000..7be0386359 --- /dev/null +++ b/scm-plugins/scm-legacy-plugin/src/main/js/index.js @@ -0,0 +1,90 @@ +// @flow + +import React from "react"; +import {withRouter} from "react-router-dom"; +import {binder} from "@scm-manager/ui-extensions"; +import {apiClient, ErrorBoundary, ErrorNotification, ProtectedRoute} from "@scm-manager/ui-components"; +import DummyComponent from "./DummyComponent"; +import type {Links} from "@scm-manager/ui-types"; + +type Props = { + authenticated?: boolean, + links: Links, + + //context objects + history: History +}; + +type State = { + error?: Error +}; + +class LegacyRepositoryRedirect extends React.Component { + constructor(props: Props, state: State) { + super(props, state); + this.state = { error: null }; + } + + handleError = (error: Error) => { + this.setState({ + error + }); + }; + + redirectLegacyRepository() { + const { history, links } = this.props; + if (location.href && location.href.includes("#diffPanel;")) { + let splittedUrl = location.href.split(";"); + let repoId = splittedUrl[1]; + let changeSetId = splittedUrl[2]; + + apiClient + .get(links.nameAndNamespace.href.replace("{id}", repoId)) + .then(response => response.json()) + .then(payload => + history.push( + "/repo/" + + payload.namespace + + "/" + + payload.name + + "/changeset/" + + changeSetId + ) + ) + .catch(this.handleError); + } + } + + render() { + const { authenticated } = this.props; + const { error } = this.state; + + if (error) { + return ( +

+
+ + + +
+
+ ); + } + + return ( + <> + {authenticated ? ( + this.redirectLegacyRepository() + ) : ( + + )} + + ); + } +} + +binder.bind("main.route", withRouter(LegacyRepositoryRedirect)); diff --git a/scm-plugins/scm-legacy-plugin/src/test/java/sonia/scm/legacy/LegacyRepositoryServiceTest.java b/scm-plugins/scm-legacy-plugin/src/test/java/sonia/scm/legacy/LegacyRepositoryServiceTest.java new file mode 100644 index 0000000000..a28f87dbf8 --- /dev/null +++ b/scm-plugins/scm-legacy-plugin/src/test/java/sonia/scm/legacy/LegacyRepositoryServiceTest.java @@ -0,0 +1,43 @@ +package sonia.scm.legacy; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import sonia.scm.NotFoundException; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryManager; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class LegacyRepositoryServiceTest { + + @Mock + private RepositoryManager repositoryManager; + + private LegacyRepositoryService legacyRepositoryService; + private final Repository repository = new Repository("abc123", "git", "space", "repo"); + + @Before + public void init() { + legacyRepositoryService = new LegacyRepositoryService(repositoryManager); + } + + @Test + public void findRepositoryNameSpaceAndNameForRepositoryId() { + when(repositoryManager.get(any(String.class))).thenReturn(repository); + NamespaceAndNameDto namespaceAndName = legacyRepositoryService.getNameAndNamespaceForRepositoryId("abc123"); + assertThat(namespaceAndName.getName()).isEqualToIgnoringCase("repo"); + assertThat(namespaceAndName.getNamespace()).isEqualToIgnoringCase("space"); + } + + @Test(expected = NotFoundException.class) + public void shouldGetNotFoundExceptionIfRepositoryNotExists() throws NotFoundException { + when(repositoryManager.get(any(String.class))).thenReturn(null); + legacyRepositoryService.getNameAndNamespaceForRepositoryId("456def"); + } +} diff --git a/scm-plugins/scm-legacy-plugin/src/test/java/sonia/scm/legacy/RepositoryLegacyProtocolRedirectFilterTest.java b/scm-plugins/scm-legacy-plugin/src/test/java/sonia/scm/legacy/RepositoryLegacyProtocolRedirectFilterTest.java new file mode 100644 index 0000000000..bd70cb9dfc --- /dev/null +++ b/scm-plugins/scm-legacy-plugin/src/test/java/sonia/scm/legacy/RepositoryLegacyProtocolRedirectFilterTest.java @@ -0,0 +1,103 @@ +package sonia.scm.legacy; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.migration.MigrationDAO; +import sonia.scm.migration.MigrationInfo; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryDAO; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +import static java.util.Collections.singletonList; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class RepositoryLegacyProtocolRedirectFilterTest { + + @Mock + MigrationDAO migrationDAO; + @Mock + RepositoryDAO repositoryDao; + @Mock + HttpServletRequest request; + @Mock + HttpServletResponse response; + @Mock + FilterChain filterChain; + + @BeforeEach + void initRequest() { + lenient().when(request.getContextPath()).thenReturn("/scm"); + lenient().when(request.getQueryString()).thenReturn(""); + } + + @Test + void shouldNotRedirectForEmptyMigrationList() throws IOException, ServletException { + when(request.getServletPath()).thenReturn("/git/old/name"); + + new RepositoryLegacyProtocolRedirectFilter(migrationDAO, repositoryDao).doFilter(request, response, filterChain); + + verify(filterChain).doFilter(request, response); + } + + @Test + void shouldRedirectForExistingRepository() throws IOException, ServletException { + when(migrationDAO.getAll()).thenReturn(singletonList(new MigrationInfo("id", "git", "old/name", "namespace", "name"))); + when(repositoryDao.get("id")).thenReturn(new Repository()); + when(request.getServletPath()).thenReturn("/git/old/name"); + + new RepositoryLegacyProtocolRedirectFilter(migrationDAO, repositoryDao).doFilter(request, response, filterChain); + + verify(response).sendRedirect("/scm/repo/namespace/name"); + verify(filterChain, never()).doFilter(request, response); + } + + @Test + void shouldRedirectForExistingRepositoryWithFurtherPathElements() throws IOException, ServletException { + when(migrationDAO.getAll()).thenReturn(singletonList(new MigrationInfo("id", "git", "old/name", "namespace", "name"))); + when(repositoryDao.get("id")).thenReturn(new Repository()); + when(request.getServletPath()).thenReturn("/git/old/name/info/refs"); + + new RepositoryLegacyProtocolRedirectFilter(migrationDAO, repositoryDao).doFilter(request, response, filterChain); + + verify(response).sendRedirect("/scm/repo/namespace/name/info/refs"); + verify(filterChain, never()).doFilter(request, response); + } + + @Test + void shouldRedirectWithQueryParameters() throws IOException, ServletException { + when(migrationDAO.getAll()).thenReturn(singletonList(new MigrationInfo("id", "git", "old/name", "namespace", "name"))); + when(repositoryDao.get("id")).thenReturn(new Repository()); + when(request.getServletPath()).thenReturn("/git/old/name/info/refs"); + when(request.getQueryString()).thenReturn("parameter=value"); + + new RepositoryLegacyProtocolRedirectFilter(migrationDAO, repositoryDao).doFilter(request, response, filterChain); + + verify(response).sendRedirect("/scm/repo/namespace/name/info/refs?parameter=value"); + verify(filterChain, never()).doFilter(request, response); + } + + @Test + void shouldNotRedirectWhenRepositoryHasBeenDeleted() throws IOException, ServletException { + when(migrationDAO.getAll()).thenReturn(singletonList(new MigrationInfo("id", "git", "old/name", "namespace", "name"))); + when(repositoryDao.get("id")).thenReturn(null); + when(request.getServletPath()).thenReturn("/git/old/name"); + + new RepositoryLegacyProtocolRedirectFilter(migrationDAO, repositoryDao).doFilter(request, response, filterChain); + + verify(response, never()).sendRedirect(any()); + verify(filterChain).doFilter(request, response); + } +} diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnConfigHelper.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnConfigHelper.java new file mode 100644 index 0000000000..ab01393b29 --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnConfigHelper.java @@ -0,0 +1,33 @@ +package sonia.scm.repository; + +import sonia.scm.ContextEntry; +import sonia.scm.io.INIConfiguration; +import sonia.scm.io.INIConfigurationReader; +import sonia.scm.io.INIConfigurationWriter; +import sonia.scm.io.INISection; + +import java.io.File; +import java.io.IOException; + +class SvnConfigHelper { + + private static final String CONFIG_FILE_NAME = "scm-manager.conf"; + private static final String CONFIG_SECTION_SCMM = "scmm"; + private static final String CONFIG_KEY_REPOSITORY_ID = "repositoryid"; + + void writeRepositoryId(Repository repository, File directory) throws IOException { + INISection iniSection = new INISection(CONFIG_SECTION_SCMM); + iniSection.setParameter(CONFIG_KEY_REPOSITORY_ID, repository.getId()); + INIConfiguration iniConfiguration = new INIConfiguration(); + iniConfiguration.addSection(iniSection); + new INIConfigurationWriter().write(iniConfiguration, new File(directory, CONFIG_FILE_NAME)); + } + + String getRepositoryId(File directory) { + try { + return new INIConfigurationReader().read(new File(directory, CONFIG_FILE_NAME)).getSection(CONFIG_SECTION_SCMM).getParameter(CONFIG_KEY_REPOSITORY_ID); + } catch (IOException e) { + throw new InternalRepositoryException(ContextEntry.ContextBuilder.entity("Directory", directory.toString()), "could not read scm configuration file", e); + } + } +} diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnRepositoryHandler.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnRepositoryHandler.java index c32310e7e7..f3be47e0c2 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnRepositoryHandler.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnRepositoryHandler.java @@ -46,11 +46,6 @@ import org.tmatesoft.svn.core.internal.io.fs.FSRepositoryFactory; import org.tmatesoft.svn.core.io.SVNRepository; import org.tmatesoft.svn.core.io.SVNRepositoryFactory; import org.tmatesoft.svn.util.SVNDebugLog; -import sonia.scm.ContextEntry; -import sonia.scm.io.INIConfiguration; -import sonia.scm.io.INIConfigurationReader; -import sonia.scm.io.INIConfigurationWriter; -import sonia.scm.io.INISection; import sonia.scm.logging.SVNKitLogger; import sonia.scm.plugin.Extension; import sonia.scm.plugin.PluginLoader; @@ -87,9 +82,6 @@ public class SvnRepositoryHandler public static final RepositoryType TYPE = new RepositoryType(TYPE_NAME, TYPE_DISPLAYNAME, SvnRepositoryServiceProvider.COMMANDS); - private static final String CONFIG_FILE_NAME = "scm-manager.conf"; - private static final String CONFIG_SECTION_SCMM = "scmm"; - private static final String CONFIG_KEY_REPOSITORY_ID = "repositoryid"; private static final Logger logger = LoggerFactory.getLogger(SvnRepositoryHandler.class); @@ -223,18 +215,10 @@ public class SvnRepositoryHandler @Override protected void postCreate(Repository repository, File directory) throws IOException { - INISection iniSection = new INISection(CONFIG_SECTION_SCMM); - iniSection.setParameter(CONFIG_KEY_REPOSITORY_ID, repository.getId()); - INIConfiguration iniConfiguration = new INIConfiguration(); - iniConfiguration.addSection(iniSection); - new INIConfigurationWriter().write(iniConfiguration, new File(directory, CONFIG_FILE_NAME)); + new SvnConfigHelper().writeRepositoryId(repository, directory); } String getRepositoryId(File directory) { - try { - return new INIConfigurationReader().read(new File(directory, CONFIG_FILE_NAME)).getSection(CONFIG_SECTION_SCMM).getParameter(CONFIG_KEY_REPOSITORY_ID); - } catch (IOException e) { - throw new InternalRepositoryException(ContextEntry.ContextBuilder.entity("Directory", directory.toString()), "could not read scm configuration file", e); - } + return new SvnConfigHelper().getRepositoryId(directory); } } diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnV2UpdateStep.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnV2UpdateStep.java new file mode 100644 index 0000000000..2423040f60 --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnV2UpdateStep.java @@ -0,0 +1,56 @@ +package sonia.scm.repository; + +import sonia.scm.migration.UpdateException; +import sonia.scm.migration.UpdateStep; +import sonia.scm.plugin.Extension; +import sonia.scm.update.UpdateStepRepositoryMetadataAccess; +import sonia.scm.version.Version; + +import javax.inject.Inject; +import java.io.IOException; +import java.nio.file.Path; + +import static sonia.scm.version.Version.parse; + +@Extension +public class SvnV2UpdateStep implements UpdateStep { + + private final RepositoryLocationResolver locationResolver; + private final UpdateStepRepositoryMetadataAccess repositoryMetadataAccess; + + @Inject + public SvnV2UpdateStep(RepositoryLocationResolver locationResolver, UpdateStepRepositoryMetadataAccess repositoryMetadataAccess) { + this.locationResolver = locationResolver; + this.repositoryMetadataAccess = repositoryMetadataAccess; + } + + @Override + public void doUpdate() { + locationResolver.forClass(Path.class).forAllLocations( + (repositoryId, path) -> { + Repository repository = repositoryMetadataAccess.read(path); + if (isSvnDirectory(repository)) { + try { + new SvnConfigHelper().writeRepositoryId(repository, path.resolve(RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY).toFile()); + } catch (IOException e) { + throw new UpdateException("could not update repository with id " + repositoryId + " in path " + path, e); + } + } + } + ); + } + + private boolean isSvnDirectory(Repository repository) { + return SvnRepositoryHandler.TYPE_NAME.equals(repository.getType()); + } + + @Override + public Version getTargetVersion() { + return parse("2.0.0"); + } + + @Override + public String getAffectedDataType() { + return "sonia.scm.plugin.svn"; + } +} diff --git a/scm-test/src/main/java/sonia/scm/TempDirRepositoryLocationResolver.java b/scm-test/src/main/java/sonia/scm/TempDirRepositoryLocationResolver.java index acffe6c769..e172b53ba2 100644 --- a/scm-test/src/main/java/sonia/scm/TempDirRepositoryLocationResolver.java +++ b/scm-test/src/main/java/sonia/scm/TempDirRepositoryLocationResolver.java @@ -4,6 +4,7 @@ import sonia.scm.repository.BasicRepositoryLocationResolver; import java.io.File; import java.nio.file.Path; +import java.util.function.BiConsumer; public class TempDirRepositoryLocationResolver extends BasicRepositoryLocationResolver { private final File tempDirectory; @@ -30,6 +31,11 @@ public class TempDirRepositoryLocationResolver extends BasicRepositoryLocationRe public void setLocation(String repositoryId, T location) { throw new UnsupportedOperationException("not implemented for tests"); } + + @Override + public void forAllLocations(BiConsumer consumer) { + consumer.accept("id", (T) tempDirectory.toPath()); + } }; } } diff --git a/scm-ui-components/packages/ui-components/src/Autocomplete.js b/scm-ui-components/packages/ui-components/src/Autocomplete.js index adf86e37b7..116df6562a 100644 --- a/scm-ui-components/packages/ui-components/src/Autocomplete.js +++ b/scm-ui-components/packages/ui-components/src/Autocomplete.js @@ -1,10 +1,9 @@ // @flow import React from "react"; -import { AsyncCreatable, Async } from "react-select"; -import type { AutocompleteObject, SelectValue } from "@scm-manager/ui-types"; +import {Async, AsyncCreatable} from "react-select"; +import type {AutocompleteObject, SelectValue} from "@scm-manager/ui-types"; import LabelWithHelpIcon from "./forms/LabelWithHelpIcon"; - type Props = { loadSuggestions: string => Promise, valueSelected: SelectValue => void, @@ -17,12 +16,9 @@ type Props = { creatable?: boolean }; - type State = {}; class Autocomplete extends React.Component { - - static defaultProps = { placeholder: "Type here", loadingMessage: "Loading...", @@ -34,7 +30,11 @@ class Autocomplete extends React.Component { }; // We overwrite this to avoid running into a bug (https://github.com/JedWatson/react-select/issues/2944) - isValidNewOption = (inputValue: string, selectValue: SelectValue, selectOptions: SelectValue[]) => { + isValidNewOption = ( + inputValue: string, + selectValue: SelectValue, + selectOptions: SelectValue[] + ) => { const isNotDuplicated = !selectOptions .map(option => option.label) .includes(inputValue); @@ -43,12 +43,21 @@ class Autocomplete extends React.Component { }; render() { - const { label, helpText, value, placeholder, loadingMessage, noOptionsMessage, loadSuggestions, creatable } = this.props; + const { + label, + helpText, + value, + placeholder, + loadingMessage, + noOptionsMessage, + loadSuggestions, + creatable + } = this.props; return (
- {creatable? + {creatable ? ( { }); }} /> - : + ) : ( { loadingMessage={() => loadingMessage} noOptionsMessage={() => noOptionsMessage} /> - - } + )}
); } } - export default Autocomplete; diff --git a/scm-ui-components/packages/ui-components/src/ErrorNotification.js b/scm-ui-components/packages/ui-components/src/ErrorNotification.js index b8acf89733..0c6367a474 100644 --- a/scm-ui-components/packages/ui-components/src/ErrorNotification.js +++ b/scm-ui-components/packages/ui-components/src/ErrorNotification.js @@ -1,7 +1,7 @@ //@flow import React from "react"; -import { translate } from "react-i18next"; -import { BackendError, ForbiddenError, UnauthorizedError } from "./errors"; +import {translate} from "react-i18next"; +import {BackendError, ForbiddenError, UnauthorizedError} from "./errors"; import Notification from "./Notification"; import BackendErrorNotification from "./BackendErrorNotification"; @@ -10,35 +10,33 @@ type Props = { error?: Error }; - class ErrorNotification extends React.Component { render() { const { t, error } = this.props; if (error) { if (error instanceof BackendError) { - return + return ; } else if (error instanceof UnauthorizedError) { return ( - {t("error-notification.prefix")}:{" "} - {t("error-notification.timeout")}{" "} + {t("errorNotification.prefix")}:{" "} + {t("errorNotification.timeout")}{" "} - {t("error-notification.loginLink")} + {t("errorNotification.loginLink")} ); } else if (error instanceof ForbiddenError) { return ( - {t("error-notification.prefix")}:{" "} - {t("error-notification.forbidden")} + {t("errorNotification.prefix")}:{" "} + {t("errorNotification.forbidden")} - ) - } else - { + ); + } else { return ( - {t("error-notification.prefix")}: {error.message} + {t("errorNotification.prefix")}: {error.message} ); } @@ -47,4 +45,4 @@ class ErrorNotification extends React.Component { } } -export default translate("commons")(ErrorNotification); +export default translate("commons")(ErrorNotification); diff --git a/scm-ui-components/packages/ui-components/src/GroupAutocomplete.js b/scm-ui-components/packages/ui-components/src/GroupAutocomplete.js new file mode 100644 index 0000000000..8aeeedc3e4 --- /dev/null +++ b/scm-ui-components/packages/ui-components/src/GroupAutocomplete.js @@ -0,0 +1,27 @@ +// @flow +import React from "react"; +import {translate} from "react-i18next"; +import type AutocompleteProps from "./UserGroupAutocomplete"; +import UserGroupAutocomplete from "./UserGroupAutocomplete"; + +type Props = AutocompleteProps & { + // Context props + t: string => string +}; + +class GroupAutocomplete extends React.Component { + render() { + const { t } = this.props; + return ( + + ); + } +} + +export default translate("commons")(GroupAutocomplete); diff --git a/scm-ui-components/packages/ui-components/src/Notification.js b/scm-ui-components/packages/ui-components/src/Notification.js index b48ae16851..46b6b8edcf 100644 --- a/scm-ui-components/packages/ui-components/src/Notification.js +++ b/scm-ui-components/packages/ui-components/src/Notification.js @@ -2,11 +2,18 @@ import * as React from "react"; import classNames from "classnames"; -type NotificationType = "primary" | "info" | "success" | "warning" | "danger"; +type NotificationType = + | "primary" + | "info" + | "success" + | "warning" + | "danger" + | "inherit"; type Props = { type: NotificationType, onClose?: () => void, + className?: string, children?: React.Node }; @@ -24,9 +31,12 @@ class Notification extends React.Component { } render() { - const { type, children } = this.props; + const { type, className, children } = this.props; + + const color = type !== "inherit" ? "is-" + type : ""; + return ( -
+
{this.renderCloseButton()} {children}
diff --git a/scm-ui-components/packages/ui-components/src/UserAutocomplete.js b/scm-ui-components/packages/ui-components/src/UserAutocomplete.js new file mode 100644 index 0000000000..308835d8db --- /dev/null +++ b/scm-ui-components/packages/ui-components/src/UserAutocomplete.js @@ -0,0 +1,27 @@ +// @flow +import React from "react"; +import {translate} from "react-i18next"; +import type AutocompleteProps from "./UserGroupAutocomplete"; +import UserGroupAutocomplete from "./UserGroupAutocomplete"; + +type Props = AutocompleteProps & { + // Context props + t: string => string +}; + +class UserAutocomplete extends React.Component { + render() { + const { t } = this.props; + return ( + + ); + } +} + +export default translate("commons")(UserAutocomplete); diff --git a/scm-ui-components/packages/ui-components/src/UserGroupAutocomplete.js b/scm-ui-components/packages/ui-components/src/UserGroupAutocomplete.js new file mode 100644 index 0000000000..d038e21221 --- /dev/null +++ b/scm-ui-components/packages/ui-components/src/UserGroupAutocomplete.js @@ -0,0 +1,52 @@ +// @flow +import React from "react"; +import type {SelectValue} from "@scm-manager/ui-types"; +import Autocomplete from "./Autocomplete"; + +export type AutocompleteProps = { + autocompleteLink: string, + valueSelected: SelectValue => void, + value?: SelectValue +}; + +type Props = AutocompleteProps & { + label: string, + noOptionsMessage: string, + loadingMessage: string, + placeholder: string +}; + +export default class UserGroupAutocomplete extends React.Component { + loadSuggestions = (inputValue: string) => { + const url = this.props.autocompleteLink; + const link = url + "?q="; + return fetch(link + inputValue) + .then(response => response.json()) + .then(json => { + return json.map(element => { + const label = element.displayName + ? `${element.displayName} (${element.id})` + : element.id; + return { + value: element, + label + }; + }); + }); + }; + + selectName = (selection: SelectValue) => { + this.props.valueSelected(selection); + }; + + render() { + return ( + + ); + } +} diff --git a/scm-ui-components/packages/ui-components/src/index.js b/scm-ui-components/packages/ui-components/src/index.js index f6d83d6d08..91cfe7ef72 100644 --- a/scm-ui-components/packages/ui-components/src/index.js +++ b/scm-ui-components/packages/ui-components/src/index.js @@ -26,6 +26,8 @@ export { default as Tooltip } from "./Tooltip"; // TODO do we need this? getPageFromMatch is already exported by urls export { getPageFromMatch } from "./urls"; export { default as Autocomplete} from "./Autocomplete"; +export { default as GroupAutocomplete} from "./GroupAutocomplete"; +export { default as UserAutocomplete} from "./UserAutocomplete"; export { default as BranchSelector } from "./BranchSelector"; export { default as Breadcrumb } from "./Breadcrumb"; export { default as MarkdownView } from "./MarkdownView"; diff --git a/scm-ui-components/packages/ui-components/src/repos/Diff.js b/scm-ui-components/packages/ui-components/src/repos/Diff.js index 369e812344..7a5bcbf4a2 100644 --- a/scm-ui-components/packages/ui-components/src/repos/Diff.js +++ b/scm-ui-components/packages/ui-components/src/repos/Diff.js @@ -1,10 +1,10 @@ //@flow import React from "react"; import DiffFile from "./DiffFile"; -import type { DiffObjectProps } from "./DiffTypes"; +import type {DiffObjectProps, File} from "./DiffTypes"; type Props = DiffObjectProps & { - diff: any + diff: File[] }; class Diff extends React.Component { diff --git a/scm-ui-components/packages/ui-components/src/repos/DiffFile.js b/scm-ui-components/packages/ui-components/src/repos/DiffFile.js index 3d2dba6c8b..8847cf4280 100644 --- a/scm-ui-components/packages/ui-components/src/repos/DiffFile.js +++ b/scm-ui-components/packages/ui-components/src/repos/DiffFile.js @@ -1,17 +1,10 @@ //@flow import React from "react"; -import { - Hunk, - Diff as DiffComponent, - getChangeKey, - Change, - DiffObjectProps, - File -} from "react-diff-view"; +import {Change, Diff as DiffComponent, DiffObjectProps, File, getChangeKey, Hunk} from "react-diff-view"; import injectSheets from "react-jss"; import classNames from "classnames"; -import { translate } from "react-i18next"; -import { ButtonGroup, Button } from "../buttons"; +import {translate} from "react-i18next"; +import {Button, ButtonGroup} from "../buttons"; const styles = { panel: { @@ -178,12 +171,20 @@ class DiffFile extends React.Component { if (key === value) { value = file.type; } + const color = + value === "added" + ? "is-success" + : value === "deleted" + ? "is-danger" + : "is-info"; + return ( diff --git a/scm-ui-components/packages/ui-components/src/repos/DiffTypes.js b/scm-ui-components/packages/ui-components/src/repos/DiffTypes.js index 74803e0a4e..dcd23bc9a8 100644 --- a/scm-ui-components/packages/ui-components/src/repos/DiffTypes.js +++ b/scm-ui-components/packages/ui-components/src/repos/DiffTypes.js @@ -27,12 +27,17 @@ export type Hunk = { content: string }; +export type ChangeType = "insert" | "delete" | "normal"; + export type Change = { content: string, - isNormal: boolean, - newLineNumber: number, - oldLineNumber: number, - type: string + isNormal?: boolean, + isInsert?: boolean, + isDelete?: boolean, + lineNumber?: number, + newLineNumber?: number, + oldLineNumber?: number, + type: ChangeType }; export type BaseContext = { diff --git a/scm-ui-components/packages/ui-components/src/repos/LoadingDiff.js b/scm-ui-components/packages/ui-components/src/repos/LoadingDiff.js index ce1a074b41..876b757f44 100644 --- a/scm-ui-components/packages/ui-components/src/repos/LoadingDiff.js +++ b/scm-ui-components/packages/ui-components/src/repos/LoadingDiff.js @@ -1,19 +1,19 @@ //@flow import React from "react"; -import { apiClient } from "../apiclient"; +import {apiClient} from "../apiclient"; import ErrorNotification from "../ErrorNotification"; import parser from "gitdiff-parser"; import Loading from "../Loading"; import Diff from "./Diff"; -import type {DiffObjectProps} from "./DiffTypes"; +import type {DiffObjectProps, File} from "./DiffTypes"; type Props = DiffObjectProps & { url: string }; type State = { - diff?: any, + diff?: File[], loading: boolean, error?: Error }; @@ -47,7 +47,8 @@ class LoadingDiff extends React.Component { .get(url) .then(response => response.text()) .then(parser.parse) - .then(diff => { + // $FlowFixMe + .then((diff: File[]) => { this.setState({ loading: false, diff: diff diff --git a/scm-ui-components/packages/ui-components/src/repos/changesets/ChangesetButtonGroup.js b/scm-ui-components/packages/ui-components/src/repos/changesets/ChangesetButtonGroup.js index 72ce9a2b38..74a4eb0050 100644 --- a/scm-ui-components/packages/ui-components/src/repos/changesets/ChangesetButtonGroup.js +++ b/scm-ui-components/packages/ui-components/src/repos/changesets/ChangesetButtonGroup.js @@ -1,9 +1,9 @@ //@flow import React from "react"; -import type { Changeset, Repository } from "@scm-manager/ui-types"; -import { ButtonAddons, Button } from "../../buttons"; -import { createChangesetLink, createSourcesLink } from "./changesets"; -import { translate } from "react-i18next"; +import type {Changeset, Repository} from "@scm-manager/ui-types"; +import {Button, ButtonAddons} from "../../buttons"; +import {createChangesetLink, createSourcesLink} from "./changesets"; +import {translate} from "react-i18next"; type Props = { repository: Repository, @@ -21,7 +21,7 @@ class ChangesetButtonGroup extends React.Component { const sourcesLink = createSourcesLink(repository, changeset); return ( - +
- +
+ +
+ +
+
diff --git a/scm-ui-components/packages/ui-components/src/repos/index.js b/scm-ui-components/packages/ui-components/src/repos/index.js index 473bbb3efc..19f9a160b3 100644 --- a/scm-ui-components/packages/ui-components/src/repos/index.js +++ b/scm-ui-components/packages/ui-components/src/repos/index.js @@ -1,5 +1,6 @@ // @flow import * as diffs from "./diffs"; + export { diffs }; export * from "./changesets"; @@ -12,6 +13,7 @@ export type { FileChangeType, Hunk, Change, + ChangeType, BaseContext, AnnotationFactory, AnnotationFactoryContext, diff --git a/scm-ui-components/packages/ui-types/src/Changesets.js b/scm-ui-components/packages/ui-types/src/Changesets.js index cab4233a7f..124fda3831 100644 --- a/scm-ui-components/packages/ui-types/src/Changesets.js +++ b/scm-ui-components/packages/ui-types/src/Changesets.js @@ -1,9 +1,9 @@ //@flow -import type {Links} from "./hal"; +import type {Collection, Links} from "./hal"; import type {Tag} from "./Tags"; import type {Branch} from "./Branches"; -export type Changeset = { +export type Changeset = Collection & { id: string, date: Date, author: { diff --git a/scm-ui/package.json b/scm-ui/package.json index dfb369e61e..a790935001 100644 --- a/scm-ui/package.json +++ b/scm-ui/package.json @@ -9,6 +9,7 @@ "@fortawesome/fontawesome-free": "^5.3.1", "@scm-manager/ui-extensions": "^0.1.2", "bulma": "^0.7.1", + "bulma-popover": "^1.0.0", "bulma-tooltip": "^2.0.2", "classnames": "^2.2.5", "font-awesome": "^4.7.0", diff --git a/scm-ui/public/images/iconCommunitySupport.png b/scm-ui/public/images/iconCommunitySupport.png new file mode 100644 index 0000000000..7a080903a0 Binary files /dev/null and b/scm-ui/public/images/iconCommunitySupport.png differ diff --git a/scm-ui/public/images/iconEnterpriseSupport.png b/scm-ui/public/images/iconEnterpriseSupport.png new file mode 100644 index 0000000000..0eadfaf85b Binary files /dev/null and b/scm-ui/public/images/iconEnterpriseSupport.png differ diff --git a/scm-ui/public/locales/de/admin.json b/scm-ui/public/locales/de/admin.json index 00793b6dce..54ae1ab91a 100644 --- a/scm-ui/public/locales/de/admin.json +++ b/scm-ui/public/locales/de/admin.json @@ -6,8 +6,18 @@ "settingsNavLink": "Einstellungen", "generalNavLink": "Generell" }, - "information": { - "currentAppVersion": "Aktuelle Software-Versionsnummer" + "info": { + "currentAppVersion": "Aktuelle Software-Versionsnummer", + "communityTitle": "Community Support", + "communityIconAlt": "Community Support Icon", + "communityInfo": "Das SCM-Manager Support-Team steht für allgemeine Fragen, die Meldung von Fehlern sowie Anfragen für Features gerne für Sie über die offiziellen Kanäle bereit.", + "communityButton": "Unser Team kontaktieren", + "enterpriseTitle": "Enterprise Support", + "enterpriseIconAlt": "Enterprise Support Icon", + "enterpriseInfo": "Sie benötigen Unterstützung bei der Integration von SCM-Manager in Ihre Prozesse, bei der Anpassung des Tools auf Ihre Anforderungen oder einfach ein Service Level Agreement (SLA)?", + "enterprisePartner": "Treten Sie mit unserem Entwicklungs-Partner Cloudogu in Kontakt! Das Team freut sich auf den Austausch über Ihre individuellen Anforderungen und erstellt Ihnen gerne ein maßgeschneidertes Angebot.", + "enterpriseLink": "https://cloudogu.com/de/scm-manager-enterprise/", + "enterpriseButton": "Enterprise Support anfragen" } }, "plugins": { @@ -44,13 +54,13 @@ "submit": "Speichern" }, "delete": { - "button": "Delete", - "subtitle": "Delete Permission Role", + "button": "Löschen", + "subtitle": "Berechtigungsrolle löschen", "confirmAlert": { - "title": "Delete Permission Role", - "message": "Do you really want to delete this permission role? All users who own this role will lose their permissions.", - "submit": "Yes", - "cancel": "No" + "title": "Berechtigungsrolle löschen?", + "message": "Wollen Sie diese Rolle wirklich löschen? Alle Benutzer mit dieser Rolle verlieren die entsprechenden Berechtigungen.", + "submit": "Ja", + "cancel": "Nein" } } } diff --git a/scm-ui/public/locales/de/commons.json b/scm-ui/public/locales/de/commons.json index 38042c4806..3964863f46 100644 --- a/scm-ui/public/locales/de/commons.json +++ b/scm-ui/public/locales/de/commons.json @@ -19,11 +19,11 @@ "subtitle": "Ein unbekannter Fehler ist aufgetreten." } }, - "error-notification": { + "errorNotification": { "prefix": "Fehler", "loginLink": "Erneute Anmeldung", "timeout": "Die Session ist abgelaufen.", - "wrong-login-credentials": "Ungültige Anmeldedaten", + "wrongLoginCredentials": "Ungültige Anmeldedaten", "forbidden": "Sie haben nicht die Berechtigung, diesen Datensatz zu sehen" }, "loading": { @@ -40,6 +40,15 @@ "admin": "Administration" }, "filterEntries": "Einträge filtern", + "autocomplete": { + "group": "Gruppe", + "user": "Benutzer", + "noGroupOptions": "Kein Gruppenname als Vorschlag verfügbar", + "groupPlaceholder": "Gruppe eingeben", + "noUserOptions": "Kein Benutzername als Vorschlag verfügbar", + "userPlaceholder": "Benutzer eingeben", + "loading": "suche..." + }, "paginator": { "next": "Weiter", "previous": "Zurück" diff --git a/scm-ui/public/locales/de/repos.json b/scm-ui/public/locales/de/repos.json index 4ba7a725e6..4f85ce0fde 100644 --- a/scm-ui/public/locales/de/repos.json +++ b/scm-ui/public/locales/de/repos.json @@ -11,7 +11,10 @@ "validation": { "namespace-invalid": "Der Namespace des Repository ist ungültig", "name-invalid": "Der Name des Repository ist ungültig", - "contact-invalid": "Der Kontakt muss eine gültige E-Mail Adresse sein" + "contact-invalid": "Der Kontakt muss eine gültige E-Mail Adresse sein", + "branch": { + "nameInvalid": "Der Name des Branches ist ungültig" + } }, "help": { "namespaceHelpText": "Der Namespace des Repository. Dieser wird Teil der URL des Repository sein.", @@ -150,13 +153,6 @@ "roleHelpText": "READ = read; WRITE = read und write; OWNER = read, write und auch die Möglichkeit Einstellungen und Berechtigungen zu verwalten. Wenn hier nichts angezeigt wird, den Erweitert-Button benutzen, um Details zu sehen.", "permissionsHelpText": "Hier können individuelle Berechtigungen unabhängig von vordefinierten Rollen vergeben werden." }, - "autocomplete": { - "no-group-options": "Kein Gruppenname als Vorschlag verfügbar", - "group-placeholder": "Gruppe eingeben", - "no-user-options": "Kein Benutzername als Vorschlag verfügbar", - "user-placeholder": "Benutzer eingeben", - "loading": "suche..." - }, "advanced": { "dialog": { "title": "Erweiterte Berechtigungen", diff --git a/scm-ui/public/locales/en/admin.json b/scm-ui/public/locales/en/admin.json index a5697ea4fe..2402f21423 100644 --- a/scm-ui/public/locales/en/admin.json +++ b/scm-ui/public/locales/en/admin.json @@ -6,8 +6,18 @@ "settingsNavLink": "Settings", "generalNavLink": "General" }, - "information": { - "currentAppVersion": "Current Application Version" + "info": { + "currentAppVersion": "Current Application Version", + "communityTitle": "Community Support", + "communityIconAlt": "Community Support Icon", + "communityInfo": "Contact the SCM-Manager support team for questions about SCM-Manager, to report bugs or to request features through the official channels.", + "communityButton": "Contact our team", + "enterpriseTitle": "Enterprise Support", + "enterpriseIconAlt": "Enterprise Support Icon", + "enterpriseInfo": "You require support with the integration of SCM-Manager into your processes, with the customization of the tool or simply a service level agreement (SLA)?", + "enterprisePartner": "Contact our development partner Cloudogu! Their team is looking forward to discussing your individual requirements with you and will be more than happy to give you a quote.", + "enterpriseLink": "https://cloudogu.com/en/scm-manager-enterprise/", + "enterpriseButton": "Request Enterprise Support" } }, "plugins": { diff --git a/scm-ui/public/locales/en/commons.json b/scm-ui/public/locales/en/commons.json index b41d2b341e..c57ef700ef 100644 --- a/scm-ui/public/locales/en/commons.json +++ b/scm-ui/public/locales/en/commons.json @@ -19,11 +19,11 @@ "subtitle": "Unknown error occurred" } }, - "error-notification": { + "errorNotification": { "prefix": "Error", "loginLink": "You can login here again.", "timeout": "The session has expired", - "wrong-login-credentials": "Invalid credentials", + "wrongLoginCredentials": "Invalid credentials", "forbidden": "You don't have permission to view this entity" }, "loading": { @@ -40,6 +40,15 @@ "admin": "Administration" }, "filterEntries": "filter entries", + "autocomplete": { + "group": "Group", + "user": "User", + "noGroupOptions": "No group suggestion available", + "groupPlaceholder": "Enter group", + "noUserOptions": "No user suggestion available", + "userPlaceholder": "Enter user", + "loading": "Loading..." + }, "paginator": { "next": "Next", "previous": "Previous" diff --git a/scm-ui/public/locales/en/repos.json b/scm-ui/public/locales/en/repos.json index 9a2e83f983..a312287a68 100644 --- a/scm-ui/public/locales/en/repos.json +++ b/scm-ui/public/locales/en/repos.json @@ -153,13 +153,6 @@ "roleHelpText": "READ = read; WRITE = read and write; OWNER = read, write and also the ability to manage the properties and permissions. If nothing is selected here, use the 'Advanced' Button to see detailed permissions.", "permissionsHelpText": "Use this to specify your own set of permissions regardless of predefined roles." }, - "autocomplete": { - "no-group-options": "No group suggestion available", - "group-placeholder": "Enter group", - "no-user-options": "No user suggestion available", - "user-placeholder": "Enter user", - "loading": "Loading..." - }, "advanced": { "dialog": { "title": "Advanced Permissions", diff --git a/scm-ui/src/admin/containers/AdminDetails.js b/scm-ui/src/admin/containers/AdminDetails.js index a7a135aa97..803524abed 100644 --- a/scm-ui/src/admin/containers/AdminDetails.js +++ b/scm-ui/src/admin/containers/AdminDetails.js @@ -1,9 +1,11 @@ // @flow import React from "react"; -import { connect } from "react-redux"; -import { translate } from "react-i18next"; -import { Loading, Title, Subtitle } from "@scm-manager/ui-components"; -import { getAppVersion } from "../../modules/indexResource"; +import {connect} from "react-redux"; +import injectSheet from "react-jss"; +import {translate} from "react-i18next"; +import classNames from "classnames"; +import {Image, Loading, Subtitle, Title} from "@scm-manager/ui-components"; +import {getAppVersion} from "../../modules/indexResource"; type Props = { loading: boolean, @@ -11,22 +13,71 @@ type Props = { version: string, - // context objects + // context props + classes: any, t: string => string }; +const styles = { + boxShadow: { + boxShadow: "0 2px 3px rgba(40, 177, 232, 0.1), 0 0 0 2px rgb(40, 177, 232, 0.2)" + }, + boxTitle: { + fontWeight: "500 !important" + }, + imagePadding: { + padding: "0.2rem 0.4rem" + } +}; + class AdminDetails extends React.Component { render() { - const { t, loading } = this.props; + const {loading, classes, t} = this.props; if (loading) { - return ; + return ; } return ( <> - - <Subtitle subtitle={this.props.version} /> + <Title title={t("admin.info.currentAppVersion")}/> + <Subtitle subtitle={this.props.version}/> + <div className={classNames("box", classes.boxShadow)}> + <article className="media"> + <div className={classNames("media-left", classes.imagePadding)}> + <Image + src="/images/iconCommunitySupport.png" + alt={t("admin.info.communityIconAlt")} + /> + </div> + <div className="media-content"> + <div className="content"> + <h3 className={classes.boxTitle}>{t("admin.info.communityTitle")}</h3> + <p>{t("admin.info.communityInfo")}</p> + <a className="button is-info is-pulled-right" target="_blank" + href="https://scm-manager.org/support/">{t("admin.info.communityButton")}</a> + </div> + </div> + </article> + </div> + <div className={classNames("box", classes.boxShadow)}> + <article className="media"> + <div className={classNames("media-left", classes.imagePadding)}> + <Image + src="/images/iconEnterpriseSupport.png" + alt={t("admin.info.enterpriseIconAlt")} + /> + </div> + <div className="media-content"> + <div className="content"> + <h3 className={classes.boxTitle}>{t("admin.info.enterpriseTitle")}</h3> + <p>{t("admin.info.enterpriseInfo")}<br/><strong>{t("admin.info.enterprisePartner")}</strong></p> + <a className="button is-info is-pulled-right is-normal" target="_blank" + href={t("admin.info.enterpriseLink")}>{t("admin.info.enterpriseButton")}</a> + </div> + </div> + </article> + </div> </> ); } @@ -39,4 +90,4 @@ const mapStateToProps = (state: any) => { }; }; -export default connect(mapStateToProps)(translate("admin")(AdminDetails)); +export default connect(mapStateToProps)(injectSheet(styles)(translate("admin")(AdminDetails))); diff --git a/scm-ui/src/containers/Login.js b/scm-ui/src/containers/Login.js index 4ae9b5a622..0f29013392 100644 --- a/scm-ui/src/containers/Login.js +++ b/scm-ui/src/containers/Login.js @@ -1,24 +1,14 @@ //@flow import React from "react"; -import { Redirect, withRouter } from "react-router-dom"; +import {Redirect, withRouter} from "react-router-dom"; import injectSheet from "react-jss"; -import { translate } from "react-i18next"; -import { - login, - isAuthenticated, - isLoginPending, - getLoginFailure -} from "../modules/auth"; -import { connect } from "react-redux"; +import {translate} from "react-i18next"; +import {getLoginFailure, isAuthenticated, isLoginPending, login} from "../modules/auth"; +import {connect} from "react-redux"; -import { - InputField, - SubmitButton, - ErrorNotification, - Image, UnauthorizedError -} from "@scm-manager/ui-components"; +import {ErrorNotification, Image, InputField, SubmitButton, UnauthorizedError} from "@scm-manager/ui-components"; import classNames from "classnames"; -import { getLoginLink } from "../modules/indexResource"; +import {getLoginLink} from "../modules/indexResource"; const styles = { avatar: { @@ -95,7 +85,7 @@ class Login extends React.Component<Props, State> { areCredentialsInvalid() { const { t, error } = this.props; if (error instanceof UnauthorizedError) { - return new Error(t("error-notification.wrong-login-credentials")); + return new Error(t("errorNotification.wrongLoginCredentials")); } else { return error; } diff --git a/scm-ui/src/containers/Main.js b/scm-ui/src/containers/Main.js index 8681f2457b..8c05578b6b 100644 --- a/scm-ui/src/containers/Main.js +++ b/scm-ui/src/containers/Main.js @@ -1,16 +1,16 @@ //@flow import React from "react"; -import { Redirect, Route, Switch, withRouter } from "react-router-dom"; -import type { Links } from "@scm-manager/ui-types"; +import {Redirect, Route, Switch, withRouter} from "react-router-dom"; +import type {Links} from "@scm-manager/ui-types"; import Overview from "../repos/containers/Overview"; import Users from "../users/containers/Users"; import Login from "../containers/Login"; import Logout from "../containers/Logout"; -import { ProtectedRoute } from "@scm-manager/ui-components"; -import { binder, ExtensionPoint } from "@scm-manager/ui-extensions"; +import {ProtectedRoute} from "@scm-manager/ui-components"; +import {binder, ExtensionPoint} from "@scm-manager/ui-extensions"; import CreateUser from "../users/containers/CreateUser"; import SingleUser from "../users/containers/SingleUser"; @@ -122,7 +122,6 @@ class Main extends React.Component<Props> { component={Profile} authenticated={authenticated} /> - <ExtensionPoint name="main.route" renderAll={true} diff --git a/scm-ui/src/repos/containers/RepositoryRoot.js b/scm-ui/src/repos/containers/RepositoryRoot.js index 6630086964..164ddcbc64 100644 --- a/scm-ui/src/repos/containers/RepositoryRoot.js +++ b/scm-ui/src/repos/containers/RepositoryRoot.js @@ -1,33 +1,20 @@ //@flow import React from "react"; -import { - fetchRepoByName, - getFetchRepoFailure, - getRepository, - isFetchRepoPending -} from "../modules/repos"; +import {fetchRepoByName, getFetchRepoFailure, getRepository, isFetchRepoPending} from "../modules/repos"; -import { connect } from "react-redux"; -import { Redirect, Route, Switch } from "react-router-dom"; -import type { Repository } from "@scm-manager/ui-types"; +import {connect} from "react-redux"; +import {Redirect, Route, Switch} from "react-router-dom"; +import type {Repository} from "@scm-manager/ui-types"; -import { - Loading, - Navigation, - SubNavigation, - NavLink, - Page, - Section, - ErrorPage -} from "@scm-manager/ui-components"; -import { translate } from "react-i18next"; +import {ErrorPage, Loading, Navigation, NavLink, Page, Section, SubNavigation} from "@scm-manager/ui-components"; +import {translate} from "react-i18next"; import RepositoryDetails from "../components/RepositoryDetails"; import EditRepo from "./EditRepo"; import BranchesOverview from "../branches/containers/BranchesOverview"; import CreateBranch from "../branches/containers/CreateBranch"; import Permissions from "../permissions/containers/Permissions"; -import type { History } from "history"; +import type {History} from "history"; import EditRepoNavLink from "../components/EditRepoNavLink"; import BranchRoot from "../branches/containers/BranchRoot"; import ChangesetsRoot from "./ChangesetsRoot"; @@ -35,8 +22,8 @@ import ChangesetView from "./ChangesetView"; import PermissionsNavLink from "../components/PermissionsNavLink"; import Sources from "../sources/containers/Sources"; import RepositoryNavLink from "../components/RepositoryNavLink"; -import { getLinks, getRepositoriesLink } from "../../modules/indexResource"; -import { binder, ExtensionPoint } from "@scm-manager/ui-extensions"; +import {getLinks, getRepositoriesLink} from "../../modules/indexResource"; +import {binder, ExtensionPoint} from "@scm-manager/ui-extensions"; type Props = { namespace: string, @@ -125,7 +112,7 @@ class RepositoryRoot extends React.Component<Props> { return ( <Page title={repository.namespace + "/" + repository.name}> <div className="columns"> - <div className="column is-three-quarters is-clipped"> + <div className="column is-three-quarters"> <Switch> <Redirect exact from={this.props.match.url} to={redirectedUrl} /> <Route diff --git a/scm-ui/src/repos/permissions/containers/CreatePermissionForm.js b/scm-ui/src/repos/permissions/containers/CreatePermissionForm.js index 112ecd1c20..33211c76f4 100644 --- a/scm-ui/src/repos/permissions/containers/CreatePermissionForm.js +++ b/scm-ui/src/repos/permissions/containers/CreatePermissionForm.js @@ -1,24 +1,20 @@ // @flow import React from "react"; -import { translate } from "react-i18next"; -import type { - RepositoryRole, - PermissionCollection, - PermissionCreateEntry, - SelectValue -} from "@scm-manager/ui-types"; +import {translate} from "react-i18next"; +import type {PermissionCollection, PermissionCreateEntry, RepositoryRole, SelectValue} from "@scm-manager/ui-types"; import { - Subtitle, - Autocomplete, - SubmitButton, Button, + GroupAutocomplete, LabelWithHelpIcon, - Radio + Radio, + SubmitButton, + Subtitle, + UserAutocomplete } from "@scm-manager/ui-components"; import * as validator from "../components/permissionValidation"; import RoleSelector from "../components/RoleSelector"; import AdvancedPermissionsDialog from "./AdvancedPermissionsDialog"; -import { findVerbsForRole } from "../modules/permissions"; +import {findVerbsForRole} from "../modules/permissions"; type Props = { availableRoles: RepositoryRole[], @@ -26,8 +22,8 @@ type Props = { createPermission: (permission: PermissionCreateEntry) => void, loading: boolean, currentPermissions: PermissionCollection, - groupAutoCompleteLink: string, - userAutoCompleteLink: string, + groupAutocompleteLink: string, + userAutocompleteLink: string, // Context props t: string => string @@ -68,65 +64,27 @@ class CreatePermissionForm extends React.Component<Props, State> { }); }; - loadUserAutocompletion = (inputValue: string) => { - return this.loadAutocompletion(this.props.userAutoCompleteLink, inputValue); - }; - - loadGroupAutocompletion = (inputValue: string) => { - return this.loadAutocompletion( - this.props.groupAutoCompleteLink, - inputValue - ); - }; - - loadAutocompletion(url: string, inputValue: string) { - const link = url + "?q="; - return fetch(link + inputValue) - .then(response => response.json()) - .then(json => { - return json.map(element => { - const label = element.displayName - ? `${element.displayName} (${element.id})` - : element.id; - return { - value: element, - label - }; - }); - }); - } - renderAutocompletionField = () => { - const { t } = this.props; - if (this.state.groupPermission) { + const group = this.state.groupPermission; + if (group) { return ( - <Autocomplete - loadSuggestions={this.loadGroupAutocompletion} - valueSelected={this.groupOrUserSelected} + <GroupAutocomplete + autocompleteLink={this.props.groupAutocompleteLink} + valueSelected={this.selectName} value={this.state.value ? this.state.value : ""} - label={t("permission.group")} - noOptionsMessage={t("permission.autocomplete.no-group-options")} - loadingMessage={t("permission.autocomplete.loading")} - placeholder={t("permission.autocomplete.group-placeholder")} - creatable={true} /> ); } return ( - <Autocomplete - loadSuggestions={this.loadUserAutocompletion} - valueSelected={this.groupOrUserSelected} + <UserAutocomplete + autocompleteLink={this.props.userAutocompleteLink} + valueSelected={this.selectName} value={this.state.value ? this.state.value : ""} - label={t("permission.user")} - noOptionsMessage={t("permission.autocomplete.no-user-options")} - loadingMessage={t("permission.autocomplete.loading")} - placeholder={t("permission.autocomplete.user-placeholder")} - creatable={true} /> ); }; - groupOrUserSelected = (value: SelectValue) => { + selectName = (value: SelectValue) => { this.setState({ value, name: value.value.id, @@ -150,15 +108,17 @@ class CreatePermissionForm extends React.Component<Props, State> { <AdvancedPermissionsDialog availableVerbs={availableVerbs} selectedVerbs={selectedVerbs} - onClose={this.closeAdvancedPermissionsDialog} + onClose={this.toggleAdvancedPermissionsDialog} onSubmit={this.submitAdvancedPermissionsDialog} /> ) : null; return ( - <div> + <> <hr /> - <Subtitle subtitle={t("permission.add-permission.add-permission-heading")} /> + <Subtitle + subtitle={t("permission.add-permission.add-permission-heading")} + /> {advancedDialog} <form onSubmit={this.submit}> <div className="field is-grouped"> @@ -201,7 +161,7 @@ class CreatePermissionForm extends React.Component<Props, State> { /> <Button label={t("permission.advanced-button.label")} - action={this.handleDetailedPermissionsPressed} + action={this.toggleAdvancedPermissionsDialog} /> </div> </div> @@ -217,16 +177,14 @@ class CreatePermissionForm extends React.Component<Props, State> { </div> </div> </form> - </div> + </> ); } - handleDetailedPermissionsPressed = () => { - this.setState({ showAdvancedDialog: true }); - }; - - closeAdvancedPermissionsDialog = () => { - this.setState({ showAdvancedDialog: false }); + toggleAdvancedPermissionsDialog = () => { + this.setState(prevState => ({ + showAdvancedDialog: !prevState.showAdvancedDialog + })); }; submitAdvancedPermissionsDialog = (newVerbs: string[]) => { diff --git a/scm-ui/src/repos/permissions/containers/Permissions.js b/scm-ui/src/repos/permissions/containers/Permissions.js index 39bd9e37c1..41cc366ea6 100644 --- a/scm-ui/src/repos/permissions/containers/Permissions.js +++ b/scm-ui/src/repos/permissions/containers/Permissions.js @@ -1,44 +1,34 @@ //@flow import React from "react"; -import { connect } from "react-redux"; -import { translate } from "react-i18next"; +import {connect} from "react-redux"; +import {translate} from "react-i18next"; import { + createPermission, + createPermissionReset, + deletePermissionReset, fetchAvailablePermissionsIfNeeded, fetchPermissions, - getFetchAvailablePermissionsFailure, getAvailablePermissions, + getAvailableRepositoryRoles, + getAvailableRepositoryVerbs, + getCreatePermissionFailure, + getDeletePermissionsFailure, + getFetchAvailablePermissionsFailure, getFetchPermissionsFailure, - isFetchAvailablePermissionsPending, - isFetchPermissionsPending, + getModifyPermissionsFailure, getPermissionsOfRepo, hasCreatePermission, - createPermission, isCreatePermissionPending, - getCreatePermissionFailure, - createPermissionReset, - getDeletePermissionsFailure, - getModifyPermissionsFailure, - modifyPermissionReset, - deletePermissionReset, - getAvailableRepositoryRoles, - getAvailableRepositoryVerbs + isFetchAvailablePermissionsPending, + isFetchPermissionsPending, + modifyPermissionReset } from "../modules/permissions"; -import { - Loading, - ErrorPage, - Subtitle, - LabelWithHelpIcon -} from "@scm-manager/ui-components"; -import type { - Permission, - PermissionCollection, - PermissionCreateEntry, - RepositoryRole -} from "@scm-manager/ui-types"; +import {ErrorPage, LabelWithHelpIcon, Loading, Subtitle} from "@scm-manager/ui-components"; +import type {Permission, PermissionCollection, PermissionCreateEntry, RepositoryRole} from "@scm-manager/ui-types"; import SinglePermission from "./SinglePermission"; import CreatePermissionForm from "./CreatePermissionForm"; -import type { History } from "history"; -import { getPermissionsLink } from "../../modules/repos"; +import type {History} from "history"; +import {getPermissionsLink} from "../../modules/repos"; import { getGroupAutoCompleteLink, getRepositoryRolesLink, @@ -60,8 +50,8 @@ type Props = { repositoryRolesLink: string, repositoryVerbsLink: string, permissionsLink: string, - groupAutoCompleteLink: string, - userAutoCompleteLink: string, + groupAutocompleteLink: string, + userAutocompleteLink: string, //dispatch functions fetchAvailablePermissionsIfNeeded: ( @@ -129,8 +119,8 @@ class Permissions extends React.Component<Props> { repoName, loadingCreatePermission, hasPermissionToCreate, - userAutoCompleteLink, - groupAutoCompleteLink + userAutocompleteLink, + groupAutocompleteLink } = this.props; if (error) { return ( @@ -153,8 +143,8 @@ class Permissions extends React.Component<Props> { createPermission={permission => this.createPermission(permission)} loading={loadingCreatePermission} currentPermissions={permissions} - userAutoCompleteLink={userAutoCompleteLink} - groupAutoCompleteLink={groupAutoCompleteLink} + userAutocompleteLink={userAutocompleteLink} + groupAutocompleteLink={groupAutocompleteLink} /> ) : null; @@ -228,8 +218,8 @@ const mapStateToProps = (state, ownProps) => { const repositoryRolesLink = getRepositoryRolesLink(state); const repositoryVerbsLink = getRepositoryVerbsLink(state); const permissionsLink = getPermissionsLink(state, namespace, repoName); - const groupAutoCompleteLink = getGroupAutoCompleteLink(state); - const userAutoCompleteLink = getUserAutoCompleteLink(state); + const groupAutocompleteLink = getGroupAutoCompleteLink(state); + const userAutocompleteLink = getUserAutoCompleteLink(state); const availablePermissions = getAvailablePermissions(state); const availableRepositoryRoles = getAvailableRepositoryRoles(state); const availableVerbs = getAvailableRepositoryVerbs(state); @@ -248,8 +238,8 @@ const mapStateToProps = (state, ownProps) => { hasPermissionToCreate, loadingCreatePermission, permissionsLink, - groupAutoCompleteLink, - userAutoCompleteLink + groupAutocompleteLink, + userAutocompleteLink }; }; diff --git a/scm-ui/src/repos/sources/containers/Sources.js b/scm-ui/src/repos/sources/containers/Sources.js index ddc469760c..11533a0526 100644 --- a/scm-ui/src/repos/sources/containers/Sources.js +++ b/scm-ui/src/repos/sources/containers/Sources.js @@ -1,25 +1,20 @@ // @flow import React from "react"; -import { connect } from "react-redux"; -import { withRouter } from "react-router-dom"; -import type { Branch, Repository } from "@scm-manager/ui-types"; +import {connect} from "react-redux"; +import {withRouter} from "react-router-dom"; +import type {Branch, Repository} from "@scm-manager/ui-types"; import FileTree from "../components/FileTree"; -import { - ErrorNotification, - Loading, - BranchSelector, - Breadcrumb -} from "@scm-manager/ui-components"; -import { translate } from "react-i18next"; +import {BranchSelector, Breadcrumb, ErrorNotification, Loading} from "@scm-manager/ui-components"; +import {translate} from "react-i18next"; import { fetchBranches, getBranches, getFetchBranchesFailure, isFetchBranchesPending } from "../../branches/modules/branches"; -import { compose } from "redux"; +import {compose} from "redux"; import Content from "./Content"; -import { fetchSources, isDirectory } from "../modules/sources"; +import {fetchSources, isDirectory} from "../modules/sources"; type Props = { repository: Repository, @@ -99,7 +94,7 @@ class Sources extends React.Component<Props> { return ( <div className="panel"> {this.renderBranchSelector()} - <Breadcrumb revision={revision} path={path} baseUrl={baseUrl} /> + <Breadcrumb revision={encodeURIComponent(revision)} path={path} baseUrl={baseUrl} /> <FileTree repository={repository} revision={revision} diff --git a/scm-ui/styles/scm.scss b/scm-ui/styles/scm.scss index 8378bb23e9..ddb2a748e5 100644 --- a/scm-ui/styles/scm.scss +++ b/scm-ui/styles/scm.scss @@ -497,6 +497,10 @@ form .field:not(.is-grouped) { } } +.modal-card-body div div:last-child { + border-bottom: none; +} + .sub-menu li { line-height: 1; @@ -521,3 +525,6 @@ form .field:not(.is-grouped) { display: none; } } + + +@import "bulma-popover/css/bulma-popover"; diff --git a/scm-ui/yarn.lock b/scm-ui/yarn.lock index 9ab4653e0f..cbeca27d09 100644 --- a/scm-ui/yarn.lock +++ b/scm-ui/yarn.lock @@ -1798,6 +1798,11 @@ builtin-status-codes@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" +bulma-popover@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/bulma-popover/-/bulma-popover-1.0.0.tgz#fe4b93fa6a68cb233145c16f69ee39ee0f7d4237" + integrity sha512-uc74pIcFIBG7vJMOOYwlcsyiN5HH1LmeDbDame2gLAtiM7EFGsHe58L6wQr6GNDJNrN/adwE2tNzqIb1yjL/Bw== + bulma-tooltip@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/bulma-tooltip/-/bulma-tooltip-2.0.2.tgz#cf0bf5ad2dc75492cbcbd4816e1a005314dc90ac" diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/EdisonHalAppender.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/EdisonHalAppender.java index 769de2b705..bb89c90556 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/EdisonHalAppender.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/EdisonHalAppender.java @@ -33,6 +33,11 @@ class EdisonHalAppender implements HalAppender { embeddedBuilder.with(rel, embedded); } + @Override + public void appendEmbedded(String rel, List<HalRepresentation> embedded) { + embeddedBuilder.with(rel, embedded); + } + private static class EdisonLinkArrayBuilder implements LinkArrayBuilder { private final Links.Builder builder; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupPermissionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupPermissionResource.java index 11934abcb0..dfce12e778 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupPermissionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupPermissionResource.java @@ -5,6 +5,7 @@ import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.TypeHint; import sonia.scm.security.PermissionAssigner; import sonia.scm.security.PermissionDescriptor; +import sonia.scm.security.PermissionPermissions; import sonia.scm.web.VndMediaType; import javax.inject.Inject; @@ -47,6 +48,7 @@ public class GroupPermissionResource { @ResponseCode(code = 500, condition = "internal server error") }) public Response getPermissions(@PathParam("id") String id) { + PermissionPermissions.read().check(); Collection<PermissionDescriptor> permissions = permissionAssigner.readPermissionsForGroup(id); return Response.ok(permissionCollectionToDtoMapper.mapForGroup(permissions, id)).build(); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeDto.java index 84fbbfe290..a87ad7df17 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeDto.java @@ -7,7 +7,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import java.util.List; +import java.util.Set; @Getter @Setter @@ -17,7 +17,7 @@ public class MeDto extends HalRepresentation { private String name; private String displayName; private String mail; - private List<String> groups; + private Set<String> groups; MeDto(Links links, Embedded embedded) { super(links, embedded); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeDtoFactory.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeDtoFactory.java index b5e1998066..c2bebd389a 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeDtoFactory.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeDtoFactory.java @@ -1,18 +1,16 @@ package sonia.scm.api.v2.resources; -import com.google.common.collect.ImmutableList; import de.otto.edison.hal.Embedded; import de.otto.edison.hal.Links; import org.apache.shiro.SecurityUtils; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.subject.Subject; -import sonia.scm.group.GroupNames; +import sonia.scm.group.GroupCollector; import sonia.scm.user.User; import sonia.scm.user.UserManager; import sonia.scm.user.UserPermissions; import javax.inject.Inject; -import java.util.Collections; import static de.otto.edison.hal.Embedded.embeddedBuilder; import static de.otto.edison.hal.Link.link; @@ -22,11 +20,13 @@ public class MeDtoFactory extends HalAppenderMapper { private final ResourceLinks resourceLinks; private final UserManager userManager; + private final GroupCollector groupCollector; @Inject - public MeDtoFactory(ResourceLinks resourceLinks, UserManager userManager) { + public MeDtoFactory(ResourceLinks resourceLinks, UserManager userManager, GroupCollector groupCollector) { this.resourceLinks = resourceLinks; this.userManager = userManager; + this.groupCollector = groupCollector; } public MeDto create() { @@ -35,16 +35,12 @@ public class MeDtoFactory extends HalAppenderMapper { MeDto dto = createDto(user); mapUserProperties(user, dto); - mapGroups(principals, dto); + mapGroups(user, dto); return dto; } - private void mapGroups(PrincipalCollection principals, MeDto dto) { - Iterable<String> groups = principals.oneByType(GroupNames.class); - if (groups == null) { - groups = Collections.emptySet(); - } - dto.setGroups(ImmutableList.copyOf(groups)); + private void mapGroups(User user, MeDto dto) { + dto.setGroups(groupCollector.collect(user.getName())); } private void mapUserProperties(User user, MeDto dto) { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MergeResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MergeResource.java index 2214b258c6..df59ba2abc 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MergeResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MergeResource.java @@ -6,6 +6,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.http.HttpStatus; import sonia.scm.ConcurrentModificationException; import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.RepositoryPermissions; import sonia.scm.repository.api.MergeCommandBuilder; import sonia.scm.repository.api.MergeCommandResult; import sonia.scm.repository.api.MergeDryRunCommandResult; @@ -49,6 +50,7 @@ public class MergeResource { NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name); log.info("Merge in Repository {}/{} from {} to {}", namespace, name, mergeCommand.getSourceRevision(), mergeCommand.getTargetRevision()); try (RepositoryService repositoryService = serviceFactory.create(namespaceAndName)) { + RepositoryPermissions.push(repositoryService.getRepository()).check(); MergeCommandResult mergeCommandResult = createMergeCommand(mergeCommand, repositoryService).executeMerge(); if (mergeCommandResult.isSuccess()) { return Response.noContent().build(); @@ -67,14 +69,19 @@ public class MergeResource { @ResponseCode(code = 500, condition = "internal server error") }) public Response dryRun(@PathParam("namespace") String namespace, @PathParam("name") String name, @Valid MergeCommandDto mergeCommand) { + NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name); log.info("Merge in Repository {}/{} from {} to {}", namespace, name, mergeCommand.getSourceRevision(), mergeCommand.getTargetRevision()); try (RepositoryService repositoryService = serviceFactory.create(namespaceAndName)) { - MergeDryRunCommandResult mergeCommandResult = createMergeCommand(mergeCommand, repositoryService).dryRun(); - if (mergeCommandResult.isMergeable()) { - return Response.noContent().build(); + if (RepositoryPermissions.push(repositoryService.getRepository()).isPermitted()) { + MergeDryRunCommandResult mergeCommandResult = createMergeCommand(mergeCommand, repositoryService).dryRun(); + if (mergeCommandResult.isMergeable()) { + return Response.noContent().build(); + } else { + throw new ConcurrentModificationException("revision", mergeCommand.getTargetRevision()); + } } else { - throw new ConcurrentModificationException("revision", mergeCommand.getTargetRevision()); + return Response.noContent().build(); } } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserPermissionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserPermissionResource.java index a961dfaa0e..fd54da503d 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserPermissionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserPermissionResource.java @@ -5,6 +5,7 @@ import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.TypeHint; import sonia.scm.security.PermissionAssigner; import sonia.scm.security.PermissionDescriptor; +import sonia.scm.security.PermissionPermissions; import sonia.scm.web.VndMediaType; import javax.inject.Inject; @@ -48,6 +49,7 @@ public class UserPermissionResource { @ResponseCode(code = 500, condition = "internal server error") }) public Response getPermissions(@PathParam("id") String id) { + PermissionPermissions.read().check(); Collection<PermissionDescriptor> permissions = permissionAssigner.readPermissionsForUser(id); return Response.ok(permissionCollectionToDtoMapper.mapForUser(permissions, id)).build(); } diff --git a/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupCollector.java b/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupCollector.java new file mode 100644 index 0000000000..8c6bac8103 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupCollector.java @@ -0,0 +1,72 @@ +package sonia.scm.group; + +import com.cronutils.utils.VisibleForTesting; +import com.google.common.collect.ImmutableSet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.cache.Cache; +import sonia.scm.cache.CacheManager; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.util.Set; + +/** + * Collect groups for a certain principal. + * <strong>Warning</strong>: The class is only for internal use and should never used directly. + */ +@Singleton +public class DefaultGroupCollector implements GroupCollector { + + private static final Logger LOG = LoggerFactory.getLogger(DefaultGroupCollector.class); + + @VisibleForTesting + static final String CACHE_NAME = "sonia.cache.externalGroups"; + + private final GroupDAO groupDAO; + private final Cache<String, Set<String>> cache; + private final Set<GroupResolver> groupResolvers; + + @Inject + public DefaultGroupCollector(GroupDAO groupDAO, CacheManager cacheManager, Set<GroupResolver> groupResolvers) { + this.groupDAO = groupDAO; + this.cache = cacheManager.getCache(CACHE_NAME); + this.groupResolvers = groupResolvers; + } + + @Override + public Set<String> collect(String principal) { + ImmutableSet.Builder<String> builder = ImmutableSet.builder(); + + builder.add(AUTHENTICATED); + builder.addAll(resolveExternalGroups(principal)); + appendInternalGroups(principal, builder); + + Set<String> groups = builder.build(); + LOG.debug("collected following groups for principal {}: {}", principal, groups); + return groups; + } + + private void appendInternalGroups(String principal, ImmutableSet.Builder<String> builder) { + for (Group group : groupDAO.getAll()) { + if (group.isMember(principal)) { + builder.add(group.getName()); + } + } + } + + private Set<String> resolveExternalGroups(String principal) { + Set<String> externalGroups = cache.get(principal); + + if (externalGroups == null) { + ImmutableSet.Builder<String> newExternalGroups = ImmutableSet.builder(); + + for (GroupResolver groupResolver : groupResolvers) { + newExternalGroups.addAll(groupResolver.resolve(principal)); + } + externalGroups = newExternalGroups.build(); + cache.put(principal, externalGroups); + } + return externalGroups; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/BootstrapModule.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/BootstrapModule.java index 9b6ea719d8..06f6462af0 100644 --- a/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/BootstrapModule.java +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/BootstrapModule.java @@ -1,6 +1,7 @@ package sonia.scm.lifecycle.modules; import com.google.inject.AbstractModule; +import com.google.inject.TypeLiteral; import com.google.inject.throwingproviders.ThrowingProviderBinder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -10,6 +11,7 @@ import sonia.scm.io.DefaultFileSystem; import sonia.scm.io.FileSystem; import sonia.scm.plugin.PluginLoader; import sonia.scm.repository.RepositoryLocationResolver; +import sonia.scm.repository.xml.MetadataStore; import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver; import sonia.scm.security.CipherHandler; import sonia.scm.security.CipherUtil; @@ -19,15 +21,20 @@ import sonia.scm.store.BlobStoreFactory; import sonia.scm.store.ConfigurationEntryStoreFactory; import sonia.scm.store.ConfigurationStoreFactory; import sonia.scm.store.DataStoreFactory; +import sonia.scm.store.DefaultBlobDirectoryAccess; import sonia.scm.store.FileBlobStoreFactory; import sonia.scm.store.JAXBConfigurationEntryStoreFactory; import sonia.scm.store.JAXBConfigurationStoreFactory; import sonia.scm.store.JAXBDataStoreFactory; import sonia.scm.store.JAXBPropertyFileAccess; +import sonia.scm.update.BlobDirectoryAccess; import sonia.scm.update.PropertyFileAccess; +import sonia.scm.update.UpdateStepRepositoryMetadataAccess; import sonia.scm.update.V1PropertyDAO; import sonia.scm.update.xml.XmlV1PropertyDAO; +import java.nio.file.Path; + public class BootstrapModule extends AbstractModule { private static final Logger LOG = LoggerFactory.getLogger(BootstrapModule.class); @@ -65,6 +72,8 @@ public class BootstrapModule extends AbstractModule { bind(PluginLoader.class).toInstance(pluginLoader); bind(V1PropertyDAO.class, XmlV1PropertyDAO.class); bind(PropertyFileAccess.class, JAXBPropertyFileAccess.class); + bind(BlobDirectoryAccess.class, DefaultBlobDirectoryAccess.class); + bind(new TypeLiteral<UpdateStepRepositoryMetadataAccess<Path>>() {}).to(new TypeLiteral<MetadataStore>() {}); } private <T> void bind(Class<T> clazz, Class<? extends T> defaultImplementation) { diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/ScmServletModule.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/ScmServletModule.java index 7b8af8fbcc..cd34d03b50 100644 --- a/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/ScmServletModule.java +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/ScmServletModule.java @@ -50,13 +50,16 @@ import sonia.scm.cache.CacheManager; import sonia.scm.cache.GuavaCacheManager; import sonia.scm.config.ScmConfiguration; import sonia.scm.event.ScmEventBus; +import sonia.scm.group.DefaultGroupCollector; import sonia.scm.group.DefaultGroupDisplayManager; import sonia.scm.group.DefaultGroupManager; +import sonia.scm.group.GroupCollector; import sonia.scm.group.GroupDAO; import sonia.scm.group.GroupDisplayManager; import sonia.scm.group.GroupManager; import sonia.scm.group.GroupManagerProvider; import sonia.scm.group.xml.XmlGroupDAO; +import sonia.scm.migration.MigrationDAO; import sonia.scm.net.SSLContextProvider; import sonia.scm.net.ahc.AdvancedHttpClient; import sonia.scm.net.ahc.ContentTransformer; @@ -97,6 +100,7 @@ import sonia.scm.template.MustacheTemplateEngine; import sonia.scm.template.TemplateEngine; import sonia.scm.template.TemplateEngineFactory; import sonia.scm.template.TemplateServlet; +import sonia.scm.update.repository.DefaultMigrationStrategyDAO; import sonia.scm.user.DefaultUserDisplayManager; import sonia.scm.user.DefaultUserManager; import sonia.scm.user.UserDAO; @@ -183,6 +187,7 @@ class ScmServletModule extends ServletModule { bind(RepositoryDAO.class, XmlRepositoryDAO.class); bind(RepositoryRoleDAO.class, XmlRepositoryRoleDAO.class); bind(RepositoryRoleManager.class).to(DefaultRepositoryRoleManager.class); + bind(MigrationDAO.class).to(DefaultMigrationStrategyDAO.class); bindDecorated(RepositoryManager.class, DefaultRepositoryManager.class, RepositoryManagerProvider.class); @@ -192,7 +197,7 @@ class ScmServletModule extends ServletModule { bindDecorated(GroupManager.class, DefaultGroupManager.class, GroupManagerProvider.class); bind(GroupDisplayManager.class, DefaultGroupDisplayManager.class); - + bind(GroupCollector.class, DefaultGroupCollector.class); bind(CGIExecutorFactory.class, DefaultCGIExecutorFactory.class); // bind sslcontext provider diff --git a/scm-webapp/src/main/java/sonia/scm/security/AuthorizationChangedEventProducer.java b/scm-webapp/src/main/java/sonia/scm/security/AuthorizationChangedEventProducer.java index 66dbb51073..3f81377992 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/AuthorizationChangedEventProducer.java +++ b/scm-webapp/src/main/java/sonia/scm/security/AuthorizationChangedEventProducer.java @@ -48,6 +48,8 @@ import sonia.scm.user.User; import sonia.scm.user.UserEvent; import sonia.scm.user.UserModificationEvent; +import javax.inject.Singleton; + /** * Receives all kinds of events, which affects authorization relevant data and fires an * {@link AuthorizationChangedEvent} if authorization data has changed. @@ -55,6 +57,7 @@ import sonia.scm.user.UserModificationEvent; * @author Sebastian Sdorra * @since 1.52 */ +@Singleton @EagerSingleton public class AuthorizationChangedEventProducer { diff --git a/scm-webapp/src/main/java/sonia/scm/security/BearerRealm.java b/scm-webapp/src/main/java/sonia/scm/security/BearerRealm.java index b237a0a5ff..324dfe9082 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/BearerRealm.java +++ b/scm-webapp/src/main/java/sonia/scm/security/BearerRealm.java @@ -104,7 +104,6 @@ public class BearerRealm extends AuthenticatingRealm return helper.authenticationInfoBuilder(accessToken.getSubject()) .withCredentials(bt.getCredentials()) .withScope(Scopes.fromClaims(accessToken.getClaims())) - .withGroups(accessToken.getGroups()) .build(); } diff --git a/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java b/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java index baff3b951c..28f61df34f 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java +++ b/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java @@ -52,17 +52,18 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.cache.Cache; import sonia.scm.cache.CacheManager; -import sonia.scm.group.GroupNames; +import sonia.scm.group.GroupCollector; import sonia.scm.group.GroupPermissions; import sonia.scm.plugin.Extension; -import sonia.scm.repository.RepositoryPermission; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryDAO; +import sonia.scm.repository.RepositoryPermission; import sonia.scm.user.User; import sonia.scm.user.UserPermissions; import sonia.scm.util.Util; import java.util.Collection; +import java.util.Set; //~--- JDK imports ------------------------------------------------------------ @@ -88,19 +89,21 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector /** * Constructs ... - * @param cacheManager + * @param cacheManager * @param repositoryDAO * @param securitySystem * @param repositoryPermissionProvider + * @param groupCollector */ @Inject public DefaultAuthorizationCollector(CacheManager cacheManager, - RepositoryDAO repositoryDAO, SecuritySystem securitySystem, RepositoryPermissionProvider repositoryPermissionProvider) + RepositoryDAO repositoryDAO, SecuritySystem securitySystem, RepositoryPermissionProvider repositoryPermissionProvider, GroupCollector groupCollector) { this.cache = cacheManager.getCache(CACHE_NAME); this.repositoryDAO = repositoryDAO; this.securitySystem = securitySystem; this.repositoryPermissionProvider = repositoryPermissionProvider; + this.groupCollector = groupCollector; } //~--- methods -------------------------------------------------------------- @@ -145,16 +148,16 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector Preconditions.checkNotNull(user, "no user found in principal collection"); - GroupNames groupNames = principals.oneByType(GroupNames.class); + Set<String> groups = groupCollector.collect(user.getName()); - CacheKey cacheKey = new CacheKey(user.getId(), groupNames); + CacheKey cacheKey = new CacheKey(user.getId(), groups); AuthorizationInfo info = cache.get(cacheKey); if (info == null) { logger.trace("collect AuthorizationInfo for user {}", user.getName()); - info = createAuthorizationInfo(user, groupNames); + info = createAuthorizationInfo(user, groups); cache.put(cacheKey, info); } else if (logger.isTraceEnabled()) @@ -166,7 +169,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector } private void collectGlobalPermissions(Builder<String> builder, - final User user, final GroupNames groups) + final User user, final Set<String> groups) { Collection<AssignedPermission> globalPermissions = securitySystem.getPermissions((AssignedPermission input) -> isUserPermitted(user, groups, input)); @@ -181,7 +184,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector } private void collectRepositoryPermissions(Builder<String> builder, User user, - GroupNames groups) + Set<String> groups) { for (Repository repository : repositoryDAO.getAll()) { @@ -190,7 +193,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector } private void collectRepositoryPermissions(Builder<String> builder, - Repository repository, User user, GroupNames groups) + Repository repository, User user, Set<String> groups) { Collection<RepositoryPermission> repositoryPermissions = repository.getPermissions(); @@ -245,7 +248,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector .getVerbs(); } - private AuthorizationInfo createAuthorizationInfo(User user, GroupNames groups) { + private AuthorizationInfo createAuthorizationInfo(User user, Set<String> groups) { Builder<String> builder = ImmutableSet.builder(); collectGlobalPermissions(builder, user, groups); @@ -279,7 +282,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector //~--- get methods ---------------------------------------------------------- - private boolean isUserPermitted(User user, GroupNames groups, + private boolean isUserPermitted(User user, Set<String> groups, PermissionObject perm) { //J- @@ -314,7 +317,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector */ private static class CacheKey { - private CacheKey(String username, GroupNames groupnames) + private CacheKey(String username, Set<String> groupnames) { this.username = username; this.groupnames = groupnames; @@ -356,7 +359,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector //~--- fields ------------------------------------------------------------- /** group names */ - private final GroupNames groupnames; + private final Set<String> groupnames; /** username */ private final String username; @@ -374,4 +377,5 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector private final SecuritySystem securitySystem; private final RepositoryPermissionProvider repositoryPermissionProvider; + private final GroupCollector groupCollector; } diff --git a/scm-webapp/src/main/java/sonia/scm/security/DefaultRealm.java b/scm-webapp/src/main/java/sonia/scm/security/DefaultRealm.java index bacbd9b314..e65c88e679 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/DefaultRealm.java +++ b/scm-webapp/src/main/java/sonia/scm/security/DefaultRealm.java @@ -34,7 +34,6 @@ package sonia.scm.security; //~--- non-JDK imports -------------------------------------------------------- import com.google.common.annotations.VisibleForTesting; - import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; @@ -45,21 +44,16 @@ import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; - -import org.apache.shiro.subject.SimplePrincipalCollection; -import sonia.scm.group.GroupNames; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import sonia.scm.plugin.Extension; -//~--- JDK imports ------------------------------------------------------------ - import javax.inject.Inject; import javax.inject.Singleton; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.util.Set; +//~--- JDK imports ------------------------------------------------------------ + /** * Default authorizing realm. * @@ -103,6 +97,9 @@ public class DefaultRealm extends AuthorizingRealm matcher.setPasswordService(service); setCredentialsMatcher(helper.wrapCredentialsMatcher(matcher)); setAuthenticationTokenClass(UsernamePasswordToken.class); + + // we cache in the AuthorizationCollector + setCachingEnabled(false); } //~--- methods -------------------------------------------------------------- @@ -149,7 +146,7 @@ public class DefaultRealm extends AuthorizingRealm LOG.trace("principal does not contain scope information, returning all permissions"); log(principals, info, null); } - + return info; } @@ -180,8 +177,6 @@ public class DefaultRealm extends AuthorizingRealm StringBuilder buffer = new StringBuilder("authorization summary: "); buffer.append(SEPARATOR).append("username : ").append(collection.getPrimaryPrincipal()); - buffer.append(SEPARATOR).append("groups : "); - append(buffer, collection.oneByType(GroupNames.class)); buffer.append(SEPARATOR).append("roles : "); append(buffer, original.getRoles()); buffer.append(SEPARATOR).append("scope : "); diff --git a/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenBuilder.java b/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenBuilder.java index cc1d4be7a7..5a74f77502 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenBuilder.java +++ b/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenBuilder.java @@ -40,11 +40,9 @@ import org.apache.shiro.SecurityUtils; import org.apache.shiro.subject.Subject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sonia.scm.group.ExternalGroupNames; import java.time.Clock; import java.time.Instant; -import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; @@ -139,12 +137,6 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder { return this; } - @Override - public JwtAccessTokenBuilder groups(String... groups) { - Collections.addAll(this.groups, groups); - return this; - } - JwtAccessTokenBuilder refreshExpiration(Instant refreshExpiration) { this.refreshExpiration = refreshExpiration; this.refreshableFor = 0; @@ -206,16 +198,6 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder { claims.setIssuer(issuer); } - if (!groups.isEmpty()) { - claims.put(JwtAccessToken.GROUPS_CLAIM_KEY, groups); - } else { - Subject currentSubject = SecurityUtils.getSubject(); - ExternalGroupNames externalGroupNames = currentSubject.getPrincipals().oneByType(ExternalGroupNames.class); - if (externalGroupNames != null) { - claims.put(JwtAccessToken.GROUPS_CLAIM_KEY, externalGroupNames.getCollection().toArray(new String[]{})); - } - } - // sign token and create compact version String compact = Jwts.builder() .setClaims(claims) diff --git a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java index 445a99feb7..173dcb0638 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java +++ b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java @@ -7,10 +7,10 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sonia.scm.lifecycle.RestartEvent; import sonia.scm.event.ScmEventBus; +import sonia.scm.lifecycle.RestartEvent; +import sonia.scm.update.repository.DefaultMigrationStrategyDAO; import sonia.scm.update.repository.MigrationStrategy; -import sonia.scm.update.repository.MigrationStrategyDao; import sonia.scm.update.repository.V1Repository; import sonia.scm.update.repository.XmlRepositoryV1UpdateStep; import sonia.scm.util.ValidationUtil; @@ -37,10 +37,10 @@ class MigrationWizardServlet extends HttpServlet { private static final Logger LOG = LoggerFactory.getLogger(MigrationWizardServlet.class); private final XmlRepositoryV1UpdateStep repositoryV1UpdateStep; - private final MigrationStrategyDao migrationStrategyDao; + private final DefaultMigrationStrategyDAO migrationStrategyDao; @Inject - MigrationWizardServlet(XmlRepositoryV1UpdateStep repositoryV1UpdateStep, MigrationStrategyDao migrationStrategyDao) { + MigrationWizardServlet(XmlRepositoryV1UpdateStep repositoryV1UpdateStep, DefaultMigrationStrategyDAO migrationStrategyDao) { this.repositoryV1UpdateStep = repositoryV1UpdateStep; this.migrationStrategyDao = migrationStrategyDao; } @@ -103,11 +103,12 @@ class MigrationWizardServlet extends HttpServlet { .forEach( entry-> { String id = entry.getId(); + String protocol = entry.getType(); String originalName = entry.getOriginalName(); String strategy = req.getParameter("strategy-" + id); String namespace = req.getParameter("namespace-" + id); String name = req.getParameter("name-" + id); - migrationStrategyDao.set(id, originalName, MigrationStrategy.valueOf(strategy), namespace, name); + migrationStrategyDao.set(id, protocol, originalName, MigrationStrategy.valueOf(strategy), namespace, name); } ); diff --git a/scm-webapp/src/main/java/sonia/scm/update/UpdateEngine.java b/scm-webapp/src/main/java/sonia/scm/update/UpdateEngine.java index f9d1d5b419..980921d283 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/UpdateEngine.java +++ b/scm-webapp/src/main/java/sonia/scm/update/UpdateEngine.java @@ -55,9 +55,10 @@ public class UpdateEngine { private void execute(UpdateStep updateStep) { try { - LOG.info("running update step for type {} and version {}", + LOG.info("running update step for type {} and version {} (class {})", updateStep.getAffectedDataType(), - updateStep.getTargetVersion() + updateStep.getTargetVersion(), + updateStep.getClass().getName() ); updateStep.doUpdate(); } catch (Exception e) { diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/DefaultMigrationStrategyDAO.java b/scm-webapp/src/main/java/sonia/scm/update/repository/DefaultMigrationStrategyDAO.java new file mode 100644 index 0000000000..4b10c3e6a7 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/DefaultMigrationStrategyDAO.java @@ -0,0 +1,44 @@ +package sonia.scm.update.repository; + +import sonia.scm.migration.MigrationDAO; +import sonia.scm.migration.MigrationInfo; +import sonia.scm.store.ConfigurationStore; +import sonia.scm.store.ConfigurationStoreFactory; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.util.Collection; +import java.util.Optional; + +import static java.util.stream.Collectors.toList; + +@Singleton +public class DefaultMigrationStrategyDAO implements MigrationDAO { + + private final RepositoryMigrationPlan plan; + private final ConfigurationStore<RepositoryMigrationPlan> store; + + @Inject + public DefaultMigrationStrategyDAO(ConfigurationStoreFactory storeFactory) { + store = storeFactory.withType(RepositoryMigrationPlan.class).withName("migration-plan").build(); + this.plan = store.getOptional().orElse(new RepositoryMigrationPlan()); + } + + public Optional<RepositoryMigrationPlan.RepositoryMigrationEntry> get(String id) { + return plan.get(id); + } + + public void set(String repositoryId, String protocol, String originalName, MigrationStrategy strategy, String newNamespace, String newName) { + plan.set(repositoryId, protocol, originalName, strategy, newNamespace, newName); + store.set(plan); + } + + @Override + public Collection<MigrationInfo> getAll() { + return plan + .getEntries() + .stream() + .map(e -> new MigrationInfo(e.getRepositoryId(), e.getProtocol(), e.getOriginalName(), e.getNewNamespace(), e.getNewName())) + .collect(toList()); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategyDao.java b/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategyDao.java deleted file mode 100644 index bdc8f97359..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategyDao.java +++ /dev/null @@ -1,30 +0,0 @@ -package sonia.scm.update.repository; - -import sonia.scm.store.ConfigurationStore; -import sonia.scm.store.ConfigurationStoreFactory; - -import javax.inject.Inject; -import javax.inject.Singleton; -import java.util.Optional; - -@Singleton -public class MigrationStrategyDao { - - private final RepositoryMigrationPlan plan; - private final ConfigurationStore<RepositoryMigrationPlan> store; - - @Inject - public MigrationStrategyDao(ConfigurationStoreFactory storeFactory) { - store = storeFactory.withType(RepositoryMigrationPlan.class).withName("migration-plan").build(); - this.plan = store.getOptional().orElse(new RepositoryMigrationPlan()); - } - - public Optional<RepositoryMigrationPlan.RepositoryMigrationEntry> get(String id) { - return plan.get(id); - } - - public void set(String repositoryId, String originalName, MigrationStrategy strategy, String newNamespace, String newName) { - plan.set(repositoryId, originalName, strategy, newNamespace, newName); - store.set(plan); - } -} diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/RepositoryMigrationPlan.java b/scm-webapp/src/main/java/sonia/scm/update/repository/RepositoryMigrationPlan.java index 5f523ee76a..81d4d532ec 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/RepositoryMigrationPlan.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/RepositoryMigrationPlan.java @@ -4,6 +4,8 @@ import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlRootElement; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Optional; @@ -29,14 +31,18 @@ class RepositoryMigrationPlan { .findFirst(); } - public void set(String repositoryId, String originalName, MigrationStrategy strategy, String newNamespace, String newName) { + public Collection<RepositoryMigrationEntry> getEntries() { + return Collections.unmodifiableList(entries); + } + + public void set(String repositoryId, String protocol, String originalName, MigrationStrategy strategy, String newNamespace, String newName) { Optional<RepositoryMigrationEntry> entry = get(repositoryId); if (entry.isPresent()) { entry.get().setStrategy(strategy); entry.get().setNewNamespace(newNamespace); entry.get().setNewName(newName); } else { - entries.add(new RepositoryMigrationEntry(repositoryId, originalName, strategy, newNamespace, newName)); + entries.add(new RepositoryMigrationEntry(repositoryId, protocol, originalName, strategy, newNamespace, newName)); } } @@ -45,6 +51,7 @@ class RepositoryMigrationPlan { static class RepositoryMigrationEntry { private String repositoryId; + private String protocol; private String originalName; private MigrationStrategy dataMigrationStrategy; private String newNamespace; @@ -53,14 +60,23 @@ class RepositoryMigrationPlan { RepositoryMigrationEntry() { } - RepositoryMigrationEntry(String repositoryId, String originalName, MigrationStrategy dataMigrationStrategy, String newNamespace, String newName) { + RepositoryMigrationEntry(String repositoryId, String protocol, String originalName, MigrationStrategy dataMigrationStrategy, String newNamespace, String newName) { this.repositoryId = repositoryId; + this.protocol = protocol; this.originalName = originalName; this.dataMigrationStrategy = dataMigrationStrategy; this.newNamespace = newNamespace; this.newName = newName; } + public String getRepositoryId() { + return repositoryId; + } + + public String getProtocol() { + return protocol; + } + public String getOriginalName() { return originalName; } diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java b/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java index f7a4e1ed37..a2f7656498 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java @@ -49,7 +49,7 @@ import static sonia.scm.version.Version.parse; * <li>a new entry in the new <code>repository-paths.xml</code> database is written,</li> * <li>the data directory is moved or copied to a SCM v2 consistent directory. How this is done * can be specified by a strategy (@see {@link MigrationStrategy}), that has to be set in - * a database file named <code>migration-plan.xml</code></li> (to create this file, use {@link MigrationStrategyDao}), + * a database file named <code>migration-plan.xml</code></li> (to create this file, use {@link DefaultMigrationStrategyDAO}), * and * <li>the new <code>metadata.xml</code> file is created.</li> * </ul> @@ -63,7 +63,7 @@ public class XmlRepositoryV1UpdateStep implements CoreUpdateStep { private final SCMContextProvider contextProvider; private final XmlRepositoryDAO repositoryDao; - private final MigrationStrategyDao migrationStrategyDao; + private final DefaultMigrationStrategyDAO migrationStrategyDao; private final Injector injector; private final ConfigurationEntryStore<V1Properties> propertyStore; @@ -71,7 +71,7 @@ public class XmlRepositoryV1UpdateStep implements CoreUpdateStep { public XmlRepositoryV1UpdateStep( SCMContextProvider contextProvider, XmlRepositoryDAO repositoryDao, - MigrationStrategyDao migrationStrategyDao, + DefaultMigrationStrategyDAO migrationStrategyDao, Injector injector, ConfigurationEntryStoreFactory configurationEntryStoreFactory ) { diff --git a/scm-webapp/src/main/java/sonia/scm/update/security/XmlSecurityV1UpdateStep.java b/scm-webapp/src/main/java/sonia/scm/update/security/XmlSecurityV1UpdateStep.java index f62b81b5df..8dac2b5073 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/security/XmlSecurityV1UpdateStep.java +++ b/scm-webapp/src/main/java/sonia/scm/update/security/XmlSecurityV1UpdateStep.java @@ -21,7 +21,10 @@ import javax.xml.bind.annotation.XmlRootElement; import java.io.File; import java.nio.file.Path; import java.util.Arrays; +import java.util.List; import java.util.function.Consumer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import static java.util.Optional.ofNullable; import static sonia.scm.version.Version.parse; @@ -29,6 +32,8 @@ import static sonia.scm.version.Version.parse; @Extension public class XmlSecurityV1UpdateStep implements UpdateStep { + private static final Pattern v1PermissionPattern = Pattern.compile("^repository:\\*:(READ|WRITE|OWNER)$"); + private static final Logger LOG = LoggerFactory.getLogger(XmlSecurityV1UpdateStep.class); private final SCMContextProvider contextProvider; @@ -46,6 +51,50 @@ public class XmlSecurityV1UpdateStep implements UpdateStep { forAllAdmins(user -> createSecurityEntry(user, false, securityStore), group -> createSecurityEntry(group, true, securityStore)); + + mapV1Permissions(securityStore); + } + + private void mapV1Permissions(ConfigurationEntryStore<AssignedPermission> securityStore) throws JAXBException { + Path v1SecurityFile = determineConfigDirectory().resolve("securityV1" + StoreConstants.FILE_EXTENSION); + + if (!v1SecurityFile.toFile().exists()) { + LOG.info("no v1 file for security found"); + return; + } + + JAXBContext jaxbContext = JAXBContext.newInstance(XmlSecurityV1UpdateStep.V1Security.class); + V1Security v1Security = (V1Security) jaxbContext.createUnmarshaller().unmarshal(v1SecurityFile.toFile()); + + v1Security.entries.forEach(assignedPermission -> { + Matcher matcher = v1PermissionPattern.matcher(assignedPermission.value.permission); + if (matcher.matches()) { + String newPermission = convertRole(matcher.group(1)); + securityStore.put(new AssignedPermission( + assignedPermission.value.name, + Boolean.parseBoolean(assignedPermission.value.groupPermission), + newPermission + )); + } + }); + } + + private String convertRole(String role) { + String newPermission; + switch (role) { + case "OWNER": + newPermission = "repository:*"; + break; + case "WRITE": + newPermission = "repository:read,pull,push:*"; + break; + case "READ": + newPermission = "repository:read,pull:*"; + break; + default: + newPermission = ""; + } + return newPermission; } private void forAllAdmins(Consumer<String> userConsumer, Consumer<String> groupConsumer) throws JAXBException { @@ -70,10 +119,9 @@ public class XmlSecurityV1UpdateStep implements UpdateStep { Arrays.stream(entries.split(",")).forEach(consumer); } - @Override public Version getTargetVersion() { - return parse("2.0.0"); + return parse("2.0.1"); } @Override @@ -102,4 +150,29 @@ public class XmlSecurityV1UpdateStep implements UpdateStep { @XmlElement(name = "admin-groups") private String adminGroups; } + + @XmlAccessorType(XmlAccessType.FIELD) + @XmlRootElement(name = "configuration") + private static class V1Security { + @XmlElement(name = "entry") + private List<Entry> entries; + } + + @XmlAccessorType(XmlAccessType.FIELD) + private static class Entry { + @XmlElement(name = "key") + private String key; + @XmlElement(name = "value") + private Value value; + } + + @XmlAccessorType(XmlAccessType.FIELD) + private static class Value { + @XmlElement(name = "permission") + String permission; + @XmlElement(name = "name") + String name; + @XmlElement(name = "group-permission") + String groupPermission; + } } diff --git a/scm-webapp/src/main/java/sonia/scm/update/user/XmlUserV1UpdateStep.java b/scm-webapp/src/main/java/sonia/scm/update/user/XmlUserV1UpdateStep.java index b2da69fd9b..f2561ac53e 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/user/XmlUserV1UpdateStep.java +++ b/scm-webapp/src/main/java/sonia/scm/update/user/XmlUserV1UpdateStep.java @@ -57,7 +57,8 @@ public class XmlUserV1UpdateStep implements UpdateStep { @Override public void doUpdate() throws JAXBException { - Optional<Path> v1UsersFile = determineV1File(); + Optional<Path> v1UsersFile = determineV1File("users"); + determineV1File("security"); if (!v1UsersFile.isPresent()) { LOG.info("no v1 file for users found"); return; @@ -107,17 +108,17 @@ public class XmlUserV1UpdateStep implements UpdateStep { return configurationEntryStoreFactory.withType(AssignedPermission.class).withName("security").build(); } - private Optional<Path> determineV1File() { - Path existingUsersFile = resolveConfigFile("users"); - Path usersV1File = resolveConfigFile("usersV1"); - if (existingUsersFile.toFile().exists()) { + private Optional<Path> determineV1File(String filename) { + Path existingFile = resolveConfigFile(filename); + Path v1File = resolveConfigFile(filename + "V1"); + if (existingFile.toFile().exists()) { try { - Files.move(existingUsersFile, usersV1File); + Files.move(existingFile, v1File); } catch (IOException e) { - throw new UpdateException("could not move old users file to " + usersV1File.toAbsolutePath()); + throw new UpdateException("could not move old " + filename + " file to " + v1File.toAbsolutePath()); } - LOG.info("moved old users file to {}", usersV1File.toAbsolutePath()); - return of(usersV1File); + LOG.info("moved old " + filename + " file to {}", v1File.toAbsolutePath()); + return of(v1File); } return empty(); } diff --git a/scm-webapp/src/main/java/sonia/scm/web/filter/DefaultHttpProtocolServletAuthenticationFilter.java b/scm-webapp/src/main/java/sonia/scm/web/filter/DefaultHttpProtocolServletAuthenticationFilter.java new file mode 100644 index 0000000000..f9b40961a3 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/web/filter/DefaultHttpProtocolServletAuthenticationFilter.java @@ -0,0 +1,22 @@ +package sonia.scm.web.filter; + +import sonia.scm.Priority; +import sonia.scm.config.ScmConfiguration; +import sonia.scm.filter.Filters; +import sonia.scm.filter.WebElement; +import sonia.scm.web.UserAgentParser; +import sonia.scm.web.WebTokenGenerator; +import sonia.scm.web.protocol.HttpProtocolServlet; + +import javax.inject.Inject; +import java.util.Set; + +@Priority(Filters.PRIORITY_AUTHENTICATION) +@WebElement(value = HttpProtocolServlet.PATTERN) +public class DefaultHttpProtocolServletAuthenticationFilter extends HttpProtocolServletAuthenticationFilterBase { + + @Inject + public DefaultHttpProtocolServletAuthenticationFilter(ScmConfiguration configuration, Set<WebTokenGenerator> tokenGenerators, UserAgentParser userAgentParser) { + super(configuration, tokenGenerators, userAgentParser); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/web/protocol/HttpProtocolServlet.java b/scm-webapp/src/main/java/sonia/scm/web/protocol/HttpProtocolServlet.java index 250846a8cc..623be728c1 100644 --- a/scm-webapp/src/main/java/sonia/scm/web/protocol/HttpProtocolServlet.java +++ b/scm-webapp/src/main/java/sonia/scm/web/protocol/HttpProtocolServlet.java @@ -16,7 +16,6 @@ import sonia.scm.repository.spi.HttpScmProtocol; import sonia.scm.web.UserAgent; import sonia.scm.web.UserAgentParser; -import javax.inject.Provider; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; @@ -33,17 +32,15 @@ public class HttpProtocolServlet extends HttpServlet { public static final String PATTERN = PATH + "/*"; private final RepositoryServiceFactory serviceFactory; - - private final Provider<HttpServletRequest> requestProvider; - + private final NamespaceAndNameFromPathExtractor pathExtractor; private final PushStateDispatcher dispatcher; private final UserAgentParser userAgentParser; @Inject - public HttpProtocolServlet(RepositoryServiceFactory serviceFactory, Provider<HttpServletRequest> requestProvider, PushStateDispatcher dispatcher, UserAgentParser userAgentParser) { + public HttpProtocolServlet(RepositoryServiceFactory serviceFactory, NamespaceAndNameFromPathExtractor pathExtractor, PushStateDispatcher dispatcher, UserAgentParser userAgentParser) { this.serviceFactory = serviceFactory; - this.requestProvider = requestProvider; + this.pathExtractor = pathExtractor; this.dispatcher = dispatcher; this.userAgentParser = userAgentParser; } @@ -55,9 +52,8 @@ public class HttpProtocolServlet extends HttpServlet { log.trace("dispatch browser request for user agent {}", userAgent); dispatcher.dispatch(request, response, request.getRequestURI()); } else { - String pathInfo = request.getPathInfo(); - Optional<NamespaceAndName> namespaceAndName = NamespaceAndNameFromPathExtractor.fromUri(pathInfo); + Optional<NamespaceAndName> namespaceAndName = pathExtractor.fromUri(pathInfo); if (namespaceAndName.isPresent()) { service(request, response, namespaceAndName.get()); } else { @@ -69,7 +65,7 @@ public class HttpProtocolServlet extends HttpServlet { private void service(HttpServletRequest req, HttpServletResponse resp, NamespaceAndName namespaceAndName) throws IOException, ServletException { try (RepositoryService repositoryService = serviceFactory.create(namespaceAndName)) { - requestProvider.get().setAttribute(DefaultRepositoryProvider.ATTRIBUTE_NAME, repositoryService.getRepository()); + req.setAttribute(DefaultRepositoryProvider.ATTRIBUTE_NAME, repositoryService.getRepository()); HttpScmProtocol protocol = repositoryService.getProtocol(HttpScmProtocol.class); protocol.serve(req, resp, getServletConfig()); } catch (NotFoundException e) { diff --git a/scm-webapp/src/main/java/sonia/scm/web/protocol/NamespaceAndNameFromPathExtractor.java b/scm-webapp/src/main/java/sonia/scm/web/protocol/NamespaceAndNameFromPathExtractor.java index 22e2433561..2c1eeb1b90 100644 --- a/scm-webapp/src/main/java/sonia/scm/web/protocol/NamespaceAndNameFromPathExtractor.java +++ b/scm-webapp/src/main/java/sonia/scm/web/protocol/NamespaceAndNameFromPathExtractor.java @@ -1,18 +1,31 @@ package sonia.scm.web.protocol; +import sonia.scm.Type; import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.RepositoryManager; import sonia.scm.util.HttpUtil; +import javax.inject.Inject; import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; import static java.util.Optional.empty; import static java.util.Optional.of; final class NamespaceAndNameFromPathExtractor { - private NamespaceAndNameFromPathExtractor() {} + private final Set<String> types; - static Optional<NamespaceAndName> fromUri(String uri) { + @Inject + public NamespaceAndNameFromPathExtractor(RepositoryManager repositoryManager) { + this.types = repositoryManager.getConfiguredTypes() + .stream() + .map(Type::getName) + .collect(Collectors.toSet()); + } + + Optional<NamespaceAndName> fromUri(String uri) { if (uri.startsWith(HttpUtil.SEPARATOR_PATH)) { uri = uri.substring(1); } @@ -30,12 +43,13 @@ final class NamespaceAndNameFromPathExtractor { } String name = uri.substring(endOfNamespace + 1, nameIndex); - - int nameDotIndex = name.indexOf('.'); + int nameDotIndex = name.lastIndexOf('.'); if (nameDotIndex >= 0) { - return of(new NamespaceAndName(namespace, name.substring(0, nameDotIndex))); - } else { - return of(new NamespaceAndName(namespace, name)); + String suffix = name.substring(nameDotIndex + 1); + if (types.contains(suffix)) { + name = name.substring(0, nameDotIndex); + } } + return of(new NamespaceAndName(namespace, name)); } } diff --git a/scm-webapp/src/main/java/sonia/scm/web/security/DefaultAdministrationContext.java b/scm-webapp/src/main/java/sonia/scm/web/security/DefaultAdministrationContext.java index 0b380c8088..b62b6c63f3 100644 --- a/scm-webapp/src/main/java/sonia/scm/web/security/DefaultAdministrationContext.java +++ b/scm-webapp/src/main/java/sonia/scm/web/security/DefaultAdministrationContext.java @@ -38,7 +38,6 @@ package sonia.scm.web.security; import com.google.inject.Inject; import com.google.inject.Injector; import com.google.inject.Singleton; - import org.apache.shiro.SecurityUtils; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.subject.SimplePrincipalCollection; @@ -46,21 +45,17 @@ import org.apache.shiro.subject.Subject; import org.apache.shiro.subject.support.SubjectThreadState; import org.apache.shiro.util.ThreadContext; import org.apache.shiro.util.ThreadState; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.SCMContext; -import sonia.scm.group.GroupNames; import sonia.scm.security.Role; import sonia.scm.user.User; import sonia.scm.util.AssertUtil; -//~--- JDK imports ------------------------------------------------------------ - +import javax.xml.bind.JAXB; import java.net.URL; -import javax.xml.bind.JAXB; +//~--- JDK imports ------------------------------------------------------------ /** * @@ -161,7 +156,6 @@ public class DefaultAdministrationContext implements AdministrationContext collection.add(adminUser.getId(), REALM); collection.add(adminUser, REALM); - collection.add(new GroupNames(), REALM); collection.add(AdministrationContextMarker.MARKER, REALM); return collection; diff --git a/scm-webapp/src/main/resources/config/ehcache.xml b/scm-webapp/src/main/resources/config/ehcache.xml deleted file mode 100644 index 19abbde727..0000000000 --- a/scm-webapp/src/main/resources/config/ehcache.xml +++ /dev/null @@ -1,271 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - - 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 - - ---> - -<!-- - Document : ehcache.xml - Created on : October 14, 2010, 6:54 AM - Author : sdorra - Description: - Purpose of the document follows. ---> - -<ehcache xmlns="http://ehcache.org/ehcache.xsd" - updateCheck="false" - maxBytesLocalDisk="512M"> - - - <!-- - Sets the path to the directory where cache .data files are created. - - If the path is a Java System Property it is replaced by - its value in the running VM. - - The following properties are translated: - user.home - User's home directory - user.dir - User's current working directory - java.io.tmpdir - Default temp file path - --> - - <diskStore path="java.io.tmpdir"/> - - - <!-- - Default Cache configuration. These will applied to caches programmatically created through - the CacheManager. - - The following attributes are required: - - maxElementsInMemory - Sets the maximum number of objects that will be created in memory - eternal - Sets whether elements are eternal. If eternal, timeouts are ignored and the - element is never expired. - overflowToDisk - Sets whether elements can overflow to disk when the in-memory cache - has reached the maxInMemory limit. - - The following attributes are optional: - timeToIdleSeconds - Sets the time to idle for an element before it expires. - i.e. The maximum amount of time between accesses before an element expires - Is only used if the element is not eternal. - Optional attribute. A value of 0 means that an Element can idle for infinity. - The default value is 0. - timeToLiveSeconds - Sets the time to live for an element before it expires. - i.e. The maximum time between creation time and when an element expires. - Is only used if the element is not eternal. - Optional attribute. A value of 0 means that and Element can live for infinity. - The default value is 0. - diskPersistent - Whether the disk store persists between restarts of the Virtual Machine. - The default value is false. - diskExpiryThreadIntervalSeconds- The number of seconds between runs of the disk expiry thread. The default value - is 120 seconds. - --> - - <defaultCache - maxEntriesLocalHeap="100" - maxEntriesLocalDisk="10000" - eternal="false" - overflowToDisk="true" - timeToIdleSeconds="1200" - timeToLiveSeconds="2400" - diskPersistent="false" - diskExpiryThreadIntervalSeconds="120" - /> - - <!-- - Authentication cache - average: 1K - --> - <cache - name="sonia.cache.auth" - maxEntriesLocalHeap="1000" - eternal="false" - overflowToDisk="false" - timeToIdleSeconds="30" - timeToLiveSeconds="60" - diskPersistent="false" - /> - - <!-- - Authorization cache - average: 3K - --> - <cache - name="sonia.cache.authorizing" - maxEntriesLocalHeap="1000" - eternal="false" - overflowToDisk="false" - timeToIdleSeconds="1200" - timeToLiveSeconds="2400" - diskPersistent="false" - copyOnRead="true" - /> - - <!-- - PluginCenter cache - average: 30K - --> - <cache - name="sonia.cache.plugins" - maxEntriesLocalHeap="5" - eternal="false" - overflowToDisk="false" - timeToLiveSeconds="3600" - diskPersistent="false" - /> - - <!-- - Search cache for users - average: 0.5K - --> - <cache - name="sonia.cache.search.users" - maxEntriesLocalHeap="10000" - eternal="false" - overflowToDisk="false" - timeToLiveSeconds="5400" - diskPersistent="false" - /> - - <!-- - Search cache for groups - average: 0.5K - --> - <cache - name="sonia.cache.search.groups" - maxEntriesLocalHeap="1000" - eternal="false" - overflowToDisk="false" - timeToLiveSeconds="5400" - diskPersistent="false" - /> - - <!-- new repository api --> - - <!-- - Changeset cache - average: 25K - --> - <cache - name="sonia.cache.cmd.log" - maxEntriesLocalHeap="200" - maxEntriesLocalDisk="10000" - eternal="true" - overflowToDisk="true" - diskPersistent="false" - copyOnRead="true" - copyOnWrite="true" - /> - - <!-- - FileObject cache - average: 1.5K - --> - <cache - name="sonia.cache.cmd.browse" - maxEntriesLocalHeap="3000" - maxEntriesLocalDisk="20000" - eternal="true" - overflowToDisk="true" - diskPersistent="false" - copyOnRead="true" - copyOnWrite="true" - /> - - <!-- - BlameResult cache - average: 15K - --> - <cache - name="sonia.cache.cmd.blame" - maxEntriesLocalHeap="1000" - maxEntriesLocalDisk="10000" - eternal="true" - overflowToDisk="true" - diskPersistent="false" - copyOnRead="true" - copyOnWrite="true" - /> - - <!-- - Tag cache - average: 5K - --> - <cache - name="sonia.cache.cmd.tags" - maxEntriesLocalHeap="500" - eternal="true" - overflowToDisk="false" - diskPersistent="false" - /> - - <!-- - Branch cache - average: 2.5K - --> - <cache - name="sonia.cache.cmd.branches" - maxEntriesLocalHeap="1000" - eternal="true" - overflowToDisk="false" - diskPersistent="false" - /> - - <!-- deprecated old repository api --> - - <cache - name="sonia.cache.repository.changesets" - maxEntriesLocalHeap="200" - eternal="false" - overflowToDisk="false" - timeToLiveSeconds="86400" - diskPersistent="false" - /> - - <cache - name="sonia.cache.repository.browser" - maxEntriesLocalHeap="200" - eternal="false" - overflowToDisk="false" - timeToLiveSeconds="86400" - diskPersistent="false" - /> - - <cache - name="sonia.cache.repository.blame" - maxEntriesLocalHeap="100" - eternal="false" - overflowToDisk="false" - timeToLiveSeconds="86400" - diskPersistent="false" - /> - -</ehcache> diff --git a/scm-webapp/src/main/resources/config/gcache.xml b/scm-webapp/src/main/resources/config/gcache.xml index eb59f36446..28876558df 100644 --- a/scm-webapp/src/main/resources/config/gcache.xml +++ b/scm-webapp/src/main/resources/config/gcache.xml @@ -40,15 +40,15 @@ /> <!-- - Authentication cache + External group cache average: 1K --> <cache - name="sonia.cache.auth" + name="sonia.cache.externalGroups" maximumSize="1000" - expireAfterAccess="30" - expireAfterWrite="60" + expireAfterAccess="60" + expireAfterWrite="120" /> <!-- diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeDtoFactoryTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeDtoFactoryTest.java index 8a00c69229..d9572dc04c 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeDtoFactoryTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeDtoFactoryTest.java @@ -1,9 +1,9 @@ package sonia.scm.api.v2.resources; +import com.google.common.collect.ImmutableSet; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.subject.Subject; import org.apache.shiro.util.ThreadContext; -import org.assertj.core.util.Lists; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -12,7 +12,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; -import sonia.scm.group.GroupNames; +import sonia.scm.group.GroupCollector; import sonia.scm.user.User; import sonia.scm.user.UserManager; import sonia.scm.user.UserTestData; @@ -20,7 +20,6 @@ import sonia.scm.user.UserTestData; import java.net.URI; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -33,6 +32,9 @@ class MeDtoFactoryTest { @Mock private UserManager userManager; + @Mock + private GroupCollector groupCollector; + @Mock private Subject subject; @@ -42,7 +44,7 @@ class MeDtoFactoryTest { void setUpContext() { ThreadContext.bind(subject); ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); - meDtoFactory = new MeDtoFactory(resourceLinks, userManager); + meDtoFactory = new MeDtoFactory(resourceLinks, userManager, groupCollector); } @AfterEach @@ -69,24 +71,16 @@ class MeDtoFactoryTest { @Test void shouldCreateMeDtoWithGroups() { - prepareSubject(UserTestData.createTrillian(), "HeartOfGold", "Puzzle42"); + when(groupCollector.collect("trillian")).thenReturn(ImmutableSet.of("HeartOfGold", "Puzzle42")); + prepareSubject(UserTestData.createTrillian()); MeDto dto = meDtoFactory.create(); assertThat(dto.getGroups()).containsOnly("HeartOfGold", "Puzzle42"); } - private void prepareSubject(User user, String... groups) { + private void prepareSubject(User user) { PrincipalCollection collection = mock(PrincipalCollection.class); when(subject.getPrincipals()).thenReturn(collection); - when(collection.oneByType(any(Class.class))).then(ic -> { - Class<?> type = ic.getArgument(0); - if (type.isAssignableFrom(User.class)) { - return user; - } else if (type.isAssignableFrom(GroupNames.class)) { - return new GroupNames(Lists.newArrayList(groups)); - } else { - return null; - } - }); + when(collection.oneByType(User.class)).thenReturn(user); } @Test diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeResourceTest.java index cd2a172c1b..7a3d1b4304 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeResourceTest.java @@ -2,6 +2,7 @@ package sonia.scm.api.v2.resources; import com.github.sdorra.shiro.ShiroRule; import com.github.sdorra.shiro.SubjectAware; +import com.google.common.collect.ImmutableSet; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.credential.PasswordService; import org.apache.shiro.subject.PrincipalCollection; @@ -16,6 +17,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import sonia.scm.ContextEntry; +import sonia.scm.group.GroupCollector; import sonia.scm.user.InvalidPasswordException; import sonia.scm.user.User; import sonia.scm.user.UserManager; @@ -28,7 +30,12 @@ import java.net.URISyntaxException; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; import static sonia.scm.api.v2.resources.DispatcherMock.createDispatcher; @@ -50,6 +57,9 @@ public class MeResourceTest { @Mock private ScmPathInfoStore scmPathInfoStore; + @Mock + private GroupCollector groupCollector; + @Mock private UserManager userManager; @@ -69,6 +79,7 @@ public class MeResourceTest { when(userManager.create(userCaptor.capture())).thenAnswer(invocation -> invocation.getArguments()[0]); doNothing().when(userManager).modify(userCaptor.capture()); doNothing().when(userManager).delete(userCaptor.capture()); + when(groupCollector.collect("trillian")).thenReturn(ImmutableSet.of("group1", "group2")); when(userManager.isTypeDefault(userCaptor.capture())).thenCallRealMethod(); when(userManager.getDefaultType()).thenReturn("xml"); MeResource meResource = new MeResource(meDtoFactory, userManager, passwordService); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MergeResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MergeResourceTest.java index d47ff35e5f..b9e720fc71 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MergeResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MergeResourceTest.java @@ -1,10 +1,14 @@ package sonia.scm.api.v2.resources; +import com.github.sdorra.shiro.SubjectAware; import com.google.common.io.Resources; import com.google.inject.util.Providers; +import org.apache.shiro.subject.Subject; +import org.apache.shiro.util.ThreadContext; import org.jboss.resteasy.core.Dispatcher; import org.jboss.resteasy.mock.MockHttpRequest; import org.jboss.resteasy.mock.MockHttpResponse; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -13,6 +17,7 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Repository; import sonia.scm.repository.api.MergeCommandBuilder; import sonia.scm.repository.api.MergeCommandResult; import sonia.scm.repository.api.MergeDryRunCommandResult; @@ -26,12 +31,20 @@ import java.net.URL; import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.when; +import static sonia.scm.repository.RepositoryTestData.createHeartOfGold; @ExtendWith(MockitoExtension.class) +@SubjectAware( + configuration = "classpath:sonia/scm/shiro-001.ini", + username = "trillian", + password = "secret" +) public class MergeResourceTest extends RepositoryTestBase { public static final String MERGE_URL = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/merge/"; + private Repository repository = createHeartOfGold(); private Dispatcher dispatcher; @Mock @@ -72,10 +85,21 @@ public class MergeResourceTest extends RepositoryTestBase { @Nested class ExecutingMergeCommand { + @Mock + private Subject subject; + @BeforeEach void initRepository() { when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(repositoryService); - when(repositoryService.getMergeCommand()).thenReturn(mergeCommandBuilder); + lenient().when(repositoryService.getMergeCommand()).thenReturn(mergeCommandBuilder); + when(repositoryService.getRepository()).thenReturn(repository); + + ThreadContext.bind(subject); + } + + @AfterEach + void tearDownShiro() { + ThreadContext.unbindSubject(); } @Test @@ -115,6 +139,7 @@ public class MergeResourceTest extends RepositoryTestBase { @Test void shouldHandleSuccessfulDryRun() throws Exception { + when(subject.isPermitted("repository:push:" + repositoryService.getRepository().getId())).thenReturn(true); when(mergeCommand.dryRun(any())).thenReturn(new MergeDryRunCommandResult(true)); URL url = Resources.getResource("sonia/scm/api/v2/mergeCommand.json"); @@ -132,6 +157,7 @@ public class MergeResourceTest extends RepositoryTestBase { @Test void shouldHandleFailedDryRun() throws Exception { + when(subject.isPermitted("repository:push:" + repositoryService.getRepository().getId())).thenReturn(true); when(mergeCommand.dryRun(any())).thenReturn(new MergeDryRunCommandResult(false)); URL url = Resources.getResource("sonia/scm/api/v2/mergeCommand.json"); @@ -146,5 +172,22 @@ public class MergeResourceTest extends RepositoryTestBase { assertThat(response.getStatus()).isEqualTo(409); } + + @Test + void shouldSkipDryRunIfSubjectHasNoPushPermission() throws Exception { + when(subject.isPermitted("repository:push:" + repositoryService.getRepository().getId())).thenReturn(false); + + URL url = Resources.getResource("sonia/scm/api/v2/mergeCommand.json"); + byte[] mergeCommandJson = Resources.toByteArray(url); + + MockHttpRequest request = MockHttpRequest + .post(MERGE_URL + "dry-run/") + .content(mergeCommandJson) + .contentType(VndMediaType.MERGE_COMMAND); + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + + assertThat(response.getStatus()).isEqualTo(204); + } } } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java index 1aef4e57cb..c4c885fa71 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java @@ -50,6 +50,7 @@ public class ResourceLinksMock { when(resourceLinks.repositoryRole()).thenReturn(new ResourceLinks.RepositoryRoleLinks(uriInfo)); when(resourceLinks.repositoryRoleCollection()).thenReturn(new ResourceLinks.RepositoryRoleCollectionLinks(uriInfo)); when(resourceLinks.namespaceStrategies()).thenReturn(new ResourceLinks.NamespaceStrategiesLinks(uriInfo)); + when(resourceLinks.pluginCollection()).thenReturn(new ResourceLinks.PluginCollectionLinks(uriInfo)); return resourceLinks; } diff --git a/scm-webapp/src/test/java/sonia/scm/group/DefaultGroupCollectorTest.java b/scm-webapp/src/test/java/sonia/scm/group/DefaultGroupCollectorTest.java new file mode 100644 index 0000000000..edd23151b9 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/group/DefaultGroupCollectorTest.java @@ -0,0 +1,100 @@ +package sonia.scm.group; + +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.cache.MapCache; +import sonia.scm.cache.MapCacheManager; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class DefaultGroupCollectorTest { + + @Mock + private GroupDAO groupDAO; + + @Mock + private GroupResolver groupResolver; + + private MapCacheManager mapCacheManager; + + private Set<GroupResolver> groupResolvers; + + private DefaultGroupCollector collector; + + @BeforeEach + void initCollector() { + groupResolvers = new HashSet<>(); + mapCacheManager = new MapCacheManager(); + collector = new DefaultGroupCollector(groupDAO, mapCacheManager, groupResolvers); + } + + @Test + void shouldAlwaysReturnAuthenticatedGroup() { + Iterable<String> groupNames = collector.collect("trillian"); + assertThat(groupNames).containsOnly("_authenticated"); + } + + @Test + void shouldReturnGroupsFromCache() { + MapCache<String, Set<String>> cache = mapCacheManager.getCache(DefaultGroupCollector.CACHE_NAME); + cache.put("trillian", ImmutableSet.of("awesome", "incredible")); + + Set<String> groups = collector.collect("trillian"); + assertThat(groups).containsOnly("_authenticated", "awesome", "incredible"); + } + + @Test + void shouldNotCallResolverIfExternalGroupsAreCached() { + groupResolvers.add(groupResolver); + + MapCache<String, Set<String>> cache = mapCacheManager.getCache(DefaultGroupCollector.CACHE_NAME); + cache.put("trillian", ImmutableSet.of("awesome", "incredible")); + + Set<String> groups = collector.collect("trillian"); + assertThat(groups).containsOnly("_authenticated", "awesome", "incredible"); + + verify(groupResolver, never()).resolve("trillian"); + } + + @Nested + class WithGroupsFromDao { + + @BeforeEach + void setUpGroupsDao() { + List<Group> groups = Lists.newArrayList( + new Group("xml", "heartOfGold", "trillian"), + new Group("xml", "g42", "dent", "prefect"), + new Group("xml", "fjordsOfAfrican", "dent", "trillian") + ); + when(groupDAO.getAll()).thenReturn(groups); + } + + @Test + void shouldReturnGroupsFromDao() { + Iterable<String> groupNames = collector.collect("trillian"); + assertThat(groupNames).containsOnly("_authenticated", "heartOfGold", "fjordsOfAfrican"); + } + + @Test + void shouldCombineWithResolvers() { + when(groupResolver.resolve("trillian")).thenReturn(ImmutableSet.of("awesome", "incredible")); + groupResolvers.add(groupResolver); + Iterable<String> groupNames = collector.collect("trillian"); + assertThat(groupNames).containsOnly("_authenticated", "heartOfGold", "fjordsOfAfrican", "awesome", "incredible"); + } + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/security/BearerRealmTest.java b/scm-webapp/src/test/java/sonia/scm/security/BearerRealmTest.java index c2d75358fd..d458f9c72c 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/BearerRealmTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/BearerRealmTest.java @@ -90,12 +90,10 @@ class BearerRealmTest { Set<String> groups = ImmutableSet.of("HeartOfGold", "Puzzle42"); when(accessToken.getSubject()).thenReturn("trillian"); - when(accessToken.getGroups()).thenReturn(groups); when(accessToken.getClaims()).thenReturn(new HashMap<>()); when(accessTokenResolver.resolve(bearerToken)).thenReturn(accessToken); when(realmHelper.authenticationInfoBuilder("trillian")).thenReturn(builder); - when(builder.withGroups(groups)).thenReturn(builder); when(builder.withCredentials("__bearer__")).thenReturn(builder); when(builder.withScope(any(Scope.class))).thenReturn(builder); when(builder.build()).thenReturn(authenticationInfo); diff --git a/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java b/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java index 8e7cb8a70e..930a06d249 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java @@ -33,6 +33,7 @@ package sonia.scm.security; import com.github.sdorra.shiro.ShiroRule; import com.github.sdorra.shiro.SubjectAware; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; @@ -49,7 +50,7 @@ import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.cache.Cache; import sonia.scm.cache.CacheManager; -import sonia.scm.group.GroupNames; +import sonia.scm.group.GroupCollector; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryDAO; import sonia.scm.repository.RepositoryPermission; @@ -58,8 +59,6 @@ import sonia.scm.repository.RepositoryTestData; import sonia.scm.user.User; import sonia.scm.user.UserTestData; -import java.util.Collections; - import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import static org.hamcrest.Matchers.containsInAnyOrder; @@ -96,6 +95,9 @@ public class DefaultAuthorizationCollectorTest { @Mock private RepositoryPermissionProvider repositoryPermissionProvider; + @Mock + private GroupCollector groupCollector; + private DefaultAuthorizationCollector collector; @Rule @@ -107,7 +109,7 @@ public class DefaultAuthorizationCollectorTest { @Before public void setUp(){ when(cacheManager.getCache(Mockito.any(String.class))).thenReturn(cache); - collector = new DefaultAuthorizationCollector(cacheManager, repositoryDAO, securitySystem, repositoryPermissionProvider); + collector = new DefaultAuthorizationCollector(cacheManager, repositoryDAO, securitySystem, repositoryPermissionProvider, groupCollector); } /** @@ -290,9 +292,13 @@ public class DefaultAuthorizationCollectorTest { SimplePrincipalCollection spc = new SimplePrincipalCollection(); spc.add(user.getName(), "unit"); spc.add(user, "unit"); - spc.add(new GroupNames(group, groups), "unit"); Subject subject = new Subject.Builder().authenticated(true).principals(spc).buildSubject(); shiro.setSubject(subject); + + ImmutableSet.Builder<String> builder = ImmutableSet.builder(); + builder.add(group); + builder.add(groups); + when(groupCollector.collect(user.getName())).thenReturn(builder.build()); } /** diff --git a/scm-webapp/src/test/java/sonia/scm/security/DefaultRealmTest.java b/scm-webapp/src/test/java/sonia/scm/security/DefaultRealmTest.java index b6fea9e897..ba23411b36 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/DefaultRealmTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/DefaultRealmTest.java @@ -36,8 +36,6 @@ package sonia.scm.security; //~--- non-JDK imports -------------------------------------------------------- import com.google.common.collect.Collections2; -import com.google.common.collect.Lists; - import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.DisabledAccountException; @@ -45,43 +43,44 @@ import org.apache.shiro.authc.IncorrectCredentialsException; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.authc.credential.DefaultPasswordService; -import org.apache.shiro.crypto.hash.DefaultHashService; -import org.apache.shiro.subject.PrincipalCollection; -import org.apache.shiro.subject.SimplePrincipalCollection; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; - -import sonia.scm.group.Group; -import sonia.scm.group.GroupDAO; -import sonia.scm.group.GroupNames; -import sonia.scm.user.User; -import sonia.scm.user.UserDAO; -import sonia.scm.user.UserTestData; - -import static org.hamcrest.Matchers.*; - -import static org.junit.Assert.*; - -import static org.mockito.Mockito.*; - -//~--- JDK imports ------------------------------------------------------------ - -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.Permission; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.authz.permission.WildcardPermissionResolver; +import org.apache.shiro.crypto.hash.DefaultHashService; +import org.apache.shiro.subject.PrincipalCollection; +import org.apache.shiro.subject.SimplePrincipalCollection; import org.hamcrest.Matchers; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import sonia.scm.group.GroupDAO; +import sonia.scm.user.User; +import sonia.scm.user.UserDAO; +import sonia.scm.user.UserTestData; + +import java.util.HashSet; +import java.util.Set; + +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +//~--- JDK imports ------------------------------------------------------------ /** * @@ -206,32 +205,6 @@ public class DefaultRealmTest ); } - /** - * Method description - * - */ - @Test - public void testGroupCollection() - { - User user = UserTestData.createTrillian(); - //J- - List<Group> groups = Lists.newArrayList( - new Group(DefaultRealm.REALM, "scm", user.getName()), - new Group(DefaultRealm.REALM, "developers", "perfect") - ); - //J+ - - when(groupDAO.getAll()).thenReturn(groups); - - UsernamePasswordToken token = daoUser(user, "secret"); - AuthenticationInfo info = realm.getAuthenticationInfo(token); - GroupNames groupNames = info.getPrincipals().oneByType(GroupNames.class); - - assertNotNull(groupNames); - assertThat(groupNames.getCollection(), hasSize(2)); - assertThat(groupNames, hasItems("scm", GroupNames.AUTHENTICATED)); - } - /** * Method description * @@ -251,12 +224,6 @@ public class DefaultRealmTest assertThat(collection.getRealmNames(), hasSize(1)); assertThat(collection.getRealmNames(), hasItem(DefaultRealm.REALM)); assertEquals(user, collection.oneByType(User.class)); - - GroupNames groups = collection.oneByType(GroupNames.class); - - assertNotNull(groups); - assertThat(groups.getCollection(), hasSize(1)); - assertThat(groups.getCollection(), hasItem(GroupNames.AUTHENTICATED)); } /** diff --git a/scm-webapp/src/test/java/sonia/scm/security/JwtAccessTokenBuilderTest.java b/scm-webapp/src/test/java/sonia/scm/security/JwtAccessTokenBuilderTest.java index e2117235fc..7a8c0ef169 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/JwtAccessTokenBuilderTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/JwtAccessTokenBuilderTest.java @@ -36,27 +36,25 @@ import com.github.sdorra.shiro.SubjectAware; import com.google.common.collect.Sets; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; -import org.apache.shiro.SecurityUtils; -import org.apache.shiro.subject.PrincipalCollection; -import org.apache.shiro.subject.Subject; import org.apache.shiro.util.ThreadContext; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.invocation.InvocationOnMock; import org.mockito.junit.MockitoJUnitRunner; -import sonia.scm.group.ExternalGroupNames; -import java.util.Arrays; -import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; -import static org.hamcrest.Matchers.*; -import static org.junit.Assert.*; -import static org.mockito.Mockito.*; +import static org.hamcrest.Matchers.isEmptyOrNullString; +import static org.hamcrest.Matchers.not; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.when; import static sonia.scm.security.SecureKeyTestUtil.createSecureKey; /** @@ -137,7 +135,6 @@ public class JwtAccessTokenBuilderTest { .issuer("https://www.scm-manager.org") .expiresIn(5, TimeUnit.SECONDS) .custom("a", "b") - .groups("one", "two", "three") .scope(Scope.valueOf("repo:*")) .build(); @@ -154,36 +151,6 @@ public class JwtAccessTokenBuilderTest { assertClaims(new JwtAccessToken(claims, compact)); } - @Test - public void testWithExternalGroups() { - applyExternalGroupsToSubject(true, "external"); - JwtAccessToken token = factory.create().subject("dent").build(); - assertArrayEquals(new String[]{"external"}, token.getCustom(JwtAccessToken.GROUPS_CLAIM_KEY).map(x -> (String[]) x).get()); - } - - @Test - public void testWithInternalGroups() { - applyExternalGroupsToSubject(false, "external"); - JwtAccessToken token = factory.create().subject("dent").build(); - assertFalse(token.getCustom(JwtAccessToken.GROUPS_CLAIM_KEY).isPresent()); - } - - private void applyExternalGroupsToSubject(boolean external, String... groups) { - Subject subject = spy(SecurityUtils.getSubject()); - when(subject.getPrincipals()).thenAnswer(invocation -> enrichWithGroups(invocation, groups, external)); - shiro.setSubject(subject); - } - - private Object enrichWithGroups(InvocationOnMock invocation, String[] groups, boolean external) throws Throwable { - PrincipalCollection principals = (PrincipalCollection) spy(invocation.callRealMethod()); - - List<String> groupCollection = Arrays.asList(groups); - if (external) { - when(principals.oneByType(ExternalGroupNames.class)).thenReturn(new ExternalGroupNames(groupCollection)); - } - return principals; - } - private void assertClaims(JwtAccessToken token){ assertThat(token.getId(), not(isEmptyOrNullString())); assertNotNull( token.getIssuedAt() ); @@ -194,6 +161,5 @@ public class JwtAccessTokenBuilderTest { assertEquals(token.getIssuer().get(), "https://www.scm-manager.org"); assertEquals("b", token.getCustom("a").get()); assertEquals("[\"repo:*\"]", token.getScope().toString()); - assertThat(token.getGroups(), containsInAnyOrder("one", "two", "three")); } } diff --git a/scm-webapp/src/test/java/sonia/scm/update/MigrationWizardServletTest.java b/scm-webapp/src/test/java/sonia/scm/update/MigrationWizardServletTest.java index 325dc2eb1c..683c446af7 100644 --- a/scm-webapp/src/test/java/sonia/scm/update/MigrationWizardServletTest.java +++ b/scm-webapp/src/test/java/sonia/scm/update/MigrationWizardServletTest.java @@ -5,8 +5,8 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.update.repository.DefaultMigrationStrategyDAO; import sonia.scm.update.repository.MigrationStrategy; -import sonia.scm.update.repository.MigrationStrategyDao; import sonia.scm.update.repository.V1Repository; import sonia.scm.update.repository.XmlRepositoryV1UpdateStep; @@ -26,7 +26,7 @@ class MigrationWizardServletTest { @Mock XmlRepositoryV1UpdateStep updateStep; @Mock - MigrationStrategyDao migrationStrategyDao; + DefaultMigrationStrategyDAO migrationStrategyDao; @Mock HttpServletRequest request; @@ -233,6 +233,6 @@ class MigrationWizardServletTest { servlet.doPost(request, response); - verify(migrationStrategyDao).set("id", "name", MigrationStrategy.COPY, "namespace", "name"); + verify(migrationStrategyDao).set("id", "git", "name", MigrationStrategy.COPY, "namespace", "name"); } } diff --git a/scm-webapp/src/test/java/sonia/scm/update/repository/MigrationStrategyDaoTest.java b/scm-webapp/src/test/java/sonia/scm/update/repository/DefaultMigrationStrategyDAOTest.java similarity index 83% rename from scm-webapp/src/test/java/sonia/scm/update/repository/MigrationStrategyDaoTest.java rename to scm-webapp/src/test/java/sonia/scm/update/repository/DefaultMigrationStrategyDAOTest.java index d3b487e916..a97d3b077a 100644 --- a/scm-webapp/src/test/java/sonia/scm/update/repository/MigrationStrategyDaoTest.java +++ b/scm-webapp/src/test/java/sonia/scm/update/repository/DefaultMigrationStrategyDAOTest.java @@ -12,7 +12,6 @@ import sonia.scm.SCMContextProvider; import sonia.scm.store.ConfigurationStoreFactory; import sonia.scm.store.JAXBConfigurationStoreFactory; -import javax.xml.bind.JAXBException; import java.nio.file.Path; import java.util.Optional; @@ -21,7 +20,7 @@ import static sonia.scm.update.repository.MigrationStrategy.INLINE; @ExtendWith(MockitoExtension.class) @ExtendWith(TempDirectory.class) -class MigrationStrategyDaoTest { +class DefaultMigrationStrategyDAOTest { @Mock SCMContextProvider contextProvider; @@ -36,7 +35,7 @@ class MigrationStrategyDaoTest { @Test void shouldReturnEmptyOptionalWhenStoreIsEmpty() { - MigrationStrategyDao dao = new MigrationStrategyDao(storeFactory); + DefaultMigrationStrategyDAO dao = new DefaultMigrationStrategyDAO(storeFactory); Optional<RepositoryMigrationPlan.RepositoryMigrationEntry> entry = dao.get("any"); @@ -45,9 +44,9 @@ class MigrationStrategyDaoTest { @Test void shouldReturnNewValue() { - MigrationStrategyDao dao = new MigrationStrategyDao(storeFactory); + DefaultMigrationStrategyDAO dao = new DefaultMigrationStrategyDAO(storeFactory); - dao.set("id", "originalName", INLINE, "space", "name"); + dao.set("id", "git", "originalName", INLINE, "space", "name"); Optional<RepositoryMigrationPlan.RepositoryMigrationEntry> entry = dao.get("id"); @@ -66,14 +65,14 @@ class MigrationStrategyDaoTest { class WithExistingDatabase { @BeforeEach void initExistingDatabase() { - MigrationStrategyDao dao = new MigrationStrategyDao(storeFactory); + DefaultMigrationStrategyDAO dao = new DefaultMigrationStrategyDAO(storeFactory); - dao.set("id", "originalName", INLINE, "space", "name"); + dao.set("id", "git", "originalName", INLINE, "space", "name"); } @Test void shouldFindExistingValue() { - MigrationStrategyDao dao = new MigrationStrategyDao(storeFactory); + DefaultMigrationStrategyDAO dao = new DefaultMigrationStrategyDAO(storeFactory); Optional<RepositoryMigrationPlan.RepositoryMigrationEntry> entry = dao.get("id"); diff --git a/scm-webapp/src/test/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStepTest.java b/scm-webapp/src/test/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStepTest.java index f4abb32698..b3be09a9a9 100644 --- a/scm-webapp/src/test/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStepTest.java +++ b/scm-webapp/src/test/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStepTest.java @@ -16,8 +16,6 @@ import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryPermission; import sonia.scm.repository.xml.XmlRepositoryDAO; import sonia.scm.store.ConfigurationEntryStore; -import sonia.scm.store.ConfigurationEntryStoreFactory; -import sonia.scm.store.InMemoryConfigurationEntryStore; import sonia.scm.store.InMemoryConfigurationEntryStoreFactory; import sonia.scm.update.UpdateStepTestUtil; @@ -49,7 +47,7 @@ class XmlRepositoryV1UpdateStepTest { @Mock XmlRepositoryDAO repositoryDAO; @Mock - MigrationStrategyDao migrationStrategyDao; + DefaultMigrationStrategyDAO migrationStrategyDao; InMemoryConfigurationEntryStoreFactory configurationEntryStoreFactory = new InMemoryConfigurationEntryStoreFactory(); @@ -91,7 +89,7 @@ class XmlRepositoryV1UpdateStepTest { void createMigrationPlan() { Answer<Object> planAnswer = invocation -> { String id = invocation.getArgument(0).toString(); - return of(new RepositoryMigrationPlan.RepositoryMigrationEntry(id, "originalName", MOVE, "namespace-" + id, "name-" + id)); + return of(new RepositoryMigrationPlan.RepositoryMigrationEntry(id, "git", "originalName", MOVE, "namespace-" + id, "name-" + id)); }; lenient().when(migrationStrategyDao.get("3b91caa5-59c3-448f-920b-769aaa56b761")).thenAnswer(planAnswer); diff --git a/scm-webapp/src/test/java/sonia/scm/update/security/XmlSecurityV1UpdateStepTest.java b/scm-webapp/src/test/java/sonia/scm/update/security/XmlSecurityV1UpdateStepTest.java index d0115e3426..6ab32f3397 100644 --- a/scm-webapp/src/test/java/sonia/scm/update/security/XmlSecurityV1UpdateStepTest.java +++ b/scm-webapp/src/test/java/sonia/scm/update/security/XmlSecurityV1UpdateStepTest.java @@ -11,10 +11,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import sonia.scm.SCMContextProvider; import sonia.scm.security.AssignedPermission; import sonia.scm.store.ConfigurationEntryStore; -import sonia.scm.store.ConfigurationEntryStoreFactory; -import sonia.scm.store.InMemoryConfigurationEntryStore; import sonia.scm.store.InMemoryConfigurationEntryStoreFactory; -import sonia.scm.update.security.XmlSecurityV1UpdateStep; import javax.xml.bind.JAXBException; import java.io.IOException; @@ -81,6 +78,32 @@ class XmlSecurityV1UpdateStepTest { .collect(toList()); assertThat(assignedPermission).contains("admins", "vogons"); } + + } + + @Nested + class WithExistingSecurityXml { + + @BeforeEach + void createSecurityV1XML(@TempDirectory.TempDir Path tempDir) throws IOException { + Path configDir = tempDir.resolve("config"); + Files.createDirectories(configDir); + copyTestDatabaseFile(configDir, "securityV1.xml"); + } + + @Test + void shouldMapV1PermissionsFromSecurityV1XML() throws JAXBException { + updateStep.doUpdate(); + List<String> assignedPermission = + assignedPermissionStore.getAll().values() + .stream() + .filter(a -> a.getPermission().getValue().contains("repository:")) + .map(AssignedPermission::getName) + .collect(toList()); + assertThat(assignedPermission).contains("scmadmin"); + assertThat(assignedPermission).contains("test"); + } + } private void copyTestDatabaseFile(Path configDir, String fileName) throws IOException { diff --git a/scm-webapp/src/test/java/sonia/scm/web/protocol/HttpProtocolServletTest.java b/scm-webapp/src/test/java/sonia/scm/web/protocol/HttpProtocolServletTest.java index 1bd6358c95..59b29c8eda 100644 --- a/scm-webapp/src/test/java/sonia/scm/web/protocol/HttpProtocolServletTest.java +++ b/scm-webapp/src/test/java/sonia/scm/web/protocol/HttpProtocolServletTest.java @@ -1,114 +1,136 @@ package sonia.scm.web.protocol; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; import sonia.scm.NotFoundException; import sonia.scm.PushStateDispatcher; import sonia.scm.repository.DefaultRepositoryProvider; import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryTestData; import sonia.scm.repository.api.RepositoryService; import sonia.scm.repository.api.RepositoryServiceFactory; import sonia.scm.repository.spi.HttpScmProtocol; import sonia.scm.web.UserAgent; import sonia.scm.web.UserAgentParser; -import javax.inject.Provider; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import java.util.Optional; -import static org.mockito.AdditionalMatchers.not; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static org.mockito.MockitoAnnotations.initMocks; - -public class HttpProtocolServletTest { +@ExtendWith(MockitoExtension.class) +class HttpProtocolServletTest { @Mock private RepositoryServiceFactory serviceFactory; + @Mock - private HttpServletRequest httpServletRequest; + private NamespaceAndNameFromPathExtractor extractor; + @Mock private PushStateDispatcher dispatcher; + @Mock private UserAgentParser userAgentParser; - @Mock - private Provider<HttpServletRequest> requestProvider; @InjectMocks private HttpProtocolServlet servlet; @Mock private RepositoryService repositoryService; + @Mock private UserAgent userAgent; @Mock private HttpServletRequest request; + @Mock private HttpServletResponse response; + @Mock private HttpScmProtocol protocol; - @Before - public void init() { - initMocks(this); - when(userAgentParser.parse(request)).thenReturn(userAgent); - when(userAgent.isBrowser()).thenReturn(false); - NamespaceAndName existingRepo = new NamespaceAndName("space", "repo"); - when(serviceFactory.create(not(eq(existingRepo)))).thenThrow(new NotFoundException("Test", "a")); - when(serviceFactory.create(existingRepo)).thenReturn(repositoryService); - when(requestProvider.get()).thenReturn(httpServletRequest); + @Nested + class Browser { + + @BeforeEach + void prepareMocks() { + when(userAgentParser.parse(request)).thenReturn(userAgent); + when(userAgent.isBrowser()).thenReturn(true); + when(request.getRequestURI()).thenReturn("uri"); + } + + @Test + void shouldDispatchBrowserRequests() throws ServletException, IOException { + when(userAgent.isBrowser()).thenReturn(true); + when(request.getRequestURI()).thenReturn("uri"); + + servlet.service(request, response); + + verify(dispatcher).dispatch(request, response, "uri"); + } + } - @Test - public void shouldDispatchBrowserRequests() throws ServletException, IOException { - when(userAgent.isBrowser()).thenReturn(true); - when(request.getRequestURI()).thenReturn("uri"); + @Nested + class ScmClient { - servlet.service(request, response); + @BeforeEach + void prepareMocks() { + when(userAgentParser.parse(request)).thenReturn(userAgent); + when(userAgent.isBrowser()).thenReturn(false); + } - verify(dispatcher).dispatch(request, response, "uri"); - } + @Test + void shouldHandleBadPaths() throws IOException, ServletException { + when(request.getPathInfo()).thenReturn("/illegal"); - @Test - public void shouldHandleBadPaths() throws IOException, ServletException { - when(request.getPathInfo()).thenReturn("/illegal"); + servlet.service(request, response); - servlet.service(request, response); + verify(response).setStatus(400); + } - verify(response).setStatus(400); - } + @Test + void shouldHandleNotExistingRepository() throws IOException, ServletException { + when(request.getPathInfo()).thenReturn("/not/exists"); - @Test - public void shouldHandleNotExistingRepository() throws IOException, ServletException { - when(request.getPathInfo()).thenReturn("/not/exists"); + NamespaceAndName repo = new NamespaceAndName("not", "exists"); + when(extractor.fromUri("/not/exists")).thenReturn(Optional.of(repo)); + when(serviceFactory.create(repo)).thenThrow(new NotFoundException("Test", "a")); - servlet.service(request, response); + servlet.service(request, response); - verify(response).setStatus(404); - } + verify(response).setStatus(404); + } - @Test - public void shouldDelegateToProvider() throws IOException, ServletException { - when(request.getPathInfo()).thenReturn("/space/name"); - NamespaceAndName namespaceAndName = new NamespaceAndName("space", "name"); - doReturn(repositoryService).when(serviceFactory).create(namespaceAndName); - Repository repository = new Repository(); - when(repositoryService.getRepository()).thenReturn(repository); - when(repositoryService.getProtocol(HttpScmProtocol.class)).thenReturn(protocol); + @Test + void shouldDelegateToProvider() throws IOException, ServletException { + NamespaceAndName repo = new NamespaceAndName("space", "name"); + when(extractor.fromUri("/space/name")).thenReturn(Optional.of(repo)); + when(serviceFactory.create(repo)).thenReturn(repositoryService); - servlet.service(request, response); + when(request.getPathInfo()).thenReturn("/space/name"); + Repository repository = RepositoryTestData.createHeartOfGold(); + when(repositoryService.getRepository()).thenReturn(repository); + when(repositoryService.getProtocol(HttpScmProtocol.class)).thenReturn(protocol); + + servlet.service(request, response); + + verify(request).setAttribute(DefaultRepositoryProvider.ATTRIBUTE_NAME, repository); + verify(protocol).serve(request, response, null); + verify(repositoryService).close(); + } - verify(httpServletRequest).setAttribute(DefaultRepositoryProvider.ATTRIBUTE_NAME, repository); - verify(protocol).serve(request, response, null); - verify(repositoryService).close(); } } diff --git a/scm-webapp/src/test/java/sonia/scm/web/protocol/NamespaceAndNameFromPathExtractorTest.java b/scm-webapp/src/test/java/sonia/scm/web/protocol/NamespaceAndNameFromPathExtractorTest.java index 0998010069..5481e2e2a5 100644 --- a/scm-webapp/src/test/java/sonia/scm/web/protocol/NamespaceAndNameFromPathExtractorTest.java +++ b/scm-webapp/src/test/java/sonia/scm/web/protocol/NamespaceAndNameFromPathExtractorTest.java @@ -1,17 +1,46 @@ package sonia.scm.web.protocol; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DynamicNode; import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestFactory; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.RepositoryManager; +import sonia.scm.repository.RepositoryType; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.Optional; import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.DynamicTest.dynamicTest; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class NamespaceAndNameFromPathExtractorTest { + + @Mock + private RepositoryManager repositoryManager; + + private NamespaceAndNameFromPathExtractor extractor; + + @BeforeEach + void setUpObjectUnderTest() { + List<RepositoryType> types = Arrays.asList( + new RepositoryType("git", "Git", Collections.emptySet()), + new RepositoryType("hg", "Mercurial", Collections.emptySet()), + new RepositoryType("svn", "Subversion", Collections.emptySet()) + ); + when(repositoryManager.getConfiguredTypes()).thenReturn(types); + extractor = new NamespaceAndNameFromPathExtractor(repositoryManager); + } -public class NamespaceAndNameFromPathExtractorTest { @TestFactory Stream<DynamicNode> shouldExtractCorrectNamespaceAndName() { return Stream.of( @@ -26,21 +55,26 @@ public class NamespaceAndNameFromPathExtractorTest { } @TestFactory - Stream<DynamicNode> shouldHandleTrailingDotSomethings() { + Stream<DynamicNode> shouldHandleTypeSuffix() { return Stream.of( "/space/repo.git", - "/space/repo.and.more", - "/space/repo." + "/space/repo.hg", + "/space/repo.svn", + "/space/repo" ).map(this::createCorrectTest); } private DynamicTest createCorrectTest(String path) { + return createCorrectTest(path, new NamespaceAndName("space", "repo")); + } + + private DynamicTest createCorrectTest(String path, NamespaceAndName expected) { return dynamicTest( "should extract correct namespace and name for path " + path, () -> { - Optional<NamespaceAndName> namespaceAndName = NamespaceAndNameFromPathExtractor.fromUri(path); + Optional<NamespaceAndName> namespaceAndName = extractor.fromUri(path); - assertThat(namespaceAndName.get()).isEqualTo(new NamespaceAndName("space", "repo")); + assertThat(namespaceAndName.get()).isEqualTo(expected); } ); } @@ -59,10 +93,26 @@ public class NamespaceAndNameFromPathExtractorTest { return dynamicTest( "should not fail for wrong path " + path, () -> { - Optional<NamespaceAndName> namespaceAndName = NamespaceAndNameFromPathExtractor.fromUri(path); + Optional<NamespaceAndName> namespaceAndName = extractor.fromUri(path); assertThat(namespaceAndName.isPresent()).isFalse(); } ); } + + @TestFactory + Stream<DynamicNode> shouldHandleDots() { + return Stream.of( + "/space/repo.with.dots.git", + "/space/repo.with.dots.hg", + "/space/repo.with.dots.svn", + "/space/repo.with.dots" + ).map(path -> createCorrectTest(path, new NamespaceAndName("space", "repo.with.dots"))); + } + + @Test + void shouldNotFailOnEndingDot() { + Optional<NamespaceAndName> namespaceAndName = extractor.fromUri("/space/repo."); + assertThat(namespaceAndName).contains(new NamespaceAndName("space", "repo.")); + } } diff --git a/scm-webapp/src/test/resources/sonia/scm/update/security/securityV1.xml b/scm-webapp/src/test/resources/sonia/scm/update/security/securityV1.xml new file mode 100644 index 0000000000..8de82f88d9 --- /dev/null +++ b/scm-webapp/src/test/resources/sonia/scm/update/security/securityV1.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" ?> +<configuration> + <entry> + <key>4lRWOA7DH1</key> + <value> + <group-permission>false</group-permission> + <name>scmadmin</name> + <permission>repository:*:READ</permission> + </value> + </entry> + <entry> + <key>CfRWOAANM2</key> + <value> + <group-permission>true</group-permission> + <name>test</name> + <permission>repository:*:OWNER</permission> + </value> + </entry> + <entry> + <key>CfRWOAANM2</key> + <value> + <group-permission>true</group-permission> + <name>test</name> + <permission>invalid:permission</permission> + </value> + </entry> + <entry> + <key>CfRWOAANM2</key> + <value> + <group-permission>true</group-permission> + <name>test</name> + <permission>repository:*:STRANGE</permission> + </value> + </entry> +</configuration>