Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 12 commits into from
Aug 10, 2023
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
6 changes: 6 additions & 0 deletions agent/structs/structs.deepcopy.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ 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.JWT != nil {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so for this I'm hoping that my PR for globusdigital/deep-copy#33 gets merged which will let us generate DeepCopy methods with build tags for both ent and oss without conflict

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if this doesn't I'll hand write the deep copy methods for now

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

manually wrote the deep copy methods for the time being, if that PR gets merged I'll update this to use the generated methods with build tags

cp.Override.JWT = o.Override.JWT.DeepCopy()
}
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"`
jm96441n marked this conversation as resolved.
Show resolved Hide resolved
}

// 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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good add, m8!

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":
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed this up so that the error messaging would be the same whether doing a consul config write as we get from the api

// 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
28 changes: 28 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.

40 changes: 40 additions & 0 deletions proto/private/pbconfigentry/config_entry.pb.binary.go

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

Loading