From f982c51a11d429885e49ec484c622f40b6350926 Mon Sep 17 00:00:00 2001 From: endigma Date: Wed, 2 Jul 2025 20:51:11 +0100 Subject: [PATCH 1/4] feat: global disable switch for persisted operations --- router/core/graph_server.go | 1 + router/core/graphql_prehandler.go | 17 ++++++++++---- router/core/operation_blocker.go | 23 +++++++++++-------- router/pkg/config/config.go | 7 +++--- router/pkg/config/config.schema.json | 5 ++++ .../pkg/config/testdata/config_defaults.json | 1 + router/pkg/config/testdata/config_full.json | 1 + 7 files changed, 38 insertions(+), 17 deletions(-) diff --git a/router/core/graph_server.go b/router/core/graph_server.go index 0c5b7cd493..528f96a4ef 100644 --- a/router/core/graph_server.go +++ b/router/core/graph_server.go @@ -1206,6 +1206,7 @@ func (s *graphServer) buildGraphMux(ctx context.Context, Enabled: s.securityConfiguration.BlockNonPersistedOperations.Enabled, Condition: s.securityConfiguration.BlockNonPersistedOperations.Condition, }, + PersistedOperationsEnabled: s.persistedOperationsConfig.Enabled, SafelistEnabled: s.persistedOperationsConfig.Safelist.Enabled, LogUnknownOperationsEnabled: s.persistedOperationsConfig.LogUnknown, exprManager: exprManager, diff --git a/router/core/graphql_prehandler.go b/router/core/graphql_prehandler.go index 9558af55b3..db13f2ddaf 100644 --- a/router/core/graphql_prehandler.go +++ b/router/core/graphql_prehandler.go @@ -479,14 +479,14 @@ func (h *PreHandler) shouldComputeOperationSha256(operationKit *OperationKit) bo hasPersistedHash := operationKit.parsedOperation.GraphQLRequestExtensions.PersistedQuery != nil && operationKit.parsedOperation.GraphQLRequestExtensions.PersistedQuery.Sha256Hash != "" // If it already has a persisted hash attached to the request, then there is no need for us to compute it anew // Otherwise, we only want to compute the hash (an expensive operation) if we're safelisting or logging unknown persisted operations - return !hasPersistedHash && (h.operationBlocker.SafelistEnabled || h.operationBlocker.LogUnknownOperationsEnabled) + return !hasPersistedHash && (h.operationBlocker.safelistEnabled || h.operationBlocker.logUnknownOperationsEnabled) } // shouldFetchPersistedOperation determines if we should fetch a persisted operation. The most intuitive case is if the // operation is a persisted operation. However, we also want to fetch persisted operations if we're enabling safelisting // and if we're logging unknown operations. This is because we want to check if the operation is already persisted in the cache func (h *PreHandler) shouldFetchPersistedOperation(operationKit *OperationKit) bool { - return operationKit.parsedOperation.IsPersistedOperation || h.operationBlocker.SafelistEnabled || h.operationBlocker.LogUnknownOperationsEnabled + return operationKit.parsedOperation.IsPersistedOperation || h.operationBlocker.safelistEnabled || h.operationBlocker.logUnknownOperationsEnabled } func (h *PreHandler) handleOperation(req *http.Request, variablesParser *astjson.Parser, httpOperation *httpOperation) error { @@ -539,7 +539,7 @@ func (h *PreHandler) handleOperation(req *http.Request, variablesParser *astjson } requestContext.operation.sha256Hash = operationKit.parsedOperation.Sha256Hash requestContext.telemetry.addCustomMetricStringAttr(ContextFieldOperationSha256, requestContext.operation.sha256Hash) - if h.operationBlocker.SafelistEnabled || h.operationBlocker.LogUnknownOperationsEnabled { + if h.operationBlocker.safelistEnabled || h.operationBlocker.logUnknownOperationsEnabled { // Set the request hash to the parsed hash, to see if it matches a persisted operation operationKit.parsedOperation.GraphQLRequestExtensions.PersistedQuery = &GraphQLRequestExtensionsPersistedQuery{ Sha256Hash: operationKit.parsedOperation.Sha256Hash, @@ -562,6 +562,13 @@ func (h *PreHandler) handleOperation(req *http.Request, variablesParser *astjson ) if h.shouldFetchPersistedOperation(operationKit) { + if !h.operationBlocker.persistedOperationsEnabled { + return &httpGraphqlError{ + message: "persisted operations are disabled", + statusCode: http.StatusOK, + } + } + ctx, span := h.tracer.Start(req.Context(), "Load Persisted Operation", trace.WithSpanKind(trace.SpanKindClient), trace.WithAttributes(requestContext.telemetry.traceAttrs...), @@ -574,9 +581,9 @@ func (h *PreHandler) handleOperation(req *http.Request, variablesParser *astjson span.SetStatus(codes.Error, err.Error()) var poNotFoundErr *persistedoperation.PersistentOperationNotFoundError - if h.operationBlocker.LogUnknownOperationsEnabled && errors.As(err, &poNotFoundErr) { + if h.operationBlocker.logUnknownOperationsEnabled && errors.As(err, &poNotFoundErr) { requestContext.logger.Warn("Unknown persisted operation found", zap.String("query", operationKit.parsedOperation.Request.Query), zap.String("sha256Hash", poNotFoundErr.Sha256Hash)) - if h.operationBlocker.SafelistEnabled { + if h.operationBlocker.safelistEnabled { span.End() return err } diff --git a/router/core/operation_blocker.go b/router/core/operation_blocker.go index f1bb7adead..69dd66d016 100644 --- a/router/core/operation_blocker.go +++ b/router/core/operation_blocker.go @@ -3,10 +3,11 @@ package core import ( "errors" "fmt" + "reflect" + "github.com/expr-lang/expr/vm" "github.com/wundergraph/cosmo/router/internal/expr" "go.uber.org/zap" - "reflect" ) var ( @@ -16,15 +17,16 @@ var ( ) type OperationBlocker struct { - SafelistEnabled bool - LogUnknownOperationsEnabled bool - blockMutations BlockMutationOptions blockSubscriptions BlockSubscriptionOptions blockNonPersisted BlockNonPersistedOptions mutationExpr *vm.Program subscriptionExpr *vm.Program nonPersistedExpr *vm.Program + + persistedOperationsEnabled bool + safelistEnabled bool + logUnknownOperationsEnabled bool } type BlockMutationOptions struct { @@ -51,17 +53,20 @@ type OperationBlockerOptions struct { BlockSubscriptions BlockSubscriptionOptions BlockNonPersisted BlockNonPersistedOptions SafelistEnabled bool + PersistedOperationsEnabled bool LogUnknownOperationsEnabled bool exprManager *expr.Manager } func NewOperationBlocker(opts *OperationBlockerOptions) (*OperationBlocker, error) { ob := &OperationBlocker{ - blockMutations: opts.BlockMutations, - blockSubscriptions: opts.BlockSubscriptions, - blockNonPersisted: opts.BlockNonPersisted, - SafelistEnabled: opts.SafelistEnabled, - LogUnknownOperationsEnabled: opts.LogUnknownOperationsEnabled, + blockMutations: opts.BlockMutations, + blockSubscriptions: opts.BlockSubscriptions, + blockNonPersisted: opts.BlockNonPersisted, + + persistedOperationsEnabled: opts.PersistedOperationsEnabled, + safelistEnabled: opts.SafelistEnabled, + logUnknownOperationsEnabled: opts.LogUnknownOperationsEnabled, } if err := ob.compileExpressions(opts.exprManager); err != nil { diff --git a/router/pkg/config/config.go b/router/pkg/config/config.go index b83f0360ac..14c638f9b4 100644 --- a/router/pkg/config/config.go +++ b/router/pkg/config/config.go @@ -799,8 +799,9 @@ type AutomaticPersistedQueriesCacheConfig struct { } type PersistedOperationsConfig struct { - LogUnknown bool `yaml:"log_unknown" env:"PERSISTED_OPERATIONS_LOG_UNKNOWN" envDefault:"false"` - Safelist SafelistConfiguration `yaml:"safelist" envPrefix:"PERSISTED_OPERATIONS_SAFELIST_"` + Enabled bool `yaml:"enabled" env:"ENABLED" envDefault:"true"` + LogUnknown bool `yaml:"log_unknown" env:"LOG_UNKNOWN" envDefault:"false"` + Safelist SafelistConfiguration `yaml:"safelist" envPrefix:"SAFELIST_"` Cache PersistedOperationsCacheConfig `yaml:"cache"` Storage PersistedOperationsStorageConfig `yaml:"storage"` } @@ -995,7 +996,7 @@ type Config struct { StorageProviders StorageProviders `yaml:"storage_providers"` ExecutionConfig ExecutionConfig `yaml:"execution_config"` - PersistedOperationsConfig PersistedOperationsConfig `yaml:"persisted_operations"` + PersistedOperationsConfig PersistedOperationsConfig `yaml:"persisted_operations" envPrefix:"PERSISTED_OPERATIONS_"` AutomaticPersistedQueries AutomaticPersistedQueriesConfig `yaml:"automatic_persisted_queries"` ApolloCompatibilityFlags ApolloCompatibilityFlags `yaml:"apollo_compatibility_flags"` ApolloRouterCompatibilityFlags ApolloRouterCompatibilityFlags `yaml:"apollo_router_compatibility_flags"` diff --git a/router/pkg/config/config.schema.json b/router/pkg/config/config.schema.json index 6fbe8851ec..48220f3b56 100644 --- a/router/pkg/config/config.schema.json +++ b/router/pkg/config/config.schema.json @@ -143,6 +143,11 @@ "additionalProperties": false, "description": "The configuration for the persisted operations.", "properties": { + "enabled": { + "type": "boolean", + "description": "Enables or disables persisted operations. If disabled, all operations sent with a persisted operation in the body are blocked.", + "default": true + }, "safelist": { "type": "object", "description": "The configuration for safelisting persisted operations.", diff --git a/router/pkg/config/testdata/config_defaults.json b/router/pkg/config/testdata/config_defaults.json index 2501daddae..ad1c4a6e48 100644 --- a/router/pkg/config/testdata/config_defaults.json +++ b/router/pkg/config/testdata/config_defaults.json @@ -420,6 +420,7 @@ } }, "PersistedOperationsConfig": { + "Enabled": true, "LogUnknown": false, "Safelist": { "Enabled": false diff --git a/router/pkg/config/testdata/config_full.json b/router/pkg/config/testdata/config_full.json index 7869972aef..1e8542cf01 100644 --- a/router/pkg/config/testdata/config_full.json +++ b/router/pkg/config/testdata/config_full.json @@ -779,6 +779,7 @@ } }, "PersistedOperationsConfig": { + "Enabled": true, "LogUnknown": true, "Safelist": { "Enabled": true From c9c277015351293535c586d600ad215e8006789e Mon Sep 17 00:00:00 2001 From: endigma Date: Wed, 2 Jul 2025 21:12:12 +0100 Subject: [PATCH 2/4] Invert config so default is false --- router/core/graph_server.go | 2 +- router/core/graphql_prehandler.go | 2 +- router/core/operation_blocker.go | 6 +++--- router/pkg/config/config.go | 2 +- router/pkg/config/config.schema.json | 6 +++--- router/pkg/config/testdata/config_defaults.json | 2 +- router/pkg/config/testdata/config_full.json | 2 +- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/router/core/graph_server.go b/router/core/graph_server.go index 528f96a4ef..c622782691 100644 --- a/router/core/graph_server.go +++ b/router/core/graph_server.go @@ -1206,7 +1206,7 @@ func (s *graphServer) buildGraphMux(ctx context.Context, Enabled: s.securityConfiguration.BlockNonPersistedOperations.Enabled, Condition: s.securityConfiguration.BlockNonPersistedOperations.Condition, }, - PersistedOperationsEnabled: s.persistedOperationsConfig.Enabled, + PersistedOperationsDisabled: s.persistedOperationsConfig.Disabled, SafelistEnabled: s.persistedOperationsConfig.Safelist.Enabled, LogUnknownOperationsEnabled: s.persistedOperationsConfig.LogUnknown, exprManager: exprManager, diff --git a/router/core/graphql_prehandler.go b/router/core/graphql_prehandler.go index db13f2ddaf..1a150ba5f6 100644 --- a/router/core/graphql_prehandler.go +++ b/router/core/graphql_prehandler.go @@ -562,7 +562,7 @@ func (h *PreHandler) handleOperation(req *http.Request, variablesParser *astjson ) if h.shouldFetchPersistedOperation(operationKit) { - if !h.operationBlocker.persistedOperationsEnabled { + if h.operationBlocker.persistedOperationsDisabled { return &httpGraphqlError{ message: "persisted operations are disabled", statusCode: http.StatusOK, diff --git a/router/core/operation_blocker.go b/router/core/operation_blocker.go index 69dd66d016..1a5ac26fc3 100644 --- a/router/core/operation_blocker.go +++ b/router/core/operation_blocker.go @@ -24,7 +24,7 @@ type OperationBlocker struct { subscriptionExpr *vm.Program nonPersistedExpr *vm.Program - persistedOperationsEnabled bool + persistedOperationsDisabled bool safelistEnabled bool logUnknownOperationsEnabled bool } @@ -53,7 +53,7 @@ type OperationBlockerOptions struct { BlockSubscriptions BlockSubscriptionOptions BlockNonPersisted BlockNonPersistedOptions SafelistEnabled bool - PersistedOperationsEnabled bool + PersistedOperationsDisabled bool LogUnknownOperationsEnabled bool exprManager *expr.Manager } @@ -64,7 +64,7 @@ func NewOperationBlocker(opts *OperationBlockerOptions) (*OperationBlocker, erro blockSubscriptions: opts.BlockSubscriptions, blockNonPersisted: opts.BlockNonPersisted, - persistedOperationsEnabled: opts.PersistedOperationsEnabled, + persistedOperationsDisabled: opts.PersistedOperationsDisabled, safelistEnabled: opts.SafelistEnabled, logUnknownOperationsEnabled: opts.LogUnknownOperationsEnabled, } diff --git a/router/pkg/config/config.go b/router/pkg/config/config.go index 14c638f9b4..2efc4f9206 100644 --- a/router/pkg/config/config.go +++ b/router/pkg/config/config.go @@ -799,7 +799,7 @@ type AutomaticPersistedQueriesCacheConfig struct { } type PersistedOperationsConfig struct { - Enabled bool `yaml:"enabled" env:"ENABLED" envDefault:"true"` + Disabled bool `yaml:"disabled" env:"DISABLED" envDefault:"false"` LogUnknown bool `yaml:"log_unknown" env:"LOG_UNKNOWN" envDefault:"false"` Safelist SafelistConfiguration `yaml:"safelist" envPrefix:"SAFELIST_"` Cache PersistedOperationsCacheConfig `yaml:"cache"` diff --git a/router/pkg/config/config.schema.json b/router/pkg/config/config.schema.json index 48220f3b56..3110bd4881 100644 --- a/router/pkg/config/config.schema.json +++ b/router/pkg/config/config.schema.json @@ -143,10 +143,10 @@ "additionalProperties": false, "description": "The configuration for the persisted operations.", "properties": { - "enabled": { + "disabled": { "type": "boolean", - "description": "Enables or disables persisted operations. If disabled, all operations sent with a persisted operation in the body are blocked.", - "default": true + "description": "Disables persisted operations. If disabled, all operations sent with a persisted operation in the body are blocked.", + "default": false }, "safelist": { "type": "object", diff --git a/router/pkg/config/testdata/config_defaults.json b/router/pkg/config/testdata/config_defaults.json index ad1c4a6e48..9a4ccea958 100644 --- a/router/pkg/config/testdata/config_defaults.json +++ b/router/pkg/config/testdata/config_defaults.json @@ -420,7 +420,7 @@ } }, "PersistedOperationsConfig": { - "Enabled": true, + "Disabled": false, "LogUnknown": false, "Safelist": { "Enabled": false diff --git a/router/pkg/config/testdata/config_full.json b/router/pkg/config/testdata/config_full.json index 1e8542cf01..146e725e6a 100644 --- a/router/pkg/config/testdata/config_full.json +++ b/router/pkg/config/testdata/config_full.json @@ -779,7 +779,7 @@ } }, "PersistedOperationsConfig": { - "Enabled": true, + "Disabled": false, "LogUnknown": true, "Safelist": { "Enabled": true From 2749fd9a7e681bec60f77c30d36a94e6bef01645 Mon Sep 17 00:00:00 2001 From: endigma Date: Wed, 2 Jul 2025 21:12:23 +0100 Subject: [PATCH 3/4] style: fix newline at end of debug config --- router/debug.config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/router/debug.config.yaml b/router/debug.config.yaml index bed83ed3b5..59cd928d6a 100644 --- a/router/debug.config.yaml +++ b/router/debug.config.yaml @@ -39,4 +39,4 @@ events: redis: - id: my-redis urls: - - 'redis://localhost:6379/2' \ No newline at end of file + - 'redis://localhost:6379/2' From 4b8601d3cf104c6800e4f17eca0054747c2a8a69 Mon Sep 17 00:00:00 2001 From: endigma Date: Thu, 3 Jul 2025 13:05:48 +0100 Subject: [PATCH 4/4] use http.StatusBadRequest for persisted operations are disabled --- router/core/graphql_prehandler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/router/core/graphql_prehandler.go b/router/core/graphql_prehandler.go index 1a150ba5f6..9bac030b0b 100644 --- a/router/core/graphql_prehandler.go +++ b/router/core/graphql_prehandler.go @@ -565,7 +565,7 @@ func (h *PreHandler) handleOperation(req *http.Request, variablesParser *astjson if h.operationBlocker.persistedOperationsDisabled { return &httpGraphqlError{ message: "persisted operations are disabled", - statusCode: http.StatusOK, + statusCode: http.StatusBadRequest, } }