Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions router-tests/cache_warmup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,8 @@ func TestCacheWarmup(t *testing.T) {
ValidationMisses: 1,
PlanHits: 4,
PlanMisses: 1,
QueryHashMisses: 2, // 2x miss for safelist queries (raw query body hashed for safelist check)
QueryHashHits: 1, // 1x hit for repeated query body hash
},
},
}, func(t *testing.T, xEnv *testenv.Environment) {
Expand Down
84 changes: 84 additions & 0 deletions router-tests/safelist_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,90 @@ func TestSafelist(t *testing.T) {
})
})

t.Run("safelist with access log sha256 attribute allows a persisted query to run", func(t *testing.T) {
testenv.Run(t, &testenv.Config{
AccessLogFields: []config.CustomAttribute{
{
Key: "operation_sha256",
ValueFrom: &config.CustomDynamicAttribute{
ContextField: core.ContextFieldOperationSha256,
},
},
},
RouterOptions: []core.Option{
core.WithPersistedOperationsConfig(config.PersistedOperationsConfig{
Safelist: config.SafelistConfiguration{Enabled: true},
}),
},
}, func(t *testing.T, xEnv *testenv.Environment) {
header := make(http.Header)
header.Add("graphql-client-name", "my-client")
res, err := xEnv.MakeGraphQLRequest(testenv.GraphQLRequest{
OperationName: []byte(`"Employees"`),
Header: header,
Query: persistedQuery,
})
require.NoError(t, err)
require.Equal(t, `{"data":{"employees":[{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":7},{"id":8},{"id":10},{"id":11},{"id":12}]}}`, res.Body)
})
})

t.Run("safelist with access log sha256 attribute allows a persisted query run with ID", func(t *testing.T) {
testenv.Run(t, &testenv.Config{
AccessLogFields: []config.CustomAttribute{
{
Key: "operation_sha256",
ValueFrom: &config.CustomDynamicAttribute{
ContextField: core.ContextFieldOperationSha256,
},
},
},
RouterOptions: []core.Option{
core.WithPersistedOperationsConfig(config.PersistedOperationsConfig{
Safelist: config.SafelistConfiguration{Enabled: true},
}),
},
}, func(t *testing.T, xEnv *testenv.Environment) {
header := make(http.Header)
header.Add("graphql-client-name", "my-client")
res, err := xEnv.MakeGraphQLRequest(testenv.GraphQLRequest{
OperationName: []byte(`"Employees"`),
Extensions: []byte(`{"persistedQuery": {"version": 1, "sha256Hash": "dc67510fb4289672bea757e862d6b00e83db5d3cbbcfb15260601b6f29bb2b8f"}}`),
Header: header,
})
require.NoError(t, err)
require.Equal(t, `{"data":{"employees":[{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":7},{"id":8},{"id":10},{"id":11},{"id":12}]}}`, res.Body)
})
})

t.Run("safelist with access log sha256 attribute rejects non persisted query", func(t *testing.T) {
testenv.Run(t, &testenv.Config{
AccessLogFields: []config.CustomAttribute{
{
Key: "operation_sha256",
ValueFrom: &config.CustomDynamicAttribute{
ContextField: core.ContextFieldOperationSha256,
},
},
},
RouterOptions: []core.Option{
core.WithPersistedOperationsConfig(config.PersistedOperationsConfig{
Safelist: config.SafelistConfiguration{Enabled: true},
}),
},
}, func(t *testing.T, xEnv *testenv.Environment) {
header := make(http.Header)
header.Add("graphql-client-name", "my-client")
res, err := xEnv.MakeGraphQLRequest(testenv.GraphQLRequest{
OperationName: []byte(`"Employees"`),
Header: header,
Query: queryWithDetails,
})
require.NoError(t, err)
require.Equal(t, persistedNotFoundResp, res.Body)
})
})

t.Run("log unknown operations", func(t *testing.T) {
t.Run("logs non persisted query but allows them to continue", func(t *testing.T) {
testenv.Run(t, &testenv.Config{
Expand Down
6 changes: 3 additions & 3 deletions router-tests/structured_logging_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1818,7 +1818,7 @@ func TestFlakyAccessLogs(t *testing.T) {
"service_name": "service-name", // From request header
"operation_persisted_hash": "dc67510fb4289672bea757e862d6b00e83db5d3cbbcfb15260601b6f29bb2b8f", // From context
"operation_hash": "1163600561566987607", // From context
"operation_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", // From context
"operation_sha256": "dc67510fb4289672bea757e862d6b00e83db5d3cbbcfb15260601b6f29bb2b8f", // From context
"operation_name": "Employees", // From context
"operation_type": "query", // From context
}
Expand Down Expand Up @@ -2029,7 +2029,7 @@ func TestFlakyAccessLogs(t *testing.T) {
)
})

t.Run("validate request.operation.sha256Hash expression with persisted hash and body", func(t *testing.T) {
t.Run("validate request.operation.sha256Hash expression with persisted hash only", func(t *testing.T) {
t.Parallel()

testenv.Run(t,
Expand Down Expand Up @@ -2062,7 +2062,7 @@ func TestFlakyAccessLogs(t *testing.T) {

val, ok := requestContext["operation_sha256_expression"].(string)
require.True(t, ok)
require.Equal(t, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", val)
require.Equal(t, "dc67510fb4289672bea757e862d6b00e83db5d3cbbcfb15260601b6f29bb2b8f", val)
},
)
})
Expand Down
10 changes: 7 additions & 3 deletions router/core/graph_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -668,21 +668,25 @@ func (s *graphMux) buildOperationCaches(srv *graphServer) (computeSha256 bool, e
}

// Currently, we only support custom attributes from the context for OTLP metrics
if len(srv.metricConfig.Attributes) > 0 {
if !computeSha256 && len(srv.metricConfig.Attributes) > 0 {
for _, customAttribute := range srv.metricConfig.Attributes {
if customAttribute.ValueFrom != nil && customAttribute.ValueFrom.ContextField == ContextFieldOperationSha256 {
computeSha256 = true
break
}
}
} else if srv.accessLogsConfig != nil {
}

if !computeSha256 && srv.accessLogsConfig != nil {
for _, customAttribute := range append(srv.accessLogsConfig.Attributes, srv.accessLogsConfig.SubgraphAttributes...) {
if customAttribute.ValueFrom != nil && customAttribute.ValueFrom.ContextField == ContextFieldOperationSha256 {
computeSha256 = true
break
}
}
} else if srv.persistedOperationsConfig.Safelist.Enabled || srv.persistedOperationsConfig.LogUnknown {
}

if srv.persistedOperationsConfig.Safelist.Enabled || srv.persistedOperationsConfig.LogUnknown {
// In these case, we'll want to compute the sha256 for every operation, in order to check that the operation
// is present in the Persisted Operation cache
computeSha256 = true
Expand Down
38 changes: 25 additions & 13 deletions router/core/graphql_prehandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -543,22 +543,34 @@ func (h *PreHandler) handleOperation(req *http.Request, httpOperation *httpOpera

// Compute the operation sha256 hash as soon as possible for observability reasons
if h.shouldComputeOperationSha256(operationKit, requestContext) {
if err := operationKit.ComputeOperationSha256(); err != nil {
return &httpGraphqlError{
message: fmt.Sprintf("error hashing operation: %s", err),
statusCode: http.StatusInternalServerError,
if operationKit.parsedOperation.Request.Query == "" && operationKit.parsedOperation.GraphQLRequestExtensions.PersistedQuery.HasHash() {
// No query body to hash; use the client-provided persisted hash for telemetry.
requestContext.operation.sha256Hash = operationKit.parsedOperation.GraphQLRequestExtensions.PersistedQuery.Sha256Hash
requestContext.expressionContext.Request.Operation.Sha256Hash = requestContext.operation.sha256Hash

setTelemetryAttributes(req.Context(), requestContext, expr.BucketSha256)

requestContext.telemetry.addCustomMetricStringAttr(ContextFieldOperationSha256, requestContext.operation.sha256Hash)
} else {
if err := operationKit.ComputeOperationSha256(); err != nil {
return &httpGraphqlError{
message: fmt.Sprintf("error hashing operation: %s", err),
statusCode: http.StatusInternalServerError,
}
}
}
requestContext.operation.sha256Hash = operationKit.parsedOperation.Sha256Hash
requestContext.expressionContext.Request.Operation.Sha256Hash = operationKit.parsedOperation.Sha256Hash
requestContext.operation.sha256Hash = operationKit.parsedOperation.Sha256Hash
requestContext.expressionContext.Request.Operation.Sha256Hash = operationKit.parsedOperation.Sha256Hash

setTelemetryAttributes(req.Context(), requestContext, expr.BucketSha256)
setTelemetryAttributes(req.Context(), requestContext, expr.BucketSha256)

requestContext.telemetry.addCustomMetricStringAttr(ContextFieldOperationSha256, requestContext.operation.sha256Hash)
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,
requestContext.telemetry.addCustomMetricStringAttr(ContextFieldOperationSha256, requestContext.operation.sha256Hash)
if h.operationBlocker.safelistEnabled || h.operationBlocker.logUnknownOperationsEnabled {
if !operationKit.parsedOperation.GraphQLRequestExtensions.PersistedQuery.HasHash() {
// 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,
}
}
}
}
}
Expand Down
Loading