diff options
author | Håkon Hallingstad <hakon@yahooinc.com> | 2022-12-05 19:29:04 +0100 |
---|---|---|
committer | Håkon Hallingstad <hakon@yahooinc.com> | 2022-12-05 19:29:04 +0100 |
commit | 268b74729c9c3ff74806875e05f52585000b4b9d (patch) | |
tree | 103bef96b641fd09b11cbab239b8886ae11d8f5f | |
parent | c04788cb28e3b72e19b4ad11c87031ff85f17681 (diff) |
Limit fields allowed to be patched from tenant host
3 files changed, 91 insertions, 1 deletions
diff --git a/config-provisioning/src/main/java/com/yahoo/config/provision/security/NodePrincipal.java b/config-provisioning/src/main/java/com/yahoo/config/provision/security/NodePrincipal.java new file mode 100644 index 00000000000..7e58c9c15ac --- /dev/null +++ b/config-provisioning/src/main/java/com/yahoo/config/provision/security/NodePrincipal.java @@ -0,0 +1,51 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.config.provision.security; + +import java.security.Principal; +import java.util.Objects; + +/** + * Represents the identity of a hosted Vespa node + * + * @author bjorncs + */ +public class NodePrincipal implements Principal { + + private final NodeIdentity identity; + + public NodePrincipal(NodeIdentity identity) { + this.identity = identity; + } + + public NodeIdentity getIdentity() { + return identity; + } + + @Override + public String getName() { + StringBuilder builder = new StringBuilder(identity.nodeType().name()); + identity.hostname().ifPresent(hostname -> builder.append('/').append(hostname.value())); + identity.applicationId().ifPresent(applicationId -> builder.append('/').append(applicationId.toShortString())); + return builder.toString(); + } + + @Override + public String toString() { + return "NodePrincipal{" + + "identity=" + identity + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + NodePrincipal that = (NodePrincipal) o; + return Objects.equals(identity, that.identity); + } + + @Override + public int hashCode() { + return Objects.hash(identity); + } +} diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodePatcher.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodePatcher.java index 328be3b32b8..e57868dac54 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodePatcher.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodePatcher.java @@ -33,6 +33,7 @@ import java.time.Duration; import java.time.Instant; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; @@ -77,10 +78,34 @@ public class NodePatcher { * Note: This may patch more than one node if the field being patched must be applied recursively to host and node. */ public void patch(String hostname, InputStream json) { + unifiedPatch(hostname, json, false); + } + + /** Apply given JSON from a tenant host that may have been compromised. */ + public void patchFromUntrustedTenantHost(String hostname, InputStream json) { + unifiedPatch(hostname, json, true); + } + + private void unifiedPatch(String hostname, InputStream json, boolean untrustedTenantHost) { Inspector root = Exceptions.uncheck(() -> SlimeUtils.jsonToSlime(json.readAllBytes())).get(); Map<String, Inspector> fields = new HashMap<>(); root.traverse(fields::put); + if (untrustedTenantHost) { + var disallowedFields = new HashSet<>(fields.keySet()); + disallowedFields.removeAll(Set.of("currentDockerImage", + "currentFirmwareCheck", + "currentOsVersion", + "currentRebootGeneration", + "currentRestartGeneration", + "reports", + "trustStore", + "vespaVersion")); + if (!disallowedFields.isEmpty()) { + throw new IllegalArgumentException("Patching fields not supported: " + disallowedFields); + } + } + // Create views grouping fields by their locking requirements Map<String, Inspector> regularFields = Maps.filterKeys(fields, k -> !IP_CONFIG_FIELDS.contains(k)); Map<String, Inspector> ipConfigFields = Maps.filterKeys(fields, IP_CONFIG_FIELDS::contains); diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodesV2ApiHandler.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodesV2ApiHandler.java index 2f35d0e7e81..6e80e559b20 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodesV2ApiHandler.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodesV2ApiHandler.java @@ -11,6 +11,7 @@ import com.yahoo.config.provision.NodeFlavors; import com.yahoo.config.provision.NodeResources; import com.yahoo.config.provision.NodeType; import com.yahoo.config.provision.TenantName; +import com.yahoo.config.provision.security.NodePrincipal; import com.yahoo.container.jdisc.HttpRequest; import com.yahoo.container.jdisc.HttpResponse; import com.yahoo.container.jdisc.ThreadedHttpRequestHandler; @@ -173,7 +174,11 @@ public class NodesV2ApiHandler extends ThreadedHttpRequestHandler { if (path.matches("/nodes/v2/node/{hostname}")) { NodePatcher patcher = new NodePatcher(nodeFlavors, nodeRepository); String hostname = path.get("hostname"); - patcher.patch(hostname, request.getData()); + if (isTenantPeer(request)) { + patcher.patchFromUntrustedTenantHost(hostname, request.getData()); + } else { + patcher.patch(hostname, request.getData()); + } return new MessageResponse("Updated " + hostname); } else if (path.matches("/nodes/v2/application/{applicationId}")) { @@ -195,6 +200,15 @@ public class NodesV2ApiHandler extends ThreadedHttpRequestHandler { throw new NotFoundException("Nothing at '" + path + "'"); } + /** Returns true if the peer is a tenant host or node. */ + private boolean isTenantPeer(HttpRequest request) { + return request.getJDiscRequest().getUserPrincipal() instanceof NodePrincipal nodePrincipal && + switch (nodePrincipal.getIdentity().nodeType()) { + case host, tenant -> true; + default -> false; + }; + } + private HttpResponse handlePOST(HttpRequest request) { Path path = new Path(request.getUri()); if (path.matches("/nodes/v2/command/restart")) { |