diff options
Diffstat (limited to 'athenz-identity-provider-service/src/test/java')
6 files changed, 206 insertions, 21 deletions
diff --git a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/instanceconfirmation/InstanceValidatorTest.java b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/instanceconfirmation/InstanceValidatorTest.java index 5ce0f3cdd7e..a35dfd878c5 100644 --- a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/instanceconfirmation/InstanceValidatorTest.java +++ b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/instanceconfirmation/InstanceValidatorTest.java @@ -58,10 +58,12 @@ public class InstanceValidatorTest { private final String domain = "domain"; private final String service = "service"; + private final AthenzService vespaTenantDomain = new AthenzService("vespa.vespa.tenant"); + @Test public void application_does_not_exist() { SuperModelProvider superModelProvider = mockSuperModelProvider(); - InstanceValidator instanceValidator = new InstanceValidator(null, superModelProvider, null, null); + InstanceValidator instanceValidator = new InstanceValidator(null, superModelProvider, null, null, vespaTenantDomain); assertFalse(instanceValidator.isValidInstance(createRegisterInstanceConfirmation(applicationId, domain, service))); } @@ -69,7 +71,7 @@ public class InstanceValidatorTest { public void application_does_not_have_domain_set() { SuperModelProvider superModelProvider = mockSuperModelProvider( mockApplicationInfo(applicationId, 5, Collections.emptyList())); - InstanceValidator instanceValidator = new InstanceValidator(null, superModelProvider, null); + InstanceValidator instanceValidator = new InstanceValidator(null, superModelProvider, null, new IdentityDocumentSigner(), vespaTenantDomain); assertFalse(instanceValidator.isValidInstance(createRegisterInstanceConfirmation(applicationId, domain, service))); } @@ -81,7 +83,7 @@ public class InstanceValidatorTest { SuperModelProvider superModelProvider = mockSuperModelProvider( mockApplicationInfo(applicationId, 5, Collections.singletonList(serviceInfo))); - InstanceValidator instanceValidator = new InstanceValidator(null, superModelProvider, null, null); + InstanceValidator instanceValidator = new InstanceValidator(null, superModelProvider, null, null, vespaTenantDomain); assertFalse(instanceValidator.isValidInstance(createRegisterInstanceConfirmation(applicationId, domain, service))); } @@ -99,7 +101,7 @@ public class InstanceValidatorTest { mockApplicationInfo(applicationId, 5, Collections.singletonList(serviceInfo))); IdentityDocumentSigner signer = mock(IdentityDocumentSigner.class); when(signer.hasValidSignature(any(), any())).thenReturn(true); - InstanceValidator instanceValidator = new InstanceValidator(mock(KeyProvider.class), superModelProvider, null, signer); + InstanceValidator instanceValidator = new InstanceValidator(mock(KeyProvider.class), superModelProvider, null, signer, vespaTenantDomain); assertTrue(instanceValidator.isValidInstance(createRegisterInstanceConfirmation(applicationId, domain, service))); } @@ -107,7 +109,7 @@ public class InstanceValidatorTest { @Test public void rejects_invalid_provider_unique_id_in_csr() { SuperModelProvider superModelProvider = mockSuperModelProvider(); - InstanceValidator instanceValidator = new InstanceValidator(null, superModelProvider, null, null); + InstanceValidator instanceValidator = new InstanceValidator(null, superModelProvider, null, null, vespaTenantDomain); InstanceConfirmation instanceConfirmation = createRegisterInstanceConfirmation(applicationId, domain, service); VespaUniqueInstanceId tamperedId = new VespaUniqueInstanceId(0, "default", "instance", "app", "tenant", "us-north-1", "dev", IdentityType.NODE); instanceConfirmation.set("sanDNS", tamperedId.asDottedString() + ".instanceid.athenz.dev-us-north-1.vespa.yahoo.cloud"); @@ -117,7 +119,7 @@ public class InstanceValidatorTest { @Test public void accepts_valid_refresh_requests() { NodeRepository nodeRepository = mock(NodeRepository.class); - InstanceValidator instanceValidator = new InstanceValidator(null, null, nodeRepository); + InstanceValidator instanceValidator = new InstanceValidator(null, null, nodeRepository, new IdentityDocumentSigner(), vespaTenantDomain); List<Node> nodeList = createNodes(10); Node node = nodeList.get(0); @@ -132,7 +134,7 @@ public class InstanceValidatorTest { @Test public void rejects_refresh_on_ip_mismatch() { NodeRepository nodeRepository = mock(NodeRepository.class); - InstanceValidator instanceValidator = new InstanceValidator(null, null, nodeRepository); + InstanceValidator instanceValidator = new InstanceValidator(null, null, nodeRepository, new IdentityDocumentSigner(), vespaTenantDomain); List<Node> nodeList = createNodes(10); Node node = nodeList.get(0); @@ -149,7 +151,7 @@ public class InstanceValidatorTest { @Test public void rejects_refresh_when_node_is_not_allocated() { NodeRepository nodeRepository = mock(NodeRepository.class); - InstanceValidator instanceValidator = new InstanceValidator(null, null, nodeRepository); + InstanceValidator instanceValidator = new InstanceValidator(null, null, nodeRepository, new IdentityDocumentSigner(), vespaTenantDomain); List<Node> nodeList = createNodes(10); when(nodeRepository.getNodes()).thenReturn(nodeList); diff --git a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/CertificateAuthorityApiTest.java b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/CertificateAuthorityApiTest.java index e377009b18c..88ee154dee8 100644 --- a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/CertificateAuthorityApiTest.java +++ b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/CertificateAuthorityApiTest.java @@ -2,25 +2,34 @@ package com.yahoo.vespa.hosted.ca.restapi; import com.yahoo.application.container.handler.Request; +import com.yahoo.jdisc.http.servlet.ServletRequest; import com.yahoo.security.KeyAlgorithm; import com.yahoo.security.KeyUtils; import com.yahoo.security.Pkcs10Csr; import com.yahoo.security.Pkcs10CsrUtils; import com.yahoo.security.X509CertificateUtils; +import com.yahoo.text.StringUtilities; +import com.yahoo.vespa.athenz.api.AthenzPrincipal; import com.yahoo.vespa.athenz.api.AthenzService; import com.yahoo.vespa.athenz.client.zts.DefaultZtsClient; import com.yahoo.vespa.config.SlimeUtils; import com.yahoo.vespa.hosted.ca.CertificateTester; +import org.apache.http.client.ResponseHandler; +import org.apache.http.client.methods.HttpUriRequest; import org.junit.Before; import org.junit.Test; import javax.net.ssl.SSLContext; import java.net.URI; import java.nio.charset.StandardCharsets; +import java.security.Principal; +import java.security.cert.X509Certificate; import java.util.List; +import java.util.Optional; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; /** * @author mpolden @@ -29,6 +38,8 @@ public class CertificateAuthorityApiTest extends ContainerTester { private static final String INSTANCE_ID = "1.cluster1.default.app1.tenant1.us-north-1.prod.node"; private static final String INSTANCE_ID_WITH_SUFFIX = INSTANCE_ID + ".instanceid.athenz.dev-us-north-1.vespa.aws.oath.cloud"; + private static final String INVALID_INSTANCE_ID = "1.cluster1.default.otherapp.othertenant.us-north-1.prod.node"; + private static final String INVALID_INSTANCE_ID_WITH_SUFFIX = INVALID_INSTANCE_ID + ".instanceid.athenz.dev-us-north-1.vespa.aws.oath.cloud"; @Before public void before() { @@ -44,24 +55,47 @@ public class CertificateAuthorityApiTest extends ContainerTester { Request.Method.POST)); // POST instance registration with ZTS client - var ztsClient = new DefaultZtsClient(URI.create("http://localhost:12345/ca/v1/"), SSLContext.getDefault()); + var ztsClient = new TestZtsClient(new AthenzPrincipal(new AthenzService("vespa.external.tenant-host")), null, URI.create("http://localhost:12345/ca/v1/"), SSLContext.getDefault()); var instanceIdentity = ztsClient.registerInstance(new AthenzService("vespa.external", "provider_prod_us-north-1"), new AthenzService("vespa.external", "tenant"), - "identity document generated by config server", + getAttestationData(), csr); assertEquals("CN=Vespa CA", instanceIdentity.certificate().getIssuerX500Principal().getName()); } + private X509Certificate registerInstance() throws Exception { + // POST instance registration + var csr = CertificateTester.createCsr(List.of("node1.example.com", INSTANCE_ID_WITH_SUFFIX)); + assertIdentityResponse(new Request("http://localhost:12345/ca/v1/instance/", + instanceRegistrationJson(csr), + Request.Method.POST)); + + // POST instance registration with ZTS client + var ztsClient = new TestZtsClient(new AthenzPrincipal(new AthenzService("vespa.external.tenant-host")), null, URI.create("http://localhost:12345/ca/v1/"), SSLContext.getDefault()); + var instanceIdentity = ztsClient.registerInstance(new AthenzService("vespa.external", "provider_prod_us-north-1"), + new AthenzService("vespa.external", "tenant"), + getAttestationData(), + csr); + return instanceIdentity.certificate(); + } + @Test public void refresh_instance() throws Exception { + // Register instance to get cert + var certificate = registerInstance(); + // POST instance refresh var csr = CertificateTester.createCsr(List.of("node1.example.com", INSTANCE_ID_WITH_SUFFIX)); - assertIdentityResponse(new Request("http://localhost:12345/ca/v1/instance/vespa.external.provider_prod_us-north-1/vespa.external/tenant/" + INSTANCE_ID, + var principal = new AthenzPrincipal(new AthenzService("vespa.external.tenant")); + var request = new Request("http://localhost:12345/ca/v1/instance/vespa.external.provider_prod_us-north-1/vespa.external/tenant/" + INSTANCE_ID, instanceRefreshJson(csr), - Request.Method.POST)); + Request.Method.POST, + principal); + request.getAttributes().put(ServletRequest.JDISC_REQUEST_X509CERT, new X509Certificate[]{certificate}); + assertIdentityResponse(request); // POST instance refresh with ZTS client - var ztsClient = new DefaultZtsClient(URI.create("http://localhost:12345/ca/v1/"), SSLContext.getDefault()); + var ztsClient = new TestZtsClient(principal, certificate, URI.create("http://localhost:12345/ca/v1/"), SSLContext.getDefault()); var instanceIdentity = ztsClient.refreshInstance(new AthenzService("vespa.external", "provider_prod_us-north-1"), new AthenzService("vespa.external", "tenant"), INSTANCE_ID, @@ -70,7 +104,7 @@ public class CertificateAuthorityApiTest extends ContainerTester { } @Test - public void invalid_requests() { + public void invalid_requests() throws Exception { // POST instance registration with missing fields assertResponse(400, "{\"error-code\":\"BAD_REQUEST\",\"message\":\"POST http://localhost:12345/ca/v1/instance/ failed: Missing required field 'provider'\"}", new Request("http://localhost:12345/ca/v1/instance/", @@ -91,11 +125,28 @@ public class CertificateAuthorityApiTest extends ContainerTester { Request.Method.POST)); // POST instance refresh where instanceId does not match CSR dnsName + var principal = new AthenzPrincipal(new AthenzService("vespa.external.tenant")); csr = CertificateTester.createCsr(List.of("node1.example.com", INSTANCE_ID_WITH_SUFFIX)); assertResponse(400, "{\"error-code\":\"BAD_REQUEST\",\"message\":\"POST http://localhost:12345/ca/v1/instance/vespa.external.provider_prod_us-north-1/vespa.external/tenant/foobar failed: Mismatch between instance ID in URL path and instance ID in CSR [instanceId=foobar,instanceIdFromCsr=1.cluster1.default.app1.tenant1.us-north-1.prod.node]\"}", new Request("http://localhost:12345/ca/v1/instance/vespa.external.provider_prod_us-north-1/vespa.external/tenant/foobar", instanceRefreshJson(csr), - Request.Method.POST)); + Request.Method.POST, + principal)); + + // POST instance refresh using zts client where client cert does not contain instanceid + var certificate = registerInstance(); + var ztsClient = new TestZtsClient(principal, certificate, URI.create("http://localhost:12345/ca/v1/"), SSLContext.getDefault()); + try { + var invalidCsr = CertificateTester.createCsr(List.of("node1.example.com", INVALID_INSTANCE_ID_WITH_SUFFIX)); + var instanceIdentity = ztsClient.refreshInstance(new AthenzService("vespa.external", "provider_prod_us-north-1"), + new AthenzService("vespa.external", "tenant"), + INSTANCE_ID, + invalidCsr); + fail("Refresh instance should have failed"); + } catch (Exception e) { + String expectedMessage = "Received error from ZTS: code=0, message=\"POST http://localhost:12345/ca/v1/instance/vespa.external.provider_prod_us-north-1/vespa.external/tenant/1.cluster1.default.app1.tenant1.us-north-1.prod.node failed: Mismatch between instance ID in URL path and instance ID in CSR [instanceId=1.cluster1.default.app1.tenant1.us-north-1.prod.node,instanceIdFromCsr=1.cluster1.default.otherapp.othertenant.us-north-1.prod.node]\""; + assertEquals(expectedMessage, e.getMessage()); + } } private void setCaCertificateAndKey() { @@ -132,10 +183,52 @@ public class CertificateAuthorityApiTest extends ContainerTester { " \"provider\": \"vespa.external.provider_prod_us-north-1\",\n" + " \"domain\": \"vespa.external\",\n" + " \"service\": \"tenant\",\n" + - " \"attestationData\": \"identity document generated by config server\",\n" + + " \"attestationData\": \""+getAttestationData()+"\",\n" + " \"csr\": \"" + csrPem + "\"\n" + "}"; return json.getBytes(StandardCharsets.UTF_8); } + private static String getAttestationData () { + var json = "{\n" + + " \"signature\": \"SIGNATURE\",\n" + + " \"signing-key-version\": 0,\n" + + " \"provider-unique-id\": \"0.default.default.application.tenant.us-north-1.dev.tenant\",\n" + + " \"provider-service\": \"domain.service\",\n" + + " \"document-version\": 1,\n" + + " \"configserver-hostname\": \"localhost\",\n" + + " \"instance-hostname\": \"docker-container\",\n" + + " \"created-at\": 1572000079.00000,\n" + + " \"ip-addresses\": [\n" + + " \"::1\"\n" + + " ],\n" + + " \"identity-type\": \"tenant\"\n" + + "}"; + return StringUtilities.escape(json); + } + + /* + Zts client that adds principal as header (since setting up ssl in test is cumbersome) + */ + private static class TestZtsClient extends DefaultZtsClient { + + private final Principal principal; + private final X509Certificate certificate; + + public TestZtsClient(Principal principal, X509Certificate certificate, URI ztsUrl, SSLContext sslContext) { + super(ztsUrl, sslContext); + this.principal = principal; + this.certificate = certificate; + } + + @Override + protected <T> T execute(HttpUriRequest request, ResponseHandler<T> responseHandler) { + request.addHeader("PRINCIPAL", principal.getName()); + Optional.ofNullable(certificate).ifPresent(cert -> { + var pem = X509CertificateUtils.toPem(certificate); + request.addHeader("CERTIFICATE", StringUtilities.escape(pem)); + }); + return super.execute(request, responseHandler); + } + } } diff --git a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/ContainerTester.java b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/ContainerTester.java index 139314b0f86..0eda6bd946b 100644 --- a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/ContainerTester.java +++ b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/ContainerTester.java @@ -67,13 +67,20 @@ public class ContainerTester { " <ztsUrl>https://localhost:123/</ztsUrl>\n" + " </config>\n" + " <component id='com.yahoo.vespa.hosted.ca.restapi.mock.SecretStoreMock'/>\n" + + " <component id='com.yahoo.vespa.hosted.ca.restapi.mock.InstanceValidatorMock'/>\n" + " <handler id='com.yahoo.vespa.hosted.ca.restapi.CertificateAuthorityApiHandler'>\n" + " <binding>http://*/ca/v1/*</binding>\n" + " </handler>\n" + " <http>\n" + " <server id='default' port='12345'/>\n" + + " <filtering>\n" + + " <request-chain id=\"my-default-chain\">\n" + + " <filter id='com.yahoo.vespa.hosted.ca.restapi.mock.PrincipalFromHeaderFilter' />\n" + + " <binding>http://*/*</binding>\n" + + " </request-chain>\n" + + " </filtering>\n" + " </http>\n" + "</container>"; } -} +}
\ No newline at end of file diff --git a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/InstanceSerializerTest.java b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/InstanceSerializerTest.java index 83ea9249ad0..edc8bec2370 100644 --- a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/InstanceSerializerTest.java +++ b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/InstanceSerializerTest.java @@ -4,6 +4,12 @@ package com.yahoo.vespa.hosted.ca.restapi; import com.yahoo.security.Pkcs10CsrUtils; import com.yahoo.security.X509CertificateUtils; import com.yahoo.slime.Slime; +import com.yahoo.text.StringUtilities; +import com.yahoo.vespa.athenz.api.AthenzService; +import com.yahoo.vespa.athenz.identityprovider.api.EntityBindingsMapper; +import com.yahoo.vespa.athenz.identityprovider.api.IdentityType; +import com.yahoo.vespa.athenz.identityprovider.api.SignedIdentityDocument; +import com.yahoo.vespa.athenz.identityprovider.api.VespaUniqueInstanceId; import com.yahoo.vespa.config.SlimeUtils; import com.yahoo.vespa.hosted.ca.CertificateTester; import com.yahoo.vespa.hosted.ca.instance.InstanceIdentity; @@ -14,6 +20,9 @@ import org.junit.Test; import java.io.IOException; import java.io.UncheckedIOException; import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Collections; import java.util.Optional; import static org.junit.Assert.assertEquals; @@ -27,15 +36,28 @@ public class InstanceSerializerTest { public void deserialize_instance_registration() { var csr = CertificateTester.createCsr(); var csrPem = Pkcs10CsrUtils.toPem(csr); - var json = "{\n" + + SignedIdentityDocument signedIdentityDocument = new SignedIdentityDocument( + "signature", + 0, + new VespaUniqueInstanceId(0, "cluster", "instance", "application", "tenant", "region", "prod", IdentityType.NODE), + new AthenzService("domain", "service"), + 0, + "configserverhostname", + "instancehostname", + Instant.now().truncatedTo(ChronoUnit.MILLIS), + // Instant.ofEpochSecond(1572000079), + Collections.emptySet(), + IdentityType.NODE); + + var json = String.format("{\n" + " \"provider\": \"provider_prod_us-north-1\",\n" + " \"domain\": \"vespa.external\",\n" + " \"service\": \"tenant\",\n" + - " \"attestationData\": \"identity document from configserevr\",\n" + + " \"attestationData\":\"%s\",\n" + " \"csr\": \"" + csrPem + "\"\n" + - "}"; + "}", StringUtilities.escape(EntityBindingsMapper.toAttestationData(signedIdentityDocument))); var instanceRegistration = new InstanceRegistration("provider_prod_us-north-1", "vespa.external", - "tenant", "identity document from configserevr", + "tenant", signedIdentityDocument, csr); var deserialized = InstanceSerializer.registrationFromSlime(SlimeUtils.jsonToSlime(json)); assertEquals(instanceRegistration, deserialized); diff --git a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/mock/InstanceValidatorMock.java b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/mock/InstanceValidatorMock.java new file mode 100644 index 00000000000..9c1d4c49b07 --- /dev/null +++ b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/mock/InstanceValidatorMock.java @@ -0,0 +1,27 @@ +// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.ca.restapi.mock; + +import com.yahoo.vespa.hosted.athenz.instanceproviderservice.instanceconfirmation.InstanceConfirmation; +import com.yahoo.vespa.hosted.athenz.instanceproviderservice.instanceconfirmation.InstanceValidator; + +/** + * @author mortent + */ +public class InstanceValidatorMock extends InstanceValidator { + + public InstanceValidatorMock() { + super(null, null, null, null, null); + } + + @Override + public boolean isValidInstance(InstanceConfirmation instanceConfirmation) { + return instanceConfirmation.attributes.get(SAN_DNS_ATTRNAME) != null && + instanceConfirmation.attributes.get(SAN_IPS_ATTRNAME) != null; + } + + @Override + public boolean isValidRefresh(InstanceConfirmation confirmation) { + return confirmation.attributes.get(SAN_DNS_ATTRNAME) != null && + confirmation.attributes.get(SAN_IPS_ATTRNAME) != null; + } +} diff --git a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/mock/PrincipalFromHeaderFilter.java b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/mock/PrincipalFromHeaderFilter.java new file mode 100644 index 00000000000..d9ee4c8bb9b --- /dev/null +++ b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/mock/PrincipalFromHeaderFilter.java @@ -0,0 +1,34 @@ +// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.ca.restapi.mock; + +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.http.filter.DiscFilterRequest; +import com.yahoo.jdisc.http.filter.SecurityRequestFilter; +import com.yahoo.jdisc.http.servlet.ServletRequest; +import com.yahoo.security.X509CertificateUtils; +import com.yahoo.text.StringUtilities; +import com.yahoo.vespa.athenz.api.AthenzPrincipal; +import com.yahoo.vespa.athenz.api.AthenzService; + +import java.security.cert.X509Certificate; +import java.util.Optional; + +/** + * Read principal from http header + * + * @author mortent + */ +public class PrincipalFromHeaderFilter implements SecurityRequestFilter { + + @Override + public void filter(DiscFilterRequest request, ResponseHandler handler) { + String principal = request.getHeader("PRINCIPAL"); + request.setUserPrincipal(new AthenzPrincipal(new AthenzService(principal))); + + Optional<String> certificate = Optional.ofNullable(request.getHeader("CERTIFICATE")); + certificate.ifPresent(cert -> { + var x509cert = X509CertificateUtils.fromPem(StringUtilities.unescape(cert)); + request.setAttribute(ServletRequest.JDISC_REQUEST_X509CERT, new X509Certificate[]{x509cert}); + }); + } +} |