Add stricter validation and some normalization code for API Gateway ConfigEntries (#16304)

* Add stricter validation and some normalization code for API Gateway ConfigEntries
This commit is contained in:
Andrew Stucki 2023-02-17 14:22:01 -05:00 committed by GitHub
parent ee99d5c3a0
commit 58801cc8aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 604 additions and 245 deletions

View File

@ -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{

View File

@ -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
}

View File

@ -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)
}

View File

@ -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

View File

@ -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)
}

View File

@ -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-----`
)