NET-4984: Update APIGW Config Entries for JWT Auth (#18366)

* Added oss config entries for Policy and JWT on APIGW

* Updated structs for config entry

* Updated comments, ran deep-copy

* Move JWT configuration into OSS file

* Add in the config entry OSS file for jwts

* Added changelog

* fixing proto spacing

* Moved to using manually written deep copy method

* Use pointers for override/default fields in apigw config entries

* Run gen scripts for changed types
This commit is contained in:
John Maguire 2023-08-10 15:49:51 -04:00 committed by GitHub
parent 05604eeec1
commit 6c8ca0f89d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 1553 additions and 875 deletions

3
.changelog/_18366.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:feature
config-entry(api-gateway): (Enterprise only) Add GatewayPolicy to APIGateway Config Entry listeners
```

View File

@ -0,0 +1,10 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
//go:build !consulent
// +build !consulent
package structs
// APIGatewayJWTRequirement holds the list of JWT providers to be verified against
type APIGatewayJWTRequirement struct{}

View File

@ -887,6 +887,17 @@ type APIGatewayListener struct {
Protocol APIGatewayListenerProtocol Protocol APIGatewayListenerProtocol
// TLS is the TLS settings for the listener. // TLS is the TLS settings for the listener.
TLS APIGatewayTLSConfiguration TLS APIGatewayTLSConfiguration
// Override is the policy that overrides all other policy and route specific configuration
Override *APIGatewayPolicy `json:",omitempty"`
// Default is the policy that is the default for the listener and route, routes can override this behavior
Default *APIGatewayPolicy `json:",omitempty"`
}
// APIGatewayPolicy holds the policy that configures the gateway listener, this is used in the `Override` and `Default` fields of a listener
type APIGatewayPolicy struct {
// JWT holds the JWT configuration for the Listener
JWT *APIGatewayJWTRequirement `json:",omitempty"`
} }
func (l APIGatewayListener) GetHostname() string { func (l APIGatewayListener) GetHostname() string {

View File

@ -30,6 +30,8 @@ func validateUnusedKeys(unused []string) error {
// to exist on the target. // to exist on the target.
case strings.HasSuffix(strings.ToLower(k), "namespace"): case strings.HasSuffix(strings.ToLower(k), "namespace"):
err = multierror.Append(err, fmt.Errorf("invalid config key %q, namespaces are a consul enterprise feature", k)) err = multierror.Append(err, fmt.Errorf("invalid config key %q, namespaces are a consul enterprise feature", k))
case strings.Contains(strings.ToLower(k), "jwt"):
err = multierror.Append(err, fmt.Errorf("invalid config key %q, api-gateway jwt validation is a consul enterprise feature", k))
default: default:
err = multierror.Append(err, fmt.Errorf("invalid config key %q", k)) err = multierror.Append(err, fmt.Errorf("invalid config key %q", k))
} }

View File

@ -18,6 +18,20 @@ func (o *APIGatewayListener) DeepCopy() *APIGatewayListener {
cp.TLS.CipherSuites = make([]types.TLSCipherSuite, len(o.TLS.CipherSuites)) cp.TLS.CipherSuites = make([]types.TLSCipherSuite, len(o.TLS.CipherSuites))
copy(cp.TLS.CipherSuites, o.TLS.CipherSuites) copy(cp.TLS.CipherSuites, o.TLS.CipherSuites)
} }
if o.Override != nil {
cp.Override = new(APIGatewayPolicy)
*cp.Override = *o.Override
if o.Override.JWT != nil {
cp.Override.JWT = o.Override.JWT.DeepCopy()
}
}
if o.Default != nil {
cp.Default = new(APIGatewayPolicy)
*cp.Default = *o.Default
if o.Default.JWT != nil {
cp.Default.JWT = o.Default.JWT.DeepCopy()
}
}
return &cp return &cp
} }

View File

@ -0,0 +1,6 @@
package structs
// DeepCopy generates a deep copy of *APIGatewayJWTRequirement
func (o *APIGatewayJWTRequirement) DeepCopy() *APIGatewayJWTRequirement {
return new(APIGatewayJWTRequirement)
}

View File

@ -284,6 +284,10 @@ type APIGatewayListener struct {
Protocol string Protocol string
// TLS is the TLS settings for the listener. // TLS is the TLS settings for the listener.
TLS APIGatewayTLSConfiguration TLS APIGatewayTLSConfiguration
// Override is the policy that overrides all other policy and route specific configuration
Override *APIGatewayPolicy `json:",omitempty"`
// Default is the policy that is the default for the listener and route, routes can override this behavior
Default *APIGatewayPolicy `json:",omitempty"`
} }
// APIGatewayTLSConfiguration specifies the configuration of a listeners // APIGatewayTLSConfiguration specifies the configuration of a listeners
@ -302,3 +306,39 @@ type APIGatewayTLSConfiguration struct {
// Only applicable to connections negotiated via TLS 1.2 or earlier // Only applicable to connections negotiated via TLS 1.2 or earlier
CipherSuites []string `json:",omitempty" alias:"cipher_suites"` CipherSuites []string `json:",omitempty" alias:"cipher_suites"`
} }
// APIGatewayPolicy holds the policy that configures the gateway listener, this is used in the `Override` and `Default` fields of a listener
type APIGatewayPolicy struct {
// JWT holds the JWT configuration for the Listener
JWT *APIGatewayJWTRequirement `json:",omitempty"`
}
// APIGatewayJWTRequirement holds the list of JWT providers to be verified against
type APIGatewayJWTRequirement struct {
// Providers is a list of providers to consider when verifying a JWT.
Providers []*APIGatewayJWTProvider `json:",omitempty"`
}
// APIGatewayJWTProvider holds the provider and claim verification information
type APIGatewayJWTProvider struct {
// Name is the name of the JWT provider. There MUST be a corresponding
// "jwt-provider" config entry with this name.
Name string `json:",omitempty"`
// VerifyClaims is a list of additional claims to verify in a JWT's payload.
VerifyClaims []*APIGatewayJWTClaimVerification `json:",omitempty" alias:"verify_claims"`
}
// APIGatewayJWTClaimVerification holds the actual claim information to be verified
type APIGatewayJWTClaimVerification struct {
// Path is the path to the claim in the token JSON.
Path []string `json:",omitempty"`
// Value is the expected value at the given path:
// - If the type at the path is a list then we verify
// that this value is contained in the list.
//
// - If the type at the path is a string then we verify
// that this value matches.
Value string `json:",omitempty"`
}

View File

@ -348,3 +348,150 @@ func TestAPI_ConfigEntries_TerminatingGateway(t *testing.T) {
_, _, err = configEntries.Get(TerminatingGateway, "foo", nil) _, _, err = configEntries.Get(TerminatingGateway, "foo", nil)
require.Error(t, err) require.Error(t, err)
} }
func TestAPI_ConfigEntries_APIGateway(t *testing.T) {
t.Parallel()
c, s := makeClient(t)
defer s.Stop()
configEntries := c.ConfigEntries()
listener1 := APIGatewayListener{
Name: "listener1",
Hostname: "host.com",
Port: 3360,
Protocol: "http",
}
listener2 := APIGatewayListener{
Name: "listener2",
Hostname: "host2.com",
Port: 3362,
Protocol: "http",
}
apigw1 := &APIGatewayConfigEntry{
Kind: APIGateway,
Name: "foo",
Meta: map[string]string{
"foo": "bar",
"gir": "zim",
},
Listeners: []APIGatewayListener{listener1},
}
apigw2 := &APIGatewayConfigEntry{
Kind: APIGateway,
Name: "bar",
Listeners: []APIGatewayListener{listener2},
}
// set it
_, wm, err := configEntries.Set(apigw1, nil)
require.NoError(t, err)
require.NotNil(t, wm)
require.NotEqual(t, 0, wm.RequestTime)
// also set the second one
_, wm, err = configEntries.Set(apigw2, nil)
require.NoError(t, err)
require.NotNil(t, wm)
require.NotEqual(t, 0, wm.RequestTime)
// get it
entry, qm, err := configEntries.Get(APIGateway, "foo", nil)
require.NoError(t, err)
require.NotNil(t, qm)
require.NotEqual(t, 0, qm.RequestTime)
// verify it
readGW, ok := entry.(*APIGatewayConfigEntry)
require.True(t, ok)
require.Equal(t, apigw1.Kind, readGW.Kind)
require.Equal(t, apigw1.Name, readGW.Name)
require.Equal(t, apigw1.Meta, readGW.Meta)
require.Equal(t, apigw1.Meta, readGW.GetMeta())
// update it
apigw1.Listeners = []APIGatewayListener{
listener1,
{
Name: "listener3",
Hostname: "host3.com",
Port: 3363,
Protocol: "http",
},
}
// CAS fail
written, _, err := configEntries.CAS(apigw1, 0, nil)
require.NoError(t, err)
require.False(t, written)
// CAS success
written, wm, err = configEntries.CAS(apigw1, readGW.ModifyIndex, nil)
require.NoError(t, err)
require.NotNil(t, wm)
require.NotEqual(t, 0, wm.RequestTime)
require.True(t, written)
// re-setting should not yield an error
_, wm, err = configEntries.Set(apigw1, nil)
require.NoError(t, err)
require.NotNil(t, wm)
require.NotEqual(t, 0, wm.RequestTime)
apigw2.Listeners = []APIGatewayListener{
listener2,
{
Name: "listener4",
Hostname: "host4.com",
Port: 3364,
Protocol: "http",
},
}
_, wm, err = configEntries.Set(apigw2, nil)
require.NoError(t, err)
require.NotNil(t, wm)
require.NotEqual(t, 0, wm.RequestTime)
// list them
entries, qm, err := configEntries.List(APIGateway, nil)
require.NoError(t, err)
require.NotNil(t, qm)
require.NotEqual(t, 0, qm.RequestTime)
require.Len(t, entries, 2)
for _, entry = range entries {
switch entry.GetName() {
case "foo":
// this also verifies that the update value was persisted and
// the updated values are seen
readGW, ok = entry.(*APIGatewayConfigEntry)
require.True(t, ok)
require.Equal(t, apigw1.Kind, readGW.Kind)
require.Equal(t, apigw1.Name, readGW.Name)
require.Len(t, readGW.Listeners, 2)
require.Equal(t, apigw1.Listeners, readGW.Listeners)
case "bar":
readGW, ok = entry.(*APIGatewayConfigEntry)
require.True(t, ok)
require.Equal(t, apigw2.Kind, readGW.Kind)
require.Equal(t, apigw2.Name, readGW.Name)
require.Len(t, readGW.Listeners, 2)
require.Equal(t, apigw2.Listeners, readGW.Listeners)
}
}
// delete it
wm, err = configEntries.Delete(APIGateway, "foo", nil)
require.NoError(t, err)
require.NotNil(t, wm)
require.NotEqual(t, 0, wm.RequestTime)
// verify deletion
_, _, err = configEntries.Get(APIGateway, "foo", nil)
require.Error(t, err)
}

View File

@ -8,12 +8,14 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"strings"
"time" "time"
"github.com/mitchellh/mapstructure"
"github.com/hashicorp/consul/api" "github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/lib/decode" "github.com/hashicorp/consul/lib/decode"
"github.com/hashicorp/go-multierror" "github.com/hashicorp/go-multierror"
"github.com/mitchellh/mapstructure"
) )
func loadFromFile(path string) (string, error) { func loadFromFile(path string) (string, error) {
@ -124,13 +126,19 @@ func newDecodeConfigEntry(raw map[string]interface{}) (api.ConfigEntry, error) {
} }
for _, k := range md.Unused { for _, k := range md.Unused {
switch k { switch {
case "kind", "Kind": case strings.ToLower(k) == "kind":
// The kind field is used to determine the target, but doesn't need // The kind field is used to determine the target, but doesn't need
// to exist on the target. // to exist on the target.
continue continue
case strings.HasSuffix(strings.ToLower(k), "namespace"):
err = multierror.Append(err, fmt.Errorf("invalid config key %q, namespaces are a consul enterprise feature", k))
case strings.Contains(strings.ToLower(k), "jwt"):
err = multierror.Append(err, fmt.Errorf("invalid config key %q, api-gateway jwt validation is a consul enterprise feature", k))
default:
err = multierror.Append(err, fmt.Errorf("invalid config key %q", k))
} }
err = multierror.Append(err, fmt.Errorf("invalid config key %q", k))
} }
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -53,6 +53,16 @@ func APIGatewayListenerToStructs(s *APIGatewayListener, t *structs.APIGatewayLis
if s.TLS != nil { if s.TLS != nil {
APIGatewayTLSConfigurationToStructs(s.TLS, &t.TLS) APIGatewayTLSConfigurationToStructs(s.TLS, &t.TLS)
} }
if s.Override != nil {
var x structs.APIGatewayPolicy
APIGatewayPolicyToStructs(s.Override, &x)
t.Override = &x
}
if s.Default != nil {
var x structs.APIGatewayPolicy
APIGatewayPolicyToStructs(s.Default, &x)
t.Default = &x
}
} }
func APIGatewayListenerFromStructs(t *structs.APIGatewayListener, s *APIGatewayListener) { func APIGatewayListenerFromStructs(t *structs.APIGatewayListener, s *APIGatewayListener) {
if s == nil { if s == nil {
@ -67,6 +77,28 @@ func APIGatewayListenerFromStructs(t *structs.APIGatewayListener, s *APIGatewayL
APIGatewayTLSConfigurationFromStructs(&t.TLS, &x) APIGatewayTLSConfigurationFromStructs(&t.TLS, &x)
s.TLS = &x s.TLS = &x
} }
if t.Override != nil {
var x APIGatewayPolicy
APIGatewayPolicyFromStructs(t.Override, &x)
s.Override = &x
}
if t.Default != nil {
var x APIGatewayPolicy
APIGatewayPolicyFromStructs(t.Default, &x)
s.Default = &x
}
}
func APIGatewayPolicyToStructs(s *APIGatewayPolicy, t *structs.APIGatewayPolicy) {
if s == nil {
return
}
t.JWT = gwJWTRequirementToStructs(s.JWT)
}
func APIGatewayPolicyFromStructs(t *structs.APIGatewayPolicy, s *APIGatewayPolicy) {
if s == nil {
return
}
s.JWT = gwJWTRequirementFromStructs(t.JWT)
} }
func APIGatewayTLSConfigurationToStructs(s *APIGatewayTLSConfiguration, t *structs.APIGatewayTLSConfiguration) { func APIGatewayTLSConfigurationToStructs(s *APIGatewayTLSConfiguration, t *structs.APIGatewayTLSConfiguration) {
if s == nil { if s == nil {

View File

@ -507,6 +507,46 @@ func (msg *APIGatewayTLSConfiguration) UnmarshalBinary(b []byte) error {
return proto.Unmarshal(b, msg) return proto.Unmarshal(b, msg)
} }
// MarshalBinary implements encoding.BinaryMarshaler
func (msg *APIGatewayPolicy) MarshalBinary() ([]byte, error) {
return proto.Marshal(msg)
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler
func (msg *APIGatewayPolicy) UnmarshalBinary(b []byte) error {
return proto.Unmarshal(b, msg)
}
// MarshalBinary implements encoding.BinaryMarshaler
func (msg *APIGatewayJWTRequirement) MarshalBinary() ([]byte, error) {
return proto.Marshal(msg)
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler
func (msg *APIGatewayJWTRequirement) UnmarshalBinary(b []byte) error {
return proto.Unmarshal(b, msg)
}
// MarshalBinary implements encoding.BinaryMarshaler
func (msg *APIGatewayJWTProvider) MarshalBinary() ([]byte, error) {
return proto.Marshal(msg)
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler
func (msg *APIGatewayJWTProvider) UnmarshalBinary(b []byte) error {
return proto.Unmarshal(b, msg)
}
// MarshalBinary implements encoding.BinaryMarshaler
func (msg *APIGatewayJWTClaimVerification) MarshalBinary() ([]byte, error) {
return proto.Marshal(msg)
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler
func (msg *APIGatewayJWTClaimVerification) UnmarshalBinary(b []byte) error {
return proto.Unmarshal(b, msg)
}
// MarshalBinary implements encoding.BinaryMarshaler // MarshalBinary implements encoding.BinaryMarshaler
func (msg *ResourceReference) MarshalBinary() ([]byte, error) { func (msg *ResourceReference) MarshalBinary() ([]byte, error) {
return proto.Marshal(msg) return proto.Marshal(msg)

File diff suppressed because it is too large Load Diff

View File

@ -706,6 +706,8 @@ message APIGatewayListener {
// mog: func-to=apiGatewayProtocolToStructs func-from=apiGatewayProtocolFromStructs // mog: func-to=apiGatewayProtocolToStructs func-from=apiGatewayProtocolFromStructs
APIGatewayListenerProtocol Protocol = 4; APIGatewayListenerProtocol Protocol = 4;
APIGatewayTLSConfiguration TLS = 5; APIGatewayTLSConfiguration TLS = 5;
APIGatewayPolicy Override = 6;
APIGatewayPolicy Default = 7;
} }
// mog annotation: // mog annotation:
@ -723,6 +725,30 @@ message APIGatewayTLSConfiguration {
repeated string CipherSuites = 4; repeated string CipherSuites = 4;
} }
// mog annotation:
//
// target=github.com/hashicorp/consul/agent/structs.APIGatewayPolicy
// output=config_entry.gen.go
// name=Structs
message APIGatewayPolicy {
// mog: func-to=gwJWTRequirementToStructs func-from=gwJWTRequirementFromStructs
APIGatewayJWTRequirement JWT = 1;
}
message APIGatewayJWTRequirement {
repeated APIGatewayJWTProvider Providers = 1;
}
message APIGatewayJWTProvider {
string Name = 1;
repeated APIGatewayJWTClaimVerification VerifyClaims = 2;
}
message APIGatewayJWTClaimVerification {
repeated string Path = 1;
string Value = 2;
}
// mog annotation: // mog annotation:
// //
// target=github.com/hashicorp/consul/agent/structs.ResourceReference // target=github.com/hashicorp/consul/agent/structs.ResourceReference

View File

@ -0,0 +1,17 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
//go:build !consulent
// +build !consulent
package pbconfigentry
import "github.com/hashicorp/consul/agent/structs"
func gwJWTRequirementToStructs(m *APIGatewayJWTRequirement) *structs.APIGatewayJWTRequirement {
return &structs.APIGatewayJWTRequirement{}
}
func gwJWTRequirementFromStructs(*structs.APIGatewayJWTRequirement) *APIGatewayJWTRequirement {
return &APIGatewayJWTRequirement{}
}