mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-02 11:35:57 +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
|
### Added
|
||||||
- Add repository import via dump file for Subversion ([#1471](https://github.com/scm-manager/scm-manager/pull/1471))
|
- 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 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
|
### Fixed
|
||||||
- Add "Api Key" page link to sub-navigation of "User" and "Me" sections ([#1464](https://github.com/scm-manager/scm-manager/pull/1464))
|
- 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
|
title: Repository subtitle: Einstellungen
|
||||||
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
|
### 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
|
### 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
|
title: Repository subtitle: Settings
|
||||||
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
|
### 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
|
### 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>
|
<jetty.maven.version>9.4.34.v20201102</jetty.maven.version>
|
||||||
|
|
||||||
<!-- security libraries -->
|
<!-- security libraries -->
|
||||||
<ssp.version>1.2.0</ssp.version>
|
<ssp.version>1.3.0</ssp.version>
|
||||||
<shiro.version>1.7.0</shiro.version>
|
<shiro.version>1.7.0</shiro.version>
|
||||||
|
|
||||||
<!-- repository libraries -->
|
<!-- 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;
|
package sonia.scm.repository;
|
||||||
|
|
||||||
|
import com.github.sdorra.ssp.Guard;
|
||||||
import com.github.sdorra.ssp.PermissionObject;
|
import com.github.sdorra.ssp.PermissionObject;
|
||||||
import com.github.sdorra.ssp.StaticPermissions;
|
import com.github.sdorra.ssp.StaticPermissions;
|
||||||
import com.google.common.base.MoreObjects;
|
import com.google.common.base.MoreObjects;
|
||||||
@@ -51,13 +52,16 @@ import java.util.Set;
|
|||||||
*
|
*
|
||||||
* @author Sebastian Sdorra
|
* @author Sebastian Sdorra
|
||||||
*/
|
*/
|
||||||
@StaticPermissions(
|
|
||||||
value = "repository",
|
|
||||||
permissions = {"read", "modify", "delete", "rename", "healthCheck", "pull", "push", "permissionRead", "permissionWrite"},
|
|
||||||
custom = true, customGlobal = true
|
|
||||||
)
|
|
||||||
@XmlAccessorType(XmlAccessType.FIELD)
|
@XmlAccessorType(XmlAccessType.FIELD)
|
||||||
@XmlRootElement(name = "repositories")
|
@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 {
|
public class Repository extends BasicPropertiesAware implements ModelObject, PermissionObject {
|
||||||
|
|
||||||
private static final long serialVersionUID = 3486560714961909711L;
|
private static final long serialVersionUID = 3486560714961909711L;
|
||||||
@@ -75,6 +79,7 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
|
|||||||
@XmlElement(name = "permission")
|
@XmlElement(name = "permission")
|
||||||
private Set<RepositoryPermission> permissions = new HashSet<>();
|
private Set<RepositoryPermission> permissions = new HashSet<>();
|
||||||
private String type;
|
private String type;
|
||||||
|
private boolean archived;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -204,6 +209,15 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
|
|||||||
return type;
|
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.
|
* Returns {@code true} if the repository is healthy.
|
||||||
*
|
*
|
||||||
@@ -276,6 +290,15 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
|
|||||||
this.type = type;
|
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) {
|
public void setHealthCheckFailures(List<HealthCheckFailure> healthCheckFailures) {
|
||||||
this.healthCheckFailures = 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);
|
afterCreation.accept(newRepository);
|
||||||
return 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();
|
return decorated.getAllNamespaces();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void archive(Repository repository) {
|
||||||
|
decorated.archive(repository);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void unarchive(Repository repository) {
|
||||||
|
decorated.unarchive(repository);
|
||||||
|
}
|
||||||
|
|
||||||
//~--- fields ---------------------------------------------------------------
|
//~--- 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.
|
* by the implementation of the repository service provider.
|
||||||
*/
|
*/
|
||||||
public BranchCommandBuilder getBranchCommand() {
|
public BranchCommandBuilder getBranchCommand() {
|
||||||
|
verifyNotArchived();
|
||||||
RepositoryPermissions.push(getRepository()).check();
|
RepositoryPermissions.push(getRepository()).check();
|
||||||
LOG.debug("create branch command for repository {}",
|
LOG.debug("create branch command for repository {}",
|
||||||
repository.getNamespaceAndName());
|
repository.getNamespaceAndName());
|
||||||
@@ -332,6 +333,7 @@ public final class RepositoryService implements Closeable {
|
|||||||
* @since 1.31
|
* @since 1.31
|
||||||
*/
|
*/
|
||||||
public PullCommandBuilder getPullCommand() {
|
public PullCommandBuilder getPullCommand() {
|
||||||
|
verifyNotArchived();
|
||||||
LOG.debug("create pull command for repository {}",
|
LOG.debug("create pull command for repository {}",
|
||||||
repository.getNamespaceAndName());
|
repository.getNamespaceAndName());
|
||||||
|
|
||||||
@@ -386,6 +388,7 @@ public final class RepositoryService implements Closeable {
|
|||||||
* by the implementation of the repository service provider.
|
* by the implementation of the repository service provider.
|
||||||
*/
|
*/
|
||||||
public TagCommandBuilder getTagCommand() {
|
public TagCommandBuilder getTagCommand() {
|
||||||
|
verifyNotArchived();
|
||||||
return new TagCommandBuilder(provider.getTagCommand());
|
return new TagCommandBuilder(provider.getTagCommand());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -415,6 +418,7 @@ public final class RepositoryService implements Closeable {
|
|||||||
* @since 2.0.0
|
* @since 2.0.0
|
||||||
*/
|
*/
|
||||||
public MergeCommandBuilder getMergeCommand() {
|
public MergeCommandBuilder getMergeCommand() {
|
||||||
|
verifyNotArchived();
|
||||||
LOG.debug("create merge command for repository {}",
|
LOG.debug("create merge command for repository {}",
|
||||||
repository.getNamespaceAndName());
|
repository.getNamespaceAndName());
|
||||||
|
|
||||||
@@ -436,6 +440,7 @@ public final class RepositoryService implements Closeable {
|
|||||||
* @since 2.0.0
|
* @since 2.0.0
|
||||||
*/
|
*/
|
||||||
public ModifyCommandBuilder getModifyCommand() {
|
public ModifyCommandBuilder getModifyCommand() {
|
||||||
|
verifyNotArchived();
|
||||||
LOG.debug("create modify command for repository {}",
|
LOG.debug("create modify command for repository {}",
|
||||||
repository.getNamespaceAndName());
|
repository.getNamespaceAndName());
|
||||||
|
|
||||||
@@ -484,6 +489,12 @@ public final class RepositoryService implements Closeable {
|
|||||||
.filter(protocol -> !Authentications.isAuthenticatedSubjectAnonymous() || protocol.isAnonymousEnabled());
|
.filter(protocol -> !Authentications.isAuthenticatedSubjectAnonymous() || protocol.isAnonymousEnabled());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void verifyNotArchived() {
|
||||||
|
if (getRepository().isArchived()) {
|
||||||
|
throw new RepositoryArchivedException(getRepository());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressWarnings({"rawtypes", "java:S3740"})
|
@SuppressWarnings({"rawtypes", "java:S3740"})
|
||||||
private ScmProtocol createProviderInstanceForRepository(ScmProtocolProvider protocolProvider) {
|
private ScmProtocol createProviderInstanceForRepository(ScmProtocolProvider protocolProvider) {
|
||||||
return protocolProvider.get(repository);
|
return protocolProvider.get(repository);
|
||||||
|
|||||||
@@ -38,6 +38,11 @@ public abstract class AbstractStore<T> implements ConfigurationStore<T> {
|
|||||||
* stored object
|
* stored object
|
||||||
*/
|
*/
|
||||||
protected T storeObject;
|
protected T storeObject;
|
||||||
|
private final boolean readOnly;
|
||||||
|
|
||||||
|
protected AbstractStore(boolean readOnly) {
|
||||||
|
this.readOnly = readOnly;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public T get() {
|
public T get() {
|
||||||
@@ -49,9 +54,12 @@ public abstract class AbstractStore<T> implements ConfigurationStore<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void set(T obejct) {
|
public void set(T object) {
|
||||||
writeObject(obejct);
|
if (readOnly) {
|
||||||
this.storeObject = obejct;
|
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));
|
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 static class DummyHttpProtocol extends HttpScmProtocol {
|
||||||
|
|
||||||
private final boolean anonymousEnabled;
|
private final boolean anonymousEnabled;
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import sonia.scm.repository.NamespaceAndName;
|
|||||||
import sonia.scm.repository.Repository;
|
import sonia.scm.repository.Repository;
|
||||||
import sonia.scm.repository.RepositoryDAO;
|
import sonia.scm.repository.RepositoryDAO;
|
||||||
import sonia.scm.repository.RepositoryLocationResolver;
|
import sonia.scm.repository.RepositoryLocationResolver;
|
||||||
|
import sonia.scm.store.StoreReadOnlyException;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -139,6 +140,9 @@ public class XmlRepositoryDAO implements RepositoryDAO {
|
|||||||
@Override
|
@Override
|
||||||
public void modify(Repository repository) {
|
public void modify(Repository repository) {
|
||||||
Repository clone = repository.clone();
|
Repository clone = repository.clone();
|
||||||
|
if (clone.isArchived() && byId.get(clone.getId()).isArchived()) {
|
||||||
|
throw new StoreReadOnlyException(repository);
|
||||||
|
}
|
||||||
|
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
// remove old namespaceAndName from map, in case of rename
|
// remove old namespaceAndName from map, in case of rename
|
||||||
@@ -158,6 +162,9 @@ public class XmlRepositoryDAO implements RepositoryDAO {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void delete(Repository repository) {
|
public void delete(Repository repository) {
|
||||||
|
if (repository.isArchived()) {
|
||||||
|
throw new StoreReadOnlyException(repository);
|
||||||
|
}
|
||||||
Path path;
|
Path path;
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
Repository prev = byId.remove(repository.getId());
|
Repository prev = byId.remove(repository.getId());
|
||||||
|
|||||||
@@ -60,10 +60,11 @@ public abstract class FileBasedStore<T> implements MultiEntryStore<T>
|
|||||||
* @param directory
|
* @param directory
|
||||||
* @param suffix
|
* @param suffix
|
||||||
*/
|
*/
|
||||||
public FileBasedStore(File directory, String suffix)
|
public FileBasedStore(File directory, String suffix, boolean readOnly)
|
||||||
{
|
{
|
||||||
this.directory = directory;
|
this.directory = directory;
|
||||||
this.suffix = suffix;
|
this.suffix = suffix;
|
||||||
|
this.readOnly = readOnly;
|
||||||
}
|
}
|
||||||
|
|
||||||
//~--- methods --------------------------------------------------------------
|
//~--- methods --------------------------------------------------------------
|
||||||
@@ -145,6 +146,8 @@ public abstract class FileBasedStore<T> implements MultiEntryStore<T>
|
|||||||
{
|
{
|
||||||
logger.trace("delete store entry {}", file);
|
logger.trace("delete store entry {}", file);
|
||||||
|
|
||||||
|
assertNotReadOnly();
|
||||||
|
|
||||||
if (file.exists() &&!file.delete())
|
if (file.exists() &&!file.delete())
|
||||||
{
|
{
|
||||||
throw new StoreException(
|
throw new StoreException(
|
||||||
@@ -185,6 +188,12 @@ public abstract class FileBasedStore<T> implements MultiEntryStore<T>
|
|||||||
return name.substring(0, name.length() - suffix.length());
|
return name.substring(0, name.length() - suffix.length());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void assertNotReadOnly() {
|
||||||
|
if (readOnly) {
|
||||||
|
throw new StoreReadOnlyException(directory.getAbsoluteFile().toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//~--- fields ---------------------------------------------------------------
|
//~--- fields ---------------------------------------------------------------
|
||||||
|
|
||||||
/** Field description */
|
/** Field description */
|
||||||
@@ -192,4 +201,6 @@ public abstract class FileBasedStore<T> implements MultiEntryStore<T>
|
|||||||
|
|
||||||
/** Field description */
|
/** Field description */
|
||||||
private final String suffix;
|
private final String suffix;
|
||||||
|
|
||||||
|
private final boolean readOnly;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ package sonia.scm.store;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import sonia.scm.SCMContextProvider;
|
import sonia.scm.SCMContextProvider;
|
||||||
|
import sonia.scm.repository.RepositoryArchivedCheck;
|
||||||
import sonia.scm.repository.RepositoryLocationResolver;
|
import sonia.scm.repository.RepositoryLocationResolver;
|
||||||
import sonia.scm.util.IOUtil;
|
import sonia.scm.util.IOUtil;
|
||||||
|
|
||||||
@@ -48,14 +49,16 @@ public abstract class FileBasedStoreFactory {
|
|||||||
* the logger for FileBasedStoreFactory
|
* the logger for FileBasedStoreFactory
|
||||||
*/
|
*/
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(FileBasedStoreFactory.class);
|
private static final Logger LOG = LoggerFactory.getLogger(FileBasedStoreFactory.class);
|
||||||
private SCMContextProvider contextProvider;
|
private final SCMContextProvider contextProvider;
|
||||||
private RepositoryLocationResolver repositoryLocationResolver;
|
private final RepositoryLocationResolver repositoryLocationResolver;
|
||||||
private Store store;
|
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.contextProvider = contextProvider;
|
||||||
this.repositoryLocationResolver = repositoryLocationResolver;
|
this.repositoryLocationResolver = repositoryLocationResolver;
|
||||||
this.store = store;
|
this.store = store;
|
||||||
|
this.archivedCheck = archivedCheck;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected File getStoreLocation(StoreParameters storeParameters) {
|
protected File getStoreLocation(StoreParameters storeParameters) {
|
||||||
@@ -79,6 +82,10 @@ public abstract class FileBasedStoreFactory {
|
|||||||
return new File(storeDirectory, name);
|
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
|
* Get the store directory of a specific repository
|
||||||
* @param store the type of the store
|
* @param store the type of the store
|
||||||
|
|||||||
@@ -58,8 +58,8 @@ public class FileBlobStore extends FileBasedStore<Blob> implements BlobStore {
|
|||||||
|
|
||||||
private final KeyGenerator keyGenerator;
|
private final KeyGenerator keyGenerator;
|
||||||
|
|
||||||
FileBlobStore(KeyGenerator keyGenerator, File directory) {
|
FileBlobStore(KeyGenerator keyGenerator, File directory, boolean readOnly) {
|
||||||
super(directory, SUFFIX);
|
super(directory, SUFFIX, readOnly);
|
||||||
this.keyGenerator = keyGenerator;
|
this.keyGenerator = keyGenerator;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,6 +74,8 @@ public class FileBlobStore extends FileBasedStore<Blob> implements BlobStore {
|
|||||||
"id argument is required");
|
"id argument is required");
|
||||||
LOG.debug("create new blob with id {}", id);
|
LOG.debug("create new blob with id {}", id);
|
||||||
|
|
||||||
|
assertNotReadOnly();
|
||||||
|
|
||||||
File file = getFile(id);
|
File file = getFile(id);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -94,6 +96,7 @@ public class FileBlobStore extends FileBasedStore<Blob> implements BlobStore {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void remove(Blob blob) {
|
public void remove(Blob blob) {
|
||||||
|
assertNotReadOnly();
|
||||||
Preconditions.checkNotNull(blob, "blob argument is required");
|
Preconditions.checkNotNull(blob, "blob argument is required");
|
||||||
remove(blob.getId());
|
remove(blob.getId());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import com.google.inject.Singleton;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import sonia.scm.SCMContextProvider;
|
import sonia.scm.SCMContextProvider;
|
||||||
|
import sonia.scm.repository.RepositoryArchivedCheck;
|
||||||
import sonia.scm.repository.RepositoryLocationResolver;
|
import sonia.scm.repository.RepositoryLocationResolver;
|
||||||
import sonia.scm.security.KeyGenerator;
|
import sonia.scm.security.KeyGenerator;
|
||||||
import sonia.scm.util.IOUtil;
|
import sonia.scm.util.IOUtil;
|
||||||
@@ -59,8 +60,8 @@ public class FileBlobStoreFactory extends FileBasedStoreFactory implements BlobS
|
|||||||
* @param keyGenerator key generator
|
* @param keyGenerator key generator
|
||||||
*/
|
*/
|
||||||
@Inject
|
@Inject
|
||||||
public FileBlobStoreFactory(SCMContextProvider contextProvider , RepositoryLocationResolver repositoryLocationResolver, KeyGenerator keyGenerator) {
|
public FileBlobStoreFactory(SCMContextProvider contextProvider , RepositoryLocationResolver repositoryLocationResolver, KeyGenerator keyGenerator, RepositoryArchivedCheck archivedCheck) {
|
||||||
super(contextProvider, repositoryLocationResolver, Store.BLOB);
|
super(contextProvider, repositoryLocationResolver, Store.BLOB, archivedCheck);
|
||||||
this.keyGenerator = keyGenerator;
|
this.keyGenerator = keyGenerator;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,8 +70,6 @@ public class FileBlobStoreFactory extends FileBasedStoreFactory implements BlobS
|
|||||||
public BlobStore getStore(StoreParameters storeParameters) {
|
public BlobStore getStore(StoreParameters storeParameters) {
|
||||||
File storeLocation = getStoreLocation(storeParameters);
|
File storeLocation = getStoreLocation(storeParameters);
|
||||||
IOUtil.mkdirs(storeLocation);
|
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.Inject;
|
||||||
import com.google.inject.Singleton;
|
import com.google.inject.Singleton;
|
||||||
import sonia.scm.SCMContextProvider;
|
import sonia.scm.SCMContextProvider;
|
||||||
|
import sonia.scm.repository.RepositoryArchivedCheck;
|
||||||
import sonia.scm.repository.RepositoryLocationResolver;
|
import sonia.scm.repository.RepositoryLocationResolver;
|
||||||
import sonia.scm.security.KeyGenerator;
|
import sonia.scm.security.KeyGenerator;
|
||||||
|
|
||||||
@@ -45,8 +46,8 @@ public class JAXBConfigurationEntryStoreFactory extends FileBasedStoreFactory
|
|||||||
private KeyGenerator keyGenerator;
|
private KeyGenerator keyGenerator;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public JAXBConfigurationEntryStoreFactory(SCMContextProvider contextProvider , RepositoryLocationResolver repositoryLocationResolver, KeyGenerator keyGenerator) {
|
public JAXBConfigurationEntryStoreFactory(SCMContextProvider contextProvider, RepositoryLocationResolver repositoryLocationResolver, KeyGenerator keyGenerator, RepositoryArchivedCheck archivedCheck) {
|
||||||
super(contextProvider, repositoryLocationResolver, Store.CONFIG);
|
super(contextProvider, repositoryLocationResolver, Store.CONFIG, archivedCheck);
|
||||||
this.keyGenerator = keyGenerator;
|
this.keyGenerator = keyGenerator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,8 @@ public class JAXBConfigurationStore<T> extends AbstractStore<T> {
|
|||||||
private final Class<T> type;
|
private final Class<T> type;
|
||||||
private final File configFile;
|
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.context = context;
|
||||||
this.type = type;
|
this.type = type;
|
||||||
this.configFile = configFile;
|
this.configFile = configFile;
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ package sonia.scm.store;
|
|||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import com.google.inject.Singleton;
|
import com.google.inject.Singleton;
|
||||||
import sonia.scm.SCMContextProvider;
|
import sonia.scm.SCMContextProvider;
|
||||||
|
import sonia.scm.repository.RepositoryArchivedCheck;
|
||||||
import sonia.scm.repository.RepositoryLocationResolver;
|
import sonia.scm.repository.RepositoryLocationResolver;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -43,8 +44,8 @@ public class JAXBConfigurationStoreFactory extends FileBasedStoreFactory impleme
|
|||||||
* @param repositoryLocationResolver Resolver to get the repository Directory
|
* @param repositoryLocationResolver Resolver to get the repository Directory
|
||||||
*/
|
*/
|
||||||
@Inject
|
@Inject
|
||||||
public JAXBConfigurationStoreFactory(SCMContextProvider contextProvider, RepositoryLocationResolver repositoryLocationResolver) {
|
public JAXBConfigurationStoreFactory(SCMContextProvider contextProvider, RepositoryLocationResolver repositoryLocationResolver, RepositoryArchivedCheck archivedCheck) {
|
||||||
super(contextProvider, repositoryLocationResolver, Store.CONFIG);
|
super(contextProvider, repositoryLocationResolver, Store.CONFIG, archivedCheck);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -55,7 +56,8 @@ public class JAXBConfigurationStoreFactory extends FileBasedStoreFactory impleme
|
|||||||
storeParameters.getType(),
|
storeParameters.getType(),
|
||||||
getStoreLocation(storeParameters.getName().concat(StoreConstants.FILE_EXTENSION),
|
getStoreLocation(storeParameters.getName().concat(StoreConstants.FILE_EXTENSION),
|
||||||
storeParameters.getType(),
|
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 KeyGenerator keyGenerator;
|
||||||
private final TypedStoreContext<T> context;
|
private final TypedStoreContext<T> context;
|
||||||
|
|
||||||
JAXBDataStore(KeyGenerator keyGenerator, TypedStoreContext<T> context, File directory) {
|
JAXBDataStore(KeyGenerator keyGenerator, TypedStoreContext<T> context, File directory, boolean readOnly) {
|
||||||
super(directory, StoreConstants.FILE_EXTENSION);
|
super(directory, StoreConstants.FILE_EXTENSION, readOnly);
|
||||||
this.keyGenerator = keyGenerator;
|
this.keyGenerator = keyGenerator;
|
||||||
this.directory = directory;
|
this.directory = directory;
|
||||||
this.context = context;
|
this.context = context;
|
||||||
@@ -63,6 +63,8 @@ public class JAXBDataStore<T> extends FileBasedStore<T> implements DataStore<T>
|
|||||||
public void put(String id, T item) {
|
public void put(String id, T item) {
|
||||||
LOG.debug("put item {} to store", id);
|
LOG.debug("put item {} to store", id);
|
||||||
|
|
||||||
|
assertNotReadOnly();
|
||||||
|
|
||||||
File file = getFile(id);
|
File file = getFile(id);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ package sonia.scm.store;
|
|||||||
|
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import com.google.inject.Singleton;
|
import com.google.inject.Singleton;
|
||||||
|
|
||||||
import sonia.scm.SCMContextProvider;
|
import sonia.scm.SCMContextProvider;
|
||||||
|
import sonia.scm.repository.RepositoryArchivedCheck;
|
||||||
import sonia.scm.repository.RepositoryLocationResolver;
|
import sonia.scm.repository.RepositoryLocationResolver;
|
||||||
import sonia.scm.security.KeyGenerator;
|
import sonia.scm.security.KeyGenerator;
|
||||||
import sonia.scm.util.IOUtil;
|
import sonia.scm.util.IOUtil;
|
||||||
@@ -44,11 +44,11 @@ import java.io.File;
|
|||||||
public class JAXBDataStoreFactory extends FileBasedStoreFactory
|
public class JAXBDataStoreFactory extends FileBasedStoreFactory
|
||||||
implements DataStoreFactory {
|
implements DataStoreFactory {
|
||||||
|
|
||||||
private KeyGenerator keyGenerator;
|
private final KeyGenerator keyGenerator;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public JAXBDataStoreFactory(SCMContextProvider contextProvider , RepositoryLocationResolver repositoryLocationResolver, KeyGenerator keyGenerator) {
|
public JAXBDataStoreFactory(SCMContextProvider contextProvider , RepositoryLocationResolver repositoryLocationResolver, KeyGenerator keyGenerator, RepositoryArchivedCheck archivedCheck) {
|
||||||
super(contextProvider, repositoryLocationResolver, Store.DATA);
|
super(contextProvider, repositoryLocationResolver, Store.DATA, archivedCheck);
|
||||||
this.keyGenerator = keyGenerator;
|
this.keyGenerator = keyGenerator;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,6 +56,6 @@ public class JAXBDataStoreFactory extends FileBasedStoreFactory
|
|||||||
public <T> DataStore<T> getStore(TypedStoreParameters<T> storeParameters) {
|
public <T> DataStore<T> getStore(TypedStoreParameters<T> storeParameters) {
|
||||||
File storeLocation = getStoreLocation(storeParameters);
|
File storeLocation = getStoreLocation(storeParameters);
|
||||||
IOUtil.mkdirs(storeLocation);
|
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.Repository;
|
||||||
import sonia.scm.repository.RepositoryLocationResolver;
|
import sonia.scm.repository.RepositoryLocationResolver;
|
||||||
import sonia.scm.repository.RepositoryPermission;
|
import sonia.scm.repository.RepositoryPermission;
|
||||||
|
import sonia.scm.store.StoreReadOnlyException;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
@@ -55,6 +56,7 @@ import java.util.function.Consumer;
|
|||||||
import static java.util.Arrays.asList;
|
import static java.util.Arrays.asList;
|
||||||
import static java.util.Collections.singletonList;
|
import static java.util.Collections.singletonList;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
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.junit.jupiter.api.Assertions.fail;
|
||||||
import static org.mockito.ArgumentMatchers.anyString;
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.never;
|
||||||
@@ -233,6 +235,16 @@ class XmlRepositoryDAOTest {
|
|||||||
assertThat(dao.get("42").getDescription()).isEqualTo("Heart of Gold");
|
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
|
@Test
|
||||||
void shouldRemoveRepository() {
|
void shouldRemoveRepository() {
|
||||||
dao.add(REPOSITORY);
|
dao.add(REPOSITORY);
|
||||||
@@ -247,6 +259,15 @@ class XmlRepositoryDAOTest {
|
|||||||
assertThat(storePath).doesNotExist();
|
assertThat(storePath).doesNotExist();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotRemoveArchivedRepository() {
|
||||||
|
REPOSITORY.setArchived(true);
|
||||||
|
dao.add(REPOSITORY);
|
||||||
|
assertThat(dao.contains("42")).isTrue();
|
||||||
|
|
||||||
|
assertThrows(StoreReadOnlyException.class, () -> dao.delete(REPOSITORY));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldRenameTheRepository() {
|
void shouldRenameTheRepository() {
|
||||||
dao.add(REPOSITORY);
|
dao.add(REPOSITORY);
|
||||||
|
|||||||
@@ -24,51 +24,209 @@
|
|||||||
|
|
||||||
package sonia.scm.store;
|
package sonia.scm.store;
|
||||||
|
|
||||||
//~--- non-JDK imports --------------------------------------------------------
|
import com.google.common.io.ByteStreams;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.Test;
|
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.Repository;
|
||||||
|
import sonia.scm.repository.RepositoryArchivedCheck;
|
||||||
|
import sonia.scm.repository.RepositoryTestData;
|
||||||
import sonia.scm.security.UUIDKeyGenerator;
|
import sonia.scm.security.UUIDKeyGenerator;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
import java.util.List;
|
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.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;
|
||||||
|
|
||||||
/**
|
class FileBlobStoreTest extends AbstractTestBase
|
||||||
*
|
|
||||||
* @author Sebastian Sdorra
|
|
||||||
*/
|
|
||||||
public class FileBlobStoreTest extends BlobStoreTestBase
|
|
||||||
{
|
{
|
||||||
|
|
||||||
/**
|
private Repository repository = RepositoryTestData.createHeartOfGold();
|
||||||
* Method description
|
private RepositoryArchivedCheck archivedCheck = mock(RepositoryArchivedCheck.class);
|
||||||
*
|
private BlobStore store;
|
||||||
*
|
|
||||||
* @return
|
@BeforeEach
|
||||||
*/
|
void createBlobStore()
|
||||||
@Override
|
|
||||||
protected BlobStoreFactory createBlobStoreFactory()
|
|
||||||
{
|
{
|
||||||
return new FileBlobStoreFactory(contextProvider, repositoryLocationResolver, new UUIDKeyGenerator());
|
store = createBlobStoreFactory()
|
||||||
|
.withName("test")
|
||||||
|
.forRepository(repository)
|
||||||
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@SuppressWarnings("unchecked")
|
void testClear()
|
||||||
public void shouldStoreAndLoadInRepository() {
|
{
|
||||||
BlobStore store = createBlobStoreFactory()
|
store.create("1");
|
||||||
.withName("test")
|
store.create("2");
|
||||||
.forRepository(new Repository("id", "git", "ns", "n"))
|
store.create("3");
|
||||||
.build();
|
|
||||||
|
|
||||||
Blob createdBlob = store.create("abc");
|
assertNotNull(store.get("1"));
|
||||||
List<Blob> storedBlobs = store.getAll();
|
assertNotNull(store.get("2"));
|
||||||
|
assertNotNull(store.get("3"));
|
||||||
|
|
||||||
assertNotNull(createdBlob);
|
store.clear();
|
||||||
assertThat(storedBlobs)
|
|
||||||
.isNotNull()
|
assertNull(store.get("1"));
|
||||||
.hasSize(1)
|
assertNull(store.get("2"));
|
||||||
.usingElementComparatorOnFields("id").containsExactly(createdBlob);
|
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
|
@Override
|
||||||
protected ConfigurationEntryStoreFactory createConfigurationStoreFactory()
|
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 org.junit.Test;
|
||||||
import sonia.scm.repository.Repository;
|
import sonia.scm.repository.Repository;
|
||||||
|
import sonia.scm.repository.RepositoryArchivedCheck;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertNotNull;
|
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}.
|
* Unit tests for {@link JAXBConfigurationStore}.
|
||||||
@@ -37,10 +41,12 @@ import static org.junit.Assert.assertNotNull;
|
|||||||
*/
|
*/
|
||||||
public class JAXBConfigurationStoreTest extends StoreTestBase {
|
public class JAXBConfigurationStoreTest extends StoreTestBase {
|
||||||
|
|
||||||
|
private final RepositoryArchivedCheck archivedCheck = mock(RepositoryArchivedCheck.class);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected ConfigurationStoreFactory createStoreFactory()
|
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")
|
@SuppressWarnings("unchecked")
|
||||||
public void shouldStoreAndLoadInRepository()
|
public void shouldStoreAndLoadInRepository()
|
||||||
{
|
{
|
||||||
|
Repository repository = new Repository("id", "git", "ns", "n");
|
||||||
ConfigurationStore<StoreObject> store = createStoreFactory()
|
ConfigurationStore<StoreObject> store = createStoreFactory()
|
||||||
.withType(StoreObject.class)
|
.withType(StoreObject.class)
|
||||||
.withName("test")
|
.withName("test")
|
||||||
.forRepository(new Repository("id", "git", "ns", "n"))
|
.forRepository(repository)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
store.set(new StoreObject("value"));
|
store.set(new StoreObject("value"));
|
||||||
@@ -60,4 +67,20 @@ public class JAXBConfigurationStoreTest extends StoreTestBase {
|
|||||||
assertNotNull(storeObject);
|
assertNotNull(storeObject);
|
||||||
assertEquals("value", storeObject.getValue());
|
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 org.junit.Test;
|
||||||
import sonia.scm.repository.Repository;
|
import sonia.scm.repository.Repository;
|
||||||
|
import sonia.scm.repository.RepositoryArchivedCheck;
|
||||||
import sonia.scm.security.UUIDKeyGenerator;
|
import sonia.scm.security.UUIDKeyGenerator;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertNotNull;
|
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 {
|
public class JAXBDataStoreTest extends DataStoreTestBase {
|
||||||
|
|
||||||
/**
|
private final RepositoryArchivedCheck archivedCheck = mock(RepositoryArchivedCheck.class);
|
||||||
* Method description
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@Override
|
@Override
|
||||||
protected DataStoreFactory createDataStoreFactory()
|
protected DataStoreFactory createDataStoreFactory()
|
||||||
{
|
{
|
||||||
return new JAXBDataStoreFactory(contextProvider, repositoryLocationResolver, new UUIDKeyGenerator());
|
return new JAXBDataStoreFactory(contextProvider, repositoryLocationResolver, new UUIDKeyGenerator(), archivedCheck);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -77,4 +76,11 @@ public class JAXBDataStoreTest extends DataStoreTestBase {
|
|||||||
assertNotNull(storeObject);
|
assertNotNull(storeObject);
|
||||||
assertEquals("abc_value", storeObject.getValue());
|
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 org.junit.jupiter.api.io.TempDir;
|
||||||
import sonia.scm.SCMContextProvider;
|
import sonia.scm.SCMContextProvider;
|
||||||
import sonia.scm.Stage;
|
import sonia.scm.Stage;
|
||||||
|
import sonia.scm.repository.RepositoryArchivedCheck;
|
||||||
import sonia.scm.security.KeyGenerator;
|
import sonia.scm.security.KeyGenerator;
|
||||||
import sonia.scm.store.JAXBConfigurationEntryStoreFactory;
|
import sonia.scm.store.JAXBConfigurationEntryStoreFactory;
|
||||||
import sonia.scm.update.RepositoryV1PropertyReader;
|
import sonia.scm.update.RepositoryV1PropertyReader;
|
||||||
@@ -38,6 +39,8 @@ import java.io.IOException;
|
|||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
|
|
||||||
class XmlV1PropertyDAOTest {
|
class XmlV1PropertyDAOTest {
|
||||||
|
|
||||||
@@ -108,7 +111,8 @@ class XmlV1PropertyDAOTest {
|
|||||||
Files.createDirectories(configPath);
|
Files.createDirectories(configPath);
|
||||||
Path propFile = configPath.resolve("repository-properties-v1.xml");
|
Path propFile = configPath.resolve("repository-properties-v1.xml");
|
||||||
Files.write(propFile, PROPERTIES.getBytes());
|
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())
|
dao.getProperties(new RepositoryV1PropertyReader())
|
||||||
.forEachEntry((key, prop) -> {
|
.forEachEntry((key, prop) -> {
|
||||||
|
|||||||
@@ -59,6 +59,12 @@
|
|||||||
<scope>compile</scope>
|
<scope>compile</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter-api</artifactId>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.github.sdorra</groupId>
|
<groupId>com.github.sdorra</groupId>
|
||||||
<artifactId>shiro-unit</artifactId>
|
<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.subject.support.SubjectThreadState;
|
||||||
import org.apache.shiro.util.LifecycleUtils;
|
import org.apache.shiro.util.LifecycleUtils;
|
||||||
import org.apache.shiro.util.ThreadState;
|
import org.apache.shiro.util.ThreadState;
|
||||||
|
|
||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
import org.junit.AfterClass;
|
import org.junit.AfterClass;
|
||||||
import org.junit.Before;
|
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.io.DefaultFileSystem;
|
||||||
import sonia.scm.repository.InitialRepositoryLocationResolver;
|
import sonia.scm.repository.InitialRepositoryLocationResolver;
|
||||||
import sonia.scm.repository.RepositoryDAO;
|
import sonia.scm.repository.RepositoryDAO;
|
||||||
@@ -44,17 +45,14 @@ import sonia.scm.repository.RepositoryLocationResolver;
|
|||||||
import sonia.scm.util.IOUtil;
|
import sonia.scm.util.IOUtil;
|
||||||
import sonia.scm.util.MockUtil;
|
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.File;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @author Sebastian Sdorra
|
* @author Sebastian Sdorra
|
||||||
@@ -73,6 +71,7 @@ public class AbstractTestBase
|
|||||||
protected RepositoryDAO repositoryDAO = mock(RepositoryDAO.class);
|
protected RepositoryDAO repositoryDAO = mock(RepositoryDAO.class);
|
||||||
protected RepositoryLocationResolver repositoryLocationResolver;
|
protected RepositoryLocationResolver repositoryLocationResolver;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
@Before
|
@Before
|
||||||
public void setUpTest() throws Exception
|
public void setUpTest() throws Exception
|
||||||
{
|
{
|
||||||
@@ -90,6 +89,7 @@ public class AbstractTestBase
|
|||||||
* Method description
|
* Method description
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
@AfterAll
|
||||||
@AfterClass
|
@AfterClass
|
||||||
public static void tearDownShiro()
|
public static void tearDownShiro()
|
||||||
{
|
{
|
||||||
@@ -162,6 +162,7 @@ public class AbstractTestBase
|
|||||||
*
|
*
|
||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
|
@AfterEach
|
||||||
@After
|
@After
|
||||||
public void tearDownTest() throws Exception
|
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>
|
</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`] = `
|
exports[`Storyshots RepositoryEntry Avatar EP 1`] = `
|
||||||
<div
|
<div
|
||||||
className="RepositoryEntrystories__Spacing-toppdg-0 iIzVNZ box box-link-shadow"
|
className="RepositoryEntrystories__Spacing-toppdg-0 iIzVNZ box box-link-shadow"
|
||||||
@@ -50545,6 +50701,7 @@ exports[`Storyshots RepositoryEntry Avatar EP 1`] = `
|
|||||||
<strong>
|
<strong>
|
||||||
heartOfGold
|
heartOfGold
|
||||||
</strong>
|
</strong>
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
<p
|
<p
|
||||||
className="shorten-text"
|
className="shorten-text"
|
||||||
@@ -50693,6 +50850,7 @@ exports[`Storyshots RepositoryEntry Before Title EP 1`] = `
|
|||||||
<strong>
|
<strong>
|
||||||
heartOfGold
|
heartOfGold
|
||||||
</strong>
|
</strong>
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
<p
|
<p
|
||||||
className="shorten-text"
|
className="shorten-text"
|
||||||
@@ -50838,6 +50996,7 @@ exports[`Storyshots RepositoryEntry Default 1`] = `
|
|||||||
<strong>
|
<strong>
|
||||||
heartOfGold
|
heartOfGold
|
||||||
</strong>
|
</strong>
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
<p
|
<p
|
||||||
className="shorten-text"
|
className="shorten-text"
|
||||||
@@ -50983,6 +51142,7 @@ exports[`Storyshots RepositoryEntry Quick Link EP 1`] = `
|
|||||||
<strong>
|
<strong>
|
||||||
heartOfGold
|
heartOfGold
|
||||||
</strong>
|
</strong>
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
<p
|
<p
|
||||||
className="shorten-text"
|
className="shorten-text"
|
||||||
|
|||||||
@@ -74,9 +74,11 @@ const QuickLink = (
|
|||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const archivedRepository = { ...repository, archived: true };
|
||||||
|
|
||||||
storiesOf("RepositoryEntry", module)
|
storiesOf("RepositoryEntry", module)
|
||||||
.addDecorator(story => <MemoryRouter initialEntries={["/"]}>{story()}</MemoryRouter>)
|
.addDecorator((story) => <MemoryRouter initialEntries={["/"]}>{story()}</MemoryRouter>)
|
||||||
.addDecorator(storyFn => <Container>{storyFn()}</Container>)
|
.addDecorator((storyFn) => <Container>{storyFn()}</Container>)
|
||||||
.add("Default", () => {
|
.add("Default", () => {
|
||||||
return <RepositoryEntry repository={repository} baseDate={baseDate} />;
|
return <RepositoryEntry repository={repository} baseDate={baseDate} />;
|
||||||
})
|
})
|
||||||
@@ -94,4 +96,9 @@ storiesOf("RepositoryEntry", module)
|
|||||||
const binder = new Binder("title");
|
const binder = new Binder("title");
|
||||||
bindQuickLink(binder, QuickLink);
|
bindQuickLink(binder, QuickLink);
|
||||||
return withBinder(binder, repository);
|
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 RepositoryAvatar from "./RepositoryAvatar";
|
||||||
import { ExtensionPoint } from "@scm-manager/ui-extensions";
|
import { ExtensionPoint } from "@scm-manager/ui-extensions";
|
||||||
import { withTranslation, WithTranslation } from "react-i18next";
|
import { withTranslation, WithTranslation } from "react-i18next";
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
type DateProp = Date | string;
|
type DateProp = Date | string;
|
||||||
|
|
||||||
@@ -38,6 +39,18 @@ type Props = WithTranslation & {
|
|||||||
baseDate?: DateProp;
|
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> {
|
class RepositoryEntry extends React.Component<Props> {
|
||||||
createLink = (repository: Repository) => {
|
createLink = (repository: Repository) => {
|
||||||
return `/repo/${repository.namespace}/${repository.name}`;
|
return `/repo/${repository.namespace}/${repository.name}`;
|
||||||
@@ -131,10 +144,14 @@ class RepositoryEntry extends React.Component<Props> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
createTitle = () => {
|
createTitle = () => {
|
||||||
const { repository } = this.props;
|
const { repository, t } = this.props;
|
||||||
|
const archivedFlag = repository.archived && (
|
||||||
|
<ArchiveTag title={t("archive.tooltip")}>{t("repository.archived")}</ArchiveTag>
|
||||||
|
);
|
||||||
return (
|
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;
|
description?: string;
|
||||||
creationDate?: string;
|
creationDate?: string;
|
||||||
lastModified?: string;
|
lastModified?: string;
|
||||||
|
archived?: boolean;
|
||||||
_links: Links;
|
_links: Links;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,8 @@
|
|||||||
"contact": "Kontakt",
|
"contact": "Kontakt",
|
||||||
"description": "Beschreibung",
|
"description": "Beschreibung",
|
||||||
"creationDate": "Erstellt",
|
"creationDate": "Erstellt",
|
||||||
"lastModified": "Zuletzt bearbeitet"
|
"lastModified": "Zuletzt bearbeitet",
|
||||||
|
"archived": "archiviert"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
"namespace-invalid": "Der Namespace des Repository ist ungültig",
|
"namespace-invalid": "Der Namespace des Repository ist ungültig",
|
||||||
@@ -237,7 +238,7 @@
|
|||||||
"submitCreate": "Speichern",
|
"submitCreate": "Speichern",
|
||||||
"submitImport": "Importieren",
|
"submitImport": "Importieren",
|
||||||
"initializeRepository": "Repository initiieren",
|
"initializeRepository": "Repository initiieren",
|
||||||
"dangerZone": "Umbenennen und Löschen",
|
"dangerZone": "Umbenennen, Archivieren und Löschen",
|
||||||
"createButton": "Neues Repository erstellen",
|
"createButton": "Neues Repository erstellen",
|
||||||
"importButton": "Repository importieren"
|
"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": {
|
"diff": {
|
||||||
"jumpToSource": "Zur Quelldatei springen",
|
"jumpToSource": "Zur Quelldatei springen",
|
||||||
"jumpToTarget": "Zur vorherigen Version der Datei springen",
|
"jumpToTarget": "Zur vorherigen Version der Datei springen",
|
||||||
|
|||||||
@@ -6,7 +6,8 @@
|
|||||||
"contact": "Contact",
|
"contact": "Contact",
|
||||||
"description": "Description",
|
"description": "Description",
|
||||||
"creationDate": "Creation Date",
|
"creationDate": "Creation Date",
|
||||||
"lastModified": "Last Modified"
|
"lastModified": "Last Modified",
|
||||||
|
"archived": "archived"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
"namespace-invalid": "The repository namespace is invalid",
|
"namespace-invalid": "The repository namespace is invalid",
|
||||||
@@ -238,7 +239,7 @@
|
|||||||
"submitCreate": "Save",
|
"submitCreate": "Save",
|
||||||
"submitImport": "Import",
|
"submitImport": "Import",
|
||||||
"initializeRepository": "Initialize repository",
|
"initializeRepository": "Initialize repository",
|
||||||
"dangerZone": "Rename and delete",
|
"dangerZone": "Rename, archive and delete",
|
||||||
"createButton": "Create Repository",
|
"createButton": "Create Repository",
|
||||||
"importButton": "Import 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": {
|
"diff": {
|
||||||
"changes": {
|
"changes": {
|
||||||
"add": "added",
|
"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 styled from "styled-components";
|
||||||
import { Subtitle } from "@scm-manager/ui-components";
|
import { Subtitle } from "@scm-manager/ui-components";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import ArchiveRepo from "./ArchiveRepo";
|
||||||
|
import UnarchiveRepo from "./UnarchiveRepo";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
repository: Repository;
|
repository: Repository;
|
||||||
@@ -67,6 +69,14 @@ const RepositoryDangerZone: FC<Props> = ({ repository, indexLinks }) => {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
dangerZone.push(<DeleteRepo repository={repository} />);
|
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) {
|
if (dangerZone.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ import { Changeset, Repository } from "@scm-manager/ui-types";
|
|||||||
import {
|
import {
|
||||||
CustomQueryFlexWrappedColumns,
|
CustomQueryFlexWrappedColumns,
|
||||||
ErrorPage,
|
ErrorPage,
|
||||||
|
FileControlFactory,
|
||||||
|
JumpToFileButton,
|
||||||
Loading,
|
Loading,
|
||||||
NavLink,
|
NavLink,
|
||||||
Page,
|
Page,
|
||||||
@@ -37,7 +39,9 @@ import {
|
|||||||
SecondaryNavigation,
|
SecondaryNavigation,
|
||||||
SecondaryNavigationColumn,
|
SecondaryNavigationColumn,
|
||||||
StateMenuContextProvider,
|
StateMenuContextProvider,
|
||||||
SubNavigation
|
SubNavigation,
|
||||||
|
Tooltip,
|
||||||
|
urls,
|
||||||
} from "@scm-manager/ui-components";
|
} from "@scm-manager/ui-components";
|
||||||
import { fetchRepoByName, getFetchRepoFailure, getRepository, isFetchRepoPending } from "../modules/repos";
|
import { fetchRepoByName, getFetchRepoFailure, getRepository, isFetchRepoPending } from "../modules/repos";
|
||||||
import RepositoryDetails from "../components/RepositoryDetails";
|
import RepositoryDetails from "../components/RepositoryDetails";
|
||||||
@@ -53,10 +57,9 @@ import { getLinks, getRepositoriesLink } from "../../modules/indexResource";
|
|||||||
import CodeOverview from "../codeSection/containers/CodeOverview";
|
import CodeOverview from "../codeSection/containers/CodeOverview";
|
||||||
import ChangesetView from "./ChangesetView";
|
import ChangesetView from "./ChangesetView";
|
||||||
import SourceExtensions from "../sources/containers/SourceExtensions";
|
import SourceExtensions from "../sources/containers/SourceExtensions";
|
||||||
import { FileControlFactory, JumpToFileButton } from "@scm-manager/ui-components";
|
|
||||||
import TagsOverview from "../tags/container/TagsOverview";
|
import TagsOverview from "../tags/container/TagsOverview";
|
||||||
import TagRoot from "../tags/container/TagRoot";
|
import TagRoot from "../tags/container/TagRoot";
|
||||||
import { urls } from "@scm-manager/ui-components";
|
import styled from "styled-components";
|
||||||
|
|
||||||
type Props = RouteComponentProps &
|
type Props = RouteComponentProps &
|
||||||
WithTranslation & {
|
WithTranslation & {
|
||||||
@@ -72,6 +75,15 @@ type Props = RouteComponentProps &
|
|||||||
fetchRepoByName: (link: string, namespace: string, name: string) => void;
|
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> {
|
class RepositoryRoot extends React.Component<Props> {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { fetchRepoByName, namespace, name, repoLink } = this.props;
|
const { fetchRepoByName, namespace, name, repoLink } = this.props;
|
||||||
@@ -141,7 +153,7 @@ class RepositoryRoot extends React.Component<Props> {
|
|||||||
const extensionProps = {
|
const extensionProps = {
|
||||||
repository,
|
repository,
|
||||||
url,
|
url,
|
||||||
indexLinks
|
indexLinks,
|
||||||
};
|
};
|
||||||
|
|
||||||
const redirectUrlFactory = binder.getExtension("repository.redirect", this.props);
|
const redirectUrlFactory = binder.getExtension("repository.redirect", this.props);
|
||||||
@@ -152,15 +164,16 @@ class RepositoryRoot extends React.Component<Props> {
|
|||||||
redirectedUrl = url + "/info";
|
redirectedUrl = url + "/info";
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileControlFactoryFactory: (changeset: Changeset) => FileControlFactory = changeset => file => {
|
const fileControlFactoryFactory: (changeset: Changeset) => FileControlFactory = (changeset) => (file) => {
|
||||||
const baseUrl = `${url}/code/sources`;
|
const baseUrl = `${url}/code/sources`;
|
||||||
const sourceLink = file.newPath && {
|
const sourceLink = file.newPath && {
|
||||||
url: `${baseUrl}/${changeset.id}/${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}`,
|
url: `${baseUrl}/${changeset._embedded.parents[0].id}/${file.oldPath}`,
|
||||||
label: t("diff.jumpToTarget")
|
label: t("diff.jumpToTarget"),
|
||||||
};
|
};
|
||||||
|
|
||||||
const links = [];
|
const links = [];
|
||||||
@@ -186,6 +199,12 @@ class RepositoryRoot extends React.Component<Props> {
|
|||||||
return links ? links.map(({ url, label }) => <JumpToFileButton tooltip={label} link={url} />) : null;
|
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 = (
|
const titleComponent = (
|
||||||
<>
|
<>
|
||||||
<Link to={`/repos/${repository.namespace}/`} className={"has-text-dark"}>
|
<Link to={`/repos/${repository.namespace}/`} className={"has-text-dark"}>
|
||||||
@@ -200,7 +219,12 @@ class RepositoryRoot extends React.Component<Props> {
|
|||||||
<Page
|
<Page
|
||||||
title={titleComponent}
|
title={titleComponent}
|
||||||
documentTitle={`${repository.namespace}/${repository.name}`}
|
documentTitle={`${repository.namespace}/${repository.name}`}
|
||||||
afterTitle={<ExtensionPoint name={"repository.afterTitle"} props={{ repository }} />}
|
afterTitle={
|
||||||
|
<>
|
||||||
|
<ExtensionPoint name={"repository.afterTitle"} props={{ repository }} />
|
||||||
|
{archivedFlag}
|
||||||
|
</>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<CustomQueryFlexWrappedColumns>
|
<CustomQueryFlexWrappedColumns>
|
||||||
<PrimaryContentColumn>
|
<PrimaryContentColumn>
|
||||||
@@ -336,7 +360,7 @@ const mapStateToProps = (state: any, ownProps: Props) => {
|
|||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
repoLink,
|
repoLink,
|
||||||
indexLinks
|
indexLinks,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -344,7 +368,7 @@ const mapDispatchToProps = (dispatch: any) => {
|
|||||||
return {
|
return {
|
||||||
fetchRepoByName: (link: string, namespace: string, name: string) => {
|
fetchRepoByName: (link: string, namespace: string, name: string) => {
|
||||||
dispatch(fetchRepoByName(link, namespace, name));
|
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) {
|
export function fetchNamespace(link: string, namespaceName: string) {
|
||||||
return function(dispatch: any) {
|
return function(dispatch: any) {
|
||||||
dispatch(fetchNamespacePending(namespaceName));
|
dispatch(fetchNamespacePending(namespaceName));
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ public class RepositoryDto extends HalRepresentation implements CreateRepository
|
|||||||
private String name;
|
private String name;
|
||||||
@NotEmpty
|
@NotEmpty
|
||||||
private String type;
|
private String type;
|
||||||
|
private boolean archived;
|
||||||
|
|
||||||
RepositoryDto(Links links, Embedded embedded) {
|
RepositoryDto(Links links, Embedded embedded) {
|
||||||
super(links, embedded);
|
super(links, embedded);
|
||||||
|
|||||||
@@ -215,10 +215,60 @@ public class RepositoryResource {
|
|||||||
schema = @Schema(implementation = ErrorDto.class)
|
schema = @Schema(implementation = ErrorDto.class)
|
||||||
))
|
))
|
||||||
@ApiResponse(responseCode = "500", description = "internal server error")
|
@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();
|
Repository repository = loadBy(namespace, name).get();
|
||||||
manager.rename(repository, renameDto.getNamespace(), renameDto.getName());
|
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) {
|
private Repository processUpdate(RepositoryDto repositoryDto, Repository existing) {
|
||||||
|
|||||||
@@ -79,6 +79,13 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper<Reposit
|
|||||||
if (RepositoryPermissions.modify(repository).isPermitted()) {
|
if (RepositoryPermissions.modify(repository).isPermitted()) {
|
||||||
linksBuilder.single(link("update", resourceLinks.repository().update(repository.getNamespace(), repository.getName())));
|
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 (RepositoryPermissions.rename(repository).isPermitted()) {
|
||||||
if (isRenameNamespacePossible()) {
|
if (isRenameNamespacePossible()) {
|
||||||
linksBuilder.single(link("renameWithNamespace", resourceLinks.repository().rename(repository.getNamespace(), repository.getName())));
|
linksBuilder.single(link("renameWithNamespace", resourceLinks.repository().rename(repository.getNamespace(), repository.getName())));
|
||||||
|
|||||||
@@ -370,6 +370,13 @@ class ResourceLinks {
|
|||||||
String importFromBundle(String type) {
|
String importFromBundle(String type) {
|
||||||
return repositoryImportLinkBuilder.method("getRepositoryImportResource").parameters().method("importFromBundle").parameters(type).href();
|
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() {
|
RepositoryCollectionLinks repositoryCollection() {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ package sonia.scm.lifecycle.modules;
|
|||||||
import com.google.inject.AbstractModule;
|
import com.google.inject.AbstractModule;
|
||||||
import com.google.inject.TypeLiteral;
|
import com.google.inject.TypeLiteral;
|
||||||
import com.google.inject.throwingproviders.ThrowingProviderBinder;
|
import com.google.inject.throwingproviders.ThrowingProviderBinder;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import sonia.scm.SCMContext;
|
import sonia.scm.SCMContext;
|
||||||
@@ -36,6 +37,8 @@ import sonia.scm.io.FileSystem;
|
|||||||
import sonia.scm.lifecycle.DefaultRestarter;
|
import sonia.scm.lifecycle.DefaultRestarter;
|
||||||
import sonia.scm.lifecycle.Restarter;
|
import sonia.scm.lifecycle.Restarter;
|
||||||
import sonia.scm.plugin.PluginLoader;
|
import sonia.scm.plugin.PluginLoader;
|
||||||
|
import sonia.scm.repository.EventDrivenRepositoryArchiveCheck;
|
||||||
|
import sonia.scm.repository.RepositoryArchivedCheck;
|
||||||
import sonia.scm.repository.RepositoryLocationResolver;
|
import sonia.scm.repository.RepositoryLocationResolver;
|
||||||
import sonia.scm.repository.xml.MetadataStore;
|
import sonia.scm.repository.xml.MetadataStore;
|
||||||
import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver;
|
import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver;
|
||||||
@@ -93,6 +96,7 @@ public class BootstrapModule extends AbstractModule {
|
|||||||
bind(CipherHandler.class).toInstance(CipherUtil.getInstance().getCipherHandler());
|
bind(CipherHandler.class).toInstance(CipherUtil.getInstance().getCipherHandler());
|
||||||
|
|
||||||
// bind core
|
// bind core
|
||||||
|
bind(RepositoryArchivedCheck.class, EventDrivenRepositoryArchiveCheck.class);
|
||||||
bind(ConfigurationStoreFactory.class, JAXBConfigurationStoreFactory.class);
|
bind(ConfigurationStoreFactory.class, JAXBConfigurationStoreFactory.class);
|
||||||
bind(ConfigurationEntryStoreFactory.class, JAXBConfigurationEntryStoreFactory.class);
|
bind(ConfigurationEntryStoreFactory.class, JAXBConfigurationEntryStoreFactory.class);
|
||||||
bind(DataStoreFactory.class, JAXBDataStoreFactory.class);
|
bind(DataStoreFactory.class, JAXBDataStoreFactory.class);
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ import sonia.scm.repository.HealthCheckContextListener;
|
|||||||
import sonia.scm.repository.NamespaceManager;
|
import sonia.scm.repository.NamespaceManager;
|
||||||
import sonia.scm.repository.NamespaceStrategy;
|
import sonia.scm.repository.NamespaceStrategy;
|
||||||
import sonia.scm.repository.NamespaceStrategyProvider;
|
import sonia.scm.repository.NamespaceStrategyProvider;
|
||||||
|
import sonia.scm.repository.PermissionProvider;
|
||||||
import sonia.scm.repository.Repository;
|
import sonia.scm.repository.Repository;
|
||||||
import sonia.scm.repository.RepositoryDAO;
|
import sonia.scm.repository.RepositoryDAO;
|
||||||
import sonia.scm.repository.RepositoryManager;
|
import sonia.scm.repository.RepositoryManager;
|
||||||
@@ -92,6 +93,7 @@ import sonia.scm.security.ConfigurableLoginAttemptHandler;
|
|||||||
import sonia.scm.security.DefaultAccessTokenCookieIssuer;
|
import sonia.scm.security.DefaultAccessTokenCookieIssuer;
|
||||||
import sonia.scm.security.DefaultSecuritySystem;
|
import sonia.scm.security.DefaultSecuritySystem;
|
||||||
import sonia.scm.security.LoginAttemptHandler;
|
import sonia.scm.security.LoginAttemptHandler;
|
||||||
|
import sonia.scm.security.RepositoryPermissionProvider;
|
||||||
import sonia.scm.security.SecuritySystem;
|
import sonia.scm.security.SecuritySystem;
|
||||||
import sonia.scm.template.MustacheTemplateEngine;
|
import sonia.scm.template.MustacheTemplateEngine;
|
||||||
import sonia.scm.template.TemplateEngine;
|
import sonia.scm.template.TemplateEngine;
|
||||||
@@ -247,6 +249,8 @@ class ScmServletModule extends ServletModule {
|
|||||||
|
|
||||||
// bind url helper
|
// bind url helper
|
||||||
bind(RootURL.class).to(DefaultRootURL.class);
|
bind(RootURL.class).to(DefaultRootURL.class);
|
||||||
|
|
||||||
|
bind(PermissionProvider.class).to(RepositoryPermissionProvider.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
private <T> void bind(Class<T> clazz, Class<? extends T> defaultImplementation) {
|
private <T> void bind(Class<T> clazz, Class<? extends T> defaultImplementation) {
|
||||||
|
|||||||
@@ -24,7 +24,6 @@
|
|||||||
|
|
||||||
package sonia.scm.repository;
|
package sonia.scm.repository;
|
||||||
|
|
||||||
import com.github.sdorra.ssp.PermissionActionCheck;
|
|
||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||||
@@ -294,6 +293,35 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager {
|
|||||||
return changedRepository;
|
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) {
|
private boolean hasNamespaceOrNameNotChanged(Repository repository, String newNamespace, String newName) {
|
||||||
return repository.getName().equals(newName)
|
return repository.getName().equals(newName)
|
||||||
&& repository.getNamespace().equals(newNamespace);
|
&& repository.getNamespace().equals(newNamespace);
|
||||||
@@ -303,12 +331,10 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager {
|
|||||||
public Collection<Repository> getAll(Predicate<Repository> filter, Comparator<Repository> comparator) {
|
public Collection<Repository> getAll(Predicate<Repository> filter, Comparator<Repository> comparator) {
|
||||||
List<Repository> repositories = Lists.newArrayList();
|
List<Repository> repositories = Lists.newArrayList();
|
||||||
|
|
||||||
PermissionActionCheck<Repository> check = RepositoryPermissions.read();
|
|
||||||
|
|
||||||
for (Repository repository : repositoryDAO.getAll()) {
|
for (Repository repository : repositoryDAO.getAll()) {
|
||||||
if (handlerMap.containsKey(repository.getType())
|
if (handlerMap.containsKey(repository.getType())
|
||||||
&& filter.test(repository)
|
&& filter.test(repository)
|
||||||
&& check.isPermitted(repository)) {
|
&& RepositoryPermissions.read().isPermitted(repository)) {
|
||||||
Repository r = repository.clone();
|
Repository r = repository.clone();
|
||||||
|
|
||||||
repositories.add(r);
|
repositories.add(r);
|
||||||
@@ -331,14 +357,12 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager {
|
|||||||
@Override
|
@Override
|
||||||
public Collection<Repository> getAll(Comparator<Repository> comparator,
|
public Collection<Repository> getAll(Comparator<Repository> comparator,
|
||||||
int start, int limit) {
|
int start, int limit) {
|
||||||
final PermissionActionCheck<Repository> check =
|
|
||||||
RepositoryPermissions.read();
|
|
||||||
|
|
||||||
return Util.createSubCollection(repositoryDAO.getAll(), comparator,
|
return Util.createSubCollection(repositoryDAO.getAll(), comparator,
|
||||||
new CollectionAppender<Repository>() {
|
new CollectionAppender<Repository>() {
|
||||||
@Override
|
@Override
|
||||||
public void append(Collection<Repository> collection, Repository item) {
|
public void append(Collection<Repository> collection, Repository item) {
|
||||||
if (check.isPermitted(item)) {
|
if (RepositoryPermissions.read().isPermitted(item)) {
|
||||||
collection.add(item.clone());
|
collection.add(item.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,10 +72,8 @@ public final class HealthChecker {
|
|||||||
public void checkAll() {
|
public void checkAll() {
|
||||||
logger.debug("check health of all repositories");
|
logger.debug("check health of all repositories");
|
||||||
|
|
||||||
PermissionActionCheck<Repository> check = RepositoryPermissions.healthCheck();
|
|
||||||
|
|
||||||
for (Repository repository : repositoryManager.getAll()) {
|
for (Repository repository : repositoryManager.getAll()) {
|
||||||
if (check.isPermitted(repository)) {
|
if (RepositoryPermissions.healthCheck().isPermitted(repository)) {
|
||||||
try {
|
try {
|
||||||
check(repository);
|
check(repository);
|
||||||
} catch (NotFoundException ex) {
|
} catch (NotFoundException ex) {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
package sonia.scm.security;
|
package sonia.scm.security;
|
||||||
|
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
|
import sonia.scm.repository.PermissionProvider;
|
||||||
import sonia.scm.repository.RepositoryRole;
|
import sonia.scm.repository.RepositoryRole;
|
||||||
import sonia.scm.repository.RepositoryRoleDAO;
|
import sonia.scm.repository.RepositoryRoleDAO;
|
||||||
|
|
||||||
@@ -32,7 +33,7 @@ import java.util.AbstractList;
|
|||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List ;
|
import java.util.List ;
|
||||||
|
|
||||||
public class RepositoryPermissionProvider {
|
public class RepositoryPermissionProvider implements PermissionProvider {
|
||||||
|
|
||||||
private final SystemRepositoryPermissionProvider systemRepositoryPermissionProvider;
|
private final SystemRepositoryPermissionProvider systemRepositoryPermissionProvider;
|
||||||
private final RepositoryRoleDAO repositoryRoleDAO;
|
private final RepositoryRoleDAO repositoryRoleDAO;
|
||||||
@@ -47,6 +48,10 @@ public class RepositoryPermissionProvider {
|
|||||||
return systemRepositoryPermissionProvider.availableVerbs();
|
return systemRepositoryPermissionProvider.availableVerbs();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Collection<String> readOnlyVerbs() {
|
||||||
|
return systemRepositoryPermissionProvider.readOnlyVerbs();
|
||||||
|
}
|
||||||
|
|
||||||
public Collection<RepositoryRole> availableRoles() {
|
public Collection<RepositoryRole> availableRoles() {
|
||||||
List<RepositoryRole> availableSystemRoles = systemRepositoryPermissionProvider.availableRoles();
|
List<RepositoryRole> availableSystemRoles = systemRepositoryPermissionProvider.availableRoles();
|
||||||
List<RepositoryRole> customRoles = repositoryRoleDAO.getAll();
|
List<RepositoryRole> customRoles = repositoryRoleDAO.getAll();
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
package sonia.scm.security;
|
package sonia.scm.security;
|
||||||
|
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import sonia.scm.plugin.PluginLoader;
|
import sonia.scm.plugin.PluginLoader;
|
||||||
@@ -34,8 +35,10 @@ import javax.xml.bind.JAXBContext;
|
|||||||
import javax.xml.bind.JAXBException;
|
import javax.xml.bind.JAXBException;
|
||||||
import javax.xml.bind.annotation.XmlAccessType;
|
import javax.xml.bind.annotation.XmlAccessType;
|
||||||
import javax.xml.bind.annotation.XmlAccessorType;
|
import javax.xml.bind.annotation.XmlAccessorType;
|
||||||
|
import javax.xml.bind.annotation.XmlAttribute;
|
||||||
import javax.xml.bind.annotation.XmlElement;
|
import javax.xml.bind.annotation.XmlElement;
|
||||||
import javax.xml.bind.annotation.XmlRootElement;
|
import javax.xml.bind.annotation.XmlRootElement;
|
||||||
|
import javax.xml.bind.annotation.XmlValue;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -54,25 +57,32 @@ public class SystemRepositoryPermissionProvider {
|
|||||||
private static final Logger logger = LoggerFactory.getLogger(SystemRepositoryPermissionProvider.class);
|
private static final Logger logger = LoggerFactory.getLogger(SystemRepositoryPermissionProvider.class);
|
||||||
private static final String REPOSITORY_PERMISSION_DESCRIPTOR = "META-INF/scm/repository-permissions.xml";
|
private static final String REPOSITORY_PERMISSION_DESCRIPTOR = "META-INF/scm/repository-permissions.xml";
|
||||||
private final List<String> availableVerbs;
|
private final List<String> availableVerbs;
|
||||||
|
private final List<String> readOnlyVerbs;
|
||||||
private final List<RepositoryRole> availableRoles;
|
private final List<RepositoryRole> availableRoles;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public SystemRepositoryPermissionProvider(PluginLoader pluginLoader) {
|
public SystemRepositoryPermissionProvider(PluginLoader pluginLoader) {
|
||||||
AvailableRepositoryPermissions availablePermissions = readAvailablePermissions(pluginLoader);
|
AvailableRepositoryPermissions availablePermissions = readAvailablePermissions(pluginLoader);
|
||||||
this.availableVerbs = removeDuplicates(availablePermissions.availableVerbs);
|
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() {
|
public List<String> availableVerbs() {
|
||||||
return availableVerbs;
|
return availableVerbs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<String> readOnlyVerbs() {
|
||||||
|
return readOnlyVerbs;
|
||||||
|
}
|
||||||
|
|
||||||
public List<RepositoryRole> availableRoles() {
|
public List<RepositoryRole> availableRoles() {
|
||||||
return availableRoles;
|
return availableRoles;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static AvailableRepositoryPermissions readAvailablePermissions(PluginLoader pluginLoader) {
|
private static AvailableRepositoryPermissions readAvailablePermissions(PluginLoader pluginLoader) {
|
||||||
Collection<String> availableVerbs = new ArrayList<>();
|
Collection<String> availableVerbs = new ArrayList<>();
|
||||||
|
Collection<String> readOnlyVerbs = new ArrayList<>();
|
||||||
Collection<RoleDescriptor> availableRoles = new ArrayList<>();
|
Collection<RoleDescriptor> availableRoles = new ArrayList<>();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -89,7 +99,8 @@ public class SystemRepositoryPermissionProvider {
|
|||||||
logger.debug("read repository permission descriptor from {}", descriptorUrl);
|
logger.debug("read repository permission descriptor from {}", descriptorUrl);
|
||||||
|
|
||||||
RepositoryPermissionsRoot repositoryPermissionsRoot = parsePermissionDescriptor(context, 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);
|
mergeRolesInto(availableRoles, repositoryPermissionsRoot.roles.roles);
|
||||||
}
|
}
|
||||||
} catch (IOException ex) {
|
} catch (IOException ex) {
|
||||||
@@ -99,7 +110,7 @@ public class SystemRepositoryPermissionProvider {
|
|||||||
"could not create jaxb context to read permission descriptors", ex);
|
"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) {
|
private static void mergeRolesInto(Collection<RoleDescriptor> targetRoles, List<RoleDescriptor> additionalRoles) {
|
||||||
@@ -138,10 +149,12 @@ public class SystemRepositoryPermissionProvider {
|
|||||||
|
|
||||||
private static class AvailableRepositoryPermissions {
|
private static class AvailableRepositoryPermissions {
|
||||||
private final Collection<String> availableVerbs;
|
private final Collection<String> availableVerbs;
|
||||||
|
private final Collection<String> readOnlyVerbs;
|
||||||
private final Collection<RoleDescriptor> availableRoles;
|
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.availableVerbs = unmodifiableCollection(availableVerbs);
|
||||||
|
this.readOnlyVerbs = unmodifiableCollection(readOnlyVerbs);
|
||||||
this.availableRoles = unmodifiableCollection(availableRoles);
|
this.availableRoles = unmodifiableCollection(availableRoles);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -156,7 +169,18 @@ public class SystemRepositoryPermissionProvider {
|
|||||||
@XmlRootElement(name = "verbs")
|
@XmlRootElement(name = "verbs")
|
||||||
private static class VerbListDescriptor {
|
private static class VerbListDescriptor {
|
||||||
@XmlElement(name = "verb")
|
@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")
|
@XmlRootElement(name = "roles")
|
||||||
|
|||||||
@@ -175,6 +175,7 @@ public class XmlRepositoryV1UpdateStep implements CoreUpdateStep {
|
|||||||
v1Repository.getContact(),
|
v1Repository.getContact(),
|
||||||
v1Repository.getDescription(),
|
v1Repository.getDescription(),
|
||||||
createPermissions(v1Repository));
|
createPermissions(v1Repository));
|
||||||
|
repository.setArchived(v1Repository.isArchived());
|
||||||
LOG.info("creating new repository {} from old repository {} in directory {}", repository, v1Repository.getName(), newPath);
|
LOG.info("creating new repository {} from old repository {} in directory {}", repository, v1Repository.getName(), newPath);
|
||||||
repositoryDao.add(repository, newPath);
|
repositoryDao.add(repository, newPath);
|
||||||
propertyStore.put(v1Repository.getId(), v1Repository.getProperties());
|
propertyStore.put(v1Repository.getId(), v1Repository.getProperties());
|
||||||
|
|||||||
@@ -44,6 +44,9 @@
|
|||||||
<permission>
|
<permission>
|
||||||
<value>repository:create</value>
|
<value>repository:create</value>
|
||||||
</permission>
|
</permission>
|
||||||
|
<permission>
|
||||||
|
<value>repository:archive:*</value>
|
||||||
|
</permission>
|
||||||
<permission>
|
<permission>
|
||||||
<value>namespace:permissionRead</value>
|
<value>namespace:permissionRead</value>
|
||||||
</permission>
|
</permission>
|
||||||
|
|||||||
@@ -25,14 +25,15 @@
|
|||||||
-->
|
-->
|
||||||
<repository-permissions>
|
<repository-permissions>
|
||||||
<verbs>
|
<verbs>
|
||||||
<verb>read</verb>
|
<verb read-only="true">read</verb>
|
||||||
<verb>modify</verb>
|
<verb>modify</verb>
|
||||||
<verb>delete</verb>
|
<verb>delete</verb>
|
||||||
<verb>rename</verb>
|
<verb>rename</verb>
|
||||||
<verb>pull</verb>
|
<verb read-only="true">pull</verb>
|
||||||
<verb>push</verb>
|
<verb>push</verb>
|
||||||
<verb>permissionRead</verb>
|
<verb read-only="true">permissionRead</verb>
|
||||||
<verb>permissionWrite</verb>
|
<verb>permissionWrite</verb>
|
||||||
|
<verb read-only="true">archive</verb>
|
||||||
<verb>*</verb>
|
<verb>*</verb>
|
||||||
</verbs>
|
</verbs>
|
||||||
<roles>
|
<roles>
|
||||||
|
|||||||
@@ -41,6 +41,12 @@
|
|||||||
"create": {
|
"create": {
|
||||||
"displayName": "Repositories erstellen",
|
"displayName": "Repositories erstellen",
|
||||||
"description": "Darf Repositories erstellen."
|
"description": "Darf Repositories erstellen."
|
||||||
|
},
|
||||||
|
"archive": {
|
||||||
|
"*": {
|
||||||
|
"displayName": "Repositories archivieren",
|
||||||
|
"description": "Darf Repositories als \"archiviert\" und somit als schreibgeschützt markieren."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
@@ -139,6 +145,10 @@
|
|||||||
"displayName": "Berechtigungen modifizieren",
|
"displayName": "Berechtigungen modifizieren",
|
||||||
"description": "Darf die Berechtigungen des Repository bearbeiten."
|
"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",
|
"displayName": "Alle Repository Rechte",
|
||||||
"description": "Darf im Repository Kontext alles ausführen. Dies beinhaltet alle Repository Berechtigungen."
|
"description": "Darf im Repository Kontext alles ausführen. Dies beinhaltet alle Repository Berechtigungen."
|
||||||
@@ -217,7 +227,7 @@
|
|||||||
},
|
},
|
||||||
"4iRct4avG1": {
|
"4iRct4avG1": {
|
||||||
"displayName": "Die Revisionen haben keinen gemeinsamen Ursprung",
|
"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": {
|
"65RdZ5atX1": {
|
||||||
"displayName": "Fehler beim Löschen von Plugin-Dateien",
|
"displayName": "Fehler beim Löschen von Plugin-Dateien",
|
||||||
@@ -241,7 +251,7 @@
|
|||||||
},
|
},
|
||||||
"4GRrgkSC01": {
|
"4GRrgkSC01": {
|
||||||
"displayName": "Unerwartetes Merge-Ergebnis",
|
"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": {
|
"6mRuFxaWM1": {
|
||||||
"displayName": "Falsche Checksumme",
|
"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."
|
"description": "Ein fehlerhaft heruntergeladenes Plugin konnte nicht gelöscht werden. Bitte prüfen Sie die Server Logs und löschen die Datei manuell."
|
||||||
},
|
},
|
||||||
"5GS6lwvWF1": {
|
"5GS6lwvWF1": {
|
||||||
"displayName": "Abhänigkeit konnte nicht gefunden werden",
|
"displayName": "Abhängigkeit 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."
|
"description": "Eine der Abhängigkeiten des Plugins konnte nicht gefunden werden. Bitte prüfen Sie die Logs für weitere Informationen."
|
||||||
},
|
},
|
||||||
"E5S6niWwi1": {
|
"E5S6niWwi1": {
|
||||||
"displayName": "Version einer Abhänigkeit zu niedrig",
|
"displayName": "Version einer Abhängigkeit zu niedrig",
|
||||||
"description": "Die Version einer Abhänigkeit des Plugin ist zu niedrig. Bitte prüfen Sie die Logs für weitere Informationen."
|
"description": "Die Version einer Abhängigkeit des Plugin ist zu niedrig. Bitte prüfen Sie die Logs für weitere Informationen."
|
||||||
},
|
},
|
||||||
"4RS6niPRX1": {
|
"4RS6niPRX1": {
|
||||||
"displayName": "Plugin information stimmen nicht überein",
|
"displayName": "Plugin information stimmen nicht überein",
|
||||||
@@ -269,7 +279,7 @@
|
|||||||
},
|
},
|
||||||
"2qRyyaVcJ1": {
|
"2qRyyaVcJ1": {
|
||||||
"displayName": "Ungültig formatiertes Element",
|
"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": {
|
"3tS0mjSoo1": {
|
||||||
"displayName": "Fehler bei der Erstellung eines Arbeitsverzeichnisses",
|
"displayName": "Fehler bei der Erstellung eines Arbeitsverzeichnisses",
|
||||||
@@ -281,7 +291,7 @@
|
|||||||
},
|
},
|
||||||
"BxS5wX2v71": {
|
"BxS5wX2v71": {
|
||||||
"displayName": "Inkorrekter Schlüssel",
|
"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": {
|
"FVS9JY1T21": {
|
||||||
"displayName": "Fehler bei der Anfrage",
|
"displayName": "Fehler bei der Anfrage",
|
||||||
@@ -290,6 +300,14 @@
|
|||||||
"D6SHRfqQw1": {
|
"D6SHRfqQw1": {
|
||||||
"displayName": "Repository Import fehlgeschlagen",
|
"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."
|
"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": {
|
"namespaceStrategies": {
|
||||||
|
|||||||
@@ -41,6 +41,12 @@
|
|||||||
"create": {
|
"create": {
|
||||||
"displayName": "Create repositories",
|
"displayName": "Create repositories",
|
||||||
"description": "May create repositories"
|
"description": "May create repositories"
|
||||||
|
},
|
||||||
|
"archive": {
|
||||||
|
"*": {
|
||||||
|
"displayName": "Archive repositories",
|
||||||
|
"description": "May mark repositories as \"archived\" and therefore unmodifiable"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
@@ -139,6 +145,10 @@
|
|||||||
"displayName": "modify permissions",
|
"displayName": "modify permissions",
|
||||||
"description": "May modify the permissions of the repository"
|
"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",
|
"displayName": "own repository",
|
||||||
"description": "May change everything for the repository (includes all other permissions)"
|
"description": "May change everything for the repository (includes all other permissions)"
|
||||||
@@ -290,6 +300,14 @@
|
|||||||
"D6SHRfqQw1": {
|
"D6SHRfqQw1": {
|
||||||
"displayName": "Repository import failed",
|
"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."
|
"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": {
|
"namespaceStrategies": {
|
||||||
|
|||||||
@@ -673,6 +673,43 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
|
|||||||
verify(ubc).unbundle(any(File.class));
|
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) {
|
private PageResult<Repository> createSingletonPageResult(Repository repository) {
|
||||||
return new PageResult<>(singletonList(repository), 0);
|
return new PageResult<>(singletonList(repository), 0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -266,6 +266,24 @@ public class RepositoryToRepositoryDtoMapperTest {
|
|||||||
assertEquals("http://1", dto.getLinks().getLinkBy("id").get().getHref());
|
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) {
|
private ScmProtocol mockProtocol(String type, String protocol) {
|
||||||
return new MockScmProtocol(type, 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.assertNull;
|
||||||
import static org.junit.Assert.assertSame;
|
import static org.junit.Assert.assertSame;
|
||||||
import static org.junit.Assert.assertThat;
|
import static org.junit.Assert.assertThat;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.argThat;
|
import static org.mockito.ArgumentMatchers.argThat;
|
||||||
import static org.mockito.Mockito.anyString;
|
import static org.mockito.Mockito.anyString;
|
||||||
import static org.mockito.Mockito.doAnswer;
|
import static org.mockito.Mockito.doAnswer;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
import static org.mockito.Mockito.spy;
|
import static org.mockito.Mockito.spy;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
//~--- JDK imports ------------------------------------------------------------
|
//~--- JDK imports ------------------------------------------------------------
|
||||||
@@ -101,6 +104,8 @@ import static org.mockito.Mockito.when;
|
|||||||
)
|
)
|
||||||
public class DefaultRepositoryManagerTest extends ManagerTestBase<Repository> {
|
public class DefaultRepositoryManagerTest extends ManagerTestBase<Repository> {
|
||||||
|
|
||||||
|
private RepositoryDAO repositoryDAO;
|
||||||
|
|
||||||
{
|
{
|
||||||
ThreadContext.unbindSubject();
|
ThreadContext.unbindSubject();
|
||||||
}
|
}
|
||||||
@@ -457,6 +462,77 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase<Repository> {
|
|||||||
.contains("default_namespace");
|
.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 --------------------------------------------------------------
|
//~--- methods --------------------------------------------------------------
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -466,7 +542,7 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase<Repository> {
|
|||||||
|
|
||||||
private DefaultRepositoryManager createRepositoryManager(KeyGenerator keyGenerator) {
|
private DefaultRepositoryManager createRepositoryManager(KeyGenerator keyGenerator) {
|
||||||
Set<RepositoryHandler> handlerSet = new HashSet<>();
|
Set<RepositoryHandler> handlerSet = new HashSet<>();
|
||||||
RepositoryDAO repositoryDAO = createRepositoryDaoMock();
|
repositoryDAO = createRepositoryDaoMock();
|
||||||
mock(ConfigurationStoreFactory.class);
|
mock(ConfigurationStoreFactory.class);
|
||||||
handlerSet.add(createRepositoryHandler("dummy", "Dummy"));
|
handlerSet.add(createRepositoryHandler("dummy", "Dummy"));
|
||||||
handlerSet.add(createRepositoryHandler("git", "Git"));
|
handlerSet.add(createRepositoryHandler("git", "Git"));
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ public class DefaultSecuritySystemTest extends AbstractTestBase
|
|||||||
public void createSecuritySystem()
|
public void createSecuritySystem()
|
||||||
{
|
{
|
||||||
jaxbConfigurationEntryStoreFactory =
|
jaxbConfigurationEntryStoreFactory =
|
||||||
spy(new JAXBConfigurationEntryStoreFactory(contextProvider , repositoryLocationResolver, new UUIDKeyGenerator() ) {});
|
spy(new JAXBConfigurationEntryStoreFactory(contextProvider , repositoryLocationResolver, new UUIDKeyGenerator(), null) {});
|
||||||
pluginLoader = mock(PluginLoader.class);
|
pluginLoader = mock(PluginLoader.class);
|
||||||
when(pluginLoader.getUberClassLoader()).thenReturn(ClassLoaders.getContextClassLoader(DefaultSecuritySystem.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.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
|
||||||
|
import static java.util.Arrays.asList;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.junit.jupiter.api.Assertions.fail;
|
import static org.junit.jupiter.api.Assertions.fail;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
@@ -55,7 +56,7 @@ class SystemRepositoryPermissionProviderTest {
|
|||||||
.filter(field -> field.getName().startsWith("ACTION_"))
|
.filter(field -> field.getName().startsWith("ACTION_"))
|
||||||
.filter(field -> !field.getName().equals("ACTION_HEALTHCHECK"))
|
.filter(field -> !field.getName().equals("ACTION_HEALTHCHECK"))
|
||||||
.map(this::getString)
|
.map(this::getString)
|
||||||
.filter(verb -> !"create".equals(verb))
|
.filter(verb -> !asList("create", "archive").contains(verb))
|
||||||
.toArray(String[]::new);
|
.toArray(String[]::new);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ class DefaultMigrationStrategyDAOTest {
|
|||||||
@BeforeEach
|
@BeforeEach
|
||||||
void initStore(@TempDir Path tempDir) {
|
void initStore(@TempDir Path tempDir) {
|
||||||
when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile());
|
when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile());
|
||||||
storeFactory = new JAXBConfigurationStoreFactory(contextProvider, null);
|
storeFactory = new JAXBConfigurationStoreFactory(contextProvider, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ class V1RepositoryFileSystem {
|
|||||||
* <id>c1597b4f-a9f0-49f7-ad1f-37d3aae1c55f</id>
|
* <id>c1597b4f-a9f0-49f7-ad1f-37d3aae1c55f</id>
|
||||||
* <name>some/more/directories/than/one</name>
|
* <name>some/more/directories/than/one</name>
|
||||||
* <public>false</public>
|
* <public>false</public>
|
||||||
* <archived>false</archived>
|
* <archived>true</archived>
|
||||||
* <type>git</type>
|
* <type>git</type>
|
||||||
* </repository>
|
* </repository>
|
||||||
* <repository>
|
* <repository>
|
||||||
|
|||||||
@@ -136,7 +136,19 @@ class XmlRepositoryV1UpdateStepTest {
|
|||||||
.get()
|
.get()
|
||||||
.hasFieldOrPropertyWithValue("type", "git")
|
.hasFieldOrPropertyWithValue("type", "git")
|
||||||
.hasFieldOrPropertyWithValue("contact", "arthur@dent.uk")
|
.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
|
@Test
|
||||||
|
|||||||
@@ -127,6 +127,6 @@ public class DefaultUserManagerTest extends UserManagerTestBase {
|
|||||||
//~--- methods --------------------------------------------------------------
|
//~--- methods --------------------------------------------------------------
|
||||||
|
|
||||||
private XmlUserDAO createXmlUserDAO() {
|
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