summaryrefslogtreecommitdiffstats
path: root/jdisc-security-filters/src/test/java/com/yahoo/jdisc/http/filter/security/cloud/CloudTokenDataPlaneFilterTest.java
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/http/filter/security/cloud/CloudTokenDataPlaneFilterTest.java
parent6dad9426c16cc5a2e95247d8e170fab07baa862e (diff)
Split token authz into dedicated filter `CloudTokenDataPlaneFilter`
Diffstat (limited to 'jdisc-security-filters/src/test/java/com/yahoo/jdisc/http/filter/security/cloud/CloudTokenDataPlaneFilterTest.java')
-rw-r--r--jdisc-security-filters/src/test/java/com/yahoo/jdisc/http/filter/security/cloud/CloudTokenDataPlaneFilterTest.java195
1 files changed, 195 insertions, 0 deletions
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