Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
4 changes: 2 additions & 2 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 @@ -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