diff options
author | Martin Polden <mpolden@mpolden.no> | 2018-02-21 11:31:18 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-02-21 11:31:18 +0100 |
commit | 15e8b4456a6b57ea8b144d175641b6032a0be267 (patch) | |
tree | 1ba8f9805257718f8dcbc81efbc257918b2cc6dd /node-repository/src/test | |
parent | 59e254a7d8ea61ddfb2adde30b0aecef70913704 (diff) | |
parent | 3564bc128ce89573799419a31b8736332eeb432e (diff) |
Merge pull request #5080 from vespa-engine/mpolden/node-repo-auth-filter
Authorization filter for node repository
Diffstat (limited to 'node-repository/src/test')
2 files changed, 234 insertions, 0 deletions
diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/AuthorizerTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/AuthorizerTest.java new file mode 100644 index 00000000000..3a616b8adc3 --- /dev/null +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/AuthorizerTest.java @@ -0,0 +1,78 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.provision.restapi.v2; + +import com.yahoo.config.provision.SystemName; +import com.yahoo.vespa.curator.mock.MockCurator; +import com.yahoo.vespa.hosted.provision.testutils.MockNodeFlavors; +import com.yahoo.vespa.hosted.provision.testutils.MockNodeRepository; +import org.junit.Before; +import org.junit.Test; + +import java.net.URI; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * @author mpolden + */ +public class AuthorizerTest { + + private Authorizer authorizer; + private MockNodeRepository nodeRepository; + + @Before + public void before() { + nodeRepository = new MockNodeRepository(new MockCurator(), new MockNodeFlavors()); + authorizer = new Authorizer(SystemName.main, nodeRepository); + } + + @Test + public void authorization() { + // Empty principal + assertFalse(authorized("", "")); + assertFalse(authorized("", "/")); + + // Node can only access its own resources + assertFalse(authorized("node1", "")); + assertFalse(authorized("node1", "/")); + assertFalse(authorized("node1", "/nodes/v2/node")); + assertFalse(authorized("node1", "/nodes/v2/node/")); + assertFalse(authorized("node1", "/nodes/v2/node/node2")); + assertFalse(authorized("node1", "/nodes/v2/state/dirty/")); + assertFalse(authorized("node1", "/nodes/v2/state/dirty/node2")); + assertFalse(authorized("node1", "/nodes/v2/acl/node2")); + // Node resource always takes precedence over filter + assertFalse(authorized("node1", "/nodes/v2/acl/node2?hostname=node1")); + assertFalse(authorized("node1", "/nodes/v2/command/reboot/")); + assertFalse(authorized("node1", "/nodes/v2/command/reboot/?hostname=")); + assertFalse(authorized("node1", "/nodes/v2/command/reboot/?hostname=node2")); + assertTrue(authorized("node1", "/nodes/v2/node/node1")); + assertTrue(authorized("node1", "/nodes/v2/state/dirty/node1")); + assertTrue(authorized("node1", "/nodes/v2/acl/node1")); + assertTrue(authorized("node1", "/nodes/v2/command/reboot?hostname=node1")); + + // Host node can access itself and its children + assertFalse(authorized("dockerhost1.yahoo.com", "/nodes/v2/node/host5.yahoo.com")); + assertFalse(authorized("dockerhost1.yahoo.com", "/nodes/v2/command/reboot?hostname=host5.yahoo.com")); + assertTrue(authorized("dockerhost1.yahoo.com", "/nodes/v2/node/dockerhost1.yahoo.com")); + assertTrue(authorized("dockerhost1.yahoo.com", "/nodes/v2/node/host4.yahoo.com")); + assertTrue(authorized("dockerhost1.yahoo.com", "/nodes/v2/command/reboot?hostname=host4.yahoo.com")); + + // Trusted services can access everything in their own system + assertFalse(authorized("vespa.vespa.cd.hosting", "/")); // Wrong system + assertTrue(new Authorizer(SystemName.cd, nodeRepository).test(() -> "vespa.vespa.cd.hosting", uri("/"))); + assertTrue(authorized("vespa.vespa.hosting", "/")); + assertTrue(authorized("vespa.vespa.hosting", "/nodes/v2/node/")); + assertTrue(authorized("vespa.vespa.hosting", "/nodes/v2/node/node1")); + } + + private boolean authorized(String principal, String path) { + return authorizer.test(() -> principal, uri(path)); + } + + private static URI uri(String path) { + return URI.create("http://localhost").resolve(path); + } + +} diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/AuthorizationFilterTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/AuthorizationFilterTest.java new file mode 100644 index 00000000000..4c80f4bc257 --- /dev/null +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/AuthorizationFilterTest.java @@ -0,0 +1,156 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.provision.restapi.v2.filter; + +import com.yahoo.application.container.handler.Request; +import com.yahoo.config.provision.SystemName; +import com.yahoo.container.jdisc.RequestHandlerTestDriver.MockResponseHandler; +import com.yahoo.jdisc.http.filter.DiscFilterRequest; +import com.yahoo.jdisc.http.servlet.ServletRequest; +import com.yahoo.vespa.curator.mock.MockCurator; +import com.yahoo.vespa.hosted.provision.restapi.v2.Authorizer; +import com.yahoo.vespa.hosted.provision.testutils.MockNodeFlavors; +import com.yahoo.vespa.hosted.provision.testutils.MockNodeRepository; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.BasicConstraints; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.math.BigInteger; +import java.net.URI; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.time.Duration; +import java.time.Instant; +import java.util.Date; +import java.util.Optional; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * @author mpolden + */ +public class AuthorizationFilterTest { + + private AuthorizationFilter filter; + + @Before + public void before() { + filter = new AuthorizationFilter(new Authorizer(SystemName.main, new MockNodeRepository(new MockCurator(), + new MockNodeFlavors())), + AuthorizationFilter::write); + } + + @Test + public void filter() { + // These are just rudimentary tests of the filter. See AuthorizerTest for more exhaustive tests + Optional<Response> response = invokeFilter(request(Request.Method.GET, "/")); + assertResponse(401, "{\"error-code\":\"UNAUTHORIZED\",\"message\":\"GET / denied for " + + "unit-test: Missing credentials\"}", response); + + response = invokeFilter(request(Request.Method.GET, "/", "foo")); + assertResponse(403, "{\"error-code\":\"FORBIDDEN\",\"message\":\"GET / " + + "denied for unit-test: Invalid credentials\"}", response); + + response = invokeFilter(request(Request.Method.GET, "/nodes/v2/node/foo", "bar")); + assertResponse(403, "{\"error-code\":\"FORBIDDEN\",\"message\":\"GET /nodes/v2/node/foo " + + "denied for unit-test: Invalid credentials\"}", response); + + response = invokeFilter(request(Request.Method.GET, "/nodes/v2/node/foo", "foo")); + assertSuccess(response); + } + + private Optional<Response> invokeFilter(DiscFilterRequest request) { + MockResponseHandler handler = new MockResponseHandler(); + filter.filter(request, handler); + return Optional.ofNullable(handler.getResponse()) + .map(response -> new Response(response.getStatus(), handler.readAll())); + } + + private static DiscFilterRequest request(Request.Method method, String path) { + return request(method, path, null); + } + + private static DiscFilterRequest request(Request.Method method, String path, String commonName) { + DiscFilterRequest request = mock(DiscFilterRequest.class); + when(request.getMethod()).thenReturn(method.name()); + when(request.getUri()).thenReturn(URI.create("http://localhost").resolve(path)); + when(request.getRemoteAddr()).thenReturn("unit-test"); + if (commonName != null) { + X509Certificate cert = certificateFor(commonName, keyPair()); + when(request.getAttribute(ServletRequest.JDISC_REQUEST_X509CERT)) + .thenReturn(new X509Certificate[]{cert}); + } + return request; + } + + private static void assertSuccess(Optional<Response> response) { + assertFalse("No error in response", response.isPresent()); + } + + private static void assertResponse(int status, String body, Optional<Response> response) { + assertTrue("Expected response from filter", response.isPresent()); + assertEquals("Response body", body, response.get().body); + assertEquals("Status code", status, response.get().status); + } + + /** Create a RSA public/private key pair */ + private static KeyPair keyPair() { + try { + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); + keyGen.initialize(2048); + return keyGen.generateKeyPair(); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + /** Create a self signed certificate for commonName using given public/private key pair */ + private static X509Certificate certificateFor(String commonName, KeyPair keyPair) { + try { + ContentSigner contentSigner = new JcaContentSignerBuilder("SHA256WithRSA") + .build(keyPair.getPrivate()); + X500Name x500Name = new X500Name("CN=" + commonName); + Instant now = Instant.now(); + Date notBefore = Date.from(now); + Date notAfter = Date.from(now.plus(Duration.ofDays(30))); + X509v3CertificateBuilder certificateBuilder = + new JcaX509v3CertificateBuilder( + x500Name, + BigInteger.valueOf(now.toEpochMilli()), notBefore, notAfter, x500Name, keyPair.getPublic() + ).addExtension(Extension.basicConstraints, true, new BasicConstraints(true)); + return new JcaX509CertificateConverter() + .setProvider(new BouncyCastleProvider()) + .getCertificate(certificateBuilder.build(contentSigner)); + } catch (OperatorCreationException |IOException |CertificateException e) { + throw new RuntimeException(e); + } + } + + private static class Response { + + private final int status; + private final String body; + + private Response(int status, String body) { + this.status = status; + this.body = body; + } + } + +} |