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:
René Pfeuffer
2020-12-16 10:58:29 +01:00
committed by GitHub
parent b167d90fea
commit 8e3b0e4145
77 changed files with 2066 additions and 438 deletions

View File

@@ -57,6 +57,7 @@ public class RepositoryDto extends HalRepresentation implements CreateRepository
private String name;
@NotEmpty
private String type;
private boolean archived;
RepositoryDto(Links links, Embedded embedded) {
super(links, embedded);

View File

@@ -215,10 +215,60 @@ public class RepositoryResource {
schema = @Schema(implementation = ErrorDto.class)
))
@ApiResponse(responseCode = "500", description = "internal server error")
public Response rename(@PathParam("namespace") String namespace, @PathParam("name") String name, @Valid RepositoryRenameDto renameDto) {
public void rename(@PathParam("namespace") String namespace, @PathParam("name") String name, @Valid RepositoryRenameDto renameDto) {
Repository repository = loadBy(namespace, name).get();
manager.rename(repository, renameDto.getNamespace(), renameDto.getName());
return Response.status(204).build();
}
/**
* Marks the given repository as "archived".
*
* @param namespace the namespace of the repository to be marked
* @param name the name of the repository to be marked
*/
@POST
@Path("archive")
@Operation(summary = "Mark repository as \"archived\"", description = "Marks the repository as \"archived\".", tags = "Repository")
@ApiResponse(responseCode = "204", description = "update success")
@ApiResponse(responseCode = "400", description = "invalid request, e.g. when the repository already is marked as archived")
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"repository:archive\" privilege")
@ApiResponse(
responseCode = "404",
description = "not found, no repository with the specified namespace and name available",
content = @Content(
mediaType = VndMediaType.ERROR_TYPE,
schema = @Schema(implementation = ErrorDto.class)
))
@ApiResponse(responseCode = "500", description = "internal server error")
public void archive(@PathParam("namespace") String namespace, @PathParam("name") String name) {
Repository repository = loadBy(namespace, name).get();
manager.archive(repository);
}
/**
* Marks the given repository as not "archived".
*
* @param namespace the namespace of the repository to remove the mark from
* @param name the name of the repository to remove the mark from
*/
@POST
@Path("unarchive")
@Operation(summary = "Mark repository as \"not archived\"", description = "Removes the \"archived\" mark from the repository.", tags = "Repository")
@ApiResponse(responseCode = "204", description = "update success")
@ApiResponse(responseCode = "400", description = "invalid request, e.g. when the repository is not marked as archived")
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"repository:archive\" privilege")
@ApiResponse(
responseCode = "404",
description = "not found, no repository with the specified namespace and name available",
content = @Content(
mediaType = VndMediaType.ERROR_TYPE,
schema = @Schema(implementation = ErrorDto.class)
))
@ApiResponse(responseCode = "500", description = "internal server error")
public void unarchive(@PathParam("namespace") String namespace, @PathParam("name") String name) {
Repository repository = loadBy(namespace, name).get();
manager.unarchive(repository);
}
private Repository processUpdate(RepositoryDto repositoryDto, Repository existing) {

View File

@@ -79,6 +79,13 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper<Reposit
if (RepositoryPermissions.modify(repository).isPermitted()) {
linksBuilder.single(link("update", resourceLinks.repository().update(repository.getNamespace(), repository.getName())));
}
if (RepositoryPermissions.archive().isPermitted(repository)) {
if (repository.isArchived()) {
linksBuilder.single(link("unarchive", resourceLinks.repository().unarchive(repository.getNamespace(), repository.getName())));
} else {
linksBuilder.single(link("archive", resourceLinks.repository().archive(repository.getNamespace(), repository.getName())));
}
}
if (RepositoryPermissions.rename(repository).isPermitted()) {
if (isRenameNamespacePossible()) {
linksBuilder.single(link("renameWithNamespace", resourceLinks.repository().rename(repository.getNamespace(), repository.getName())));

View File

@@ -370,6 +370,13 @@ class ResourceLinks {
String importFromBundle(String type) {
return repositoryImportLinkBuilder.method("getRepositoryImportResource").parameters().method("importFromBundle").parameters(type).href();
}
String archive(String namespace, String name) {
return repositoryLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("archive").parameters().href();
}
String unarchive(String namespace, String name) {
return repositoryLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("unarchive").parameters().href();
}
}
RepositoryCollectionLinks repositoryCollection() {

View File

@@ -21,12 +21,13 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.lifecycle.modules;
import com.google.inject.AbstractModule;
import com.google.inject.TypeLiteral;
import com.google.inject.throwingproviders.ThrowingProviderBinder;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.SCMContext;
@@ -36,6 +37,8 @@ import sonia.scm.io.FileSystem;
import sonia.scm.lifecycle.DefaultRestarter;
import sonia.scm.lifecycle.Restarter;
import sonia.scm.plugin.PluginLoader;
import sonia.scm.repository.EventDrivenRepositoryArchiveCheck;
import sonia.scm.repository.RepositoryArchivedCheck;
import sonia.scm.repository.RepositoryLocationResolver;
import sonia.scm.repository.xml.MetadataStore;
import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver;
@@ -93,6 +96,7 @@ public class BootstrapModule extends AbstractModule {
bind(CipherHandler.class).toInstance(CipherUtil.getInstance().getCipherHandler());
// bind core
bind(RepositoryArchivedCheck.class, EventDrivenRepositoryArchiveCheck.class);
bind(ConfigurationStoreFactory.class, JAXBConfigurationStoreFactory.class);
bind(ConfigurationEntryStoreFactory.class, JAXBConfigurationEntryStoreFactory.class);
bind(DataStoreFactory.class, JAXBDataStoreFactory.class);

View File

@@ -72,6 +72,7 @@ import sonia.scm.repository.HealthCheckContextListener;
import sonia.scm.repository.NamespaceManager;
import sonia.scm.repository.NamespaceStrategy;
import sonia.scm.repository.NamespaceStrategyProvider;
import sonia.scm.repository.PermissionProvider;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryDAO;
import sonia.scm.repository.RepositoryManager;
@@ -92,6 +93,7 @@ import sonia.scm.security.ConfigurableLoginAttemptHandler;
import sonia.scm.security.DefaultAccessTokenCookieIssuer;
import sonia.scm.security.DefaultSecuritySystem;
import sonia.scm.security.LoginAttemptHandler;
import sonia.scm.security.RepositoryPermissionProvider;
import sonia.scm.security.SecuritySystem;
import sonia.scm.template.MustacheTemplateEngine;
import sonia.scm.template.TemplateEngine;
@@ -247,6 +249,8 @@ class ScmServletModule extends ServletModule {
// bind url helper
bind(RootURL.class).to(DefaultRootURL.class);
bind(PermissionProvider.class).to(RepositoryPermissionProvider.class);
}
private <T> void bind(Class<T> clazz, Class<? extends T> defaultImplementation) {

View File

@@ -24,7 +24,6 @@
package sonia.scm.repository;
import com.github.sdorra.ssp.PermissionActionCheck;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
@@ -294,6 +293,35 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager {
return changedRepository;
}
@Override
public void archive(Repository repository) {
setArchived(repository, true);
}
@Override
public void unarchive(Repository repository) {
setArchived(repository, false);
}
private void setArchived(Repository repository, boolean archived) {
Repository originalRepository = repositoryDAO.get(repository.getNamespaceAndName());
if (archived == originalRepository.isArchived()) {
throw new NoChangesMadeException(repository);
}
Repository changedRepository = originalRepository.clone();
changedRepository.setArchived(archived);
managerDaoAdapter.modify(
changedRepository,
RepositoryPermissions::archive,
notModified -> {
},
notModified -> fireEvent(HandlerEventType.MODIFY, changedRepository, originalRepository));
}
private boolean hasNamespaceOrNameNotChanged(Repository repository, String newNamespace, String newName) {
return repository.getName().equals(newName)
&& repository.getNamespace().equals(newNamespace);
@@ -303,12 +331,10 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager {
public Collection<Repository> getAll(Predicate<Repository> filter, Comparator<Repository> comparator) {
List<Repository> repositories = Lists.newArrayList();
PermissionActionCheck<Repository> check = RepositoryPermissions.read();
for (Repository repository : repositoryDAO.getAll()) {
if (handlerMap.containsKey(repository.getType())
&& filter.test(repository)
&& check.isPermitted(repository)) {
&& RepositoryPermissions.read().isPermitted(repository)) {
Repository r = repository.clone();
repositories.add(r);
@@ -331,14 +357,12 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager {
@Override
public Collection<Repository> getAll(Comparator<Repository> comparator,
int start, int limit) {
final PermissionActionCheck<Repository> check =
RepositoryPermissions.read();
return Util.createSubCollection(repositoryDAO.getAll(), comparator,
new CollectionAppender<Repository>() {
@Override
public void append(Collection<Repository> collection, Repository item) {
if (check.isPermitted(item)) {
if (RepositoryPermissions.read().isPermitted(item)) {
collection.add(item.clone());
}
}

View File

@@ -72,10 +72,8 @@ public final class HealthChecker {
public void checkAll() {
logger.debug("check health of all repositories");
PermissionActionCheck<Repository> check = RepositoryPermissions.healthCheck();
for (Repository repository : repositoryManager.getAll()) {
if (check.isPermitted(repository)) {
if (RepositoryPermissions.healthCheck().isPermitted(repository)) {
try {
check(repository);
} catch (NotFoundException ex) {

View File

@@ -21,10 +21,11 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.security;
import com.google.inject.Inject;
import sonia.scm.repository.PermissionProvider;
import sonia.scm.repository.RepositoryRole;
import sonia.scm.repository.RepositoryRoleDAO;
@@ -32,7 +33,7 @@ import java.util.AbstractList;
import java.util.Collection;
import java.util.List ;
public class RepositoryPermissionProvider {
public class RepositoryPermissionProvider implements PermissionProvider {
private final SystemRepositoryPermissionProvider systemRepositoryPermissionProvider;
private final RepositoryRoleDAO repositoryRoleDAO;
@@ -47,6 +48,10 @@ public class RepositoryPermissionProvider {
return systemRepositoryPermissionProvider.availableVerbs();
}
public Collection<String> readOnlyVerbs() {
return systemRepositoryPermissionProvider.readOnlyVerbs();
}
public Collection<RepositoryRole> availableRoles() {
List<RepositoryRole> availableSystemRoles = systemRepositoryPermissionProvider.availableRoles();
List<RepositoryRole> customRoles = repositoryRoleDAO.getAll();

View File

@@ -21,10 +21,11 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.security;
import com.google.inject.Inject;
import lombok.EqualsAndHashCode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.plugin.PluginLoader;
@@ -34,8 +35,10 @@ import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlValue;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
@@ -54,25 +57,32 @@ public class SystemRepositoryPermissionProvider {
private static final Logger logger = LoggerFactory.getLogger(SystemRepositoryPermissionProvider.class);
private static final String REPOSITORY_PERMISSION_DESCRIPTOR = "META-INF/scm/repository-permissions.xml";
private final List<String> availableVerbs;
private final List<String> readOnlyVerbs;
private final List<RepositoryRole> availableRoles;
@Inject
public SystemRepositoryPermissionProvider(PluginLoader pluginLoader) {
AvailableRepositoryPermissions availablePermissions = readAvailablePermissions(pluginLoader);
this.availableVerbs = removeDuplicates(availablePermissions.availableVerbs);
this.availableRoles = removeDuplicates(availablePermissions.availableRoles.stream().map(r -> new RepositoryRole(r.name, r.verbs.verbs, "system")).collect(toList()));
this.readOnlyVerbs = removeDuplicates(availablePermissions.readOnlyVerbs);
this.availableRoles = removeDuplicates(availablePermissions.availableRoles.stream().map(r -> new RepositoryRole(r.name, r.verbs.verbs.stream().map(verb -> verb.value).collect(toList()), "system")).collect(toList()));
}
public List<String> availableVerbs() {
return availableVerbs;
}
public List<String> readOnlyVerbs() {
return readOnlyVerbs;
}
public List<RepositoryRole> availableRoles() {
return availableRoles;
}
private static AvailableRepositoryPermissions readAvailablePermissions(PluginLoader pluginLoader) {
Collection<String> availableVerbs = new ArrayList<>();
Collection<String> readOnlyVerbs = new ArrayList<>();
Collection<RoleDescriptor> availableRoles = new ArrayList<>();
try {
@@ -89,7 +99,8 @@ public class SystemRepositoryPermissionProvider {
logger.debug("read repository permission descriptor from {}", descriptorUrl);
RepositoryPermissionsRoot repositoryPermissionsRoot = parsePermissionDescriptor(context, descriptorUrl);
availableVerbs.addAll(repositoryPermissionsRoot.verbs.verbs);
repositoryPermissionsRoot.verbs.verbs.forEach(verb -> availableVerbs.add(verb.value));
repositoryPermissionsRoot.verbs.verbs.stream().filter(verb -> verb.readOnly).map(verb -> verb.value).forEach(readOnlyVerbs::add);
mergeRolesInto(availableRoles, repositoryPermissionsRoot.roles.roles);
}
} catch (IOException ex) {
@@ -99,7 +110,7 @@ public class SystemRepositoryPermissionProvider {
"could not create jaxb context to read permission descriptors", ex);
}
return new AvailableRepositoryPermissions(availableVerbs, availableRoles);
return new AvailableRepositoryPermissions(availableVerbs, readOnlyVerbs, availableRoles);
}
private static void mergeRolesInto(Collection<RoleDescriptor> targetRoles, List<RoleDescriptor> additionalRoles) {
@@ -138,10 +149,12 @@ public class SystemRepositoryPermissionProvider {
private static class AvailableRepositoryPermissions {
private final Collection<String> availableVerbs;
private final Collection<String> readOnlyVerbs;
private final Collection<RoleDescriptor> availableRoles;
private AvailableRepositoryPermissions(Collection<String> availableVerbs, Collection<RoleDescriptor> availableRoles) {
private AvailableRepositoryPermissions(Collection<String> availableVerbs, Collection<String> readOnlyVerbs, Collection<RoleDescriptor> availableRoles) {
this.availableVerbs = unmodifiableCollection(availableVerbs);
this.readOnlyVerbs = unmodifiableCollection(readOnlyVerbs);
this.availableRoles = unmodifiableCollection(availableRoles);
}
}
@@ -156,7 +169,18 @@ public class SystemRepositoryPermissionProvider {
@XmlRootElement(name = "verbs")
private static class VerbListDescriptor {
@XmlElement(name = "verb")
private Set<String> verbs = new LinkedHashSet<>();
private Set<Verb> verbs = new LinkedHashSet<>();
}
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "verb")
@EqualsAndHashCode
private static class Verb {
@XmlValue
private String value;
@XmlAttribute(name = "read-only")
@EqualsAndHashCode.Exclude
private boolean readOnly;
}
@XmlRootElement(name = "roles")

View File

@@ -175,6 +175,7 @@ public class XmlRepositoryV1UpdateStep implements CoreUpdateStep {
v1Repository.getContact(),
v1Repository.getDescription(),
createPermissions(v1Repository));
repository.setArchived(v1Repository.isArchived());
LOG.info("creating new repository {} from old repository {} in directory {}", repository, v1Repository.getName(), newPath);
repositoryDao.add(repository, newPath);
propertyStore.put(v1Repository.getId(), v1Repository.getProperties());