From d77048f1ea7136af9f627182bc79126f9472a060 Mon Sep 17 00:00:00 2001 From: Ronald Ekambi Date: Thu, 15 Jun 2023 09:53:33 -0400 Subject: [PATCH] Stop referenced jwt providers from being deleted --- .changelog/17755.txt | 3 + agent/consul/state/config_entry.go | 63 ++++++++++++ agent/consul/state/config_entry_test.go | 129 ++++++++++++++++++++++++ 3 files changed, 195 insertions(+) create mode 100644 .changelog/17755.txt diff --git a/.changelog/17755.txt b/.changelog/17755.txt new file mode 100644 index 0000000000..7edf7b26e1 --- /dev/null +++ b/.changelog/17755.txt @@ -0,0 +1,3 @@ +```release-note:improvement +mesh: Stop jwt providers referenced by intentions from being deleted. +``` \ No newline at end of file diff --git a/agent/consul/state/config_entry.go b/agent/consul/state/config_entry.go index 340a53f119..d73a75dcdb 100644 --- a/agent/consul/state/config_entry.go +++ b/agent/consul/state/config_entry.go @@ -634,6 +634,12 @@ func validateProposedConfigEntryInGraph( case structs.TCPRoute: case structs.RateLimitIPConfig: case structs.JWTProvider: + if newEntry == nil && existingEntry != nil { + err := validateJWTProviderIsReferenced(tx, kindName, existingEntry) + if err != nil { + return err + } + } default: return fmt.Errorf("unhandled kind %q during validation of %q", kindName.Kind, kindName.Name) } @@ -704,6 +710,63 @@ func getReferencedProviderNames(j *structs.IntentionJWTRequirement, s []*structs return providerNames } +func validateJWTProviderIsReferenced(tx ReadTxn, kn configentry.KindName, ce structs.ConfigEntry) error { + meta := acl.NewEnterpriseMetaWithPartition( + kn.EnterpriseMeta.PartitionOrDefault(), + acl.DefaultNamespaceName, + ) + entry, ok := ce.(*structs.JWTProviderConfigEntry) + if !ok { + return fmt.Errorf("invalid jwt provider config entry: %T", entry) + } + + _, ixnEntries, err := configEntriesByKindTxn(tx, nil, structs.ServiceIntentions, &meta) + if err != nil { + return err + } + + providerNames, err := collectJWTProviderNames(ixnEntries) + if err != nil { + return err + } + + _, exist := providerNames[entry.Name] + if exist { + return fmt.Errorf("cannot delete jwt provider config entry referenced by an intention. Provider name:%s", entry.Name) + } + + return nil +} + +func collectJWTProviderNames(e []structs.ConfigEntry) (map[string]struct{}, error) { + names := make(map[string]struct{}) + + for _, entry := range e { + ixn, ok := entry.(*structs.ServiceIntentionsConfigEntry) + if !ok { + return names, fmt.Errorf("type %T is not a service intentions config entry", entry) + } + + if ixn.JWT != nil { + for _, prov := range ixn.JWT.Providers { + names[prov.Name] = struct{}{} + } + } + + for _, s := range ixn.Sources { + for _, perm := range s.Permissions { + if perm.JWT == nil { + continue + } + for _, prov := range perm.JWT.Providers { + names[prov.Name] = struct{}{} + } + } + } + } + return names, nil +} + // This fetches all the jwt-providers config entries and iterates over them // to validate that any provider referenced exists. // This is okay because we assume there are very few jwt-providers per partition diff --git a/agent/consul/state/config_entry_test.go b/agent/consul/state/config_entry_test.go index 572719dc4b..bc14399263 100644 --- a/agent/consul/state/config_entry_test.go +++ b/agent/consul/state/config_entry_test.go @@ -3714,3 +3714,132 @@ func TestStateStore_DiscoveryChain_AttachVirtualIPs(t *testing.T) { require.Equal(t, []string{"2.2.2.2", "3.3.3.3"}, chain.ManualVirtualIPs) } + +func TestCollectJWTProviderNames(t *testing.T) { + oktaProvider := structs.IntentionJWTProvider{Name: "okta"} + auth0Provider := structs.IntentionJWTProvider{Name: "auth0"} + cases := map[string]struct { + entries []structs.ConfigEntry + expected map[string]struct{} + }{ + "no jwt at any level": { + entries: []structs.ConfigEntry{}, + expected: map[string]struct{}{}, + }, + "only top level jwt with no permissions": { + entries: []structs.ConfigEntry{ + &structs.ServiceIntentionsConfigEntry{ + Kind: "service-intentions", + Name: "api-intention", + JWT: &structs.IntentionJWTRequirement{ + Providers: []*structs.IntentionJWTProvider{&oktaProvider, &auth0Provider}, + }, + }, + }, + expected: map[string]struct{}{ + "okta": {}, "auth0": {}, + }, + }, + "top level jwt with permissions": { + entries: []structs.ConfigEntry{ + &structs.ServiceIntentionsConfigEntry{ + Kind: "service-intentions", + Name: "api-intention", + JWT: &structs.IntentionJWTRequirement{ + Providers: []*structs.IntentionJWTProvider{&oktaProvider}, + }, + Sources: []*structs.SourceIntention{ + { + Name: "api", + Action: "allow", + Permissions: []*structs.IntentionPermission{ + { + Action: "allow", + JWT: &structs.IntentionJWTRequirement{ + Providers: []*structs.IntentionJWTProvider{&oktaProvider}, + }, + }, + }, + }, + { + Name: "serv", + Action: "allow", + Permissions: []*structs.IntentionPermission{ + { + Action: "allow", + JWT: &structs.IntentionJWTRequirement{ + Providers: []*structs.IntentionJWTProvider{&auth0Provider}, + }, + }, + }, + }, + { + Name: "web", + Action: "allow", + Permissions: []*structs.IntentionPermission{ + {Action: "allow"}, + }, + }, + }, + }, + }, + expected: map[string]struct{}{ + "okta": {}, "auth0": {}, + }, + }, + "no top level jwt and existing permissions": { + entries: []structs.ConfigEntry{ + &structs.ServiceIntentionsConfigEntry{ + Kind: "service-intentions", + Name: "api-intention", + Sources: []*structs.SourceIntention{ + { + Name: "api", + Action: "allow", + Permissions: []*structs.IntentionPermission{ + { + Action: "allow", + JWT: &structs.IntentionJWTRequirement{ + Providers: []*structs.IntentionJWTProvider{&oktaProvider}, + }, + }, + }, + }, + { + Name: "serv", + Action: "allow", + Permissions: []*structs.IntentionPermission{ + { + Action: "allow", + JWT: &structs.IntentionJWTRequirement{ + Providers: []*structs.IntentionJWTProvider{&auth0Provider}, + }, + }, + }, + }, + { + Name: "web", + Action: "allow", + Permissions: []*structs.IntentionPermission{ + {Action: "allow"}, + }, + }, + }, + }, + }, + expected: map[string]struct{}{ + "okta": {}, "auth0": {}, + }, + }, + } + + for name, tt := range cases { + tt := tt + t.Run(name, func(t *testing.T) { + names, err := collectJWTProviderNames(tt.entries) + + require.NoError(t, err) + require.Equal(t, tt.expected, names) + }) + } +}