Skip to content

Commit

Permalink
Merge branch 'main' into dave/eng-6004-make-header-rules-case-insensi…
Browse files Browse the repository at this point in the history
…tive
  • Loading branch information
df-wg authored Feb 13, 2025
2 parents 9a9822f + b81b828 commit 0d07cf6
Show file tree
Hide file tree
Showing 15 changed files with 417 additions and 24 deletions.
7 changes: 6 additions & 1 deletion cli/test/parse-operations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,16 @@ describe('parse operations from different formats', () => {
const operations = parseOperations(operation);
expect(operations).toEqual([{ id, contents: operation }]);
});
test('returns consistent hash', () => {
const operation = `query Employees {\n employees {\n id\n }\n}`;
const operations = parseOperations(operation);
expect(operations).toEqual([{ id: "33651da3d80e420709520fb900c7ab8ec4151555da56062feeee428cf7f3a5dd", contents: operation }]);
});
test('parse operations from Apollo', async() => {
const persistedQueries = await fs.readFile(path.join('test', 'testdata', 'persisted-query-manifest.json'), 'utf8');
const operations = parseOperations(persistedQueries);
expect(operations).toEqual([
{ id: "2d9df67f96ce804da7a9107d33373132a53bf56aec29ef4b4e06569a43a16935", contents: "query Employees {\n employees {\n id\n }\n}" },
{ id: "33651da3d80e420709520fb900c7ab8ec4151555da56062feeee428cf7f3a5dd", contents: "query Employees {\n employees {\n id\n }\n}" },
]);
});
test('parse query map', async() => {
Expand Down
2 changes: 1 addition & 1 deletion cli/test/testdata/persisted-query-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": 1,
"operations": [
{
"id": "2d9df67f96ce804da7a9107d33373132a53bf56aec29ef4b4e06569a43a16935",
"id": "33651da3d80e420709520fb900c7ab8ec4151555da56062feeee428cf7f3a5dd",
"name": "Employees",
"type": "query",
"body": "query Employees {\n employees {\n id\n }\n}"
Expand Down
63 changes: 63 additions & 0 deletions router-tests/cache_warmup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,69 @@ func TestCacheWarmup(t *testing.T) {
})
})

t.Run("cache warmup persisted operation with multiple operations works with safelist enabled", func(t *testing.T) {
t.Parallel()
testenv.Run(t, &testenv.Config{
RouterOptions: []core.Option{
core.WithCacheWarmupConfig(&config.CacheWarmupConfiguration{
Enabled: true,
Source: config.CacheWarmupSource{
Filesystem: &config.CacheWarmupFileSystemSource{
Path: "testenv/testdata/cache_warmup/json_po_multi_operations",
},
},
}),
core.WithPersistedOperationsConfig(config.PersistedOperationsConfig{
Safelist: config.SafelistConfiguration{Enabled: true},
}),
},
AssertCacheMetrics: &testenv.CacheMetricsAssertions{
BaseGraphAssertions: testenv.CacheMetricsAssertion{
QueryNormalizationMisses: 1, // 1x miss during first safelist call
QueryNormalizationHits: 1, // 1x hit during second safelist call
PersistedQueryNormalizationHits: 2, // 1x hit after warmup, when called with operation name. No hit from second request because of missing operation name, it recomputes it
PersistedQueryNormalizationMisses: 5, // 1x miss during warmup, 1 miss for first operation trying without operation name, 1 miss for second operation trying without operation name, 2x miss during safelist because went to normal query normalization cache
ValidationHits: 4,
ValidationMisses: 1,
PlanHits: 4,
PlanMisses: 1,
},
},
}, 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(`"A"`),
Extensions: []byte(`{"persistedQuery": {"version": 1, "sha256Hash": "724399f210ef3f16e6e5427a70bb9609ecea7297e99c3e9241d5912d04eabe60"}}`),
Header: header,
})
require.NoError(t, err)
require.Equal(t, `{"data":{"a":{"id":1,"details":{"pets":null}}}}`, res.Body)

res2, err2 := xEnv.MakeGraphQLRequest(testenv.GraphQLRequest{
Extensions: []byte(`{"persistedQuery": {"version": 1, "sha256Hash": "724399f210ef3f16e6e5427a70bb9609ecea7297e99c3e9241d5912d04eabe60"}}`),
Header: header,
})
require.NoError(t, err2)
require.Equal(t, `{"data":{"a":{"id":1,"details":{"pets":null}}}}`, res2.Body)

res3, err3 := xEnv.MakeGraphQLRequest(testenv.GraphQLRequest{
Header: header,
Query: "query A {\n a: employee(id: 1) {\n id\n details {\n pets {\n name\n }\n }\n }\n}\n\nquery B ($id: Int!) {\n b: employee(id: $id) {\n id\n details {\n pets {\n name\n }\n }\n }\n}",
})
require.NoError(t, err3)
require.Equal(t, `{"data":{"a":{"id":1,"details":{"pets":null}}}}`, res3.Body)

res4, err4 := xEnv.MakeGraphQLRequest(testenv.GraphQLRequest{
OperationName: []byte(`"A"`),
Header: header,
Query: "query A {\n a: employee(id: 1) {\n id\n details {\n pets {\n name\n }\n }\n }\n}\n\nquery B ($id: Int!) {\n b: employee(id: $id) {\n id\n details {\n pets {\n name\n }\n }\n }\n}",
})
require.NoError(t, err4)
require.Equal(t, `{"data":{"a":{"id":1,"details":{"pets":null}}}}`, res4.Body)
})
})

t.Run("cache warmup workers throttle", func(t *testing.T) {
t.Parallel()
logger, err := zap.NewDevelopment()
Expand Down
220 changes: 220 additions & 0 deletions router-tests/safelist_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
package integration

import (
"net/http"
"testing"

"github.com/stretchr/testify/require"
"github.com/wundergraph/cosmo/router-tests/testenv"
"github.com/wundergraph/cosmo/router/core"
"github.com/wundergraph/cosmo/router/pkg/config"
"go.uber.org/zap/zapcore"
)

func TestSafelist(t *testing.T) {
t.Parallel()

var (
persistedQuery = "query Employees {\n employees {\n id\n }\n}"
nonPersistedQuery = "query Employees {\n\n\n employees {\n id\n }\n}"
queryWithDetails = "query Employees {\n employees {\n id\n details {\n forename\n} \n}\n}"
persistedNotFoundResp = `{"errors":[{"message":"PersistedQueryNotFound","extensions":{"code":"PERSISTED_QUERY_NOT_FOUND"}}]}`
)

t.Run("router fails if APQ and Safelist are both enabled", func(t *testing.T) {
testenv.FailsOnStartup(t, &testenv.Config{
ApqConfig: config.AutomaticPersistedQueriesConfig{Enabled: true},
RouterOptions: []core.Option{
core.WithPersistedOperationsConfig(config.PersistedOperationsConfig{
Safelist: config.SafelistConfiguration{Enabled: true},
}),
},
}, func(t *testing.T, err error) {
require.Contains(t, err.Error(), "automatic persisted queries and safelist cannot be enabled at the same time")
})
})

t.Run("safelist should allow a persisted query to run", func(t *testing.T) {
testenv.Run(t, &testenv.Config{
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 should allow a persisted query (run with ID) to run", func(t *testing.T) {
testenv.Run(t, &testenv.Config{
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 should reject a query with different spacing from the persisted operation", func(t *testing.T) {
testenv.Run(t, &testenv.Config{
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: nonPersistedQuery,
})
require.NoError(t, err)
require.Equal(t, persistedNotFoundResp, res.Body)
})
})

t.Run("safelist should block a non persisted query", func(t *testing.T) {
testenv.Run(t, &testenv.Config{
ApqConfig: config.AutomaticPersistedQueriesConfig{
Enabled: false,
},
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{
ApqConfig: config.AutomaticPersistedQueriesConfig{
Enabled: false,
},
RouterOptions: []core.Option{
core.WithPersistedOperationsConfig(config.PersistedOperationsConfig{
Safelist: config.SafelistConfiguration{Enabled: false},
LogUnknown: true,
}),
},
LogObservation: testenv.LogObservationConfig{
Enabled: true,
LogLevel: zapcore.InfoLevel,
},
}, 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: nonPersistedQuery,
})
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)

logEntries := xEnv.Observer().FilterMessageSnippet("Unknown persisted operation found").All()
require.Len(t, logEntries, 1)
requestContext := logEntries[0].ContextMap()
require.Equal(t, nonPersistedQuery, requestContext["query"])
require.Equal(t, "5e72e7c4cf0f86f7bc7044eb0c932917f3491c5f63fb769b96e5ded98c4ac0a5", requestContext["sha256Hash"])
})
})

t.Run("logs non persisted query and stops them if safelist set", func(t *testing.T) {
testenv.Run(t, &testenv.Config{
ApqConfig: config.AutomaticPersistedQueriesConfig{
Enabled: false,
},
RouterOptions: []core.Option{
core.WithPersistedOperationsConfig(config.PersistedOperationsConfig{
Safelist: config.SafelistConfiguration{Enabled: true},
LogUnknown: true,
}),
},
LogObservation: testenv.LogObservationConfig{
Enabled: true,
LogLevel: zapcore.InfoLevel,
},
}, 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: nonPersistedQuery,
})
require.NoError(t, err)
require.Equal(t, persistedNotFoundResp, res.Body)

logEntries := xEnv.Observer().FilterMessageSnippet("Unknown persisted operation found").All()
require.Len(t, logEntries, 1)
requestContext := logEntries[0].ContextMap()
require.Equal(t, nonPersistedQuery, requestContext["query"])
require.Equal(t, "5e72e7c4cf0f86f7bc7044eb0c932917f3491c5f63fb769b96e5ded98c4ac0a5", requestContext["sha256Hash"])
})
})

t.Run("doesn't log persisted queries and allows them to continue", func(t *testing.T) {
testenv.Run(t, &testenv.Config{
ApqConfig: config.AutomaticPersistedQueriesConfig{
Enabled: false,
},
RouterOptions: []core.Option{
core.WithPersistedOperationsConfig(config.PersistedOperationsConfig{
Safelist: config.SafelistConfiguration{Enabled: true},
LogUnknown: true,
}),
},
LogObservation: testenv.LogObservationConfig{
Enabled: true,
LogLevel: zapcore.InfoLevel,
},
}, 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)

logEntries := xEnv.Observer().FilterMessageSnippet("Unknown persisted operation found").All()
require.Len(t, logEntries, 0)
})
})
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"version": 1,
"body": "query Employees {\n employees {\n id\n }\n}"
}
6 changes: 6 additions & 0 deletions router/core/graph_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,10 @@ func (s *graphMux) buildOperationCaches(srv *graphServer) (computeSha256 bool, e
break
}
}
} else 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
}

if computeSha256 {
Expand Down Expand Up @@ -1025,6 +1029,8 @@ func (s *graphServer) buildGraphMux(ctx context.Context,
Enabled: s.securityConfiguration.BlockNonPersistedOperations.Enabled,
Condition: s.securityConfiguration.BlockNonPersistedOperations.Condition,
},
SafelistEnabled: s.persistedOperationsConfig.Safelist.Enabled,
LogUnknownOperationsEnabled: s.persistedOperationsConfig.LogUnknown,
})
if err != nil {
return nil, fmt.Errorf("failed to create operation blocker: %w", err)
Expand Down
Loading

0 comments on commit 0d07cf6

Please sign in to comment.