summaryrefslogtreecommitdiffstats
path: root/config-model
diff options
context:
space:
mode:
authorBjørn Christian Seime <bjorncs@yahooinc.com>2023-06-15 16:49:48 +0200
committerBjørn Christian Seime <bjorncs@yahooinc.com>2023-06-15 16:49:48 +0200
commit4611c4e3ab75f2a3cd291495fc8f0d4f32425807 (patch)
tree85faff4e70b90d67195b28f69cce6d436785247b /config-model
parent12da7ac0dc15cc66b6c6a9621472d2ec8e1e73d0 (diff)
Add token support to `client` configuration
Diffstat (limited to 'config-model')
-rw-r--r--config-model/src/main/java/com/yahoo/config/model/deploy/TestProperties.java9
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/http/Client.java29
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/xml/CloudDataPlaneFilter.java41
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java56
-rw-r--r--config-model/src/main/resources/schema/containercluster.rnc8
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/CertificateRemovalChangeValidatorTest.java6
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/xml/CloudDataPlaneFilterTest.java41
-rw-r--r--config-model/src/test/schema-test-files/services.xml7
8 files changed, 159 insertions, 38 deletions
diff --git a/config-model/src/main/java/com/yahoo/config/model/deploy/TestProperties.java b/config-model/src/main/java/com/yahoo/config/model/deploy/TestProperties.java
index 0e39b7b5c3a..2b55b1f1d10 100644
--- a/config-model/src/main/java/com/yahoo/config/model/deploy/TestProperties.java
+++ b/config-model/src/main/java/com/yahoo/config/model/deploy/TestProperties.java
@@ -11,12 +11,14 @@ import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.AthenzDomain;
import com.yahoo.config.provision.CloudAccount;
import com.yahoo.config.provision.ClusterSpec;
+import com.yahoo.config.provision.DataplaneToken;
import com.yahoo.config.provision.HostName;
import com.yahoo.config.provision.Zone;
import com.yahoo.vespa.model.container.ApplicationContainerCluster;
import java.net.URI;
import java.security.cert.X509Certificate;
+import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
@@ -85,6 +87,7 @@ public class TestProperties implements ModelContext.Properties, ModelContext.Fea
private boolean allowUserFilters = true;
private boolean allowMoreThanOneContentGroupDown = false;
private boolean enableConditionalPutRemoveWriteRepair = false;
+ private List<DataplaneToken> dataplaneTokens;
@Override public ModelContext.FeatureFlags featureFlags() { return this; }
@Override public boolean multitenant() { return multitenant; }
@@ -144,6 +147,7 @@ public class TestProperties implements ModelContext.Properties, ModelContext.Fea
@Override public boolean enableGlobalPhase() { return true; } // Enable global-phase by default for unit tests only
@Override public boolean allowMoreThanOneContentGroupDown(ClusterSpec.Id id) { return allowMoreThanOneContentGroupDown; }
@Override public boolean enableConditionalPutRemoveWriteRepair() { return enableConditionalPutRemoveWriteRepair; }
+ @Override public List<DataplaneToken> dataplaneTokens() { return dataplaneTokens; }
public TestProperties sharedStringRepoNoReclaim(boolean sharedStringRepoNoReclaim) {
this.sharedStringRepoNoReclaim = sharedStringRepoNoReclaim;
@@ -384,6 +388,11 @@ public class TestProperties implements ModelContext.Properties, ModelContext.Fea
public TestProperties setAllowUserFilters(boolean b) { this.allowUserFilters = b; return this; }
+ public TestProperties setDataplaneTokens(Collection<DataplaneToken> tokens) {
+ this.dataplaneTokens = List.copyOf(tokens);
+ return this;
+ }
+
public static class Spec implements ConfigServerSpec {
private final String hostName;
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/http/Client.java b/config-model/src/main/java/com/yahoo/vespa/model/container/http/Client.java
index 7707949714e..1388e9647a6 100644
--- a/config-model/src/main/java/com/yahoo/vespa/model/container/http/Client.java
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/http/Client.java
@@ -1,6 +1,8 @@
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.model.container.http;
+import com.yahoo.config.provision.DataplaneToken;
+
import java.security.cert.X509Certificate;
import java.util.List;
@@ -10,19 +12,22 @@ import java.util.List;
* @author mortent
*/
public class Client {
- private String id;
- private List<String> permissions;
- private List<X509Certificate> certificates;
- private boolean internal;
-
- public Client(String id, List<String> permissions, List<X509Certificate> certificates) {
- this(id, permissions, certificates, false);
+ private final String id;
+ private final List<String> permissions;
+ private final List<X509Certificate> certificates;
+ private final List<DataplaneToken> tokens;
+ private final boolean internal;
+
+ public Client(String id, List<String> permissions, List<X509Certificate> certificates, List<DataplaneToken> tokens) {
+ this(id, permissions, certificates, tokens, false);
}
- private Client(String id, List<String> permissions, List<X509Certificate> certificates, boolean internal) {
+ private Client(String id, List<String> permissions, List<X509Certificate> certificates, List<DataplaneToken> tokens,
+ boolean internal) {
this.id = id;
- this.permissions = permissions;
- this.certificates = certificates;
+ this.permissions = List.copyOf(permissions);
+ this.certificates = List.copyOf(certificates);
+ this.tokens = List.copyOf(tokens);
this.internal = internal;
}
@@ -38,11 +43,13 @@ public class Client {
return certificates;
}
+ public List<DataplaneToken> tokens() { return tokens; }
+
public boolean internal() {
return internal;
}
public static Client internalClient(List<X509Certificate> certificates) {
- return new Client("_internal", List.of("read","write"), certificates, true);
+ return new Client("_internal", List.of("read","write"), certificates, List.of(), true);
}
}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/xml/CloudDataPlaneFilter.java b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/CloudDataPlaneFilter.java
index 6767a61d02b..548e05014d0 100644
--- a/config-model/src/main/java/com/yahoo/vespa/model/container/xml/CloudDataPlaneFilter.java
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/CloudDataPlaneFilter.java
@@ -4,27 +4,40 @@ package com.yahoo.vespa.model.container.xml;
import com.yahoo.component.ComponentSpecification;
import com.yahoo.component.chain.dependencies.Dependencies;
import com.yahoo.component.chain.model.ChainedComponentModel;
+import com.yahoo.config.model.deploy.DeployState;
+import com.yahoo.config.provision.DataplaneToken;
import com.yahoo.container.bundle.BundleInstantiationSpecification;
import com.yahoo.jdisc.http.filter.security.cloud.config.CloudDataPlaneFilterConfig;
import com.yahoo.security.X509CertificateUtils;
+import com.yahoo.security.token.TokenDomain;
import com.yahoo.vespa.model.container.ApplicationContainerCluster;
import com.yahoo.vespa.model.container.http.Client;
import com.yahoo.vespa.model.container.http.Filter;
+import java.util.Collection;
import java.util.List;
+import static com.yahoo.security.ArrayUtils.concat;
+import static com.yahoo.security.ArrayUtils.fromUtf8Bytes;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
class CloudDataPlaneFilter extends Filter implements CloudDataPlaneFilterConfig.Producer {
private static final String CLASS = "com.yahoo.jdisc.http.filter.security.cloud.CloudDataPlaneFilter";
private static final String BUNDLE = "jdisc-security-filters";
- private final ApplicationContainerCluster cluster;
- private final boolean legacyMode;
+ private final Collection<Client> clients;
+ private final boolean clientsLegacyMode;
+ private final String tokenContext;
- CloudDataPlaneFilter(ApplicationContainerCluster cluster, boolean legacyMode) {
+ CloudDataPlaneFilter(ApplicationContainerCluster cluster, DeployState state) {
super(model());
- this.cluster = cluster;
- this.legacyMode = legacyMode;
+ this.clients = List.copyOf(cluster.getClients());
+ this.clientsLegacyMode = cluster.clientsLegacyMode();
+ // Token domain must be identical to the domain used for generating the tokens
+ this.tokenContext = fromUtf8Bytes(TokenDomain.of(fromUtf8Bytes(concat(
+ "Vespa Cloud tenant data plane:".getBytes(UTF_8),
+ state.getProperties().applicationId().tenant().value().getBytes(UTF_8)))).checkHashContext());
}
private static ChainedComponentModel model() {
@@ -36,18 +49,26 @@ class CloudDataPlaneFilter extends Filter implements CloudDataPlaneFilterConfig.
@Override
public void getConfig(CloudDataPlaneFilterConfig.Builder builder) {
- if (legacyMode) {
+ if (clientsLegacyMode) {
builder.legacyMode(true);
} else {
- List<Client> clients = cluster.getClients();
- builder.legacyMode(false);
- List<CloudDataPlaneFilterConfig.Clients.Builder> clientsList = clients.stream()
+ var clientsCfg = clients.stream()
.map(x -> new CloudDataPlaneFilterConfig.Clients.Builder()
.id(x.id())
.certificates(X509CertificateUtils.toPem(x.certificates()))
+ .tokens(tokensConfig(x.tokens()))
.permissions(x.permissions()))
.toList();
- builder.clients(clientsList);
+ builder.clients(clientsCfg).legacyMode(false).tokenContext(tokenContext);
}
}
+
+ private static List<CloudDataPlaneFilterConfig.Clients.Tokens.Builder> tokensConfig(Collection<DataplaneToken> tokens) {
+ return tokens.stream()
+ .map(token -> new CloudDataPlaneFilterConfig.Clients.Tokens.Builder()
+ .id(token.tokenId())
+ .fingerprints(token.versions().stream().map(DataplaneToken.Version::fingerprint).toList())
+ .checkAccessHashes(token.versions().stream().map(DataplaneToken.Version::checkAccessHash).toList()))
+ .toList();
+ }
}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java
index adf805a9d10..2b5232eba8c 100644
--- a/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java
@@ -29,6 +29,7 @@ import com.yahoo.config.provision.AthenzService;
import com.yahoo.config.provision.Capacity;
import com.yahoo.config.provision.ClusterMembership;
import com.yahoo.config.provision.ClusterSpec;
+import com.yahoo.config.provision.DataplaneToken;
import com.yahoo.config.provision.HostName;
import com.yahoo.config.provision.InstanceName;
import com.yahoo.config.provision.NodeType;
@@ -121,6 +122,7 @@ import java.util.Optional;
import java.util.OptionalInt;
import java.util.Set;
import java.util.function.Consumer;
+import java.util.function.Function;
import java.util.logging.Level;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@@ -471,7 +473,7 @@ public class ContainerModelBuilder extends ConfigModelBuilder<ContainerModel> {
var dataplanePort = getDataplanePort(deployState);
// Setup secure filter chain
var secureChain = new HttpFilterChain("cloud-data-plane-secure", HttpFilterChain.Type.SYSTEM);
- secureChain.addInnerComponent(new CloudDataPlaneFilter(cluster, cluster.clientsLegacyMode()));
+ secureChain.addInnerComponent(new CloudDataPlaneFilter(cluster, deployState));
cluster.getHttp().getFilterChains().add(secureChain);
// Set cloud data plane filter as default request filter chain for data plane connector
cluster.getHttp().getHttpServer().orElseThrow().getConnectorFactories().stream()
@@ -505,15 +507,16 @@ public class ContainerModelBuilder extends ConfigModelBuilder<ContainerModel> {
Element clientsElement = XML.getChild(spec, "clients");
boolean legacyMode = false;
if (clientsElement == null) {
- Client defaultClient = new Client("default",
- List.of(),
- getCertificates(app.getFile(Path.fromString("security/clients.pem"))));
- clients = List.of(defaultClient);
+ clients = List.of(new Client(
+ "default", List.of(), getCertificates(app.getFile(Path.fromString("security/clients.pem"))), List.of()));
legacyMode = true;
} else {
clients = XML.getChildren(clientsElement, "client").stream()
- .map(this::getClient)
+ .flatMap(elem -> getClient(elem, deployState).stream())
.toList();
+ boolean atLeastOneClientWithCertificate = clients.stream().anyMatch(client -> !client.certificates().isEmpty());
+ if (!atLeastOneClientWithCertificate)
+ throw new IllegalArgumentException("At least one client must require a certificate");
}
List<X509Certificate> operatorAndTesterCertificates = deployState.getProperties().operatorCertificates();
@@ -522,9 +525,9 @@ public class ContainerModelBuilder extends ConfigModelBuilder<ContainerModel> {
cluster.setClients(legacyMode, clients);
}
- private Client getClient(Element clientElement) {
- String id = XML.attribute("id", clientElement).orElseThrow();
- if (id.startsWith("_")) throw new IllegalArgumentException("Invalid client id '%s', id cannot start with '_'".formatted(id));
+ private Optional<Client> getClient(Element clientElement, DeployState state) {
+ String clientId = XML.attribute("id", clientElement).orElseThrow();
+ if (clientId.startsWith("_")) throw new IllegalArgumentException("Invalid client id '%s', id cannot start with '_'".formatted(clientId));
List<String> permissions = XML.attribute("permissions", clientElement)
.map(p -> p.split(",")).stream()
.flatMap(Arrays::stream)
@@ -535,12 +538,43 @@ public class ContainerModelBuilder extends ConfigModelBuilder<ContainerModel> {
var file = app.getFile(Path.fromString(certElem.getAttribute("file")));
if (!file.exists()) {
throw new IllegalArgumentException("Certificate file '%s' for client '%s' does not exist"
- .formatted(file.getPath().getRelative(), id));
+ .formatted(file.getPath().getRelative(), clientId));
}
return getCertificates(file).stream();
})
.toList();
- return new Client(id, permissions, certificates);
+ // A client cannot use both tokens and certificates
+ if (!certificates.isEmpty()) return Optional.of(new Client(clientId, permissions, certificates, List.of()));
+
+ var knownTokens = state.getProperties().dataplaneTokens().stream()
+ .collect(Collectors.toMap(DataplaneToken::tokenId, Function.identity()));
+
+ var referencedTokens = XML.getChildren(clientElement, "token").stream()
+ .map(elem -> {
+ var tokenId = elem.getAttribute("id");
+ var token = knownTokens.get(tokenId);
+ if (token == null)
+ throw new IllegalArgumentException(
+ "Token '%s' for client '%s' does not exist".formatted(tokenId, clientId));
+ return token;
+ })
+ .filter(token -> {
+ boolean empty = token.versions().isEmpty();
+ if (empty)
+ log.logApplicationPackage(
+ WARNING, "Token '%s' for client '%s' has no activate versions"
+ .formatted(token.tokenId(), clientId));
+ return !empty;
+ })
+ .toList();
+
+ // Don't include 'client' that refers to token without versions
+ if (referencedTokens.isEmpty()) {
+ log.log(Level.INFO, "Skipping client '%s' as it does not refer to any activate tokens".formatted(clientId));
+ return Optional.empty();
+ }
+
+ return Optional.of(new Client(clientId, permissions, List.of(), referencedTokens));
}
private List<X509Certificate> getCertificates(ApplicationFile file) {
diff --git a/config-model/src/main/resources/schema/containercluster.rnc b/config-model/src/main/resources/schema/containercluster.rnc
index b8c02b013aa..285911549d7 100644
--- a/config-model/src/main/resources/schema/containercluster.rnc
+++ b/config-model/src/main/resources/schema/containercluster.rnc
@@ -140,9 +140,11 @@ Clients = element clients {
Client = element client {
ComponentId &
attribute permissions { string } &
- element certificate {
- attribute file { string }
- }+
+ (
+ element certificate { attribute file { string } }+
+ |
+ element token { attribute id { string } }+
+ )
}
# SEARCH:
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/CertificateRemovalChangeValidatorTest.java b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/CertificateRemovalChangeValidatorTest.java
index b955ada20d9..d8dfe204453 100644
--- a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/CertificateRemovalChangeValidatorTest.java
+++ b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/CertificateRemovalChangeValidatorTest.java
@@ -31,9 +31,9 @@ public class CertificateRemovalChangeValidatorTest {
void validate() {
Instant now = LocalDate.parse("2000-01-01", DateTimeFormatter.ISO_DATE).atStartOfDay().atZone(ZoneOffset.UTC).toInstant();
- Client c1 = new Client("c1", List.of(), List.of(certificate("cn=c1")));
- Client c2 = new Client("c2", List.of(), List.of(certificate("cn=c2")));
- Client c3 = new Client("c3", List.of(), List.of(certificate("cn=c3")));
+ Client c1 = new Client("c1", List.of(), List.of(certificate("cn=c1")), List.of());
+ Client c2 = new Client("c2", List.of(), List.of(certificate("cn=c2")), List.of());
+ Client c3 = new Client("c3", List.of(), List.of(certificate("cn=c3")), List.of());
Client internal = Client.internalClient(List.of(certificate("cn=internal")));
CertificateRemovalChangeValidator validator = new CertificateRemovalChangeValidator();
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/xml/CloudDataPlaneFilterTest.java b/config-model/src/test/java/com/yahoo/vespa/model/container/xml/CloudDataPlaneFilterTest.java
index 94d92b355f9..5bb0254f1cc 100644
--- a/config-model/src/test/java/com/yahoo/vespa/model/container/xml/CloudDataPlaneFilterTest.java
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/xml/CloudDataPlaneFilterTest.java
@@ -6,6 +6,7 @@ import com.yahoo.config.model.builder.xml.test.DomBuilderTest;
import com.yahoo.config.model.deploy.DeployState;
import com.yahoo.config.model.deploy.TestProperties;
import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.config.provision.DataplaneToken;
import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.RegionName;
import com.yahoo.config.provision.SystemName;
@@ -36,12 +37,14 @@ import java.security.KeyPair;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
+import java.util.Collection;
import java.util.List;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertIterableEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -118,6 +121,41 @@ public class CloudDataPlaneFilterTest extends ContainerModelBuilderTestBase {
}
@Test
+ void generates_correct_config_for_tokens() throws IOException {
+ var certFile = securityFolder.resolve("foo.pem");
+ var clusterElem = DomBuilderTest.parse(
+ """
+ <container version='1.0'>
+ <clients>
+ <client id="foo" permissions="read,write">
+ <certificate file="%s"/>
+ </client>
+ <client id="bar" permissions="read">
+ <token id="my-token"/>
+ </client>
+ </clients>
+ </container>
+ """
+ .formatted(applicationFolder.toPath().relativize(certFile).toString()));
+ createCertificate(certFile);
+ buildModel(clusterElem);
+
+ var cfg = root.getConfig(CloudDataPlaneFilterConfig.class, cloudDataPlaneFilterConfigId);
+ var tokenClient = cfg.clients().stream().filter(c -> c.id().equals("bar")).findAny().orElse(null);
+ assertNotNull(tokenClient);
+ assertEquals(List.of("read"), tokenClient.permissions());
+ var expectedTokenCfg = tokenConfig(
+ "my-token", List.of("myfingerprint1", "myfingerprint2"), List.of("myaccesshash1", "myaccesshash2"));
+ assertEquals(List.of(expectedTokenCfg), tokenClient.tokens());
+ }
+
+ private static CloudDataPlaneFilterConfig.Clients.Tokens tokenConfig(
+ String id, Collection<String> fingerprints, Collection<String> accessCheckHashes) {
+ return new CloudDataPlaneFilterConfig.Clients.Tokens.Builder()
+ .id(id).fingerprints(fingerprints).checkAccessHashes(accessCheckHashes).build();
+ }
+
+ @Test
public void it_rejects_files_without_certificates() throws IOException {
Path certFile = securityFolder.resolve("foo.pem");
Element clusterElem = DomBuilderTest.parse(
@@ -189,6 +227,9 @@ public class CloudDataPlaneFilterTest extends ContainerModelBuilderTestBase {
.properties(
new TestProperties()
.setEndpointCertificateSecrets(Optional.of(new EndpointCertificateSecrets("CERT", "KEY")))
+ .setDataplaneTokens(List.of(new DataplaneToken("my-token", List.of(
+ new DataplaneToken.Version("myfingerprint1", "myaccesshash1"),
+ new DataplaneToken.Version("myfingerprint2", "myaccesshash2")))))
.setHostedVespa(true))
.zone(new Zone(SystemName.PublicCd, Environment.dev, RegionName.defaultName()))
.build();
diff --git a/config-model/src/test/schema-test-files/services.xml b/config-model/src/test/schema-test-files/services.xml
index 8806a4e082a..e5cb7e8ef54 100644
--- a/config-model/src/test/schema-test-files/services.xml
+++ b/config-model/src/test/schema-test-files/services.xml
@@ -218,6 +218,13 @@
<certificate file="security/file1.pem" />
<certificate file="security/file2.pem" />
</client>
+ <client id="client3" permissions="read">
+ <token id="my-token-1" />
+ <token id="my-token-2" />
+ </client>
+ <client id="client4" permissions="write">
+ <token id="my-token-3" />
+ </client>
</clients>
<document-processing>