Skip to content

Commit

Permalink
NET-4984: Update APIGW Config Entries for JWT Auth (#18366)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
jm96441n authored Aug 10, 2023
1 parent 05604ee commit 6c8ca0f
Show file tree
Hide file tree
Showing 14 changed files with 1,539 additions and 861 deletions.
3 changes: 3 additions & 0 deletions .changelog/_18366.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:feature
config-entry(api-gateway): (Enterprise only) Add GatewayPolicy to APIGateway Config Entry listeners
```
10 changes: 10 additions & 0 deletions agent/structs/config_entry_apigw_jwt_oss.go
Original file line number Diff line number Diff line change
@@ -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{}
11 changes: 11 additions & 0 deletions agent/structs/config_entry_gateways.go
Original file line number Diff line number Diff line change
Expand Up @@ -887,6 +887,17 @@ type APIGatewayListener struct {
Protocol APIGatewayListenerProtocol
// TLS is the TLS settings for the listener.
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 {
Expand Down
2 changes: 2 additions & 0 deletions agent/structs/config_entry_oss.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ func validateUnusedKeys(unused []string) error {
// to exist on the target.
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))
}
Expand Down
14 changes: 14 additions & 0 deletions agent/structs/structs.deepcopy.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,20 @@ func (o *APIGatewayListener) DeepCopy() *APIGatewayListener {
cp.TLS.CipherSuites = make([]types.TLSCipherSuite, len(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
}

Expand Down
6 changes: 6 additions & 0 deletions agent/structs/structs.deepcopy_oss.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package structs

// DeepCopy generates a deep copy of *APIGatewayJWTRequirement
func (o *APIGatewayJWTRequirement) DeepCopy() *APIGatewayJWTRequirement {
return new(APIGatewayJWTRequirement)
}
40 changes: 40 additions & 0 deletions api/config_entry_gateways.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,10 @@ type APIGatewayListener struct {
Protocol string
// TLS is the TLS settings for the listener.
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 listener’s
Expand All @@ -302,3 +306,39 @@ type APIGatewayTLSConfiguration struct {
// Only applicable to connections negotiated via TLS 1.2 or earlier
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"`
}
147 changes: 147 additions & 0 deletions api/config_entry_gateways_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -348,3 +348,150 @@ func TestAPI_ConfigEntries_TerminatingGateway(t *testing.T) {
_, _, err = configEntries.Get(TerminatingGateway, "foo", nil)
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)
}
16 changes: 12 additions & 4 deletions command/helpers/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ import (
"fmt"
"io"
"os"
"strings"
"time"

"github.com/mitchellh/mapstructure"

"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/lib/decode"
"github.com/hashicorp/go-multierror"
"github.com/mitchellh/mapstructure"
)

func loadFromFile(path string) (string, error) {
Expand Down Expand Up @@ -124,13 +126,19 @@ func newDecodeConfigEntry(raw map[string]interface{}) (api.ConfigEntry, error) {
}

for _, k := range md.Unused {
switch k {
case "kind", "Kind":
switch {
case strings.ToLower(k) == "kind":
// The kind field is used to determine the target, but doesn't need
// to exist on the target.
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 {
return nil, err
Expand Down
32 changes: 32 additions & 0 deletions proto/private/pbconfigentry/config_entry.gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 6c8ca0f

Please sign in to comment.