Skip to content
This repository has been archived by the owner on Aug 6, 2023. It is now read-only.

feat: Add server certificate validation with Root CA and insecure option #1

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/data-sources/http.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ The following arguments are supported:
* `request_headers` - (Optional) A map of strings representing additional HTTP
headers to include in the request.

* `ca_certificate` - (Optional) PEM-encoded root certificates bundle for TLS authentication.

* `insecure` - (Optional) Whether server should be accessed without verifying the TLS certificate. Defaults to false.

## Attributes Reference

The following attributes are exported:
Expand Down
38 changes: 37 additions & 1 deletion internal/provider/data_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package provider

import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
"mime"
Expand Down Expand Up @@ -49,15 +51,49 @@ func dataSource() *schema.Resource {
Type: schema.TypeString,
},
},
"ca_certificate": {
Type: schema.TypeString,
Optional: true,
},

"insecure": {
Type: schema.TypeBool,
Optional: true,
Default: false,
},
},
}
}

func dataSourceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) (diags diag.Diagnostics) {
url := d.Get("url").(string)
headers := d.Get("request_headers").(map[string]interface{})
caCert := d.Get("ca_certificate").(string)

// Get the System Cert Pool
caCertPool, err := x509.SystemCertPool()
if err != nil {
return append(diags, diag.Errorf("Error tls: %s", err)...)
}

// Use `ca_certificate` cert pool
if caCert != "" {
caCertPool = x509.NewCertPool()
if ok := caCertPool.AppendCertsFromPEM([]byte(caCert)); !ok {
return append(diags, diag.Errorf("Error tls: Can't add the CA certificate to certificate pool")...)
}
}

client := &http.Client{}
tr := &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: caCertPool,
InsecureSkipVerify: d.Get("insecure").(bool),
},
}

client := &http.Client{
Transport: tr,
}

req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
Expand Down
257 changes: 226 additions & 31 deletions internal/provider/data_source_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package provider

import (
"crypto/x509"
"encoding/pem"
"fmt"
"net/http"
"net/http/httptest"
"regexp"
"strings"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
Expand Down Expand Up @@ -254,46 +257,238 @@ func TestDataSource_utf16(t *testing.T) {
// },
// })
// }
const testDataSourceConfig_basic_TLS_insecure = `
data "http" "http_test" {
url = "%s/meta_%d.txt"
insecure = true
}

output "body" {
value = data.http.http_test.body
}

output "response_headers" {
value = data.http.http_test.response_headers
}
`

func TestDataSource_http200_TLS_insecure(t *testing.T) {
testHttpMock := setUpMockHttpTLSServer()

defer testHttpMock.server.Close()

resource.UnitTest(t, resource.TestCase{
Providers: testProviders,
Steps: []resource.TestStep{
{
Config: fmt.Sprintf(testDataSourceConfig_basic_TLS_insecure, testHttpMock.server.URL, 200),
Check: func(s *terraform.State) error {
_, ok := s.RootModule().Resources["data.http.http_test"]
if !ok {
return fmt.Errorf("missing data resource")
}

outputs := s.RootModule().Outputs

if outputs["body"].Value != "1.0.0" {
return fmt.Errorf(
`'body' output is %s; want '1.0.0'`,
outputs["body"].Value,
)
}

response_headers := outputs["response_headers"].Value.(map[string]interface{})

if response_headers["X-Single"].(string) != "foobar" {
return fmt.Errorf(
`'X-Single' response header is %s; want 'foobar'`,
response_headers["X-Single"].(string),
)
}

if response_headers["X-Double"].(string) != "1, 2" {
return fmt.Errorf(
`'X-Double' response header is %s; want '1, 2'`,
response_headers["X-Double"].(string),
)
}

return nil
},
},
},
})
}

const testDataSourceConfig_basic_TLS_CA = `
data "http" "http_test" {
url = "%s/meta_%d.txt"
ca_certificate = <<EOF
%s
EOF
}

output "body" {
value = data.http.http_test.body
}

output "response_headers" {
value = data.http.http_test.response_headers
}
`

func TestDataSource_http200_TLS_CA(t *testing.T) {
testHttpMock := setUpMockHttpTLSServer()

defer testHttpMock.server.Close()

resource.UnitTest(t, resource.TestCase{
Providers: testProviders,
Steps: []resource.TestStep{
{
Config: fmt.Sprintf(testDataSourceConfig_basic_TLS_CA, testHttpMock.server.URL, 200, CertToPEM(testHttpMock.server.Certificate())),
Check: func(s *terraform.State) error {
_, ok := s.RootModule().Resources["data.http.http_test"]
if !ok {
return fmt.Errorf("missing data resource")
}

outputs := s.RootModule().Outputs

if outputs["body"].Value != "1.0.0" {
return fmt.Errorf(
`'body' output is %s; want '1.0.0'`,
outputs["body"].Value,
)
}

response_headers := outputs["response_headers"].Value.(map[string]interface{})

if response_headers["X-Single"].(string) != "foobar" {
return fmt.Errorf(
`'X-Single' response header is %s; want 'foobar'`,
response_headers["X-Single"].(string),
)
}

if response_headers["X-Double"].(string) != "1, 2" {
return fmt.Errorf(
`'X-Double' response header is %s; want '1, 2'`,
response_headers["X-Double"].(string),
)
}

return nil
},
},
},
})
}

const testDataSourceConfig_utf8_TLS_insecure = `
data "http" "http_test" {
url = "%s/utf-8/meta_%d.txt"
insecure = true
}

output "body" {
value = "${data.http.http_test.body}"
}
`

func TestDataSource_utf8_TLS_insecure(t *testing.T) {
testHttpMock := setUpMockHttpTLSServer()

defer testHttpMock.server.Close()

resource.UnitTest(t, resource.TestCase{
Providers: testProviders,
Steps: []resource.TestStep{
{
Config: fmt.Sprintf(testDataSourceConfig_utf8_TLS_insecure, testHttpMock.server.URL, 200),
Check: func(s *terraform.State) error {
_, ok := s.RootModule().Resources["data.http.http_test"]
if !ok {
return fmt.Errorf("missing data resource")
}

outputs := s.RootModule().Outputs

if outputs["body"].Value != "1.0.0" {
return fmt.Errorf(
`'body' output is %s; want '1.0.0'`,
outputs["body"].Value,
)
}

return nil
},
},
},
})
}

func setUpMockHttpServer() *TestHttpMock {
Server := httptest.NewServer(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
httpReqHandler(w, r)
}),
)

return &TestHttpMock{
server: Server,
}
}

w.Header().Set("Content-Type", "text/plain")
w.Header().Add("X-Single", "foobar")
w.Header().Add("X-Double", "1")
w.Header().Add("X-Double", "2")
if r.URL.Path == "/meta_200.txt" {
w.WriteHeader(http.StatusOK)
w.Write([]byte("1.0.0"))
} else if r.URL.Path == "/restricted/meta_200.txt" {
if r.Header.Get("Authorization") == "Zm9vOmJhcg==" {
w.WriteHeader(http.StatusOK)
w.Write([]byte("1.0.0"))
} else {
w.WriteHeader(http.StatusForbidden)
}
} else if r.URL.Path == "/utf-8/meta_200.txt" {
w.Header().Set("Content-Type", "text/plain; charset=UTF-8")
w.WriteHeader(http.StatusOK)
w.Write([]byte("1.0.0"))
} else if r.URL.Path == "/utf-16/meta_200.txt" {
w.Header().Set("Content-Type", "application/json; charset=UTF-16")
w.WriteHeader(http.StatusOK)
w.Write([]byte("\"1.0.0\""))
} else if r.URL.Path == "/x509/cert.pem" {
w.Header().Set("Content-Type", "application/x-x509-ca-cert")
w.WriteHeader(http.StatusOK)
w.Write([]byte("pem"))
} else if r.URL.Path == "/meta_404.txt" {
w.WriteHeader(http.StatusNotFound)
} else {
w.WriteHeader(http.StatusNotFound)
}
func setUpMockHttpTLSServer() *TestHttpMock {
Server := httptest.NewTLSServer(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
httpReqHandler(w, r)
}),
)

return &TestHttpMock{
server: Server,
}
}

func httpReqHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
w.Header().Add("X-Single", "foobar")
w.Header().Add("X-Double", "1")
w.Header().Add("X-Double", "2")
if r.URL.Path == "/meta_200.txt" {
w.WriteHeader(http.StatusOK)
w.Write([]byte("1.0.0"))
} else if r.URL.Path == "/restricted/meta_200.txt" {
if r.Header.Get("Authorization") == "Zm9vOmJhcg==" {
w.WriteHeader(http.StatusOK)
w.Write([]byte("1.0.0"))
} else {
w.WriteHeader(http.StatusForbidden)
}
} else if r.URL.Path == "/utf-8/meta_200.txt" {
w.Header().Set("Content-Type", "text/plain; charset=UTF-8")
w.WriteHeader(http.StatusOK)
w.Write([]byte("1.0.0"))
} else if r.URL.Path == "/utf-16/meta_200.txt" {
w.Header().Set("Content-Type", "application/json; charset=UTF-16")
w.WriteHeader(http.StatusOK)
w.Write([]byte("\"1.0.0\""))
} else if r.URL.Path == "/x509/cert.pem" {
w.Header().Set("Content-Type", "application/x-x509-ca-cert")
w.WriteHeader(http.StatusOK)
w.Write([]byte("pem"))
} else if r.URL.Path == "/meta_404.txt" {
w.WriteHeader(http.StatusNotFound)
} else {
w.WriteHeader(http.StatusNotFound)
}
}

// CertToPEM is a utility function returns a PEM encoded x509 Certificate
func CertToPEM(cert *x509.Certificate) string {
certPem := string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}))

return strings.Trim(certPem, "\n")
}