From b17afb040ad32887d6ec49c9b844a1610c36ed5e Mon Sep 17 00:00:00 2001 From: Martin Polden Date: Mon, 12 Mar 2018 15:58:29 +0100 Subject: Extract tester class --- .../hosted/provision/restapi/v2/Authorizer.java | 7 +- .../restapi/v2/filter/AuthorizationFilter.java | 2 + .../restapi/v2/filter/AuthorizationFilterTest.java | 141 ++--------------- .../provision/restapi/v2/filter/FilterTester.java | 168 +++++++++++++++++++++ 4 files changed, 188 insertions(+), 130 deletions(-) create mode 100644 node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/FilterTester.java (limited to 'node-repository') diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/Authorizer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/Authorizer.java index ec49560ebd9..29e98a4b0cb 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/Authorizer.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/Authorizer.java @@ -19,11 +19,8 @@ import java.util.function.BiPredicate; import java.util.stream.Collectors; /** - * Authorizer for the node-repository and orchestrator REST APIs. This contains the authorization rules for all API - * paths. - * - * Ideally, the authorization rules for orchestrator APIs should live in the orchestrator module. However, the node - * repository is required to make decisions in some cases, which is not accessible in the orchestrator module. + * Authorizer for config server REST APIs. This contains the rules for all API paths where the authorization process + * requires information from the node-repository to make a decision * * @author mpolden */ diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/AuthorizationFilter.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/AuthorizationFilter.java index a54b6cd135c..bd0512ee306 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/AuthorizationFilter.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/AuthorizationFilter.java @@ -31,6 +31,8 @@ import java.util.function.BiPredicate; import java.util.logging.Logger; /** + * Authorization filter for all paths in config server. + * * @author mpolden */ public class AuthorizationFilter implements SecurityRequestFilter { 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 index 4c80f4bc257..b14cc570a75 100644 --- 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 @@ -1,156 +1,47 @@ // 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.application.container.handler.Request.Method; 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.restapi.v2.filter.FilterTester.Request; 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; + private FilterTester tester; @Before public void before() { - filter = new AuthorizationFilter(new Authorizer(SystemName.main, new MockNodeRepository(new MockCurator(), + tester = new FilterTester(new AuthorizationFilter(new Authorizer(SystemName.main, + new MockNodeRepository(new MockCurator(), new MockNodeFlavors())), - AuthorizationFilter::write); + AuthorizationFilter::write)); } @Test public void filter() { // These are just rudimentary tests of the filter. See AuthorizerTest for more exhaustive tests - Optional 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 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) { - assertFalse("No error in response", response.isPresent()); - } - - private static void assertResponse(int status, String body, Optional 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); - } - } + tester.assertRequest(new Request(Method.GET, "/"), 401, + "{\"error-code\":\"UNAUTHORIZED\",\"message\":\"GET / denied for " + + "unit-test: Missing credentials\"}"); - private static class Response { + tester.assertRequest(new Request(Method.GET, "/").commonName("foo"), 403, + "{\"error-code\":\"FORBIDDEN\",\"message\":\"GET / " + + "denied for unit-test: Invalid credentials\"}"); - private final int status; - private final String body; + tester.assertRequest(new Request(Method.GET, "/nodes/v2/node/foo").commonName("bar"), + 403, "{\"error-code\":\"FORBIDDEN\",\"message\":\"GET /nodes/v2/node/foo " + + "denied for unit-test: Invalid credentials\"}"); - private Response(int status, String body) { - this.status = status; - this.body = body; - } + tester.assertSuccess(new Request(Method.GET, "/nodes/v2/node/foo").commonName("foo")); } } diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/FilterTester.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/FilterTester.java new file mode 100644 index 00000000000..ceb34cdfea8 --- /dev/null +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/FilterTester.java @@ -0,0 +1,168 @@ +// Copyright 2018 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.Method; +import com.yahoo.container.jdisc.RequestHandlerTestDriver; +import com.yahoo.jdisc.http.filter.DiscFilterRequest; +import com.yahoo.jdisc.http.filter.SecurityRequestFilter; +import com.yahoo.jdisc.http.servlet.ServletRequest; +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 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 FilterTester { + + private final SecurityRequestFilter filter; + + public FilterTester(SecurityRequestFilter filter) { + this.filter = filter; + } + + public void assertSuccess(Request request) { + assertFalse("No response written by filter", getResponse(request).isPresent()); + } + + public void assertRequest(Request request, int status, String body) { + Optional response = getResponse(request); + assertTrue("Expected response from filter", response.isPresent()); + assertEquals("Response body", body, response.get().body); + assertEquals("Status code", status, response.get().status); + } + + private Optional getResponse(Request request) { + RequestHandlerTestDriver.MockResponseHandler handler = new RequestHandlerTestDriver.MockResponseHandler(); + filter.filter(toDiscFilterRequest(request), handler); + return Optional.ofNullable(handler.getResponse()) + .map(response -> new Response(response.getStatus(), handler.readAll())); + } + + private static DiscFilterRequest toDiscFilterRequest(Request request) { + DiscFilterRequest r = mock(DiscFilterRequest.class); + when(r.getMethod()).thenReturn(request.method().name()); + when(r.getUri()).thenReturn(URI.create("http://localhost").resolve(request.path())); + when(r.getRemoteAddr()).thenReturn(request.remoteAddr()); + if (request.commonName().isPresent()) { + X509Certificate cert = certificateFor(request.commonName().get(), keyPair()); + when(r.getAttribute(ServletRequest.JDISC_REQUEST_X509CERT)) + .thenReturn(new X509Certificate[]{cert}); + } + return r; + } + + /** 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; + } + + } + + public static class Request { + + private final Method method; + private final String path; + private String remoteAddr; + private String commonName; + + public Request(Method method, String path) { + this.method = method; + this.path = path; + this.commonName = null; + this.remoteAddr = "unit-test"; + } + + public Method method() { + return method; + } + + public String path() { + return path; + } + + public String remoteAddr() { + return remoteAddr; + } + + public Optional commonName() { + return Optional.ofNullable(commonName); + } + + public Request commonName(String commonName) { + this.commonName = commonName; + return this; + } + + public Request remoteAddr(String remoteAddr) { + this.remoteAddr = remoteAddr; + return this; + } + + } + +} -- cgit v1.2.3