diff --git a/server/server_test.go b/server/server_test.go index 4a0b1a4c64..4416c88b69 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -6,14 +6,10 @@ package server import ( "context" - "crypto/tls" - "crypto/x509" "encoding/json" "fmt" - "io/ioutil" "net/http" "net/http/httptest" - "net/url" "reflect" "sort" "strings" @@ -3097,128 +3093,6 @@ func mustUnmarshalTrace(t types.TraceV1) (trace types.TraceV1Raw) { return trace } -func TestAuthenticationTLS(t *testing.T) { - ctx := context.Background() - store := inmem.New() - m, err := plugins.New([]byte{}, "test", store) - if err != nil { - t.Fatal(err) - } - - if err := m.Start(ctx); err != nil { - t.Fatal(err) - } - - txn := storage.NewTransactionOrDie(ctx, store, storage.WriteParams) - - authzPolicy := `package system.authz -import input.identity -default allow = false -allow { - identity = "CN=my-client" -}` - - if err := store.UpsertPolicy(ctx, txn, "test", []byte(authzPolicy)); err != nil { - t.Fatal(err) - } - - if err := store.Commit(ctx, txn); err != nil { - t.Fatal(err) - } - - caCertPEM, err := ioutil.ReadFile("testdata/ca.pem") - if err != nil { - t.Fatal(err) - } - pool := x509.NewCertPool() - if ok := pool.AppendCertsFromPEM(caCertPEM); !ok { - t.Fatal("failed to parse CA cert") - } - cert, err := tls.LoadX509KeyPair("testdata/server-cert.pem", "testdata/server-key.pem") - if err != nil { - t.Fatal(err) - } - - server, err := New(). - WithAddresses([]string{"https://127.0.0.1:8182"}). - WithStore(store). - WithManager(m). - WithCertificate(&cert). - WithCertPool(pool). - WithAuthentication(AuthenticationTLS). - WithAuthorization(AuthorizationBasic). - Init(ctx) - if err != nil { - t.Fatal(err) - } - - // Replicating some of what happens in the server's HTTPS listener - s := httptest.NewUnstartedServer(server.Handler) - s.TLS = &tls.Config{ - Certificates: []tls.Certificate{cert}, - ClientAuth: tls.RequireAndVerifyClientCert, - ClientCAs: pool, - } - s.StartTLS() - defer s.Close() - endpoint := s.URL + "/v1/data/foo" - - t.Run("happy path", func(t *testing.T) { - c := newClient(t, pool, "testdata/client-cert.pem", "testdata/client-key.pem") - resp, err := c.Get(endpoint) - if err != nil { - t.Fatalf("GET: %v", err) - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - t.Errorf("expected status 200, got %s", resp.Status) - } - }) - - t.Run("authn successful, authz failed", func(t *testing.T) { - c := newClient(t, pool, "testdata/client-cert-2.pem", "testdata/client-key-2.pem") - resp, err := c.Get(endpoint) - if err != nil { - t.Fatalf("GET: %v", err) - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusUnauthorized { - t.Errorf("expected status 401, got %s", resp.Status) - } - }) - - t.Run("client trusts server, but doesn't provide client cert", func(t *testing.T) { - c := newClient(t, pool) - _, err := c.Get(endpoint) - if _, ok := err.(*url.Error); !ok { - t.Errorf("expected *url.Error, got %T: %v", err, err) - } - }) -} - -func newClient(t *testing.T, pool *x509.CertPool, clientKeyPair ...string) *http.Client { - t.Helper() - c := *http.DefaultClient - // Note: zero-values in http.Transport are bad settings -- they let the client - // leak connections -- but it's good enough for these tests. Don't instantiate - // http.Transport without providing non-zero values in non-test code, please. - // See https://github.com/golang/go/issues/19620 for details. - tr := &http.Transport{ - TLSClientConfig: &tls.Config{ - RootCAs: pool, - }, - } - if len(clientKeyPair) == 2 { - clientCert, err := tls.LoadX509KeyPair(clientKeyPair[0], clientKeyPair[1]) - if err != nil { - t.Fatalf("read test client cert/key: %v", err) - } - tr.TLSClientConfig.Certificates = []tls.Certificate{clientCert} - } - c.Transport = tr - return &c -} - func TestShutdown(t *testing.T) { f := newFixture(t, func(s *Server) { s.WithDiagnosticAddresses([]string{":8443"}) diff --git a/server/testdata/.gitignore b/test/e2e/tls/testdata/.gitignore similarity index 100% rename from server/testdata/.gitignore rename to test/e2e/tls/testdata/.gitignore diff --git a/server/testdata/ca.pem b/test/e2e/tls/testdata/ca.pem similarity index 100% rename from server/testdata/ca.pem rename to test/e2e/tls/testdata/ca.pem diff --git a/server/testdata/client-cert-2.pem b/test/e2e/tls/testdata/client-cert-2.pem similarity index 100% rename from server/testdata/client-cert-2.pem rename to test/e2e/tls/testdata/client-cert-2.pem diff --git a/server/testdata/client-cert.pem b/test/e2e/tls/testdata/client-cert.pem similarity index 100% rename from server/testdata/client-cert.pem rename to test/e2e/tls/testdata/client-cert.pem diff --git a/server/testdata/client-key-2.pem b/test/e2e/tls/testdata/client-key-2.pem similarity index 100% rename from server/testdata/client-key-2.pem rename to test/e2e/tls/testdata/client-key-2.pem diff --git a/server/testdata/client-key.pem b/test/e2e/tls/testdata/client-key.pem similarity index 100% rename from server/testdata/client-key.pem rename to test/e2e/tls/testdata/client-key.pem diff --git a/server/testdata/gencerts.sh b/test/e2e/tls/testdata/gencerts.sh similarity index 100% rename from server/testdata/gencerts.sh rename to test/e2e/tls/testdata/gencerts.sh diff --git a/server/testdata/server-cert.pem b/test/e2e/tls/testdata/server-cert.pem similarity index 100% rename from server/testdata/server-cert.pem rename to test/e2e/tls/testdata/server-cert.pem diff --git a/server/testdata/server-key.pem b/test/e2e/tls/testdata/server-key.pem similarity index 100% rename from server/testdata/server-key.pem rename to test/e2e/tls/testdata/server-key.pem diff --git a/test/e2e/tls/tls_test.go b/test/e2e/tls/tls_test.go new file mode 100644 index 0000000000..15f58e232c --- /dev/null +++ b/test/e2e/tls/tls_test.go @@ -0,0 +1,144 @@ +package tls + +import ( + "crypto/tls" + "crypto/x509" + "flag" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "os" + "testing" + + "github.com/open-policy-agent/opa/server" + "github.com/open-policy-agent/opa/test/e2e" +) + +var testRuntime *e2e.TestRuntime +var pool *x509.CertPool + +// print error to stderr, exit 1 +func fatal(err interface{}) { + fmt.Fprintf(os.Stderr, "%s\n", err) + os.Exit(1) +} + +func TestMain(m *testing.M) { + flag.Parse() + + caCertPEM, err := ioutil.ReadFile("testdata/ca.pem") + if err != nil { + fatal(err) + } + pool = x509.NewCertPool() + if ok := pool.AppendCertsFromPEM(caCertPEM); !ok { + fatal("failed to parse CA cert") + } + cert, err := tls.LoadX509KeyPair("testdata/server-cert.pem", "testdata/server-key.pem") + if err != nil { + fatal(err) + } + + // We need the policy to be present already, otherwise authorization + // for the health endpoint is going to fail on server startup. + authzPolicy := []byte(`package system.authz +import input.identity +default allow = false +allow { + identity = "CN=my-client" +}`) + + tmpfile, err := ioutil.TempFile("", "authz.*.rego") + if err != nil { + fatal(err) + } + defer os.Remove(tmpfile.Name()) + + if _, err := tmpfile.Write(authzPolicy); err != nil { + fatal(err) + } + if err := tmpfile.Close(); err != nil { + fatal(err) + } + + testServerParams := e2e.NewAPIServerTestParams() + testServerParams.Addrs = &[]string{"https://127.0.0.1:0"} + testServerParams.CertPool = pool + testServerParams.Certificate = &cert + testServerParams.Authentication = server.AuthenticationTLS + testServerParams.Authorization = server.AuthorizationBasic + testServerParams.Paths = []string{"system.authz:" + tmpfile.Name()} + + testRuntime, err = e2e.NewTestRuntime(testServerParams) + if err != nil { + fatal(err) + } + + // We need a client with proper TLS setup, otherwise the health check + // that loops to determine if the server is ready will fail. + testRuntime.Client = newClient(pool, "testdata/client-cert.pem", "testdata/client-key.pem") + + os.Exit(testRuntime.RunTests(m)) +} + +func TestAuthenticationTLS(t *testing.T) { + endpoint := testRuntime.URL() + "/v1/data/foo" + + // Note: This test is redundant. When the testRuntime starts the server, it + // already queries the health endpoint using a properly authenticated, and + // authorized, http client. + t.Run("happy path", func(t *testing.T) { + c := newClient(pool, "testdata/client-cert.pem", "testdata/client-key.pem") + resp, err := c.Get(endpoint) + if err != nil { + t.Fatalf("GET: %v", err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + t.Errorf("expected status 200, got %s", resp.Status) + } + }) + + t.Run("authn successful, authz failed", func(t *testing.T) { + c := newClient(pool, "testdata/client-cert-2.pem", "testdata/client-key-2.pem") + resp, err := c.Get(endpoint) + if err != nil { + t.Fatalf("GET: %v", err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusUnauthorized { + t.Errorf("expected status 401, got %s", resp.Status) + } + }) + + t.Run("client trusts server, but doesn't provide client cert", func(t *testing.T) { + c := newClient(pool) + _, err := c.Get(endpoint) + if _, ok := err.(*url.Error); !ok { + t.Errorf("expected *url.Error, got %T: %v", err, err) + } + }) +} + +func newClient(pool *x509.CertPool, clientKeyPair ...string) *http.Client { + c := *http.DefaultClient + // Note: zero-values in http.Transport are bad settings -- they let the client + // leak connections -- but it's good enough for these tests. Don't instantiate + // http.Transport without providing non-zero values in non-test code, please. + // See https://github.com/golang/go/issues/19620 for details. + tr := &http.Transport{ + TLSClientConfig: &tls.Config{ + RootCAs: pool, + }, + } + if len(clientKeyPair) == 2 { + clientCert, err := tls.LoadX509KeyPair(clientKeyPair[0], clientKeyPair[1]) + if err != nil { + panic(err) + } + tr.TLSClientConfig.Certificates = []tls.Certificate{clientCert} + } + c.Transport = tr + return &c +}