diff options
Diffstat (limited to 'jdisc-security-filters/src/test/java/com/yahoo/jdisc/http/filter')
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 |