aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/xml/CloudDataPlaneFilter.java5
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/xml/CloudDataPlaneFilterTest.java12
-rw-r--r--config-provisioning/src/main/java/com/yahoo/config/provision/DataplaneToken.java4
-rw-r--r--configdefinitions/src/vespa/cloud-data-plane-filter.def1
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/tenant/DataplaneTokenSerializer.java8
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionPreparerTest.java2
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/tenant/DataplaneTokenSerializerTest.java9
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dataplanetoken/DataplaneToken.java5
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dataplanetoken/DataplaneTokenVersions.java4
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/DataplaneTokenSerializer.java7
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java8
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/dataplanetoken/DataplaneTokenService.java11
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/dataplanetoken/DataplaneTokenServiceTest.java18
-rw-r--r--jdisc-security-filters/pom.xml6
-rw-r--r--jdisc-security-filters/src/main/java/com/yahoo/jdisc/http/filter/security/cloud/CloudDataPlaneFilter.java34
-rw-r--r--jdisc-security-filters/src/test/java/com/yahoo/jdisc/http/filter/security/cloud/CloudDataPlaneFilterTest.java49
16 files changed, 143 insertions, 40 deletions
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 2deaf81d338..efa5ee01506 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
@@ -13,6 +13,7 @@ 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.time.Instant;
import java.util.Collection;
import java.util.List;
@@ -62,7 +63,9 @@ class CloudDataPlaneFilter extends Filter implements CloudDataPlaneFilterConfig.
.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()))
+ .checkAccessHashes(token.versions().stream().map(DataplaneToken.Version::checkAccessHash).toList())
+ .expirations(token.versions().stream().map(v -> v.expiration().map(Instant::toString).orElse("<none>")).toList()))
.toList();
}
+
}
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 e11eec1ffd7..02ff7b8a03f 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
@@ -35,6 +35,7 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyPair;
import java.security.cert.X509Certificate;
+import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Collection;
@@ -147,14 +148,15 @@ public class CloudDataPlaneFilterTest extends ContainerModelBuilderTestBase {
assertEquals(List.of("read"), tokenClient.permissions());
assertTrue(tokenClient.certificates().isEmpty());
var expectedTokenCfg = tokenConfig(
- "my-token", List.of("myfingerprint1", "myfingerprint2"), List.of("myaccesshash1", "myaccesshash2"));
+ "my-token", List.of("myfingerprint1", "myfingerprint2"), List.of("myaccesshash1", "myaccesshash2"),
+ List.of("<none>", "2243-10-17T00:00:00Z"));
assertEquals(List.of(expectedTokenCfg), tokenClient.tokens());
}
private static CloudDataPlaneFilterConfig.Clients.Tokens tokenConfig(
- String id, Collection<String> fingerprints, Collection<String> accessCheckHashes) {
+ String id, Collection<String> fingerprints, Collection<String> accessCheckHashes, Collection<String> expirations) {
return new CloudDataPlaneFilterConfig.Clients.Tokens.Builder()
- .id(id).fingerprints(fingerprints).checkAccessHashes(accessCheckHashes).build();
+ .id(id).fingerprints(fingerprints).checkAccessHashes(accessCheckHashes).expirations(expirations).build();
}
@Test
@@ -230,8 +232,8 @@ public class CloudDataPlaneFilterTest extends ContainerModelBuilderTestBase {
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")))))
+ new DataplaneToken.Version("myfingerprint1", "myaccesshash1", Optional.empty()),
+ new DataplaneToken.Version("myfingerprint2", "myaccesshash2", Optional.of(Instant.EPOCH.plus(Duration.ofDays(100000))))))))
.setHostedVespa(true))
.zone(new Zone(SystemName.PublicCd, Environment.dev, RegionName.defaultName()))
.build();
diff --git a/config-provisioning/src/main/java/com/yahoo/config/provision/DataplaneToken.java b/config-provisioning/src/main/java/com/yahoo/config/provision/DataplaneToken.java
index 9b0367a4652..546a27a31ed 100644
--- a/config-provisioning/src/main/java/com/yahoo/config/provision/DataplaneToken.java
+++ b/config-provisioning/src/main/java/com/yahoo/config/provision/DataplaneToken.java
@@ -1,7 +1,9 @@
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.config.provision;
+import java.time.Instant;
import java.util.List;
+import java.util.Optional;
/**
* Id, fingerprints and check access hashes of a data plane token
@@ -10,6 +12,6 @@ import java.util.List;
*/
public record DataplaneToken(String tokenId, List<Version> versions) {
- public record Version(String fingerprint, String checkAccessHash){
+ public record Version(String fingerprint, String checkAccessHash, Optional<Instant> expiration) {
}
}
diff --git a/configdefinitions/src/vespa/cloud-data-plane-filter.def b/configdefinitions/src/vespa/cloud-data-plane-filter.def
index fbad95e2c2a..d73c5a49c81 100644
--- a/configdefinitions/src/vespa/cloud-data-plane-filter.def
+++ b/configdefinitions/src/vespa/cloud-data-plane-filter.def
@@ -9,3 +9,4 @@ clients[].certificates[] string
clients[].tokens[].id string
clients[].tokens[].fingerprints[] string
clients[].tokens[].checkAccessHashes[] string
+clients[].tokens[].expirations[] string
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/tenant/DataplaneTokenSerializer.java b/configserver/src/main/java/com/yahoo/vespa/config/server/tenant/DataplaneTokenSerializer.java
index 27012ae69fe..a8dc48e6c14 100644
--- a/configserver/src/main/java/com/yahoo/vespa/config/server/tenant/DataplaneTokenSerializer.java
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/tenant/DataplaneTokenSerializer.java
@@ -7,7 +7,9 @@ import com.yahoo.slime.Inspector;
import com.yahoo.slime.Slime;
import com.yahoo.slime.SlimeUtils;
+import java.time.Instant;
import java.util.List;
+import java.util.Optional;
/**
* Serialize/deserialize dataplane tokens
@@ -20,6 +22,7 @@ public class DataplaneTokenSerializer {
private static final String VERSIONS_FIELD = "versions";
private static final String FINGERPRINT_FIELD = "fingerPrint";
private static final String CHECKACCESSHASH_FIELD = "checkAccessHash";
+ private static final String EXPIRATION_FIELD = "expiration";
private DataplaneTokenSerializer() {}
@@ -39,9 +42,11 @@ public class DataplaneTokenSerializer {
}
private static DataplaneToken.Version tokenValue(Inspector inspector) {
+ String expirationStr = inspector.field(EXPIRATION_FIELD).asString();
return new DataplaneToken.Version(
inspector.field(FINGERPRINT_FIELD).asString(),
- inspector.field(CHECKACCESSHASH_FIELD).asString());
+ inspector.field(CHECKACCESSHASH_FIELD).asString(),
+ expirationStr.equals("<none>") ? Optional.empty() : Optional.of(Instant.parse(expirationStr)));
}
public static Slime toSlime(List<DataplaneToken> dataplaneTokens) {
@@ -55,6 +60,7 @@ public class DataplaneTokenSerializer {
Cursor val = versions.addObject();
val.setString(FINGERPRINT_FIELD, v.fingerprint());
val.setString(CHECKACCESSHASH_FIELD, v.checkAccessHash());
+ val.setString(EXPIRATION_FIELD, v.expiration().map(Instant::toString).orElse("<none>"));
});
}
return slime;
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionPreparerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionPreparerTest.java
index f7b2d4119a7..52d5ba16562 100644
--- a/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionPreparerTest.java
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionPreparerTest.java
@@ -366,7 +366,7 @@ public class SessionPreparerTest {
TestModelFactory modelFactory = new TestModelFactory(version123);
preparer = createPreparer(new ModelFactoryRegistry(List.of(modelFactory)), HostProvisionerProvider.empty());
ApplicationId applicationId = applicationId("test");
- List<DataplaneToken> expected = List.of(new DataplaneToken("id", List.of(new DataplaneToken.Version("f1", "ch1"))));
+ List<DataplaneToken> expected = List.of(new DataplaneToken("id", List.of(new DataplaneToken.Version("f1", "ch1", Optional.empty()))));
PrepareParams params = new PrepareParams.Builder().applicationId(applicationId)
.dataplaneTokens(expected)
.build();
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/tenant/DataplaneTokenSerializerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/tenant/DataplaneTokenSerializerTest.java
index 505abcb5598..59e98742777 100644
--- a/configserver/src/test/java/com/yahoo/vespa/config/server/tenant/DataplaneTokenSerializerTest.java
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/tenant/DataplaneTokenSerializerTest.java
@@ -5,7 +5,10 @@ import com.yahoo.config.provision.DataplaneToken;
import com.yahoo.slime.Slime;
import org.junit.Test;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
import java.util.List;
+import java.util.Optional;
import static org.junit.Assert.assertEquals;
@@ -18,10 +21,10 @@ public class DataplaneTokenSerializerTest {
public void testSerialization() {
List<DataplaneToken> tokens = List.of(
new DataplaneToken("id1",
- List.of(new DataplaneToken.Version("id1_fingerPrint1", "id1_checkaccesshash1"))),
+ List.of(new DataplaneToken.Version("id1_fingerPrint1", "id1_checkaccesshash1", Optional.empty()))),
new DataplaneToken("id2",
- List.of(new DataplaneToken.Version("id2_fingerPrint1", "id2_checkaccesshash1"),
- new DataplaneToken.Version("id3_fingerPrint1", "id3_checkaccesshash1"))));
+ List.of(new DataplaneToken.Version("id2_fingerPrint1", "id2_checkaccesshash1", Optional.of(Instant.EPOCH)),
+ new DataplaneToken.Version("id3_fingerPrint1", "id3_checkaccesshash1", Optional.of(Instant.EPOCH.plus(20000, ChronoUnit.DAYS))))));
Slime slime = DataplaneTokenSerializer.toSlime(tokens);
List<DataplaneToken> deserialized = DataplaneTokenSerializer.fromSlime(slime.get());
assertEquals(tokens, deserialized);
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dataplanetoken/DataplaneToken.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dataplanetoken/DataplaneToken.java
index f0cc87df1fe..76df5ce13dd 100644
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dataplanetoken/DataplaneToken.java
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dataplanetoken/DataplaneToken.java
@@ -1,6 +1,9 @@
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.api.integration.dataplanetoken;
+import java.time.Instant;
+import java.util.Optional;
+
/**
* Represents a generated data plane token.
*
@@ -8,5 +11,5 @@ package com.yahoo.vespa.hosted.controller.api.integration.dataplanetoken;
*
* @author mortent
*/
-public record DataplaneToken(TokenId tokenId, FingerPrint fingerPrint, String tokenValue) {
+public record DataplaneToken(TokenId tokenId, FingerPrint fingerPrint, String tokenValue, Optional<Instant> expiration) {
}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dataplanetoken/DataplaneTokenVersions.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dataplanetoken/DataplaneTokenVersions.java
index 618bfbc8a41..1ce558bd84e 100644
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dataplanetoken/DataplaneTokenVersions.java
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dataplanetoken/DataplaneTokenVersions.java
@@ -3,6 +3,7 @@ package com.yahoo.vespa.hosted.controller.api.integration.dataplanetoken;
import java.time.Instant;
import java.util.List;
+import java.util.Optional;
/**
* List of dataplane token versions of a token id.
@@ -10,6 +11,7 @@ import java.util.List;
* @author mortent
*/
public record DataplaneTokenVersions(TokenId tokenId, List<Version> tokenVersions) {
- public record Version(FingerPrint fingerPrint, String checkAccessHash, Instant creationTime, String author) {
+ public record Version(FingerPrint fingerPrint, String checkAccessHash, Instant creationTime,
+ Optional<Instant> expiration, String author) {
}
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/DataplaneTokenSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/DataplaneTokenSerializer.java
index 90be14fd349..5df183d9abb 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/DataplaneTokenSerializer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/DataplaneTokenSerializer.java
@@ -10,6 +10,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.dataplanetoken.TokenId;
import java.time.Instant;
import java.util.List;
+import java.util.Optional;
/**
* @author mortent
@@ -23,6 +24,7 @@ public class DataplaneTokenSerializer {
private static final String checkAccessHashField = "checkAccessHash";
private static final String creationTimeField = "creationTime";
private static final String authorField = "author";
+ private static final String expirationField = "expiration";
public static Slime toSlime(List<DataplaneTokenVersions> dataplaneTokenVersions) {
Slime slime = new Slime();
@@ -39,6 +41,7 @@ public class DataplaneTokenSerializer {
versionCursor.setLong(creationTimeField, version.creationTime().toEpochMilli());
versionCursor.setString(creationTimeField, version.creationTime().toString());
versionCursor.setString(authorField, version.author());
+ versionCursor.setString(expirationField, version.expiration().map(Instant::toString).orElse("<none>"));
});
});
return slime;
@@ -55,7 +58,9 @@ public class DataplaneTokenSerializer {
String checkAccessHash = versionCursor.field(checkAccessHashField).asString();
Instant creationTime = SlimeUtils.instant(versionCursor.field(creationTimeField));
String author = versionCursor.field(authorField).asString();
- return new DataplaneTokenVersions.Version(fingerPrint, checkAccessHash, creationTime, author);
+ String expirationStr = versionCursor.field(expirationField).asString();
+ Optional<Instant> expiration = expirationStr.equals("<none>") ? Optional.empty() : Optional.of(Instant.parse(expirationStr));
+ return new DataplaneTokenVersions.Version(fingerPrint, checkAccessHash, creationTime, expiration, author);
})
.toList();
return new DataplaneTokenVersions(id, versions);
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java
index daa64ddb07f..99ad75d0ec4 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java
@@ -111,6 +111,7 @@ import com.yahoo.vespa.hosted.controller.notification.Notification;
import com.yahoo.vespa.hosted.controller.notification.NotificationSource;
import com.yahoo.vespa.hosted.controller.persistence.SupportAccessSerializer;
import com.yahoo.vespa.hosted.controller.restapi.ErrorResponses;
+import com.yahoo.vespa.hosted.controller.restapi.dataplanetoken.DataplaneTokenService;
import com.yahoo.vespa.hosted.controller.routing.RoutingStatus;
import com.yahoo.vespa.hosted.controller.routing.context.DeploymentRoutingContext;
import com.yahoo.vespa.hosted.controller.routing.rotation.RotationId;
@@ -985,12 +986,17 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
private HttpResponse generateToken(String tenant, String tokenid, HttpRequest request) {
- DataplaneToken token = controller.dataplaneTokenService().generateToken(TenantName.from(tenant), TokenId.of(tokenid), request.getJDiscRequest().getUserPrincipal());
+ // 'expiration=PT0S' for no expiration, no 'expiration' for default TTL.
+ Duration expiration = Optional.ofNullable(request.getProperty("expiration"))
+ .map(Duration::parse).orElse(DataplaneTokenService.DEFAULT_TTL);
+ DataplaneToken token = controller.dataplaneTokenService().generateToken(
+ TenantName.from(tenant), TokenId.of(tokenid), expiration, request.getJDiscRequest().getUserPrincipal());
Slime slime = new Slime();
Cursor tokenObject = slime.setObject();
tokenObject.setString("id", token.tokenId().value());
tokenObject.setString("token", token.tokenValue());
tokenObject.setString("fingerprint", token.fingerPrint().value());
+ tokenObject.setString("expiration", token.expiration().map(Instant::toString).orElse("<none>"));
return new SlimeJsonResponse(slime);
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/dataplanetoken/DataplaneTokenService.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/dataplanetoken/DataplaneTokenService.java
index b3e5f663317..32872a01bce 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/dataplanetoken/DataplaneTokenService.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/dataplanetoken/DataplaneTokenService.java
@@ -15,6 +15,8 @@ import com.yahoo.vespa.hosted.controller.api.integration.dataplanetoken.TokenId;
import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
import java.security.Principal;
+import java.time.Duration;
+import java.time.Instant;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
@@ -30,6 +32,7 @@ public class DataplaneTokenService {
private static final String TOKEN_PREFIX = "vespa_cloud_";
private static final int TOKEN_BYTES = 32;
private static final int CHECK_HASH_BYTES = 32;
+ public static final Duration DEFAULT_TTL = Duration.ofDays(30);
private final Controller controller;
@@ -51,10 +54,12 @@ public class DataplaneTokenService {
*
* @param tenantName name of the tenant to connect the token to
* @param tokenId The user generated name/id of the token
+ * @param ttl The time to live of the token. Use {@link Duration#ZERO} for no TTL.
* @param principal The principal making the request
* @return a DataplaneToken containing the secret generated token
*/
- public DataplaneToken generateToken(TenantName tenantName, TokenId tokenId, Principal principal) {
+ public DataplaneToken generateToken(TenantName tenantName, TokenId tokenId, Duration ttl, Principal principal) {
+ Optional<Instant> expiration = ttl.isZero() ? Optional.empty() : Optional.ofNullable(controller.clock().instant().plus(ttl));
TokenDomain tokenDomain = TokenDomain.of("Vespa Cloud tenant data plane:%s".formatted(tenantName.value()));
Token token = TokenGenerator.generateToken(tokenDomain, TOKEN_PREFIX, TOKEN_BYTES);
TokenCheckHash checkHash = TokenCheckHash.of(token, CHECK_HASH_BYTES);
@@ -62,6 +67,7 @@ public class DataplaneTokenService {
FingerPrint.of(token.fingerprint().toDelimitedHexString()),
checkHash.toHexString(),
controller.clock().instant(),
+ expiration,
principal.getName());
CuratorDb curator = controller.curator();
@@ -85,7 +91,8 @@ public class DataplaneTokenService {
curator.writeDataplaneTokens(tenantName, dataplaneTokenVersions);
// Return the data plane token including the secret token.
- return new DataplaneToken(tokenId, FingerPrint.of(token.fingerprint().toDelimitedHexString()), token.secretTokenString());
+ return new DataplaneToken(tokenId, FingerPrint.of(token.fingerprint().toDelimitedHexString()),
+ token.secretTokenString(), expiration);
}
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/dataplanetoken/DataplaneTokenServiceTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/dataplanetoken/DataplaneTokenServiceTest.java
index 066eecc2c95..9a8e43d1597 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/dataplanetoken/DataplaneTokenServiceTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/dataplanetoken/DataplaneTokenServiceTest.java
@@ -12,6 +12,7 @@ import com.yahoo.vespa.hosted.controller.api.role.SimplePrincipal;
import org.junit.jupiter.api.Test;
import java.security.Principal;
+import java.time.Duration;
import java.util.Collection;
import java.util.List;
@@ -29,15 +30,16 @@ public class DataplaneTokenServiceTest {
@Test
void generates_and_persists_token() {
- DataplaneToken dataplaneToken = dataplaneTokenService.generateToken(tenantName, tokenId, principal);
+ DataplaneToken dataplaneToken = dataplaneTokenService.generateToken(tenantName, tokenId, Duration.ofDays(100), principal);
List<DataplaneTokenVersions> dataplaneTokenVersions = dataplaneTokenService.listTokens(tenantName);
assertEquals(dataplaneToken.fingerPrint(), dataplaneTokenVersions.get(0).tokenVersions().get(0).fingerPrint());
+ assertEquals(dataplaneToken.expiration(), dataplaneTokenVersions.get(0).tokenVersions().get(0).expiration());
}
@Test
void generating_new_token_appends() {
- DataplaneToken dataplaneToken1 = dataplaneTokenService.generateToken(tenantName, tokenId, principal);
- DataplaneToken dataplaneToken2 = dataplaneTokenService.generateToken(tenantName, tokenId, principal);
+ DataplaneToken dataplaneToken1 = dataplaneTokenService.generateToken(tenantName, tokenId, Duration.ofDays(1), principal);
+ DataplaneToken dataplaneToken2 = dataplaneTokenService.generateToken(tenantName, tokenId, Duration.ZERO, principal);
assertNotEquals(dataplaneToken1.fingerPrint(), dataplaneToken2.fingerPrint());
List<DataplaneTokenVersions> dataplaneTokenVersions = dataplaneTokenService.listTokens(tenantName);
@@ -52,8 +54,8 @@ public class DataplaneTokenServiceTest {
@Test
void delete_last_fingerprint_deletes_token() {
- DataplaneToken dataplaneToken1 = dataplaneTokenService.generateToken(tenantName, tokenId, principal);
- DataplaneToken dataplaneToken2 = dataplaneTokenService.generateToken(tenantName, tokenId, principal);
+ DataplaneToken dataplaneToken1 = dataplaneTokenService.generateToken(tenantName, tokenId, Duration.ZERO, principal);
+ DataplaneToken dataplaneToken2 = dataplaneTokenService.generateToken(tenantName, tokenId, Duration.ZERO, principal);
dataplaneTokenService.deleteToken(tenantName, tokenId, dataplaneToken1.fingerPrint());
dataplaneTokenService.deleteToken(tenantName, tokenId, dataplaneToken2.fingerPrint());
assertEquals(List.of(), dataplaneTokenService.listTokens(tenantName));
@@ -61,8 +63,8 @@ public class DataplaneTokenServiceTest {
@Test
void deleting_nonexistent_fingerprint_throws() {
- DataplaneToken dataplaneToken = dataplaneTokenService.generateToken(tenantName, tokenId, principal);
- DataplaneToken dataplaneToken2 = dataplaneTokenService.generateToken(tenantName, tokenId, principal);
+ DataplaneToken dataplaneToken = dataplaneTokenService.generateToken(tenantName, tokenId, Duration.ZERO, principal);
+ DataplaneToken dataplaneToken2 = dataplaneTokenService.generateToken(tenantName, tokenId, Duration.ZERO, principal);
dataplaneTokenService.deleteToken(tenantName, tokenId, dataplaneToken.fingerPrint());
// Token currently contains value of "dataplaneToken2"
@@ -72,7 +74,7 @@ public class DataplaneTokenServiceTest {
@Test
void deleting_nonexistent_token_throws() {
- DataplaneToken dataplaneToken = dataplaneTokenService.generateToken(tenantName, tokenId, principal);
+ DataplaneToken dataplaneToken = dataplaneTokenService.generateToken(tenantName, tokenId, Duration.ZERO, principal);
dataplaneTokenService.deleteToken(tenantName, tokenId, dataplaneToken.fingerPrint());
// Token is created and deleted above, no longer exists
diff --git a/jdisc-security-filters/pom.xml b/jdisc-security-filters/pom.xml
index 652b864747d..3440f9089d7 100644
--- a/jdisc-security-filters/pom.xml
+++ b/jdisc-security-filters/pom.xml
@@ -63,6 +63,12 @@
<artifactId>jetty-util</artifactId>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>testutil</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
</dependencies>
<build>
diff --git a/jdisc-security-filters/src/main/java/com/yahoo/jdisc/http/filter/security/cloud/CloudDataPlaneFilter.java b/jdisc-security-filters/src/main/java/com/yahoo/jdisc/http/filter/security/cloud/CloudDataPlaneFilter.java
index 96602fcd899..554c1d924a2 100644
--- a/jdisc-security-filters/src/main/java/com/yahoo/jdisc/http/filter/security/cloud/CloudDataPlaneFilter.java
+++ b/jdisc-security-filters/src/main/java/com/yahoo/jdisc/http/filter/security/cloud/CloudDataPlaneFilter.java
@@ -20,6 +20,8 @@ import com.yahoo.security.token.TokenFingerprint;
import java.security.Principal;
import java.security.cert.X509Certificate;
+import java.time.Clock;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashMap;
@@ -53,21 +55,23 @@ public class CloudDataPlaneFilter extends JsonSecurityRequestFilterBase {
private final boolean legacyMode;
private final List<Client> allowedClients;
private final TokenDomain tokenDomain;
+ private final Clock clock;
@Inject
public CloudDataPlaneFilter(CloudDataPlaneFilterConfig cfg,
ComponentRegistry<DataplaneProxyCredentials> optionalReverseProxy) {
- this(cfg, reverseProxyCert(optionalReverseProxy).orElse(null));
+ this(cfg, reverseProxyCert(optionalReverseProxy).orElse(null), Clock.systemUTC());
}
- CloudDataPlaneFilter(CloudDataPlaneFilterConfig cfg, X509Certificate reverseProxyCert) {
+ CloudDataPlaneFilter(CloudDataPlaneFilterConfig cfg, X509Certificate reverseProxyCert, Clock clock) {
this.legacyMode = cfg.legacyMode();
this.tokenDomain = TokenDomain.of(cfg.tokenContext());
+ this.clock = clock;
if (legacyMode) {
allowedClients = List.of();
log.fine(() -> "Legacy mode enabled");
} else {
- allowedClients = parseClients(cfg, reverseProxyCert);
+ allowedClients = parseClients(cfg, reverseProxyCert, clock);
}
}
@@ -76,7 +80,8 @@ public class CloudDataPlaneFilter extends JsonSecurityRequestFilterBase {
return optionalReverseProxy.allComponents().stream().findAny().map(DataplaneProxyCredentials::certificate);
}
- private static List<Client> parseClients(CloudDataPlaneFilterConfig cfg, X509Certificate reverseProxyCert) {
+ private static List<Client> parseClients(CloudDataPlaneFilterConfig cfg, X509Certificate reverseProxyCert, Clock clock) {
+ var now = clock.instant();
Set<String> ids = new HashSet<>();
List<Client> clients = new ArrayList<>(cfg.clients().size());
boolean hasClientRequiringCertificate = false;
@@ -112,8 +117,14 @@ public class CloudDataPlaneFilter extends JsonSecurityRequestFilterBase {
for (var token : c.tokens()) {
for (int version = 0; version < token.checkAccessHashes().size(); version++) {
var tokenVersion = TokenVersion.of(
- token.id(), token.fingerprints().get(version), token.checkAccessHashes().get(version));
- tokens.put(tokenVersion.accessHash(), tokenVersion);
+ token.id(), token.fingerprints().get(version), token.checkAccessHashes().get(version),
+ token.expirations().get(version));
+ var expiration = tokenVersion.expiration().orElse(null);
+ if (expiration != null && now.isAfter(expiration))
+ log.fine(() -> "Ignoring expired version %s of token '%s' (expiration=%s)".formatted(
+ tokenVersion.fingerprint(), tokenVersion.id(), expiration));
+ else
+ tokens.put(tokenVersion.accessHash(), tokenVersion);
}
}
// Add reverse proxy certificate as required certificate for client definition
@@ -128,6 +139,7 @@ public class CloudDataPlaneFilter extends JsonSecurityRequestFilterBase {
@Override
protected Optional<ErrorResponse> filter(DiscFilterRequest req) {
+ var now = clock.instant();
var certs = req.getClientCertificateChain();
log.fine(() -> "Certificate chain contains %d elements".formatted(certs.size()));
if (certs.isEmpty()) {
@@ -164,6 +176,8 @@ public class CloudDataPlaneFilter extends JsonSecurityRequestFilterBase {
if (requestTokenHash == null) continue;
var matchedToken = c.tokens().get(requestTokenHash);
if (matchedToken == null) continue;
+ var expiration = matchedToken.expiration().orElse(null);
+ if (expiration != null && now.isAfter(expiration)) continue;
matchedTokens.add(matchedToken);
}
clientIds.add(c.id());
@@ -178,6 +192,7 @@ public class CloudDataPlaneFilter extends JsonSecurityRequestFilterBase {
if (matchedToken != null) {
addAccessLogEntry(req, "token.id", matchedToken.id());
addAccessLogEntry(req, "token.hash", matchedToken.fingerprint().toDelimitedHexString());
+ addAccessLogEntry(req, "token.exp", matchedToken.expiration().map(Instant::toString).orElse("<none>"));
}
log.fine(() -> "Client with ids=%s, permissions=%s"
.formatted(clientIds, permissions.stream().map(Permission::asString).toList()));
@@ -225,9 +240,10 @@ public class CloudDataPlaneFilter extends JsonSecurityRequestFilterBase {
}
}
- private record TokenVersion(String id, TokenFingerprint fingerprint, TokenCheckHash accessHash) {
- static TokenVersion of(String id, String fingerprint, String accessHash) {
- return new TokenVersion(id, TokenFingerprint.ofHex(fingerprint), TokenCheckHash.ofHex(accessHash));
+ private record TokenVersion(String id, TokenFingerprint fingerprint, TokenCheckHash accessHash, Optional<Instant> expiration) {
+ static TokenVersion of(String id, String fingerprint, String accessHash, String expiration) {
+ return new TokenVersion(id, TokenFingerprint.ofHex(fingerprint), TokenCheckHash.ofHex(accessHash),
+ expiration.equals("<none>") ? Optional.empty() : Optional.of(Instant.parse(expiration)));
}
}
diff --git a/jdisc-security-filters/src/test/java/com/yahoo/jdisc/http/filter/security/cloud/CloudDataPlaneFilterTest.java b/jdisc-security-filters/src/test/java/com/yahoo/jdisc/http/filter/security/cloud/CloudDataPlaneFilterTest.java
index d05baccc069..d9daf8b6f46 100644
--- a/jdisc-security-filters/src/test/java/com/yahoo/jdisc/http/filter/security/cloud/CloudDataPlaneFilterTest.java
+++ b/jdisc-security-filters/src/test/java/com/yahoo/jdisc/http/filter/security/cloud/CloudDataPlaneFilterTest.java
@@ -17,11 +17,15 @@ import com.yahoo.security.token.Token;
import com.yahoo.security.token.TokenCheckHash;
import com.yahoo.security.token.TokenDomain;
import com.yahoo.security.token.TokenGenerator;
+import com.yahoo.test.ManualClock;
+import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import javax.security.auth.x500.X500Principal;
import java.math.BigInteger;
import java.security.cert.X509Certificate;
+import java.time.Duration;
+import java.time.Instant;
import java.util.List;
import java.util.Set;
@@ -52,11 +56,16 @@ class CloudDataPlaneFilterTest {
private static final String TOKEN_SEARCH_CLIENT = "token-search-client";
private static final String TOKEN_CONTEXT = "my-token-context";
private static final String TOKEN_ID = "my-token-id";
+ private static final Instant TOKEN_EXPIRATION = EPOCH.plus(Duration.ofDays(1));
private static final Token VALID_TOKEN =
TokenGenerator.generateToken(TokenDomain.of(TOKEN_CONTEXT), "vespa_token_", CHECK_HASH_BYTES);
private static final Token UNKNOWN_TOKEN =
TokenGenerator.generateToken(TokenDomain.of(TOKEN_CONTEXT), "vespa_token_", CHECK_HASH_BYTES);
+ private ManualClock clock;
+
+ @BeforeEach void resetClock() { clock = new ManualClock(EPOCH); }
+
@Test
void accepts_any_trusted_client_certificate_in_legacy_mode() {
var req = FilterTestUtils.newRequestBuilder().withClientCertificate(LEGACY_CLIENT_CERT).build();
@@ -150,6 +159,7 @@ class CloudDataPlaneFilterTest {
assertEquals(new ClientPrincipal(Set.of(TOKEN_SEARCH_CLIENT), Set.of(READ)), req.getUserPrincipal());
assertEquals(TOKEN_ID, entry.getKeyValues().get("token.id").get(0));
assertEquals(VALID_TOKEN.fingerprint().toDelimitedHexString(), entry.getKeyValues().get("token.hash").get(0));
+ assertEquals(TOKEN_EXPIRATION.toString(), entry.getKeyValues().get("token.exp").get(0));
}
@Test
@@ -228,13 +238,40 @@ class CloudDataPlaneFilterTest {
assertEquals(new ClientPrincipal(Set.of(FEED_CLIENT_ID), Set.of(WRITE)), req.getUserPrincipal());
}
- private static CloudDataPlaneFilter newFilterWithLegacyMode() {
+ @Test
+ void fails_for_expired_token() {
+ var entry = new AccessLogEntry();
+ var req = FilterTestUtils.newRequestBuilder()
+ .withMethod(Method.GET)
+ .withAccessLogEntry(entry)
+ .withClientCertificate(REVERSE_PROXY_CERT)
+ .withHeader("Authorization", "Bearer " + VALID_TOKEN.secretTokenString())
+ .build();
+ var filter = newFilterWithClientsConfig();
+
+ var responseHandler = new MockResponseHandler();
+ filter.filter(req, responseHandler);
+ assertNull(responseHandler.getResponse());
+
+ clock.advance(Duration.ofDays(1));
+ responseHandler = new MockResponseHandler();
+ filter.filter(req, responseHandler);
+ assertNull(responseHandler.getResponse());
+
+ clock.advance(Duration.ofMillis(1));
+ responseHandler = new MockResponseHandler();
+ filter.filter(req, responseHandler);
+ assertNotNull(responseHandler.getResponse());
+ assertEquals(FORBIDDEN, responseHandler.getResponse().getStatus());
+ }
+
+ private CloudDataPlaneFilter newFilterWithLegacyMode() {
return new CloudDataPlaneFilter(
new CloudDataPlaneFilterConfig.Builder()
- .legacyMode(true).build(), (X509Certificate) null);
+ .legacyMode(true).build(), (X509Certificate) null, clock);
}
- private static CloudDataPlaneFilter newFilterWithClientsConfig() {
+ private CloudDataPlaneFilter newFilterWithClientsConfig() {
return new CloudDataPlaneFilter(
new CloudDataPlaneFilterConfig.Builder()
.tokenContext(TOKEN_CONTEXT)
@@ -251,11 +288,13 @@ class CloudDataPlaneFilterTest {
.tokens(new CloudDataPlaneFilterConfig.Clients.Tokens.Builder()
.id(TOKEN_ID)
.checkAccessHashes(TokenCheckHash.of(VALID_TOKEN, 32).toHexString())
- .fingerprints(VALID_TOKEN.fingerprint().toDelimitedHexString()))
+ .fingerprints(VALID_TOKEN.fingerprint().toDelimitedHexString())
+ .expirations(TOKEN_EXPIRATION.toString()))
.permissions(READ.asString())
.id(TOKEN_SEARCH_CLIENT)))
.build(),
- REVERSE_PROXY_CERT);
+ REVERSE_PROXY_CERT,
+ clock);
}
private static X509Certificate certificate(String name) {