mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-01 19:15:52 +01:00
Archive repository (#1477)
This adds a flag "archived" to repositories. Repositories marked with this can no longer be modified in any way. To do this, we switch to a new version of Shiro Static Permissions (sdorra/shiro-static-permissions#4) and specify a permission guard to check for every permission request, whether the repository in question is archived or not. Further we implement checks in stores and other activies so that no writing request may be executed by mistake. Co-authored-by: Eduard Heimbuch <eduard.heimbuch@cloudogu.com>
This commit is contained in:
@@ -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))
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 98 KiB |
@@ -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.
|
||||
|
||||

|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 92 KiB |
@@ -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.
|
||||
|
||||

|
||||
|
||||
|
||||
2
pom.xml
2
pom.xml
@@ -935,7 +935,7 @@
|
||||
<jetty.maven.version>9.4.34.v20201102</jetty.maven.version>
|
||||
|
||||
<!-- security libraries -->
|
||||
<ssp.version>1.2.0</ssp.version>
|
||||
<ssp.version>1.3.0</ssp.version>
|
||||
<shiro.version>1.7.0</shiro.version>
|
||||
|
||||
<!-- repository libraries -->
|
||||
|
||||
@@ -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<String> 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<String> availableVerbs();
|
||||
|
||||
/**
|
||||
* The collection of verbs that are marked as "read only". These verbs are safe for archived or otherwise read only
|
||||
* repositories.
|
||||
*/
|
||||
Collection<String> readOnlyVerbs();
|
||||
|
||||
/**
|
||||
* The collection of roles defined and extended by core and plugins.
|
||||
*/
|
||||
Collection<RepositoryRole> availableRoles();
|
||||
}
|
||||
@@ -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,13 +52,16 @@ 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")
|
||||
@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<RepositoryPermission> permissions = new HashSet<>();
|
||||
private String type;
|
||||
private boolean archived;
|
||||
|
||||
|
||||
/**
|
||||
@@ -204,6 +209,15 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
|
||||
return type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns <code>true</code>, 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 <code>true</code> 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<HealthCheckFailure> healthCheckFailures) {
|
||||
this.healthCheckFailures = healthCheckFailures;
|
||||
}
|
||||
|
||||
@@ -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 <code>true</code> when the repository with the given id is archived, <code>false</code> 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 <code>true</code> when the given repository is archived, <code>false</code> otherwise.
|
||||
*/
|
||||
default boolean isArchived(Repository repository) {
|
||||
return isArchived(repository.getId());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 ---------------------------------------------------------------
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<Repository> {
|
||||
|
||||
private static final Collection<String> READ_ONLY_VERBS = Collections.synchronizedSet(new HashSet<>());
|
||||
|
||||
static void setReadOnlyVerbs(Collection<String> readOnlyVerbs) {
|
||||
READ_ONLY_VERBS.addAll(readOnlyVerbs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PermissionActionCheckInterceptor<Repository> intercept(String permission) {
|
||||
if (READ_ONLY_VERBS.contains(permission)) {
|
||||
return new PermissionActionCheckInterceptor<Repository>() {};
|
||||
} else {
|
||||
return new WriteInterceptor();
|
||||
}
|
||||
}
|
||||
|
||||
private static class WriteInterceptor implements PermissionActionCheckInterceptor<Repository> {
|
||||
@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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -38,6 +38,11 @@ public abstract class AbstractStore<T> implements ConfigurationStore<T> {
|
||||
* 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<T> implements ConfigurationStore<T> {
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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<Repository> 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<Repository> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -60,10 +60,11 @@ public abstract class FileBasedStore<T> implements MultiEntryStore<T>
|
||||
* @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<T> implements MultiEntryStore<T>
|
||||
{
|
||||
logger.trace("delete store entry {}", file);
|
||||
|
||||
assertNotReadOnly();
|
||||
|
||||
if (file.exists() &&!file.delete())
|
||||
{
|
||||
throw new StoreException(
|
||||
@@ -185,6 +188,12 @@ public abstract class FileBasedStore<T> implements MultiEntryStore<T>
|
||||
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<T> implements MultiEntryStore<T>
|
||||
|
||||
/** Field description */
|
||||
private final String suffix;
|
||||
|
||||
private final boolean readOnly;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -58,8 +58,8 @@ public class FileBlobStore extends FileBasedStore<Blob> 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<Blob> 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<Blob> implements BlobStore {
|
||||
|
||||
@Override
|
||||
public void remove(Blob blob) {
|
||||
assertNotReadOnly();
|
||||
Preconditions.checkNotNull(blob, "blob argument is required");
|
||||
remove(blob.getId());
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +46,8 @@ 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) {
|
||||
public JAXBConfigurationStore(TypedStoreContext<T> context, Class<T> type, File configFile, boolean readOnly) {
|
||||
super(readOnly);
|
||||
this.context = context;
|
||||
this.type = type;
|
||||
this.configFile = configFile;
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,8 +52,8 @@ public class JAXBDataStore<T> extends FileBasedStore<T> implements DataStore<T>
|
||||
private final KeyGenerator keyGenerator;
|
||||
private final TypedStoreContext<T> context;
|
||||
|
||||
JAXBDataStore(KeyGenerator keyGenerator, TypedStoreContext<T> context, File directory) {
|
||||
super(directory, StoreConstants.FILE_EXTENSION);
|
||||
JAXBDataStore(KeyGenerator keyGenerator, TypedStoreContext<T> 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<T> extends FileBasedStore<T> implements DataStore<T>
|
||||
public void put(String id, T item) {
|
||||
LOG.debug("put item {} to store", id);
|
||||
|
||||
assertNotReadOnly();
|
||||
|
||||
File file = getFile(id);
|
||||
|
||||
try {
|
||||
|
||||
@@ -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 <T> DataStore<T> getStore(TypedStoreParameters<T> 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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -24,51 +24,209 @@
|
||||
|
||||
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<Blob> 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<Blob> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -26,9 +26,13 @@ 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<StoreObject> 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<StoreObject> store = createStoreFactory()
|
||||
.withType(StoreObject.class)
|
||||
.withName("test")
|
||||
.forRepository(repository)
|
||||
.build();
|
||||
|
||||
assertThrows(RuntimeException.class, () -> store.set(new StoreObject("value")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) -> {
|
||||
|
||||
@@ -59,6 +59,12 @@
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.sdorra</groupId>
|
||||
<artifactId>shiro-unit</artifactId>
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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<Blob> 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;
|
||||
}
|
||||
@@ -50505,6 +50505,162 @@ exports[`Storyshots Popover Link 1`] = `
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Storyshots RepositoryEntry Archived 1`] = `
|
||||
<div
|
||||
className="RepositoryEntrystories__Spacing-toppdg-0 iIzVNZ box box-link-shadow"
|
||||
>
|
||||
<a
|
||||
className="overlay-column"
|
||||
href="/repo/hitchhiker/heartOfGold"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<article
|
||||
className="CardColumn__NoEventWrapper-sc-1w6lsih-0 eUWboI media"
|
||||
>
|
||||
<figure
|
||||
className="CardColumn__AvatarWrapper-sc-1w6lsih-1 lhzEPm media-left"
|
||||
>
|
||||
<p
|
||||
className="image is-64x64"
|
||||
>
|
||||
<img
|
||||
alt="Logo"
|
||||
src="test-file-stub"
|
||||
/>
|
||||
</p>
|
||||
</figure>
|
||||
<div
|
||||
className="CardColumn__FlexFullHeight-sc-1w6lsih-2 hWRPir media-content text-box is-flex"
|
||||
>
|
||||
<div
|
||||
className="is-flex"
|
||||
>
|
||||
<div
|
||||
className="CardColumn__ContentLeft-sc-1w6lsih-4 iRVRBC content"
|
||||
>
|
||||
<p
|
||||
className="shorten-text is-marginless"
|
||||
>
|
||||
|
||||
<strong>
|
||||
heartOfGold
|
||||
</strong>
|
||||
|
||||
|
||||
<i
|
||||
className="fas fa-archive has-text-grey-light"
|
||||
/>
|
||||
|
||||
<span
|
||||
className="RepositoryEntry__Smaller-sc-6jys82-0 lpzPr"
|
||||
>
|
||||
repository.archived
|
||||
</span>
|
||||
</p>
|
||||
<p
|
||||
className="shorten-text"
|
||||
>
|
||||
The starship Heart of Gold was the first spacecraft to make use of the Infinite Improbability Drive
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="CardColumn__FooterWrapper-sc-1w6lsih-3 hzknmV level is-flex"
|
||||
>
|
||||
<div
|
||||
className="CardColumn__RightMarginDiv-sc-1w6lsih-6 bnJfDV level-left is-hidden-mobile"
|
||||
>
|
||||
<a
|
||||
className="RepositoryEntryLink__PointerEventsLink-sc-1hpqj0w-0 iZuqoP level-item"
|
||||
href="/repo/hitchhiker/heartOfGold/branches/"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<span
|
||||
className="tooltip has-tooltip-top"
|
||||
data-tooltip="repositoryRoot.tooltip.branches"
|
||||
>
|
||||
<i
|
||||
className="fas fa-code-branch has-text-inherit fa-lg"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
className="RepositoryEntryLink__PointerEventsLink-sc-1hpqj0w-0 iZuqoP level-item"
|
||||
href="/repo/hitchhiker/heartOfGold/tags/"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<span
|
||||
className="tooltip has-tooltip-top"
|
||||
data-tooltip="repositoryRoot.tooltip.tags"
|
||||
>
|
||||
<i
|
||||
className="fas fa-tags has-text-inherit fa-lg"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
className="RepositoryEntryLink__PointerEventsLink-sc-1hpqj0w-0 iZuqoP level-item"
|
||||
href="/repo/hitchhiker/heartOfGold/code/changesets/"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<span
|
||||
className="tooltip has-tooltip-top"
|
||||
data-tooltip="repositoryRoot.tooltip.commits"
|
||||
>
|
||||
<i
|
||||
className="fas fa-exchange-alt has-text-inherit fa-lg"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
className="RepositoryEntryLink__PointerEventsLink-sc-1hpqj0w-0 iZuqoP level-item"
|
||||
href="/repo/hitchhiker/heartOfGold/code/sources/"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<span
|
||||
className="tooltip has-tooltip-top"
|
||||
data-tooltip="repositoryRoot.tooltip.sources"
|
||||
>
|
||||
<i
|
||||
className="fas fa-code has-text-inherit fa-lg"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
className="RepositoryEntryLink__PointerEventsLink-sc-1hpqj0w-0 iZuqoP level-item"
|
||||
href="/repo/hitchhiker/heartOfGold/settings/general"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<span
|
||||
className="tooltip has-tooltip-top"
|
||||
data-tooltip="repositoryRoot.tooltip.settings"
|
||||
>
|
||||
<i
|
||||
className="fas fa-cog has-text-inherit fa-lg"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
className="CardColumn__InheritFlexShrinkDiv-sc-1w6lsih-7 kdhCxo level-right is-block is-mobile is-marginless shorten-text"
|
||||
>
|
||||
<small
|
||||
className="level-item"
|
||||
>
|
||||
<time
|
||||
className="DateElement-sc-1schp8c-0 IMGpa"
|
||||
title="2020-03-23 09:26:01"
|
||||
>
|
||||
3 days ago
|
||||
</time>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Storyshots RepositoryEntry Avatar EP 1`] = `
|
||||
<div
|
||||
className="RepositoryEntrystories__Spacing-toppdg-0 iIzVNZ box box-link-shadow"
|
||||
@@ -50545,6 +50701,7 @@ exports[`Storyshots RepositoryEntry Avatar EP 1`] = `
|
||||
<strong>
|
||||
heartOfGold
|
||||
</strong>
|
||||
|
||||
</p>
|
||||
<p
|
||||
className="shorten-text"
|
||||
@@ -50693,6 +50850,7 @@ exports[`Storyshots RepositoryEntry Before Title EP 1`] = `
|
||||
<strong>
|
||||
heartOfGold
|
||||
</strong>
|
||||
|
||||
</p>
|
||||
<p
|
||||
className="shorten-text"
|
||||
@@ -50838,6 +50996,7 @@ exports[`Storyshots RepositoryEntry Default 1`] = `
|
||||
<strong>
|
||||
heartOfGold
|
||||
</strong>
|
||||
|
||||
</p>
|
||||
<p
|
||||
className="shorten-text"
|
||||
@@ -50983,6 +51142,7 @@ exports[`Storyshots RepositoryEntry Quick Link EP 1`] = `
|
||||
<strong>
|
||||
heartOfGold
|
||||
</strong>
|
||||
|
||||
</p>
|
||||
<p
|
||||
className="shorten-text"
|
||||
|
||||
@@ -74,9 +74,11 @@ const QuickLink = (
|
||||
</a>
|
||||
);
|
||||
|
||||
const archivedRepository = { ...repository, archived: true };
|
||||
|
||||
storiesOf("RepositoryEntry", module)
|
||||
.addDecorator(story => <MemoryRouter initialEntries={["/"]}>{story()}</MemoryRouter>)
|
||||
.addDecorator(storyFn => <Container>{storyFn()}</Container>)
|
||||
.addDecorator((story) => <MemoryRouter initialEntries={["/"]}>{story()}</MemoryRouter>)
|
||||
.addDecorator((storyFn) => <Container>{storyFn()}</Container>)
|
||||
.add("Default", () => {
|
||||
return <RepositoryEntry repository={repository} baseDate={baseDate} />;
|
||||
})
|
||||
@@ -94,4 +96,9 @@ storiesOf("RepositoryEntry", module)
|
||||
const binder = new Binder("title");
|
||||
bindQuickLink(binder, QuickLink);
|
||||
return withBinder(binder, repository);
|
||||
})
|
||||
.add("Archived", () => {
|
||||
const binder = new Binder("title");
|
||||
bindAvatar(binder, Git);
|
||||
return withBinder(binder, archivedRepository);
|
||||
});
|
||||
|
||||
@@ -28,6 +28,7 @@ import RepositoryEntryLink from "./RepositoryEntryLink";
|
||||
import RepositoryAvatar from "./RepositoryAvatar";
|
||||
import { ExtensionPoint } from "@scm-manager/ui-extensions";
|
||||
import { withTranslation, WithTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
|
||||
type DateProp = Date | string;
|
||||
|
||||
@@ -38,6 +39,18 @@ type Props = WithTranslation & {
|
||||
baseDate?: DateProp;
|
||||
};
|
||||
|
||||
const ArchiveTag = styled.span`
|
||||
margin-left: 0.2rem;
|
||||
background-color: #9a9a9a;
|
||||
padding: 0.25rem;
|
||||
border-radius: 5px;
|
||||
color: white;
|
||||
overflow: visible;
|
||||
pointer-events: all;
|
||||
font-weight: bold;
|
||||
font-size: 0.7rem;
|
||||
`;
|
||||
|
||||
class RepositoryEntry extends React.Component<Props> {
|
||||
createLink = (repository: Repository) => {
|
||||
return `/repo/${repository.namespace}/${repository.name}`;
|
||||
@@ -131,10 +144,14 @@ class RepositoryEntry extends React.Component<Props> {
|
||||
};
|
||||
|
||||
createTitle = () => {
|
||||
const { repository } = this.props;
|
||||
const { repository, t } = this.props;
|
||||
const archivedFlag = repository.archived && (
|
||||
<ArchiveTag title={t("archive.tooltip")}>{t("repository.archived")}</ArchiveTag>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<ExtensionPoint name="repository.card.beforeTitle" props={{ repository }} /> <strong>{repository.name}</strong>
|
||||
<ExtensionPoint name="repository.card.beforeTitle" props={{ repository }} />
|
||||
<strong>{repository.name}</strong> {archivedFlag}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -32,6 +32,7 @@ export type Repository = {
|
||||
description?: string;
|
||||
creationDate?: string;
|
||||
lastModified?: string;
|
||||
archived?: boolean;
|
||||
_links: Links;
|
||||
};
|
||||
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"contact": "Kontakt",
|
||||
"description": "Beschreibung",
|
||||
"creationDate": "Erstellt",
|
||||
"lastModified": "Zuletzt bearbeitet"
|
||||
"lastModified": "Zuletzt bearbeitet",
|
||||
"archived": "archiviert"
|
||||
},
|
||||
"validation": {
|
||||
"namespace-invalid": "Der Namespace des Repository ist ungültig",
|
||||
@@ -237,7 +238,7 @@
|
||||
"submitCreate": "Speichern",
|
||||
"submitImport": "Importieren",
|
||||
"initializeRepository": "Repository initiieren",
|
||||
"dangerZone": "Umbenennen und Löschen",
|
||||
"dangerZone": "Umbenennen, Archivieren und Löschen",
|
||||
"createButton": "Neues Repository erstellen",
|
||||
"importButton": "Repository importieren"
|
||||
},
|
||||
@@ -351,6 +352,31 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"archiveRepo": {
|
||||
"button": "Repository archivieren",
|
||||
"subtitle": "Repository archivieren",
|
||||
"description": "Archivierte Repositories können nicht mehr verändert werden.",
|
||||
"confirmAlert": {
|
||||
"title": "Repository archivieren",
|
||||
"message": "Soll das Repository wirklich archiviert werden?",
|
||||
"submit": "Ja",
|
||||
"cancel": "Nein"
|
||||
}
|
||||
},
|
||||
"unarchiveRepo": {
|
||||
"button": "Archivierung zurücknehmen",
|
||||
"subtitle": "Archivierung zurücknehmen",
|
||||
"description": "Wenn das Repository nicht mehr archiviert ist, kann es wieder verändert werden.",
|
||||
"confirmAlert": {
|
||||
"title": "Archivierung zurücknehmen",
|
||||
"message": "Soll die Archivierung des Repositorys wirklich rückgängig gemacht werden?",
|
||||
"submit": "Ja",
|
||||
"cancel": "Nein"
|
||||
}
|
||||
},
|
||||
"archive": {
|
||||
"tooltip": "Nur lesender Zugriff möglich. Das Archiv kann nicht verändert werden."
|
||||
},
|
||||
"diff": {
|
||||
"jumpToSource": "Zur Quelldatei springen",
|
||||
"jumpToTarget": "Zur vorherigen Version der Datei springen",
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"contact": "Contact",
|
||||
"description": "Description",
|
||||
"creationDate": "Creation Date",
|
||||
"lastModified": "Last Modified"
|
||||
"lastModified": "Last Modified",
|
||||
"archived": "archived"
|
||||
},
|
||||
"validation": {
|
||||
"namespace-invalid": "The repository namespace is invalid",
|
||||
@@ -238,7 +239,7 @@
|
||||
"submitCreate": "Save",
|
||||
"submitImport": "Import",
|
||||
"initializeRepository": "Initialize repository",
|
||||
"dangerZone": "Rename and delete",
|
||||
"dangerZone": "Rename, archive and delete",
|
||||
"createButton": "Create Repository",
|
||||
"importButton": "Import repository"
|
||||
},
|
||||
@@ -352,6 +353,31 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"archiveRepo": {
|
||||
"button": "Archive Repository",
|
||||
"subtitle": "Archive this repository",
|
||||
"description": "An archived repository can no longer be modified.",
|
||||
"confirmAlert": {
|
||||
"title": "Archive Repository",
|
||||
"message": "Shall the repository really be archived?",
|
||||
"submit": "Yes",
|
||||
"cancel": "No"
|
||||
}
|
||||
},
|
||||
"unarchiveRepo": {
|
||||
"button": "Remove Archive Mark",
|
||||
"subtitle": "Remove Archive Mark",
|
||||
"description": "When the archive mark is removed, this repository can be modified again.",
|
||||
"confirmAlert": {
|
||||
"title": "Remove Archive Mark",
|
||||
"message": "Shall the archive mark really be removed?",
|
||||
"submit": "Yes",
|
||||
"cancel": "No"
|
||||
}
|
||||
},
|
||||
"archive": {
|
||||
"tooltip": "Read only. The archive cannot be changed."
|
||||
},
|
||||
"diff": {
|
||||
"changes": {
|
||||
"add": "added",
|
||||
|
||||
123
scm-ui/ui-webapp/src/repos/containers/ArchiveRepo.tsx
Normal file
123
scm-ui/ui-webapp/src/repos/containers/ArchiveRepo.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
import React, { FC, useState } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Repository } from "@scm-manager/ui-types";
|
||||
import { Button, ConfirmAlert, ErrorNotification, Level } from "@scm-manager/ui-components";
|
||||
import { archiveRepo, getModifyRepoFailure, isModifyRepoPending } from "../modules/repos";
|
||||
|
||||
type Props = {
|
||||
loading: boolean;
|
||||
error: Error;
|
||||
repository: Repository;
|
||||
confirmDialog?: boolean;
|
||||
archiveRepo: (p1: Repository, p2: () => void) => void;
|
||||
};
|
||||
|
||||
const ArchiveRepo: FC<Props> = ({ confirmDialog = true, repository, archiveRepo, loading, error }: Props) => {
|
||||
const [showConfirmAlert, setShowConfirmAlert] = useState(false);
|
||||
const [t] = useTranslation("repos");
|
||||
|
||||
const archived = () => {
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
const archiveRepoCallback = () => {
|
||||
archiveRepo(repository, archived);
|
||||
};
|
||||
|
||||
const confirmArchive = () => {
|
||||
setShowConfirmAlert(true);
|
||||
};
|
||||
|
||||
const isArchiveable = () => {
|
||||
return repository._links.archive;
|
||||
};
|
||||
|
||||
const action = confirmDialog ? confirmArchive : archiveRepoCallback;
|
||||
|
||||
if (!isArchiveable()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (showConfirmAlert) {
|
||||
return (
|
||||
<ConfirmAlert
|
||||
title={t("archiveRepo.confirmAlert.title")}
|
||||
message={t("archiveRepo.confirmAlert.message")}
|
||||
buttons={[
|
||||
{
|
||||
className: "is-outlined",
|
||||
label: t("archiveRepo.confirmAlert.submit"),
|
||||
onClick: () => archiveRepoCallback(),
|
||||
},
|
||||
{
|
||||
label: t("archiveRepo.confirmAlert.cancel"),
|
||||
onClick: () => null,
|
||||
},
|
||||
]}
|
||||
close={() => setShowConfirmAlert(false)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ErrorNotification error={error} />
|
||||
<Level
|
||||
left={
|
||||
<p>
|
||||
<strong>{t("archiveRepo.subtitle")}</strong>
|
||||
<br />
|
||||
{t("archiveRepo.description")}
|
||||
</p>
|
||||
}
|
||||
right={
|
||||
<Button color="warning" icon="archive" label={t("archiveRepo.button")} action={action} loading={loading} />
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: any, ownProps: Props) => {
|
||||
const { namespace, name } = ownProps.repository;
|
||||
const loading = isModifyRepoPending(state, namespace, name);
|
||||
const error = getModifyRepoFailure(state, namespace, name);
|
||||
return {
|
||||
loading,
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch: any) => {
|
||||
return {
|
||||
archiveRepo: (repo: Repository, callback: () => void) => {
|
||||
dispatch(archiveRepo(repo, callback));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ArchiveRepo);
|
||||
@@ -29,6 +29,8 @@ import DeleteRepo from "./DeleteRepo";
|
||||
import styled from "styled-components";
|
||||
import { Subtitle } from "@scm-manager/ui-components";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import ArchiveRepo from "./ArchiveRepo";
|
||||
import UnarchiveRepo from "./UnarchiveRepo";
|
||||
|
||||
type Props = {
|
||||
repository: Repository;
|
||||
@@ -67,6 +69,14 @@ const RepositoryDangerZone: FC<Props> = ({ repository, indexLinks }) => {
|
||||
// @ts-ignore
|
||||
dangerZone.push(<DeleteRepo repository={repository} />);
|
||||
}
|
||||
if (repository?._links?.archive) {
|
||||
// @ts-ignore
|
||||
dangerZone.push(<ArchiveRepo repository={repository} />);
|
||||
}
|
||||
if (repository?._links?.unarchive) {
|
||||
// @ts-ignore
|
||||
dangerZone.push(<UnarchiveRepo repository={repository} />);
|
||||
}
|
||||
|
||||
if (dangerZone.length === 0) {
|
||||
return null;
|
||||
|
||||
@@ -30,6 +30,8 @@ import { Changeset, Repository } from "@scm-manager/ui-types";
|
||||
import {
|
||||
CustomQueryFlexWrappedColumns,
|
||||
ErrorPage,
|
||||
FileControlFactory,
|
||||
JumpToFileButton,
|
||||
Loading,
|
||||
NavLink,
|
||||
Page,
|
||||
@@ -37,7 +39,9 @@ import {
|
||||
SecondaryNavigation,
|
||||
SecondaryNavigationColumn,
|
||||
StateMenuContextProvider,
|
||||
SubNavigation
|
||||
SubNavigation,
|
||||
Tooltip,
|
||||
urls,
|
||||
} from "@scm-manager/ui-components";
|
||||
import { fetchRepoByName, getFetchRepoFailure, getRepository, isFetchRepoPending } from "../modules/repos";
|
||||
import RepositoryDetails from "../components/RepositoryDetails";
|
||||
@@ -53,10 +57,9 @@ import { getLinks, getRepositoriesLink } from "../../modules/indexResource";
|
||||
import CodeOverview from "../codeSection/containers/CodeOverview";
|
||||
import ChangesetView from "./ChangesetView";
|
||||
import SourceExtensions from "../sources/containers/SourceExtensions";
|
||||
import { FileControlFactory, JumpToFileButton } from "@scm-manager/ui-components";
|
||||
import TagsOverview from "../tags/container/TagsOverview";
|
||||
import TagRoot from "../tags/container/TagRoot";
|
||||
import { urls } from "@scm-manager/ui-components";
|
||||
import styled from "styled-components";
|
||||
|
||||
type Props = RouteComponentProps &
|
||||
WithTranslation & {
|
||||
@@ -72,6 +75,15 @@ type Props = RouteComponentProps &
|
||||
fetchRepoByName: (link: string, namespace: string, name: string) => void;
|
||||
};
|
||||
|
||||
const ArchiveTag = styled.span`
|
||||
margin-left: 0.2rem;
|
||||
background-color: #9a9a9a;
|
||||
padding: 0.4rem;
|
||||
border-radius: 5px;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
`;
|
||||
|
||||
class RepositoryRoot extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
const { fetchRepoByName, namespace, name, repoLink } = this.props;
|
||||
@@ -141,7 +153,7 @@ class RepositoryRoot extends React.Component<Props> {
|
||||
const extensionProps = {
|
||||
repository,
|
||||
url,
|
||||
indexLinks
|
||||
indexLinks,
|
||||
};
|
||||
|
||||
const redirectUrlFactory = binder.getExtension("repository.redirect", this.props);
|
||||
@@ -152,15 +164,16 @@ class RepositoryRoot extends React.Component<Props> {
|
||||
redirectedUrl = url + "/info";
|
||||
}
|
||||
|
||||
const fileControlFactoryFactory: (changeset: Changeset) => FileControlFactory = changeset => file => {
|
||||
const fileControlFactoryFactory: (changeset: Changeset) => FileControlFactory = (changeset) => (file) => {
|
||||
const baseUrl = `${url}/code/sources`;
|
||||
const sourceLink = file.newPath && {
|
||||
url: `${baseUrl}/${changeset.id}/${file.newPath}/`,
|
||||
label: t("diff.jumpToSource")
|
||||
label: t("diff.jumpToSource"),
|
||||
};
|
||||
const targetLink = file.oldPath && changeset._embedded?.parents?.length === 1 && {
|
||||
const targetLink = file.oldPath &&
|
||||
changeset._embedded?.parents?.length === 1 && {
|
||||
url: `${baseUrl}/${changeset._embedded.parents[0].id}/${file.oldPath}`,
|
||||
label: t("diff.jumpToTarget")
|
||||
label: t("diff.jumpToTarget"),
|
||||
};
|
||||
|
||||
const links = [];
|
||||
@@ -186,6 +199,12 @@ class RepositoryRoot extends React.Component<Props> {
|
||||
return links ? links.map(({ url, label }) => <JumpToFileButton tooltip={label} link={url} />) : null;
|
||||
};
|
||||
|
||||
const archivedFlag = repository.archived && (
|
||||
<Tooltip message={t("archive.tooltip")}>
|
||||
<ArchiveTag className="is-size-6">{t("repository.archived")}</ArchiveTag>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
const titleComponent = (
|
||||
<>
|
||||
<Link to={`/repos/${repository.namespace}/`} className={"has-text-dark"}>
|
||||
@@ -200,7 +219,12 @@ class RepositoryRoot extends React.Component<Props> {
|
||||
<Page
|
||||
title={titleComponent}
|
||||
documentTitle={`${repository.namespace}/${repository.name}`}
|
||||
afterTitle={<ExtensionPoint name={"repository.afterTitle"} props={{ repository }} />}
|
||||
afterTitle={
|
||||
<>
|
||||
<ExtensionPoint name={"repository.afterTitle"} props={{ repository }} />
|
||||
{archivedFlag}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<CustomQueryFlexWrappedColumns>
|
||||
<PrimaryContentColumn>
|
||||
@@ -336,7 +360,7 @@ const mapStateToProps = (state: any, ownProps: Props) => {
|
||||
loading,
|
||||
error,
|
||||
repoLink,
|
||||
indexLinks
|
||||
indexLinks,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -344,7 +368,7 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
return {
|
||||
fetchRepoByName: (link: string, namespace: string, name: string) => {
|
||||
dispatch(fetchRepoByName(link, namespace, name));
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
123
scm-ui/ui-webapp/src/repos/containers/UnarchiveRepo.tsx
Normal file
123
scm-ui/ui-webapp/src/repos/containers/UnarchiveRepo.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
import React, { FC, useState } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Repository } from "@scm-manager/ui-types";
|
||||
import { Button, ConfirmAlert, ErrorNotification, Level } from "@scm-manager/ui-components";
|
||||
import { getModifyRepoFailure, isModifyRepoPending, unarchiveRepo } from "../modules/repos";
|
||||
|
||||
type Props = {
|
||||
loading: boolean;
|
||||
error: Error;
|
||||
repository: Repository;
|
||||
confirmDialog?: boolean;
|
||||
unarchiveRepo: (p1: Repository, p2: () => void) => void;
|
||||
};
|
||||
|
||||
const UnarchiveRepo: FC<Props> = ({ confirmDialog = true, repository, unarchiveRepo, loading, error }: Props) => {
|
||||
const [showConfirmAlert, setShowConfirmAlert] = useState(false);
|
||||
const [t] = useTranslation("repos");
|
||||
|
||||
const unarchived = () => {
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
const unarchiveRepoCallback = () => {
|
||||
unarchiveRepo(repository, unarchived);
|
||||
};
|
||||
|
||||
const confirmUnarchive = () => {
|
||||
setShowConfirmAlert(true);
|
||||
};
|
||||
|
||||
const isUnarchiveable = () => {
|
||||
return repository._links.unarchive;
|
||||
};
|
||||
|
||||
const action = confirmDialog ? confirmUnarchive : unarchiveRepoCallback;
|
||||
|
||||
if (!isUnarchiveable()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (showConfirmAlert) {
|
||||
return (
|
||||
<ConfirmAlert
|
||||
title={t("unarchiveRepo.confirmAlert.title")}
|
||||
message={t("unarchiveRepo.confirmAlert.message")}
|
||||
buttons={[
|
||||
{
|
||||
className: "is-outlined",
|
||||
label: t("unarchiveRepo.confirmAlert.submit"),
|
||||
onClick: () => unarchiveRepoCallback(),
|
||||
},
|
||||
{
|
||||
label: t("unarchiveRepo.confirmAlert.cancel"),
|
||||
onClick: () => null,
|
||||
},
|
||||
]}
|
||||
close={() => setShowConfirmAlert(false)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ErrorNotification error={error} />
|
||||
<Level
|
||||
left={
|
||||
<p>
|
||||
<strong>{t("unarchiveRepo.subtitle")}</strong>
|
||||
<br />
|
||||
{t("unarchiveRepo.description")}
|
||||
</p>
|
||||
}
|
||||
right={
|
||||
<Button color="warning" icon="box-open" label={t("unarchiveRepo.button")} action={action} loading={loading} />
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: any, ownProps: Props) => {
|
||||
const { namespace, name } = ownProps.repository;
|
||||
const loading = isModifyRepoPending(state, namespace, name);
|
||||
const error = getModifyRepoFailure(state, namespace, name);
|
||||
return {
|
||||
loading,
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch: any) => {
|
||||
return {
|
||||
unarchiveRepo: (repo: Repository, callback: () => void) => {
|
||||
dispatch(unarchiveRepo(repo, callback));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(UnarchiveRepo);
|
||||
@@ -397,6 +397,42 @@ export function deleteRepoFailure(repository: Repository, error: Error): Action
|
||||
};
|
||||
}
|
||||
|
||||
// archive
|
||||
|
||||
export function archiveRepo(repository: Repository, callback?: () => void) {
|
||||
return function(dispatch: any) {
|
||||
dispatch(modifyRepoPending(repository));
|
||||
return apiClient
|
||||
.post((repository._links.archive as Link).href)
|
||||
.then(() => {
|
||||
dispatch(modifyRepoSuccess(repository));
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
dispatch(modifyRepoFailure(repository, err));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function unarchiveRepo(repository: Repository, callback?: () => void) {
|
||||
return function(dispatch: any) {
|
||||
dispatch(modifyRepoPending(repository));
|
||||
return apiClient
|
||||
.post((repository._links.unarchive as Link).href)
|
||||
.then(() => {
|
||||
dispatch(modifyRepoSuccess(repository));
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
dispatch(modifyRepoFailure(repository, err));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchNamespace(link: string, namespaceName: string) {
|
||||
return function(dispatch: any) {
|
||||
dispatch(fetchNamespacePending(namespaceName));
|
||||
|
||||
@@ -57,6 +57,7 @@ public class RepositoryDto extends HalRepresentation implements CreateRepository
|
||||
private String name;
|
||||
@NotEmpty
|
||||
private String type;
|
||||
private boolean archived;
|
||||
|
||||
RepositoryDto(Links links, Embedded embedded) {
|
||||
super(links, embedded);
|
||||
|
||||
@@ -215,10 +215,60 @@ public class RepositoryResource {
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
))
|
||||
@ApiResponse(responseCode = "500", description = "internal server error")
|
||||
public Response rename(@PathParam("namespace") String namespace, @PathParam("name") String name, @Valid RepositoryRenameDto renameDto) {
|
||||
public void rename(@PathParam("namespace") String namespace, @PathParam("name") String name, @Valid RepositoryRenameDto renameDto) {
|
||||
Repository repository = loadBy(namespace, name).get();
|
||||
manager.rename(repository, renameDto.getNamespace(), renameDto.getName());
|
||||
return Response.status(204).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the given repository as "archived".
|
||||
*
|
||||
* @param namespace the namespace of the repository to be marked
|
||||
* @param name the name of the repository to be marked
|
||||
*/
|
||||
@POST
|
||||
@Path("archive")
|
||||
@Operation(summary = "Mark repository as \"archived\"", description = "Marks the repository as \"archived\".", tags = "Repository")
|
||||
@ApiResponse(responseCode = "204", description = "update success")
|
||||
@ApiResponse(responseCode = "400", description = "invalid request, e.g. when the repository already is marked as archived")
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"repository:archive\" privilege")
|
||||
@ApiResponse(
|
||||
responseCode = "404",
|
||||
description = "not found, no repository with the specified namespace and name available",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
))
|
||||
@ApiResponse(responseCode = "500", description = "internal server error")
|
||||
public void archive(@PathParam("namespace") String namespace, @PathParam("name") String name) {
|
||||
Repository repository = loadBy(namespace, name).get();
|
||||
manager.archive(repository);
|
||||
}
|
||||
/**
|
||||
* Marks the given repository as not "archived".
|
||||
*
|
||||
* @param namespace the namespace of the repository to remove the mark from
|
||||
* @param name the name of the repository to remove the mark from
|
||||
*/
|
||||
@POST
|
||||
@Path("unarchive")
|
||||
@Operation(summary = "Mark repository as \"not archived\"", description = "Removes the \"archived\" mark from the repository.", tags = "Repository")
|
||||
@ApiResponse(responseCode = "204", description = "update success")
|
||||
@ApiResponse(responseCode = "400", description = "invalid request, e.g. when the repository is not marked as archived")
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"repository:archive\" privilege")
|
||||
@ApiResponse(
|
||||
responseCode = "404",
|
||||
description = "not found, no repository with the specified namespace and name available",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
))
|
||||
@ApiResponse(responseCode = "500", description = "internal server error")
|
||||
public void unarchive(@PathParam("namespace") String namespace, @PathParam("name") String name) {
|
||||
Repository repository = loadBy(namespace, name).get();
|
||||
manager.unarchive(repository);
|
||||
}
|
||||
|
||||
private Repository processUpdate(RepositoryDto repositoryDto, Repository existing) {
|
||||
|
||||
@@ -79,6 +79,13 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper<Reposit
|
||||
if (RepositoryPermissions.modify(repository).isPermitted()) {
|
||||
linksBuilder.single(link("update", resourceLinks.repository().update(repository.getNamespace(), repository.getName())));
|
||||
}
|
||||
if (RepositoryPermissions.archive().isPermitted(repository)) {
|
||||
if (repository.isArchived()) {
|
||||
linksBuilder.single(link("unarchive", resourceLinks.repository().unarchive(repository.getNamespace(), repository.getName())));
|
||||
} else {
|
||||
linksBuilder.single(link("archive", resourceLinks.repository().archive(repository.getNamespace(), repository.getName())));
|
||||
}
|
||||
}
|
||||
if (RepositoryPermissions.rename(repository).isPermitted()) {
|
||||
if (isRenameNamespacePossible()) {
|
||||
linksBuilder.single(link("renameWithNamespace", resourceLinks.repository().rename(repository.getNamespace(), repository.getName())));
|
||||
|
||||
@@ -370,6 +370,13 @@ class ResourceLinks {
|
||||
String importFromBundle(String type) {
|
||||
return repositoryImportLinkBuilder.method("getRepositoryImportResource").parameters().method("importFromBundle").parameters(type).href();
|
||||
}
|
||||
String archive(String namespace, String name) {
|
||||
return repositoryLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("archive").parameters().href();
|
||||
}
|
||||
|
||||
String unarchive(String namespace, String name) {
|
||||
return repositoryLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("unarchive").parameters().href();
|
||||
}
|
||||
}
|
||||
|
||||
RepositoryCollectionLinks repositoryCollection() {
|
||||
|
||||
@@ -27,6 +27,7 @@ package sonia.scm.lifecycle.modules;
|
||||
import com.google.inject.AbstractModule;
|
||||
import com.google.inject.TypeLiteral;
|
||||
import com.google.inject.throwingproviders.ThrowingProviderBinder;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.SCMContext;
|
||||
@@ -36,6 +37,8 @@ import sonia.scm.io.FileSystem;
|
||||
import sonia.scm.lifecycle.DefaultRestarter;
|
||||
import sonia.scm.lifecycle.Restarter;
|
||||
import sonia.scm.plugin.PluginLoader;
|
||||
import sonia.scm.repository.EventDrivenRepositoryArchiveCheck;
|
||||
import sonia.scm.repository.RepositoryArchivedCheck;
|
||||
import sonia.scm.repository.RepositoryLocationResolver;
|
||||
import sonia.scm.repository.xml.MetadataStore;
|
||||
import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver;
|
||||
@@ -93,6 +96,7 @@ public class BootstrapModule extends AbstractModule {
|
||||
bind(CipherHandler.class).toInstance(CipherUtil.getInstance().getCipherHandler());
|
||||
|
||||
// bind core
|
||||
bind(RepositoryArchivedCheck.class, EventDrivenRepositoryArchiveCheck.class);
|
||||
bind(ConfigurationStoreFactory.class, JAXBConfigurationStoreFactory.class);
|
||||
bind(ConfigurationEntryStoreFactory.class, JAXBConfigurationEntryStoreFactory.class);
|
||||
bind(DataStoreFactory.class, JAXBDataStoreFactory.class);
|
||||
|
||||
@@ -72,6 +72,7 @@ import sonia.scm.repository.HealthCheckContextListener;
|
||||
import sonia.scm.repository.NamespaceManager;
|
||||
import sonia.scm.repository.NamespaceStrategy;
|
||||
import sonia.scm.repository.NamespaceStrategyProvider;
|
||||
import sonia.scm.repository.PermissionProvider;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryDAO;
|
||||
import sonia.scm.repository.RepositoryManager;
|
||||
@@ -92,6 +93,7 @@ import sonia.scm.security.ConfigurableLoginAttemptHandler;
|
||||
import sonia.scm.security.DefaultAccessTokenCookieIssuer;
|
||||
import sonia.scm.security.DefaultSecuritySystem;
|
||||
import sonia.scm.security.LoginAttemptHandler;
|
||||
import sonia.scm.security.RepositoryPermissionProvider;
|
||||
import sonia.scm.security.SecuritySystem;
|
||||
import sonia.scm.template.MustacheTemplateEngine;
|
||||
import sonia.scm.template.TemplateEngine;
|
||||
@@ -247,6 +249,8 @@ class ScmServletModule extends ServletModule {
|
||||
|
||||
// bind url helper
|
||||
bind(RootURL.class).to(DefaultRootURL.class);
|
||||
|
||||
bind(PermissionProvider.class).to(RepositoryPermissionProvider.class);
|
||||
}
|
||||
|
||||
private <T> void bind(Class<T> clazz, Class<? extends T> defaultImplementation) {
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
|
||||
package sonia.scm.repository;
|
||||
|
||||
import com.github.sdorra.ssp.PermissionActionCheck;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||
@@ -294,6 +293,35 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager {
|
||||
return changedRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void archive(Repository repository) {
|
||||
setArchived(repository, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unarchive(Repository repository) {
|
||||
setArchived(repository, false);
|
||||
}
|
||||
|
||||
private void setArchived(Repository repository, boolean archived) {
|
||||
Repository originalRepository = repositoryDAO.get(repository.getNamespaceAndName());
|
||||
|
||||
if (archived == originalRepository.isArchived()) {
|
||||
throw new NoChangesMadeException(repository);
|
||||
}
|
||||
|
||||
Repository changedRepository = originalRepository.clone();
|
||||
|
||||
changedRepository.setArchived(archived);
|
||||
|
||||
managerDaoAdapter.modify(
|
||||
changedRepository,
|
||||
RepositoryPermissions::archive,
|
||||
notModified -> {
|
||||
},
|
||||
notModified -> fireEvent(HandlerEventType.MODIFY, changedRepository, originalRepository));
|
||||
}
|
||||
|
||||
private boolean hasNamespaceOrNameNotChanged(Repository repository, String newNamespace, String newName) {
|
||||
return repository.getName().equals(newName)
|
||||
&& repository.getNamespace().equals(newNamespace);
|
||||
@@ -303,12 +331,10 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager {
|
||||
public Collection<Repository> getAll(Predicate<Repository> filter, Comparator<Repository> comparator) {
|
||||
List<Repository> repositories = Lists.newArrayList();
|
||||
|
||||
PermissionActionCheck<Repository> check = RepositoryPermissions.read();
|
||||
|
||||
for (Repository repository : repositoryDAO.getAll()) {
|
||||
if (handlerMap.containsKey(repository.getType())
|
||||
&& filter.test(repository)
|
||||
&& check.isPermitted(repository)) {
|
||||
&& RepositoryPermissions.read().isPermitted(repository)) {
|
||||
Repository r = repository.clone();
|
||||
|
||||
repositories.add(r);
|
||||
@@ -331,14 +357,12 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager {
|
||||
@Override
|
||||
public Collection<Repository> getAll(Comparator<Repository> comparator,
|
||||
int start, int limit) {
|
||||
final PermissionActionCheck<Repository> check =
|
||||
RepositoryPermissions.read();
|
||||
|
||||
return Util.createSubCollection(repositoryDAO.getAll(), comparator,
|
||||
new CollectionAppender<Repository>() {
|
||||
@Override
|
||||
public void append(Collection<Repository> collection, Repository item) {
|
||||
if (check.isPermitted(item)) {
|
||||
if (RepositoryPermissions.read().isPermitted(item)) {
|
||||
collection.add(item.clone());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,10 +72,8 @@ public final class HealthChecker {
|
||||
public void checkAll() {
|
||||
logger.debug("check health of all repositories");
|
||||
|
||||
PermissionActionCheck<Repository> check = RepositoryPermissions.healthCheck();
|
||||
|
||||
for (Repository repository : repositoryManager.getAll()) {
|
||||
if (check.isPermitted(repository)) {
|
||||
if (RepositoryPermissions.healthCheck().isPermitted(repository)) {
|
||||
try {
|
||||
check(repository);
|
||||
} catch (NotFoundException ex) {
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
package sonia.scm.security;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import sonia.scm.repository.PermissionProvider;
|
||||
import sonia.scm.repository.RepositoryRole;
|
||||
import sonia.scm.repository.RepositoryRoleDAO;
|
||||
|
||||
@@ -32,7 +33,7 @@ import java.util.AbstractList;
|
||||
import java.util.Collection;
|
||||
import java.util.List ;
|
||||
|
||||
public class RepositoryPermissionProvider {
|
||||
public class RepositoryPermissionProvider implements PermissionProvider {
|
||||
|
||||
private final SystemRepositoryPermissionProvider systemRepositoryPermissionProvider;
|
||||
private final RepositoryRoleDAO repositoryRoleDAO;
|
||||
@@ -47,6 +48,10 @@ public class RepositoryPermissionProvider {
|
||||
return systemRepositoryPermissionProvider.availableVerbs();
|
||||
}
|
||||
|
||||
public Collection<String> readOnlyVerbs() {
|
||||
return systemRepositoryPermissionProvider.readOnlyVerbs();
|
||||
}
|
||||
|
||||
public Collection<RepositoryRole> availableRoles() {
|
||||
List<RepositoryRole> availableSystemRoles = systemRepositoryPermissionProvider.availableRoles();
|
||||
List<RepositoryRole> customRoles = repositoryRoleDAO.getAll();
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
package sonia.scm.security;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.plugin.PluginLoader;
|
||||
@@ -34,8 +35,10 @@ import javax.xml.bind.JAXBContext;
|
||||
import javax.xml.bind.JAXBException;
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlAttribute;
|
||||
import javax.xml.bind.annotation.XmlElement;
|
||||
import javax.xml.bind.annotation.XmlRootElement;
|
||||
import javax.xml.bind.annotation.XmlValue;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
@@ -54,25 +57,32 @@ public class SystemRepositoryPermissionProvider {
|
||||
private static final Logger logger = LoggerFactory.getLogger(SystemRepositoryPermissionProvider.class);
|
||||
private static final String REPOSITORY_PERMISSION_DESCRIPTOR = "META-INF/scm/repository-permissions.xml";
|
||||
private final List<String> availableVerbs;
|
||||
private final List<String> readOnlyVerbs;
|
||||
private final List<RepositoryRole> availableRoles;
|
||||
|
||||
@Inject
|
||||
public SystemRepositoryPermissionProvider(PluginLoader pluginLoader) {
|
||||
AvailableRepositoryPermissions availablePermissions = readAvailablePermissions(pluginLoader);
|
||||
this.availableVerbs = removeDuplicates(availablePermissions.availableVerbs);
|
||||
this.availableRoles = removeDuplicates(availablePermissions.availableRoles.stream().map(r -> new RepositoryRole(r.name, r.verbs.verbs, "system")).collect(toList()));
|
||||
this.readOnlyVerbs = removeDuplicates(availablePermissions.readOnlyVerbs);
|
||||
this.availableRoles = removeDuplicates(availablePermissions.availableRoles.stream().map(r -> new RepositoryRole(r.name, r.verbs.verbs.stream().map(verb -> verb.value).collect(toList()), "system")).collect(toList()));
|
||||
}
|
||||
|
||||
public List<String> availableVerbs() {
|
||||
return availableVerbs;
|
||||
}
|
||||
|
||||
public List<String> readOnlyVerbs() {
|
||||
return readOnlyVerbs;
|
||||
}
|
||||
|
||||
public List<RepositoryRole> availableRoles() {
|
||||
return availableRoles;
|
||||
}
|
||||
|
||||
private static AvailableRepositoryPermissions readAvailablePermissions(PluginLoader pluginLoader) {
|
||||
Collection<String> availableVerbs = new ArrayList<>();
|
||||
Collection<String> readOnlyVerbs = new ArrayList<>();
|
||||
Collection<RoleDescriptor> availableRoles = new ArrayList<>();
|
||||
|
||||
try {
|
||||
@@ -89,7 +99,8 @@ public class SystemRepositoryPermissionProvider {
|
||||
logger.debug("read repository permission descriptor from {}", descriptorUrl);
|
||||
|
||||
RepositoryPermissionsRoot repositoryPermissionsRoot = parsePermissionDescriptor(context, descriptorUrl);
|
||||
availableVerbs.addAll(repositoryPermissionsRoot.verbs.verbs);
|
||||
repositoryPermissionsRoot.verbs.verbs.forEach(verb -> availableVerbs.add(verb.value));
|
||||
repositoryPermissionsRoot.verbs.verbs.stream().filter(verb -> verb.readOnly).map(verb -> verb.value).forEach(readOnlyVerbs::add);
|
||||
mergeRolesInto(availableRoles, repositoryPermissionsRoot.roles.roles);
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
@@ -99,7 +110,7 @@ public class SystemRepositoryPermissionProvider {
|
||||
"could not create jaxb context to read permission descriptors", ex);
|
||||
}
|
||||
|
||||
return new AvailableRepositoryPermissions(availableVerbs, availableRoles);
|
||||
return new AvailableRepositoryPermissions(availableVerbs, readOnlyVerbs, availableRoles);
|
||||
}
|
||||
|
||||
private static void mergeRolesInto(Collection<RoleDescriptor> targetRoles, List<RoleDescriptor> additionalRoles) {
|
||||
@@ -138,10 +149,12 @@ public class SystemRepositoryPermissionProvider {
|
||||
|
||||
private static class AvailableRepositoryPermissions {
|
||||
private final Collection<String> availableVerbs;
|
||||
private final Collection<String> readOnlyVerbs;
|
||||
private final Collection<RoleDescriptor> availableRoles;
|
||||
|
||||
private AvailableRepositoryPermissions(Collection<String> availableVerbs, Collection<RoleDescriptor> availableRoles) {
|
||||
private AvailableRepositoryPermissions(Collection<String> availableVerbs, Collection<String> readOnlyVerbs, Collection<RoleDescriptor> availableRoles) {
|
||||
this.availableVerbs = unmodifiableCollection(availableVerbs);
|
||||
this.readOnlyVerbs = unmodifiableCollection(readOnlyVerbs);
|
||||
this.availableRoles = unmodifiableCollection(availableRoles);
|
||||
}
|
||||
}
|
||||
@@ -156,7 +169,18 @@ public class SystemRepositoryPermissionProvider {
|
||||
@XmlRootElement(name = "verbs")
|
||||
private static class VerbListDescriptor {
|
||||
@XmlElement(name = "verb")
|
||||
private Set<String> verbs = new LinkedHashSet<>();
|
||||
private Set<Verb> verbs = new LinkedHashSet<>();
|
||||
}
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
@XmlRootElement(name = "verb")
|
||||
@EqualsAndHashCode
|
||||
private static class Verb {
|
||||
@XmlValue
|
||||
private String value;
|
||||
@XmlAttribute(name = "read-only")
|
||||
@EqualsAndHashCode.Exclude
|
||||
private boolean readOnly;
|
||||
}
|
||||
|
||||
@XmlRootElement(name = "roles")
|
||||
|
||||
@@ -175,6 +175,7 @@ public class XmlRepositoryV1UpdateStep implements CoreUpdateStep {
|
||||
v1Repository.getContact(),
|
||||
v1Repository.getDescription(),
|
||||
createPermissions(v1Repository));
|
||||
repository.setArchived(v1Repository.isArchived());
|
||||
LOG.info("creating new repository {} from old repository {} in directory {}", repository, v1Repository.getName(), newPath);
|
||||
repositoryDao.add(repository, newPath);
|
||||
propertyStore.put(v1Repository.getId(), v1Repository.getProperties());
|
||||
|
||||
@@ -44,6 +44,9 @@
|
||||
<permission>
|
||||
<value>repository:create</value>
|
||||
</permission>
|
||||
<permission>
|
||||
<value>repository:archive:*</value>
|
||||
</permission>
|
||||
<permission>
|
||||
<value>namespace:permissionRead</value>
|
||||
</permission>
|
||||
|
||||
@@ -25,14 +25,15 @@
|
||||
-->
|
||||
<repository-permissions>
|
||||
<verbs>
|
||||
<verb>read</verb>
|
||||
<verb read-only="true">read</verb>
|
||||
<verb>modify</verb>
|
||||
<verb>delete</verb>
|
||||
<verb>rename</verb>
|
||||
<verb>pull</verb>
|
||||
<verb read-only="true">pull</verb>
|
||||
<verb>push</verb>
|
||||
<verb>permissionRead</verb>
|
||||
<verb read-only="true">permissionRead</verb>
|
||||
<verb>permissionWrite</verb>
|
||||
<verb read-only="true">archive</verb>
|
||||
<verb>*</verb>
|
||||
</verbs>
|
||||
<roles>
|
||||
|
||||
@@ -41,6 +41,12 @@
|
||||
"create": {
|
||||
"displayName": "Repositories erstellen",
|
||||
"description": "Darf Repositories erstellen."
|
||||
},
|
||||
"archive": {
|
||||
"*": {
|
||||
"displayName": "Repositories archivieren",
|
||||
"description": "Darf Repositories als \"archiviert\" und somit als schreibgeschützt markieren."
|
||||
}
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
@@ -139,6 +145,10 @@
|
||||
"displayName": "Berechtigungen modifizieren",
|
||||
"description": "Darf die Berechtigungen des Repository bearbeiten."
|
||||
},
|
||||
"archive": {
|
||||
"displayName": "Repository archivieren",
|
||||
"description": "Darf das Repository als \"archiviert\" und somit als schreibgeschützt markieren."
|
||||
},
|
||||
"*": {
|
||||
"displayName": "Alle Repository Rechte",
|
||||
"description": "Darf im Repository Kontext alles ausführen. Dies beinhaltet alle Repository Berechtigungen."
|
||||
@@ -217,7 +227,7 @@
|
||||
},
|
||||
"4iRct4avG1": {
|
||||
"displayName": "Die Revisionen haben keinen gemeinsamen Ursprung",
|
||||
"description": "Die Historie der Revisionen hat keinen gemeinsamen Urspung und kann somit auch nicht gegen einen solchen verglichen werden."
|
||||
"description": "Die Historie der Revisionen hat keinen gemeinsamen Ursprung und kann somit auch nicht gegen einen solchen verglichen werden."
|
||||
},
|
||||
"65RdZ5atX1": {
|
||||
"displayName": "Fehler beim Löschen von Plugin-Dateien",
|
||||
@@ -241,7 +251,7 @@
|
||||
},
|
||||
"4GRrgkSC01": {
|
||||
"displayName": "Unerwartetes Merge-Ergebnis",
|
||||
"description": "Der Merge hatte ein unerwartetes Ergebis, das nicht automatisiert behandelt werden konnte. Nähere Details sind im Log zu finden. Führen Sie den Merge ggf. manuell durch."
|
||||
"description": "Der Merge hatte ein unerwartetes Ergebnis, das nicht automatisiert behandelt werden konnte. Nähere Details sind im Log zu finden. Führen Sie den Merge ggf. manuell durch."
|
||||
},
|
||||
"6mRuFxaWM1": {
|
||||
"displayName": "Falsche Checksumme",
|
||||
@@ -256,12 +266,12 @@
|
||||
"description": "Ein fehlerhaft heruntergeladenes Plugin konnte nicht gelöscht werden. Bitte prüfen Sie die Server Logs und löschen die Datei manuell."
|
||||
},
|
||||
"5GS6lwvWF1": {
|
||||
"displayName": "Abhänigkeit konnte nicht gefunden werden",
|
||||
"description": "Eine der Abhänigkeiten des Plugins konnte nicht gefunden werden. Bitte prüfen Sie die Logs für weitere Informationen."
|
||||
"displayName": "Abhängigkeit konnte nicht gefunden werden",
|
||||
"description": "Eine der Abhängigkeiten des Plugins konnte nicht gefunden werden. Bitte prüfen Sie die Logs für weitere Informationen."
|
||||
},
|
||||
"E5S6niWwi1": {
|
||||
"displayName": "Version einer Abhänigkeit zu niedrig",
|
||||
"description": "Die Version einer Abhänigkeit des Plugin ist zu niedrig. Bitte prüfen Sie die Logs für weitere Informationen."
|
||||
"displayName": "Version einer Abhängigkeit zu niedrig",
|
||||
"description": "Die Version einer Abhängigkeit des Plugin ist zu niedrig. Bitte prüfen Sie die Logs für weitere Informationen."
|
||||
},
|
||||
"4RS6niPRX1": {
|
||||
"displayName": "Plugin information stimmen nicht überein",
|
||||
@@ -269,7 +279,7 @@
|
||||
},
|
||||
"2qRyyaVcJ1": {
|
||||
"displayName": "Ungültig formatiertes Element",
|
||||
"description": "Die Eingabe beinhaltete unfültige Formate. Bitte prüfen Sie die Server Logs für genauere Informationen."
|
||||
"description": "Die Eingabe beinhaltete ungültige Formate. Bitte prüfen Sie die Server Logs für genauere Informationen."
|
||||
},
|
||||
"3tS0mjSoo1": {
|
||||
"displayName": "Fehler bei der Erstellung eines Arbeitsverzeichnisses",
|
||||
@@ -281,7 +291,7 @@
|
||||
},
|
||||
"BxS5wX2v71": {
|
||||
"displayName": "Inkorrekter Schlüssel",
|
||||
"description": "Der bereitgestellte Schlüssel ist kein korrekt formartierter öffentlicher Schlüssel."
|
||||
"description": "Der bereitgestellte Schlüssel ist kein korrekt formatierter öffentlicher Schlüssel."
|
||||
},
|
||||
"FVS9JY1T21": {
|
||||
"displayName": "Fehler bei der Anfrage",
|
||||
@@ -290,6 +300,14 @@
|
||||
"D6SHRfqQw1": {
|
||||
"displayName": "Repository Import fehlgeschlagen",
|
||||
"description": "Das Repository konnte nicht importiert werden. Möglicherweise wurden die Zugangsdaten (Benutzername/Passwort) nicht gesetzt oder sind fehlerhaft. Bitte prüfen Sie Ihre Eingaben."
|
||||
},
|
||||
"3FSIYtBJw1": {
|
||||
"displayName": "Ein Datensatz konnte nicht gespeichert werden",
|
||||
"description": "Ein Datensatz konnte nicht gespeichert werden, da der entsprechende Speicher als schreibgeschützt markiert wurde. Weitere Hinweise finden sich im Log."
|
||||
},
|
||||
"3hSIlptme1": {
|
||||
"displayName": "Repository ist archiviert",
|
||||
"description": "Das Repository ist als \"archiviert\" markiert und darf nicht modifiziert werden."
|
||||
}
|
||||
},
|
||||
"namespaceStrategies": {
|
||||
|
||||
@@ -41,6 +41,12 @@
|
||||
"create": {
|
||||
"displayName": "Create repositories",
|
||||
"description": "May create repositories"
|
||||
},
|
||||
"archive": {
|
||||
"*": {
|
||||
"displayName": "Archive repositories",
|
||||
"description": "May mark repositories as \"archived\" and therefore unmodifiable"
|
||||
}
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
@@ -139,6 +145,10 @@
|
||||
"displayName": "modify permissions",
|
||||
"description": "May modify the permissions of the repository"
|
||||
},
|
||||
"archive": {
|
||||
"displayName": "archive repository",
|
||||
"description": "May mark the repository as \"archived\" and therefore unmodifiable"
|
||||
},
|
||||
"*": {
|
||||
"displayName": "own repository",
|
||||
"description": "May change everything for the repository (includes all other permissions)"
|
||||
@@ -290,6 +300,14 @@
|
||||
"D6SHRfqQw1": {
|
||||
"displayName": "Repository import failed",
|
||||
"description": "The repository could not be imported. It's likely that either the credentials (username/password) are wrong or missing. Please check your inputs."
|
||||
},
|
||||
"3FSIYtBJw1": {
|
||||
"displayName": "An entity could not be stored",
|
||||
"description": "An entity could not be stored, because the corresponding store was set to read only. Please see the log for more information."
|
||||
},
|
||||
"3hSIlptme1": {
|
||||
"displayName": "Repository is archived",
|
||||
"description": "The repository is marked as \"archived\" and therefore must noch be modified."
|
||||
}
|
||||
},
|
||||
"namespaceStrategies": {
|
||||
|
||||
@@ -673,6 +673,43 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
|
||||
verify(ubc).unbundle(any(File.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldMarkRepositoryAsArchived() throws Exception {
|
||||
String namespace = "space";
|
||||
String name = "repo";
|
||||
Repository repository = mockRepository(namespace, name);
|
||||
when(manager.get(new NamespaceAndName(namespace, name))).thenReturn(repository);
|
||||
|
||||
MockHttpRequest request = MockHttpRequest
|
||||
.post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/archive")
|
||||
.content(new byte[]{});
|
||||
MockHttpResponse response = new MockHttpResponse();
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertEquals(SC_NO_CONTENT, response.getStatus());
|
||||
verify(repositoryManager).archive(repository);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldRemoveArchiveMarkFromRepository() throws Exception {
|
||||
String namespace = "space";
|
||||
String name = "repo";
|
||||
Repository repository = mockRepository(namespace, name);
|
||||
repository.setArchived(true);
|
||||
when(manager.get(new NamespaceAndName(namespace, name))).thenReturn(repository);
|
||||
|
||||
MockHttpRequest request = MockHttpRequest
|
||||
.post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/unarchive")
|
||||
.content(new byte[]{});
|
||||
MockHttpResponse response = new MockHttpResponse();
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertEquals(SC_NO_CONTENT, response.getStatus());
|
||||
verify(repositoryManager).unarchive(repository);
|
||||
}
|
||||
|
||||
private PageResult<Repository> createSingletonPageResult(Repository repository) {
|
||||
return new PageResult<>(singletonList(repository), 0);
|
||||
}
|
||||
|
||||
@@ -266,6 +266,24 @@ public class RepositoryToRepositoryDtoMapperTest {
|
||||
assertEquals("http://1", dto.getLinks().getLinkBy("id").get().getHref());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldCreateArchiveLink() {
|
||||
RepositoryDto dto = mapper.map(createTestRepository());
|
||||
assertEquals(
|
||||
"http://example.com/base/v2/repositories/testspace/test/archive",
|
||||
dto.getLinks().getLinkBy("archive").get().getHref());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldCreateUnArchiveLink() {
|
||||
Repository repository = createTestRepository();
|
||||
repository.setArchived(true);
|
||||
RepositoryDto dto = mapper.map(repository);
|
||||
assertEquals(
|
||||
"http://example.com/base/v2/repositories/testspace/test/unarchive",
|
||||
dto.getLinks().getLinkBy("unarchive").get().getHref());
|
||||
}
|
||||
|
||||
private ScmProtocol mockProtocol(String type, String protocol) {
|
||||
return new MockScmProtocol(type, protocol);
|
||||
}
|
||||
|
||||
@@ -78,13 +78,16 @@ import static org.junit.Assert.assertNotSame;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.Mockito.anyString;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
@@ -101,6 +104,8 @@ import static org.mockito.Mockito.when;
|
||||
)
|
||||
public class DefaultRepositoryManagerTest extends ManagerTestBase<Repository> {
|
||||
|
||||
private RepositoryDAO repositoryDAO;
|
||||
|
||||
{
|
||||
ThreadContext.unbindSubject();
|
||||
}
|
||||
@@ -457,6 +462,77 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase<Repository> {
|
||||
.contains("default_namespace");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldMarkRepositoryAsArchived() {
|
||||
Repository repository = createTestRepository();
|
||||
RepositoryManager repoManager = (RepositoryManager) manager;
|
||||
|
||||
repoManager.archive(repository);
|
||||
|
||||
verify(repositoryDAO).modify(argThat(Repository::isArchived));
|
||||
}
|
||||
|
||||
@Test
|
||||
@SubjectAware(username = "dent")
|
||||
public void shouldNotMarkRepositoryAsArchivedWithoutPermission() {
|
||||
Repository repository = RepositoryTestData.createHeartOfGold();
|
||||
when(repositoryDAO.get(repository.getNamespaceAndName())).thenReturn(repository);
|
||||
when(repositoryDAO.get(repository.getId())).thenReturn(repository);
|
||||
RepositoryManager repoManager = (RepositoryManager) manager;
|
||||
|
||||
assertThrows(UnauthorizedException.class, () -> repoManager.archive(repository));
|
||||
|
||||
verify(repositoryDAO, never()).modify(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNotMarkRepositoryAsArchivedTwice() {
|
||||
Repository repository = RepositoryTestData.createHeartOfGold();
|
||||
repository.setArchived(true);
|
||||
createRepository(repository);
|
||||
RepositoryManager repoManager = (RepositoryManager) manager;
|
||||
|
||||
assertThrows(NoChangesMadeException.class, () -> repoManager.archive(repository));
|
||||
|
||||
verify(repositoryDAO, never()).modify(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldRemoveArchiveMarkFromRepository() {
|
||||
Repository repository = RepositoryTestData.createHeartOfGold();
|
||||
repository.setArchived(true);
|
||||
createRepository(repository);
|
||||
RepositoryManager repoManager = (RepositoryManager) manager;
|
||||
|
||||
repoManager.unarchive(repository);
|
||||
|
||||
verify(repositoryDAO).modify(argThat(r -> !r.isArchived()));
|
||||
}
|
||||
|
||||
@Test
|
||||
@SubjectAware(username = "dent")
|
||||
public void shouldNotRemoveArchiveMarkFromRepositoryWithoutPermission() {
|
||||
Repository repository = RepositoryTestData.createHeartOfGold();
|
||||
when(repositoryDAO.get(repository.getNamespaceAndName())).thenReturn(repository);
|
||||
when(repositoryDAO.get(repository.getId())).thenReturn(repository);
|
||||
repository.setArchived(true);
|
||||
RepositoryManager repoManager = (RepositoryManager) manager;
|
||||
|
||||
assertThrows(UnauthorizedException.class, () -> repoManager.unarchive(repository));
|
||||
|
||||
verify(repositoryDAO, never()).modify(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNotRemoveArchiveMarkFromNotArchivedRepository() {
|
||||
Repository repository = createTestRepository();
|
||||
RepositoryManager repoManager = (RepositoryManager) manager;
|
||||
|
||||
assertThrows(NoChangesMadeException.class, () -> repoManager.unarchive(repository));
|
||||
|
||||
verify(repositoryDAO, never()).modify(any());
|
||||
}
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
@Override
|
||||
@@ -466,7 +542,7 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase<Repository> {
|
||||
|
||||
private DefaultRepositoryManager createRepositoryManager(KeyGenerator keyGenerator) {
|
||||
Set<RepositoryHandler> handlerSet = new HashSet<>();
|
||||
RepositoryDAO repositoryDAO = createRepositoryDaoMock();
|
||||
repositoryDAO = createRepositoryDaoMock();
|
||||
mock(ConfigurationStoreFactory.class);
|
||||
handlerSet.add(createRepositoryHandler("dummy", "Dummy"));
|
||||
handlerSet.add(createRepositoryHandler("git", "Git"));
|
||||
|
||||
@@ -68,7 +68,7 @@ public class DefaultSecuritySystemTest extends AbstractTestBase
|
||||
public void createSecuritySystem()
|
||||
{
|
||||
jaxbConfigurationEntryStoreFactory =
|
||||
spy(new JAXBConfigurationEntryStoreFactory(contextProvider , repositoryLocationResolver, new UUIDKeyGenerator() ) {});
|
||||
spy(new JAXBConfigurationEntryStoreFactory(contextProvider , repositoryLocationResolver, new UUIDKeyGenerator(), null) {});
|
||||
pluginLoader = mock(PluginLoader.class);
|
||||
when(pluginLoader.getUberClassLoader()).thenReturn(ClassLoaders.getContextClassLoader(DefaultSecuritySystem.class));
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ import java.lang.reflect.Field;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
import static org.mockito.Mockito.mock;
|
||||
@@ -55,7 +56,7 @@ class SystemRepositoryPermissionProviderTest {
|
||||
.filter(field -> field.getName().startsWith("ACTION_"))
|
||||
.filter(field -> !field.getName().equals("ACTION_HEALTHCHECK"))
|
||||
.map(this::getString)
|
||||
.filter(verb -> !"create".equals(verb))
|
||||
.filter(verb -> !asList("create", "archive").contains(verb))
|
||||
.toArray(String[]::new);
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ class DefaultMigrationStrategyDAOTest {
|
||||
@BeforeEach
|
||||
void initStore(@TempDir Path tempDir) {
|
||||
when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile());
|
||||
storeFactory = new JAXBConfigurationStoreFactory(contextProvider, null);
|
||||
storeFactory = new JAXBConfigurationStoreFactory(contextProvider, null, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -52,7 +52,7 @@ class V1RepositoryFileSystem {
|
||||
* <id>c1597b4f-a9f0-49f7-ad1f-37d3aae1c55f</id>
|
||||
* <name>some/more/directories/than/one</name>
|
||||
* <public>false</public>
|
||||
* <archived>false</archived>
|
||||
* <archived>true</archived>
|
||||
* <type>git</type>
|
||||
* </repository>
|
||||
* <repository>
|
||||
|
||||
@@ -136,7 +136,19 @@ class XmlRepositoryV1UpdateStepTest {
|
||||
.get()
|
||||
.hasFieldOrPropertyWithValue("type", "git")
|
||||
.hasFieldOrPropertyWithValue("contact", "arthur@dent.uk")
|
||||
.hasFieldOrPropertyWithValue("description", "A repository with two folders.");
|
||||
.hasFieldOrPropertyWithValue("description", "A repository with two folders.")
|
||||
.hasFieldOrPropertyWithValue("archived", false);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMapArchivedAttribute() throws JAXBException {
|
||||
updateStep.doUpdate();
|
||||
|
||||
Optional<Repository> repository = findByNamespace("namespace-c1597b4f-a9f0-49f7-ad1f-37d3aae1c55f");
|
||||
|
||||
assertThat(repository)
|
||||
.get()
|
||||
.hasFieldOrPropertyWithValue("archived", true);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -127,6 +127,6 @@ public class DefaultUserManagerTest extends UserManagerTestBase {
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
private XmlUserDAO createXmlUserDAO() {
|
||||
return new XmlUserDAO(new JAXBConfigurationStoreFactory(contextProvider, locationResolver));
|
||||
return new XmlUserDAO(new JAXBConfigurationStoreFactory(contextProvider, locationResolver, null));
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user