Add tag guard

This commit is contained in:
Rene Pfeuffer
2024-10-16 16:14:22 +02:00
parent 3a88dff70f
commit afdaee0442
11 changed files with 316 additions and 6 deletions

View File

@@ -16,6 +16,7 @@
package sonia.scm.api.v2.resources;
import com.google.common.annotations.VisibleForTesting;
import de.otto.edison.hal.Embedded;
import de.otto.edison.hal.Links;
import jakarta.inject.Inject;
@@ -27,10 +28,13 @@ import org.mapstruct.ObjectFactory;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryPermissions;
import sonia.scm.repository.Tag;
import sonia.scm.repository.TagGuard;
import sonia.scm.repository.TagGuardDeletionRequest;
import sonia.scm.web.EdisonHalAppender;
import java.time.Instant;
import java.util.Optional;
import java.util.Set;
import static de.otto.edison.hal.Embedded.embeddedBuilder;
import static de.otto.edison.hal.Link.link;
@@ -41,6 +45,8 @@ public abstract class TagToTagDtoMapper extends HalAppenderMapper {
@Inject
private ResourceLinks resourceLinks;
@Inject
private Set<TagGuard> tagGuardSet;
@Mapping(target = "date", source = "date", qualifiedByName = "mapDate")
@Mapping(target = "attributes", ignore = true) // We do not map HAL attributes
@@ -54,7 +60,7 @@ public abstract class TagToTagDtoMapper extends HalAppenderMapper {
.single(link("sources", resourceLinks.source().self(repository.getNamespace(), repository.getName(), tag.getRevision())))
.single(link("changeset", resourceLinks.changeset().self(repository.getNamespace(), repository.getName(), tag.getRevision())));
if (tag.getDeletable() && RepositoryPermissions.push(repository).isPermitted()) {
if (tag.getDeletable() && RepositoryPermissions.push(repository).isPermitted() && tagGuardSet.stream().allMatch(guard -> guard.canDelete(new TagGuardDeletionRequest(repository, tag)))) {
linksBuilder
.single(link("delete", resourceLinks.tag().delete(repository.getNamespace(), repository.getName(), tag.getName())));
}
@@ -69,4 +75,14 @@ public abstract class TagToTagDtoMapper extends HalAppenderMapper {
Instant map(Optional<Long> value) {
return value.map(Instant::ofEpochMilli).orElse(null);
}
@VisibleForTesting
void setResourceLinks(ResourceLinks resourceLinks) {
this.resourceLinks = resourceLinks;
}
@VisibleForTesting
void setTagGuardSet(Set<TagGuard> tagGuardSet) {
this.tagGuardSet = tagGuardSet;
}
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright (c) 2020 - present Cloudogu GmbH
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
package sonia.scm.repository;
import sonia.scm.ExceptionWithContext;
import static sonia.scm.ContextEntry.ContextBuilder.entity;
class TagProtectionException extends ExceptionWithContext {
TagProtectionException(Repository repository, Tag tag, String message) {
super(entity(Tag.class, tag.getName()).in(repository).build(), message);
}
@Override
public String getCode() {
return "99UQi5FKd1";
}
}

View File

@@ -0,0 +1,75 @@
/*
* Copyright (c) 2020 - present Cloudogu GmbH
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
package sonia.scm.repository;
import com.github.legman.Subscribe;
import com.google.inject.Inject;
import lombok.extern.slf4j.Slf4j;
import sonia.scm.EagerSingleton;
import sonia.scm.plugin.Extension;
import sonia.scm.repository.api.HookFeature;
import java.util.Set;
@Slf4j
@Extension
@EagerSingleton
public class TagProtectionPreReceiveRepositoryHook {
private final Set<TagGuard> tagGuards;
@Inject
public TagProtectionPreReceiveRepositoryHook(Set<TagGuard> tagGuards) {
this.tagGuards = tagGuards;
}
@Subscribe(async = false)
public void onEvent(PreReceiveRepositoryHookEvent event) {
if (tagGuards.isEmpty()) {
log.trace("no tag guards available, skipping tag protection");
return;
}
Repository repository = event.getRepository();
if (repository == null) {
log.trace("received hook without repository, skipping tag protection");
return;
}
if (!event.getContext().isFeatureSupported(HookFeature.TAG_PROVIDER)) {
log.trace("repository {} does not support tag provider, skipping tag protection", repository);
return;
}
event
.getContext()
.getTagProvider()
.getDeletedTags()
.forEach(tag -> {
boolean tagMustBeProtected = tagGuards.stream().anyMatch(guard -> !guard.canDelete(new TagGuardDeletionRequest(repository, tag)));
if (tagMustBeProtected) {
String message = String.format("Deleting tag '%s' not allowed in repository %s", tag.getName(), repository);
log.info(message);
if (event.getContext().isFeatureSupported(HookFeature.MESSAGE_PROVIDER)) {
event.getContext().getMessageProvider().sendMessage(message);
}
throw new TagProtectionException(repository, tag, message);
} else {
log.trace("tag {} does not have to be protected", tag);
}
});
}
}