Merge branch 'develop' into bugfix/markdown_view_anchor_links

This commit is contained in:
Sebastian Sdorra
2020-08-13 10:12:46 +02:00
committed by GitHub
259 changed files with 14716 additions and 1557 deletions

View File

@@ -5,14 +5,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased ## Unreleased
### Added ### Added
- Introduced merge detection for receive hooks ([#1278](https://github.com/scm-manager/scm-manager/pull/1278)) - Introduced merge detection for receive hooks ([#1278](https://github.com/scm-manager/scm-manager/pull/1278))
- Anonymous mode for the web ui ([#1284](https://github.com/scm-manager/scm-manager/pull/1284))
- Add link to source file in diff sections ([#1267](https://github.com/scm-manager/scm-manager/pull/1267))
- Check versions of plugin dependencies on plugin installation ([#1283](https://github.com/scm-manager/scm-manager/pull/1283)) - Check versions of plugin dependencies on plugin installation ([#1283](https://github.com/scm-manager/scm-manager/pull/1283))
- Sign PR merges and commits performed through ui with generated private key ([#1285](https://github.com/scm-manager/scm-manager/pull/1285))
- Add generic popover component to ui-components ([#1285](https://github.com/scm-manager/scm-manager/pull/1285))
- Show changeset signatures in ui and add public keys ([#1273](https://github.com/scm-manager/scm-manager/pull/1273))
### Fixed ### Fixed
- Repository names may not end with ".git" ([#1277](https://github.com/scm-manager/scm-manager/pull/1277)) - Repository names may not end with ".git" ([#1277](https://github.com/scm-manager/scm-manager/pull/1277))
- Add preselected value to options in dropdown component if missing ([#1287](https://github.com/scm-manager/scm-manager/pull/1287)) - Add preselected value to options in dropdown component if missing ([#1287](https://github.com/scm-manager/scm-manager/pull/1287))
- Show error message if plugin loading failed ([#1289](https://github.com/scm-manager/scm-manager/pull/1289))
- Fix timing problem with anchor links for markdown view ([#1290](https://github.com/scm-manager/scm-manager/pull/1290)) - Fix timing problem with anchor links for markdown view ([#1290](https://github.com/scm-manager/scm-manager/pull/1290))
## [2.3.1] - 2020-08-04 ## [2.3.1] - 2020-08-04
@@ -20,13 +25,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- New api to resolve SCM-Manager root url ([#1276](https://github.com/scm-manager/scm-manager/pull/1276)) - New api to resolve SCM-Manager root url ([#1276](https://github.com/scm-manager/scm-manager/pull/1276))
### Changed ### Changed
- Help tooltips are now mutliline by default ([#1271](https://github.com/scm-manager/scm-manager/pull/1271)) - Help tooltips are now multiline by default ([#1271](https://github.com/scm-manager/scm-manager/pull/1271))
### Fixed ### Fixed
- Fixed unnecessary horizontal scrollbar in modal dialogs ([#1271](https://github.com/scm-manager/scm-manager/pull/1271)) - Fixed unnecessary horizontal scrollbar in modal dialogs ([#1271](https://github.com/scm-manager/scm-manager/pull/1271))
- Avoid stacktrace logging when protocol url is accessed outside of request scope ([#1276](https://github.com/scm-manager/scm-manager/pull/1276)) - Avoid stacktrace logging when protocol url is accessed outside of request scope ([#1276](https://github.com/scm-manager/scm-manager/pull/1276))
## [2.3.0] - 2020-07-23 ## [2.3.0] - 2020-07-23
### Added ### Added
- Add branch link provider to access branch links in plugins ([#1243](https://github.com/scm-manager/scm-manager/pull/1243)) - Add branch link provider to access branch links in plugins ([#1243](https://github.com/scm-manager/scm-manager/pull/1243))
- Add key value input field component ([#1246](https://github.com/scm-manager/scm-manager/pull/1246)) - Add key value input field component ([#1246](https://github.com/scm-manager/scm-manager/pull/1246))

Binary file not shown.

Before

Width:  |  Height:  |  Size: 188 KiB

After

Width:  |  Height:  |  Size: 177 KiB

View File

@@ -26,8 +26,9 @@ Um Angriffe auf den SCM-Manager mit Cross Site Scripting (XSS / XSRF) zu erschwe
#### Plugin-Center-URL #### Plugin-Center-URL
Der SCM-Manager kann ein Plugin-Center anbinden, um schnell und bequem Plugins verwalten zu können. Um ein anderes SCM-Plugin-Center als das vorkonfigurierte zu verwenden, reicht es aus diese URL zu ändern. Läuft der SCM-Manager im Cloudogu EcoSystem kann die Plugin Center URL über einen Eintrag im etcd gesetzt werden. Der SCM-Manager kann ein Plugin-Center anbinden, um schnell und bequem Plugins verwalten zu können. Um ein anderes SCM-Plugin-Center als das vorkonfigurierte zu verwenden, reicht es aus diese URL zu ändern. Läuft der SCM-Manager im Cloudogu EcoSystem kann die Plugin Center URL über einen Eintrag im etcd gesetzt werden.
#### Anonyme Zugriff erlauben #### Anonyme Zugriff
Der SCM-Manager 2 hat das Konzept für anonyme Zugriffe über einen "_anonymous"-Benutzer realisiert. Beim Aktivieren des anonymen Zugriffs wird ein neuer Benutzer erstellt mit dem Namen "_anonymous". Dieser Nutzer kann wie ein gewöhnlicher Benutzer für unterschiedliche Aktionen berechtigt werden. Bei einem Zugriff auf den SCM-Manager ohne Zugangsdaten (gilt nicht für die Web-Oberflächen) wird dieser anonyme Benutzer verwendet. Der SCM-Manager 2 hat das Konzept für anonyme Zugriffe über einen "_anonymous"-Benutzer realisiert. Beim Aktivieren des anonymen Zugriffs wird ein neuer Benutzer erstellt mit dem Namen "_anonymous". Dieser Nutzer kann wie ein gewöhnlicher Benutzer für unterschiedliche Aktionen berechtigt werden. Bei einem Zugriff auf den SCM-Manager ohne Zugangsdaten wird dieser anonyme Benutzer verwendet.
Ist der anonyme Zugriff nur für Protokoll aktiviert, können die REST API und die VCS Protokolle anonym genutzt werden. Wurde der anonyme Zugriff vollständig aktiviert, ist auch ein Zugriff über den Webclient anonym möglich.
Beispiel: Falls der anonyme Zugriff aktiviert ist und der "_anonymous"-Benutzer volle Zugriffsrechte auf ein bestimmtes Git-Repository hat, kann jeder über eine Kommandozeile mit den klassischen Git-Befehlen ohne Zugangsdaten auf dieses Repository zugreifen. Zugriffe über SSH werden aktuell nicht unterstützt. Beispiel: Falls der anonyme Zugriff aktiviert ist und der "_anonymous"-Benutzer volle Zugriffsrechte auf ein bestimmtes Git-Repository hat, kann jeder über eine Kommandozeile mit den klassischen Git-Befehlen ohne Zugangsdaten auf dieses Repository zugreifen. Zugriffe über SSH werden aktuell nicht unterstützt.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 KiB

After

Width:  |  Height:  |  Size: 206 KiB

View File

@@ -17,6 +17,8 @@ Die Übersicht der Changesets/Commits zeigt die Änderungshistorie je Branch an.
Über den Details-Button kann man sich den Inhalt / die Änderungen dieses Changesets ansehen. Über den Details-Button kann man sich den Inhalt / die Änderungen dieses Changesets ansehen.
Der Schlüssel Icon zeigt an, ob ein Changeset signiert wurde. Um die Signatur zu validieren, können die Benutzer ihre öffentlichen Schlüssel (Public Keys) im SCM-Manager hinterlegen. Ein grüner Schlüssel bedeutet die Signatur konnte erfolgreich gegen einen hinterlegten öffentlichen Schlüssel im SCM-Manager verifiziert werden. Ein grauer Schlüssel heißt, dass die Signatur zu keinem Schlüssel im SCM-Manager passt. Und ein roter Schlüssel warnt vor einer ungültigen (möglicherweise gefälschten) Signatur.
Über den Sources-Button gelangt man zur Sources-Übersicht und es wird der Datenstand zum Zeitpunkt nach diesem Commit angezeigt. Über den Sources-Button gelangt man zur Sources-Übersicht und es wird der Datenstand zum Zeitpunkt nach diesem Commit angezeigt.
![Repository-Code-Changesets](assets/repository-code-changesetsView.png) ![Repository-Code-Changesets](assets/repository-code-changesetsView.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

View File

@@ -19,3 +19,8 @@ Hier werden die globalen (nicht-Repository-bezogenen) Berechtigungen für einen
Für die einzelnen Rechte sind Tooltips verfügbar, welche Auskunft über die Auswirkungen der jeweiligen Berechtigung geben. Für die einzelnen Rechte sind Tooltips verfügbar, welche Auskunft über die Auswirkungen der jeweiligen Berechtigung geben.
![Benutzer Berechtigungen](assets/user-settings-permissions.png) ![Benutzer Berechtigungen](assets/user-settings-permissions.png)
### Öffentliche Schlüssel
Es können öffentliche Schlüssel (Public Keys) zu Benutzern hinzugefügt werden, um die Changeset Signaturen damit zu verifizieren.
![Öffentliche Schlüssel](assets/user-settings-publickeys.png)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 182 KiB

After

Width:  |  Height:  |  Size: 300 KiB

View File

@@ -26,8 +26,9 @@ Activate this option to make attacks using cross site scripting (XSS / XSRF) on
#### Plugin Center URL #### Plugin Center URL
A plugin center can be used to conveniently manage plugins. If you want to use a plugin center that is not the default one, you only have to change this URL. If SCM-Manager is operated as part of a Cloudogu EcoSystem, the plugin center URL can be changed in the etcd. A plugin center can be used to conveniently manage plugins. If you want to use a plugin center that is not the default one, you only have to change this URL. If SCM-Manager is operated as part of a Cloudogu EcoSystem, the plugin center URL can be changed in the etcd.
#### Enable Anonymous Access #### Anonymous Access
In SCM-Manager 2 the access for anonymous access is realized by using an "_anonymous" user. When the feature is activated, a new user with the name "_anonymous" is created. This user can be authorized just like any other user. This user is used for access to SCM-Manager without login credentials (this does not apply to access via web UI). In SCM-Manager 2 the access for anonymous access is realized by using an "_anonymous" user. When the feature is activated, a new user with the name "_anonymous" is created. This user can be authorized just like any other user. This user is used for access to SCM-Manager without login credentials.
If the anonymous mode is protocol only you may access the SCM-Manager via the REST API and VCS protocols. With fully enabled anonymous access you can also use the webclient without credentials.
Example: If anonymous access is enabled and the "_anonymous" user has full access on a certain Git repository, everybody can access this repository via command line and the classic Git commands without any login credentials. Access via SSH is not supported at this time. Example: If anonymous access is enabled and the "_anonymous" user has full access on a certain Git repository, everybody can access this repository via command line and the classic Git commands without any login credentials. Access via SSH is not supported at this time.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 205 KiB

After

Width:  |  Height:  |  Size: 354 KiB

View File

@@ -17,6 +17,8 @@ The changesets/commits overview shows the change history of the branch. Each ent
The Details button leads to the content/changes of a changeset. The Details button leads to the content/changes of a changeset.
The key icon shows if the changeset was signed. The users can add their public keys to SCM-Manager for signature verification. The green key means that the signature could be verified successfully against an existing public key. The grey key shows that no matching key could be found for the signature. The red key warns you about an invalid (possible faked) signature.
The Sources button leads to the sources overview that shows the state from after this commit. The Sources button leads to the sources overview that shows the state from after this commit.
![Repository-Code-Changesets](assets/repository-code-changesetsView.png) ![Repository-Code-Changesets](assets/repository-code-changesetsView.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

View File

@@ -19,3 +19,8 @@ In the permissions section, the global, therefore not repository-specific permis
There is a tooltip for each permission that provide some more details about the option. There is a tooltip for each permission that provide some more details about the option.
![User Permissions](assets/user-settings-permissions.png) ![User Permissions](assets/user-settings-permissions.png)
### Public keys
Add public keys to users to enable changeset signature verification.
![Public keys](assets/user-settings-publickeys.png)

25
pom.xml
View File

@@ -525,6 +525,26 @@
<version>1.14</version> <version>1.14</version>
</dependency> </dependency>
<!-- gpg -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpg-jdk15on</artifactId>
<version>${bouncycastle.version}</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>${bouncycastle.version}</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>${bouncycastle.version}</version>
</dependency>
</dependencies> </dependencies>
</dependencyManagement> </dependencyManagement>
@@ -595,7 +615,7 @@
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId> <artifactId>maven-enforcer-plugin</artifactId>
<version>3.0.0-M1</version> <version>3.0.0-M3</version>
<executions> <executions>
<execution> <execution>
<id>enforce-java</id> <id>enforce-java</id>
@@ -639,7 +659,7 @@
<dependency> <dependency>
<groupId>org.codehaus.mojo</groupId> <groupId>org.codehaus.mojo</groupId>
<artifactId>extra-enforcer-rules</artifactId> <artifactId>extra-enforcer-rules</artifactId>
<version>1.0-beta-7</version> <version>1.3</version>
</dependency> </dependency>
</dependencies> </dependencies>
</plugin> </plugin>
@@ -899,6 +919,7 @@
<guice.version>4.2.3</guice.version> <guice.version>4.2.3</guice.version>
<jaxb.version>2.3.3</jaxb.version> <jaxb.version>2.3.3</jaxb.version>
<hibernate-validator.version>6.1.5.Final</hibernate-validator.version> <hibernate-validator.version>6.1.5.Final</hibernate-validator.version>
<bouncycastle.version>1.65</bouncycastle.version>
<!-- event bus --> <!-- event bus -->
<legman.version>1.6.2</legman.version> <legman.version>1.6.2</legman.version>

View File

@@ -112,6 +112,12 @@
<version>${guice.version}</version> <version>${guice.version}</version>
</dependency> </dependency>
<dependency>
<groupId>com.google.inject.extensions</groupId>
<artifactId>guice-assistedinject</artifactId>
<version>${guice.version}</version>
</dependency>
<!-- rest api --> <!-- rest api -->
<dependency> <dependency>

View File

@@ -61,6 +61,8 @@ public class ChangesetDto extends HalRepresentation {
private List<ContributorDto> contributors; private List<ContributorDto> contributors;
private List<SignatureDto> signatures;
public ChangesetDto(Links links, Embedded embedded) { public ChangesetDto(Links links, Embedded embedded) {
super(links, embedded); super(links, embedded);
} }

View File

@@ -0,0 +1,54 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.api.v2.resources;
import de.otto.edison.hal.HalRepresentation;
import de.otto.edison.hal.Links;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import sonia.scm.repository.Person;
import sonia.scm.repository.SignatureStatus;
import java.util.Optional;
import java.util.Set;
@Getter
@Setter
@NoArgsConstructor
@SuppressWarnings("squid:S2160")
public class SignatureDto extends HalRepresentation {
private String keyId;
private String type;
private SignatureStatus status;
private Optional<String> owner;
private Set<Person> contacts;
public SignatureDto(Links links) {
super(links);
}
}

View File

@@ -30,6 +30,7 @@ import com.google.inject.Singleton;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.event.ScmEventBus; import sonia.scm.event.ScmEventBus;
import sonia.scm.security.AnonymousMode;
import sonia.scm.util.HttpUtil; import sonia.scm.util.HttpUtil;
import sonia.scm.xml.XmlSetStringAdapter; import sonia.scm.xml.XmlSetStringAdapter;
@@ -161,7 +162,7 @@ public class ScmConfiguration implements Configuration {
* @see <a href="http://momentjs.com/docs/#/parsing/" target="_blank">http://momentjs.com/docs/#/parsing/</a> * @see <a href="http://momentjs.com/docs/#/parsing/" target="_blank">http://momentjs.com/docs/#/parsing/</a>
*/ */
private String dateFormat = DEFAULT_DATEFORMAT; private String dateFormat = DEFAULT_DATEFORMAT;
private boolean anonymousAccessEnabled = false; private AnonymousMode anonymousMode = AnonymousMode.OFF;
/** /**
* Enables xsrf cookie protection. * Enables xsrf cookie protection.
@@ -200,7 +201,7 @@ public class ScmConfiguration implements Configuration {
this.realmDescription = other.realmDescription; this.realmDescription = other.realmDescription;
this.dateFormat = other.dateFormat; this.dateFormat = other.dateFormat;
this.pluginUrl = other.pluginUrl; this.pluginUrl = other.pluginUrl;
this.anonymousAccessEnabled = other.anonymousAccessEnabled; this.anonymousMode = other.anonymousMode;
this.enableProxy = other.enableProxy; this.enableProxy = other.enableProxy;
this.proxyPort = other.proxyPort; this.proxyPort = other.proxyPort;
this.proxyServer = other.proxyServer; this.proxyServer = other.proxyServer;
@@ -311,8 +312,24 @@ public class ScmConfiguration implements Configuration {
return realmDescription; return realmDescription;
} }
/**
* Returns the currently enabled type of anonymous mode.
*
* @return anonymous mode
* @since 2.4.0
*/
public AnonymousMode getAnonymousMode() {
return anonymousMode;
}
/**
* Returns {@code true} if anonymous mode is enabled.
* @return {@code true} if anonymous mode is enabled
* @deprecated since 2.4.0 use {@link ScmConfiguration#getAnonymousMode} instead
*/
@Deprecated
public boolean isAnonymousAccessEnabled() { public boolean isAnonymousAccessEnabled() {
return anonymousAccessEnabled; return anonymousMode != AnonymousMode.OFF;
} }
public boolean isDisableGroupingGrid() { public boolean isDisableGroupingGrid() {
@@ -360,8 +377,28 @@ public class ScmConfiguration implements Configuration {
return skipFailedAuthenticators; return skipFailedAuthenticators;
} }
/**
* Enables the anonymous access at protocol level.
* @param anonymousAccessEnabled enable or disables the anonymous access
* @deprecated since 2.4.0 use {@link ScmConfiguration#setAnonymousMode(AnonymousMode)} instead
*/
@Deprecated
public void setAnonymousAccessEnabled(boolean anonymousAccessEnabled) { public void setAnonymousAccessEnabled(boolean anonymousAccessEnabled) {
this.anonymousAccessEnabled = anonymousAccessEnabled; if (anonymousAccessEnabled) {
this.anonymousMode = AnonymousMode.PROTOCOL_ONLY;
} else {
this.anonymousMode = AnonymousMode.OFF;
}
}
/**
* Configures the anonymous mode.
* @param mode type of anonymous mode
*
* @since 2.4.0
*/
public void setAnonymousMode(AnonymousMode mode) {
this.anonymousMode = mode;
} }
public void setBaseUrl(String baseUrl) { public void setBaseUrl(String baseUrl) {

View File

@@ -29,6 +29,7 @@ import com.google.inject.Inject;
import sonia.scm.EagerSingleton; import sonia.scm.EagerSingleton;
import sonia.scm.SCMContext; import sonia.scm.SCMContext;
import sonia.scm.plugin.Extension; import sonia.scm.plugin.Extension;
import sonia.scm.security.AnonymousMode;
import sonia.scm.user.UserManager; import sonia.scm.user.UserManager;
@Extension @Extension
@@ -48,7 +49,7 @@ public class ScmConfigurationChangedListener {
} }
private void createAnonymousUserIfRequired(ScmConfigurationChangedEvent event) { private void createAnonymousUserIfRequired(ScmConfigurationChangedEvent event) {
if (event.getConfiguration().isAnonymousAccessEnabled() && !userManager.contains(SCMContext.USER_ANONYMOUS)) { if (event.getConfiguration().getAnonymousMode() != AnonymousMode.OFF && !userManager.contains(SCMContext.USER_ANONYMOUS)) {
userManager.create(SCMContext.ANONYMOUS); userManager.create(SCMContext.ANONYMOUS);
} }
} }

View File

@@ -32,6 +32,7 @@ import sonia.scm.util.ValidationUtil;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
@@ -85,6 +86,8 @@ public class Changeset extends BasicPropertiesAware implements ModelObject {
*/ */
private Collection<Contributor> contributors; private Collection<Contributor> contributors;
private List<Signature> signatures = new ArrayList<>();
public Changeset() {} public Changeset() {}
public Changeset(String id, Long date, Person author) public Changeset(String id, Long date, Person author)
@@ -348,4 +351,31 @@ public class Changeset extends BasicPropertiesAware implements ModelObject {
this.contributors.addAll(contributors); this.contributors.addAll(contributors);
} }
} }
/**
* Sets a collection of signatures which belong to this changeset.
* @param signatures collection of signatures
* @since 2.4.0
*/
public void setSignatures(Collection<Signature> signatures) {
this.signatures = new ArrayList<>(signatures);
}
/**
* Returns a immutable list of signatures.
* @return signatures
* @since 2.4.0
*/
public List<Signature> getSignatures() {
return Collections.unmodifiableList(signatures);
}
/**
* Adds a signature to the list of signatures.
* @param signature
* @since 2.4.0
*/
public void addSignature(Signature signature) {
signatures.add(signature);
}
} }

View File

@@ -0,0 +1,53 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
import lombok.Value;
import java.io.Serializable;
import java.util.Optional;
import java.util.Set;
/**
* Signature is the output of a signature verification.
*
* @since 2.4.0
*/
@Value
public class Signature implements Serializable {
private static final long serialVersionUID = 1L;
private final String keyId;
private final String type;
private final SignatureStatus status;
private final String owner;
private final Set<Person> contacts;
public Optional<String> getOwner() {
return Optional.ofNullable(owner);
}
}

View File

@@ -0,0 +1,32 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
/**
* @since 2.4.0
*/
public enum SignatureStatus {
VERIFIED, NOT_FOUND, INVALID;
}

View File

@@ -137,6 +137,16 @@ public class MergeCommandBuilder {
return this; return this;
} }
/**
* Disables adding a verifiable signature to the merge commit.
* @return This builder instance.
* @since 2.4.0
*/
public MergeCommandBuilder disableSigning() {
request.setSign(false);
return this;
}
/** /**
* Use this to set the strategy of the merge commit manually. * Use this to set the strategy of the merge commit manually.
* *

View File

@@ -164,6 +164,16 @@ public class ModifyCommandBuilder {
return this; return this;
} }
/**
* Disables adding a verifiable signature to the modification commit.
* @return This builder instance.
* @since 2.4.0
*/
public ModifyCommandBuilder disableSigning() {
request.setSign(false);
return this;
}
/** /**
* Set the expected revision of the branch, before the changes are applied. If the branch does not have the * Set the expected revision of the branch, before the changes are applied. If the branch does not have the
* expected revision, a concurrent modification exception will be thrown when the command is executed and no * expected revision, a concurrent modification exception will be thrown when the command is executed and no

View File

@@ -55,6 +55,8 @@ import sonia.scm.repository.RepositoryPermissions;
import sonia.scm.repository.spi.RepositoryServiceProvider; import sonia.scm.repository.spi.RepositoryServiceProvider;
import sonia.scm.repository.spi.RepositoryServiceResolver; import sonia.scm.repository.spi.RepositoryServiceResolver;
import sonia.scm.repository.work.WorkdirProvider; import sonia.scm.repository.work.WorkdirProvider;
import sonia.scm.security.PublicKeyCreatedEvent;
import sonia.scm.security.PublicKeyDeletedEvent;
import sonia.scm.security.ScmSecurityException; import sonia.scm.security.ScmSecurityException;
import java.util.Set; import java.util.Set;
@@ -100,14 +102,12 @@ import static sonia.scm.NotFoundException.notFound;
* </code></pre> * </code></pre>
* *
* @author Sebastian Sdorra * @author Sebastian Sdorra
* @since 1.17
*
* @apiviz.landmark * @apiviz.landmark
* @apiviz.uses sonia.scm.repository.api.RepositoryService * @apiviz.uses sonia.scm.repository.api.RepositoryService
* @since 1.17
*/ */
@Singleton @Singleton
public final class RepositoryServiceFactory public final class RepositoryServiceFactory {
{
/** /**
* the logger for RepositoryServiceFactory * the logger for RepositoryServiceFactory
@@ -122,7 +122,6 @@ public final class RepositoryServiceFactory
* should not be called manually, it should only be used by the injection * should not be called manually, it should only be used by the injection
* container. * container.
* *
*
* @param configuration configuration * @param configuration configuration
* @param cacheManager cache manager * @param cacheManager cache manager
* @param repositoryManager manager for repositories * @param repositoryManager manager for repositories
@@ -136,8 +135,7 @@ public final class RepositoryServiceFactory
public RepositoryServiceFactory(ScmConfiguration configuration, public RepositoryServiceFactory(ScmConfiguration configuration,
CacheManager cacheManager, RepositoryManager repositoryManager, CacheManager cacheManager, RepositoryManager repositoryManager,
Set<RepositoryServiceResolver> resolvers, PreProcessorUtil preProcessorUtil, Set<RepositoryServiceResolver> resolvers, PreProcessorUtil preProcessorUtil,
Set<ScmProtocolProvider> protocolProviders, WorkdirProvider workdirProvider) @SuppressWarnings("rawtypes") Set<ScmProtocolProvider> protocolProviders, WorkdirProvider workdirProvider) {
{
this( this(
configuration, cacheManager, repositoryManager, resolvers, configuration, cacheManager, repositoryManager, resolvers,
preProcessorUtil, protocolProviders, workdirProvider, ScmEventBus.getInstance() preProcessorUtil, protocolProviders, workdirProvider, ScmEventBus.getInstance()
@@ -149,8 +147,7 @@ public final class RepositoryServiceFactory
CacheManager cacheManager, RepositoryManager repositoryManager, CacheManager cacheManager, RepositoryManager repositoryManager,
Set<RepositoryServiceResolver> resolvers, PreProcessorUtil preProcessorUtil, Set<RepositoryServiceResolver> resolvers, PreProcessorUtil preProcessorUtil,
Set<ScmProtocolProvider> protocolProviders, WorkdirProvider workdirProvider, Set<ScmProtocolProvider> protocolProviders, WorkdirProvider workdirProvider,
ScmEventBus eventBus) ScmEventBus eventBus) {
{
this.configuration = configuration; this.configuration = configuration;
this.cacheManager = cacheManager; this.cacheManager = cacheManager;
this.repositoryManager = repositoryManager; this.repositoryManager = repositoryManager;
@@ -167,12 +164,9 @@ public final class RepositoryServiceFactory
/** /**
* Creates a new RepositoryService for the given repository. * Creates a new RepositoryService for the given repository.
* *
*
* @param repositoryId id of the repository * @param repositoryId id of the repository
*
* @return a implementation of RepositoryService * @return a implementation of RepositoryService
* for the given type of repository * for the given type of repository
*
* @throws NotFoundException if no repository * @throws NotFoundException if no repository
* with the given id is available * with the given id is available
* @throws RepositoryServiceNotFoundException if no repository service * @throws RepositoryServiceNotFoundException if no repository service
@@ -187,8 +181,7 @@ public final class RepositoryServiceFactory
Repository repository = repositoryManager.get(repositoryId); Repository repository = repositoryManager.get(repositoryId);
if (repository == null) if (repository == null) {
{
throw new NotFoundException(Repository.class, repositoryId); throw new NotFoundException(Repository.class, repositoryId);
} }
@@ -198,12 +191,9 @@ public final class RepositoryServiceFactory
/** /**
* Creates a new RepositoryService for the given repository. * Creates a new RepositoryService for the given repository.
* *
*
* @param namespaceAndName namespace and name of the repository * @param namespaceAndName namespace and name of the repository
*
* @return a implementation of RepositoryService * @return a implementation of RepositoryService
* for the given type of repository * for the given type of repository
*
* @throws NotFoundException if no repository * @throws NotFoundException if no repository
* with the given id is available * with the given id is available
* @throws RepositoryServiceNotFoundException if no repository service * @throws RepositoryServiceNotFoundException if no repository service
@@ -212,15 +202,13 @@ public final class RepositoryServiceFactory
* @throws ScmSecurityException if current user has not read permissions * @throws ScmSecurityException if current user has not read permissions
* for that repository * for that repository
*/ */
public RepositoryService create(NamespaceAndName namespaceAndName) public RepositoryService create(NamespaceAndName namespaceAndName) {
{
Preconditions.checkArgument(namespaceAndName != null, Preconditions.checkArgument(namespaceAndName != null,
"a non empty namespace and name is required"); "a non empty namespace and name is required");
Repository repository = repositoryManager.get(namespaceAndName); Repository repository = repositoryManager.get(namespaceAndName);
if (repository == null) if (repository == null) {
{
throw notFound(entity(namespaceAndName)); throw notFound(entity(namespaceAndName));
} }
@@ -230,20 +218,16 @@ public final class RepositoryServiceFactory
/** /**
* Creates a new RepositoryService for the given repository. * Creates a new RepositoryService for the given repository.
* *
*
* @param repository the repository * @param repository the repository
*
* @return a implementation of RepositoryService * @return a implementation of RepositoryService
* for the given type of repository * for the given type of repository
*
* @throws RepositoryServiceNotFoundException if no repository service * @throws RepositoryServiceNotFoundException if no repository service
* implementation for this kind of repository is available * implementation for this kind of repository is available
* @throws NullPointerException if the repository is null * @throws NullPointerException if the repository is null
* @throws ScmSecurityException if current user has not read permissions * @throws ScmSecurityException if current user has not read permissions
* for that repository * for that repository
*/ */
public RepositoryService create(Repository repository) public RepositoryService create(Repository repository) {
{
Preconditions.checkNotNull(repository, "repository is required"); Preconditions.checkNotNull(repository, "repository is required");
// check for read permissions of current user // check for read permissions of current user
@@ -251,14 +235,11 @@ public final class RepositoryServiceFactory
RepositoryService service = null; RepositoryService service = null;
for (RepositoryServiceResolver resolver : resolvers) for (RepositoryServiceResolver resolver : resolvers) {
{
RepositoryServiceProvider provider = resolver.resolve(repository); RepositoryServiceProvider provider = resolver.resolve(repository);
if (provider != null) if (provider != null) {
{ if (logger.isDebugEnabled()) {
if (logger.isDebugEnabled())
{
logger.debug( logger.debug(
"create new repository service for repository {} of type {}", "create new repository service for repository {} of type {}",
repository.getName(), repository.getType()); repository.getName(), repository.getType());
@@ -271,8 +252,7 @@ public final class RepositoryServiceFactory
} }
} }
if (service == null) if (service == null) {
{
throw new RepositoryServiceNotFoundException(repository); throw new RepositoryServiceNotFoundException(repository);
} }
@@ -284,8 +264,7 @@ public final class RepositoryServiceFactory
/** /**
* Hook and listener to clear all relevant repository caches. * Hook and listener to clear all relevant repository caches.
*/ */
private static class CacheClearHook private static class CacheClearHook {
{
private final Set<Cache<?, ?>> caches = Sets.newHashSet(); private final Set<Cache<?, ?>> caches = Sets.newHashSet();
private final CacheManager cacheManager; private final CacheManager cacheManager;
@@ -296,8 +275,7 @@ public final class RepositoryServiceFactory
* *
* @param cacheManager cache manager * @param cacheManager cache manager
*/ */
public CacheClearHook(CacheManager cacheManager) public CacheClearHook(CacheManager cacheManager) {
{
this.cacheManager = cacheManager; this.cacheManager = cacheManager;
this.caches.add(cacheManager.getCache(BlameCommandBuilder.CACHE_NAME)); this.caches.add(cacheManager.getCache(BlameCommandBuilder.CACHE_NAME));
this.caches.add(cacheManager.getCache(BrowseCommandBuilder.CACHE_NAME)); this.caches.add(cacheManager.getCache(BrowseCommandBuilder.CACHE_NAME));
@@ -324,12 +302,10 @@ public final class RepositoryServiceFactory
* @param event hook event * @param event hook event
*/ */
@Subscribe(referenceType = ReferenceType.STRONG) @Subscribe(referenceType = ReferenceType.STRONG)
public void onEvent(PostReceiveRepositoryHookEvent event) public void onEvent(PostReceiveRepositoryHookEvent event) {
{
Repository repository = event.getRepository(); Repository repository = event.getRepository();
if (repository != null) if (repository != null) {
{
String id = repository.getId(); String id = repository.getId();
clearCaches(id); clearCaches(id);
@@ -342,10 +318,8 @@ public final class RepositoryServiceFactory
* @param event repository event * @param event repository event
*/ */
@Subscribe(referenceType = ReferenceType.STRONG) @Subscribe(referenceType = ReferenceType.STRONG)
public void onEvent(RepositoryEvent event) public void onEvent(RepositoryEvent event) {
{ if (event.getEventType() == HandlerEventType.DELETE) {
if (event.getEventType() == HandlerEventType.DELETE)
{
clearCaches(event.getItem().getId()); clearCaches(event.getItem().getId());
} }
} }
@@ -357,37 +331,53 @@ public final class RepositoryServiceFactory
cacheManager.getCache(BranchesCommandBuilder.CACHE_NAME).removeAll(predicate); cacheManager.getCache(BranchesCommandBuilder.CACHE_NAME).removeAll(predicate);
} }
@Subscribe
public void onEvent(PublicKeyDeletedEvent event) {
cacheManager.getCache(LogCommandBuilder.CACHE_NAME).clear();
}
@Subscribe
public void onEvent(PublicKeyCreatedEvent event) {
cacheManager.getCache(LogCommandBuilder.CACHE_NAME).clear();
}
@SuppressWarnings({"unchecked", "java:S3740", "rawtypes"}) @SuppressWarnings({"unchecked", "java:S3740", "rawtypes"})
private void clearCaches(final String repositoryId) private void clearCaches(final String repositoryId) {
{ if (logger.isDebugEnabled()) {
if (logger.isDebugEnabled())
{
logger.debug("clear caches for repository id {}", repositoryId); logger.debug("clear caches for repository id {}", repositoryId);
} }
RepositoryCacheKeyPredicate predicate = new RepositoryCacheKeyPredicate(repositoryId); RepositoryCacheKeyPredicate predicate = new RepositoryCacheKeyPredicate(repositoryId);
caches.forEach((cache) -> { caches.forEach(cache -> cache.removeAll(predicate));
cache.removeAll(predicate);
});
} }
} }
//~--- fields --------------------------------------------------------------- //~--- fields ---------------------------------------------------------------
/** cache manager */ /**
* cache manager
*/
private final CacheManager cacheManager; private final CacheManager cacheManager;
/** scm-manager configuration */ /**
* scm-manager configuration
*/
private final ScmConfiguration configuration; private final ScmConfiguration configuration;
/** pre processor util */ /**
* pre processor util
*/
private final PreProcessorUtil preProcessorUtil; private final PreProcessorUtil preProcessorUtil;
/** repository manager */ /**
* repository manager
*/
private final RepositoryManager repositoryManager; private final RepositoryManager repositoryManager;
/** service resolvers */ /**
* service resolvers
*/
private final Set<RepositoryServiceResolver> resolvers; private final Set<RepositoryServiceResolver> resolvers;
private Set<ScmProtocolProvider> protocolProviders; private Set<ScmProtocolProvider> protocolProviders;

View File

@@ -43,6 +43,7 @@ public class MergeCommandRequest implements Validateable, Resetable, Serializabl
private Person author; private Person author;
private String messageTemplate; private String messageTemplate;
private MergeStrategy mergeStrategy; private MergeStrategy mergeStrategy;
private boolean sign = true;
public String getBranchToMerge() { public String getBranchToMerge() {
return branchToMerge; return branchToMerge;
@@ -84,6 +85,14 @@ public class MergeCommandRequest implements Validateable, Resetable, Serializabl
this.mergeStrategy = mergeStrategy; this.mergeStrategy = mergeStrategy;
} }
public boolean isSign() {
return sign;
}
public void setSign(boolean sign) {
this.sign = sign;
}
public boolean isValid() { public boolean isValid() {
return !Strings.isNullOrEmpty(getBranchToMerge()) return !Strings.isNullOrEmpty(getBranchToMerge())
&& !Strings.isNullOrEmpty(getTargetBranch()); && !Strings.isNullOrEmpty(getTargetBranch());
@@ -92,6 +101,7 @@ public class MergeCommandRequest implements Validateable, Resetable, Serializabl
public void reset() { public void reset() {
this.setBranchToMerge(null); this.setBranchToMerge(null);
this.setTargetBranch(null); this.setTargetBranch(null);
this.setSign(true);
} }
@Override @Override
@@ -109,7 +119,8 @@ public class MergeCommandRequest implements Validateable, Resetable, Serializabl
return Objects.equal(branchToMerge, other.branchToMerge) return Objects.equal(branchToMerge, other.branchToMerge)
&& Objects.equal(targetBranch, other.targetBranch) && Objects.equal(targetBranch, other.targetBranch)
&& Objects.equal(author, other.author) && Objects.equal(author, other.author)
&& Objects.equal(mergeStrategy, other.mergeStrategy); && Objects.equal(mergeStrategy, other.mergeStrategy)
&& Objects.equal(sign, other.sign);
} }
@Override @Override
@@ -124,6 +135,7 @@ public class MergeCommandRequest implements Validateable, Resetable, Serializabl
.add("targetBranch", targetBranch) .add("targetBranch", targetBranch)
.add("author", author) .add("author", author)
.add("mergeStrategy", mergeStrategy) .add("mergeStrategy", mergeStrategy)
.add("sign", sign)
.toString(); .toString();
} }
} }

View File

@@ -49,6 +49,7 @@ public class ModifyCommandRequest implements Resetable, Validateable, CommandWit
private String branch; private String branch;
private String expectedRevision; private String expectedRevision;
private boolean defaultPath; private boolean defaultPath;
private boolean sign = true;
@Override @Override
public void reset() { public void reset() {
@@ -57,6 +58,7 @@ public class ModifyCommandRequest implements Resetable, Validateable, CommandWit
commitMessage = null; commitMessage = null;
branch = null; branch = null;
defaultPath = false; defaultPath = false;
sign = true;
} }
public void addRequest(PartialRequest request) { public void addRequest(PartialRequest request) {
@@ -75,6 +77,10 @@ public class ModifyCommandRequest implements Resetable, Validateable, CommandWit
this.branch = branch; this.branch = branch;
} }
public void setSign(boolean sign) {
this.sign = sign;
}
public List<PartialRequest> getRequests() { public List<PartialRequest> getRequests() {
return Collections.unmodifiableList(requests); return Collections.unmodifiableList(requests);
} }
@@ -112,6 +118,10 @@ public class ModifyCommandRequest implements Resetable, Validateable, CommandWit
this.defaultPath = defaultPath; this.defaultPath = defaultPath;
} }
public boolean isSign() {
return sign;
}
public interface PartialRequest { public interface PartialRequest {
void execute(ModifyCommand.Worker worker) throws IOException; void execute(ModifyCommand.Worker worker) throws IOException;
} }

View File

@@ -0,0 +1,33 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.security;
/**
* Available modes for anonymous access
* @since 2.4.0
*/
public enum AnonymousMode {
FULL, PROTOCOL_ONLY, OFF
}

View File

@@ -29,6 +29,8 @@ import sonia.scm.SCMContext;
public class Authentications { public class Authentications {
private Authentications() {}
public static boolean isAuthenticatedSubjectAnonymous() { public static boolean isAuthenticatedSubjectAnonymous() {
return isSubjectAnonymous((String) SecurityUtils.getSubject().getPrincipal()); return isSubjectAnonymous((String) SecurityUtils.getSubject().getPrincipal());
} }

View File

@@ -0,0 +1,66 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.security;
import java.util.Optional;
/**
* Allows signing and verification using gpg.
*
* @since 2.4.0
*/
public interface GPG {
/**
* Returns the id of the key from the given signature.
*
* @param signature signature
* @return public key id
*/
String findPublicKeyId(byte[] signature);
/**
* Returns the public key with the given id or an empty optional.
*
* @param id id of public
* @return public key or empty optional
*/
Optional<PublicKey> findPublicKey(String id);
/**
* Returns all public keys assigned to the given username
*
* @param username username of the public key owner
* @return collection of public keys
*/
Iterable<PublicKey> findPublicKeysByUsername(String username);
/**
* Returns the default private key of the currently authenticated user.
*
* @return default private key
*/
PrivateKey getPrivateKey();
}

View File

@@ -0,0 +1,46 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.security;
import sonia.scm.BadRequestException;
import sonia.scm.ContextEntry;
import java.util.List;
@SuppressWarnings("squid:MaximumInheritanceDepth") // exceptions have a deep inheritance depth themselves; therefore we accept this here
public class NotPublicKeyException extends BadRequestException {
public NotPublicKeyException(List<ContextEntry> context, String message) {
super(context, message);
}
public NotPublicKeyException(List<ContextEntry> context, String message, Exception cause) {
super(context, message, cause);
}
@Override
public String getCode() {
return "BxS5wX2v71";
}
}

View File

@@ -0,0 +1,57 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.security;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
/**
* Can be used to create signatures of data.
* @since 2.4.0
*/
public interface PrivateKey {
/**
* Returns the key's id.
* @return id
*/
String getId();
/**
* Creates a signature for the given data.
* @param stream data stream to sign
* @return signature
*/
byte[] sign(InputStream stream);
/**
* Creates a signature for the given data.
* @param data data to sign
* @return signature
*/
default byte[] sign(byte[] data) {
return sign(new ByteArrayInputStream(data));
}
}

View File

@@ -0,0 +1,88 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.security;
import sonia.scm.repository.Person;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.Optional;
import java.util.Set;
/**
* The public key can be used to verify signatures.
*
* @since 2.4.0
*/
public interface PublicKey {
/**
* Returns id of the public key.
*
* @return id of key
*/
String getId();
/**
* Returns the username of the owner or an empty optional.
*
* @return owner or empty optional
*/
Optional<String> getOwner();
/**
* Returns raw of the public key.
*
* @return raw of key
*/
String getRaw();
/**
* Returns the contacts of the publickey.
*
* @return owner or empty optional
*/
Set<Person> getContacts();
/**
* Verifies that the signature is valid for the given data.
*
* @param stream stream of data to verify
* @param signature signature
* @return {@code true} if the signature is valid for the given data
*/
boolean verify(InputStream stream, byte[] signature);
/**
* Verifies that the signature is valid for the given data.
*
* @param data data to verify
* @param signature signature
* @return {@code true} if the signature is valid for the given data
*/
default boolean verify(byte[] data, byte[] signature) {
return verify(new ByteArrayInputStream(data), signature);
}
}

View File

@@ -0,0 +1,44 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.security;
import sonia.scm.event.Event;
/**
* This event is fired when a public key was created in SCM-Manager.
* @since 2.4.0
*/
@Event
public final class PublicKeyCreatedEvent {
private final PublicKey key;
public PublicKeyCreatedEvent(PublicKey key) {
this.key = key;
}
public PublicKey getKey() {
return key;
}
}

View File

@@ -0,0 +1,44 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.security;
import sonia.scm.event.Event;
/**
* This event is fired when a public key was removed from SCM-Manager.
* @since 2.4.0
*/
@Event
public final class PublicKeyDeletedEvent {
private final PublicKey key;
public PublicKeyDeletedEvent(PublicKey key) {
this.key = key;
}
public PublicKey getKey() {
return key;
}
}

View File

@@ -0,0 +1,55 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.security;
import org.apache.shiro.authc.AuthenticationException;
/**
* This exception is thrown if the session token is expired
* @since 2.4.0
*/
@SuppressWarnings("squid:MaximumInheritanceDepth") // exceptions have a deep inheritance depth themselves; therefore we accept this here
public class TokenExpiredException extends AuthenticationException {
/**
* Constructs a new SessionExpiredException.
*
* @param message the reason for the exception
*/
public TokenExpiredException(String message) {
super(message);
}
/**
* Constructs a new SessionExpiredException.
*
* @param message the reason for the exception
* @param cause the underlying Throwable that caused this exception to be thrown.
*/
public TokenExpiredException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -50,7 +50,7 @@ import java.security.Principal;
@StaticPermissions( @StaticPermissions(
value = "user", value = "user",
globalPermissions = {"create", "list", "autocomplete"}, globalPermissions = {"create", "list", "autocomplete"},
permissions = {"read", "modify", "delete", "changePassword"}, permissions = {"read", "modify", "delete", "changePassword", "changePublicKeys"},
custom = true, customGlobal = true custom = true, customGlobal = true
) )
@XmlRootElement(name = "users") @XmlRootElement(name = "users")

View File

@@ -36,7 +36,9 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.SCMContext; import sonia.scm.SCMContext;
import sonia.scm.config.ScmConfiguration; import sonia.scm.config.ScmConfiguration;
import sonia.scm.security.AnonymousMode;
import sonia.scm.security.AnonymousToken; import sonia.scm.security.AnonymousToken;
import sonia.scm.security.TokenExpiredException;
import sonia.scm.util.HttpUtil; import sonia.scm.util.HttpUtil;
import sonia.scm.util.Util; import sonia.scm.util.Util;
import sonia.scm.web.WebTokenGenerator; import sonia.scm.web.WebTokenGenerator;
@@ -48,8 +50,6 @@ import javax.servlet.http.HttpServletResponse;
import java.io.IOException; import java.io.IOException;
import java.util.Set; import java.util.Set;
//~--- JDK imports ------------------------------------------------------------
/** /**
* Handles authentication, if a one of the {@link WebTokenGenerator} returns * Handles authentication, if a one of the {@link WebTokenGenerator} returns
* an {@link AuthenticationToken}. * an {@link AuthenticationToken}.
@@ -58,20 +58,17 @@ import java.util.Set;
* @since 2.0.0 * @since 2.0.0
*/ */
@Singleton @Singleton
public class AuthenticationFilter extends HttpFilter public class AuthenticationFilter extends HttpFilter {
{
/** marker for failed authentication */ private static final Logger logger = LoggerFactory.getLogger(AuthenticationFilter.class);
/**
* marker for failed authentication
*/
private static final String ATTRIBUTE_FAILED_AUTH = "sonia.scm.auth.failed"; private static final String ATTRIBUTE_FAILED_AUTH = "sonia.scm.auth.failed";
/** Field description */ private final Set<WebTokenGenerator> tokenGenerators;
private static final String HEADER_AUTHORIZATION = "Authorization"; protected ScmConfiguration configuration;
/** the logger for AuthenticationFilter */
private static final Logger logger =
LoggerFactory.getLogger(AuthenticationFilter.class);
//~--- constructors ---------------------------------------------------------
/** /**
* Constructs a new basic authenticaton filter. * Constructs a new basic authenticaton filter.
@@ -85,8 +82,6 @@ public class AuthenticationFilter extends HttpFilter
this.tokenGenerators = tokenGenerators; this.tokenGenerators = tokenGenerators;
} }
//~--- methods --------------------------------------------------------------
/** /**
* Handles authentication, if a one of the {@link WebTokenGenerator} returns * Handles authentication, if a one of the {@link WebTokenGenerator} returns
* an {@link AuthenticationToken}. * an {@link AuthenticationToken}.
@@ -94,38 +89,28 @@ public class AuthenticationFilter extends HttpFilter
* @param request servlet request * @param request servlet request
* @param response servlet response * @param response servlet response
* @param chain filter chain * @param chain filter chain
*
* @throws IOException * @throws IOException
* @throws ServletException * @throws ServletException
*/ */
@Override @Override
protected void doFilter(HttpServletRequest request, protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
throws IOException, ServletException
{
Subject subject = SecurityUtils.getSubject(); Subject subject = SecurityUtils.getSubject();
AuthenticationToken token = createToken(request); AuthenticationToken token = createToken(request);
if (token != null) if (token != null) {
{
logger.trace( logger.trace(
"found authentication token on request, start authentication"); "found authentication token on request, start authentication");
handleAuthentication(request, response, chain, subject, token); handleAuthentication(request, response, chain, subject, token);
} } else if (subject.isAuthenticated()) {
else if (subject.isAuthenticated())
{
logger.trace("user is already authenticated"); logger.trace("user is already authenticated");
processChain(request, response, chain, subject); processChain(request, response, chain, subject);
} } else if (isAnonymousAccessEnabled()) {
else if (isAnonymousAccessEnabled() && !HttpUtil.isWUIRequest(request))
{
logger.trace("anonymous access granted"); logger.trace("anonymous access granted");
subject.login(new AnonymousToken()); subject.login(new AnonymousToken());
processChain(request, response, chain, subject); processChain(request, response, chain, subject);
} } else {
else
{
logger.trace("could not find user send unauthorized"); logger.trace("could not find user send unauthorized");
handleUnauthorized(request, response, chain); handleUnauthorized(request, response, chain);
} }
@@ -138,25 +123,19 @@ public class AuthenticationFilter extends HttpFilter
* @param request servlet request * @param request servlet request
* @param response servlet response * @param response servlet response
* @param chain filter chain * @param chain filter chain
*
* @throws IOException * @throws IOException
* @throws ServletException * @throws ServletException
*
* @since 1.8 * @since 1.8
*/ */
protected void handleUnauthorized(HttpServletRequest request, protected void handleUnauthorized(HttpServletRequest request,
HttpServletResponse response, FilterChain chain) HttpServletResponse response, FilterChain chain)
throws IOException, ServletException throws IOException, ServletException {
{
// send only forbidden, if the authentication has failed. // send only forbidden, if the authentication has failed.
// see https://bitbucket.org/sdorra/scm-manager/issue/545/git-clone-with-username-in-url-does-not // see https://bitbucket.org/sdorra/scm-manager/issue/545/git-clone-with-username-in-url-does-not
if (Boolean.TRUE.equals(request.getAttribute(ATTRIBUTE_FAILED_AUTH))) if (Boolean.TRUE.equals(request.getAttribute(ATTRIBUTE_FAILED_AUTH))) {
{
sendFailedAuthenticationError(request, response); sendFailedAuthenticationError(request, response);
} } else {
else
{
sendUnauthorizedError(request, response); sendUnauthorizedError(request, response);
} }
} }
@@ -164,16 +143,13 @@ public class AuthenticationFilter extends HttpFilter
/** /**
* Sends an error for a failed authentication back to client. * Sends an error for a failed authentication back to client.
* *
*
* @param request http request * @param request http request
* @param response http response * @param response http response
*
* @throws IOException * @throws IOException
*/ */
protected void sendFailedAuthenticationError(HttpServletRequest request, protected void sendFailedAuthenticationError(HttpServletRequest request,
HttpServletResponse response) HttpServletResponse response)
throws IOException throws IOException {
{
HttpUtil.sendUnauthorized(request, response, HttpUtil.sendUnauthorized(request, response,
configuration.getRealmDescription()); configuration.getRealmDescription());
} }
@@ -181,38 +157,27 @@ public class AuthenticationFilter extends HttpFilter
/** /**
* Sends an unauthorized error back to client. * Sends an unauthorized error back to client.
* *
*
* @param request http request * @param request http request
* @param response http response * @param response http response
*
* @throws IOException * @throws IOException
*/ */
protected void sendUnauthorizedError(HttpServletRequest request, protected void sendUnauthorizedError(HttpServletRequest request, HttpServletResponse response) throws IOException {
HttpServletResponse response) HttpUtil.sendUnauthorized(request, response, configuration.getRealmDescription());
throws IOException
{
HttpUtil.sendUnauthorized(request, response,
configuration.getRealmDescription());
} }
/** /**
* Iterates all {@link WebTokenGenerator} and creates an * Iterates all {@link WebTokenGenerator} and creates an
* {@link AuthenticationToken} from the given request. * {@link AuthenticationToken} from the given request.
* *
*
* @param request http servlet request * @param request http servlet request
*
* @return authentication token of {@code null} * @return authentication token of {@code null}
*/ */
private AuthenticationToken createToken(HttpServletRequest request) private AuthenticationToken createToken(HttpServletRequest request) {
{
AuthenticationToken token = null; AuthenticationToken token = null;
for (WebTokenGenerator generator : tokenGenerators) for (WebTokenGenerator generator : tokenGenerators) {
{
token = generator.createToken(request); token = generator.createToken(request);
if (token != null) if (token != null) {
{
logger.trace("generated web token {} from generator {}", logger.trace("generated web token {} from generator {}",
token.getClass(), generator.getClass()); token.getClass(), generator.getClass());
@@ -226,30 +191,31 @@ public class AuthenticationFilter extends HttpFilter
/** /**
* Handle authentication with the given {@link AuthenticationToken}. * Handle authentication with the given {@link AuthenticationToken}.
* *
*
* @param request http servlet request * @param request http servlet request
* @param response http servlet response * @param response http servlet response
* @param chain filter chain * @param chain filter chain
* @param subject subject * @param subject subject
* @param token authentication token * @param token authentication token
*
* @throws IOException * @throws IOException
* @throws ServletException * @throws ServletException
*/ */
private void handleAuthentication(HttpServletRequest request, private void handleAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Subject subject, HttpServletResponse response, FilterChain chain, Subject subject,
AuthenticationToken token) AuthenticationToken token)
throws IOException, ServletException throws IOException, ServletException {
{
logger.trace("found basic authorization header, start authentication"); logger.trace("found basic authorization header, start authentication");
try try {
{
subject.login(token); subject.login(token);
processChain(request, response, chain, subject); processChain(request, response, chain, subject);
} catch (TokenExpiredException ex) {
if (logger.isTraceEnabled()) {
logger.trace("{} expired", token.getClass(), ex);
} else {
logger.debug("{} expired", token.getClass());
} }
catch (AuthenticationException ex) handleUnauthorized(request, response, chain);
{ } catch (AuthenticationException ex) {
logger.warn("authentication failed", ex); logger.warn("authentication failed", ex);
handleUnauthorized(request, response, chain); handleUnauthorized(request, response, chain);
} }
@@ -258,33 +224,26 @@ public class AuthenticationFilter extends HttpFilter
/** /**
* Process the filter chain. * Process the filter chain.
* *
*
* @param request http servlet request * @param request http servlet request
* @param response http servlet response * @param response http servlet response
* @param chain filter chain * @param chain filter chain
* @param subject subject * @param subject subject
*
* @throws IOException * @throws IOException
* @throws ServletException * @throws ServletException
*/ */
private void processChain(HttpServletRequest request, private void processChain(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Subject subject) HttpServletResponse response, FilterChain chain, Subject subject)
throws IOException, ServletException throws IOException, ServletException {
{
String username = Util.EMPTY_STRING; String username = Util.EMPTY_STRING;
if (!subject.isAuthenticated()) if (!subject.isAuthenticated()) {
{
// anonymous access // anonymous access
username = SCMContext.USER_ANONYMOUS; username = SCMContext.USER_ANONYMOUS;
} } else {
else
{
Object obj = subject.getPrincipal(); Object obj = subject.getPrincipal();
if (obj != null) if (obj != null) {
{
username = obj.toString(); username = obj.toString();
} }
} }
@@ -293,24 +252,12 @@ public class AuthenticationFilter extends HttpFilter
response); response);
} }
//~--- get methods ----------------------------------------------------------
/** /**
* Returns {@code true} if anonymous access is enabled. * Returns {@code true} if anonymous access is enabled.
* *
*
* @return {@code true} if anonymous access is enabled * @return {@code true} if anonymous access is enabled
*/ */
private boolean isAnonymousAccessEnabled() private boolean isAnonymousAccessEnabled() {
{ return (configuration != null) && configuration.getAnonymousMode() != AnonymousMode.OFF;
return (configuration != null) && configuration.isAnonymousAccessEnabled();
} }
//~--- fields ---------------------------------------------------------------
/** set of web token generators */
private final Set<WebTokenGenerator> tokenGenerators;
/** scm main configuration */
protected ScmConfiguration configuration;
} }

View File

@@ -29,6 +29,7 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.security.AnonymousMode;
import sonia.scm.user.UserManager; import sonia.scm.user.UserManager;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
@@ -52,7 +53,7 @@ class ScmConfigurationChangedListenerTest {
when(userManager.contains(any())).thenReturn(false); when(userManager.contains(any())).thenReturn(false);
ScmConfiguration changes = new ScmConfiguration(); ScmConfiguration changes = new ScmConfiguration();
changes.setAnonymousAccessEnabled(true); changes.setAnonymousMode(AnonymousMode.FULL);
scmConfiguration.load(changes); scmConfiguration.load(changes);
listener.handleEvent(new ScmConfigurationChangedEvent(scmConfiguration)); listener.handleEvent(new ScmConfigurationChangedEvent(scmConfiguration));
@@ -64,7 +65,7 @@ class ScmConfigurationChangedListenerTest {
when(userManager.contains(any())).thenReturn(true); when(userManager.contains(any())).thenReturn(true);
ScmConfiguration changes = new ScmConfiguration(); ScmConfiguration changes = new ScmConfiguration();
changes.setAnonymousAccessEnabled(true); changes.setAnonymousMode(AnonymousMode.FULL);
scmConfiguration.load(changes); scmConfiguration.load(changes);
listener.handleEvent(new ScmConfigurationChangedEvent(scmConfiguration)); listener.handleEvent(new ScmConfigurationChangedEvent(scmConfiguration));

View File

@@ -24,8 +24,6 @@
package sonia.scm.web.filter; package sonia.scm.web.filter;
//~--- non-JDK imports --------------------------------------------------------
import com.github.sdorra.shiro.ShiroRule; import com.github.sdorra.shiro.ShiroRule;
import com.github.sdorra.shiro.SubjectAware; import com.github.sdorra.shiro.SubjectAware;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
@@ -38,6 +36,7 @@ import org.junit.runner.RunWith;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner; import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.config.ScmConfiguration; import sonia.scm.config.ScmConfiguration;
import sonia.scm.security.BearerToken;
import sonia.scm.web.WebTokenGenerator; import sonia.scm.web.WebTokenGenerator;
import javax.servlet.FilterChain; import javax.servlet.FilterChain;
@@ -47,191 +46,98 @@ import javax.servlet.http.HttpServletResponse;
import java.io.IOException; import java.io.IOException;
import static org.mockito.Mockito.any; import static org.mockito.Mockito.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
//~--- JDK imports ------------------------------------------------------------
/**
*
* @author Sebastian Sdorra
*/
@RunWith(MockitoJUnitRunner.class) @RunWith(MockitoJUnitRunner.class)
@SubjectAware(configuration = "classpath:sonia/scm/shiro.ini") @SubjectAware(configuration = "classpath:sonia/scm/shiro.ini")
public class AuthenticationFilterTest public class AuthenticationFilterTest {
{
@Rule
public ShiroRule shiro = new ShiroRule();
@Mock
private FilterChain chain;
@Mock
private HttpServletRequest request;
@Mock
private HttpServletResponse response;
private ScmConfiguration configuration;
/**
* Method description
*
*
* @throws IOException
* @throws ServletException
*/
@Test @Test
@SubjectAware(username = "trillian", password = "secret") @SubjectAware(username = "trillian", password = "secret")
public void testDoFilterAuthenticated() throws IOException, ServletException public void testDoFilterAuthenticated() throws IOException, ServletException {
{
AuthenticationFilter filter = createAuthenticationFilter(); AuthenticationFilter filter = createAuthenticationFilter();
filter.doFilter(request, response, chain); filter.doFilter(request, response, chain);
verify(chain).doFilter(any(HttpServletRequest.class), verify(chain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
any(HttpServletResponse.class));
} }
/**
* Method description
*
*
* @throws IOException
* @throws ServletException
*/
@Test @Test
public void testDoFilterUnauthorized() throws IOException, ServletException public void testDoFilterUnauthorized() throws IOException, ServletException {
{
AuthenticationFilter filter = createAuthenticationFilter(); AuthenticationFilter filter = createAuthenticationFilter();
filter.doFilter(request, response, chain); filter.doFilter(request, response, chain);
verify(response).sendError(HttpServletResponse.SC_UNAUTHORIZED, verify(response).sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authorization Required");
"Authorization Required");
} }
/**
* Method description
*
*
* @throws IOException
* @throws ServletException
*/
@Test @Test
public void testDoFilterWithAuthenticationFailed() public void testDoFilterWithAuthenticationFailed() throws IOException, ServletException {
throws IOException, ServletException AuthenticationFilter filter = createAuthenticationFilter(new DemoWebTokenGenerator("trillian", "sec"));
{
AuthenticationFilter filter =
createAuthenticationFilter(new DemoWebTokenGenerator("trillian", "sec"));
filter.doFilter(request, response, chain); filter.doFilter(request, response, chain);
verify(response).sendError(HttpServletResponse.SC_UNAUTHORIZED,
"Authorization Required"); verify(response).sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authorization Required");
} }
/**
* Method description
*
*
* @throws IOException
* @throws ServletException
*/
@Test @Test
public void testDoFilterWithAuthenticationSuccess() public void testDoFilterWithAuthenticationSuccess() throws IOException, ServletException {
throws IOException, ServletException AuthenticationFilter filter = createAuthenticationFilter();
{
AuthenticationFilter filter =
createAuthenticationFilter(new DemoWebTokenGenerator("trillian",
"secret"));
filter.doFilter(request, response, chain); filter.doFilter(request, response, chain);
verify(chain).doFilter(any(HttpServletRequest.class),
any(HttpServletResponse.class)); verify(response).sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authorization Required");
} }
//~--- set methods ---------------------------------------------------------- @Test
public void testExpiredBearerToken() throws IOException, ServletException {
WebTokenGenerator generator = mock(WebTokenGenerator.class);
when(generator.createToken(request)).thenReturn(BearerToken.create(null,
"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzY21hZG1pbiIsImp0aSI6IjNqUzZ4TzMwUzEiLCJpYXQiOjE1OTY3ODA5Mjg"
+ "sImV4cCI6MTU5Njc0NDUyOCwic2NtLW1hbmFnZXIucmVmcmVzaEV4cGlyYXRpb24iOjE1OTY4MjQxMjg2MDIsInNjbS1tYW5h"
+ "Z2VyLnBhcmVudFRva2VuSWQiOiIzalM2eE8zMFMxIn0.utZLmzGZr-M6MP19yrd0dgLPkJ0u1xojwHKQi36_QAs"));
AuthenticationFilter filter = createAuthenticationFilter(generator);
filter.doFilter(request, response, chain);
verify(response).sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authorization Required");
}
/**
* Method description
*
*/
@Before @Before
public void setUp() public void setUp() {
{
configuration = new ScmConfiguration(); configuration = new ScmConfiguration();
} }
//~--- methods -------------------------------------------------------------- private AuthenticationFilter createAuthenticationFilter(WebTokenGenerator... generators) {
return new AuthenticationFilter(configuration, ImmutableSet.copyOf(generators));
/**
* Method description
*
*
* @param generators
*
* @return
*/
private AuthenticationFilter createAuthenticationFilter(
WebTokenGenerator... generators)
{
return new AuthenticationFilter(configuration,
ImmutableSet.copyOf(generators));
} }
//~--- inner classes -------------------------------------------------------- private static class DemoWebTokenGenerator implements WebTokenGenerator {
/** private final String username;
* Class description private final String password;
*
*
* @version Enter version here..., 15/02/21
* @author Enter your name here...
*/
private static class DemoWebTokenGenerator implements WebTokenGenerator
{
/** public DemoWebTokenGenerator(String username, String password) {
* Constructs ...
*
*
* @param username
* @param password
*/
public DemoWebTokenGenerator(String username, String password)
{
this.username = username; this.username = username;
this.password = password; this.password = password;
} }
//~--- methods ------------------------------------------------------------
/**
* Method description
*
*
* @param request
*
* @return
*/
@Override @Override
public AuthenticationToken createToken(HttpServletRequest request) public AuthenticationToken createToken(HttpServletRequest request) {
{
return new UsernamePasswordToken(username, password); return new UsernamePasswordToken(username, password);
} }
//~--- fields -------------------------------------------------------------
/** Field description */
private final String password;
/** Field description */
private final String username;
} }
//~--- fields ---------------------------------------------------------------
/** Field description */
@Rule
public ShiroRule shiro = new ShiroRule();
/** Field description */
@Mock
private FilterChain chain;
/** Field description */
private ScmConfiguration configuration;
/** Field description */
@Mock
private HttpServletRequest request;
/** Field description */
@Mock
private HttpServletResponse response;
} }

View File

@@ -41,6 +41,7 @@ import sonia.scm.it.utils.ScmTypes;
import sonia.scm.it.utils.TestData; import sonia.scm.it.utils.TestData;
import sonia.scm.repository.client.api.RepositoryClient; import sonia.scm.repository.client.api.RepositoryClient;
import sonia.scm.repository.client.api.RepositoryClientException; import sonia.scm.repository.client.api.RepositoryClientException;
import sonia.scm.security.AnonymousMode;
import javax.json.Json; import javax.json.Json;
import javax.json.JsonArray; import javax.json.JsonArray;
@@ -77,10 +78,10 @@ class AnonymousAccessITCase {
@Nested @Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestInstance(TestInstance.Lifecycle.PER_CLASS)
class WithAnonymousAccess { class WithProtocolOnlyAnonymousAccess {
@BeforeAll @BeforeAll
void enableAnonymousAccess() { void enableAnonymousAccess() {
setAnonymousAccess(true); setAnonymousAccess(AnonymousMode.PROTOCOL_ONLY);
} }
@BeforeEach @BeforeEach
@@ -120,7 +121,7 @@ class AnonymousAccessITCase {
@BeforeEach @BeforeEach
void grantAnonymousAccessToRepo() { void grantAnonymousAccessToRepo() {
ScmTypes.availableScmTypes().stream().forEach(type -> TestData.createUserPermission(USER_ANONYMOUS, WRITE, type)); ScmTypes.availableScmTypes().forEach(type -> TestData.createUserPermission(USER_ANONYMOUS, WRITE, type));
} }
@ParameterizedTest @ParameterizedTest
@@ -142,13 +143,84 @@ class AnonymousAccessITCase {
@AfterAll @AfterAll
void disableAnonymousAccess() { void disableAnonymousAccess() {
setAnonymousAccess(false); setAnonymousAccess(AnonymousMode.OFF);
} }
} }
private static void setAnonymousAccess(boolean anonymousAccessEnabled) { @Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class WithFullAnonymousAccess {
@BeforeAll
void enableAnonymousAccess() {
setAnonymousAccess(AnonymousMode.FULL);
}
@BeforeEach
void createRepository() {
TestData.createDefault();
}
@Test
void shouldGrantAnonymousAccessToRepositoryList() {
assertEquals(200, RestAssured.given()
.when()
.get(RestUtil.REST_BASE_URL.resolve("repositories"))
.statusCode());
}
@Nested
class WithoutAnonymousAccessForRepository {
@ParameterizedTest
@ArgumentsSource(ScmTypes.class)
void shouldGrantAnonymousAccessToRepository(String type) {
assertEquals(401, RestAssured.given()
.when()
.get(getDefaultRepositoryUrl(type))
.statusCode());
}
@ParameterizedTest
@ArgumentsSource(ScmTypes.class)
void shouldNotCloneRepository(String type, @TempDir Path temporaryFolder) {
assertThrows(RepositoryClientException.class, () -> RepositoryUtil.createAnonymousRepositoryClient(type, Files.createDirectories(temporaryFolder).toFile()));
}
}
@Nested
class WithAnonymousAccessForRepository {
@BeforeEach
void grantAnonymousAccessToRepo() {
ScmTypes.availableScmTypes().forEach(type -> TestData.createUserPermission(USER_ANONYMOUS, WRITE, type));
}
@ParameterizedTest
@ArgumentsSource(ScmTypes.class)
void shouldGrantAnonymousAccessToRepository(String type) {
assertEquals(200, RestAssured.given()
.when()
.get(getDefaultRepositoryUrl(type))
.statusCode());
}
@ParameterizedTest
@ArgumentsSource(ScmTypes.class)
void shouldCloneRepository(String type, @TempDir Path temporaryFolder) throws IOException {
RepositoryClient client = RepositoryUtil.createAnonymousRepositoryClient(type, Files.createDirectories(temporaryFolder).toFile());
assertEquals(1, Objects.requireNonNull(client.getWorkingCopy().list()).length);
}
}
@AfterAll
void disableAnonymousAccess() {
setAnonymousAccess(AnonymousMode.OFF);
}
}
private static void setAnonymousAccess(AnonymousMode anonymousMode) {
RestUtil.given("application/vnd.scmm-config+json;v=2") RestUtil.given("application/vnd.scmm-config+json;v=2")
.body(createConfig(anonymousAccessEnabled)) .body(createConfig(anonymousMode))
.when() .when()
.put(RestUtil.REST_BASE_URL.toASCIIString() + "config") .put(RestUtil.REST_BASE_URL.toASCIIString() + "config")
@@ -157,12 +229,12 @@ class AnonymousAccessITCase {
.statusCode(HttpServletResponse.SC_NO_CONTENT); .statusCode(HttpServletResponse.SC_NO_CONTENT);
} }
private static String createConfig(boolean anonymousAccessEnabled) { private static String createConfig(AnonymousMode anonymousMode) {
JsonArray emptyArray = Json.createBuilderFactory(emptyMap()).createArrayBuilder().build(); JsonArray emptyArray = Json.createBuilderFactory(emptyMap()).createArrayBuilder().build();
return JSON_BUILDER return JSON_BUILDER
.add("adminGroups", emptyArray) .add("adminGroups", emptyArray)
.add("adminUsers", emptyArray) .add("adminUsers", emptyArray)
.add("anonymousAccessEnabled", anonymousAccessEnabled) .add("anonymousMode", anonymousMode.toString())
.add("baseUrl", "https://next-scm.cloudogu.com/scm") .add("baseUrl", "https://next-scm.cloudogu.com/scm")
.add("dateFormat", "YYYY-MM-DD HH:mm:ss") .add("dateFormat", "YYYY-MM-DD HH:mm:ss")
.add("disableGroupingGrid", false) .add("disableGroupingGrid", false)

View File

@@ -29,200 +29,106 @@ package org.eclipse.jgit.transport;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.Provider; import com.google.inject.Provider;
import org.eclipse.jgit.errors.NoRemoteRepositoryException; import org.eclipse.jgit.errors.NoRemoteRepositoryException;
import org.eclipse.jgit.errors.NotSupportedException;
import org.eclipse.jgit.errors.TransportException; import org.eclipse.jgit.errors.TransportException;
import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.RepositoryCache; import org.eclipse.jgit.lib.RepositoryCache;
import sonia.scm.repository.GitChangesetConverterFactory;
import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.GitRepositoryHandler;
import sonia.scm.repository.spi.HookEventFacade; import sonia.scm.repository.spi.HookEventFacade;
import sonia.scm.web.CollectingPackParserListener; import sonia.scm.web.CollectingPackParserListener;
import sonia.scm.web.GitReceiveHook; import sonia.scm.web.GitReceiveHook;
//~--- JDK imports ------------------------------------------------------------
import java.io.File; import java.io.File;
import java.util.Set; import java.util.Set;
//~--- JDK imports ------------------------------------------------------------
/** /**
*
* @author Sebastian Sdorra * @author Sebastian Sdorra
*/ */
public class ScmTransportProtocol extends TransportProtocol public class ScmTransportProtocol extends TransportProtocol {
{
/** Field description */
public static final String NAME = "scm"; public static final String NAME = "scm";
/** Field description */
private static final Set<String> SCHEMES = ImmutableSet.of(NAME); private static final Set<String> SCHEMES = ImmutableSet.of(NAME);
//~--- constructors --------------------------------------------------------- private Provider<GitChangesetConverterFactory> converterFactory;
private Provider<HookEventFacade> hookEventFacadeProvider;
private Provider<GitRepositoryHandler> repositoryHandlerProvider;
/** public ScmTransportProtocol() {
* Constructs ... }
*
*/
public ScmTransportProtocol() {}
/**
* Constructs ...
*
*
*
* @param hookEventFacadeProvider
*
* @param repositoryHandlerProvider
*/
@Inject @Inject
public ScmTransportProtocol( public ScmTransportProtocol(
Provider<GitChangesetConverterFactory> converterFactory,
Provider<HookEventFacade> hookEventFacadeProvider, Provider<HookEventFacade> hookEventFacadeProvider,
Provider<GitRepositoryHandler> repositoryHandlerProvider) Provider<GitRepositoryHandler> repositoryHandlerProvider) {
{ this.converterFactory = converterFactory;
this.hookEventFacadeProvider = hookEventFacadeProvider; this.hookEventFacadeProvider = hookEventFacadeProvider;
this.repositoryHandlerProvider = repositoryHandlerProvider; this.repositoryHandlerProvider = repositoryHandlerProvider;
} }
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param uri
* @param local
* @param remoteName
*
* @return
*/
@Override @Override
public boolean canHandle(URIish uri, Repository local, String remoteName) public boolean canHandle(URIish uri, Repository local, String remoteName) {
{ return (uri.getPath() != null) && (uri.getPort() <= 0)
if ((uri.getPath() == null) || (uri.getPort() > 0) && (uri.getUser() == null) && (uri.getPass() == null)
|| (uri.getUser() != null) || (uri.getPass() != null) && (uri.getHost() == null)
|| (uri.getHost() != null) && ((uri.getScheme() == null) || getSchemes().contains(uri.getScheme()));
|| ((uri.getScheme() != null) &&!getSchemes().contains(uri.getScheme())))
{
return false;
} }
return true;
}
/**
* Method description
*
*
* @param uri
* @param local
* @param remoteName
*
* @return
*
* @throws NotSupportedException
* @throws TransportException
*/
@Override @Override
public Transport open(URIish uri, Repository local, String remoteName) public Transport open(URIish uri, Repository local, String remoteName) throws TransportException {
throws TransportException
{
File localDirectory = local.getDirectory(); File localDirectory = local.getDirectory();
File path = local.getFS().resolve(localDirectory, uri.getPath()); File path = local.getFS().resolve(localDirectory, uri.getPath());
File gitDir = RepositoryCache.FileKey.resolve(path, local.getFS()); File gitDir = RepositoryCache.FileKey.resolve(path, local.getFS());
if (gitDir == null) if (gitDir == null) {
{
throw new NoRemoteRepositoryException(uri, JGitText.get().notFound); throw new NoRemoteRepositoryException(uri, JGitText.get().notFound);
} }
//J-
return new TransportLocalWithHooks( return new TransportLocalWithHooks(
converterFactory.get(),
hookEventFacadeProvider.get(), hookEventFacadeProvider.get(),
repositoryHandlerProvider.get(), repositoryHandlerProvider.get(),
local, uri, gitDir local, uri, gitDir
); );
//J+
} }
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @return
*/
@Override @Override
public String getName() public String getName() {
{
return NAME; return NAME;
} }
/**
* Method description
*
*
* @return
*/
@Override @Override
public Set<String> getSchemes() public Set<String> getSchemes() {
{
return SCHEMES; return SCHEMES;
} }
//~--- inner classes -------------------------------------------------------- private static class TransportLocalWithHooks extends TransportLocal {
/** private final GitChangesetConverterFactory converterFactory;
* Class description private final GitRepositoryHandler handler;
* private final HookEventFacade hookEventFacade;
*
* @version Enter version here..., 13/05/19
* @author Enter your name here...
*/
private static class TransportLocalWithHooks extends TransportLocal
{
/** public TransportLocalWithHooks(
* Constructs ... GitChangesetConverterFactory converterFactory,
* HookEventFacade hookEventFacade,
* GitRepositoryHandler handler,
* Repository local, URIish uri, File gitDir) {
* @param hookEventFacade
* @param handler
* @param local
* @param uri
* @param gitDir
*/
public TransportLocalWithHooks(HookEventFacade hookEventFacade,
GitRepositoryHandler handler, Repository local, URIish uri, File gitDir)
{
super(local, uri, gitDir); super(local, uri, gitDir);
this.converterFactory = converterFactory;
this.hookEventFacade = hookEventFacade; this.hookEventFacade = hookEventFacade;
this.handler = handler; this.handler = handler;
} }
//~--- methods ------------------------------------------------------------
/**
* Method description
*
*
* @param dst
*
* @return
*/
@Override @Override
ReceivePack createReceivePack(Repository dst) ReceivePack createReceivePack(Repository dst) {
{
ReceivePack pack = new ReceivePack(dst); ReceivePack pack = new ReceivePack(dst);
if ((hookEventFacade != null) && (handler != null)) if ((hookEventFacade != null) && (handler != null) && (converterFactory != null)) {
{ GitReceiveHook hook = new GitReceiveHook(converterFactory, hookEventFacade, handler);
GitReceiveHook hook = new GitReceiveHook(hookEventFacade, handler);
pack.setPreReceiveHook(hook); pack.setPreReceiveHook(hook);
pack.setPostReceiveHook(hook); pack.setPostReceiveHook(hook);
@@ -232,22 +138,6 @@ public class ScmTransportProtocol extends TransportProtocol
return pack; return pack;
} }
//~--- fields -------------------------------------------------------------
/** Field description */
private GitRepositoryHandler handler;
/** Field description */
private HookEventFacade hookEventFacade;
} }
//~--- fields ---------------------------------------------------------------
/** Field description */
private Provider<HookEventFacade> hookEventFacadeProvider;
/** Field description */
private Provider<GitRepositoryHandler> repositoryHandlerProvider;
} }

View File

@@ -29,6 +29,7 @@ import org.eclipse.jgit.transport.ReceivePack;
import org.eclipse.jgit.transport.resolver.ReceivePackFactory; import org.eclipse.jgit.transport.resolver.ReceivePackFactory;
import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException; import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException;
import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException; import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException;
import sonia.scm.repository.GitChangesetConverterFactory;
import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.GitRepositoryHandler;
import sonia.scm.repository.spi.HookEventFacade; import sonia.scm.repository.spi.HookEventFacade;
import sonia.scm.web.CollectingPackParserListener; import sonia.scm.web.CollectingPackParserListener;
@@ -39,9 +40,9 @@ public abstract class BaseReceivePackFactory<T> implements ReceivePackFactory<T>
private final GitRepositoryHandler handler; private final GitRepositoryHandler handler;
private final GitReceiveHook hook; private final GitReceiveHook hook;
protected BaseReceivePackFactory(GitRepositoryHandler handler, HookEventFacade hookEventFacade) { protected BaseReceivePackFactory(GitChangesetConverterFactory converterFactory, GitRepositoryHandler handler, HookEventFacade hookEventFacade) {
this.handler = handler; this.handler = handler;
this.hook = new GitReceiveHook(hookEventFacade, handler); this.hook = new GitReceiveHook(converterFactory, hookEventFacade, handler);
} }
@Override @Override

View File

@@ -28,14 +28,15 @@ import com.google.inject.Inject;
import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.transport.ReceivePack; import org.eclipse.jgit.transport.ReceivePack;
import sonia.scm.protocolcommand.RepositoryContext; import sonia.scm.protocolcommand.RepositoryContext;
import sonia.scm.repository.GitChangesetConverterFactory;
import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.GitRepositoryHandler;
import sonia.scm.repository.spi.HookEventFacade; import sonia.scm.repository.spi.HookEventFacade;
public class ScmReceivePackFactory extends BaseReceivePackFactory<RepositoryContext> { public class ScmReceivePackFactory extends BaseReceivePackFactory<RepositoryContext> {
@Inject @Inject
public ScmReceivePackFactory(GitRepositoryHandler handler, HookEventFacade hookEventFacade) { public ScmReceivePackFactory(GitChangesetConverterFactory converterFactory, GitRepositoryHandler handler, HookEventFacade hookEventFacade) {
super(handler, hookEventFacade); super(converterFactory, handler, hookEventFacade);
} }
@Override @Override

View File

@@ -26,6 +26,7 @@ package sonia.scm.repository;
//~--- non-JDK imports -------------------------------------------------------- //~--- non-JDK imports --------------------------------------------------------
import com.google.common.base.Strings;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.collect.Multimap; import com.google.common.collect.Multimap;
import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectId;
@@ -33,138 +34,55 @@ import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.treewalk.TreeWalk;
import org.slf4j.Logger; import org.eclipse.jgit.util.RawParseUtils;
import org.slf4j.LoggerFactory; import sonia.scm.security.GPG;
import sonia.scm.security.PublicKey;
import sonia.scm.util.Util; import sonia.scm.util.Util;
import java.io.ByteArrayOutputStream;
import java.io.Closeable; import java.io.Closeable;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Optional;
//~--- JDK imports ------------------------------------------------------------ //~--- JDK imports ------------------------------------------------------------
/** /**
*
* @author Sebastian Sdorra * @author Sebastian Sdorra
*/ */
public class GitChangesetConverter implements Closeable public class GitChangesetConverter implements Closeable {
{
/** private final GPG gpg;
* the logger for GitChangesetConverter private final Multimap<ObjectId, String> tags;
*/ private final TreeWalk treeWalk;
private static final Logger logger =
LoggerFactory.getLogger(GitChangesetConverter.class);
//~--- constructors ---------------------------------------------------------
/**
* Constructs ...
*
*
* @param repository
*/
public GitChangesetConverter(org.eclipse.jgit.lib.Repository repository)
{
this(repository, null);
}
/**
* Constructs ...
*
*
* @param repository
* @param revWalk
*/
public GitChangesetConverter(org.eclipse.jgit.lib.Repository repository,
RevWalk revWalk)
{
this.repository = repository;
if (revWalk != null)
{
this.revWalk = revWalk;
}
else
{
this.revWalk = new RevWalk(repository);
}
public GitChangesetConverter(GPG gpg, org.eclipse.jgit.lib.Repository repository, RevWalk revWalk) {
this.gpg = gpg;
this.tags = GitUtil.createTagMap(repository, revWalk); this.tags = GitUtil.createTagMap(repository, revWalk);
treeWalk = new TreeWalk(repository); this.treeWalk = new TreeWalk(repository);
} }
//~--- methods -------------------------------------------------------------- public Changeset createChangeset(RevCommit commit) {
/**
* Method description
*
*/
@Override
public void close()
{
GitUtil.release(treeWalk);
}
/**
* Method description
*
*
* @param commit
*
* @return
*
* @throws IOException
*/
public Changeset createChangeset(RevCommit commit)
{
return createChangeset(commit, Collections.emptyList()); return createChangeset(commit, Collections.emptyList());
} }
/** public Changeset createChangeset(RevCommit commit, String branch) {
* Method description
*
*
* @param commit
* @param branch
*
* @return
*
* @throws IOException
*/
public Changeset createChangeset(RevCommit commit, String branch)
{
return createChangeset(commit, Lists.newArrayList(branch)); return createChangeset(commit, Lists.newArrayList(branch));
} }
/** public Changeset createChangeset(RevCommit commit, List<String> branches) {
* Method description
*
*
*
* @param commit
* @param branches
*
* @return
*
* @throws IOException
*/
public Changeset createChangeset(RevCommit commit, List<String> branches)
{
String id = commit.getId().name(); String id = commit.getId().name();
List<String> parentList = null; List<String> parentList = null;
RevCommit[] parents = commit.getParents(); RevCommit[] parents = commit.getParents();
if (Util.isNotEmpty(parents)) if (Util.isNotEmpty(parents)) {
{ parentList = new ArrayList<>();
parentList = new ArrayList<String>();
for (RevCommit parent : parents) for (RevCommit parent : parents) {
{
parentList.add(parent.getId().name()); parentList.add(parent.getId().name());
} }
} }
@@ -175,8 +93,7 @@ public class GitChangesetConverter implements Closeable
Person author = createPersonFor(authorIndent); Person author = createPersonFor(authorIndent);
String message = commit.getFullMessage(); String message = commit.getFullMessage();
if (message != null) if (message != null) {
{
message = message.trim(); message = message.trim();
} }
@@ -185,41 +102,83 @@ public class GitChangesetConverter implements Closeable
changeset.addContributor(new Contributor("Committed-by", createPersonFor(committerIdent))); changeset.addContributor(new Contributor("Committed-by", createPersonFor(committerIdent)));
} }
if (parentList != null) if (parentList != null) {
{
changeset.setParents(parentList); changeset.setParents(parentList);
} }
Collection<String> tagCollection = tags.get(commit.getId()); Collection<String> tagCollection = tags.get(commit.getId());
if (Util.isNotEmpty(tagCollection)) if (Util.isNotEmpty(tagCollection)) {
{
// create a copy of the tag collection to reduce memory on caching // create a copy of the tag collection to reduce memory on caching
changeset.getTags().addAll(Lists.newArrayList(tagCollection)); changeset.getTags().addAll(Lists.newArrayList(tagCollection));
} }
changeset.setBranches(branches); changeset.setBranches(branches);
Signature signature = createSignature(commit);
if (signature != null) {
changeset.addSignature(signature);
}
return changeset; return changeset;
} }
private static final byte[] GPG_HEADER = {'g', 'p', 'g', 's', 'i', 'g'};
private Signature createSignature(RevCommit commit) {
byte[] raw = commit.getRawBuffer();
int start = RawParseUtils.headerStart(GPG_HEADER, raw, 0);
if (start < 0) {
return null;
}
int end = RawParseUtils.headerEnd(raw, start);
byte[] signature = Arrays.copyOfRange(raw, start, end);
String publicKeyId = gpg.findPublicKeyId(signature);
if (Strings.isNullOrEmpty(publicKeyId)) {
// key not found
return new Signature(publicKeyId, "gpg", SignatureStatus.NOT_FOUND, null, Collections.emptySet());
}
Optional<PublicKey> publicKeyById = gpg.findPublicKey(publicKeyId);
if (!publicKeyById.isPresent()) {
// key not found
return new Signature(publicKeyId, "gpg", SignatureStatus.NOT_FOUND, null, Collections.emptySet());
}
PublicKey publicKey = publicKeyById.get();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
byte[] headerPrefix = Arrays.copyOfRange(raw, 0, start - GPG_HEADER.length - 1);
baos.write(headerPrefix);
byte[] headerSuffix = Arrays.copyOfRange(raw, end + 1, raw.length);
baos.write(headerSuffix);
} catch (IOException ex) {
// this will never happen, because we are writing into memory
throw new IllegalStateException("failed to write into memory", ex);
}
boolean verified = publicKey.verify(baos.toByteArray(), signature);
return new Signature(
publicKeyId,
"gpg",
verified ? SignatureStatus.VERIFIED : SignatureStatus.INVALID,
publicKey.getOwner().orElse(null),
publicKey.getContacts()
);
}
public Person createPersonFor(PersonIdent personIndent) { public Person createPersonFor(PersonIdent personIndent) {
return new Person(personIndent.getName(), personIndent.getEmailAddress()); return new Person(personIndent.getName(), personIndent.getEmailAddress());
} }
@Override
public void close() {
GitUtil.release(treeWalk);
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private org.eclipse.jgit.lib.Repository repository;
/** Field description */
private RevWalk revWalk;
/** Field description */
private Multimap<ObjectId, String> tags;
/** Field description */
private TreeWalk treeWalk;
} }

View File

@@ -0,0 +1,50 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevWalk;
import sonia.scm.security.GPG;
import javax.inject.Inject;
public class GitChangesetConverterFactory {
private final GPG gpg;
@Inject
public GitChangesetConverterFactory(GPG gpg) {
this.gpg = gpg;
}
public GitChangesetConverter create(Repository repository) {
return new GitChangesetConverter(gpg, repository, new RevWalk(repository));
}
public GitChangesetConverter create(Repository repository, RevWalk revWalk) {
return new GitChangesetConverter(gpg, repository, revWalk);
}
}

View File

@@ -72,9 +72,10 @@ public class GitHookChangesetCollector
* @param rpack * @param rpack
* @param receiveCommands * @param receiveCommands
*/ */
public GitHookChangesetCollector(ReceivePack rpack, public GitHookChangesetCollector(GitChangesetConverterFactory converterFactory, ReceivePack rpack,
List<ReceiveCommand> receiveCommands) List<ReceiveCommand> receiveCommands)
{ {
this.converterFactory = converterFactory;
this.rpack = rpack; this.rpack = rpack;
this.receiveCommands = receiveCommands; this.receiveCommands = receiveCommands;
this.listener = CollectingPackParserListener.get(rpack); this.listener = CollectingPackParserListener.get(rpack);
@@ -100,7 +101,7 @@ public class GitHookChangesetCollector
try try
{ {
walk = rpack.getRevWalk(); walk = rpack.getRevWalk();
converter = new GitChangesetConverter(repository, walk); converter = converterFactory.create(repository, walk);
for (ReceiveCommand rc : receiveCommands) for (ReceiveCommand rc : receiveCommands)
{ {
@@ -222,5 +223,6 @@ public class GitHookChangesetCollector
private final List<ReceiveCommand> receiveCommands; private final List<ReceiveCommand> receiveCommands;
private final GitChangesetConverterFactory converterFactory;
private final ReceivePack rpack; private final ReceivePack rpack;
} }

View File

@@ -0,0 +1,61 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
import org.eclipse.jgit.api.errors.CanceledException;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.GpgSignature;
import org.eclipse.jgit.lib.GpgSigner;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.transport.CredentialsProvider;
import sonia.scm.security.GPG;
import javax.inject.Inject;
import java.io.UnsupportedEncodingException;
public class ScmGpgSigner extends GpgSigner {
private final GPG gpg;
@Inject
public ScmGpgSigner(GPG gpg) {
this.gpg = gpg;
}
@Override
public void sign(CommitBuilder commitBuilder, String keyId, PersonIdent personIdent, CredentialsProvider credentialsProvider) throws CanceledException {
try {
final byte[] signature = this.gpg.getPrivateKey().sign(commitBuilder.build());
commitBuilder.setGpgSignature(new GpgSignature(signature));
} catch (UnsupportedEncodingException e) {
throw new IllegalStateException(e);
}
}
@Override
public boolean canLocateSigningKey(String keyId, PersonIdent personIdent, CredentialsProvider credentialsProvider) throws CanceledException {
return true;
}
}

View File

@@ -0,0 +1,53 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
import org.eclipse.jgit.lib.GpgSigner;
import sonia.scm.plugin.Extension;
import javax.inject.Inject;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
@Extension
public class ScmGpgSignerInitializer implements ServletContextListener {
private final ScmGpgSigner scmGpgSigner;
@Inject
public ScmGpgSignerInitializer(ScmGpgSigner scmGpgSigner) {
this.scmGpgSigner = scmGpgSigner;
}
@Override
public void contextInitialized(ServletContextEvent servletContextEvent) {
GpgSigner.setDefault(scmGpgSigner);
}
@Override
public void contextDestroyed(ServletContextEvent servletContextEvent) {
// Do nothing
}
}

View File

@@ -28,6 +28,7 @@ import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.transport.ReceiveCommand; import org.eclipse.jgit.transport.ReceiveCommand;
import sonia.scm.repository.GitChangesetConverterFactory;
import sonia.scm.repository.GitUtil; import sonia.scm.repository.GitUtil;
import sonia.scm.repository.spi.GitLogComputer; import sonia.scm.repository.spi.GitLogComputer;
import sonia.scm.repository.spi.HookMergeDetectionProvider; import sonia.scm.repository.spi.HookMergeDetectionProvider;
@@ -39,11 +40,13 @@ public class GitReceiveHookMergeDetectionProvider implements HookMergeDetectionP
private final Repository repository; private final Repository repository;
private final String repositoryId; private final String repositoryId;
private final List<ReceiveCommand> receiveCommands; private final List<ReceiveCommand> receiveCommands;
private final GitChangesetConverterFactory converterFactory;
public GitReceiveHookMergeDetectionProvider(Repository repository, String repositoryId, List<ReceiveCommand> receiveCommands) { public GitReceiveHookMergeDetectionProvider(Repository repository, String repositoryId, List<ReceiveCommand> receiveCommands, GitChangesetConverterFactory converterFactory) {
this.repository = repository; this.repository = repository;
this.repositoryId = repositoryId; this.repositoryId = repositoryId;
this.receiveCommands = receiveCommands; this.receiveCommands = receiveCommands;
this.converterFactory = converterFactory;
} }
@Override @Override
@@ -53,7 +56,7 @@ public class GitReceiveHookMergeDetectionProvider implements HookMergeDetectionP
request.setAncestorChangeset(findRelevantRevisionForBranchIfToBeUpdated(target)); request.setAncestorChangeset(findRelevantRevisionForBranchIfToBeUpdated(target));
request.setPagingLimit(1); request.setPagingLimit(1);
return new GitLogComputer(repositoryId, repository).compute(request).getTotal() == 0; return new GitLogComputer(repositoryId, repository, converterFactory).compute(request).getTotal() == 0;
} }
private String findRelevantRevisionForBranchIfToBeUpdated(String branch) { private String findRelevantRevisionForBranchIfToBeUpdated(String branch) {

View File

@@ -63,11 +63,9 @@ import static sonia.scm.repository.GitUtil.getBranchIdOrCurrentHead;
//~--- JDK imports ------------------------------------------------------------ //~--- JDK imports ------------------------------------------------------------
/** /**
*
* @author Sebastian Sdorra * @author Sebastian Sdorra
*/ */
class AbstractGitCommand class AbstractGitCommand {
{
/** /**
* the logger for AbstractGitCommand * the logger for AbstractGitCommand
@@ -78,10 +76,8 @@ class AbstractGitCommand
* Constructs ... * Constructs ...
* *
* @param context * @param context
*
*/ */
AbstractGitCommand(GitContext context) AbstractGitCommand(GitContext context) {
{
this.repository = context.getRepository(); this.repository = context.getRepository();
this.context = context; this.context = context;
} }
@@ -91,19 +87,16 @@ class AbstractGitCommand
/** /**
* Method description * Method description
* *
*
* @return * @return
*
* @throws IOException * @throws IOException
*/ */
Repository open() throws IOException Repository open() throws IOException {
{
return context.open(); return context.open();
} }
ObjectId getCommitOrDefault(Repository gitRepository, String requestedCommit) throws IOException { ObjectId getCommitOrDefault(Repository gitRepository, String requestedCommit) throws IOException {
ObjectId commit; ObjectId commit;
if ( Strings.isNullOrEmpty(requestedCommit) ) { if (Strings.isNullOrEmpty(requestedCommit)) {
commit = getDefaultBranch(gitRepository); commit = getDefaultBranch(gitRepository);
} else { } else {
commit = gitRepository.resolve(requestedCommit); commit = gitRepository.resolve(requestedCommit);
@@ -121,7 +114,7 @@ class AbstractGitCommand
} }
Ref getBranchOrDefault(Repository gitRepository, String requestedBranch) throws IOException { Ref getBranchOrDefault(Repository gitRepository, String requestedBranch) throws IOException {
if ( Strings.isNullOrEmpty(requestedBranch) ) { if (Strings.isNullOrEmpty(requestedBranch)) {
String defaultBranchName = context.getConfig().getDefaultBranch(); String defaultBranchName = context.getConfig().getDefaultBranch();
return getBranchIdOrCurrentHead(gitRepository, defaultBranchName); return getBranchIdOrCurrentHead(gitRepository, defaultBranchName);
} else { } else {
@@ -220,7 +213,7 @@ class AbstractGitCommand
} }
} }
Optional<RevCommit> doCommit(String message, Person author) { Optional<RevCommit> doCommit(String message, Person author, boolean sign) {
Person authorToUse = determineAuthor(author); Person authorToUse = determineAuthor(author);
try { try {
Status status = clone.status().call(); Status status = clone.status().call();
@@ -229,6 +222,8 @@ class AbstractGitCommand
.setAuthor(authorToUse.getName(), authorToUse.getMail()) .setAuthor(authorToUse.getName(), authorToUse.getMail())
.setCommitter("SCM-Manager", "noreply@scm-manager.org") .setCommitter("SCM-Manager", "noreply@scm-manager.org")
.setMessage(message) .setMessage(message)
.setSign(sign)
.setSigningKey(sign ? "SCM-MANAGER-DEFAULT-KEY" : null)
.call()); .call());
} else { } else {
return empty(); return empty();
@@ -288,9 +283,13 @@ class AbstractGitCommand
//~--- fields --------------------------------------------------------------- //~--- fields ---------------------------------------------------------------
/** Field description */ /**
* Field description
*/
protected GitContext context; protected GitContext context;
/** Field description */ /**
* Field description
*/
protected sonia.scm.repository.Repository repository; protected sonia.scm.repository.Repository repository;
} }

View File

@@ -36,6 +36,7 @@ import org.eclipse.jgit.revwalk.RevWalk;
import sonia.scm.repository.Changeset; import sonia.scm.repository.Changeset;
import sonia.scm.repository.ChangesetPagingResult; import sonia.scm.repository.ChangesetPagingResult;
import sonia.scm.repository.GitChangesetConverter; import sonia.scm.repository.GitChangesetConverter;
import sonia.scm.repository.GitChangesetConverterFactory;
import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.GitRepositoryHandler;
import sonia.scm.repository.GitUtil; import sonia.scm.repository.GitUtil;
import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.InternalRepositoryException;
@@ -58,18 +59,10 @@ public abstract class AbstractGitIncomingOutgoingCommand
/** Field description */ /** Field description */
private static final String REMOTE_REF_PREFIX = "refs/remote/scm/%s/"; private static final String REMOTE_REF_PREFIX = "refs/remote/scm/%s/";
//~--- constructors --------------------------------------------------------- AbstractGitIncomingOutgoingCommand(GitContext context, GitRepositoryHandler handler, GitChangesetConverterFactory converterFactory) {
/**
* Constructs ...
*
* @param handler
* @param context
*/
AbstractGitIncomingOutgoingCommand(GitRepositoryHandler handler, GitContext context)
{
super(context); super(context);
this.handler = handler; this.handler = handler;
this.converterFactory = converterFactory;
} }
//~--- methods -------------------------------------------------------------- //~--- methods --------------------------------------------------------------
@@ -132,7 +125,7 @@ public abstract class AbstractGitIncomingOutgoingCommand
try try
{ {
walk = new RevWalk(git.getRepository()); walk = new RevWalk(git.getRepository());
converter = new GitChangesetConverter(git.getRepository(), walk); converter = converterFactory.create(git.getRepository(), walk);
org.eclipse.jgit.api.LogCommand log = git.log(); org.eclipse.jgit.api.LogCommand log = git.log();
@@ -203,4 +196,5 @@ public abstract class AbstractGitIncomingOutgoingCommand
/** Field description */ /** Field description */
private GitRepositoryHandler handler; private GitRepositoryHandler handler;
private final GitChangesetConverterFactory converterFactory;
} }

View File

@@ -41,6 +41,7 @@ import sonia.scm.repository.GitUtil;
import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.Person; import sonia.scm.repository.Person;
import javax.inject.Inject;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -64,6 +65,7 @@ public class GitBlameCommand extends AbstractGitCommand implements BlameCommand
//~--- constructors --------------------------------------------------------- //~--- constructors ---------------------------------------------------------
@Inject
public GitBlameCommand(GitContext context) public GitBlameCommand(GitContext context)
{ {
super(context); super(context);

View File

@@ -43,6 +43,7 @@ import sonia.scm.repository.api.HookContext;
import sonia.scm.repository.api.HookContextFactory; import sonia.scm.repository.api.HookContextFactory;
import sonia.scm.repository.api.HookFeature; import sonia.scm.repository.api.HookFeature;
import javax.inject.Inject;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@@ -57,6 +58,7 @@ public class GitBranchCommand extends AbstractGitCommand implements BranchComman
private final HookContextFactory hookContextFactory; private final HookContextFactory hookContextFactory;
private final ScmEventBus eventBus; private final ScmEventBus eventBus;
@Inject
GitBranchCommand(GitContext context, HookContextFactory hookContextFactory, ScmEventBus eventBus) { GitBranchCommand(GitContext context, HookContextFactory hookContextFactory, ScmEventBus eventBus) {
super(context); super(context);
this.hookContextFactory = hookContextFactory; this.hookContextFactory = hookContextFactory;

View File

@@ -38,6 +38,7 @@ import sonia.scm.repository.Branch;
import sonia.scm.repository.GitUtil; import sonia.scm.repository.GitUtil;
import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.InternalRepositoryException;
import javax.inject.Inject;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@@ -53,6 +54,7 @@ public class GitBranchesCommand extends AbstractGitCommand implements BranchesCo
private static final Logger LOG = LoggerFactory.getLogger(GitBranchesCommand.class); private static final Logger LOG = LoggerFactory.getLogger(GitBranchesCommand.class);
@Inject
public GitBranchesCommand(GitContext context) public GitBranchesCommand(GitContext context)
{ {
super(context); super(context);

View File

@@ -57,6 +57,7 @@ import sonia.scm.store.BlobStore;
import sonia.scm.util.Util; import sonia.scm.util.Util;
import sonia.scm.web.lfs.LfsBlobStoreFactory; import sonia.scm.web.lfs.LfsBlobStoreFactory;
import javax.inject.Inject;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
@@ -111,6 +112,11 @@ public class GitBrowseCommand extends AbstractGitCommand
private int resultCount = 0; private int resultCount = 0;
@Inject
public GitBrowseCommand(GitContext context, LfsBlobStoreFactory lfsBlobStoreFactory, SyncAsyncExecutorProvider executorProvider) {
this(context, lfsBlobStoreFactory, executorProvider.createExecutorWithDefaultTimeout());
}
public GitBrowseCommand(GitContext context, LfsBlobStoreFactory lfsBlobStoreFactory, SyncAsyncExecutor executor) { public GitBrowseCommand(GitContext context, LfsBlobStoreFactory lfsBlobStoreFactory, SyncAsyncExecutor executor) {
super(context); super(context);
this.lfsBlobStoreFactory = lfsBlobStoreFactory; this.lfsBlobStoreFactory = lfsBlobStoreFactory;

View File

@@ -44,6 +44,7 @@ import sonia.scm.util.IOUtil;
import sonia.scm.util.Util; import sonia.scm.util.Util;
import sonia.scm.web.lfs.LfsBlobStoreFactory; import sonia.scm.web.lfs.LfsBlobStoreFactory;
import javax.inject.Inject;
import java.io.Closeable; import java.io.Closeable;
import java.io.FilterInputStream; import java.io.FilterInputStream;
import java.io.IOException; import java.io.IOException;
@@ -61,6 +62,7 @@ public class GitCatCommand extends AbstractGitCommand implements CatCommand {
private final LfsBlobStoreFactory lfsBlobStoreFactory; private final LfsBlobStoreFactory lfsBlobStoreFactory;
@Inject
public GitCatCommand(GitContext context, LfsBlobStoreFactory lfsBlobStoreFactory) { public GitCatCommand(GitContext context, LfsBlobStoreFactory lfsBlobStoreFactory) {
super(context); super(context);
this.lfsBlobStoreFactory = lfsBlobStoreFactory; this.lfsBlobStoreFactory = lfsBlobStoreFactory;

View File

@@ -0,0 +1,48 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository.spi;
import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider;
import sonia.scm.repository.GitRepositoryHandler;
import sonia.scm.repository.Repository;
import javax.inject.Inject;
class GitContextFactory {
private final GitRepositoryHandler handler;
private final GitRepositoryConfigStoreProvider storeProvider;
@Inject
GitContextFactory(GitRepositoryHandler handler, GitRepositoryConfigStoreProvider storeProvider) {
this.handler = handler;
this.storeProvider = storeProvider;
}
GitContext create(Repository repository) {
return new GitContext(handler.getDirectory(repository.getId()), repository, storeProvider);
}
}

View File

@@ -29,6 +29,7 @@ import org.eclipse.jgit.diff.DiffFormatter;
import org.eclipse.jgit.util.QuotedString; import org.eclipse.jgit.util.QuotedString;
import sonia.scm.repository.api.DiffCommandBuilder; import sonia.scm.repository.api.DiffCommandBuilder;
import javax.inject.Inject;
import java.io.BufferedOutputStream; import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
@@ -41,6 +42,7 @@ import static java.nio.charset.StandardCharsets.UTF_8;
*/ */
public class GitDiffCommand extends AbstractGitCommand implements DiffCommand { public class GitDiffCommand extends AbstractGitCommand implements DiffCommand {
@Inject
GitDiffCommand(GitContext context) { GitDiffCommand(GitContext context) {
super(context); super(context);
} }

View File

@@ -32,6 +32,7 @@ import sonia.scm.repository.api.DiffFile;
import sonia.scm.repository.api.DiffResult; import sonia.scm.repository.api.DiffResult;
import sonia.scm.repository.api.Hunk; import sonia.scm.repository.api.Hunk;
import javax.inject.Inject;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.util.Iterator; import java.util.Iterator;
@@ -39,6 +40,7 @@ import java.util.stream.Collectors;
public class GitDiffResultCommand extends AbstractGitCommand implements DiffResultCommand { public class GitDiffResultCommand extends AbstractGitCommand implements DiffResultCommand {
@Inject
GitDiffResultCommand(GitContext context) { GitDiffResultCommand(GitContext context) {
super(context); super(context);
} }

View File

@@ -29,6 +29,7 @@ package sonia.scm.repository.spi;
import org.eclipse.jgit.transport.ReceiveCommand; import org.eclipse.jgit.transport.ReceiveCommand;
import org.eclipse.jgit.transport.ReceivePack; import org.eclipse.jgit.transport.ReceivePack;
import sonia.scm.repository.GitChangesetConverterFactory;
import sonia.scm.repository.GitHookChangesetCollector; import sonia.scm.repository.GitHookChangesetCollector;
//~--- JDK imports ------------------------------------------------------------ //~--- JDK imports ------------------------------------------------------------
@@ -39,56 +40,27 @@ import java.util.List;
* *
* @author Sebastian Sdorra * @author Sebastian Sdorra
*/ */
public class GitHookChangesetProvider implements HookChangesetProvider public class GitHookChangesetProvider implements HookChangesetProvider {
{
/** private final GitChangesetConverterFactory converterFactory;
* Constructs ... private final ReceivePack receivePack;
* private final List<ReceiveCommand> receiveCommands;
*
* @param receivePack private HookChangesetResponse response;
* @param receiveCommands
*/ public GitHookChangesetProvider(GitChangesetConverterFactory converterFactory, ReceivePack receivePack,
public GitHookChangesetProvider(ReceivePack receivePack, List<ReceiveCommand> receiveCommands) {
List<ReceiveCommand> receiveCommands) this.converterFactory = converterFactory;
{
this.receivePack = receivePack; this.receivePack = receivePack;
this.receiveCommands = receiveCommands; this.receiveCommands = receiveCommands;
} }
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param request
*
* @return
*/
@Override @Override
public synchronized HookChangesetResponse handleRequest( public synchronized HookChangesetResponse handleRequest(HookChangesetRequest request) {
HookChangesetRequest request) if (response == null) {
{ GitHookChangesetCollector collector = new GitHookChangesetCollector(converterFactory, receivePack, receiveCommands);
if (response == null)
{
GitHookChangesetCollector collector =
new GitHookChangesetCollector(receivePack, receiveCommands);
response = new HookChangesetResponse(collector.collectChangesets()); response = new HookChangesetResponse(collector.collectChangesets());
} }
return response; return response;
} }
//~--- fields ---------------------------------------------------------------
/** Field description */
private List<ReceiveCommand> receiveCommands;
/** Field description */
private ReceivePack receivePack;
/** Field description */
private HookChangesetResponse response;
} }

View File

@@ -30,6 +30,7 @@ import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.transport.ReceiveCommand; import org.eclipse.jgit.transport.ReceiveCommand;
import org.eclipse.jgit.transport.ReceivePack; import org.eclipse.jgit.transport.ReceivePack;
import sonia.scm.repository.api.GitHookBranchProvider; import sonia.scm.repository.api.GitHookBranchProvider;
import sonia.scm.repository.GitChangesetConverterFactory;
import sonia.scm.repository.api.GitHookMessageProvider; import sonia.scm.repository.api.GitHookMessageProvider;
import sonia.scm.repository.api.GitHookTagProvider; import sonia.scm.repository.api.GitHookTagProvider;
import sonia.scm.repository.api.GitReceiveHookMergeDetectionProvider; import sonia.scm.repository.api.GitReceiveHookMergeDetectionProvider;
@@ -62,13 +63,15 @@ public class GitHookContextProvider extends HookContextProvider
//~--- constructors --------------------------------------------------------- //~--- constructors ---------------------------------------------------------
private final GitChangesetConverterFactory converterFactory;
/** /**
* Constructs a new instance * Constructs a new instance
* @param receivePack git receive pack * @param receivePack git receive pack
* @param receiveCommands received commands * @param receiveCommands received commands
*/ */
public GitHookContextProvider( public GitHookContextProvider(
ReceivePack receivePack, GitChangesetConverterFactory converterFactory, ReceivePack receivePack,
List<ReceiveCommand> receiveCommands, List<ReceiveCommand> receiveCommands,
Repository repository, Repository repository,
String repositoryId String repositoryId
@@ -77,8 +80,9 @@ public class GitHookContextProvider extends HookContextProvider
this.receiveCommands = receiveCommands; this.receiveCommands = receiveCommands;
this.repository = repository; this.repository = repository;
this.repositoryId = repositoryId; this.repositoryId = repositoryId;
this.changesetProvider = new GitHookChangesetProvider(receivePack, this.changesetProvider = new GitHookChangesetProvider(converterFactory, receivePack,
receiveCommands); receiveCommands);
this.converterFactory = converterFactory;
} }
//~--- methods -------------------------------------------------------------- //~--- methods --------------------------------------------------------------
@@ -110,7 +114,7 @@ public class GitHookContextProvider extends HookContextProvider
@Override @Override
public HookMergeDetectionProvider getMergeDetectionProvider() { public HookMergeDetectionProvider getMergeDetectionProvider() {
return new GitReceiveHookMergeDetectionProvider(repository, repositoryId, receiveCommands); return new GitReceiveHookMergeDetectionProvider(repository, repositoryId, receiveCommands, converterFactory);
} }
@Override @Override

View File

@@ -29,8 +29,10 @@ package sonia.scm.repository.spi;
import org.eclipse.jgit.api.LogCommand; import org.eclipse.jgit.api.LogCommand;
import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectId;
import sonia.scm.repository.ChangesetPagingResult; import sonia.scm.repository.ChangesetPagingResult;
import sonia.scm.repository.GitChangesetConverterFactory;
import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.GitRepositoryHandler;
import javax.inject.Inject;
import java.io.IOException; import java.io.IOException;
//~--- JDK imports ------------------------------------------------------------ //~--- JDK imports ------------------------------------------------------------
@@ -40,18 +42,11 @@ import java.io.IOException;
* @author Sebastian Sdorra * @author Sebastian Sdorra
*/ */
public class GitIncomingCommand extends AbstractGitIncomingOutgoingCommand public class GitIncomingCommand extends AbstractGitIncomingOutgoingCommand
implements IncomingCommand implements IncomingCommand {
{
/** @Inject
* Constructs ... GitIncomingCommand(GitContext context, GitRepositoryHandler handler, GitChangesetConverterFactory converterFactory) {
* super(context, handler, converterFactory);
* @param handler
* @param context
*/
GitIncomingCommand(GitRepositoryHandler handler, GitContext context)
{
super(handler, context);
} }
//~--- get methods ---------------------------------------------------------- //~--- get methods ----------------------------------------------------------

View File

@@ -36,10 +36,12 @@ import org.slf4j.LoggerFactory;
import sonia.scm.repository.Changeset; import sonia.scm.repository.Changeset;
import sonia.scm.repository.ChangesetPagingResult; import sonia.scm.repository.ChangesetPagingResult;
import sonia.scm.repository.GitChangesetConverter; import sonia.scm.repository.GitChangesetConverter;
import sonia.scm.repository.GitChangesetConverterFactory;
import sonia.scm.repository.GitUtil; import sonia.scm.repository.GitUtil;
import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.util.IOUtil; import sonia.scm.util.IOUtil;
import javax.inject.Inject;
import java.io.IOException; import java.io.IOException;
import static sonia.scm.ContextEntry.ContextBuilder.entity; import static sonia.scm.ContextEntry.ContextBuilder.entity;
@@ -60,6 +62,7 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
private static final Logger logger = private static final Logger logger =
LoggerFactory.getLogger(GitLogCommand.class); LoggerFactory.getLogger(GitLogCommand.class);
public static final String REVISION = "Revision"; public static final String REVISION = "Revision";
private final GitChangesetConverterFactory converterFactory;
//~--- constructors --------------------------------------------------------- //~--- constructors ---------------------------------------------------------
@@ -70,9 +73,11 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
* @param context * @param context
* *
*/ */
GitLogCommand(GitContext context) @Inject
GitLogCommand(GitContext context, GitChangesetConverterFactory converterFactory)
{ {
super(context); super(context);
this.converterFactory = converterFactory;
} }
//~--- get methods ---------------------------------------------------------- //~--- get methods ----------------------------------------------------------
@@ -110,7 +115,7 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
if (commit != null) if (commit != null)
{ {
converter = new GitChangesetConverter(gr, revWalk); converter = converterFactory.create(gr, revWalk);
if (isBranchRequested(request)) { if (isBranchRequested(request)) {
String branch = request.getBranch(); String branch = request.getBranch();
@@ -177,7 +182,7 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
if (Strings.isNullOrEmpty(request.getBranch())) { if (Strings.isNullOrEmpty(request.getBranch())) {
request.setBranch(context.getConfig().getDefaultBranch()); request.setBranch(context.getConfig().getDefaultBranch());
} }
return new GitLogComputer(this.repository.getId(), gitRepository).compute(request); return new GitLogComputer(this.repository.getId(), gitRepository, converterFactory).compute(request);
} catch (IOException e) { } catch (IOException e) {
throw new InternalRepositoryException(repository, "could not create change log", e); throw new InternalRepositoryException(repository, "could not create change log", e);
} }

View File

@@ -42,6 +42,7 @@ import sonia.scm.NotFoundException;
import sonia.scm.repository.Changeset; import sonia.scm.repository.Changeset;
import sonia.scm.repository.ChangesetPagingResult; import sonia.scm.repository.ChangesetPagingResult;
import sonia.scm.repository.GitChangesetConverter; import sonia.scm.repository.GitChangesetConverter;
import sonia.scm.repository.GitChangesetConverterFactory;
import sonia.scm.repository.GitUtil; import sonia.scm.repository.GitUtil;
import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.util.IOUtil; import sonia.scm.util.IOUtil;
@@ -59,10 +60,12 @@ public class GitLogComputer {
private final String repositoryId; private final String repositoryId;
private final Repository gitRepository; private final Repository gitRepository;
private final GitChangesetConverterFactory converterFactory;
public GitLogComputer(String repositoryId, Repository repository) { public GitLogComputer(String repositoryId, Repository repository, GitChangesetConverterFactory converterFactory) {
this.repositoryId = repositoryId; this.repositoryId = repositoryId;
this.gitRepository = repository; this.gitRepository = repository;
this.converterFactory = converterFactory;
} }
public ChangesetPagingResult compute(LogCommandRequest request) { public ChangesetPagingResult compute(LogCommandRequest request) {
@@ -123,7 +126,7 @@ public class GitLogComputer {
revWalk = new RevWalk(gitRepository); revWalk = new RevWalk(gitRepository);
converter = new GitChangesetConverter(gitRepository, revWalk); converter = converterFactory.create(gitRepository, revWalk);
if (!Strings.isNullOrEmpty(request.getPath())) { if (!Strings.isNullOrEmpty(request.getPath())) {
revWalk.setTreeFilter( revWalk.setTreeFilter(

View File

@@ -36,6 +36,7 @@ import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.merge.ResolveMerger; import org.eclipse.jgit.merge.ResolveMerger;
import org.eclipse.jgit.treewalk.CanonicalTreeParser; import org.eclipse.jgit.treewalk.CanonicalTreeParser;
import org.eclipse.jgit.treewalk.filter.PathFilter; import org.eclipse.jgit.treewalk.filter.PathFilter;
import sonia.scm.repository.GitRepositoryHandler;
import sonia.scm.repository.GitWorkingCopyFactory; import sonia.scm.repository.GitWorkingCopyFactory;
import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.api.MergeCommandResult; import sonia.scm.repository.api.MergeCommandResult;
@@ -43,6 +44,7 @@ import sonia.scm.repository.api.MergeDryRunCommandResult;
import sonia.scm.repository.api.MergeStrategy; import sonia.scm.repository.api.MergeStrategy;
import sonia.scm.repository.api.MergeStrategyNotSupportedException; import sonia.scm.repository.api.MergeStrategyNotSupportedException;
import javax.inject.Inject;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.util.Set; import java.util.Set;
@@ -61,6 +63,11 @@ public class GitMergeCommand extends AbstractGitCommand implements MergeCommand
MergeStrategy.SQUASH MergeStrategy.SQUASH
); );
@Inject
GitMergeCommand(GitContext context, GitRepositoryHandler handler) {
this(context, handler.getWorkingCopyFactory());
}
GitMergeCommand(GitContext context, GitWorkingCopyFactory workingCopyFactory) { GitMergeCommand(GitContext context, GitWorkingCopyFactory workingCopyFactory) {
super(context); super(context);
this.workingCopyFactory = workingCopyFactory; this.workingCopyFactory = workingCopyFactory;

View File

@@ -56,6 +56,7 @@ abstract class GitMergeStrategy extends AbstractGitCommand.GitCloneWorker<MergeC
private final ObjectId revisionToMerge; private final ObjectId revisionToMerge;
private final Person author; private final Person author;
private final String messageTemplate; private final String messageTemplate;
private final boolean sign;
GitMergeStrategy(Git clone, MergeCommandRequest request, GitContext context, sonia.scm.repository.Repository repository) { GitMergeStrategy(Git clone, MergeCommandRequest request, GitContext context, sonia.scm.repository.Repository repository) {
super(clone, context, repository); super(clone, context, repository);
@@ -63,6 +64,7 @@ abstract class GitMergeStrategy extends AbstractGitCommand.GitCloneWorker<MergeC
this.branchToMerge = request.getBranchToMerge(); this.branchToMerge = request.getBranchToMerge();
this.author = request.getAuthor(); this.author = request.getAuthor();
this.messageTemplate = request.getMessageTemplate(); this.messageTemplate = request.getMessageTemplate();
this.sign = request.isSign();
try { try {
this.targetRevision = resolveRevision(request.getTargetBranch()); this.targetRevision = resolveRevision(request.getTargetBranch());
this.revisionToMerge = resolveRevision(request.getBranchToMerge()); this.revisionToMerge = resolveRevision(request.getBranchToMerge());
@@ -88,7 +90,7 @@ abstract class GitMergeStrategy extends AbstractGitCommand.GitCloneWorker<MergeC
Optional<RevCommit> doCommit() { Optional<RevCommit> doCommit() {
logger.debug("merged branch {} into {}", branchToMerge, targetBranch); logger.debug("merged branch {} into {}", branchToMerge, targetBranch);
return doCommit(MessageFormat.format(determineMessageTemplate(), branchToMerge, targetBranch), author); return doCommit(MessageFormat.format(determineMessageTemplate(), branchToMerge, targetBranch), author, sign);
} }
MergeCommandResult createSuccessResult(String newRevision) { MergeCommandResult createSuccessResult(String newRevision) {

View File

@@ -41,6 +41,7 @@ import sonia.scm.repository.Modified;
import sonia.scm.repository.Removed; import sonia.scm.repository.Removed;
import sonia.scm.repository.Renamed; import sonia.scm.repository.Renamed;
import javax.inject.Inject;
import java.io.IOException; import java.io.IOException;
import java.text.MessageFormat; import java.text.MessageFormat;
import java.util.ArrayList; import java.util.ArrayList;
@@ -53,7 +54,8 @@ import static sonia.scm.ContextEntry.ContextBuilder.entity;
@Slf4j @Slf4j
public class GitModificationsCommand extends AbstractGitCommand implements ModificationsCommand { public class GitModificationsCommand extends AbstractGitCommand implements ModificationsCommand {
protected GitModificationsCommand(GitContext context) { @Inject
GitModificationsCommand(GitContext context) {
super(context); super(context);
} }

View File

@@ -34,11 +34,13 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.ConcurrentModificationException; import sonia.scm.ConcurrentModificationException;
import sonia.scm.NoChangesMadeException; import sonia.scm.NoChangesMadeException;
import sonia.scm.repository.GitRepositoryHandler;
import sonia.scm.repository.GitWorkingCopyFactory; import sonia.scm.repository.GitWorkingCopyFactory;
import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.web.lfs.LfsBlobStoreFactory; import sonia.scm.web.lfs.LfsBlobStoreFactory;
import javax.inject.Inject;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Path; import java.nio.file.Path;
@@ -53,6 +55,11 @@ public class GitModifyCommand extends AbstractGitCommand implements ModifyComman
private final GitWorkingCopyFactory workingCopyFactory; private final GitWorkingCopyFactory workingCopyFactory;
private final LfsBlobStoreFactory lfsBlobStoreFactory; private final LfsBlobStoreFactory lfsBlobStoreFactory;
@Inject
GitModifyCommand(GitContext context, GitRepositoryHandler repositoryHandler, LfsBlobStoreFactory lfsBlobStoreFactory) {
this(context, repositoryHandler.getWorkingCopyFactory(), lfsBlobStoreFactory);
}
GitModifyCommand(GitContext context, GitWorkingCopyFactory workingCopyFactory, LfsBlobStoreFactory lfsBlobStoreFactory) { GitModifyCommand(GitContext context, GitWorkingCopyFactory workingCopyFactory, LfsBlobStoreFactory lfsBlobStoreFactory) {
super(context); super(context);
this.workingCopyFactory = workingCopyFactory; this.workingCopyFactory = workingCopyFactory;
@@ -86,7 +93,7 @@ public class GitModifyCommand extends AbstractGitCommand implements ModifyComman
r.execute(this); r.execute(this);
} }
failIfNotChanged(() -> new NoChangesMadeException(repository, ModifyWorker.this.request.getBranch())); failIfNotChanged(() -> new NoChangesMadeException(repository, ModifyWorker.this.request.getBranch()));
Optional<RevCommit> revCommit = doCommit(request.getCommitMessage(), request.getAuthor()); Optional<RevCommit> revCommit = doCommit(request.getCommitMessage(), request.getAuthor(), request.isSign());
push(); push();
return revCommit.orElseThrow(() -> new NoChangesMadeException(repository, ModifyWorker.this.request.getBranch())).name(); return revCommit.orElseThrow(() -> new NoChangesMadeException(repository, ModifyWorker.this.request.getBranch())).name();
} }

View File

@@ -29,8 +29,10 @@ package sonia.scm.repository.spi;
import org.eclipse.jgit.api.LogCommand; import org.eclipse.jgit.api.LogCommand;
import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectId;
import sonia.scm.repository.ChangesetPagingResult; import sonia.scm.repository.ChangesetPagingResult;
import sonia.scm.repository.GitChangesetConverterFactory;
import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.GitRepositoryHandler;
import javax.inject.Inject;
import java.io.IOException; import java.io.IOException;
//~--- JDK imports ------------------------------------------------------------ //~--- JDK imports ------------------------------------------------------------
@@ -40,18 +42,12 @@ import java.io.IOException;
* @author Sebastian Sdorra * @author Sebastian Sdorra
*/ */
public class GitOutgoingCommand extends AbstractGitIncomingOutgoingCommand public class GitOutgoingCommand extends AbstractGitIncomingOutgoingCommand
implements OutgoingCommand implements OutgoingCommand {
{
/** @Inject
* Constructs ... GitOutgoingCommand(GitContext context, GitRepositoryHandler handler, GitChangesetConverterFactory converterFactory)
*
* @param handler
* @param context
*/
GitOutgoingCommand(GitRepositoryHandler handler, GitContext context)
{ {
super(handler, context); super(context, handler, converterFactory);
} }
//~--- get methods ---------------------------------------------------------- //~--- get methods ----------------------------------------------------------

View File

@@ -44,6 +44,7 @@ import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.api.PullResponse; import sonia.scm.repository.api.PullResponse;
import javax.inject.Inject;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.net.URL; import java.net.URL;
@@ -73,6 +74,7 @@ public class GitPullCommand extends AbstractGitPushOrPullCommand
* @param handler * @param handler
* @param context * @param context
*/ */
@Inject
public GitPullCommand(GitRepositoryHandler handler, GitContext context) public GitPullCommand(GitRepositoryHandler handler, GitContext context)
{ {
super(handler, context); super(handler, context);

View File

@@ -31,6 +31,7 @@ import org.slf4j.LoggerFactory;
import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.GitRepositoryHandler;
import sonia.scm.repository.api.PushResponse; import sonia.scm.repository.api.PushResponse;
import javax.inject.Inject;
import java.io.IOException; import java.io.IOException;
//~--- JDK imports ------------------------------------------------------------ //~--- JDK imports ------------------------------------------------------------
@@ -55,8 +56,8 @@ public class GitPushCommand extends AbstractGitPushOrPullCommand
* @param handler * @param handler
* @param context * @param context
*/ */
public GitPushCommand(GitRepositoryHandler handler, GitContext context) @Inject
{ public GitPushCommand(GitRepositoryHandler handler, GitContext context) {
super(handler, context); super(handler, context);
this.handler = handler; this.handler = handler;
} }

View File

@@ -25,21 +25,14 @@
package sonia.scm.repository.spi; package sonia.scm.repository.spi;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider; import com.google.inject.AbstractModule;
import sonia.scm.event.ScmEventBus; import com.google.inject.Injector;
import sonia.scm.repository.Feature; import sonia.scm.repository.Feature;
import sonia.scm.repository.GitRepositoryHandler;
import sonia.scm.repository.Repository;
import sonia.scm.repository.api.Command; import sonia.scm.repository.api.Command;
import sonia.scm.repository.api.HookContextFactory;
import sonia.scm.web.lfs.LfsBlobStoreFactory;
import java.io.IOException;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.Set; import java.util.Set;
//~--- JDK imports ------------------------------------------------------------
/** /**
* *
* @author Sebastian Sdorra * @author Sebastian Sdorra
@@ -47,8 +40,6 @@ import java.util.Set;
public class GitRepositoryServiceProvider extends RepositoryServiceProvider public class GitRepositoryServiceProvider extends RepositoryServiceProvider
{ {
/** Field description */
//J-
public static final Set<Command> COMMANDS = ImmutableSet.of( public static final Set<Command> COMMANDS = ImmutableSet.of(
Command.BLAME, Command.BLAME,
Command.BROWSE, Command.BROWSE,
@@ -66,105 +57,51 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider
Command.MERGE, Command.MERGE,
Command.MODIFY Command.MODIFY
); );
protected static final Set<Feature> FEATURES = EnumSet.of(Feature.INCOMING_REVISION); protected static final Set<Feature> FEATURES = EnumSet.of(Feature.INCOMING_REVISION);
//J+
private final GitContext context;
private final Injector commandInjector;
//~--- constructors --------------------------------------------------------- //~--- constructors ---------------------------------------------------------
public GitRepositoryServiceProvider(GitRepositoryHandler handler, Repository repository, GitRepositoryConfigStoreProvider storeProvider, LfsBlobStoreFactory lfsBlobStoreFactory, HookContextFactory hookContextFactory, ScmEventBus eventBus, SyncAsyncExecutorProvider executorProvider) { GitRepositoryServiceProvider(Injector injector, GitContext context) {
this.handler = handler; this.context = context;
this.lfsBlobStoreFactory = lfsBlobStoreFactory; commandInjector = injector.createChildInjector(new AbstractModule() {
this.hookContextFactory = hookContextFactory; @Override
this.eventBus = eventBus; protected void configure() {
this.executorProvider = executorProvider; bind(GitContext.class).toInstance(context);
this.context = new GitContext(handler.getDirectory(repository.getId()), repository, storeProvider); }
});
} }
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @throws IOException
*/
@Override @Override
public void close() throws IOException public BlameCommand getBlameCommand() {
{
context.close();
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @return
*/
@Override
public BlameCommand getBlameCommand()
{
return new GitBlameCommand(context); return new GitBlameCommand(context);
} }
/**
* Method description
*
*
* @return
*/
@Override @Override
public BranchesCommand getBranchesCommand() public BranchesCommand getBranchesCommand() {
{
return new GitBranchesCommand(context); return new GitBranchesCommand(context);
} }
/**
* Method description
*
*
* @return
*/
@Override @Override
public BranchCommand getBranchCommand() public BranchCommand getBranchCommand() {
{ return commandInjector.getInstance(GitBranchCommand.class);
return new GitBranchCommand(context, hookContextFactory, eventBus);
} }
/**
* Method description
*
*
* @return
*/
@Override @Override
public BrowseCommand getBrowseCommand() public BrowseCommand getBrowseCommand() {
{ return commandInjector.getInstance(GitBrowseCommand.class);
return new GitBrowseCommand(context, lfsBlobStoreFactory, executorProvider.createExecutorWithDefaultTimeout());
} }
/**
* Method description
*
*
* @return
*/
@Override @Override
public CatCommand getCatCommand() public CatCommand getCatCommand() {
{ return commandInjector.getInstance(GitCatCommand.class);
return new GitCatCommand(context, lfsBlobStoreFactory);
} }
/**
* Method description
*
*
* @return
*/
@Override @Override
public DiffCommand getDiffCommand() public DiffCommand getDiffCommand() {
{
return new GitDiffCommand(context); return new GitDiffCommand(context);
} }
@@ -173,28 +110,14 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider
return new GitDiffResultCommand(context); return new GitDiffResultCommand(context);
} }
/**
* Method description
*
*
* @return
*/
@Override @Override
public IncomingCommand getIncomingCommand() public IncomingCommand getIncomingCommand() {
{ return commandInjector.getInstance(GitIncomingCommand.class);
return new GitIncomingCommand(handler, context);
} }
/**
* Method description
*
*
* @return
*/
@Override @Override
public LogCommand getLogCommand() public LogCommand getLogCommand() {
{ return commandInjector.getInstance(GitLogCommand.class);
return new GitLogCommand(context);
} }
@Override @Override
@@ -202,93 +125,48 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider
return new GitModificationsCommand(context); return new GitModificationsCommand(context);
} }
/**
* Method description
*
*
* @return
*/
@Override @Override
public OutgoingCommand getOutgoingCommand() public OutgoingCommand getOutgoingCommand() {
{ return commandInjector.getInstance(GitOutgoingCommand.class);
return new GitOutgoingCommand(handler, context);
} }
/**
* Method description
*
*
* @return
*/
@Override @Override
public PullCommand getPullCommand() public PullCommand getPullCommand() {
{ return commandInjector.getInstance(GitPullCommand.class);
return new GitPullCommand(handler, context);
} }
/**
* Method description
*
*
* @return
*/
@Override @Override
public PushCommand getPushCommand() public PushCommand getPushCommand() {
{ return commandInjector.getInstance(GitPushCommand.class);
return new GitPushCommand(handler, context);
} }
/**
* Method description
*
*
* @return
*/
@Override @Override
public Set<Command> getSupportedCommands() public TagsCommand getTagsCommand() {
{
return COMMANDS;
}
/**
* Method description
*
*
* @return
*/
@Override
public TagsCommand getTagsCommand()
{
return new GitTagsCommand(context); return new GitTagsCommand(context);
} }
@Override @Override
public MergeCommand getMergeCommand() { public MergeCommand getMergeCommand() {
return new GitMergeCommand(context, handler.getWorkingCopyFactory()); return commandInjector.getInstance(GitMergeCommand.class);
} }
@Override @Override
public ModifyCommand getModifyCommand() { public ModifyCommand getModifyCommand() {
return new GitModifyCommand(context, handler.getWorkingCopyFactory(), lfsBlobStoreFactory); return commandInjector.getInstance(GitModifyCommand.class);
}
@Override
public Set<Command> getSupportedCommands() {
return COMMANDS;
} }
@Override @Override
public Set<Feature> getSupportedFeatures() { public Set<Feature> getSupportedFeatures() {
return FEATURES; return FEATURES;
} }
//~--- fields ---------------------------------------------------------------
/** Field description */ @Override
private final GitContext context; public void close() {
context.close();
/** Field description */ }
private final GitRepositoryHandler handler;
private final LfsBlobStoreFactory lfsBlobStoreFactory;
private final HookContextFactory hookContextFactory;
private final ScmEventBus eventBus;
private final SyncAsyncExecutorProvider executorProvider;
} }

View File

@@ -27,13 +27,10 @@ package sonia.scm.repository.spi;
//~--- non-JDK imports -------------------------------------------------------- //~--- non-JDK imports --------------------------------------------------------
import com.google.inject.Inject; import com.google.inject.Inject;
import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider; import com.google.inject.Injector;
import sonia.scm.event.ScmEventBus;
import sonia.scm.plugin.Extension; import sonia.scm.plugin.Extension;
import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.GitRepositoryHandler;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.api.HookContextFactory;
import sonia.scm.web.lfs.LfsBlobStoreFactory;
/** /**
* *
@@ -42,31 +39,20 @@ import sonia.scm.web.lfs.LfsBlobStoreFactory;
@Extension @Extension
public class GitRepositoryServiceResolver implements RepositoryServiceResolver { public class GitRepositoryServiceResolver implements RepositoryServiceResolver {
private final GitRepositoryHandler handler; private final Injector injector;
private final GitRepositoryConfigStoreProvider storeProvider; private final GitContextFactory contextFactory;
private final LfsBlobStoreFactory lfsBlobStoreFactory;
private final HookContextFactory hookContextFactory;
private final ScmEventBus eventBus;
private final SyncAsyncExecutorProvider executorProvider;
@Inject @Inject
public GitRepositoryServiceResolver(GitRepositoryHandler handler, GitRepositoryConfigStoreProvider storeProvider, LfsBlobStoreFactory lfsBlobStoreFactory, HookContextFactory hookContextFactory, ScmEventBus eventBus, SyncAsyncExecutorProvider executorProvider) { public GitRepositoryServiceResolver(Injector injector, GitContextFactory contextFactory) {
this.handler = handler; this.injector = injector;
this.storeProvider = storeProvider; this.contextFactory = contextFactory;
this.lfsBlobStoreFactory = lfsBlobStoreFactory;
this.hookContextFactory = hookContextFactory;
this.eventBus = eventBus;
this.executorProvider = executorProvider;
} }
@Override @Override
public GitRepositoryServiceProvider resolve(Repository repository) { public GitRepositoryServiceProvider resolve(Repository repository) {
GitRepositoryServiceProvider provider = null;
if (GitRepositoryHandler.TYPE_NAME.equalsIgnoreCase(repository.getType())) { if (GitRepositoryHandler.TYPE_NAME.equalsIgnoreCase(repository.getType())) {
provider = new GitRepositoryServiceProvider(handler, repository, storeProvider, lfsBlobStoreFactory, hookContextFactory, eventBus, executorProvider); return new GitRepositoryServiceProvider(injector, contextFactory.create(repository));
} }
return null;
return provider;
} }
} }

View File

@@ -34,6 +34,7 @@ import org.eclipse.jgit.transport.ReceiveCommand;
import org.eclipse.jgit.transport.ReceivePack; import org.eclipse.jgit.transport.ReceivePack;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.repository.GitChangesetConverterFactory;
import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.GitRepositoryHandler;
import sonia.scm.repository.RepositoryHookType; import sonia.scm.repository.RepositoryHookType;
import sonia.scm.repository.spi.GitHookContextProvider; import sonia.scm.repository.spi.GitHookContextProvider;
@@ -66,9 +67,10 @@ public class GitReceiveHook implements PreReceiveHook, PostReceiveHook
* @param hookEventFacade * @param hookEventFacade
* @param handler * @param handler
*/ */
public GitReceiveHook(HookEventFacade hookEventFacade, public GitReceiveHook(GitChangesetConverterFactory converterFactory, HookEventFacade hookEventFacade,
GitRepositoryHandler handler) GitRepositoryHandler handler)
{ {
this.converterFactory = converterFactory;
this.hookEventFacade = hookEventFacade; this.hookEventFacade = hookEventFacade;
this.handler = handler; this.handler = handler;
} }
@@ -122,7 +124,7 @@ public class GitReceiveHook implements PreReceiveHook, PostReceiveHook
logger.trace("resolved repository to {}", repositoryId); logger.trace("resolved repository to {}", repositoryId);
GitHookContextProvider context = new GitHookContextProvider(rpack, receiveCommands, repository, repositoryId); GitHookContextProvider context = new GitHookContextProvider(converterFactory, rpack, receiveCommands, repository, repositoryId);
hookEventFacade.handle(repositoryId).fireHookEvent(type, context); hookEventFacade.handle(repositoryId).fireHookEvent(type, context);
@@ -187,6 +189,7 @@ public class GitReceiveHook implements PreReceiveHook, PostReceiveHook
/** Field description */ /** Field description */
private GitRepositoryHandler handler; private GitRepositoryHandler handler;
private final GitChangesetConverterFactory converterFactory;
/** Field description */ /** Field description */
private HookEventFacade hookEventFacade; private HookEventFacade hookEventFacade;
} }

View File

@@ -34,6 +34,7 @@ import org.eclipse.jgit.transport.resolver.ReceivePackFactory;
import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException; import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException;
import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException; import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException;
import sonia.scm.protocolcommand.git.BaseReceivePackFactory; import sonia.scm.protocolcommand.git.BaseReceivePackFactory;
import sonia.scm.repository.GitChangesetConverterFactory;
import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.GitRepositoryHandler;
import sonia.scm.repository.spi.HookEventFacade; import sonia.scm.repository.spi.HookEventFacade;
@@ -53,8 +54,8 @@ public class GitReceivePackFactory extends BaseReceivePackFactory<HttpServletReq
private ReceivePackFactory<HttpServletRequest> wrapped; private ReceivePackFactory<HttpServletRequest> wrapped;
@Inject @Inject
public GitReceivePackFactory(GitRepositoryHandler handler, HookEventFacade hookEventFacade) { public GitReceivePackFactory(GitChangesetConverterFactory converterFactory, GitRepositoryHandler handler, HookEventFacade hookEventFacade) {
super(handler, hookEventFacade); super(converterFactory, handler, hookEventFacade);
this.wrapped = new DefaultReceivePackFactory(); this.wrapped = new DefaultReceivePackFactory();
} }

View File

@@ -40,6 +40,7 @@ import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner; import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.repository.GitConfig; import sonia.scm.repository.GitConfig;
import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.GitRepositoryHandler;
import sonia.scm.repository.GitTestHelper;
import sonia.scm.web.CollectingPackParserListener; import sonia.scm.web.CollectingPackParserListener;
import sonia.scm.web.GitReceiveHook; import sonia.scm.web.GitReceiveHook;
@@ -82,7 +83,7 @@ public class BaseReceivePackFactoryTest {
ReceivePack receivePack = new ReceivePack(repository); ReceivePack receivePack = new ReceivePack(repository);
when(wrappedReceivePackFactory.create(request, repository)).thenReturn(receivePack); when(wrappedReceivePackFactory.create(request, repository)).thenReturn(receivePack);
factory = new BaseReceivePackFactory<Object>(handler, null) { factory = new BaseReceivePackFactory<Object>(GitTestHelper.createConverterFactory(), handler, null) {
@Override @Override
protected ReceivePack createBasicReceivePack(Object request, Repository repository) throws ServiceNotEnabledException, ServiceNotAuthorizedException { protected ReceivePack createBasicReceivePack(Object request, Repository repository) throws ServiceNotEnabledException, ServiceNotAuthorizedException {
return wrappedReceivePackFactory.create(request, repository); return wrappedReceivePackFactory.create(request, repository);

View File

@@ -0,0 +1,304 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
import org.bouncycastle.bcpg.ArmoredOutputStream;
import org.bouncycastle.bcpg.BCPGOutputStream;
import org.bouncycastle.bcpg.HashAlgorithmTags;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPKeyPair;
import org.bouncycastle.openpgp.PGPPrivateKey;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.openpgp.PGPSignatureGenerator;
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder;
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPKeyPair;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.api.errors.JGitInternalException;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.GpgSignature;
import org.eclipse.jgit.lib.GpgSigner;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
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.junit.jupiter.api.io.TempDir;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.security.GPG;
import sonia.scm.security.PublicKey;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.Security;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class GitChangesetConverterTest {
private static Git git;
@BeforeAll
static void setUpRepository(@TempDir Path repositoryPath) throws GitAPIException {
// we use the same repository for all tests to speed things up
git = Git.init().setDirectory(repositoryPath.toFile()).call();
}
@AfterAll
static void closeRepository() {
git.close();
}
@Test
void shouldConvertChangeset() throws GitAPIException, IOException {
long now = System.currentTimeMillis() - 1000L;
Changeset changeset = commit(
"Tricia McMillan", "trillian@hitchhiker.com", "Added awesome markdown file"
);
assertThat(changeset.getId()).isNotEmpty();
assertThat(changeset.getDate()).isGreaterThanOrEqualTo(now);
assertThat(changeset.getDescription()).isEqualTo("Added awesome markdown file");
Person author = changeset.getAuthor();
assertThat(author.getName()).isEqualTo("Tricia McMillan");
assertThat(author.getMail()).isEqualTo("trillian@hitchhiker.com");
}
private Changeset commit(String name, String mail, String message) throws GitAPIException, IOException {
addRandomFileToRepository();
RevCommit commit = git.commit()
.setAuthor(name, mail)
.setMessage(message)
.call();
GitChangesetConverterFactory converterFactory = GitTestHelper.createConverterFactory();
return converterFactory.create(git.getRepository()).createChangeset(commit);
}
private void addRandomFileToRepository() throws IOException, GitAPIException {
File directory = git.getRepository().getWorkTree();
String name = UUID.randomUUID().toString();
File file = new File(directory, name + ".md");
Files.write(file.toPath(), ("# Greetings\n\nFrom " + name).getBytes(StandardCharsets.UTF_8));
git.add().addFilepattern(name + ".md").call();
}
@Nested
class SignatureTests {
@Mock
private GPG gpg;
@Mock
private PublicKey publicKey;
private PGPKeyPair keyPair;
private GpgSigner defaultSigner;
@BeforeEach
void setUpTestingSignerAndCaptureDefault() throws Exception {
defaultSigner = GpgSigner.getDefault();
// we use the same keypair for all tests to speed things up a little bit
if (keyPair == null) {
keyPair = createKeyPair();
GpgSigner.setDefault(new TestingGpgSigner(keyPair));
}
}
@AfterEach
void restoreDefaultSigner() {
GpgSigner.setDefault(defaultSigner);
}
@Test
void shouldReturnUnknownSignature() throws Exception {
String identity = "0xAWESOMExBOB";
when(gpg.findPublicKeyId(any())).thenReturn(identity);
Signature signature = addSignedCommitAndReturnSignature(identity);
assertThat(signature).isEqualTo(new Signature(identity, "gpg", SignatureStatus.NOT_FOUND, null, Collections.emptySet()));
}
@Test
void shouldReturnKnownButInvalidSignature() throws Exception {
String identity = "0xAWESOMExBOB";
String owner = "BobTheSigner";
setPublicKey(identity, owner, false);
Signature signature = addSignedCommitAndReturnSignature(identity);
assertThat(signature).isEqualTo(new Signature(identity, "gpg", SignatureStatus.INVALID, owner, Collections.emptySet()));
}
@Test
void shouldReturnValidSignature() throws Exception {
String identity = "0xAWESOMExBOB";
String owner = "BobTheSigner";
setPublicKey(identity, owner, true);
Signature signature = addSignedCommitAndReturnSignature(identity);
assertThat(signature).isEqualTo(new Signature(identity, "gpg", SignatureStatus.VERIFIED, owner, Collections.emptySet()));
}
@Test
void shouldPassDataAndSignatureForVerification() throws Exception {
setPublicKey("0x42", "Me", true);
addSignedCommitAndReturnSignature("0x42");
ArgumentCaptor<byte[]> dataCaptor = ArgumentCaptor.forClass(byte[].class);
ArgumentCaptor<byte[]> signatureCaptor = ArgumentCaptor.forClass(byte[].class);
verify(publicKey).verify(dataCaptor.capture(), signatureCaptor.capture());
String data = new String(dataCaptor.getValue());
assertThat(data).contains("author Bob The Signer <sign@bob.de>");
String signature = new String(signatureCaptor.getValue());
assertThat(signature).contains("BEGIN PGP SIGNATURE", "END PGP SIGNATURE");
}
@Test
void shouldNotReturnSignatureForNonSignedCommit() throws GitAPIException, IOException {
Changeset changeset = commit("Bob", "unsigned@bob.de", "not signed");
assertThat(changeset.getSignatures()).isEmpty();
}
private void setPublicKey(String id, String owner, boolean valid) {
when(gpg.findPublicKeyId(any())).thenReturn(id);
when(gpg.findPublicKey(id)).thenReturn(Optional.of(publicKey));
when(publicKey.getOwner()).thenReturn(Optional.of(owner));
when(publicKey.verify(any(byte[].class), any(byte[].class))).thenReturn(valid);
}
private Signature addSignedCommitAndReturnSignature(String keyIdentity) throws IOException, GitAPIException {
RevCommit commit = addSignedCommit(keyIdentity);
GitChangesetConverterFactory factory = new GitChangesetConverterFactory(gpg);
GitChangesetConverter converter = factory.create(git.getRepository());
List<Signature> signatures = converter.createChangeset(commit).getSignatures();
assertThat(signatures).isNotEmpty().hasSize(1);
return signatures.get(0);
}
private RevCommit addSignedCommit(String keyIdentity) throws IOException, GitAPIException {
addRandomFileToRepository();
return git.commit()
.setAuthor("Bob The Signer", "sign@bob.de")
.setMessage("Signed from Bob")
.setSign(true)
.setSigningKey(keyIdentity)
.call();
}
}
private PGPKeyPair createKeyPair() throws PGPException, NoSuchProviderException, NoSuchAlgorithmException {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
// we use a small key size to speedup test, a much larger size should be used for production
keyPairGenerator.initialize(512);
KeyPair pair = keyPairGenerator.generateKeyPair();
return new JcaPGPKeyPair(PGPPublicKey.RSA_GENERAL, pair, new Date());
}
private static class TestingGpgSigner extends GpgSigner {
private final PGPKeyPair keyPair;
TestingGpgSigner(PGPKeyPair keyPair) {
this.keyPair = keyPair;
}
@Override
public boolean canLocateSigningKey(String gpgSigningKey, PersonIdent committer, CredentialsProvider credentialsProvider) {
return true;
}
@Override
public void sign(CommitBuilder commit, String gpgSigningKey,
PersonIdent committer, CredentialsProvider credentialsProvider) {
try {
if (keyPair == null) {
throw new JGitInternalException(JGitText.get().unableToSignCommitNoSecretKey);
}
PGPPrivateKey privateKey = keyPair.getPrivateKey();
PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator(
new JcaPGPContentSignerBuilder(
keyPair.getPublicKey().getAlgorithm(),
HashAlgorithmTags.SHA256).setProvider(BouncyCastleProvider.PROVIDER_NAME)
);
signatureGenerator.init(PGPSignature.BINARY_DOCUMENT, privateKey);
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
try (BCPGOutputStream out = new BCPGOutputStream(new ArmoredOutputStream(buffer))) {
signatureGenerator.update(commit.build());
signatureGenerator.generate().encode(out);
}
commit.setGpgSignature(new GpgSignature(buffer.toByteArray()));
} catch (PGPException | IOException e) {
throw new JGitInternalException(e.getMessage(), e);
}
}
}
// register bouncy castle provider on load
static {
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
Security.addProvider(new BouncyCastleProvider());
}
}
}

View File

@@ -0,0 +1,90 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
import org.eclipse.jgit.api.errors.CanceledException;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.GpgSignature;
import org.eclipse.jgit.lib.GpgSigner;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.transport.CredentialsProvider;
import sonia.scm.security.GPG;
import sonia.scm.security.PrivateKey;
import sonia.scm.security.PublicKey;
import java.util.Collections;
import java.util.Optional;
public final class GitTestHelper {
private GitTestHelper() {
}
public static GitChangesetConverterFactory createConverterFactory() {
return new GitChangesetConverterFactory(new NoopGPG());
}
public static class SimpleGpgSigner extends GpgSigner {
public static byte[] getSignature() {
return "SIGNATURE".getBytes();
}
@Override
public void sign(CommitBuilder commitBuilder, String s, PersonIdent personIdent, CredentialsProvider
credentialsProvider) throws CanceledException {
commitBuilder.setGpgSignature(new GpgSignature(SimpleGpgSigner.getSignature()));
}
@Override
public boolean canLocateSigningKey(String s, PersonIdent personIdent, CredentialsProvider credentialsProvider) throws CanceledException {
return true;
}
}
private static class NoopGPG implements GPG {
@Override
public String findPublicKeyId(byte[] signature) {
return "secret-key";
}
@Override
public Optional<PublicKey> findPublicKey(String id) {
return Optional.empty();
}
@Override
public Iterable<PublicKey> findPublicKeysByUsername(String username) {
return Collections.emptySet();
}
@Override
public PrivateKey getPrivateKey() {
return null;
}
}
}

View File

@@ -0,0 +1,116 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.CanceledException;
import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.GpgSigner;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.security.GPG;
import sonia.scm.security.PrivateKey;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class ScmGpgSignerTest {
@Mock
GPG gpg;
@Mock
PersonIdent personIdent;
@Mock
CredentialsProvider credentialsProvider;
private ScmGpgSigner signer;
private final PrivateKey privateKey = new PrivateKey() {
@Override
public String getId() {
return "Private Key";
}
@Override
public byte[] sign(InputStream stream) {
return "MY FANCY SIGNATURE".getBytes();
}
};
@BeforeEach
void beforeEach() {
signer = new ScmGpgSigner(gpg);
}
@Test
void sign(@TempDir Path workdir) throws Exception {
when(gpg.getPrivateKey()).thenReturn(privateKey);
GpgSigner.setDefault(signer);
Path repositoryPath = workdir.resolve("repository");
Git git = Git.init().setDirectory(repositoryPath.toFile()).call();
Files.write(repositoryPath.resolve("README.md"), "# Hello".getBytes(StandardCharsets.UTF_8));
git.add().addFilepattern("README.md").call();
git.commit()
.setAuthor("Bob The Signer", "sign@bob.de")
.setMessage("Signed from Bob")
.setSign(true)
.setSigningKey("Private Key")
.call();
RevCommit commit = git.log().setMaxCount(1).call().iterator().next();
final byte[] rawCommit = commit.getRawBuffer();
final String commitString = new String(rawCommit);
assertThat(commitString).contains("gpgsig MY FANCY SIGNATURE");
}
@Test
void canLocateSigningKey() throws CanceledException {
assertThat(signer.canLocateSigningKey("foo", personIdent, credentialsProvider)).isTrue();
}
}

View File

@@ -32,6 +32,8 @@ import org.eclipse.jgit.revwalk.RevCommit;
import sonia.scm.repository.Changeset; import sonia.scm.repository.Changeset;
import sonia.scm.repository.GitChangesetConverter; import sonia.scm.repository.GitChangesetConverter;
import sonia.scm.repository.GitChangesetConverterFactory;
import sonia.scm.repository.GitTestHelper;
import sonia.scm.repository.client.api.RepositoryClientException; import sonia.scm.repository.client.api.RepositoryClientException;
//~--- JDK imports ------------------------------------------------------------ //~--- JDK imports ------------------------------------------------------------
@@ -71,7 +73,8 @@ public class GitCommitCommand implements CommitCommand
@Override @Override
public Changeset commit(CommitRequest request) throws IOException public Changeset commit(CommitRequest request) throws IOException
{ {
try (GitChangesetConverter converter = new GitChangesetConverter(git.getRepository())) GitChangesetConverterFactory converterFactory = GitTestHelper.createConverterFactory();
try (GitChangesetConverter converter = converterFactory.create(git.getRepository()))
{ {
RevCommit commit = git.commit() RevCommit commit = git.commit()
.setAuthor(request.getAuthor().getName(), request.getAuthor().getMail()) .setAuthor(request.getAuthor().getName(), request.getAuthor().getMail())

View File

@@ -32,6 +32,7 @@ import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.revwalk.RevWalk;
import sonia.scm.repository.Changeset; import sonia.scm.repository.Changeset;
import sonia.scm.repository.GitChangesetConverter; import sonia.scm.repository.GitChangesetConverter;
import sonia.scm.repository.GitTestHelper;
import sonia.scm.repository.client.api.RepositoryClientException; import sonia.scm.repository.client.api.RepositoryClientException;
import java.io.IOException; import java.io.IOException;
@@ -46,7 +47,7 @@ public class GitMergeCommand implements MergeCommand {
@Override @Override
public Changeset merge(MergeRequest request) throws IOException { public Changeset merge(MergeRequest request) throws IOException {
try (GitChangesetConverter converter = new GitChangesetConverter(git.getRepository())) { try (GitChangesetConverter converter = GitTestHelper.createConverterFactory().create(git.getRepository())) {
ObjectId resolved = git.getRepository().resolve(request.getBranch()); ObjectId resolved = git.getRepository().resolve(request.getBranch());
org.eclipse.jgit.api.MergeCommand mergeCommand = git.merge() org.eclipse.jgit.api.MergeCommand mergeCommand = git.merge()
.include(request.getBranch(), resolved) .include(request.getBranch(), resolved)

View File

@@ -40,7 +40,9 @@ import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.rules.TemporaryFolder; import org.junit.rules.TemporaryFolder;
import sonia.scm.repository.Changeset; import sonia.scm.repository.Changeset;
import sonia.scm.repository.GitChangesetConverterFactory;
import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.GitRepositoryHandler;
import sonia.scm.repository.GitTestHelper;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.user.User; import sonia.scm.user.User;
import sonia.scm.user.UserTestData; import sonia.scm.user.UserTestData;
@@ -110,23 +112,7 @@ public class AbstractRemoteCommandTestBase
{ {
// store reference to handle weak references // store reference to handle weak references
proto = new ScmTransportProtocol(new Provider<HookEventFacade>() proto = new ScmTransportProtocol(GitTestHelper::createConverterFactory, () -> null, () -> null);
{
@Override
public HookEventFacade get()
{
return null;
}
}, new Provider<GitRepositoryHandler>()
{
@Override
public GitRepositoryHandler get()
{
return null;
}
});
Transport.register(proto); Transport.register(proto);
} }

View File

@@ -27,7 +27,9 @@ package sonia.scm.repository.spi;
import org.eclipse.jgit.transport.ScmTransportProtocol; import org.eclipse.jgit.transport.ScmTransportProtocol;
import org.eclipse.jgit.transport.Transport; import org.eclipse.jgit.transport.Transport;
import org.junit.rules.ExternalResource; import org.junit.rules.ExternalResource;
import sonia.scm.repository.GitChangesetConverterFactory;
import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.GitRepositoryHandler;
import sonia.scm.repository.GitTestHelper;
import sonia.scm.repository.PreProcessorUtil; import sonia.scm.repository.PreProcessorUtil;
import sonia.scm.repository.RepositoryManager; import sonia.scm.repository.RepositoryManager;
import sonia.scm.repository.api.HookContextFactory; import sonia.scm.repository.api.HookContextFactory;
@@ -42,12 +44,12 @@ public class BindTransportProtocolRule extends ExternalResource {
private ScmTransportProtocol scmTransportProtocol; private ScmTransportProtocol scmTransportProtocol;
@Override @Override
protected void before() throws Throwable { protected void before() {
HookContextFactory hookContextFactory = new HookContextFactory(mock(PreProcessorUtil.class)); HookContextFactory hookContextFactory = new HookContextFactory(mock(PreProcessorUtil.class));
RepositoryManager repositoryManager = mock(RepositoryManager.class); RepositoryManager repositoryManager = mock(RepositoryManager.class);
HookEventFacade hookEventFacade = new HookEventFacade(of(repositoryManager), hookContextFactory); HookEventFacade hookEventFacade = new HookEventFacade(of(repositoryManager), hookContextFactory);
GitRepositoryHandler gitRepositoryHandler = mock(GitRepositoryHandler.class); GitRepositoryHandler gitRepositoryHandler = mock(GitRepositoryHandler.class);
scmTransportProtocol = new ScmTransportProtocol(of(hookEventFacade), of(gitRepositoryHandler)); scmTransportProtocol = new ScmTransportProtocol(of(GitTestHelper.createConverterFactory()), of(hookEventFacade), of(gitRepositoryHandler));
Transport.register(scmTransportProtocol); Transport.register(scmTransportProtocol);

View File

@@ -24,7 +24,6 @@
package sonia.scm.repository.spi; package sonia.scm.repository.spi;
import org.assertj.core.api.Assertions;
import org.junit.Test; import org.junit.Test;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;

View File

@@ -32,6 +32,8 @@ import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider; import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider;
import sonia.scm.repository.ChangesetPagingResult; import sonia.scm.repository.ChangesetPagingResult;
import sonia.scm.repository.GitChangesetConverterFactory;
import sonia.scm.repository.GitTestHelper;
import sonia.scm.store.InMemoryConfigurationStoreFactory; import sonia.scm.store.InMemoryConfigurationStoreFactory;
import java.io.IOException; import java.io.IOException;
@@ -173,14 +175,11 @@ public class GitIncomingCommandTest
assertEquals(0, cpr.getTotal()); assertEquals(0, cpr.getTotal());
} }
/** private GitIncomingCommand createCommand() {
* Method description return new GitIncomingCommand(
* new GitContext(incomingDirectory, incomingRepository, new GitRepositoryConfigStoreProvider(new InMemoryConfigurationStoreFactory())),
* handler,
* @return GitTestHelper.createConverterFactory()
*/ );
private GitIncomingCommand createCommand()
{
return new GitIncomingCommand(handler, new GitContext(incomingDirectory, incomingRepository, new GitRepositoryConfigStoreProvider(new InMemoryConfigurationStoreFactory())));
} }
} }

View File

@@ -27,6 +27,8 @@ package sonia.scm.repository.spi;
import org.junit.Test; import org.junit.Test;
import sonia.scm.NotFoundException; import sonia.scm.NotFoundException;
import sonia.scm.repository.ChangesetPagingResult; import sonia.scm.repository.ChangesetPagingResult;
import sonia.scm.repository.GitChangesetConverterFactory;
import sonia.scm.repository.GitTestHelper;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
@@ -108,8 +110,7 @@ public class GitLogCommandAncestorTest extends AbstractGitCommandTestBase
createCommand().getChangesets(request); createCommand().getChangesets(request);
} }
private GitLogCommand createCommand() private GitLogCommand createCommand() {
{ return new GitLogCommand(createContext(), GitTestHelper.createConverterFactory());
return new GitLogCommand(createContext());
} }
} }

View File

@@ -31,7 +31,9 @@ import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner; import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.repository.Changeset; import sonia.scm.repository.Changeset;
import sonia.scm.repository.ChangesetPagingResult; import sonia.scm.repository.ChangesetPagingResult;
import sonia.scm.repository.GitChangesetConverterFactory;
import sonia.scm.repository.GitRepositoryConfig; import sonia.scm.repository.GitRepositoryConfig;
import sonia.scm.repository.GitTestHelper;
import sonia.scm.repository.Modifications; import sonia.scm.repository.Modifications;
import sonia.scm.repository.Person; import sonia.scm.repository.Person;
@@ -293,8 +295,7 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase
return new File(repositoryDirectory, "HEAD"); return new File(repositoryDirectory, "HEAD");
} }
private GitLogCommand createCommand() private GitLogCommand createCommand() {
{ return new GitLogCommand(createContext(), GitTestHelper.createConverterFactory());
return new GitLogCommand(createContext());
} }
} }

View File

@@ -29,17 +29,24 @@ import com.github.sdorra.shiro.SubjectAware;
import org.apache.shiro.subject.SimplePrincipalCollection; import org.apache.shiro.subject.SimplePrincipalCollection;
import org.apache.shiro.subject.Subject; import org.apache.shiro.subject.Subject;
import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.CanceledException;
import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.GpgSignature;
import org.eclipse.jgit.lib.GpgSigner;
import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.junit.BeforeClass;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
import sonia.scm.NoChangesMadeException; import sonia.scm.NoChangesMadeException;
import sonia.scm.NotFoundException; import sonia.scm.NotFoundException;
import sonia.scm.repository.Added; import sonia.scm.repository.Added;
import sonia.scm.repository.GitTestHelper;
import sonia.scm.repository.GitWorkingCopyFactory; import sonia.scm.repository.GitWorkingCopyFactory;
import sonia.scm.repository.Person; import sonia.scm.repository.Person;
import sonia.scm.repository.api.MergeCommandResult; import sonia.scm.repository.api.MergeCommandResult;
@@ -68,6 +75,11 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase {
@Rule @Rule
public BindTransportProtocolRule transportProtocolRule = new BindTransportProtocolRule(); public BindTransportProtocolRule transportProtocolRule = new BindTransportProtocolRule();
@BeforeClass
public static void setSigner() {
GpgSigner.setDefault(new GitTestHelper.SimpleGpgSigner());
}
@Test @Test
public void shouldDetectMergeableBranches() { public void shouldDetectMergeableBranches() {
GitMergeCommand command = createCommand(); GitMergeCommand command = createCommand();
@@ -419,6 +431,48 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase {
command.dryRun(request); command.dryRun(request);
} }
@Test
public void shouldSignMergeCommit() throws IOException, GitAPIException {
GitMergeCommand command = createCommand();
MergeCommandRequest request = new MergeCommandRequest();
request.setTargetBranch("master");
request.setBranchToMerge("empty_merge");
request.setMergeStrategy(MergeStrategy.MERGE_COMMIT);
request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det"));
MergeCommandResult mergeCommandResult = command.merge(request);
assertThat(mergeCommandResult.isSuccess()).isTrue();
Repository repository = createContext().open();
Iterable<RevCommit> commits = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call();
RevCommit mergeCommit = commits.iterator().next();
assertThat(mergeCommit.getRawGpgSignature()).isNotEmpty();
assertThat(mergeCommit.getRawGpgSignature()).isEqualTo(GitTestHelper.SimpleGpgSigner.getSignature());
}
@Test
public void shouldNotSignMergeCommitIfSigningIsDisabled() throws IOException, GitAPIException {
GitMergeCommand command = createCommand();
MergeCommandRequest request = new MergeCommandRequest();
request.setTargetBranch("master");
request.setBranchToMerge("empty_merge");
request.setMergeStrategy(MergeStrategy.MERGE_COMMIT);
request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det"));
request.setSign(false);
MergeCommandResult mergeCommandResult = command.merge(request);
assertThat(mergeCommandResult.isSuccess()).isTrue();
Repository repository = createContext().open();
Iterable<RevCommit> commits = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call();
RevCommit mergeCommit = commits.iterator().next();
assertThat(mergeCommit.getRawGpgSignature()).isNullOrEmpty();
}
private GitMergeCommand createCommand() { private GitMergeCommand createCommand() {
return createCommand(git -> { return createCommand(git -> {
}); });

View File

@@ -27,23 +27,33 @@ package sonia.scm.repository.spi;
import com.github.sdorra.shiro.ShiroRule; import com.github.sdorra.shiro.ShiroRule;
import com.github.sdorra.shiro.SubjectAware; import com.github.sdorra.shiro.SubjectAware;
import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.CanceledException;
import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.errors.CorruptObjectException; import org.eclipse.jgit.errors.CorruptObjectException;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.GpgSignature;
import org.eclipse.jgit.lib.GpgSigner;
import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.treewalk.CanonicalTreeParser; import org.eclipse.jgit.treewalk.CanonicalTreeParser;
import org.junit.BeforeClass;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.jupiter.api.BeforeEach;
import org.junit.rules.TemporaryFolder; import org.junit.rules.TemporaryFolder;
import sonia.scm.AlreadyExistsException; import sonia.scm.AlreadyExistsException;
import sonia.scm.BadRequestException; import sonia.scm.BadRequestException;
import sonia.scm.ConcurrentModificationException; import sonia.scm.ConcurrentModificationException;
import sonia.scm.NotFoundException; import sonia.scm.NotFoundException;
import sonia.scm.repository.GitTestHelper;
import sonia.scm.repository.Person; import sonia.scm.repository.Person;
import sonia.scm.repository.work.NoneCachingWorkingCopyPool; import sonia.scm.repository.work.NoneCachingWorkingCopyPool;
import sonia.scm.repository.work.WorkdirProvider; import sonia.scm.repository.work.WorkdirProvider;
import sonia.scm.security.PublicKey;
import sonia.scm.web.lfs.LfsBlobStoreFactory; import sonia.scm.web.lfs.LfsBlobStoreFactory;
import java.io.File; import java.io.File;
@@ -65,6 +75,11 @@ public class GitModifyCommandTest extends AbstractGitCommandTestBase {
private final LfsBlobStoreFactory lfsBlobStoreFactory = mock(LfsBlobStoreFactory.class); private final LfsBlobStoreFactory lfsBlobStoreFactory = mock(LfsBlobStoreFactory.class);
@BeforeClass
public static void setSigner() {
GpgSigner.setDefault(new GitTestHelper.SimpleGpgSigner());
}
@Test @Test
public void shouldCreateCommit() throws IOException, GitAPIException { public void shouldCreateCommit() throws IOException, GitAPIException {
File newFile = Files.write(temporaryFolder.newFile().toPath(), "new content".getBytes()).toFile(); File newFile = Files.write(temporaryFolder.newFile().toPath(), "new content".getBytes()).toFile();
@@ -306,6 +321,48 @@ public class GitModifyCommandTest extends AbstractGitCommandTestBase {
command.execute(request); command.execute(request);
} }
@Test
public void shouldSignCreatedCommit() throws IOException, GitAPIException {
File newFile = Files.write(temporaryFolder.newFile().toPath(), "new content".getBytes()).toFile();
GitModifyCommand command = createCommand();
ModifyCommandRequest request = new ModifyCommandRequest();
request.setCommitMessage("test commit");
request.addRequest(new ModifyCommandRequest.CreateFileRequest("new_file", newFile, false));
request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det"));
command.execute(request);
try (Git git = new Git(createContext().open())) {
RevCommit lastCommit = getLastCommit(git);
assertThat(lastCommit.getRawGpgSignature()).isNotEmpty();
assertThat(lastCommit.getRawGpgSignature()).isEqualTo(GitTestHelper.SimpleGpgSigner.getSignature());
}
}
@Test
public void shouldNotSignCreatedCommitIfSigningDisabled() throws IOException, GitAPIException {
File newFile = Files.write(temporaryFolder.newFile().toPath(), "new content".getBytes()).toFile();
GitModifyCommand command = createCommand();
ModifyCommandRequest request = new ModifyCommandRequest();
request.setCommitMessage("test commit");
request.setSign(false);
request.addRequest(new ModifyCommandRequest.CreateFileRequest("new_file", newFile, false));
request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det"));
command.execute(request);
try (Git git = new Git(createContext().open())) {
RevCommit lastCommit = getLastCommit(git);
assertThat(lastCommit.getRawGpgSignature()).isNullOrEmpty();
}
}
private void assertInTree(TreeAssertions assertions) throws IOException, GitAPIException { private void assertInTree(TreeAssertions assertions) throws IOException, GitAPIException {
try (Git git = new Git(createContext().open())) { try (Git git = new Git(createContext().open())) {
RevCommit lastCommit = getLastCommit(git); RevCommit lastCommit = getLastCommit(git);

View File

@@ -31,6 +31,8 @@ import org.eclipse.jgit.revwalk.RevCommit;
import org.junit.Test; import org.junit.Test;
import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider; import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider;
import sonia.scm.repository.ChangesetPagingResult; import sonia.scm.repository.ChangesetPagingResult;
import sonia.scm.repository.GitChangesetConverterFactory;
import sonia.scm.repository.GitTestHelper;
import sonia.scm.store.InMemoryConfigurationStoreFactory; import sonia.scm.store.InMemoryConfigurationStoreFactory;
import java.io.IOException; import java.io.IOException;
@@ -151,6 +153,10 @@ public class GitOutgoingCommandTest extends AbstractRemoteCommandTestBase
*/ */
private GitOutgoingCommand createCommand() private GitOutgoingCommand createCommand()
{ {
return new GitOutgoingCommand(handler, new GitContext(outgoingDirectory, outgoingRepository, new GitRepositoryConfigStoreProvider(new InMemoryConfigurationStoreFactory()))); return new GitOutgoingCommand(
new GitContext(outgoingDirectory, outgoingRepository, new GitRepositoryConfigStoreProvider(new InMemoryConfigurationStoreFactory())),
handler,
GitTestHelper.createConverterFactory()
);
} }
} }

View File

@@ -0,0 +1,74 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository.spi;
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
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.repository.GitRepositoryHandler;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.verify;
@ExtendWith(MockitoExtension.class)
class GitRepositoryServiceProviderTest {
@Mock
private GitRepositoryHandler handler;
@Mock
private GitContext context;
@Test
void shouldCreatePushCommand() {
GitRepositoryServiceProvider provider = createProvider();
PushCommand pushCommand = provider.getPushCommand();
assertThat(pushCommand).isNotNull().isInstanceOf(GitPushCommand.class);
}
@Test
void shouldDelegateCloseToContext() {
createProvider().close();
verify(context).close();
}
private GitRepositoryServiceProvider createProvider() {
return new GitRepositoryServiceProvider(createParentInjector(), context);
}
private Injector createParentInjector() {
return Guice.createInjector(new AbstractModule() {
@Override
protected void configure() {
bind(GitRepositoryHandler.class).toInstance(handler);
}
});
}
}

View File

@@ -0,0 +1,63 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository.spi;
import com.google.inject.Injector;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.repository.RepositoryTestData;
import static org.assertj.core.api.Assertions.assertThat;
@ExtendWith(MockitoExtension.class)
class GitRepositoryServiceResolverTest {
@Mock
private Injector injector;
@Mock
private GitContextFactory contextFactory;
@InjectMocks
private GitRepositoryServiceResolver resolver;
@Test
void shouldCreateRepositoryServiceProvider() {
GitRepositoryServiceProvider provider = resolver.resolve(RepositoryTestData.createHeartOfGold("git"));
assertThat(provider).isNotNull();
}
@ParameterizedTest
@ValueSource(strings = { "hg","svn", "unknown"})
void shouldReturnNullForNonGitRepositories(String type) {
GitRepositoryServiceProvider provider = resolver.resolve(RepositoryTestData.createHeartOfGold(type));
assertThat(provider).isNull();
}
}

View File

@@ -34,7 +34,9 @@ import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.TemporaryFolder; import org.junit.rules.TemporaryFolder;
import sonia.scm.repository.GitChangesetConverterFactory;
import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.GitRepositoryHandler;
import sonia.scm.repository.GitTestHelper;
import sonia.scm.repository.PreProcessorUtil; import sonia.scm.repository.PreProcessorUtil;
import sonia.scm.repository.RepositoryManager; import sonia.scm.repository.RepositoryManager;
import sonia.scm.repository.api.HookContextFactory; import sonia.scm.repository.api.HookContextFactory;
@@ -65,7 +67,7 @@ public class SimpleGitWorkingCopyFactoryTest extends AbstractGitCommandTestBase
HookContextFactory hookContextFactory = new HookContextFactory(mock(PreProcessorUtil.class)); HookContextFactory hookContextFactory = new HookContextFactory(mock(PreProcessorUtil.class));
HookEventFacade hookEventFacade = new HookEventFacade(of(mock(RepositoryManager.class)), hookContextFactory); HookEventFacade hookEventFacade = new HookEventFacade(of(mock(RepositoryManager.class)), hookContextFactory);
GitRepositoryHandler gitRepositoryHandler = mock(GitRepositoryHandler.class); GitRepositoryHandler gitRepositoryHandler = mock(GitRepositoryHandler.class);
proto = new ScmTransportProtocol(of(hookEventFacade), of(gitRepositoryHandler)); proto = new ScmTransportProtocol(of(GitTestHelper.createConverterFactory()), of(hookEventFacade), of(gitRepositoryHandler));
Transport.register(proto); Transport.register(proto);
workdirProvider = new WorkdirProvider(temporaryFolder.newFolder()); workdirProvider = new WorkdirProvider(temporaryFolder.newFolder());
} }

View File

@@ -0,0 +1,3 @@
{
"baseUrl": "http://localhost:8081/scm"
}

View File

@@ -0,0 +1,44 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
describe("With Anonymous mode disabled", () => {
before("Disable anonymous access", () => {
cy.login("scmadmin", "scmadmin");
cy.setAnonymousMode("OFF");
cy.byTestId("primary-navigation-logout").click();
});
it("Should show login page without primary navigation", () => {
cy.byTestId("login-button");
cy.containsNotByTestId("div", "primary-navigation-login");
cy.containsNotByTestId("div", "primary-navigation-repositories");
});
it("Should redirect after login", () => {
cy.login("scmadmin", "scmadmin");
cy.visit("/me");
cy.byTestId("footer-user-profile");
cy.byTestId("primary-navigation-logout").click();
});
});

View File

@@ -0,0 +1,86 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
describe("With Anonymous mode fully enabled", () => {
before("Set anonymous mode to full", () => {
cy.login("scmadmin", "scmadmin");
cy.setAnonymousMode("FULL");
// Give anonymous user permissions
cy.byTestId("primary-navigation-users").click();
cy.byTestId("_anonymous").click();
cy.byTestId("user-settings-link").click();
cy.byTestId("user-permissions-link").click();
cy.byTestId("read-all-repositories").click();
cy.byTestId("set-permissions-button").click();
cy.byTestId("primary-navigation-logout").click();
});
it("Should show repositories overview with Login button in primary navigation", () => {
cy.visit("/repos/");
cy.byTestId("repository-overview-filter");
cy.byTestId("scm-anonymous");
cy.byTestId("primary-navigation-login");
});
it("Should show login page on url", () => {
cy.visit("/login/");
cy.byTestId("login-button");
});
it("Should show login page on link click", () => {
cy.visit("/repos/");
cy.byTestId("repository-overview-filter");
cy.byTestId("primary-navigation-login").click();
cy.byTestId("login-button");
});
it("Should login and direct to repositories overview", () => {
cy.login("scmadmin", "scmadmin");
cy.visit("/login");
cy.byTestId("scm-administrator");
cy.byTestId("primary-navigation-logout").click();
});
it("Should logout and direct to login page", () => {
cy.login("scmadmin", "scmadmin");
cy.visit("/repos/");
cy.byTestId("repository-overview-filter");
cy.byTestId("scm-administrator");
cy.byTestId("primary-navigation-logout").click();
cy.byTestId("login-button");
});
it("Anonymous user should not be able to change password", () => {
cy.visit("/repos/");
cy.byTestId("footer-user-profile").click();
cy.byTestId("scm-anonymous");
cy.containsNotByTestId("ul", "user-settings-link");
cy.get("section").not("Change password");
});
after("Disable anonymous access", () => {
cy.login("scmadmin", "scmadmin");
cy.setAnonymousMode("OFF");
cy.byTestId("primary-navigation-logout").click();
});
});

View File

@@ -0,0 +1,44 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
describe("With Anonymous mode protocol only enabled", () => {
before("Set anonymous mode to protocol only", () => {
cy.login("scmadmin", "scmadmin");
cy.setAnonymousMode("PROTOCOL_ONLY");
cy.byTestId("primary-navigation-logout").click();
});
it("Should show login page without primary navigation", () => {
cy.visit("/repos/");
cy.byTestId("login-button");
cy.containsNotByTestId("div", "primary-navigation-login");
cy.containsNotByTestId("div", "primary-navigation-repositories");
});
after("Disable anonymous access", () => {
cy.login("scmadmin", "scmadmin");
cy.setAnonymousMode("OFF");
cy.byTestId("primary-navigation-logout").click();
});
});

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