mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-01-20 06:22:10 +01:00
Add security notifications to inform about vulnerabilities (#1924)
Add security notifications in SCM-Manager to inform running instances about known security issues. These alerts can be core or plugin specific and will be shown to every user in the header. Co-authored-by: Matthias Thieroff <matthias.thieroff@cloudogu.com> Co-authored-by: Philipp Ahrendt <philipp.ahrendt@cloudogu.com> Co-authored-by: Sebastian Sdorra <sebastian.sdorra@cloudogu.com>
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
/*
|
||||
* 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.api.v2.resources;
|
||||
|
||||
import com.cronutils.utils.VisibleForTesting;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.hash.Hasher;
|
||||
import com.google.common.hash.Hashing;
|
||||
import de.otto.edison.hal.HalRepresentation;
|
||||
import de.otto.edison.hal.Link;
|
||||
import de.otto.edison.hal.Links;
|
||||
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.Value;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.config.ScmConfiguration;
|
||||
import sonia.scm.plugin.PluginLoader;
|
||||
import sonia.scm.util.SystemUtil;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.WebApplicationException;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.io.Serializable;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Path("v2/alerts")
|
||||
@OpenAPIDefinition(tags = {
|
||||
@Tag(name = "Alerts", description = "Alert related endpoints")
|
||||
})
|
||||
public class AlertsResource {
|
||||
|
||||
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||
|
||||
private final SCMContextProvider scmContextProvider;
|
||||
private final ScmConfiguration scmConfiguration;
|
||||
private final PluginLoader pluginLoader;
|
||||
private final Supplier<String> dateSupplier;
|
||||
|
||||
@Inject
|
||||
public AlertsResource(SCMContextProvider scmContextProvider, ScmConfiguration scmConfiguration, PluginLoader pluginLoader) {
|
||||
this(scmContextProvider, scmConfiguration, pluginLoader, () -> LocalDateTime.now().format(FORMATTER));
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
AlertsResource(SCMContextProvider scmContextProvider, ScmConfiguration scmConfiguration, PluginLoader pluginLoader, Supplier<String> dateSupplier) {
|
||||
this.scmContextProvider = scmContextProvider;
|
||||
this.scmConfiguration = scmConfiguration;
|
||||
this.pluginLoader = pluginLoader;
|
||||
this.dateSupplier = dateSupplier;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("")
|
||||
@Produces(VndMediaType.ALERTS_REQUEST)
|
||||
@Operation(
|
||||
summary = "Alerts",
|
||||
description = "Returns url and body prepared for the alert service",
|
||||
tags = "Alerts",
|
||||
operationId = "alerts_get_request"
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "success",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ALERTS_REQUEST,
|
||||
schema = @Schema(implementation = HalRepresentation.class)
|
||||
)
|
||||
)
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "internal server error",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
))
|
||||
public AlertsRequest getAlertsRequest(@Context UriInfo uriInfo) throws IOException {
|
||||
if (Strings.isNullOrEmpty(scmConfiguration.getAlertsUrl())) {
|
||||
throw new WebApplicationException("Alerts disabled", Response.Status.CONFLICT);
|
||||
}
|
||||
|
||||
String instanceId = scmContextProvider.getInstanceId();
|
||||
String version = scmContextProvider.getVersion();
|
||||
String os = SystemUtil.getOS();
|
||||
String arch = SystemUtil.getArch();
|
||||
String jre = SystemUtil.getJre();
|
||||
|
||||
List<Plugin> plugins = pluginLoader.getInstalledPlugins().stream()
|
||||
.map(p -> p.getDescriptor().getInformation())
|
||||
.map(i -> new Plugin(i.getName(), i.getVersion()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
String url = scmConfiguration.getAlertsUrl();
|
||||
AlertsRequestBody body = new AlertsRequestBody(instanceId, version, os, arch, jre, plugins);
|
||||
String checksum = createChecksum(url, body);
|
||||
|
||||
Links links = createLinks(uriInfo, url);
|
||||
return new AlertsRequest(links, checksum, body);
|
||||
}
|
||||
|
||||
private Links createLinks(UriInfo uriInfo, String alertsUrl) {
|
||||
return Links.linkingTo()
|
||||
.self(uriInfo.getAbsolutePath().toASCIIString())
|
||||
.single(Link.link("alerts", alertsUrl))
|
||||
.build();
|
||||
}
|
||||
|
||||
@SuppressWarnings("UnstableApiUsage")
|
||||
private String createChecksum(String url, AlertsRequestBody body) throws IOException {
|
||||
Hasher hasher = Hashing.sha256().newHasher();
|
||||
hasher.putString(url, StandardCharsets.UTF_8);
|
||||
hasher.putString(dateSupplier.get(), StandardCharsets.UTF_8);
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
try (ObjectOutputStream out = new ObjectOutputStream(baos)) {
|
||||
out.writeObject(body);
|
||||
}
|
||||
|
||||
hasher.putBytes(baos.toByteArray());
|
||||
return hasher.hash().toString();
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@SuppressWarnings("java:S2160") // we need no equals here
|
||||
public static class AlertsRequest extends HalRepresentation {
|
||||
|
||||
private String checksum;
|
||||
private AlertsRequestBody body;
|
||||
|
||||
public AlertsRequest(Links links, String checksum, AlertsRequestBody body) {
|
||||
super(links);
|
||||
this.checksum = checksum;
|
||||
this.body = body;
|
||||
}
|
||||
}
|
||||
|
||||
@Value
|
||||
public static class AlertsRequestBody implements Serializable {
|
||||
|
||||
String instanceId;
|
||||
String version;
|
||||
String os;
|
||||
String arch;
|
||||
String jre;
|
||||
@SuppressWarnings("java:S1948") // the field is serializable, but sonar does not get it
|
||||
List<Plugin> plugins;
|
||||
|
||||
}
|
||||
|
||||
@Value
|
||||
public static class Plugin implements Serializable {
|
||||
|
||||
String name;
|
||||
String version;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -61,6 +61,7 @@ public class ConfigDto extends HalRepresentation implements UpdateConfigDto {
|
||||
private boolean enabledApiKeys;
|
||||
private String namespaceStrategy;
|
||||
private String loginInfoUrl;
|
||||
private String alertsUrl;
|
||||
private String releaseFeedUrl;
|
||||
private String mailDomainName;
|
||||
private Set<String> emergencyContacts;
|
||||
|
||||
@@ -146,6 +146,10 @@ public class IndexDtoGenerator extends HalAppenderMapper {
|
||||
|
||||
builder.array(searchLinks());
|
||||
builder.single(link("searchableTypes", resourceLinks.search().searchableTypes()));
|
||||
|
||||
if (!Strings.isNullOrEmpty(configuration.getAlertsUrl())) {
|
||||
builder.single(link("alerts", resourceLinks.alerts().get()));
|
||||
}
|
||||
} else {
|
||||
builder.single(link("login", resourceLinks.authentication().jsonLogin()));
|
||||
}
|
||||
|
||||
@@ -1222,4 +1222,23 @@ class ResourceLinks {
|
||||
return indexLinkBuilder.method("authResource").parameters().method("authenticationInfo").parameters().href();
|
||||
}
|
||||
}
|
||||
|
||||
public AlertsLinks alerts() {
|
||||
return new AlertsLinks(scmPathInfoStore.get().get());
|
||||
}
|
||||
|
||||
static class AlertsLinks {
|
||||
|
||||
private final LinkBuilder indexLinkBuilder;
|
||||
|
||||
AlertsLinks(ScmPathInfo pathInfo) {
|
||||
indexLinkBuilder = new LinkBuilder(pathInfo, AlertsResource.class);
|
||||
}
|
||||
|
||||
String get() {
|
||||
return indexLinkBuilder.method("getAlertsRequest").parameters().href();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -71,6 +71,14 @@ interface UpdateConfigDto {
|
||||
|
||||
String getLoginInfoUrl();
|
||||
|
||||
/**
|
||||
* Get the url to the alerts api.
|
||||
*
|
||||
* @return alerts url
|
||||
* @since 2.30.0
|
||||
*/
|
||||
String getAlertsUrl();
|
||||
|
||||
String getReleaseFeedUrl();
|
||||
|
||||
String getMailDomainName();
|
||||
|
||||
@@ -30,6 +30,7 @@ import com.google.common.base.Function;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
|
||||
@@ -84,6 +85,16 @@ public final class ExplodedSmp
|
||||
|
||||
//~--- get methods ----------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Returns {@code true} if the exploded smp contains a core plugin
|
||||
* @return {@code true} for a core plugin
|
||||
* @since 2.30.0
|
||||
*/
|
||||
public boolean isCore() {
|
||||
return Files.exists(path.resolve(PluginConstants.FILE_CORE));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the path to the plugin directory.
|
||||
*
|
||||
|
||||
@@ -34,6 +34,7 @@ import com.google.common.collect.Sets;
|
||||
import com.google.common.hash.Hashing;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.SCMContext;
|
||||
import sonia.scm.lifecycle.classloading.ClassLoaderLifeCycle;
|
||||
|
||||
import javax.xml.bind.JAXBContext;
|
||||
@@ -175,7 +176,7 @@ public final class PluginProcessor
|
||||
Set<ExplodedSmp> plugins = concat(installedPlugins, newlyInstalledPlugins);
|
||||
|
||||
logger.trace("start building plugin tree");
|
||||
PluginTree pluginTree = new PluginTree(plugins);
|
||||
PluginTree pluginTree = new PluginTree(SCMContext.getContext().getStage(), plugins);
|
||||
|
||||
logger.info("install plugin tree:\n{}", pluginTree);
|
||||
|
||||
@@ -468,16 +469,13 @@ public final class PluginProcessor
|
||||
Path descriptorPath = directory.resolve(PluginConstants.FILE_DESCRIPTOR);
|
||||
|
||||
if (Files.exists(descriptorPath)) {
|
||||
|
||||
boolean core = Files.exists(directory.resolve(PluginConstants.FILE_CORE));
|
||||
|
||||
ClassLoader cl = createClassLoader(classLoader, smp);
|
||||
|
||||
InstalledPluginDescriptor descriptor = createDescriptor(cl, descriptorPath);
|
||||
|
||||
WebResourceLoader resourceLoader = createWebResourceLoader(directory);
|
||||
|
||||
plugin = new InstalledPlugin(descriptor, cl, resourceLoader, directory, core);
|
||||
plugin = new InstalledPlugin(descriptor, cl, resourceLoader, directory, smp.isCore());
|
||||
} else {
|
||||
logger.warn("found plugin directory without plugin descriptor");
|
||||
}
|
||||
|
||||
@@ -24,10 +24,10 @@
|
||||
|
||||
package sonia.scm.plugin;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.Stage;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
@@ -35,76 +35,62 @@ import java.util.LinkedHashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
public final class PluginTree
|
||||
{
|
||||
public final class PluginTree {
|
||||
|
||||
/** Field description */
|
||||
private static final int SCM_VERSION = 2;
|
||||
|
||||
/**
|
||||
* the logger for PluginTree
|
||||
*/
|
||||
private static final Logger logger =
|
||||
LoggerFactory.getLogger(PluginTree.class);
|
||||
private static final Logger LOG = LoggerFactory.getLogger(PluginTree.class);
|
||||
|
||||
//~--- constructors ---------------------------------------------------------
|
||||
private final Stage stage;
|
||||
private final List<PluginNode> rootNodes;
|
||||
|
||||
/**
|
||||
* Constructs ...
|
||||
*
|
||||
*
|
||||
* @param smps
|
||||
*/
|
||||
public PluginTree(ExplodedSmp... smps)
|
||||
{
|
||||
this(Arrays.asList(smps));
|
||||
public PluginTree(Stage stage, ExplodedSmp... smps) {
|
||||
this(stage, Arrays.asList(smps));
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs ...
|
||||
*
|
||||
*
|
||||
* @param smps
|
||||
*/
|
||||
public PluginTree(Collection<ExplodedSmp> smps)
|
||||
{
|
||||
|
||||
public PluginTree(Stage stage, Collection<ExplodedSmp> smps) {
|
||||
this.stage = stage;
|
||||
smps.forEach(s -> {
|
||||
InstalledPluginDescriptor plugin = s.getPlugin();
|
||||
logger.trace("plugin: {}", plugin.getInformation().getName());
|
||||
logger.trace("dependencies: {}", plugin.getDependencies());
|
||||
logger.trace("optional dependencies: {}", plugin.getOptionalDependencies());
|
||||
LOG.trace("plugin: {}", plugin.getInformation().getName());
|
||||
LOG.trace("dependencies: {}", plugin.getDependencies());
|
||||
LOG.trace("optional dependencies: {}", plugin.getOptionalDependencies());
|
||||
});
|
||||
|
||||
rootNodes = new SmpNodeBuilder().buildNodeTree(smps);
|
||||
|
||||
for (ExplodedSmp smp : smps)
|
||||
{
|
||||
InstalledPluginDescriptor plugin = smp.getPlugin();
|
||||
for (ExplodedSmp smp : smps) {
|
||||
checkIfSupported(smp);
|
||||
}
|
||||
}
|
||||
|
||||
if (plugin.getScmVersion() != SCM_VERSION)
|
||||
{
|
||||
//J-
|
||||
throw new PluginException(
|
||||
String.format(
|
||||
"scm version %s of plugin %s does not match, required is version %s",
|
||||
plugin.getScmVersion(), plugin.getInformation().getId(), SCM_VERSION
|
||||
)
|
||||
);
|
||||
//J+
|
||||
}
|
||||
private void checkIfSupported(ExplodedSmp smp) {
|
||||
InstalledPluginDescriptor plugin = smp.getPlugin();
|
||||
|
||||
PluginCondition condition = plugin.getCondition();
|
||||
if (plugin.getScmVersion() != SCM_VERSION) {
|
||||
throw new PluginException(
|
||||
String.format(
|
||||
"scm version %s of plugin %s does not match, required is version %s",
|
||||
plugin.getScmVersion(), plugin.getInformation().getId(), SCM_VERSION
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!condition.isSupported())
|
||||
{
|
||||
//J-
|
||||
checkIfConditionsMatch(smp, plugin);
|
||||
}
|
||||
|
||||
private void checkIfConditionsMatch(ExplodedSmp smp, InstalledPluginDescriptor plugin) {
|
||||
PluginCondition condition = plugin.getCondition();
|
||||
if (!condition.isSupported()) {
|
||||
if (smp.isCore() && stage == Stage.DEVELOPMENT) {
|
||||
LOG.warn("plugin {} does not match conditions {}", plugin.getInformation().getId(), condition);
|
||||
} else {
|
||||
throw new PluginConditionFailedException(
|
||||
condition,
|
||||
String.format(
|
||||
@@ -112,29 +98,17 @@ public final class PluginTree
|
||||
plugin.getInformation().getId(), condition
|
||||
)
|
||||
);
|
||||
//J+
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//~--- get methods ----------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public List<PluginNode> getLeafLastNodes()
|
||||
{
|
||||
public List<PluginNode> getLeafLastNodes() {
|
||||
LinkedHashSet<PluginNode> leafFirst = new LinkedHashSet<>();
|
||||
|
||||
rootNodes.forEach(node -> appendLeafFirst(leafFirst, node));
|
||||
|
||||
LinkedList<PluginNode> leafLast = new LinkedList<>();
|
||||
|
||||
leafFirst.forEach(leafLast::addFirst);
|
||||
|
||||
return leafLast;
|
||||
}
|
||||
|
||||
@@ -143,9 +117,6 @@ public final class PluginTree
|
||||
leafFirst.add(node);
|
||||
}
|
||||
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder buffer = new StringBuilder();
|
||||
@@ -163,8 +134,4 @@ public final class PluginTree
|
||||
}
|
||||
}
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
|
||||
/** Field description */
|
||||
private final List<PluginNode> rootNodes;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user