diff --git a/CHANGELOG.md b/CHANGELOG.md
index db392e814a..1b8d7ac29b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Add repository import via dump file for Subversion ([#1471](https://github.com/scm-manager/scm-manager/pull/1471))
- Add support for permalinks to lines in source code view ([#1472](https://github.com/scm-manager/scm-manager/pull/1472))
+- Add "archive" flag for repositories to make them immutable ([#1477](https://github.com/scm-manager/scm-manager/pull/1477))
### Fixed
- Add "Api Key" page link to sub-navigation of "User" and "Me" sections ([#1464](https://github.com/scm-manager/scm-manager/pull/1464))
diff --git a/docs/de/user/repo/assets/repository-settings-general-git.png b/docs/de/user/repo/assets/repository-settings-general-git.png
index 3047de709c..b3747418f0 100644
Binary files a/docs/de/user/repo/assets/repository-settings-general-git.png and b/docs/de/user/repo/assets/repository-settings-general-git.png differ
diff --git a/docs/de/user/repo/settings.md b/docs/de/user/repo/settings.md
index 124fcd8f0a..aacbc0a10a 100644
--- a/docs/de/user/repo/settings.md
+++ b/docs/de/user/repo/settings.md
@@ -1,22 +1,36 @@
---
-title: Repository
-subtitle: Einstellungen
+title: Repository subtitle: Einstellungen
---
-Unter den Repository Einstellungen befinden sich zwei Einträge. Wenn weitere Plugins installiert sind, können es deutlich mehr Unterseiten sein.
+Unter den Repository Einstellungen befinden sich zwei Einträge. Wenn weitere Plugins installiert sind, können es
+deutlich mehr Unterseiten sein.
### Generell
-Unter dem Eintrag "Generell" kann man die Zusatzinformationen zum Repository editieren. Da es sich im Beispiel um ein Git Repository handelt, kann ebenfalls der Standard-Branch für dieses Repository gesetzt werden. Der Standard-Branch sorgt dafür, dass beim Arbeiten mit diesem Repository dieser Branch vorrangig geöffnet wird, falls kein expliziter Branch ausgewählt wurde.
-Innerhalb der Gefahrenzone unten auf der Seite gibt es mit entsprechenden Rechten die Möglichkeit das Repository umzubenennen oder zu löschen. Wenn in der globalen SCM-Manager Konfiguration die Namespace Strategie `benutzerdefiniert` ausgewählt ist, kann zusätzlich zum Repository Namen auch der Namespace umbenannt werden.
+Unter dem Eintrag "Generell" kann man die Zusatzinformationen zum Repository editieren. Da es sich im Beispiel um ein
+Git Repository handelt, kann ebenfalls der Standard-Branch für dieses Repository gesetzt werden. Der Standard-Branch
+sorgt dafür, dass beim Arbeiten mit diesem Repository dieser Branch vorrangig geöffnet wird, falls kein expliziter
+Branch ausgewählt wurde.
+
+Innerhalb der Gefahrenzone unten auf der Seite gibt es mit entsprechenden Rechten die Möglichkeit das Repository
+umzubenennen, zu löschen oder als archiviert zu markieren. Wenn in der globalen SCM-Manager Konfiguration die Namespace
+Strategie `benutzerdefiniert` ausgewählt ist, kann zusätzlich zum Repository Namen auch der Namespace umbenannt werden.
+Ein archiviertes Repository kann nicht mehr verändert werden.

### Berechtigungen
-Dank des fein granularen Berechtigungskonzepts des SCM-Managers können Nutzern und Gruppen, basierend auf definierbaren Rollen oder auf individuellen Einstellungen, Rechte zugewiesen werden. Berechtigungen können global, auf Namespace-Ebene und auf Repository-Ebene vergeben werden. Globale Berechtigungen werden in der Administrations-Oberfläche des SCM-Managers vergeben. Unter diesem Eintrag handelt es sich um Repository-bezogene Berechtigungen.
-Die Berechtigungen können jeweils für Gruppen und für Benutzer vergeben werden. Dabei gibt es die Möglichkeiten die Berechtigungen über Berechtigungsrollen zu definieren oder jede Berechtigung einzeln zu vergeben. Die Berechtigungsrollen können in der Administrations-Oberfläche definiert werden.
+Dank des fein granularen Berechtigungskonzepts des SCM-Managers können Nutzern und Gruppen, basierend auf definierbaren
+Rollen oder auf individuellen Einstellungen, Rechte zugewiesen werden. Berechtigungen können global, auf Namespace-Ebene
+und auf Repository-Ebene vergeben werden. Globale Berechtigungen werden in der Administrations-Oberfläche des
+SCM-Managers vergeben. Unter diesem Eintrag handelt es sich um Repository-bezogene Berechtigungen.
-Berechtigungen auf Namespace-Ebene können über die Einstellungen für Namespaces bearbeitet werden. Diese sind über das Einstellungs-Symbol neben den Namespace-Überschriften auf der Repository-Übersicht erreichbar.
+Die Berechtigungen können jeweils für Gruppen und für Benutzer vergeben werden. Dabei gibt es die Möglichkeiten die
+Berechtigungen über Berechtigungsrollen zu definieren oder jede Berechtigung einzeln zu vergeben. Die
+Berechtigungsrollen können in der Administrations-Oberfläche definiert werden.
+
+Berechtigungen auf Namespace-Ebene können über die Einstellungen für Namespaces bearbeitet werden. Diese sind über das
+Einstellungs-Symbol neben den Namespace-Überschriften auf der Repository-Übersicht erreichbar.

diff --git a/docs/en/user/repo/assets/repository-settings-general-git.png b/docs/en/user/repo/assets/repository-settings-general-git.png
index 2a0f5258da..76a78e68b1 100644
Binary files a/docs/en/user/repo/assets/repository-settings-general-git.png and b/docs/en/user/repo/assets/repository-settings-general-git.png differ
diff --git a/docs/en/user/repo/settings.md b/docs/en/user/repo/settings.md
index 52e7da246e..133a27e9f8 100644
--- a/docs/en/user/repo/settings.md
+++ b/docs/en/user/repo/settings.md
@@ -1,22 +1,33 @@
---
-title: Repository
-subtitle: Settings
+title: Repository subtitle: Settings
---
-By default, there are two items in the repository settings. Depending on additional plugins that are installed, there can be considerably more items.
+By default, there are two items in the repository settings. Depending on additional plugins that are installed, there
+can be considerably more items.
### General
-The "General" item allows you to edit the additional information of the repository. Git repositories for example also have the option to change the default branch here. The default branch is the one that is used when working with the repository if no specific branch is selected.
-In the danger zone at the bottom you may rename the repository or delete it. If the namespace strategy in the global SCM-Manager config is set to `custom` you may even rename the repository namespace.
+The "General" item allows you to edit the additional information of the repository. Git repositories for example also
+have the option to change the default branch here. The default branch is the one that is used when working with the
+repository if no specific branch is selected.
+
+In the danger zone at the bottom you may rename the repository, delete it or mark it as archived. If the namespace
+strategy in the global SCM-Manager config is set to `custom` you may even rename the repository namespace. If a
+repository is marked as archived, it can no longer be modified.

### Permissions
-Thanks to the finely granular permission concept of SCM-Manager, users and groups can be authorized based on definable roles or individual settings. Permissions can be granted globally, namespace-wide, or repository-specific. Global permissions are managed in the administration area of SCM-Manager. The following image shows repository-specific permissions.
-Permissions can be granted to groups or users. It is possible to manage each permission individually or to create roles that contain several permissions. Roles can be defined in the administration area.
+Thanks to the finely granular permission concept of SCM-Manager, users and groups can be authorized based on definable
+roles or individual settings. Permissions can be granted globally, namespace-wide, or repository-specific. Global
+permissions are managed in the administration area of SCM-Manager. The following image shows repository-specific
+permissions.
-Namespace-wide permissions can be configured in the namespace settings. These can be accessed via the settings icon on the right-hand side of the namespace heading in the repository overview.
+Permissions can be granted to groups or users. It is possible to manage each permission individually or to create roles
+that contain several permissions. Roles can be defined in the administration area.
+
+Namespace-wide permissions can be configured in the namespace settings. These can be accessed via the settings icon on
+the right-hand side of the namespace heading in the repository overview.

diff --git a/pom.xml b/pom.xml
index 0c1c2bc550..29be306bc7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -935,7 +935,7 @@
9.4.34.v20201102
- 1.2.0
+ 1.3.01.7.0
diff --git a/scm-core/src/main/java/sonia/scm/repository/EventDrivenRepositoryArchiveCheck.java b/scm-core/src/main/java/sonia/scm/repository/EventDrivenRepositoryArchiveCheck.java
new file mode 100644
index 0000000000..e961fe82cc
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/repository/EventDrivenRepositoryArchiveCheck.java
@@ -0,0 +1,72 @@
+/*
+ * 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 com.github.legman.Subscribe;
+import sonia.scm.EagerSingleton;
+import sonia.scm.plugin.Extension;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+
+@Extension
+@EagerSingleton
+/**
+ * Default implementation of {@link RepositoryArchivedCheck}. This tracks the archive status of repositories by using
+ * {@link RepositoryModificationEvent}s. The initial set of archived repositories is read by
+ * {@link EventDrivenRepositoryArchiveCheckInitializer} on startup.
+ */
+public final class EventDrivenRepositoryArchiveCheck implements RepositoryArchivedCheck {
+
+ private static final Collection ARCHIVED_REPOSITORIES = Collections.synchronizedSet(new HashSet<>());
+
+ static void setAsArchived(String repositoryId) {
+ ARCHIVED_REPOSITORIES.add(repositoryId);
+ }
+
+ static void removeFromArchived(String repositoryId) {
+ ARCHIVED_REPOSITORIES.remove(repositoryId);
+ }
+
+ static boolean isRepositoryArchived(String repositoryId) {
+ return ARCHIVED_REPOSITORIES.contains(repositoryId);
+ }
+
+ @Override
+ public boolean isArchived(String repositoryId) {
+ return isRepositoryArchived(repositoryId);
+ }
+
+ @Subscribe(async = false)
+ public void updateListener(RepositoryModificationEvent event) {
+ Repository repository = event.getItem();
+ if (repository.isArchived()) {
+ EventDrivenRepositoryArchiveCheck.setAsArchived(repository.getId());
+ } else {
+ EventDrivenRepositoryArchiveCheck.removeFromArchived(repository.getId());
+ }
+ }
+}
diff --git a/scm-core/src/main/java/sonia/scm/repository/EventDrivenRepositoryArchiveCheckInitializer.java b/scm-core/src/main/java/sonia/scm/repository/EventDrivenRepositoryArchiveCheckInitializer.java
new file mode 100644
index 0000000000..10e7430399
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/repository/EventDrivenRepositoryArchiveCheckInitializer.java
@@ -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 sonia.scm.EagerSingleton;
+import sonia.scm.Initable;
+import sonia.scm.SCMContextProvider;
+import sonia.scm.plugin.Extension;
+
+import javax.inject.Inject;
+
+@Extension
+@EagerSingleton
+final class EventDrivenRepositoryArchiveCheckInitializer implements Initable {
+
+ private final RepositoryDAO repositoryDAO;
+
+ @Inject
+ EventDrivenRepositoryArchiveCheckInitializer(RepositoryDAO repositoryDAO) {
+ this.repositoryDAO = repositoryDAO;
+ }
+
+ @Override
+ public void init(SCMContextProvider context) {
+ repositoryDAO.getAll()
+ .stream()
+ .filter(Repository::isArchived)
+ .map(Repository::getId)
+ .forEach(EventDrivenRepositoryArchiveCheck::setAsArchived);
+ }
+}
diff --git a/scm-core/src/main/java/sonia/scm/repository/PermissionProvider.java b/scm-core/src/main/java/sonia/scm/repository/PermissionProvider.java
new file mode 100644
index 0000000000..4326bc62c9
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/repository/PermissionProvider.java
@@ -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 java.util.Collection;
+
+/**
+ * Provider for available verbs and roles for repository permissions, such as "read", "modify", "pull", "push", etc.
+ * This collection of verbs can be extended by plugins and be grouped to roles, such as "READ", "WRITE", etc.
+ * The permissions are configured by "repository-permissions.xml" files from the core and from plugins.
+ *
+ * @since 2.12.0
+ */
+public interface PermissionProvider {
+
+ /**
+ * The collection of all registered verbs.
+ */
+ Collection availableVerbs();
+
+ /**
+ * The collection of verbs that are marked as "read only". These verbs are safe for archived or otherwise read only
+ * repositories.
+ */
+ Collection readOnlyVerbs();
+
+ /**
+ * The collection of roles defined and extended by core and plugins.
+ */
+ Collection availableRoles();
+}
diff --git a/scm-core/src/main/java/sonia/scm/repository/Repository.java b/scm-core/src/main/java/sonia/scm/repository/Repository.java
index 92bbe6183b..c0e47713bd 100644
--- a/scm-core/src/main/java/sonia/scm/repository/Repository.java
+++ b/scm-core/src/main/java/sonia/scm/repository/Repository.java
@@ -24,6 +24,7 @@
package sonia.scm.repository;
+import com.github.sdorra.ssp.Guard;
import com.github.sdorra.ssp.PermissionObject;
import com.github.sdorra.ssp.StaticPermissions;
import com.google.common.base.MoreObjects;
@@ -51,14 +52,17 @@ import java.util.Set;
*
* @author Sebastian Sdorra
*/
-@StaticPermissions(
- value = "repository",
- permissions = {"read", "modify", "delete", "rename", "healthCheck", "pull", "push", "permissionRead", "permissionWrite"},
- custom = true, customGlobal = true
-)
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "repositories")
-public class Repository extends BasicPropertiesAware implements ModelObject, PermissionObject{
+@StaticPermissions(
+ value = "repository",
+ permissions = {"read", "modify", "delete", "rename", "healthCheck", "pull", "push", "permissionRead", "permissionWrite", "archive"},
+ custom = true, customGlobal = true,
+ guards = {
+ @Guard(guard = RepositoryPermissionGuard.class)
+ }
+)
+public class Repository extends BasicPropertiesAware implements ModelObject, PermissionObject {
private static final long serialVersionUID = 3486560714961909711L;
@@ -75,6 +79,7 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
@XmlElement(name = "permission")
private Set permissions = new HashSet<>();
private String type;
+ private boolean archived;
/**
@@ -204,6 +209,15 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
return type;
}
+ /**
+ * Returns true, when the repository is marked as "archived". An archived repository cannot be modified.
+ *
+ * @since 2.11.0
+ */
+ public boolean isArchived() {
+ return archived;
+ }
+
/**
* Returns {@code true} if the repository is healthy.
*
@@ -276,6 +290,15 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
this.type = type;
}
+ /**
+ * Set this to true to mark the repository as "archived". An archived repository cannot be modified.
+ *
+ * @since 2.11.0
+ */
+ public void setArchived(boolean archived) {
+ this.archived = archived;
+ }
+
public void setHealthCheckFailures(List healthCheckFailures) {
this.healthCheckFailures = healthCheckFailures;
}
diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryArchivedCheck.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryArchivedCheck.java
new file mode 100644
index 0000000000..2955bdcbb5
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryArchivedCheck.java
@@ -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;
+
+/**
+ * Implementations of this class can be used to check whether a repository is archived.
+ *
+ * @since 1.12.0
+ */
+public interface RepositoryArchivedCheck {
+
+ /**
+ * Checks whether the repository with the given id is archived or not.
+ * @param repositoryId The id of the repository to check.
+ * @return true when the repository with the given id is archived, false otherwise.
+ */
+ boolean isArchived(String repositoryId);
+
+ /**
+ * Checks whether the given repository is archived or not. This checks the status on behalf of the id of the
+ * repository, not by the archive flag provided by the repository itself.
+ * @param repository The repository to check.
+ * @return true when the given repository is archived, false otherwise.
+ */
+ default boolean isArchived(Repository repository) {
+ return isArchived(repository.getId());
+ }
+}
diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java
index ba5ff44ce2..b9b6212380 100644
--- a/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java
+++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java
@@ -113,4 +113,18 @@ public interface RepositoryManager
afterCreation.accept(newRepository);
return newRepository;
}
+
+ /**
+ * @param repository the {@link Repository} to be archived.
+ *
+ * @since 2.12.0
+ */
+ void archive(Repository repository);
+
+ /**
+ * @param repository the {@link Repository} to be "unarchived".
+ *
+ * @since 2.12.0
+ */
+ void unarchive(Repository repository);
}
diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryManagerDecorator.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryManagerDecorator.java
index 6a661c83e5..f4aed720d8 100644
--- a/scm-core/src/main/java/sonia/scm/repository/RepositoryManagerDecorator.java
+++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryManagerDecorator.java
@@ -136,6 +136,16 @@ public class RepositoryManagerDecorator
return decorated.getAllNamespaces();
}
+ @Override
+ public void archive(Repository repository) {
+ decorated.archive(repository);
+ }
+
+ @Override
+ public void unarchive(Repository repository) {
+ decorated.unarchive(repository);
+ }
+
//~--- fields ---------------------------------------------------------------
/**
diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryPermissionGuard.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryPermissionGuard.java
new file mode 100644
index 0000000000..7b17dc30d9
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryPermissionGuard.java
@@ -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;
+
+import com.github.sdorra.ssp.PermissionActionCheckInterceptor;
+import com.github.sdorra.ssp.PermissionGuard;
+import org.apache.shiro.authz.AuthorizationException;
+import org.apache.shiro.subject.Subject;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.function.BooleanSupplier;
+
+import static sonia.scm.repository.EventDrivenRepositoryArchiveCheck.isRepositoryArchived;
+
+/**
+ * This intercepts permission checks for repositories and blocks write permissions for archived repositories.
+ * Read only permissions are set at startup by {@link RepositoryPermissionGuardInitializer}.
+ */
+public class RepositoryPermissionGuard implements PermissionGuard {
+
+ private static final Collection READ_ONLY_VERBS = Collections.synchronizedSet(new HashSet<>());
+
+ static void setReadOnlyVerbs(Collection readOnlyVerbs) {
+ READ_ONLY_VERBS.addAll(readOnlyVerbs);
+ }
+
+ @Override
+ public PermissionActionCheckInterceptor intercept(String permission) {
+ if (READ_ONLY_VERBS.contains(permission)) {
+ return new PermissionActionCheckInterceptor() {};
+ } else {
+ return new WriteInterceptor();
+ }
+ }
+
+ private static class WriteInterceptor implements PermissionActionCheckInterceptor {
+ @Override
+ public void check(Subject subject, String id, Runnable delegate) {
+ delegate.run();
+ if (isRepositoryArchived(id)) {
+ throw new AuthorizationException("repository is archived");
+ }
+ }
+
+ @Override
+ public boolean isPermitted(Subject subject, String id, BooleanSupplier delegate) {
+ return !isRepositoryArchived(id) && delegate.getAsBoolean();
+ }
+ }
+}
diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryPermissionGuardInitializer.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryPermissionGuardInitializer.java
new file mode 100644
index 0000000000..148782c73c
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryPermissionGuardInitializer.java
@@ -0,0 +1,52 @@
+/*
+ * 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 sonia.scm.EagerSingleton;
+import sonia.scm.Initable;
+import sonia.scm.SCMContextProvider;
+import sonia.scm.plugin.Extension;
+
+import javax.inject.Inject;
+
+/**
+ * Initializes read only permissions for {@link RepositoryPermissionGuard} at startup.
+ */
+@Extension
+@EagerSingleton
+final class RepositoryPermissionGuardInitializer implements Initable {
+
+ private final PermissionProvider permissionProvider;
+
+ @Inject
+ RepositoryPermissionGuardInitializer(PermissionProvider permissionProvider) {
+ this.permissionProvider = permissionProvider;
+ }
+
+ @Override
+ public void init(SCMContextProvider context) {
+ RepositoryPermissionGuard.setReadOnlyVerbs(permissionProvider.readOnlyVerbs());
+ }
+}
diff --git a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryArchivedException.java b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryArchivedException.java
new file mode 100644
index 0000000000..4343a5702e
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryArchivedException.java
@@ -0,0 +1,45 @@
+/*
+ * 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 sonia.scm.ExceptionWithContext;
+import sonia.scm.repository.Repository;
+
+import static java.lang.String.format;
+import static sonia.scm.ContextEntry.ContextBuilder.entity;
+
+public class RepositoryArchivedException extends ExceptionWithContext {
+
+ public static final String CODE = "3hSIlptme1";
+
+ protected RepositoryArchivedException(Repository repository) {
+ super(entity(repository).build(), format("Repository %s is marked as archived and must not be modified", repository));
+ }
+
+ @Override
+ public String getCode() {
+ return CODE;
+ }
+}
diff --git a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java
index 9c6e91d182..f8a4db1d10 100644
--- a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java
+++ b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java
@@ -182,6 +182,7 @@ public final class RepositoryService implements Closeable {
* by the implementation of the repository service provider.
*/
public BranchCommandBuilder getBranchCommand() {
+ verifyNotArchived();
RepositoryPermissions.push(getRepository()).check();
LOG.debug("create branch command for repository {}",
repository.getNamespaceAndName());
@@ -332,6 +333,7 @@ public final class RepositoryService implements Closeable {
* @since 1.31
*/
public PullCommandBuilder getPullCommand() {
+ verifyNotArchived();
LOG.debug("create pull command for repository {}",
repository.getNamespaceAndName());
@@ -386,6 +388,7 @@ public final class RepositoryService implements Closeable {
* by the implementation of the repository service provider.
*/
public TagCommandBuilder getTagCommand() {
+ verifyNotArchived();
return new TagCommandBuilder(provider.getTagCommand());
}
@@ -415,6 +418,7 @@ public final class RepositoryService implements Closeable {
* @since 2.0.0
*/
public MergeCommandBuilder getMergeCommand() {
+ verifyNotArchived();
LOG.debug("create merge command for repository {}",
repository.getNamespaceAndName());
@@ -436,6 +440,7 @@ public final class RepositoryService implements Closeable {
* @since 2.0.0
*/
public ModifyCommandBuilder getModifyCommand() {
+ verifyNotArchived();
LOG.debug("create modify command for repository {}",
repository.getNamespaceAndName());
@@ -484,6 +489,12 @@ public final class RepositoryService implements Closeable {
.filter(protocol -> !Authentications.isAuthenticatedSubjectAnonymous() || protocol.isAnonymousEnabled());
}
+ private void verifyNotArchived() {
+ if (getRepository().isArchived()) {
+ throw new RepositoryArchivedException(getRepository());
+ }
+ }
+
@SuppressWarnings({"rawtypes", "java:S3740"})
private ScmProtocol createProviderInstanceForRepository(ScmProtocolProvider protocolProvider) {
return protocolProvider.get(repository);
diff --git a/scm-core/src/main/java/sonia/scm/store/AbstractStore.java b/scm-core/src/main/java/sonia/scm/store/AbstractStore.java
index d5c626c667..0cdb72ae4e 100644
--- a/scm-core/src/main/java/sonia/scm/store/AbstractStore.java
+++ b/scm-core/src/main/java/sonia/scm/store/AbstractStore.java
@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
-
+
package sonia.scm.store;
/**
@@ -38,6 +38,11 @@ public abstract class AbstractStore implements ConfigurationStore {
* stored object
*/
protected T storeObject;
+ private final boolean readOnly;
+
+ protected AbstractStore(boolean readOnly) {
+ this.readOnly = readOnly;
+ }
@Override
public T get() {
@@ -49,9 +54,12 @@ public abstract class AbstractStore implements ConfigurationStore {
}
@Override
- public void set(T obejct) {
- writeObject(obejct);
- this.storeObject = obejct;
+ public void set(T object) {
+ if (readOnly) {
+ throw new StoreReadOnlyException(object);
+ }
+ writeObject(object);
+ this.storeObject = object;
}
/**
diff --git a/scm-core/src/main/java/sonia/scm/store/StoreReadOnlyException.java b/scm-core/src/main/java/sonia/scm/store/StoreReadOnlyException.java
new file mode 100644
index 0000000000..afc82e7259
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/store/StoreReadOnlyException.java
@@ -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.store;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import sonia.scm.ExceptionWithContext;
+
+import static sonia.scm.ContextEntry.ContextBuilder.noContext;
+
+public class StoreReadOnlyException extends ExceptionWithContext {
+
+ private static final Logger LOG = LoggerFactory.getLogger(StoreReadOnlyException.class);
+
+ public static final String CODE = "3FSIYtBJw1";
+
+ public StoreReadOnlyException(String location) {
+ super(noContext(), String.format("Store is read only, could not write location %s", location));
+ LOG.error(getMessage());
+ }
+
+ public StoreReadOnlyException(Object object) {
+ super(noContext(), String.format("Store is read only, could not write object of type %s: %s", object.getClass(), object));
+ LOG.error(getMessage());
+ }
+
+ @Override
+ public String getCode () {
+ return CODE;
+ }
+ }
diff --git a/scm-core/src/test/java/sonia/scm/repository/EventDrivenRepositoryArchiveCheckTest.java b/scm-core/src/test/java/sonia/scm/repository/EventDrivenRepositoryArchiveCheckTest.java
new file mode 100644
index 0000000000..4a518f95e3
--- /dev/null
+++ b/scm-core/src/test/java/sonia/scm/repository/EventDrivenRepositoryArchiveCheckTest.java
@@ -0,0 +1,75 @@
+/*
+ * 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.junit.jupiter.api.Test;
+import sonia.scm.HandlerEventType;
+
+import java.util.Collections;
+
+import static java.util.Collections.singleton;
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static sonia.scm.repository.EventDrivenRepositoryArchiveCheck.setAsArchived;
+
+class EventDrivenRepositoryArchiveCheckTest {
+
+ private static final Repository NORMAL_REPOSITORY = new Repository("hog", "git", "hitchhiker", "hog");
+ private static final Repository ARCHIVED_REPOSITORY = new Repository("hog", "git", "hitchhiker", "hog");
+ static {
+ ARCHIVED_REPOSITORY.setArchived(true);
+ }
+
+ EventDrivenRepositoryArchiveCheck check = new EventDrivenRepositoryArchiveCheck();
+
+ @Test
+ void shouldBeNotArchivedByDefault() {
+ assertThat(check.isArchived("hog")).isFalse();
+ }
+
+ @Test
+ void shouldBeArchivedAfterFlagHasBeenSet() {
+ check.updateListener(new RepositoryModificationEvent(HandlerEventType.MODIFY, ARCHIVED_REPOSITORY, NORMAL_REPOSITORY));
+ assertThat(check.isArchived("hog")).isTrue();
+ }
+
+ @Test
+ void shouldNotBeArchivedAfterFlagHasBeenRemoved() {
+ setAsArchived("hog");
+ check.updateListener(new RepositoryModificationEvent(HandlerEventType.MODIFY, NORMAL_REPOSITORY, ARCHIVED_REPOSITORY));
+ assertThat(check.isArchived("hog")).isFalse();
+ }
+
+ @Test
+ void shouldBeInitialized() {
+ RepositoryDAO repositoryDAO = mock(RepositoryDAO.class);
+ when(repositoryDAO.getAll()).thenReturn(singleton(ARCHIVED_REPOSITORY));
+
+ new EventDrivenRepositoryArchiveCheckInitializer(repositoryDAO).init(null);
+
+ assertThat(check.isArchived("hog")).isTrue();
+ }
+}
diff --git a/scm-core/src/test/java/sonia/scm/repository/RepositoryPermissionGuardTest.java b/scm-core/src/test/java/sonia/scm/repository/RepositoryPermissionGuardTest.java
new file mode 100644
index 0000000000..6d2d3215f6
--- /dev/null
+++ b/scm-core/src/test/java/sonia/scm/repository/RepositoryPermissionGuardTest.java
@@ -0,0 +1,146 @@
+/*
+ * 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 com.github.sdorra.ssp.PermissionActionCheckInterceptor;
+import org.apache.shiro.authz.AuthorizationException;
+import org.apache.shiro.subject.Subject;
+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.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.function.BooleanSupplier;
+
+import static java.util.Arrays.asList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class RepositoryPermissionGuardTest {
+
+ @Mock
+ private Subject subject;
+ @Mock
+ private BooleanSupplier permittedDelegate;
+ @Mock
+ private Runnable checkDelegate;
+
+ @BeforeAll
+ static void setReadOnlyVerbs() {
+ RepositoryPermissionGuard.setReadOnlyVerbs(asList("read"));
+ }
+
+ @Nested
+ class ForReadOnlyVerb {
+
+ PermissionActionCheckInterceptor readInterceptor = new RepositoryPermissionGuard().intercept("read");
+
+ @Test
+ void shouldNotInterceptPermissionCheck() {
+ when(permittedDelegate.getAsBoolean()).thenReturn(true);
+
+ assertThat(readInterceptor.isPermitted(subject, "1", permittedDelegate)).isTrue();
+
+ verify(permittedDelegate).getAsBoolean();
+ }
+
+ @Test
+ void shouldNotInterceptCheckRequest() {
+ readInterceptor.check(subject, "1", checkDelegate);
+
+ verify(checkDelegate).run();
+ }
+ }
+
+ @Nested
+ class ForModifyingVerb {
+
+ PermissionActionCheckInterceptor readInterceptor = new RepositoryPermissionGuard().intercept("modify");
+
+ @Nested
+ class WithNormalRepository {
+
+ @Test
+ void shouldInterceptPermissionCheck() {
+ when(permittedDelegate.getAsBoolean()).thenReturn(true);
+
+ assertThat(readInterceptor.isPermitted(subject, "1", permittedDelegate)).isTrue();
+
+ verify(permittedDelegate).getAsBoolean();
+ }
+
+ @Test
+ void shouldInterceptCheckRequest() {
+ readInterceptor.check(subject, "1", checkDelegate);
+
+ verify(checkDelegate).run();
+ }
+ }
+
+ @Nested
+ class WithArchivedRepository {
+
+ @BeforeEach
+ void mockArchivedRepository() {
+ EventDrivenRepositoryArchiveCheck.setAsArchived("1");
+ }
+
+ @AfterEach
+ void removeArchiveFlag() {
+ EventDrivenRepositoryArchiveCheck.removeFromArchived("1");
+ }
+
+ @Test
+ void shouldInterceptPermissionCheck() {
+ assertThat(readInterceptor.isPermitted(subject, "1", permittedDelegate)).isFalse();
+
+ verify(permittedDelegate, never()).getAsBoolean();
+ }
+
+ @Test
+ void shouldInterceptCheckRequest() {
+ assertThrows(AuthorizationException.class, () -> readInterceptor.check(subject, "1", checkDelegate));
+ }
+
+ @Test
+ void shouldThrowConcretePermissionExceptionOverArchiveException() {
+ doThrow(new AuthorizationException()).when(checkDelegate).run();
+
+ assertThrows(AuthorizationException.class, () -> readInterceptor.check(subject, "1", checkDelegate));
+
+ verify(checkDelegate).run();
+ }
+ }
+ }
+}
diff --git a/scm-core/src/test/java/sonia/scm/repository/api/RepositoryServiceTest.java b/scm-core/src/test/java/sonia/scm/repository/api/RepositoryServiceTest.java
index c702cc64de..fcde81f786 100644
--- a/scm-core/src/test/java/sonia/scm/repository/api/RepositoryServiceTest.java
+++ b/scm-core/src/test/java/sonia/scm/repository/api/RepositoryServiceTest.java
@@ -109,6 +109,19 @@ class RepositoryServiceTest {
assertThrows(IllegalArgumentException.class, () -> repositoryService.getProtocol(UnknownScmProtocol.class));
}
+ @Test
+ void shouldFailForArchivedRepository() {
+ repository.setArchived(true);
+ RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider()), null, eMail);
+
+ assertThrows(RepositoryArchivedException.class, () -> repositoryService.getModifyCommand());
+ assertThrows(RepositoryArchivedException.class, () -> repositoryService.getBranchCommand());
+ assertThrows(RepositoryArchivedException.class, () -> repositoryService.getPullCommand());
+ assertThrows(RepositoryArchivedException.class, () -> repositoryService.getTagCommand());
+ assertThrows(RepositoryArchivedException.class, () -> repositoryService.getMergeCommand());
+ assertThrows(RepositoryArchivedException.class, () -> repositoryService.getModifyCommand());
+ }
+
private static class DummyHttpProtocol extends HttpScmProtocol {
private final boolean anonymousEnabled;
diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryDAO.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryDAO.java
index 7ec23ce880..d99de42a91 100644
--- a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryDAO.java
+++ b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryDAO.java
@@ -34,6 +34,7 @@ import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryDAO;
import sonia.scm.repository.RepositoryLocationResolver;
+import sonia.scm.store.StoreReadOnlyException;
import javax.inject.Inject;
import java.io.IOException;
@@ -139,6 +140,9 @@ public class XmlRepositoryDAO implements RepositoryDAO {
@Override
public void modify(Repository repository) {
Repository clone = repository.clone();
+ if (clone.isArchived() && byId.get(clone.getId()).isArchived()) {
+ throw new StoreReadOnlyException(repository);
+ }
synchronized (this) {
// remove old namespaceAndName from map, in case of rename
@@ -158,6 +162,9 @@ public class XmlRepositoryDAO implements RepositoryDAO {
@Override
public void delete(Repository repository) {
+ if (repository.isArchived()) {
+ throw new StoreReadOnlyException(repository);
+ }
Path path;
synchronized (this) {
Repository prev = byId.remove(repository.getId());
diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/FileBasedStore.java b/scm-dao-xml/src/main/java/sonia/scm/store/FileBasedStore.java
index d3b186df94..480b74f31b 100644
--- a/scm-dao-xml/src/main/java/sonia/scm/store/FileBasedStore.java
+++ b/scm-dao-xml/src/main/java/sonia/scm/store/FileBasedStore.java
@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
-
+
package sonia.scm.store;
//~--- non-JDK imports --------------------------------------------------------
@@ -60,10 +60,11 @@ public abstract class FileBasedStore implements MultiEntryStore
* @param directory
* @param suffix
*/
- public FileBasedStore(File directory, String suffix)
+ public FileBasedStore(File directory, String suffix, boolean readOnly)
{
this.directory = directory;
this.suffix = suffix;
+ this.readOnly = readOnly;
}
//~--- methods --------------------------------------------------------------
@@ -145,6 +146,8 @@ public abstract class FileBasedStore implements MultiEntryStore
{
logger.trace("delete store entry {}", file);
+ assertNotReadOnly();
+
if (file.exists() &&!file.delete())
{
throw new StoreException(
@@ -185,6 +188,12 @@ public abstract class FileBasedStore implements MultiEntryStore
return name.substring(0, name.length() - suffix.length());
}
+ protected void assertNotReadOnly() {
+ if (readOnly) {
+ throw new StoreReadOnlyException(directory.getAbsoluteFile().toString());
+ }
+ }
+
//~--- fields ---------------------------------------------------------------
/** Field description */
@@ -192,4 +201,6 @@ public abstract class FileBasedStore implements MultiEntryStore
/** Field description */
private final String suffix;
+
+ private final boolean readOnly;
}
diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/FileBasedStoreFactory.java b/scm-dao-xml/src/main/java/sonia/scm/store/FileBasedStoreFactory.java
index f29cf728d0..9d9520b75e 100644
--- a/scm-dao-xml/src/main/java/sonia/scm/store/FileBasedStoreFactory.java
+++ b/scm-dao-xml/src/main/java/sonia/scm/store/FileBasedStoreFactory.java
@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
-
+
package sonia.scm.store;
//~--- non-JDK imports --------------------------------------------------------
@@ -29,6 +29,7 @@ package sonia.scm.store;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.SCMContextProvider;
+import sonia.scm.repository.RepositoryArchivedCheck;
import sonia.scm.repository.RepositoryLocationResolver;
import sonia.scm.util.IOUtil;
@@ -48,14 +49,16 @@ public abstract class FileBasedStoreFactory {
* the logger for FileBasedStoreFactory
*/
private static final Logger LOG = LoggerFactory.getLogger(FileBasedStoreFactory.class);
- private SCMContextProvider contextProvider;
- private RepositoryLocationResolver repositoryLocationResolver;
- private Store store;
+ private final SCMContextProvider contextProvider;
+ private final RepositoryLocationResolver repositoryLocationResolver;
+ private final Store store;
+ private final RepositoryArchivedCheck archivedCheck;
- protected FileBasedStoreFactory(SCMContextProvider contextProvider, RepositoryLocationResolver repositoryLocationResolver, Store store) {
+ protected FileBasedStoreFactory(SCMContextProvider contextProvider, RepositoryLocationResolver repositoryLocationResolver, Store store, RepositoryArchivedCheck archivedCheck) {
this.contextProvider = contextProvider;
this.repositoryLocationResolver = repositoryLocationResolver;
this.store = store;
+ this.archivedCheck = archivedCheck;
}
protected File getStoreLocation(StoreParameters storeParameters) {
@@ -79,6 +82,10 @@ public abstract class FileBasedStoreFactory {
return new File(storeDirectory, name);
}
+ protected boolean mustBeReadOnly(StoreParameters storeParameters) {
+ return storeParameters.getRepositoryId() != null && archivedCheck.isArchived(storeParameters.getRepositoryId());
+ }
+
/**
* Get the store directory of a specific repository
* @param store the type of the store
diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/FileBlobStore.java b/scm-dao-xml/src/main/java/sonia/scm/store/FileBlobStore.java
index a77853e414..9345cdbde9 100644
--- a/scm-dao-xml/src/main/java/sonia/scm/store/FileBlobStore.java
+++ b/scm-dao-xml/src/main/java/sonia/scm/store/FileBlobStore.java
@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
-
+
package sonia.scm.store;
//~--- non-JDK imports --------------------------------------------------------
@@ -58,8 +58,8 @@ public class FileBlobStore extends FileBasedStore implements BlobStore {
private final KeyGenerator keyGenerator;
- FileBlobStore(KeyGenerator keyGenerator, File directory) {
- super(directory, SUFFIX);
+ FileBlobStore(KeyGenerator keyGenerator, File directory, boolean readOnly) {
+ super(directory, SUFFIX, readOnly);
this.keyGenerator = keyGenerator;
}
@@ -74,6 +74,8 @@ public class FileBlobStore extends FileBasedStore implements BlobStore {
"id argument is required");
LOG.debug("create new blob with id {}", id);
+ assertNotReadOnly();
+
File file = getFile(id);
try {
@@ -94,6 +96,7 @@ public class FileBlobStore extends FileBasedStore implements BlobStore {
@Override
public void remove(Blob blob) {
+ assertNotReadOnly();
Preconditions.checkNotNull(blob, "blob argument is required");
remove(blob.getId());
}
diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/FileBlobStoreFactory.java b/scm-dao-xml/src/main/java/sonia/scm/store/FileBlobStoreFactory.java
index 2bd6cdc886..45982a2c16 100644
--- a/scm-dao-xml/src/main/java/sonia/scm/store/FileBlobStoreFactory.java
+++ b/scm-dao-xml/src/main/java/sonia/scm/store/FileBlobStoreFactory.java
@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
-
+
package sonia.scm.store;
//~--- non-JDK imports --------------------------------------------------------
@@ -31,6 +31,7 @@ import com.google.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.SCMContextProvider;
+import sonia.scm.repository.RepositoryArchivedCheck;
import sonia.scm.repository.RepositoryLocationResolver;
import sonia.scm.security.KeyGenerator;
import sonia.scm.util.IOUtil;
@@ -59,8 +60,8 @@ public class FileBlobStoreFactory extends FileBasedStoreFactory implements BlobS
* @param keyGenerator key generator
*/
@Inject
- public FileBlobStoreFactory(SCMContextProvider contextProvider , RepositoryLocationResolver repositoryLocationResolver, KeyGenerator keyGenerator) {
- super(contextProvider, repositoryLocationResolver, Store.BLOB);
+ public FileBlobStoreFactory(SCMContextProvider contextProvider , RepositoryLocationResolver repositoryLocationResolver, KeyGenerator keyGenerator, RepositoryArchivedCheck archivedCheck) {
+ super(contextProvider, repositoryLocationResolver, Store.BLOB, archivedCheck);
this.keyGenerator = keyGenerator;
}
@@ -69,8 +70,6 @@ public class FileBlobStoreFactory extends FileBasedStoreFactory implements BlobS
public BlobStore getStore(StoreParameters storeParameters) {
File storeLocation = getStoreLocation(storeParameters);
IOUtil.mkdirs(storeLocation);
- return new FileBlobStore(keyGenerator, storeLocation);
+ return new FileBlobStore(keyGenerator, storeLocation, mustBeReadOnly(storeParameters));
}
-
-
}
diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationEntryStoreFactory.java b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationEntryStoreFactory.java
index 7feae4e0df..0727acac19 100644
--- a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationEntryStoreFactory.java
+++ b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationEntryStoreFactory.java
@@ -29,6 +29,7 @@ package sonia.scm.store;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import sonia.scm.SCMContextProvider;
+import sonia.scm.repository.RepositoryArchivedCheck;
import sonia.scm.repository.RepositoryLocationResolver;
import sonia.scm.security.KeyGenerator;
@@ -45,8 +46,8 @@ public class JAXBConfigurationEntryStoreFactory extends FileBasedStoreFactory
private KeyGenerator keyGenerator;
@Inject
- public JAXBConfigurationEntryStoreFactory(SCMContextProvider contextProvider , RepositoryLocationResolver repositoryLocationResolver, KeyGenerator keyGenerator) {
- super(contextProvider, repositoryLocationResolver, Store.CONFIG);
+ public JAXBConfigurationEntryStoreFactory(SCMContextProvider contextProvider, RepositoryLocationResolver repositoryLocationResolver, KeyGenerator keyGenerator, RepositoryArchivedCheck archivedCheck) {
+ super(contextProvider, repositoryLocationResolver, Store.CONFIG, archivedCheck);
this.keyGenerator = keyGenerator;
}
diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationStore.java b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationStore.java
index 6b2cf9993a..474a7b76a8 100644
--- a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationStore.java
+++ b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationStore.java
@@ -46,7 +46,8 @@ public class JAXBConfigurationStore extends AbstractStore {
private final Class type;
private final File configFile;
- public JAXBConfigurationStore(TypedStoreContext context, Class type, File configFile) {
+ public JAXBConfigurationStore(TypedStoreContext context, Class type, File configFile, boolean readOnly) {
+ super(readOnly);
this.context = context;
this.type = type;
this.configFile = configFile;
diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationStoreFactory.java b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationStoreFactory.java
index 29cdf471a3..5f4a17c014 100644
--- a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationStoreFactory.java
+++ b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationStoreFactory.java
@@ -27,6 +27,7 @@ package sonia.scm.store;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import sonia.scm.SCMContextProvider;
+import sonia.scm.repository.RepositoryArchivedCheck;
import sonia.scm.repository.RepositoryLocationResolver;
/**
@@ -43,8 +44,8 @@ public class JAXBConfigurationStoreFactory extends FileBasedStoreFactory impleme
* @param repositoryLocationResolver Resolver to get the repository Directory
*/
@Inject
- public JAXBConfigurationStoreFactory(SCMContextProvider contextProvider, RepositoryLocationResolver repositoryLocationResolver) {
- super(contextProvider, repositoryLocationResolver, Store.CONFIG);
+ public JAXBConfigurationStoreFactory(SCMContextProvider contextProvider, RepositoryLocationResolver repositoryLocationResolver, RepositoryArchivedCheck archivedCheck) {
+ super(contextProvider, repositoryLocationResolver, Store.CONFIG, archivedCheck);
}
@Override
@@ -55,7 +56,8 @@ public class JAXBConfigurationStoreFactory extends FileBasedStoreFactory impleme
storeParameters.getType(),
getStoreLocation(storeParameters.getName().concat(StoreConstants.FILE_EXTENSION),
storeParameters.getType(),
- storeParameters.getRepositoryId())
+ storeParameters.getRepositoryId()),
+ mustBeReadOnly(storeParameters)
);
}
}
diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBDataStore.java b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBDataStore.java
index e963c58983..e25a66ff43 100644
--- a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBDataStore.java
+++ b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBDataStore.java
@@ -52,8 +52,8 @@ public class JAXBDataStore extends FileBasedStore implements DataStore
private final KeyGenerator keyGenerator;
private final TypedStoreContext context;
- JAXBDataStore(KeyGenerator keyGenerator, TypedStoreContext context, File directory) {
- super(directory, StoreConstants.FILE_EXTENSION);
+ JAXBDataStore(KeyGenerator keyGenerator, TypedStoreContext context, File directory, boolean readOnly) {
+ super(directory, StoreConstants.FILE_EXTENSION, readOnly);
this.keyGenerator = keyGenerator;
this.directory = directory;
this.context = context;
@@ -63,6 +63,8 @@ public class JAXBDataStore extends FileBasedStore implements DataStore
public void put(String id, T item) {
LOG.debug("put item {} to store", id);
+ assertNotReadOnly();
+
File file = getFile(id);
try {
diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBDataStoreFactory.java b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBDataStoreFactory.java
index 80e2d260d2..d0ec860221 100644
--- a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBDataStoreFactory.java
+++ b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBDataStoreFactory.java
@@ -28,8 +28,8 @@ package sonia.scm.store;
import com.google.inject.Inject;
import com.google.inject.Singleton;
-
import sonia.scm.SCMContextProvider;
+import sonia.scm.repository.RepositoryArchivedCheck;
import sonia.scm.repository.RepositoryLocationResolver;
import sonia.scm.security.KeyGenerator;
import sonia.scm.util.IOUtil;
@@ -44,11 +44,11 @@ import java.io.File;
public class JAXBDataStoreFactory extends FileBasedStoreFactory
implements DataStoreFactory {
- private KeyGenerator keyGenerator;
+ private final KeyGenerator keyGenerator;
@Inject
- public JAXBDataStoreFactory(SCMContextProvider contextProvider , RepositoryLocationResolver repositoryLocationResolver, KeyGenerator keyGenerator) {
- super(contextProvider, repositoryLocationResolver, Store.DATA);
+ public JAXBDataStoreFactory(SCMContextProvider contextProvider , RepositoryLocationResolver repositoryLocationResolver, KeyGenerator keyGenerator, RepositoryArchivedCheck archivedCheck) {
+ super(contextProvider, repositoryLocationResolver, Store.DATA, archivedCheck);
this.keyGenerator = keyGenerator;
}
@@ -56,6 +56,6 @@ public class JAXBDataStoreFactory extends FileBasedStoreFactory
public DataStore getStore(TypedStoreParameters storeParameters) {
File storeLocation = getStoreLocation(storeParameters);
IOUtil.mkdirs(storeLocation);
- return new JAXBDataStore<>(keyGenerator, TypedStoreContext.of(storeParameters), storeLocation);
+ return new JAXBDataStore<>(keyGenerator, TypedStoreContext.of(storeParameters), storeLocation, mustBeReadOnly(storeParameters));
}
}
diff --git a/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java b/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java
index 4b7bce2093..d5f89597f3 100644
--- a/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java
+++ b/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java
@@ -43,6 +43,7 @@ import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryLocationResolver;
import sonia.scm.repository.RepositoryPermission;
+import sonia.scm.store.StoreReadOnlyException;
import java.io.IOException;
import java.net.URL;
@@ -55,6 +56,7 @@ import java.util.function.Consumer;
import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.never;
@@ -233,6 +235,16 @@ class XmlRepositoryDAOTest {
assertThat(dao.get("42").getDescription()).isEqualTo("Heart of Gold");
}
+ @Test
+ void shouldNotModifyArchivedRepository() {
+ REPOSITORY.setArchived(true);
+ dao.add(REPOSITORY);
+
+ Repository heartOfGold = createRepository("42");
+ heartOfGold.setArchived(true);
+ assertThrows(StoreReadOnlyException.class, () -> dao.modify(heartOfGold));
+ }
+
@Test
void shouldRemoveRepository() {
dao.add(REPOSITORY);
@@ -247,6 +259,15 @@ class XmlRepositoryDAOTest {
assertThat(storePath).doesNotExist();
}
+ @Test
+ void shouldNotRemoveArchivedRepository() {
+ REPOSITORY.setArchived(true);
+ dao.add(REPOSITORY);
+ assertThat(dao.contains("42")).isTrue();
+
+ assertThrows(StoreReadOnlyException.class, () -> dao.delete(REPOSITORY));
+ }
+
@Test
void shouldRenameTheRepository() {
dao.add(REPOSITORY);
diff --git a/scm-dao-xml/src/test/java/sonia/scm/store/FileBlobStoreTest.java b/scm-dao-xml/src/test/java/sonia/scm/store/FileBlobStoreTest.java
index dbcfe2a692..12d78d4c7c 100644
--- a/scm-dao-xml/src/test/java/sonia/scm/store/FileBlobStoreTest.java
+++ b/scm-dao-xml/src/test/java/sonia/scm/store/FileBlobStoreTest.java
@@ -21,54 +21,212 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
-
+
package sonia.scm.store;
-//~--- non-JDK imports --------------------------------------------------------
-
-import org.junit.Test;
+import com.google.common.io.ByteStreams;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import sonia.scm.AbstractTestBase;
import sonia.scm.repository.Repository;
+import sonia.scm.repository.RepositoryArchivedCheck;
+import sonia.scm.repository.RepositoryTestData;
import sonia.scm.security.UUIDKeyGenerator;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
import java.util.List;
-import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
-/**
- *
- * @author Sebastian Sdorra
- */
-public class FileBlobStoreTest extends BlobStoreTestBase
+class FileBlobStoreTest extends AbstractTestBase
{
- /**
- * Method description
- *
- *
- * @return
- */
- @Override
- protected BlobStoreFactory createBlobStoreFactory()
+ private Repository repository = RepositoryTestData.createHeartOfGold();
+ private RepositoryArchivedCheck archivedCheck = mock(RepositoryArchivedCheck.class);
+ private BlobStore store;
+
+ @BeforeEach
+ void createBlobStore()
{
- return new FileBlobStoreFactory(contextProvider, repositoryLocationResolver, new UUIDKeyGenerator());
+ store = createBlobStoreFactory()
+ .withName("test")
+ .forRepository(repository)
+ .build();
}
@Test
- @SuppressWarnings("unchecked")
- public void shouldStoreAndLoadInRepository() {
- BlobStore store = createBlobStoreFactory()
- .withName("test")
- .forRepository(new Repository("id", "git", "ns", "n"))
- .build();
+ void testClear()
+ {
+ store.create("1");
+ store.create("2");
+ store.create("3");
- Blob createdBlob = store.create("abc");
- List storedBlobs = store.getAll();
+ assertNotNull(store.get("1"));
+ assertNotNull(store.get("2"));
+ assertNotNull(store.get("3"));
- assertNotNull(createdBlob);
- assertThat(storedBlobs)
- .isNotNull()
- .hasSize(1)
- .usingElementComparatorOnFields("id").containsExactly(createdBlob);
+ store.clear();
+
+ assertNull(store.get("1"));
+ assertNull(store.get("2"));
+ assertNull(store.get("3"));
+ }
+
+ @Test
+ void testContent() throws IOException
+ {
+ Blob blob = store.create();
+
+ write(blob, "Hello");
+ assertEquals("Hello", read(blob));
+
+ blob = store.get(blob.getId());
+ assertEquals("Hello", read(blob));
+
+ write(blob, "Other Text");
+ assertEquals("Other Text", read(blob));
+
+ blob = store.get(blob.getId());
+ assertEquals("Other Text", read(blob));
+ }
+
+ @Test
+ void testCreateAlreadyExistingEntry()
+ {
+ assertNotNull(store.create("1"));
+ assertThrows(EntryAlreadyExistsStoreException.class, () -> store.create("1"));
+ }
+
+ @Test
+ void testCreateWithId()
+ {
+ Blob blob = store.create("1");
+
+ assertNotNull(blob);
+
+ blob = store.get("1");
+ assertNotNull(blob);
+ }
+
+ @Test
+ void testCreateWithoutId()
+ {
+ Blob blob = store.create();
+
+ assertNotNull(blob);
+
+ String id = blob.getId();
+
+ assertNotNull(id);
+
+ blob = store.get(id);
+ assertNotNull(blob);
+ }
+
+ @Test
+ void testGet()
+ {
+ Blob blob = store.get("1");
+
+ assertNull(blob);
+
+ blob = store.create("1");
+ assertNotNull(blob);
+
+ blob = store.get("1");
+ assertNotNull(blob);
+ }
+
+ @Test
+ void testGetAll()
+ {
+ store.create("1");
+ store.create("2");
+ store.create("3");
+
+ List all = store.getAll();
+
+ assertNotNull(all);
+ assertFalse(all.isEmpty());
+ assertEquals(3, all.size());
+
+ boolean c1 = false;
+ boolean c2 = false;
+ boolean c3 = false;
+
+ for (Blob b : all)
+ {
+ if ("1".equals(b.getId()))
+ {
+ c1 = true;
+ }
+ else if ("2".equals(b.getId()))
+ {
+ c2 = true;
+ }
+ else if ("3".equals(b.getId()))
+ {
+ c3 = true;
+ }
+ }
+
+ assertTrue(c1);
+ assertTrue(c2);
+ assertTrue(c3);
+ }
+
+ @Nested
+ class WithArchivedRepository {
+
+ @BeforeEach
+ void setRepositoryArchived() {
+ store.create("1"); // store for test must not be empty
+ when(archivedCheck.isArchived(repository.getId())).thenReturn(true);
+ createBlobStore();
+ }
+
+ @Test
+ void shouldNotClear() {
+ assertThrows(StoreReadOnlyException.class, () -> store.clear());
+ }
+
+ @Test
+ void shouldNotRemove() {
+ assertThrows(StoreReadOnlyException.class, () -> store.remove("1"));
+ }
+ }
+
+ private String read(Blob blob) throws IOException
+ {
+ InputStream input = blob.getInputStream();
+ byte[] bytes = ByteStreams.toByteArray(input);
+
+ input.close();
+
+ return new String(bytes);
+ }
+
+ private void write(Blob blob, String content) throws IOException
+ {
+ OutputStream output = blob.getOutputStream();
+
+ output.write(content.getBytes());
+ output.close();
+ blob.commit();
+ }
+
+ protected BlobStoreFactory createBlobStoreFactory()
+ {
+ return new FileBlobStoreFactory(contextProvider, repositoryLocationResolver, new UUIDKeyGenerator(), archivedCheck);
}
}
diff --git a/scm-dao-xml/src/test/java/sonia/scm/store/JAXBConfigurationEntryStoreTest.java b/scm-dao-xml/src/test/java/sonia/scm/store/JAXBConfigurationEntryStoreTest.java
index 5a9e419486..7347876a55 100644
--- a/scm-dao-xml/src/test/java/sonia/scm/store/JAXBConfigurationEntryStoreTest.java
+++ b/scm-dao-xml/src/test/java/sonia/scm/store/JAXBConfigurationEntryStoreTest.java
@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
-
+
package sonia.scm.store;
//~--- non-JDK imports --------------------------------------------------------
@@ -85,7 +85,7 @@ public class JAXBConfigurationEntryStoreTest
assertEquals("tuser3", a3.getName());
}
-
+
/**
* Method description
*
@@ -154,7 +154,7 @@ public class JAXBConfigurationEntryStoreTest
@Override
protected ConfigurationEntryStoreFactory createConfigurationStoreFactory()
{
- return new JAXBConfigurationEntryStoreFactory(contextProvider, repositoryLocationResolver, new UUIDKeyGenerator());
+ return new JAXBConfigurationEntryStoreFactory(contextProvider, repositoryLocationResolver, new UUIDKeyGenerator(), null);
}
/**
diff --git a/scm-dao-xml/src/test/java/sonia/scm/store/JAXBConfigurationStoreTest.java b/scm-dao-xml/src/test/java/sonia/scm/store/JAXBConfigurationStoreTest.java
index 29c52a835e..5e130d76ad 100644
--- a/scm-dao-xml/src/test/java/sonia/scm/store/JAXBConfigurationStoreTest.java
+++ b/scm-dao-xml/src/test/java/sonia/scm/store/JAXBConfigurationStoreTest.java
@@ -21,14 +21,18 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
-
+
package sonia.scm.store;
import org.junit.Test;
import sonia.scm.repository.Repository;
+import sonia.scm.repository.RepositoryArchivedCheck;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
/**
* Unit tests for {@link JAXBConfigurationStore}.
@@ -37,10 +41,12 @@ import static org.junit.Assert.assertNotNull;
*/
public class JAXBConfigurationStoreTest extends StoreTestBase {
+ private final RepositoryArchivedCheck archivedCheck = mock(RepositoryArchivedCheck.class);
+
@Override
protected ConfigurationStoreFactory createStoreFactory()
{
- return new JAXBConfigurationStoreFactory(contextProvider, repositoryLocationResolver);
+ return new JAXBConfigurationStoreFactory(contextProvider, repositoryLocationResolver, archivedCheck);
}
@@ -48,10 +54,11 @@ public class JAXBConfigurationStoreTest extends StoreTestBase {
@SuppressWarnings("unchecked")
public void shouldStoreAndLoadInRepository()
{
+ Repository repository = new Repository("id", "git", "ns", "n");
ConfigurationStore store = createStoreFactory()
.withType(StoreObject.class)
.withName("test")
- .forRepository(new Repository("id", "git", "ns", "n"))
+ .forRepository(repository)
.build();
store.set(new StoreObject("value"));
@@ -60,4 +67,20 @@ public class JAXBConfigurationStoreTest extends StoreTestBase {
assertNotNull(storeObject);
assertEquals("value", storeObject.getValue());
}
+
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void shouldNotWriteArchivedRepository()
+ {
+ Repository repository = new Repository("id", "git", "ns", "n");
+ when(archivedCheck.isArchived("id")).thenReturn(true);
+ ConfigurationStore store = createStoreFactory()
+ .withType(StoreObject.class)
+ .withName("test")
+ .forRepository(repository)
+ .build();
+
+ assertThrows(RuntimeException.class, () -> store.set(new StoreObject("value")));
+ }
}
diff --git a/scm-dao-xml/src/test/java/sonia/scm/store/JAXBDataStoreTest.java b/scm-dao-xml/src/test/java/sonia/scm/store/JAXBDataStoreTest.java
index 13b9ad29b8..c32542a02f 100644
--- a/scm-dao-xml/src/test/java/sonia/scm/store/JAXBDataStoreTest.java
+++ b/scm-dao-xml/src/test/java/sonia/scm/store/JAXBDataStoreTest.java
@@ -28,10 +28,13 @@ package sonia.scm.store;
import org.junit.Test;
import sonia.scm.repository.Repository;
+import sonia.scm.repository.RepositoryArchivedCheck;
import sonia.scm.security.UUIDKeyGenerator;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
/**
*
@@ -39,16 +42,12 @@ import static org.junit.Assert.assertNotNull;
*/
public class JAXBDataStoreTest extends DataStoreTestBase {
- /**
- * Method description
- *
- *
- * @return
- */
+ private final RepositoryArchivedCheck archivedCheck = mock(RepositoryArchivedCheck.class);
+
@Override
protected DataStoreFactory createDataStoreFactory()
{
- return new JAXBDataStoreFactory(contextProvider, repositoryLocationResolver, new UUIDKeyGenerator());
+ return new JAXBDataStoreFactory(contextProvider, repositoryLocationResolver, new UUIDKeyGenerator(), archivedCheck);
}
@Override
@@ -77,4 +76,11 @@ public class JAXBDataStoreTest extends DataStoreTestBase {
assertNotNull(storeObject);
assertEquals("abc_value", storeObject.getValue());
}
+
+ @Test(expected = StoreReadOnlyException.class)
+ public void shouldNotStoreForReadOnlyRepository()
+ {
+ when(archivedCheck.isArchived(repository.getId())).thenReturn(true);
+ getDataStore(StoreObject.class, repository).put("abc", new StoreObject("abc_value"));
+ }
}
diff --git a/scm-dao-xml/src/test/java/sonia/scm/update/xml/XmlV1PropertyDAOTest.java b/scm-dao-xml/src/test/java/sonia/scm/update/xml/XmlV1PropertyDAOTest.java
index 80509851ba..68cfe7ada3 100644
--- a/scm-dao-xml/src/test/java/sonia/scm/update/xml/XmlV1PropertyDAOTest.java
+++ b/scm-dao-xml/src/test/java/sonia/scm/update/xml/XmlV1PropertyDAOTest.java
@@ -29,6 +29,7 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import sonia.scm.SCMContextProvider;
import sonia.scm.Stage;
+import sonia.scm.repository.RepositoryArchivedCheck;
import sonia.scm.security.KeyGenerator;
import sonia.scm.store.JAXBConfigurationEntryStoreFactory;
import sonia.scm.update.RepositoryV1PropertyReader;
@@ -38,6 +39,8 @@ import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
+import static org.mockito.Mockito.mock;
+
class XmlV1PropertyDAOTest {
@@ -108,7 +111,8 @@ class XmlV1PropertyDAOTest {
Files.createDirectories(configPath);
Path propFile = configPath.resolve("repository-properties-v1.xml");
Files.write(propFile, PROPERTIES.getBytes());
- XmlV1PropertyDAO dao = new XmlV1PropertyDAO(new JAXBConfigurationEntryStoreFactory(new SimpleContextProvider(temp), null, new SimpleKeyGenerator()));
+ RepositoryArchivedCheck archivedCheck = mock(RepositoryArchivedCheck.class);
+ XmlV1PropertyDAO dao = new XmlV1PropertyDAO(new JAXBConfigurationEntryStoreFactory(new SimpleContextProvider(temp), null, new SimpleKeyGenerator(), archivedCheck));
dao.getProperties(new RepositoryV1PropertyReader())
.forEachEntry((key, prop) -> {
diff --git a/scm-test/pom.xml b/scm-test/pom.xml
index a3c5e38cde..b51c0f05e8 100644
--- a/scm-test/pom.xml
+++ b/scm-test/pom.xml
@@ -59,6 +59,12 @@
compile
+
+ org.junit.jupiter
+ junit-jupiter-api
+ compile
+
+
com.github.sdorrashiro-unit
diff --git a/scm-test/src/main/java/sonia/scm/AbstractTestBase.java b/scm-test/src/main/java/sonia/scm/AbstractTestBase.java
index 45e931036d..535b84bfc2 100644
--- a/scm-test/src/main/java/sonia/scm/AbstractTestBase.java
+++ b/scm-test/src/main/java/sonia/scm/AbstractTestBase.java
@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
-
+
package sonia.scm;
//~--- non-JDK imports --------------------------------------------------------
@@ -32,11 +32,12 @@ import org.apache.shiro.subject.Subject;
import org.apache.shiro.subject.support.SubjectThreadState;
import org.apache.shiro.util.LifecycleUtils;
import org.apache.shiro.util.ThreadState;
-
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
-
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
import sonia.scm.io.DefaultFileSystem;
import sonia.scm.repository.InitialRepositoryLocationResolver;
import sonia.scm.repository.RepositoryDAO;
@@ -44,17 +45,14 @@ import sonia.scm.repository.RepositoryLocationResolver;
import sonia.scm.util.IOUtil;
import sonia.scm.util.MockUtil;
-import static org.junit.Assert.*;
-import static org.mockito.Mockito.mock;
-
-//~--- JDK imports ------------------------------------------------------------
-
import java.io.File;
-
import java.io.IOException;
import java.util.UUID;
import java.util.logging.Logger;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+
/**
*
* @author Sebastian Sdorra
@@ -73,6 +71,7 @@ public class AbstractTestBase
protected RepositoryDAO repositoryDAO = mock(RepositoryDAO.class);
protected RepositoryLocationResolver repositoryLocationResolver;
+ @BeforeEach
@Before
public void setUpTest() throws Exception
{
@@ -90,6 +89,7 @@ public class AbstractTestBase
* Method description
*
*/
+ @AfterAll
@AfterClass
public static void tearDownShiro()
{
@@ -162,6 +162,7 @@ public class AbstractTestBase
*
* @throws Exception
*/
+ @AfterEach
@After
public void tearDownTest() throws Exception
{
diff --git a/scm-test/src/main/java/sonia/scm/store/BlobStoreTestBase.java b/scm-test/src/main/java/sonia/scm/store/BlobStoreTestBase.java
deleted file mode 100644
index 48da04018c..0000000000
--- a/scm-test/src/main/java/sonia/scm/store/BlobStoreTestBase.java
+++ /dev/null
@@ -1,264 +0,0 @@
-/*
- * 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.store;
-
-//~--- non-JDK imports --------------------------------------------------------
-
-import com.google.common.io.ByteStreams;
-import org.junit.Before;
-import org.junit.Test;
-import sonia.scm.AbstractTestBase;
-import sonia.scm.repository.RepositoryTestData;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.util.List;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-
-//~--- JDK imports ------------------------------------------------------------
-
-/**
- *
- * @author Sebastian Sdorra
- */
-public abstract class BlobStoreTestBase extends AbstractTestBase
-{
-
- protected abstract BlobStoreFactory createBlobStoreFactory();
-
- /**
- * Method description
- *
- */
- @Before
- public void createBlobStore()
- {
- store = createBlobStoreFactory()
- .withName("test")
- .forRepository(RepositoryTestData.createHeartOfGold())
- .build();
- store.clear();
- }
-
- /**
- * Method description
- *
- */
- @Test
- public void testClear()
- {
- store.create("1");
- store.create("2");
- store.create("3");
-
- assertNotNull(store.get("1"));
- assertNotNull(store.get("2"));
- assertNotNull(store.get("3"));
-
- store.clear();
-
- assertNull(store.get("1"));
- assertNull(store.get("2"));
- assertNull(store.get("3"));
- }
-
- /**
- * Method description
- *
- *
- * @throws IOException
- */
- @Test
- public void testContent() throws IOException
- {
- Blob blob = store.create();
-
- write(blob, "Hello");
- assertEquals("Hello", read(blob));
-
- blob = store.get(blob.getId());
- assertEquals("Hello", read(blob));
-
- write(blob, "Other Text");
- assertEquals("Other Text", read(blob));
-
- blob = store.get(blob.getId());
- assertEquals("Other Text", read(blob));
- }
-
- /**
- * Method description
- *
- */
- @Test(expected = EntryAlreadyExistsStoreException.class)
- public void testCreateAlreadyExistingEntry()
- {
- assertNotNull(store.create("1"));
- store.create("1");
- }
-
- /**
- * Method description
- *
- */
- @Test
- public void testCreateWithId()
- {
- Blob blob = store.create("1");
-
- assertNotNull(blob);
-
- blob = store.get("1");
- assertNotNull(blob);
- }
-
- /**
- * Method description
- *
- */
- @Test
- public void testCreateWithoutId()
- {
- Blob blob = store.create();
-
- assertNotNull(blob);
-
- String id = blob.getId();
-
- assertNotNull(id);
-
- blob = store.get(id);
- assertNotNull(blob);
- }
-
- /**
- * Method description
- *
- */
- @Test
- public void testGet()
- {
- Blob blob = store.get("1");
-
- assertNull(blob);
-
- blob = store.create("1");
- assertNotNull(blob);
-
- blob = store.get("1");
- assertNotNull(blob);
- }
-
- /**
- * Method description
- *
- */
- @Test
- public void testGetAll()
- {
- store.create("1");
- store.create("2");
- store.create("3");
-
- List all = store.getAll();
-
- assertNotNull(all);
- assertFalse(all.isEmpty());
- assertEquals(3, all.size());
-
- boolean c1 = false;
- boolean c2 = false;
- boolean c3 = false;
-
- for (Blob b : all)
- {
- if ("1".equals(b.getId()))
- {
- c1 = true;
- }
- else if ("2".equals(b.getId()))
- {
- c2 = true;
- }
- else if ("3".equals(b.getId()))
- {
- c3 = true;
- }
- }
-
- assertTrue(c1);
- assertTrue(c2);
- assertTrue(c3);
- }
-
- /**
- * Method description
- *
- *
- * @param blob
- *
- * @return
- *
- * @throws IOException
- */
- private String read(Blob blob) throws IOException
- {
- InputStream input = blob.getInputStream();
- byte[] bytes = ByteStreams.toByteArray(input);
-
- input.close();
-
- return new String(bytes);
- }
-
- /**
- * Method description
- *
- *
- * @param blob
- * @param content
- *
- * @throws IOException
- */
- private void write(Blob blob, String content) throws IOException
- {
- OutputStream output = blob.getOutputStream();
-
- output.write(content.getBytes());
- output.close();
- blob.commit();
- }
-
- //~--- fields ---------------------------------------------------------------
-
- /** Field description */
- private BlobStore store;
-}
diff --git a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap
index 8c1fdd3930..8b94d259a8 100644
--- a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap
+++ b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap
@@ -50505,6 +50505,162 @@ exports[`Storyshots Popover Link 1`] = `
`;
+exports[`Storyshots RepositoryEntry Archived 1`] = `
+