diff --git a/agent/structs/config_entry_gateways.go b/agent/structs/config_entry_gateways.go index 7d4ffd4479..0d4019896f 100644 --- a/agent/structs/config_entry_gateways.go +++ b/agent/structs/config_entry_gateways.go @@ -713,6 +713,18 @@ type APIGatewayConfigEntry struct { RaftIndex } +func (e *APIGatewayConfigEntry) GetKind() string { return APIGateway } +func (e *APIGatewayConfigEntry) GetName() string { return e.Name } +func (e *APIGatewayConfigEntry) GetMeta() map[string]string { return e.Meta } +func (e *APIGatewayConfigEntry) GetRaftIndex() *RaftIndex { return &e.RaftIndex } +func (e *APIGatewayConfigEntry) GetEnterpriseMeta() *acl.EnterpriseMeta { return &e.EnterpriseMeta } + +var _ ControlledConfigEntry = (*APIGatewayConfigEntry)(nil) + +func (e *APIGatewayConfigEntry) GetStatus() Status { return e.Status } +func (e *APIGatewayConfigEntry) SetStatus(status Status) { e.Status = status } +func (e *APIGatewayConfigEntry) DefaultStatus() Status { return Status{} } + func (e *APIGatewayConfigEntry) ListenerIsReady(name string) bool { for _, condition := range e.Status.Conditions { if !condition.Resource.IsSame(&ResourceReference{ @@ -732,34 +744,28 @@ func (e *APIGatewayConfigEntry) ListenerIsReady(name string) bool { return true } -func (e *APIGatewayConfigEntry) GetKind() string { - return APIGateway -} - -func (e *APIGatewayConfigEntry) GetName() string { - if e == nil { - return "" - } - return e.Name -} - -func (e *APIGatewayConfigEntry) GetMeta() map[string]string { - if e == nil { - return nil - } - return e.Meta -} - func (e *APIGatewayConfigEntry) Normalize() error { for i, listener := range e.Listeners { protocol := strings.ToLower(string(listener.Protocol)) listener.Protocol = APIGatewayListenerProtocol(protocol) e.Listeners[i] = listener + + for i, cert := range listener.TLS.Certificates { + if cert.Kind == "" { + cert.Kind = InlineCertificate + } + listener.TLS.Certificates[i] = cert + } } + return nil } func (e *APIGatewayConfigEntry) Validate() error { + if err := validateConfigEntryMeta(e.Meta); err != nil { + return err + } + if err := e.validateListenerNames(); err != nil { return err } @@ -843,34 +849,6 @@ func (e *APIGatewayConfigEntry) CanWrite(authz acl.Authorizer) error { return authz.ToAllowAuthorizer().MeshWriteAllowed(&authzContext) } -func (e *APIGatewayConfigEntry) GetRaftIndex() *RaftIndex { - if e == nil { - return &RaftIndex{} - } - return &e.RaftIndex -} - -func (e *APIGatewayConfigEntry) GetEnterpriseMeta() *acl.EnterpriseMeta { - if e == nil { - return nil - } - return &e.EnterpriseMeta -} - -var _ ControlledConfigEntry = (*APIGatewayConfigEntry)(nil) - -func (e *APIGatewayConfigEntry) GetStatus() Status { - return e.Status -} - -func (e *APIGatewayConfigEntry) SetStatus(status Status) { - e.Status = status -} - -func (e *APIGatewayConfigEntry) DefaultStatus() Status { - return Status{} -} - // APIGatewayListenerProtocol is the protocol that an APIGateway listener uses type APIGatewayListenerProtocol string @@ -991,27 +969,10 @@ func (e *BoundAPIGatewayConfigEntry) IsInitializedForGateway(gateway *APIGateway return true } -func (e *BoundAPIGatewayConfigEntry) GetKind() string { - return BoundAPIGateway -} - -func (e *BoundAPIGatewayConfigEntry) GetName() string { - if e == nil { - return "" - } - return e.Name -} - -func (e *BoundAPIGatewayConfigEntry) GetMeta() map[string]string { - if e == nil { - return nil - } - return e.Meta -} - -func (e *BoundAPIGatewayConfigEntry) Normalize() error { - return nil -} +func (e *BoundAPIGatewayConfigEntry) GetKind() string { return BoundAPIGateway } +func (e *BoundAPIGatewayConfigEntry) GetName() string { return e.Name } +func (e *BoundAPIGatewayConfigEntry) GetMeta() map[string]string { return e.Meta } +func (e *BoundAPIGatewayConfigEntry) Normalize() error { return nil } func (e *BoundAPIGatewayConfigEntry) Validate() error { allowedCertificateKinds := map[string]bool{ diff --git a/agent/structs/config_entry_inline_certificate.go b/agent/structs/config_entry_inline_certificate.go index 18e3c7716d..bdbcbc6ae0 100644 --- a/agent/structs/config_entry_inline_certificate.go +++ b/agent/structs/config_entry_inline_certificate.go @@ -8,6 +8,7 @@ import ( "fmt" "github.com/hashicorp/consul/acl" + "github.com/miekg/dns" ) // InlineCertificateConfigEntry manages the configuration for an inline certificate @@ -39,6 +40,10 @@ func (e *InlineCertificateConfigEntry) GetEnterpriseMeta() *acl.EnterpriseMeta { func (e *InlineCertificateConfigEntry) GetRaftIndex() *RaftIndex { return &e.RaftIndex } func (e *InlineCertificateConfigEntry) Validate() error { + if err := validateConfigEntryMeta(e.Meta); err != nil { + return err + } + privateKeyBlock, _ := pem.Decode([]byte(e.PrivateKey)) if privateKeyBlock == nil { return errors.New("failed to parse private key PEM") @@ -61,6 +66,18 @@ func (e *InlineCertificateConfigEntry) Validate() error { return err } + // validate that each host referenced in the CN, DNSSans, and IPSans + // are valid hostnames + hosts, err := e.Hosts() + if err != nil { + return err + } + for _, host := range hosts { + if _, ok := dns.IsDomainName(host); !ok { + return fmt.Errorf("host %q must be a valid DNS hostname", host) + } + } + return nil } diff --git a/agent/structs/config_entry_inline_certificate_test.go b/agent/structs/config_entry_inline_certificate_test.go index 3e9d84e4f4..db46ca92a7 100644 --- a/agent/structs/config_entry_inline_certificate_test.go +++ b/agent/structs/config_entry_inline_certificate_test.go @@ -5,6 +5,73 @@ import "testing" const ( // generated via openssl req -x509 -sha256 -days 1825 -newkey rsa:2048 -keyout private.key -out certificate.crt validPrivateKey = `-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA0wzZeonUklhOvJ0AxcdDdCTiMwR9tsm/6IGcw9Jm50xVY+qg +5GFg1RWrQaODq7Gjqd/JDUAwtTBnQMs1yt6nbsHe2QhbD4XeqtZ+6fTv1ZpG3k8F +eB/M01xFqovczRV/ie77wd4vqoPD+AcfD8NDAFJt3htwUgGIqkQHP329Sh3TtLga +9ZMCs1MoTT+POYGUPL8bwt9R6ClNrucbH4Bs6OnX2ZFbKF75O9OHKNxWTmpDSodv +OFbFyKps3BfnPuF0Z6mj5M5yZeCjmtfS25PrsM3pMBGK5YHb0MlFfZIrIGboMbrz +9F/BMQJ64pMe43KwqHvTnbKWhp6PzLhEkPGLnwIDAQABAoIBADBEJAiONPszDu67 +yU1yAM8zEDgysr127liyK7PtDnOfVXgAVMNmMcsJpZzhVF+TxKY487YAFCOb6kE7 +OBYpTYla9SgVbR3js8TGQUgoKCFlowd8cvfB7gn4dEZIrjqIzB4zdYgk1Cne8JZs +qoHkWhJcx5ugEtPuXd7yp+WxT/T+6uOro06scp67NhP5t9yoAGFv5Vdb577RuzRo +Wkd9higQ9A20+GtjCY0EYxdgRviWvW7mM5/F+Lzcaui86ME+ga754gX8zgW3+NJ5 +LMsz5OLSnh291Uyjmr77HWBv/xvpq01Fls0LyJcgxFVZuJs5GQz+l3otSqv4FTP6 +Ua9w/YECgYEA8To3dgUK1QhzX5rwhWtlst3pItGTvmEdNzXmjgSylu7uKM13i+xg +llhp2uXrOEtuL+xtBZdeFNaijusbyqjg0xj6e4o31c19okuuDkJD5/sfQq22bvrn +gVJMGuESprIiPePrEyrXCHOdxH6eDgR2dIzAeO5vz0nnKGFAWrJJbvECgYEA3/mJ +eacXOJznw4Sa8jGWS2FtZLKxDHph7uDKMJmuG0ukb3aHJ9dMHrPleCLo8mhpoObA +hueoIbIP7swGrQx79+nZbnQpF6rMp6FAU5bF3gSrj1eWbaeh8pn9mrv4hal9USmn +orTbXMxDp3XSh7voR8Fqy5tMQqwZ+Lz74ccbw48CgYEA5cEhGdNrocPOv3x/IVRN +JLOfXX5nTaiJfxBja1imEIO5ajtoZWjaBdhn2gmqo4+UfyicHfsxrH9RjPX5HmkC +2Yys5gWbcJOr2Wxjd0k+DDFucL+rRsDKxq1vtxov/X0kh/YQ68ydynr0BTbjq04s +1I1KtOPEspYdCKS3+qpcrsECgYBtvYeVesBO9do9G0kMKC26y4bdEwzaz1ASykNn +IrWDHEH6dznr1HqwhHaHsZsvwucWdlmZAAKKWAOkfoU63uYS55qomvPTa9WQwNqS +2koi6Wjh+Al1uvAHvVncKgOwAgar8Nv5ReJBirgPYhSAexpppiRclL/93vNuw7Iq +wvMgkwKBgQC5wnb6SUUrzzKKSRgyusHM/XrjiKgVKq7lvFE9/iJkcw+BEXpjjbEe +RyD0a7PRtCfR39SMVrZp4KXVNNK5ln0WhuLvraMDwOpH9JDWHQiAhuJ3ooSwBylK ++QCLjyOtWAGZAIBRJyb1txfTXZ++dldkOjBi3bmEiadOa48ksvDsNQ== +-----END RSA PRIVATE KEY-----` + validCertificate = `-----BEGIN CERTIFICATE----- +MIIDQjCCAioCCQC6cMRYsE+ahDANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJV +UzELMAkGA1UECAwCQ0ExCzAJBgNVBAcMAkxBMQ0wCwYDVQQKDARUZXN0MQ0wCwYD +VQQLDARTdHViMRwwGgYDVQQDDBNob3N0LmNvbnN1bC5leGFtcGxlMB4XDTIzMDIx +NzAyMTA1MloXDTI4MDIxNjAyMTA1MlowYzELMAkGA1UEBhMCVVMxCzAJBgNVBAgM +AkNBMQswCQYDVQQHDAJMQTENMAsGA1UECgwEVGVzdDENMAsGA1UECwwEU3R1YjEc +MBoGA1UEAwwTaG9zdC5jb25zdWwuZXhhbXBsZTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBANMM2XqJ1JJYTrydAMXHQ3Qk4jMEfbbJv+iBnMPSZudMVWPq +oORhYNUVq0Gjg6uxo6nfyQ1AMLUwZ0DLNcrep27B3tkIWw+F3qrWfun079WaRt5P +BXgfzNNcRaqL3M0Vf4nu+8HeL6qDw/gHHw/DQwBSbd4bcFIBiKpEBz99vUod07S4 +GvWTArNTKE0/jzmBlDy/G8LfUegpTa7nGx+AbOjp19mRWyhe+TvThyjcVk5qQ0qH +bzhWxciqbNwX5z7hdGepo+TOcmXgo5rX0tuT67DN6TARiuWB29DJRX2SKyBm6DG6 +8/RfwTECeuKTHuNysKh7052yloaej8y4RJDxi58CAwEAATANBgkqhkiG9w0BAQsF +AAOCAQEAHF10odRNJ7TKvcD2JPtR8wMacfldSiPcQnn+rhMUyBaKOoSrALxOev+N +L8N+RtEV+KXkyBkvT71OZzEpY9ROwqOQ/acnMdbfG0IBPbg3c/7WDD2sjcdr1zvc +U3T7WJ7G3guZ5aWCuAGgOyT6ZW8nrDa4yFbKZ1PCJkvUQ2ttO1lXmyGPM533Y2pi +SeXP6LL7z5VNqYO3oz5IJEstt10IKxdmb2gKFhHjgEmHN2gFL0jaPi4mjjaINrxq +MdqcM9IzLr26AjZ45NuI9BCcZWO1mraaQTOIb3QL5LyqaC7CRJXLYPSGARthyDhq +J3TrQE3YVrL4D9xnklT86WDnZKApJg== +-----END CERTIFICATE-----` + mismatchedCertificate = `-----BEGIN CERTIFICATE----- +MIIDQjCCAioCCQC2H6+PYz23xDANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJV +UzELMAkGA1UECAwCQ0ExCzAJBgNVBAcMAkxBMQ0wCwYDVQQKDARUZXN0MQwwCgYD +VQQLDANGb28xHTAbBgNVBAMMFG90aGVyLmNvbnN1bC5leGFtcGxlMB4XDTIzMDIx +NzAyMTM0OVoXDTI4MDIxNjAyMTM0OVowYzELMAkGA1UEBhMCVVMxCzAJBgNVBAgM +AkNBMQswCQYDVQQHDAJMQTENMAsGA1UECgwEVGVzdDEMMAoGA1UECwwDRm9vMR0w +GwYDVQQDDBRvdGhlci5jb25zdWwuZXhhbXBsZTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAO0IH/dzmWJaTPVL32xQVHivrnQk38vskW0ymILYuaismUMJ +0+xrcaTcVljU+3nKhmSW9wcYSFY02GcGWAdcw8x8xO801cna020T+DIWiYaljXT3 +agrbYfULF9q+ihT6IL1D2mFa0AW1x6Bk1XAmZRSTpRBhp7iFNnCXGRK8sSSr95ge +DxaRyj/2F8t6kG+ANPkRBiPd2rRgsYQjuTLuZYBvseeJygnSF8ty1QMg6koz7kdN +bPon3Q5GFH71WNwzm9G3DWjMIu+dhpHz7rsbCnhwLB5lh1jsZBYkAMt3kiyY0g4I +ReuiVWesMe+AMG/DQZvZ5mE252QFJ92dLTeo5RcCAwEAATANBgkqhkiG9w0BAQsF +AAOCAQEAijm6blixjl+pMRAj7EajoPjU+GqhooZayJrvdwvofwcPxQYpkPuh7Uc6 +l2z494b75cRzMw7wS+iW/ad8NYrfw1JwHMsUfncxs5LDO5GsKl9Krg/39goDl3wC +ywTcl00y+FMYfldNPjKDLunENmn+yPa2pKuBVQ0yOKALp+oUeJFVzRNPV5fohlBi +HjypkO0KaVmCG6P01cqCgVkNzxnX9qQYP3YXX1yt5iOcI7QcoOa5WnRhOuD8WqJ1 +v3AZGYNvKyXf9E5nD0y2Cmz6t1awjFjzMlXMx6AdHrjWqxtHhYQ1xz4P4NfzK27m +cCtURSzXMgcrSeZLepBfdICf+0/0+Q== +-----END CERTIFICATE-----` + emptyCNPrivateKey = `-----BEGIN RSA PRIVATE KEY----- MIIEowIBAAKCAQEAx95Opa6t4lGEpiTUogEBptqOdam2ch4BHQGhNhX/MrDwwuZQ httBwMfngQ/wd9NmYEPAwj0dumUoAITIq6i2jQlhqTodElkbsd5vWY8R/bxJWQSo NvVE12TlzECxGpJEiHt4W0r8pGffk+rvpljiUyCfnT1kGF3znOSjK1hRMTn6RKWC @@ -31,7 +98,7 @@ T0+9gwKBgHDoerX7NTskg0H0t8O+iSMevdxpEWp34ZYa9gHiftTQGyrRgERCa7Gj nZPAxKb2JoWyfnu3v7G5gZ8fhDFsiOxLbZv6UZJBbUIh1MjJISpXrForDrC2QNLX kHrHfwBFDB3KMudhQknsJzEJKCL/KmFH6o0MvsoaT9yzEl3K+ah/ -----END RSA PRIVATE KEY-----` - validCertificate = `-----BEGIN CERTIFICATE----- + emptyCNCertificate = `-----BEGIN CERTIFICATE----- MIICljCCAX4CCQCQMDsYO8FrPjANBgkqhkiG9w0BAQsFADANMQswCQYDVQQGEwJV UzAeFw0yMjEyMjAxNzUwMjVaFw0yNzEyMTkxNzUwMjVaMA0xCzAJBgNVBAYTAlVT MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx95Opa6t4lGEpiTUogEB @@ -46,26 +113,12 @@ RahYIzNLRBTLrwadLAZkApUpZvB8qDK4knsTWFYujNsylCww2A6ajzIMFNU4GkUK NtyHRuD+KYRmjXtyX1yHNqfGN3vOQmwavHq2R8wHYuBSc6LAHHV9vG+j0VsgMELO qwxn8SmLkSKbf2+MsQVzLCXXN5u+D8Yv+4py+oKP4EQ5aFZuDEx+r/G/31rTthww AAJAMaoXmoYVdgXV+CPuBb2M4XCpuzLu3bcA2PXm5ipSyIgntMKwXV7r ------END CERTIFICATE-----` - mismatchedCertificate = `-----BEGIN CERTIFICATE----- -MIICljCCAX4CCQC49bq8e0QgLDANBgkqhkiG9w0BAQsFADANMQswCQYDVQQGEwJV -UzAeFw0yMjEyMjAxNzUyMzJaFw0yNzEyMTkxNzUyMzJaMA0xCzAJBgNVBAYTAlVT -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk7Are9ulVDY0IqaG5Pt/ -OVuS0kmDhgVUfQBM5JDGRfIsu1ebn68kn5JGCTQ+nC8nU9QXRJS7vG6As5GWm08W -FpkOyIbHLjOhWtYCYzQ+0R+sSSoMnczgl8l6wIUIkR3Vpoy6QUsSZbvo4/xDi3Uk -1CF+JMTM2oFDLD8PNrNzW/txRyTugK36W1G1ofUhvP6EHsTjmVcZwBcLOKToov6L -Ai758MLztl1/X/90DNdZwuHC9fGIgx52Ojz3+XIocXFttr+J8xZglMCtqL4n40bh -5b1DE+hC3NHQmA+7Chc99z28baj2cU1woNk/TO+ewqpyvj+WPWwGOQt3U63ZoPaw -yQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQCMF3JlrDdcSv2KYrxEp1tWB/GglI8a -JiSvrf3hePaRz59099bg4DoHzTn0ptOcOPOO9epDPbCJrUqLuPlwvrQRvll6GaW1 -y3TcbnE1AbwTAjbOTgpLhvuj6IVlyNNLoKbjZqs4A8N8i6UkQ7Y8qg77lwxD3QoH -pWLwGZKJifKPa7ObVWmKj727kbU59nA2Hx+Y4qa/MyiPWxJM9Y0JsFGxSBxp4kmQ -q4ikzSWaPv/TvtV+d4mO1H44aggdNMCYIQd/5BXQzG40l+ecHnBueJyG312ax/Zp -NsYUAKQT864cGlxrnWVgT4sW/tsl9Qen7g9iAdeBAPvLO7cQjAjtc7KZ -----END CERTIFICATE-----` ) func TestInlineCertificate(t *testing.T) { + t.Parallel() + cases := map[string]configEntryTestcase{ "invalid private key": { entry: &InlineCertificateConfigEntry{ @@ -101,6 +154,15 @@ func TestInlineCertificate(t *testing.T) { Certificate: validCertificate, }, }, + "empty cn certificate": { + entry: &InlineCertificateConfigEntry{ + Kind: InlineCertificate, + Name: "cert-five", + PrivateKey: emptyCNPrivateKey, + Certificate: emptyCNCertificate, + }, + validateErr: "host \"\" must be a valid DNS hostname", + }, } testConfigEntryNormalizeAndValidate(t, cases) } diff --git a/agent/structs/config_entry_routes.go b/agent/structs/config_entry_routes.go index 26e50b3f2d..027c8f7f7e 100644 --- a/agent/structs/config_entry_routes.go +++ b/agent/structs/config_entry_routes.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/hashicorp/consul/acl" + "github.com/miekg/dns" ) // BoundRoute indicates a route that has parent gateways which @@ -41,15 +42,22 @@ type HTTPRouteConfigEntry struct { RaftIndex } -func (e *HTTPRouteConfigEntry) GetServices() []HTTPService { - targets := []HTTPService{} - for _, rule := range e.Rules { - for _, service := range rule.Services { - targets = append(targets, service) - } - } - return targets -} +func (e *HTTPRouteConfigEntry) GetKind() string { return HTTPRoute } +func (e *HTTPRouteConfigEntry) GetName() string { return e.Name } +func (e *HTTPRouteConfigEntry) GetMeta() map[string]string { return e.Meta } +func (e *HTTPRouteConfigEntry) GetEnterpriseMeta() *acl.EnterpriseMeta { return &e.EnterpriseMeta } +func (e *HTTPRouteConfigEntry) GetRaftIndex() *RaftIndex { return &e.RaftIndex } + +var _ ControlledConfigEntry = (*HTTPRouteConfigEntry)(nil) + +func (e *HTTPRouteConfigEntry) GetStatus() Status { return e.Status } +func (e *HTTPRouteConfigEntry) SetStatus(status Status) { e.Status = status } +func (e *HTTPRouteConfigEntry) DefaultStatus() Status { return Status{} } + +var _ BoundRoute = (*HTTPRouteConfigEntry)(nil) + +func (e *HTTPRouteConfigEntry) GetParents() []ResourceReference { return e.Parents } +func (e *HTTPRouteConfigEntry) GetProtocol() APIGatewayListenerProtocol { return ListenerProtocolHTTP } func (e *HTTPRouteConfigEntry) GetServiceNames() []ServiceName { services := []ServiceName{} @@ -59,36 +67,209 @@ func (e *HTTPRouteConfigEntry) GetServiceNames() []ServiceName { return services } -func (e *HTTPRouteConfigEntry) GetKind() string { - return HTTPRoute -} - -func (e *HTTPRouteConfigEntry) GetName() string { - if e == nil { - return "" +func (e *HTTPRouteConfigEntry) GetServices() []HTTPService { + targets := []HTTPService{} + for _, rule := range e.Rules { + targets = append(targets, rule.Services...) } - return e.Name -} - -func (e *HTTPRouteConfigEntry) GetParents() []ResourceReference { - if e == nil { - return []ResourceReference{} - } - return e.Parents -} - -func (e *HTTPRouteConfigEntry) GetProtocol() APIGatewayListenerProtocol { - return ListenerProtocolHTTP + return targets } func (e *HTTPRouteConfigEntry) Normalize() error { + for i, parent := range e.Parents { + if parent.Kind == "" { + parent.Kind = APIGateway + e.Parents[i] = parent + } + } + + for i, rule := range e.Rules { + for j, match := range rule.Matches { + rule.Matches[j] = normalizeHTTPMatch(match) + } + e.Rules[i] = rule + } + return nil } +func normalizeHTTPMatch(match HTTPMatch) HTTPMatch { + method := string(match.Method) + method = strings.ToUpper(method) + match.Method = HTTPMatchMethod(method) + + pathMatch := match.Path.Match + if string(pathMatch) == "" { + match.Path.Match = HTTPPathMatchPrefix + match.Path.Value = "/" + } + + return match +} + func (e *HTTPRouteConfigEntry) Validate() error { + for _, host := range e.Hostnames { + // validate that each host referenced in a valid dns name and has + // no wildcards in it + if _, ok := dns.IsDomainName(host); !ok { + return fmt.Errorf("host %q must be a valid DNS hostname", host) + } + + if strings.ContainsRune(host, '*') { + return fmt.Errorf("host %q must not be a wildcard", host) + } + } + + validParentKinds := map[string]bool{ + APIGateway: true, + } + + for _, parent := range e.Parents { + if !validParentKinds[parent.Kind] { + return fmt.Errorf("unsupported parent kind: %q, must be 'api-gateway'", parent.Kind) + } + } + + if err := validateConfigEntryMeta(e.Meta); err != nil { + return err + } + + for i, rule := range e.Rules { + if err := validateRule(rule); err != nil { + return fmt.Errorf("Rule[%d], %w", i, err) + } + } + return nil } +func validateRule(rule HTTPRouteRule) error { + if err := validateFilters(rule.Filters); err != nil { + return err + } + + for i, match := range rule.Matches { + if err := validateMatch(match); err != nil { + return fmt.Errorf("Match[%d], %w", i, err) + } + } + + for i, service := range rule.Services { + if err := validateHTTPService(service); err != nil { + return fmt.Errorf("Service[%d], %w", i, err) + } + } + + return nil +} + +func validateMatch(match HTTPMatch) error { + if match.Method != HTTPMatchMethodAll { + if !isValidHTTPMethod(string(match.Method)) { + return fmt.Errorf("Method contains an invalid method %q", match.Method) + } + } + + for i, query := range match.Query { + if err := validateHTTPQueryMatch(query); err != nil { + return fmt.Errorf("Query[%d], %w", i, err) + } + } + + for i, header := range match.Headers { + if err := validateHTTPHeaderMatch(header); err != nil { + return fmt.Errorf("Headers[%d], %w", i, err) + } + } + + if err := validateHTTPPathMatch(match.Path); err != nil { + return fmt.Errorf("Path, %w", err) + } + + return nil +} + +func validateHTTPService(service HTTPService) error { + return validateFilters(service.Filters) +} + +func validateFilters(filter HTTPFilters) error { + for i, header := range filter.Headers { + if err := validateHeaderFilter(header); err != nil { + return fmt.Errorf("HTTPFilters, Headers[%d], %w", i, err) + } + } + + for i, rewrite := range filter.URLRewrites { + if err := validateURLRewrite(rewrite); err != nil { + return fmt.Errorf("HTTPFilters, URLRewrite[%d], %w", i, err) + } + } + + return nil +} + +func validateURLRewrite(rewrite URLRewrite) error { + // TODO: we don't really have validation of the actual params + // passed as "PrefixRewrite" in our discoverychain config + // entries, figure out if we should have something here + return nil +} + +func validateHeaderFilter(filter HTTPHeaderFilter) error { + // TODO: we don't really have validation of the values + // passed as header modifiers in our current discoverychain + // config entries, figure out if we need to + return nil +} + +func validateHTTPQueryMatch(query HTTPQueryMatch) error { + if query.Name == "" { + return fmt.Errorf("missing required Name field") + } + + switch query.Match { + case HTTPQueryMatchExact, + HTTPQueryMatchPresent, + HTTPQueryMatchRegularExpression: + return nil + default: + return fmt.Errorf("match type should be one of present, exact, or regex") + } +} + +func validateHTTPHeaderMatch(header HTTPHeaderMatch) error { + if header.Name == "" { + return fmt.Errorf("missing required Name field") + } + + switch header.Match { + case HTTPHeaderMatchExact, + HTTPHeaderMatchPrefix, + HTTPHeaderMatchRegularExpression, + HTTPHeaderMatchSuffix, + HTTPHeaderMatchPresent: + return nil + default: + return fmt.Errorf("match type should be one of present, exact, prefix, suffix, or regex") + } +} + +func validateHTTPPathMatch(path HTTPPathMatch) error { + switch path.Match { + case HTTPPathMatchExact, + HTTPPathMatchPrefix: + if !strings.HasPrefix(path.Value, "/") { + return fmt.Errorf("%s type match doesn't start with '/': %q", path.Match, path.Value) + } + fallthrough + case HTTPPathMatchRegularExpression: + return nil + default: + return fmt.Errorf("match type should be one of exact, prefix, or regex") + } +} + func (e *HTTPRouteConfigEntry) CanRead(authz acl.Authorizer) error { var authzContext acl.AuthorizerContext e.FillAuthzContext(&authzContext) @@ -101,27 +282,6 @@ func (e *HTTPRouteConfigEntry) CanWrite(authz acl.Authorizer) error { return authz.ToAllowAuthorizer().MeshWriteAllowed(&authzContext) } -func (e *HTTPRouteConfigEntry) GetMeta() map[string]string { - if e == nil { - return nil - } - return e.Meta -} - -func (e *HTTPRouteConfigEntry) GetEnterpriseMeta() *acl.EnterpriseMeta { - if e == nil { - return nil - } - return &e.EnterpriseMeta -} - -func (e *HTTPRouteConfigEntry) GetRaftIndex() *RaftIndex { - if e == nil { - return &RaftIndex{} - } - return &e.RaftIndex -} - func (e *HTTPRouteConfigEntry) FilteredHostnames(listenerHostname string) []string { if len(e.Hostnames) == 0 { // we have no hostnames specified here, so treat it like a wildcard @@ -279,20 +439,6 @@ func (s HTTPService) ServiceName() ServiceName { return NewServiceName(s.Name, &s.EnterpriseMeta) } -var _ ControlledConfigEntry = (*HTTPRouteConfigEntry)(nil) - -func (e *HTTPRouteConfigEntry) GetStatus() Status { - return e.Status -} - -func (e *HTTPRouteConfigEntry) SetStatus(status Status) { - e.Status = status -} - -func (e *HTTPRouteConfigEntry) DefaultStatus() Status { - return Status{} -} - // TCPRouteConfigEntry manages the configuration for a TCP route // with the given name. type TCPRouteConfigEntry struct { @@ -317,9 +463,22 @@ type TCPRouteConfigEntry struct { RaftIndex } -func (e *TCPRouteConfigEntry) GetServices() []TCPService { - return e.Services -} +func (e *TCPRouteConfigEntry) GetKind() string { return TCPRoute } +func (e *TCPRouteConfigEntry) GetName() string { return e.Name } +func (e *TCPRouteConfigEntry) GetMeta() map[string]string { return e.Meta } +func (e *TCPRouteConfigEntry) GetRaftIndex() *RaftIndex { return &e.RaftIndex } +func (e *TCPRouteConfigEntry) GetEnterpriseMeta() *acl.EnterpriseMeta { return &e.EnterpriseMeta } + +var _ ControlledConfigEntry = (*TCPRouteConfigEntry)(nil) + +func (e *TCPRouteConfigEntry) GetStatus() Status { return e.Status } +func (e *TCPRouteConfigEntry) SetStatus(status Status) { e.Status = status } +func (e *TCPRouteConfigEntry) DefaultStatus() Status { return Status{} } + +var _ BoundRoute = (*TCPRouteConfigEntry)(nil) + +func (e *TCPRouteConfigEntry) GetParents() []ResourceReference { return e.Parents } +func (e *TCPRouteConfigEntry) GetProtocol() APIGatewayListenerProtocol { return ListenerProtocolTCP } func (e *TCPRouteConfigEntry) GetServiceNames() []ServiceName { services := []ServiceName{} @@ -329,34 +488,7 @@ func (e *TCPRouteConfigEntry) GetServiceNames() []ServiceName { return services } -func (e *TCPRouteConfigEntry) GetKind() string { - return TCPRoute -} - -func (e *TCPRouteConfigEntry) GetName() string { - if e == nil { - return "" - } - return e.Name -} - -func (e *TCPRouteConfigEntry) GetParents() []ResourceReference { - if e == nil { - return []ResourceReference{} - } - return e.Parents -} - -func (e *TCPRouteConfigEntry) GetProtocol() APIGatewayListenerProtocol { - return ListenerProtocolTCP -} - -func (e *TCPRouteConfigEntry) GetMeta() map[string]string { - if e == nil { - return nil - } - return e.Meta -} +func (e *TCPRouteConfigEntry) GetServices() []TCPService { return e.Services } func (e *TCPRouteConfigEntry) Normalize() error { for i, parent := range e.Parents { @@ -381,6 +513,11 @@ func (e *TCPRouteConfigEntry) Validate() error { return fmt.Errorf("unsupported parent kind: %q, must be 'api-gateway'", parent.Kind) } } + + if err := validateConfigEntryMeta(e.Meta); err != nil { + return err + } + return nil } @@ -396,34 +533,6 @@ func (e *TCPRouteConfigEntry) CanWrite(authz acl.Authorizer) error { return authz.ToAllowAuthorizer().MeshWriteAllowed(&authzContext) } -func (e *TCPRouteConfigEntry) GetRaftIndex() *RaftIndex { - if e == nil { - return &RaftIndex{} - } - return &e.RaftIndex -} - -func (e *TCPRouteConfigEntry) GetEnterpriseMeta() *acl.EnterpriseMeta { - if e == nil { - return nil - } - return &e.EnterpriseMeta -} - -var _ ControlledConfigEntry = (*TCPRouteConfigEntry)(nil) - -func (e *TCPRouteConfigEntry) GetStatus() Status { - return e.Status -} - -func (e *TCPRouteConfigEntry) SetStatus(status Status) { - e.Status = status -} - -func (e *TCPRouteConfigEntry) DefaultStatus() Status { - return Status{} -} - // TCPService is a service reference for a TCPRoute type TCPService struct { Name string diff --git a/agent/structs/config_entry_routes_test.go b/agent/structs/config_entry_routes_test.go index dc89ca9e09..ecacafdbf5 100644 --- a/agent/structs/config_entry_routes_test.go +++ b/agent/structs/config_entry_routes_test.go @@ -7,6 +7,8 @@ import ( ) func TestTCPRoute(t *testing.T) { + t.Parallel() + cases := map[string]configEntryTestcase{ "multiple services": { entry: &TCPRouteConfigEntry{ @@ -56,3 +58,207 @@ func TestTCPRoute(t *testing.T) { } testConfigEntryNormalizeAndValidate(t, cases) } + +func TestHTTPRoute(t *testing.T) { + t.Parallel() + + cases := map[string]configEntryTestcase{ + "normalize parent kind": { + entry: &HTTPRouteConfigEntry{ + Kind: HTTPRoute, + Name: "route-one", + Parents: []ResourceReference{{ + Name: "gateway", + }}, + }, + normalizeOnly: true, + check: func(t *testing.T, entry ConfigEntry) { + expectedParent := ResourceReference{ + Kind: APIGateway, + Name: "gateway", + } + route := entry.(*HTTPRouteConfigEntry) + require.Len(t, route.Parents, 1) + require.Equal(t, expectedParent, route.Parents[0]) + }, + }, + "invalid parent kind": { + entry: &HTTPRouteConfigEntry{ + Kind: HTTPRoute, + Name: "route-two", + Parents: []ResourceReference{{ + Kind: "route", + Name: "gateway", + }}, + }, + validateErr: "unsupported parent kind", + }, + "wildcard hostnames": { + entry: &HTTPRouteConfigEntry{ + Kind: HTTPRoute, + Name: "route-two", + Parents: []ResourceReference{{ + Name: "gateway", + }}, + Hostnames: []string{"*"}, + }, + validateErr: "host \"*\" must not be a wildcard", + }, + "wildcard subdomain": { + entry: &HTTPRouteConfigEntry{ + Kind: HTTPRoute, + Name: "route-two", + Parents: []ResourceReference{{ + Name: "gateway", + }}, + Hostnames: []string{"*.consul.example"}, + }, + validateErr: "host \"*.consul.example\" must not be a wildcard", + }, + "valid dns hostname": { + entry: &HTTPRouteConfigEntry{ + Kind: HTTPRoute, + Name: "route-two", + Parents: []ResourceReference{{ + Name: "gateway", + }}, + Hostnames: []string{"...not legal"}, + }, + validateErr: "host \"...not legal\" must be a valid DNS hostname", + }, + "rule matches invalid header match type": { + entry: &HTTPRouteConfigEntry{ + Kind: HTTPRoute, + Name: "route-two", + Parents: []ResourceReference{{ + Name: "gateway", + }}, + Rules: []HTTPRouteRule{{ + Matches: []HTTPMatch{{ + Headers: []HTTPHeaderMatch{{ + Match: HTTPHeaderMatchType("foo"), + Name: "foo", + }}, + }}, + }}, + }, + validateErr: "Rule[0], Match[0], Headers[0], match type should be one of present, exact, prefix, suffix, or regex", + }, + "rule matches invalid header match name": { + entry: &HTTPRouteConfigEntry{ + Kind: HTTPRoute, + Name: "route-two", + Parents: []ResourceReference{{ + Name: "gateway", + }}, + Rules: []HTTPRouteRule{{ + Matches: []HTTPMatch{{ + Headers: []HTTPHeaderMatch{{ + Match: HTTPHeaderMatchPresent, + }}, + }}, + }}, + }, + validateErr: "Rule[0], Match[0], Headers[0], missing required Name field", + }, + "rule matches invalid query match type": { + entry: &HTTPRouteConfigEntry{ + Kind: HTTPRoute, + Name: "route-two", + Parents: []ResourceReference{{ + Name: "gateway", + }}, + Rules: []HTTPRouteRule{{ + Matches: []HTTPMatch{{ + Query: []HTTPQueryMatch{{ + Match: HTTPQueryMatchType("foo"), + Name: "foo", + }}, + }}, + }}, + }, + validateErr: "Rule[0], Match[0], Query[0], match type should be one of present, exact, or regex", + }, + "rule matches invalid query match name": { + entry: &HTTPRouteConfigEntry{ + Kind: HTTPRoute, + Name: "route-two", + Parents: []ResourceReference{{ + Name: "gateway", + }}, + Rules: []HTTPRouteRule{{ + Matches: []HTTPMatch{{ + Query: []HTTPQueryMatch{{ + Match: HTTPQueryMatchPresent, + }}, + }}, + }}, + }, + validateErr: "Rule[0], Match[0], Query[0], missing required Name field", + }, + "rule matches invalid path match type": { + entry: &HTTPRouteConfigEntry{ + Kind: HTTPRoute, + Name: "route-two", + Parents: []ResourceReference{{ + Name: "gateway", + }}, + Rules: []HTTPRouteRule{{ + Matches: []HTTPMatch{{ + Path: HTTPPathMatch{ + Match: HTTPPathMatchType("foo"), + }, + }}, + }}, + }, + validateErr: "Rule[0], Match[0], Path, match type should be one of exact, prefix, or regex", + }, + "rule matches invalid path match prefix": { + entry: &HTTPRouteConfigEntry{ + Kind: HTTPRoute, + Name: "route-two", + Parents: []ResourceReference{{ + Name: "gateway", + }}, + Rules: []HTTPRouteRule{{ + Matches: []HTTPMatch{{ + Path: HTTPPathMatch{ + Match: HTTPPathMatchPrefix, + }, + }}, + }}, + }, + validateErr: "Rule[0], Match[0], Path, prefix type match doesn't start with '/': \"\"", + }, + "rule matches invalid method": { + entry: &HTTPRouteConfigEntry{ + Kind: HTTPRoute, + Name: "route-two", + Parents: []ResourceReference{{ + Name: "gateway", + }}, + Rules: []HTTPRouteRule{{ + Matches: []HTTPMatch{{ + Method: HTTPMatchMethod("foo"), + }}, + }}, + }, + validateErr: "Rule[0], Match[0], Method contains an invalid method \"FOO\"", + }, + "rule normalizes method casing and path matches": { + entry: &HTTPRouteConfigEntry{ + Kind: HTTPRoute, + Name: "route-two", + Parents: []ResourceReference{{ + Name: "gateway", + }}, + Rules: []HTTPRouteRule{{ + Matches: []HTTPMatch{{ + Method: HTTPMatchMethod("trace"), + }}, + }}, + }, + }, + } + testConfigEntryNormalizeAndValidate(t, cases) +} diff --git a/api/config_entry_inline_certificate_test.go b/api/config_entry_inline_certificate_test.go index 3b1cc1f54e..78771fcc78 100644 --- a/api/config_entry_inline_certificate_test.go +++ b/api/config_entry_inline_certificate_test.go @@ -10,47 +10,51 @@ import ( const ( // generated via openssl req -x509 -sha256 -days 1825 -newkey rsa:2048 -keyout private.key -out certificate.crt validPrivateKey = `-----BEGIN RSA PRIVATE KEY----- -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 -----END RSA PRIVATE KEY-----` validCertificate = `-----BEGIN CERTIFICATE----- -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 +J3TrQE3YVrL4D9xnklT86WDnZKApJg== -----END CERTIFICATE-----` )