mirror of https://github.com/status-im/consul.git
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:
parent
05604eeec1
commit
6c8ca0f89d
|
@ -0,0 +1,3 @@
|
|||
```release-note:feature
|
||||
config-entry(api-gateway): (Enterprise only) Add GatewayPolicy to APIGateway Config Entry listeners
|
||||
```
|
|
@ -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{}
|
|
@ -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 {
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
package structs
|
||||
|
||||
// DeepCopy generates a deep copy of *APIGatewayJWTRequirement
|
||||
func (o *APIGatewayJWTRequirement) DeepCopy() *APIGatewayJWTRequirement {
|
||||
return new(APIGatewayJWTRequirement)
|
||||
}
|
|
@ -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
|
||||
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
@ -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
|
||||
|
|
|
@ -53,6 +53,16 @@ func APIGatewayListenerToStructs(s *APIGatewayListener, t *structs.APIGatewayLis
|
|||
if s.TLS != nil {
|
||||
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) {
|
||||
if s == nil {
|
||||
|
@ -67,6 +77,28 @@ func APIGatewayListenerFromStructs(t *structs.APIGatewayListener, s *APIGatewayL
|
|||
APIGatewayTLSConfigurationFromStructs(&t.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) {
|
||||
if s == nil {
|
||||
|
|
|
@ -507,6 +507,46 @@ func (msg *APIGatewayTLSConfiguration) UnmarshalBinary(b []byte) error {
|
|||
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
|
||||
func (msg *ResourceReference) MarshalBinary() ([]byte, error) {
|
||||
return proto.Marshal(msg)
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -706,6 +706,8 @@ message APIGatewayListener {
|
|||
// mog: func-to=apiGatewayProtocolToStructs func-from=apiGatewayProtocolFromStructs
|
||||
APIGatewayListenerProtocol Protocol = 4;
|
||||
APIGatewayTLSConfiguration TLS = 5;
|
||||
APIGatewayPolicy Override = 6;
|
||||
APIGatewayPolicy Default = 7;
|
||||
}
|
||||
|
||||
// mog annotation:
|
||||
|
@ -723,6 +725,30 @@ message APIGatewayTLSConfiguration {
|
|||
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:
|
||||
//
|
||||
// target=github.com/hashicorp/consul/agent/structs.ResourceReference
|
||||
|
|
|
@ -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{}
|
||||
}
|
Loading…
Reference in New Issue