Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement SVN mirror command #1660

Closed
wants to merge 15 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/de/user/admin/settings.md
Expand Up @@ -41,6 +41,9 @@ Ist der Benutzer Konverter aktiviert, werden alle internen Benutzer beim Einlogg
#### Fallback E-Mail Domain Name
Dieser Domain Name wird genutzt, wenn für einen User eine E-Mail-Adresse benötigt wird, für den keine hinterlegt ist. Diese Domain wird nicht zum Versenden von E-Mails genutzt und auch keine anderweitige Verbindung aufgebaut.

#### Notfallkontakte
Die folgenden Benutzer werden über administrative Vorfälle informiert (z. B. fehlgeschlagene Integritätsprüfungen).

#### Anmeldeversuche
Es lässt sich konfigurieren wie häufig sich ein Benutzer falsch anmelden darf, bevor dessen Benutzerkonto gesperrt wird. Der Zähler für fehlerhafte Anmeldeversuche wird nach einem erfolgreichen Login zurückgesetzt. Man kann dieses Feature abschalten, indem man "-1" in die Konfiguration einträgt.

Expand Down
3 changes: 3 additions & 0 deletions docs/en/user/admin/settings.md
Expand Up @@ -41,6 +41,9 @@ Internal users will automatically be converted to external on their first login
#### Fallback Mail Domain Name
This domain name will be used to create email addresses for users without one when needed. It will not be used to send mails nor will be accessed otherwise.

#### Emergency Contacts
The following users will be notified of administrative incidents (e.g. failed health checks).

#### Login Attempt Limit
It can be configured how many failed login attempts a user can have before the account gets disabled. The counter for failed login attempts is reset after a successful login. This feature can be deactivated by setting the value "-1".

Expand Down
2 changes: 2 additions & 0 deletions gradle/changelog/health_check_notifications.yaml
@@ -0,0 +1,2 @@
- type: added
description: Notifications for health checks ([#1664](https://github.com/scm-manager/scm-manager/pull/1664))
2 changes: 2 additions & 0 deletions gradle/changelog/svn_mirror_command.yaml
@@ -0,0 +1,2 @@
- type: added
description: Implement Subversion mirror command ([#1660](https://github.com/scm-manager/scm-manager/pull/1660))
21 changes: 21 additions & 0 deletions scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java
Expand Up @@ -214,6 +214,14 @@ public class ScmConfiguration implements Configuration {
@XmlElement(name = "mail-domain-name")
private String mailDomainName = DEFAULT_MAIL_DOMAIN_NAME;

/**
* List of users that will be notified of administrative incidents.
*
* @since 2.19.0
*/
@XmlElement(name = "emergency-contacts")
private Set<String> emergencyContacts;

/**
* Fires the {@link ScmConfigurationChangedEvent}.
*/
Expand Down Expand Up @@ -253,6 +261,7 @@ public void load(ScmConfiguration other) {
this.loginInfoUrl = other.loginInfoUrl;
this.releaseFeedUrl = other.releaseFeedUrl;
this.mailDomainName = other.mailDomainName;
this.emergencyContacts = other.emergencyContacts;
this.enabledUserConverter = other.enabledUserConverter;
this.enabledApiKeys = other.enabledApiKeys;
}
Expand Down Expand Up @@ -456,6 +465,14 @@ public boolean isSkipFailedAuthenticators() {
return skipFailedAuthenticators;
}

public Set<String> getEmergencyContacts() {
if (emergencyContacts == null) {
emergencyContacts = Sets.newHashSet();
}

return emergencyContacts;
}

/**
* Enables the anonymous access at protocol level.
*
Expand Down Expand Up @@ -621,6 +638,10 @@ public void setLoginInfoUrl(String loginInfoUrl) {
this.loginInfoUrl = loginInfoUrl;
}

public void setEmergencyContacts(Set<String> emergencyContacts) {
this.emergencyContacts = emergencyContacts;
}

@Override
// Only for permission checks, don't serialize to XML
@XmlTransient
Expand Down
Expand Up @@ -75,4 +75,7 @@ default void check(String repositoryId) {
}
}

default boolean isReadOnly(String permission, String repositoryId) {
return isReadOnly(repositoryId);
}
}
Expand Up @@ -62,16 +62,23 @@ public PermissionActionCheckInterceptor<Repository> intercept(String permission)
if (READ_ONLY_VERBS.contains(permission)) {
return new PermissionActionCheckInterceptor<Repository>() {};
} else {
return new WriteInterceptor();
return new WriteInterceptor(permission);
}
}

private static class WriteInterceptor implements PermissionActionCheckInterceptor<Repository> {

private final String permission;

private WriteInterceptor(String permission) {
this.permission = permission;
}

@Override
public void check(Subject subject, String id, Runnable delegate) {
delegate.run();
for (ReadOnlyCheck check : readOnlyChecks) {
if (check.isReadOnly(id)) {
if (check.isReadOnly(permission, id)) {
throw new AuthorizationException(check.getReason());
}
}
Expand All @@ -83,7 +90,7 @@ public boolean isPermitted(Subject subject, String id, BooleanSupplier delegate)
}

private boolean isWritable(String id) {
return readOnlyChecks.stream().noneMatch(c -> c.isReadOnly(id));
return readOnlyChecks.stream().noneMatch(c -> c.isReadOnly(permission, id));
}
}
}
Expand Up @@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

package sonia.scm.repository;

/**
Expand All @@ -30,17 +30,14 @@
* @author Sebastian Sdorra
* @since 1.23
*/
public class WrappedRepositoryHookEvent extends RepositoryHookEvent
{
public class WrappedRepositoryHookEvent extends RepositoryHookEvent {

/**
* Constructs a new WrappedRepositoryHookEvent.
*
*
* @param wrappedEvent event to wrap
*/
protected WrappedRepositoryHookEvent(RepositoryHookEvent wrappedEvent)
{
protected WrappedRepositoryHookEvent(RepositoryHookEvent wrappedEvent) {
super(wrappedEvent.getContext(), wrappedEvent.getRepository(),
wrappedEvent.getType());
}
Expand All @@ -50,28 +47,24 @@ protected WrappedRepositoryHookEvent(RepositoryHookEvent wrappedEvent)
/**
* Returns a wrapped instance of the {@link RepositoryHookEvent}-
*
*
* @param event event to wrap
*
* @return wrapper
*/
public static WrappedRepositoryHookEvent wrap(RepositoryHookEvent event)
{
public static WrappedRepositoryHookEvent wrap(RepositoryHookEvent event) {
WrappedRepositoryHookEvent wrappedEvent = null;

switch (event.getType())
{
case POST_RECEIVE :
switch (event.getType()) {
case POST_RECEIVE:
wrappedEvent = new PostReceiveRepositoryHookEvent(event);

break;

case PRE_RECEIVE :
case PRE_RECEIVE:
wrappedEvent = new PreReceiveRepositoryHookEvent(event);

break;

default :
default:
throw new IllegalArgumentException("unsupported hook event type");
}

Expand Down
@@ -0,0 +1,36 @@
/*
* 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.api;

import lombok.AllArgsConstructor;
import lombok.Getter;

@AllArgsConstructor
@Getter
public class Pkcs12ClientCertificateCredential implements Credential {

private final byte[] certificate;
private final char[] password;
}
8 changes: 5 additions & 3 deletions scm-core/src/main/java/sonia/scm/store/AbstractStore.java
Expand Up @@ -24,6 +24,8 @@

package sonia.scm.store;

import java.util.function.BooleanSupplier;

/**
* Base class for {@link ConfigurationStore}.
*
Expand All @@ -38,9 +40,9 @@ public abstract class AbstractStore<T> implements ConfigurationStore<T> {
* stored object
*/
protected T storeObject;
private final boolean readOnly;
private final BooleanSupplier readOnly;

protected AbstractStore(boolean readOnly) {
protected AbstractStore(BooleanSupplier readOnly) {
this.readOnly = readOnly;
}

Expand All @@ -55,7 +57,7 @@ public T get() {

@Override
public void set(T object) {
if (readOnly) {
if (readOnly.getAsBoolean()) {
throw new StoreReadOnlyException(object);
}
writeObject(object);
Expand Down
Expand Up @@ -31,7 +31,7 @@

class ReadOnlyCheckTest {

private final ReadOnlyCheck check = new TesingReadOnlyCheck();
private final ReadOnlyCheck check = new TestingReadOnlyCheck();

private final Repository repository = new Repository("42", "git", "hitchhiker", "hog");

Expand All @@ -55,7 +55,12 @@ void shouldNotThrowException() {
assertDoesNotThrow(() -> check.check("21"));
}

private class TesingReadOnlyCheck implements ReadOnlyCheck {
@Test
void shouldDelegateToNormalCheck() {
assertThat(check.isReadOnly("any", "42")).isTrue();
}

private class TestingReadOnlyCheck implements ReadOnlyCheck {

@Override
public String getReason() {
Expand Down
Expand Up @@ -28,6 +28,7 @@
import org.slf4j.LoggerFactory;

import java.io.File;
import java.util.function.BooleanSupplier;

/**
* JAXB implementation of {@link ConfigurationStore}.
Expand All @@ -46,7 +47,7 @@ public class JAXBConfigurationStore<T> extends AbstractStore<T> {
private final Class<T> type;
private final File configFile;

public JAXBConfigurationStore(TypedStoreContext<T> context, Class<T> type, File configFile, boolean readOnly) {
public JAXBConfigurationStore(TypedStoreContext<T> context, Class<T> type, File configFile, BooleanSupplier readOnly) {
super(readOnly);
this.context = context;
this.type = type;
Expand Down
Expand Up @@ -57,7 +57,7 @@ public <T> JAXBConfigurationStore<T> getStore(TypedStoreParameters<T> storeParam
getStoreLocation(storeParameters.getName().concat(StoreConstants.FILE_EXTENSION),
storeParameters.getType(),
storeParameters.getRepositoryId()),
mustBeReadOnly(storeParameters)
() -> mustBeReadOnly(storeParameters)
);
}
}
2 changes: 1 addition & 1 deletion scm-plugins/scm-git-plugin/build.gradle
Expand Up @@ -27,7 +27,7 @@ plugins {
id 'org.scm-manager.smp' version '0.7.5'
}

def jgitVersion = '5.10.0.202012080955-r-scm2'
def jgitVersion = '5.11.1.202105131744-r-scm1'

dependencies {
// required by scm-it
Expand Down
Expand Up @@ -26,6 +26,8 @@

//~--- JDK imports ------------------------------------------------------------

import com.google.common.base.Strings;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
Expand All @@ -39,6 +41,8 @@
@XmlAccessorType(XmlAccessType.FIELD)
public class GitConfig extends RepositoryConfig {

private static final String FALLBACK_BRANCH = "main";

@SuppressWarnings("WeakerAccess") // This might be needed for permission checking
public static final String PERMISSION = "git";

Expand All @@ -49,7 +53,7 @@ public class GitConfig extends RepositoryConfig {
private boolean nonFastForwardDisallowed;

@XmlElement(name = "default-branch")
private String defaultBranch = "main";
private String defaultBranch = FALLBACK_BRANCH;

public String getGcExpression() {
return gcExpression;
Expand All @@ -68,6 +72,9 @@ public void setNonFastForwardDisallowed(boolean nonFastForwardDisallowed) {
}

public String getDefaultBranch() {
if (Strings.isNullOrEmpty(defaultBranch)) {
return FALLBACK_BRANCH;
}
return defaultBranch;
}

Expand Down
Expand Up @@ -27,6 +27,7 @@
import com.google.common.base.Stopwatch;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.api.errors.TransportException;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
Expand Down Expand Up @@ -75,6 +76,13 @@ public ParentAndClone<Repository, Repository> initialize(File target, String ini
}

return new ParentAndClone<>(null, clone, target);
} catch (TransportException e) {
String message = e.getMessage();
if (initialBranch != null && message.contains(initialBranch) && message.contains("not found")) {
throw notFound(entity("Branch", initialBranch).in(context.getRepository()));
} else {
throw new InternalRepositoryException(context.getRepository(), "could not clone working copy of repository", e);
}
} catch (GitAPIException | IOException e) {
throw new InternalRepositoryException(context.getRepository(), "could not clone working copy of repository", e);
} finally {
Expand Down
Expand Up @@ -28,8 +28,10 @@
import org.eclipse.jgit.transport.FetchResult;
import sonia.scm.ContextEntry;
import sonia.scm.event.ScmEventBus;
import sonia.scm.repository.PostReceiveRepositoryHookEvent;
import sonia.scm.repository.RepositoryHookEvent;
import sonia.scm.repository.Tag;
import sonia.scm.repository.WrappedRepositoryHookEvent;
import sonia.scm.repository.api.ImportFailedException;

import javax.inject.Inject;
Expand All @@ -51,12 +53,12 @@ class PostReceiveRepositoryHookEventFactory {
}

void fireForFetch(Git git, FetchResult result) {
RepositoryHookEvent event;
PostReceiveRepositoryHookEvent event;
try {
List<String> branches = getBranchesFromFetchResult(result);
List<Tag> tags = getTagsFromFetchResult(result);
GitLazyChangesetResolver changesetResolver = new GitLazyChangesetResolver(context.getRepository(), git);
event = eventFactory.createEvent(context, branches, tags, changesetResolver);
event = new PostReceiveRepositoryHookEvent(WrappedRepositoryHookEvent.wrap(eventFactory.createEvent(context, branches, tags, changesetResolver)));
} catch (IOException e) {
throw new ImportFailedException(
ContextEntry.ContextBuilder.entity(context.getRepository()).build(),
Expand Down