summaryrefslogtreecommitdiffstats
path: root/node-repository/src/test
diff options
context:
space:
mode:
authorMartin Polden <mpolden@mpolden.no>2018-02-21 11:31:18 +0100
committerGitHub <noreply@github.com>2018-02-21 11:31:18 +0100
commit15e8b4456a6b57ea8b144d175641b6032a0be267 (patch)
tree1ba8f9805257718f8dcbc81efbc257918b2cc6dd /node-repository/src/test
parent59e254a7d8ea61ddfb2adde30b0aecef70913704 (diff)
parent3564bc128ce89573799419a31b8736332eeb432e (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')
-rw-r--r--node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/AuthorizerTest.java78
-rw-r--r--node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/AuthorizationFilterTest.java156
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;
+ }
+ }
+
+}