From d4e20cc7b0932c9fbdb3478c444a1e249773e968 Mon Sep 17 00:00:00 2001 From: Sebastian Spaink Date: Wed, 4 Feb 2026 11:11:59 -0600 Subject: [PATCH] Add config option to disable named inter-query built-in cache (#8287) Signed-off-by: Sebastian Spaink --- docs/docs/configuration.md | 2 + v1/topdown/cache/cache.go | 25 ++++- v1/topdown/cache/cache_test.go | 181 ++++++++++++++++++++++++++++++--- v1/topdown/tokens.go | 6 +- v1/util/test/populate.go | 5 + 5 files changed, 200 insertions(+), 19 deletions(-) diff --git a/docs/docs/configuration.md b/docs/docs/configuration.md index 86946f20f6..d70ddc5d6c 100644 --- a/docs/docs/configuration.md +++ b/docs/docs/configuration.md @@ -924,7 +924,9 @@ that requires GraphQL schemas. | `caching.inter_query_builtin_cache.stale_entry_eviction_period_seconds` | `int64` | No | Stale entry eviction period in seconds. OPA will drop expired items from the cache every `stale_entry_eviction_period_seconds`. By default, set to `0` indicating stale entry eviction is disabled. | | `caching.inter_query_builtin_value_cache.max_num_entries` | `int` | No | Maximum number of entries in the Inter-query value cache. OPA will drop random items from the cache if this limit is exceeded. By default, set to `0` indicating unlimited size. | | `caching.inter_query_builtin_value_cache.named.io_jwt.max_num_entries` | `int` | No | Maximum number of entries in the `io_jwt` cache, used by the [`io.jwt` token verification](./policy-reference/builtins/tokens) built-in functions. OPA will drop random items from the cache if this limit is exceeded. By default, this cache is disabled. | +| `caching.inter_query_builtin_value_cache.named.io_jwt.disabled` | `bool` | No | Explicitly disable `io_jwt`, by default this is `true`. Setting this to `false` will enable `io_jwt` and set `max_num_entries` to `0` unless configured otherwise. | | `caching.inter_query_builtin_value_cache.named.graphql.max_num_entries` | `int` | No | Maximum number of entries in the `graphql` cache, used by the [`graphql` builtins](./policy-reference/builtins/graphql) built-in functions to cache parsed schemas. OPA will drop random items from the cache if this limit is exceeded. By default, this cache is set to a maximum of 10 entries. | +| `caching.inter_query_builtin_value_cache.named.graphql.disabled` | `bool` | No | Explicitly disable `graphql`, by default this is `false`. Setting this to `true` will disable `graphql`. | ## Distributed tracing diff --git a/v1/topdown/cache/cache.go b/v1/topdown/cache/cache.go index d514bed787..1c2eacc99e 100644 --- a/v1/topdown/cache/cache.go +++ b/v1/topdown/cache/cache.go @@ -32,7 +32,7 @@ func getDefaultInterQueryBuiltinValueCacheConfig(name string) *NamedValueCacheCo // RegisterDefaultInterQueryBuiltinValueCacheConfig registers a default configuration for the inter-query value cache; // used when none has been explicitly configured. -// To disable a named cache when not configured, pass a nil config. +// To disable a named cache when not configured, pass a config with the disabled value set to true. func RegisterDefaultInterQueryBuiltinValueCacheConfig(name string, config *NamedValueCacheConfig) { interQueryBuiltinValueCacheDefaultConfigs[name] = config } @@ -58,7 +58,8 @@ func (c *Config) Clone() *Config { // NamedValueCacheConfig represents the configuration of a named cache that built-in functions can utilize. // A default configuration to be used if not explicitly configured can be registered using RegisterDefaultInterQueryBuiltinValueCacheConfig. type NamedValueCacheConfig struct { - MaxNumEntries *int `json:"max_num_entries,omitempty"` + MaxNumEntries *int `json:"max_num_entries,omitempty"` + Disabled *bool `json:"disabled,omitempty"` } // Clone creates a deep copy of NamedValueCacheConfig. @@ -73,6 +74,10 @@ func (n *NamedValueCacheConfig) Clone() *NamedValueCacheConfig { maxEntries := *n.MaxNumEntries clone.MaxNumEntries = &maxEntries } + if n.Disabled != nil { + disabled := *n.Disabled + clone.Disabled = &disabled + } return clone } @@ -220,9 +225,15 @@ func (c *Config) validateAndInjectDefaults() error { } for name, namedConfig := range c.InterQueryBuiltinValueCache.NamedCacheConfigs { - numEntries := *namedConfig.MaxNumEntries - if numEntries < 0 { - return fmt.Errorf("invalid max_num_entries %v for named cache %v", numEntries, name) + if namedConfig == nil || (namedConfig.MaxNumEntries == nil && namedConfig.Disabled == nil) { + return fmt.Errorf("missing configuration for named cache %v", name) + } + + if namedConfig.MaxNumEntries != nil { + numEntries := *namedConfig.MaxNumEntries + if numEntries < 0 { + return fmt.Errorf("invalid max_num_entries %v for named cache %v", numEntries, name) + } } } @@ -605,6 +616,10 @@ func (c *interQueryBuiltinValueCache) GetCache(name string) InterQueryValueCache return nil } + if config.Disabled != nil && *config.Disabled { + return nil + } + nc = &interQueryValueCacheBucket{ items: *newItemsMap(), config: config, diff --git a/v1/topdown/cache/cache_test.go b/v1/topdown/cache/cache_test.go index c045a4550f..8f595d6119 100644 --- a/v1/topdown/cache/cache_test.go +++ b/v1/topdown/cache/cache_test.go @@ -27,7 +27,7 @@ func TestParseCachingConfig(t *testing.T) { maxNumEntriesInterQueryValueCache := new(int) *maxNumEntriesInterQueryValueCache = defaultInterQueryBuiltinValueCacheSize - expected := &Config{ + defaultExpected := &Config{ InterQueryBuiltinCache: InterQueryBuiltinCacheConfig{ MaxSizeBytes: maxSize, StaleEntryEvictionPeriodSeconds: period, @@ -39,20 +39,64 @@ func TestParseCachingConfig(t *testing.T) { } tests := map[string]struct { - input []byte - wantErr bool + input []byte + wantErr bool + expectedConfig *Config }{ "empty_config": { - input: nil, + input: nil, + wantErr: false, + expectedConfig: defaultExpected, + }, + "invalid_config_missing_configuration": { + input: []byte(`inter_query_builtin_value_cache: + named: + io_jwt:`), + wantErr: true, + }, + "invalid_config_missing_values": { + input: []byte(`inter_query_builtin_value_cache: + named: + io_jwt: + disabled: + max_num_entries: `), + wantErr: true, + }, + "valid_config_using_disabled": { + input: []byte(`inter_query_builtin_value_cache: + named: + io_jwt: + disabled: true`), wantErr: false, + expectedConfig: func() *Config { + disabled := true + n := NamedValueCacheConfig{ + Disabled: &disabled, + } + return &Config{ + InterQueryBuiltinCache: InterQueryBuiltinCacheConfig{ + MaxSizeBytes: maxSize, + StaleEntryEvictionPeriodSeconds: period, + ForcedEvictionThresholdPercentage: threshold, + }, + InterQueryBuiltinValueCache: InterQueryBuiltinValueCacheConfig{ + MaxNumEntries: maxNumEntriesInterQueryValueCache, + NamedCacheConfigs: map[string]*NamedValueCacheConfig{ + "io_jwt": &n, + }, + }, + } + }(), }, "default_limit": { - input: []byte(`{"inter_query_builtin_cache": {},}`), - wantErr: false, + input: []byte(`{"inter_query_builtin_cache": {},}`), + wantErr: false, + expectedConfig: defaultExpected, }, "default_num_entries": { - input: []byte(`{"inter_query_builtin_value_cache": {},}`), - wantErr: false, + input: []byte(`{"inter_query_builtin_value_cache": {},}`), + wantErr: false, + expectedConfig: defaultExpected, }, "bad_limit": { input: []byte(`{"inter_query_builtin_cache": {"max_size_bytes": "100"},}`), @@ -77,8 +121,8 @@ func TestParseCachingConfig(t *testing.T) { } } - if !tc.wantErr && !reflect.DeepEqual(config, expected) { - t.Fatalf("want %v got %v", expected, config) + if !tc.wantErr && !reflect.DeepEqual(config, tc.expectedConfig) { + t.Fatalf("want %v got %v", tc.expectedConfig, config) } }) } @@ -92,13 +136,63 @@ func TestParseCachingConfig(t *testing.T) { } limit := int64(100) - expected.InterQueryBuiltinCache.MaxSizeBytes = &limit + defaultExpected.InterQueryBuiltinCache.MaxSizeBytes = &limit - if !reflect.DeepEqual(config, expected) { - t.Fatalf("want %v got %v", expected, config) + if !reflect.DeepEqual(config, defaultExpected) { + t.Fatalf("want %v got %v", defaultExpected, config) } } +func TestInterValueCache_DisabledConfig(t *testing.T) { + t.Run("config disabled", func(t *testing.T) { + // user configured `baz` to be disabled + disabled := true + cacheConfig := Config{ + InterQueryBuiltinValueCache: InterQueryBuiltinValueCacheConfig{ + NamedCacheConfigs: map[string]*NamedValueCacheConfig{ + "baz": { + Disabled: &disabled, + }, + }, + }, + } + + // by default, it is enabled + var defaultDisabled bool + RegisterDefaultInterQueryBuiltinValueCacheConfig("baz", &NamedValueCacheConfig{ + Disabled: &defaultDisabled, + }) + + c := NewInterQueryValueCache(t.Context(), &cacheConfig) + if c.GetCache("baz") != nil { + t.Fatal("Expected cache to be disabled") + } + }) + + t.Run("config enabled", func(t *testing.T) { + var disabled bool + cacheConfig := Config{ + InterQueryBuiltinValueCache: InterQueryBuiltinValueCacheConfig{ + NamedCacheConfigs: map[string]*NamedValueCacheConfig{ + "baz": { + Disabled: &disabled, + }, + }, + }, + } + + defaultDisabled := true + RegisterDefaultInterQueryBuiltinValueCacheConfig("baz", &NamedValueCacheConfig{ + Disabled: &defaultDisabled, + }) + + c := NewInterQueryValueCache(t.Context(), &cacheConfig) + if c.GetCache("baz") == nil { + t.Fatal("Expected cache to be enabled") + } + }) +} + func TestInterValueCache_DefaultConfiguration(t *testing.T) { t.Run("default config not set", func(t *testing.T) { config := Config{ @@ -168,6 +262,67 @@ func TestInterValueCache_DefaultConfiguration(t *testing.T) { func TestInterValueCache_NamedCaches(t *testing.T) { t.Parallel() + t.Run("configured disable is respected", func(t *testing.T) { + disabled := true + config := Config{ + InterQueryBuiltinValueCache: InterQueryBuiltinValueCacheConfig{ + NamedCacheConfigs: map[string]*NamedValueCacheConfig{ + "foo": { + Disabled: &disabled, + }, + }, + }, + } + + c := NewInterQueryValueCache(t.Context(), &config) + + nc := c.GetCache("foo") + if nc != nil { + t.Fatalf("Expected cache to be disabled, but got %v", nc) + } + }) + + t.Run("configured enable is respected", func(t *testing.T) { + var disabled bool + config := Config{ + InterQueryBuiltinValueCache: InterQueryBuiltinValueCacheConfig{ + NamedCacheConfigs: map[string]*NamedValueCacheConfig{ + "foo": { + Disabled: &disabled, + }, + }, + }, + } + + c := NewInterQueryValueCache(t.Context(), &config) + + nc := c.GetCache("foo") + if nc == nil { + t.Fatalf("Expected cache to be enabled, but got %v", nc) + } + }) + + t.Run("Disable respected even if max_num_entries is specified", func(t *testing.T) { + disabled := true + config := Config{ + InterQueryBuiltinValueCache: InterQueryBuiltinValueCacheConfig{ + NamedCacheConfigs: map[string]*NamedValueCacheConfig{ + "foo": { + MaxNumEntries: &[]int{5}[0], + Disabled: &disabled, + }, + }, + }, + } + + c := NewInterQueryValueCache(t.Context(), &config) + + nc := c.GetCache("foo") + if nc != nil { + t.Fatalf("Expected cache to be disabled, but got %v", nc) + } + }) + t.Run("configured max is respected", func(t *testing.T) { config := Config{ InterQueryBuiltinValueCache: InterQueryBuiltinValueCacheConfig{ diff --git a/v1/topdown/tokens.go b/v1/topdown/tokens.go index bcb794a0a9..bb2c9e1c1c 100644 --- a/v1/topdown/tokens.go +++ b/v1/topdown/tokens.go @@ -1290,7 +1290,11 @@ func createTokenCacheKey(serializedJwt ast.Value, publicKey ast.Value) ast.Value func init() { // By default, the JWT cache is disabled. - cache.RegisterDefaultInterQueryBuiltinValueCacheConfig(tokenCacheName, nil) + disabled := true + var tokenCache = cache.NamedValueCacheConfig{ + Disabled: &disabled, + } + cache.RegisterDefaultInterQueryBuiltinValueCacheConfig(tokenCacheName, &tokenCache) RegisterBuiltinFunc(ast.JWTDecode.Name, builtinJWTDecode) RegisterBuiltinFunc(ast.JWTVerifyRS256.Name, builtinJWTVerifyRS256) diff --git a/v1/util/test/populate.go b/v1/util/test/populate.go index f6176204f2..bd4f14cb64 100644 --- a/v1/util/test/populate.go +++ b/v1/util/test/populate.go @@ -79,6 +79,11 @@ func populateDefaultTypes(t *testing.T, fieldType reflect.Type, fieldValue refle populateStruct(t, fieldType.Elem(), newStruct.Elem()) fieldValue.Set(newStruct) + return true + case reflect.Bool: + newBool := true + fieldValue.Set(reflect.ValueOf(&newBool)) + return true }