aboutsummaryrefslogtreecommitdiffstats
path: root/jdisc-security-filters/src/test/java/com/yahoo/jdisc
diff options
context:
space:
mode:
authorBjørn Christian Seime <bjorncs@yahooinc.com>2023-07-18 14:46:19 +0200
committerBjørn Christian Seime <bjorncs@yahooinc.com>2023-07-19 15:53:57 +0200
commitb630d4ed852ba0ad802667995f3f8238db2c9c3f (patch)
tree53a6a3a62a0c2cda16fcbd2233d993d2e0f960aa /jdisc-security-filters/src/test/java/com/yahoo/jdisc
parent6dad9426c16cc5a2e95247d8e170fab07baa862e (diff)
Split token authz into dedicated filter `CloudTokenDataPlaneFilter`
Diffstat (limited to 'jdisc-security-filters/src/test/java/com/yahoo/jdisc')
-rw-r--r--jdisc-security-filters/src/test/java/com/yahoo/jdisc/http/filter/security/cloud/CloudDataPlaneFilterTest.java168
-rw-r--r--jdisc-security-filters/src/test/java/com/yahoo/jdisc/http/filter/security/cloud/CloudTokenDataPlaneFilterTest.java195
2 files changed, 200 insertions, 163 deletions
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 d9daf8b6f46..8d2fd1f569e 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
@@ -5,35 +5,24 @@ import com.yahoo.container.jdisc.AclMapping.Action;
import com.yahoo.container.jdisc.HttpMethodAclMapping;
import com.yahoo.container.jdisc.RequestHandlerSpec;
import com.yahoo.container.jdisc.RequestHandlerTestDriver.MockResponseHandler;
-import com.yahoo.container.logging.AccessLogEntry;
import com.yahoo.jdisc.http.HttpRequest.Method;
-import com.yahoo.jdisc.http.filter.security.cloud.CloudDataPlaneFilter.ClientPrincipal;
import com.yahoo.jdisc.http.filter.security.cloud.config.CloudDataPlaneFilterConfig;
import com.yahoo.jdisc.http.filter.util.FilterTestUtils;
import com.yahoo.security.KeyUtils;
import com.yahoo.security.X509CertificateBuilder;
import com.yahoo.security.X509CertificateUtils;
-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;
import static com.yahoo.jdisc.Response.Status.FORBIDDEN;
import static com.yahoo.jdisc.Response.Status.UNAUTHORIZED;
-import static com.yahoo.jdisc.http.filter.security.cloud.CloudDataPlaneFilter.CHECK_HASH_BYTES;
-import static com.yahoo.jdisc.http.filter.security.cloud.CloudDataPlaneFilter.Permission.READ;
-import static com.yahoo.jdisc.http.filter.security.cloud.CloudDataPlaneFilter.Permission.WRITE;
+import static com.yahoo.jdisc.http.filter.security.cloud.Permission.READ;
+import static com.yahoo.jdisc.http.filter.security.cloud.Permission.WRITE;
import static com.yahoo.security.KeyAlgorithm.EC;
import static com.yahoo.security.SignatureAlgorithm.SHA256_WITH_ECDSA;
import static java.time.Instant.EPOCH;
@@ -50,21 +39,8 @@ class CloudDataPlaneFilterTest {
private static final X509Certificate FEED_CERT = certificate("my-feed-client");
private static final X509Certificate SEARCH_CERT = certificate("my-search-client");
private static final X509Certificate LEGACY_CLIENT_CERT = certificate("my-legacy-client");
- private static final X509Certificate REVERSE_PROXY_CERT = certificate("nginx");
private static final String FEED_CLIENT_ID = "feed-client";
private static final String MTLS_SEARCH_CLIENT_ID = "mtls-search-client";
- 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() {
@@ -144,137 +120,13 @@ class CloudDataPlaneFilterTest {
assertEquals(FORBIDDEN, responseHandler.getResponse().getStatus());
}
- @Test
- void accepts_reverse_proxy_with_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 responseHandler = new MockResponseHandler();
- newFilterWithClientsConfig().filter(req, responseHandler);
- assertNull(responseHandler.getResponse());
- 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
- void fails_for_reverse_proxy_with_token_wrong_permission() {
- var req = FilterTestUtils.newRequestBuilder()
- .withMethod(Method.POST)
- .withClientCertificate(REVERSE_PROXY_CERT)
- .withHeader("Authorization", "Bearer " + VALID_TOKEN.secretTokenString())
- .build();
- var responseHandler = new MockResponseHandler();
- newFilterWithClientsConfig().filter(req, responseHandler);
- assertNotNull(responseHandler.getResponse());
- assertEquals(FORBIDDEN, responseHandler.getResponse().getStatus());
- }
-
- @Test
- void fails_for_reverse_proxy_without_token() {
- var req = FilterTestUtils.newRequestBuilder()
- .withMethod(Method.GET)
- .withClientCertificate(REVERSE_PROXY_CERT)
- .build();
- var responseHandler = new MockResponseHandler();
- newFilterWithClientsConfig().filter(req, responseHandler);
- assertNotNull(responseHandler.getResponse());
- assertEquals(FORBIDDEN, responseHandler.getResponse().getStatus());
- }
-
- @Test
- void fails_for_reverse_proxy_with_unknown_token() {
- var req = FilterTestUtils.newRequestBuilder()
- .withMethod(Method.GET)
- .withClientCertificate(REVERSE_PROXY_CERT)
- .withHeader("Authorization", "Bearer " + UNKNOWN_TOKEN.secretTokenString())
- .build();
- var responseHandler = new MockResponseHandler();
- newFilterWithClientsConfig().filter(req, responseHandler);
- assertNotNull(responseHandler.getResponse());
- assertEquals(FORBIDDEN, responseHandler.getResponse().getStatus());
- }
-
- @Test
- void fails_for_missing_certificate_with_token() {
- var req = FilterTestUtils.newRequestBuilder()
- .withMethod(Method.GET)
- .withHeader("Authorization", "Bearer " + VALID_TOKEN.secretTokenString())
- .build();
- var responseHandler = new MockResponseHandler();
- newFilterWithClientsConfig().filter(req, responseHandler);
- assertNotNull(responseHandler.getResponse());
- assertEquals(UNAUTHORIZED, responseHandler.getResponse().getStatus());
- }
-
- @Test
- void fails_for_unknown_certificate_with_token() {
- var req = FilterTestUtils.newRequestBuilder()
- .withMethod(Method.GET)
- .withClientCertificate(LEGACY_CLIENT_CERT)
- .withHeader("Authorization", "Bearer " + VALID_TOKEN.secretTokenString())
- .build();
- var responseHandler = new MockResponseHandler();
- newFilterWithClientsConfig().filter(req, responseHandler);
- assertNotNull(responseHandler.getResponse());
- assertEquals(FORBIDDEN, responseHandler.getResponse().getStatus());
- }
-
- @Test
- void certificate_has_precedence_over_token() {
- var req = FilterTestUtils.newRequestBuilder()
- .withMethod(Method.POST)
- .withClientCertificate(FEED_CERT)
- .withHeader("Authorization", "Bearer " + VALID_TOKEN.secretTokenString())
- .build();
- var responseHandler = new MockResponseHandler();
- newFilterWithClientsConfig().filter(req, responseHandler);
- assertNull(responseHandler.getResponse());
- assertEquals(new ClientPrincipal(Set.of(FEED_CLIENT_ID), Set.of(WRITE)), req.getUserPrincipal());
- }
-
- @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, clock);
+ return new CloudDataPlaneFilter(new CloudDataPlaneFilterConfig.Builder().legacyMode(true).build());
}
private CloudDataPlaneFilter newFilterWithClientsConfig() {
return new CloudDataPlaneFilter(
new CloudDataPlaneFilterConfig.Builder()
- .tokenContext(TOKEN_CONTEXT)
.clients(List.of(
new CloudDataPlaneFilterConfig.Clients.Builder()
.certificates(X509CertificateUtils.toPem(FEED_CERT))
@@ -283,18 +135,8 @@ class CloudDataPlaneFilterTest {
new CloudDataPlaneFilterConfig.Clients.Builder()
.certificates(X509CertificateUtils.toPem(SEARCH_CERT))
.permissions(READ.asString())
- .id(MTLS_SEARCH_CLIENT_ID),
- new CloudDataPlaneFilterConfig.Clients.Builder()
- .tokens(new CloudDataPlaneFilterConfig.Clients.Tokens.Builder()
- .id(TOKEN_ID)
- .checkAccessHashes(TokenCheckHash.of(VALID_TOKEN, 32).toHexString())
- .fingerprints(VALID_TOKEN.fingerprint().toDelimitedHexString())
- .expirations(TOKEN_EXPIRATION.toString()))
- .permissions(READ.asString())
- .id(TOKEN_SEARCH_CLIENT)))
- .build(),
- REVERSE_PROXY_CERT,
- clock);
+ .id(MTLS_SEARCH_CLIENT_ID)))
+ .build());
}
private static X509Certificate certificate(String name) {
diff --git a/jdisc-security-filters/src/test/java/com/yahoo/jdisc/http/filter/security/cloud/CloudTokenDataPlaneFilterTest.java b/jdisc-security-filters/src/test/java/com/yahoo/jdisc/http/filter/security/cloud/CloudTokenDataPlaneFilterTest.java
new file mode 100644
index 00000000000..cac7818a1fc
--- /dev/null
+++ b/jdisc-security-filters/src/test/java/com/yahoo/jdisc/http/filter/security/cloud/CloudTokenDataPlaneFilterTest.java
@@ -0,0 +1,195 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.filter.security.cloud;
+
+import com.yahoo.container.jdisc.AclMapping.Action;
+import com.yahoo.container.jdisc.HttpMethodAclMapping;
+import com.yahoo.container.jdisc.RequestHandlerSpec;
+import com.yahoo.container.jdisc.RequestHandlerTestDriver.MockResponseHandler;
+import com.yahoo.container.logging.AccessLogEntry;
+import com.yahoo.jdisc.http.HttpRequest.Method;
+import com.yahoo.jdisc.http.filter.security.cloud.config.CloudTokenDataPlaneFilterConfig;
+import com.yahoo.jdisc.http.filter.util.FilterTestUtils;
+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 java.time.Duration;
+import java.time.Instant;
+import java.util.List;
+import java.util.Set;
+
+import static com.yahoo.jdisc.Response.Status.FORBIDDEN;
+import static com.yahoo.jdisc.Response.Status.UNAUTHORIZED;
+import static com.yahoo.jdisc.http.filter.security.cloud.CloudTokenDataPlaneFilter.CHECK_HASH_BYTES;
+import static com.yahoo.jdisc.http.filter.security.cloud.Permission.READ;
+import static com.yahoo.jdisc.http.filter.security.cloud.Permission.WRITE;
+import static java.time.Instant.EPOCH;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+/**
+ * @author bjorncs
+ */
+class CloudTokenDataPlaneFilterTest {
+
+ private static final String TOKEN_SEARCH_CLIENT = "token-search-client";
+ private static final String TOKEN_FEED_CLIENT = "token-feed-client";
+ private static final String TOKEN_CONTEXT = "my-token-context";
+ private static final String READ_TOKEN_ID = "my-read-token-id";
+ private static final String WRITE_TOKEN_ID = "my-write-token-id";
+ private static final Instant TOKEN_EXPIRATION = EPOCH.plus(Duration.ofDays(1));
+ private static final Token READ_TOKEN =
+ TokenGenerator.generateToken(TokenDomain.of(TOKEN_CONTEXT), "vespa_token_", CHECK_HASH_BYTES);
+ private static final Token WRITE_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 supports_handler_with_custom_request_spec() {
+ // Spec that maps POST as action 'read'
+ var spec = RequestHandlerSpec.builder()
+ .withAclMapping(HttpMethodAclMapping.standard()
+ .override(Method.POST, Action.READ).build())
+ .build();
+ var req = FilterTestUtils.newRequestBuilder()
+ .withMethod(Method.POST)
+ .withHeader("Authorization", "Bearer " + READ_TOKEN.secretTokenString())
+ .withAttribute(RequestHandlerSpec.ATTRIBUTE_NAME, spec)
+ .build();
+ var responseHandler = new MockResponseHandler();
+ newFilterWithClientsConfig().filter(req, responseHandler);
+ assertNull(responseHandler.getResponse());
+ assertEquals(new ClientPrincipal(Set.of(TOKEN_SEARCH_CLIENT), Set.of(READ)), req.getUserPrincipal());
+ }
+
+ @Test
+ void fails_on_handler_with_custom_request_spec_with_invalid_action() {
+ // Spec that maps POST as action 'read'
+ var spec = RequestHandlerSpec.builder()
+ .withAclMapping(HttpMethodAclMapping.standard()
+ .override(Method.GET, Action.custom("custom")).build())
+ .build();
+ var req = FilterTestUtils.newRequestBuilder()
+ .withMethod(Method.GET)
+ .withHeader("Authorization", "Bearer " + READ_TOKEN.secretTokenString())
+ .withAttribute(RequestHandlerSpec.ATTRIBUTE_NAME, spec)
+ .build();
+ var responseHandler = new MockResponseHandler();
+ newFilterWithClientsConfig().filter(req, responseHandler);
+ assertNotNull(responseHandler.getResponse());
+ assertEquals(FORBIDDEN, responseHandler.getResponse().getStatus());
+ }
+
+ @Test
+ void accepts_valid_token() {
+ var entry = new AccessLogEntry();
+ var req = FilterTestUtils.newRequestBuilder()
+ .withMethod(Method.GET)
+ .withAccessLogEntry(entry)
+ .withHeader("Authorization", "Bearer " + READ_TOKEN.secretTokenString())
+ .build();
+ var responseHandler = new MockResponseHandler();
+ newFilterWithClientsConfig().filter(req, responseHandler);
+ assertNull(responseHandler.getResponse());
+ assertEquals(new ClientPrincipal(Set.of(TOKEN_SEARCH_CLIENT), Set.of(READ)), req.getUserPrincipal());
+ assertEquals(READ_TOKEN_ID, entry.getKeyValues().get("token.id").get(0));
+ assertEquals(READ_TOKEN.fingerprint().toDelimitedHexString(), entry.getKeyValues().get("token.hash").get(0));
+ assertEquals(TOKEN_EXPIRATION.toString(), entry.getKeyValues().get("token.exp").get(0));
+ }
+
+ @Test
+ void fails_for_token_with_invalid_permission() {
+ var req = FilterTestUtils.newRequestBuilder()
+ .withMethod(Method.GET)
+ .withHeader("Authorization", "Bearer " + WRITE_TOKEN.secretTokenString())
+ .build();
+ var responseHandler = new MockResponseHandler();
+ newFilterWithClientsConfig().filter(req, responseHandler);
+ assertNotNull(responseHandler.getResponse());
+ assertEquals(FORBIDDEN, responseHandler.getResponse().getStatus());
+ }
+
+ @Test
+ void fails_for_missing_token() {
+ var req = FilterTestUtils.newRequestBuilder()
+ .withMethod(Method.GET)
+ .build();
+ var responseHandler = new MockResponseHandler();
+ newFilterWithClientsConfig().filter(req, responseHandler);
+ assertNotNull(responseHandler.getResponse());
+ assertEquals(UNAUTHORIZED, responseHandler.getResponse().getStatus());
+ }
+
+ @Test
+ void fails_for_unknown_token() {
+ var req = FilterTestUtils.newRequestBuilder()
+ .withMethod(Method.GET)
+ .withHeader("Authorization", "Bearer " + UNKNOWN_TOKEN.secretTokenString())
+ .build();
+ var responseHandler = new MockResponseHandler();
+ newFilterWithClientsConfig().filter(req, responseHandler);
+ assertNotNull(responseHandler.getResponse());
+ assertEquals(FORBIDDEN, responseHandler.getResponse().getStatus());
+ }
+
+ @Test
+ void fails_for_expired_token() {
+ var entry = new AccessLogEntry();
+ var req = FilterTestUtils.newRequestBuilder()
+ .withMethod(Method.GET)
+ .withAccessLogEntry(entry)
+ .withHeader("Authorization", "Bearer " + READ_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 CloudTokenDataPlaneFilter newFilterWithClientsConfig() {
+ return new CloudTokenDataPlaneFilter(
+ new CloudTokenDataPlaneFilterConfig.Builder()
+ .tokenContext(TOKEN_CONTEXT)
+ .clients(List.of(
+ new CloudTokenDataPlaneFilterConfig.Clients.Builder()
+ .tokens(new CloudTokenDataPlaneFilterConfig.Clients.Tokens.Builder()
+ .id(READ_TOKEN_ID)
+ .checkAccessHashes(TokenCheckHash.of(READ_TOKEN, 32).toHexString())
+ .fingerprints(READ_TOKEN.fingerprint().toDelimitedHexString())
+ .expirations(TOKEN_EXPIRATION.toString()))
+ .permissions(READ.asString())
+ .id(TOKEN_SEARCH_CLIENT),
+ new CloudTokenDataPlaneFilterConfig.Clients.Builder()
+ .tokens(new CloudTokenDataPlaneFilterConfig.Clients.Tokens.Builder()
+ .id(WRITE_TOKEN_ID)
+ .checkAccessHashes(TokenCheckHash.of(WRITE_TOKEN, 32).toHexString())
+ .fingerprints(WRITE_TOKEN.fingerprint().toDelimitedHexString())
+ .expirations(TOKEN_EXPIRATION.toString()))
+ .permissions(WRITE.asString())
+ .id(TOKEN_FEED_CLIENT)))
+ .build(),
+ clock);
+ }
+
+} \ No newline at end of file