Skip to content

Commit 96fee4b

Browse files
feat(api): add query depth limit (#1096)
1 parent eec984e commit 96fee4b

File tree

12 files changed

+212
-31
lines changed

12 files changed

+212
-31
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ require (
2020
github.com/onsi/gomega v1.39.1
2121
github.com/onuryilmaz/ginprom v0.0.2
2222
github.com/openfga/go-sdk v0.7.4
23+
github.com/oyyblin/gqlgen-depth-limit-extension v0.1.0
2324
github.com/patrickmn/go-cache v2.1.0+incompatible
2425
github.com/prometheus/client_golang v1.23.2
2526
github.com/samber/lo v1.52.0

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,8 @@ github.com/openfga/go-sdk v0.7.4 h1:WBZDjl5Aqy1pFsDCL9LGZ5teJsYh42giFWA7G4AHfkw=
216216
github.com/openfga/go-sdk v0.7.4/go.mod h1:jGyDrPZauqrGM89iSqvjVwwF80fKCTOIERGZ+X3H4pI=
217217
github.com/openfga/language/pkg/go v0.2.0-beta.2 h1:PH4AOYSREgkMZSHO+RLOwkCLcg/i1cTxrVJraM6/YT4=
218218
github.com/openfga/language/pkg/go v0.2.0-beta.2/go.mod h1:ll/hN6kS4EE6B/7J/PbZqac9Nuv7ZHpI+Jfh36JLrbs=
219+
github.com/oyyblin/gqlgen-depth-limit-extension v0.1.0 h1:QnLrbTxU/95PBhEPOpIw2YspwSagu4xFmNNyga5LOWA=
220+
github.com/oyyblin/gqlgen-depth-limit-extension v0.1.0/go.mod h1:yWijMUBy85Lo9A0p3AdoItf6LQ6PNG1xER0ouWgjrMM=
219221
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
220222
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
221223
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=

internal/api/graphql/server.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/cloudoperators/heureka/internal/app"
1313
"github.com/cloudoperators/heureka/internal/util"
1414
"github.com/gin-gonic/gin"
15+
"github.com/oyyblin/gqlgen-depth-limit-extension/depth"
1516
)
1617

1718
type GraphQLAPI struct {
@@ -22,11 +23,15 @@ type GraphQLAPI struct {
2223
}
2324

2425
func NewGraphQLAPI(a app.Heureka, cfg util.Config) *GraphQLAPI {
26+
server := handler.NewDefaultServer(graph.NewExecutableSchema(resolver.NewResolver(a)))
27+
server.Use(depth.FixedDepthLimit(cfg.GQLDepthLimit))
28+
2529
graphQLAPI := GraphQLAPI{
26-
Server: handler.NewDefaultServer(graph.NewExecutableSchema(resolver.NewResolver(a))),
30+
Server: server,
2731
App: a,
2832
auth: middleware.NewAuth(&cfg, true),
2933
}
34+
3035
return &graphQLAPI
3136
}
3237

internal/e2e/authentication_test.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ func (at *authenticationTest) teardown() {
157157
}
158158

159159
func (at *authenticationTest) createIssueByUser(issue entity.Issue, user entity.User) model.Issue {
160-
respData := e2e_common.ExecuteGqlQueryFromFileWithHeaders[struct {
160+
respData, err := e2e_common.ExecuteGqlQueryFromFileWithHeaders[struct {
161161
Issue model.Issue `json:"createIssue"`
162162
}](
163163
at.cfg.Port,
@@ -171,6 +171,7 @@ func (at *authenticationTest) createIssueByUser(issue entity.Issue, user entity.
171171
},
172172
at.getHeaders(user))
173173

174+
Expect(err).ToNot(HaveOccurred())
174175
Expect(*respData.Issue.PrimaryName).To(Equal(issue.PrimaryName))
175176
Expect(*respData.Issue.Description).To(Equal(issue.Description))
176177
Expect(respData.Issue.Type.String()).To(Equal(issue.Type.String()))
@@ -179,7 +180,7 @@ func (at *authenticationTest) createIssueByUser(issue entity.Issue, user entity.
179180

180181
func (at *authenticationTest) updateIssueByUser(issue entity.Issue, user entity.User) model.Issue {
181182
issue.Description = "New Description"
182-
respData := e2e_common.ExecuteGqlQueryFromFileWithHeaders[struct {
183+
respData, err := e2e_common.ExecuteGqlQueryFromFileWithHeaders[struct {
183184
Issue model.Issue `json:"updateIssue"`
184185
}](
185186
at.cfg.Port,
@@ -190,13 +191,14 @@ func (at *authenticationTest) updateIssueByUser(issue entity.Issue, user entity.
190191
},
191192
at.getHeaders(user))
192193

194+
Expect(err).ToNot(HaveOccurred())
193195
Expect(*respData.Issue.Description).To(Equal(issue.Description))
194196
Expect(*respData.Issue.Metadata.CreatedBy).To(Equal(strconv.FormatInt(util.SystemUserId, 10)))
195197
return respData.Issue
196198
}
197199

198200
func (at *authenticationTest) deleteIssueByUser(issue entity.Issue, user entity.User) string {
199-
respData := e2e_common.ExecuteGqlQueryFromFileWithHeaders[struct {
201+
respData, err := e2e_common.ExecuteGqlQueryFromFileWithHeaders[struct {
200202
Id string `json:"deleteIssue"`
201203
}](
202204
at.cfg.Port,
@@ -206,12 +208,13 @@ func (at *authenticationTest) deleteIssueByUser(issue entity.Issue, user entity.
206208
},
207209
at.getHeaders(user))
208210

211+
Expect(err).ToNot(HaveOccurred())
209212
Expect(respData.Id).To(Equal(strconv.FormatInt(issue.Id, 10)))
210213
return respData.Id
211214
}
212215

213216
func (at *authenticationTest) getDeletedIssue(issueId string, user entity.User) model.Issue {
214-
respData := e2e_common.ExecuteGqlQueryFromFileWithHeaders[struct {
217+
respData, err := e2e_common.ExecuteGqlQueryFromFileWithHeaders[struct {
215218
Issues model.IssueConnection `json:"Issues"`
216219
}](
217220
at.cfg.Port,
@@ -225,6 +228,7 @@ func (at *authenticationTest) getDeletedIssue(issueId string, user entity.User)
225228

226229
item, ok := lo.Find(respData.Issues.Edges, func(e *model.IssueEdge) bool { return e.Node.ID == issueId })
227230
Expect(ok).To(BeTrue(), "issue id '%s' not found in deleted items")
231+
Expect(err).ToNot(HaveOccurred())
228232
return *item.Node
229233
}
230234

internal/e2e/common/gql.go

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,38 +11,52 @@ import (
1111
util2 "github.com/cloudoperators/heureka/pkg/util"
1212
"github.com/machinebox/graphql"
1313
. "github.com/onsi/gomega"
14-
"github.com/sirupsen/logrus"
1514
)
1615

1716
var GqlStandardHeaders map[string]string = map[string]string{
1817
"Cache-Control": "no-cache",
1918
}
2019

21-
func ExecuteGqlQueryFromFile[T any](port string, queryFilePath string, vars map[string]interface{}) T {
20+
func ExecuteGqlQuery[T any](client *graphql.Client, req *graphql.Request) (T, error) {
21+
var result T
22+
23+
err := util2.RequestWithBackoff(func() error {
24+
return client.Run(context.Background(), req, &result)
25+
})
26+
27+
return result, err
28+
}
29+
30+
func ExecuteGqlQueryFromFile[T any](port string, queryFilePath string, vars map[string]any) (T, error) {
2231
return ExecuteGqlQueryFromFileWithHeaders[T](port, queryFilePath, vars, GqlStandardHeaders)
2332
}
2433

25-
func ExecuteGqlQueryFromFileWithHeaders[T any](port string, queryFilePath string, vars map[string]interface{}, headers map[string]string) T {
34+
func ExecuteGqlQueryFromFileWithHeaders[T any](port string, queryFilePath string, vars map[string]any,
35+
headers map[string]string,
36+
) (T, error) {
2637
client, req := newClientAndRequestForGqlFileQuery(port, queryFilePath, vars, headers)
2738

28-
var result T
29-
if err := util2.RequestWithBackoff(func() error { return client.Run(context.Background(), req, &result) }); err != nil {
30-
logrus.WithError(err).WithField("request", req).Fatalln("Error while unmarshaling")
31-
}
32-
return result
39+
return ExecuteGqlQuery[T](client, req)
3340
}
3441

35-
func newClientAndRequestForGqlFileQuery(port string, queryFilePath string, vars map[string]interface{}, headers map[string]string) (*graphql.Client, *graphql.Request) {
42+
func newClientAndRequestForGqlFileQuery(port string, queryFilePath string, vars map[string]any,
43+
headers map[string]string,
44+
) (*graphql.Client, *graphql.Request) {
3645
client := graphql.NewClient(fmt.Sprintf("http://localhost:%s/query", port))
46+
3747
b, err := os.ReadFile(queryFilePath)
3848
Expect(err).To(BeNil())
49+
3950
str := string(b)
4051
req := graphql.NewRequest(str)
52+
4153
for k, v := range vars {
4254
req.Var(k, v)
4355
}
56+
4457
for k, v := range headers {
4558
req.Header.Set(k, v)
4659
}
60+
4761
return client, req
4862
}

internal/e2e/image_query_test.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ var _ = Describe("Getting Images via API", Label("e2e", "Images"), func() {
3939
imgTest.seed10Entries()
4040
})
4141
It("returns the expected content and the expected PageInfo when filtered using service", func() {
42-
respData := e2e_common.ExecuteGqlQueryFromFile[struct {
42+
respData, err := e2e_common.ExecuteGqlQueryFromFile[struct {
4343
Images model.ImageConnection `json:"Images"`
4444
}](
4545
imgTest.port,
@@ -55,6 +55,7 @@ var _ = Describe("Getting Images via API", Label("e2e", "Images"), func() {
5555
expectRespDataCounts(respData.Images, 5, 3)
5656
expectRespImagesFilledAndInOrder(&respData.Images)
5757
expectPageInfoTwoPagesBeingOnFirst(respData.Images.PageInfo)
58+
Expect(err).ToNot(HaveOccurred())
5859
Expect(*respData.Images.Counts).To(Equal(imgTest.counts))
5960
})
6061
It("returns the expected content and the expected PageInfo when filtered using repository", func() {
@@ -74,7 +75,7 @@ var _ = Describe("Getting Images via API", Label("e2e", "Images"), func() {
7475
// belong to first service and first component
7576
counts := model.SeverityCounts{Critical: 2, Total: 2}
7677

77-
respData := e2e_common.ExecuteGqlQueryFromFile[struct {
78+
respData, err := e2e_common.ExecuteGqlQueryFromFile[struct {
7879
Images model.ImageConnection `json:"Images"`
7980
}](
8081
imgTest.port,
@@ -91,6 +92,7 @@ var _ = Describe("Getting Images via API", Label("e2e", "Images"), func() {
9192
expectRespDataCounts(respData.Images, 1, 1)
9293
expectRespImagesFilledAndInOrder(&respData.Images)
9394
Expect(*respData.Images.Counts).To(Equal(counts))
95+
Expect(err).ToNot(HaveOccurred())
9496
})
9597
})
9698
})

internal/e2e/image_version_query_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,13 +101,14 @@ var _ = Describe("Getting ImageVersions via API", Label("e2e", "ImageVersions"),
101101
It("can query image versions", func() {
102102
idsBySeverity := []string{"3", "8", "2", "7", "1", "6", "5", "4", "10", "9"}
103103

104-
respData := e2e_common.ExecuteGqlQueryFromFile[struct {
104+
respData, err := e2e_common.ExecuteGqlQueryFromFile[struct {
105105
ImageVersions model.ImageVersionConnection `json:"ImageVersions"`
106106
}](
107107
cfg.Port,
108108
"../api/graphql/graph/queryCollection/imageVersion/query.graphql",
109109
map[string]interface{}{"first": 10, "after": ""},
110110
)
111+
Expect(err).ToNot(HaveOccurred())
111112
Expect(respData.ImageVersions.TotalCount).To(Equal(10))
112113
Expect(len(respData.ImageVersions.Edges)).To(Equal(10))
113114

internal/e2e/patch_query_test.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ var _ = Describe("Getting Patches via API", Label("e2e", "Patches"), func() {
4444

4545
When("the database is empty", func() {
4646
It("returns empty result set", func() {
47-
respData := e2e_common.ExecuteGqlQueryFromFile[struct {
47+
respData, err := e2e_common.ExecuteGqlQueryFromFile[struct {
4848
Patches model.PatchConnection `json:"Patches"`
4949
}](
5050
cfg.Port,
@@ -54,6 +54,7 @@ var _ = Describe("Getting Patches via API", Label("e2e", "Patches"), func() {
5454
"first": 10,
5555
"after": "",
5656
})
57+
Expect(err).NotTo(HaveOccurred())
5758
Expect(respData.Patches.TotalCount).To(Equal(0))
5859
})
5960
})
@@ -63,20 +64,22 @@ var _ = Describe("Getting Patches via API", Label("e2e", "Patches"), func() {
6364
type patchRespDataType struct {
6465
Patches model.PatchConnection `json:"Patches"`
6566
}
66-
var respData patchRespDataType
67+
6768
BeforeEach(func() {
6869
seedCollection = seeder.SeedDbWithNFakeData(10)
6970
})
7071
Context("and no additional filters are present", func() {
7172
It("returns correct result count", func() {
72-
respData = e2e_common.ExecuteGqlQueryFromFile[patchRespDataType](
73+
respData, err := e2e_common.ExecuteGqlQueryFromFile[patchRespDataType](
7374
cfg.Port,
7475
"../api/graphql/graph/queryCollection/patch/query.graphql",
7576
map[string]interface{}{
7677
"filter": map[string]string{},
7778
"first": 5,
7879
"after": "",
7980
})
81+
82+
Expect(err).ToNot(HaveOccurred())
8083
Expect(respData.Patches.TotalCount).To(Equal(len(seedCollection.PatchRows)))
8184
Expect(len(respData.Patches.Edges)).To(Equal(5))
8285
//- returns the expected PageInfo
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package e2e_test
5+
6+
import (
7+
"fmt"
8+
9+
e2e_common "github.com/cloudoperators/heureka/internal/e2e/common"
10+
"github.com/cloudoperators/heureka/internal/util"
11+
util2 "github.com/cloudoperators/heureka/pkg/util"
12+
13+
"github.com/cloudoperators/heureka/internal/api/graphql/graph/model"
14+
"github.com/cloudoperators/heureka/internal/database/mariadb"
15+
"github.com/cloudoperators/heureka/internal/server"
16+
"github.com/machinebox/graphql"
17+
. "github.com/onsi/ginkgo/v2"
18+
. "github.com/onsi/gomega"
19+
)
20+
21+
const (
22+
queryWithDepthExceeded = `{
23+
Services {
24+
edges {
25+
node {
26+
supportGroups {
27+
edges {
28+
node {
29+
services {
30+
edges {
31+
node {
32+
id
33+
}
34+
}
35+
}
36+
users {
37+
edges {
38+
node {
39+
id
40+
supportGroups {
41+
edges {
42+
node {
43+
id
44+
}
45+
}
46+
}
47+
}
48+
}
49+
}
50+
}
51+
}
52+
}
53+
}
54+
}
55+
}
56+
}`
57+
queryWithNormalDepth = `{
58+
Services {
59+
edges {
60+
node {
61+
supportGroups {
62+
edges {
63+
node {
64+
services {
65+
edges {
66+
node {
67+
id
68+
}
69+
}
70+
}
71+
users {
72+
edges {
73+
node {
74+
id
75+
}
76+
}
77+
}
78+
}
79+
}
80+
}
81+
}
82+
}
83+
}
84+
}`
85+
)
86+
87+
var _ = Describe("Getting data via API", Label("e2e", "Depth Limiting"), func() {
88+
var s *server.Server
89+
var cfg util.Config
90+
var db *mariadb.SqlDatabase
91+
92+
BeforeEach(func() {
93+
var err error
94+
db = dbm.NewTestSchemaWithoutMigration()
95+
Expect(err).To(BeNil(), "Database Seeder Setup should work")
96+
97+
cfg = dbm.DbConfig()
98+
cfg.Port = util2.GetRandomFreePort()
99+
// Set depth limit as 10 for testing pourpose
100+
cfg.GQLDepthLimit = 10
101+
s = e2e_common.NewRunningServer(cfg)
102+
})
103+
104+
AfterEach(func() {
105+
e2e_common.ServerTeardown(s)
106+
dbm.TestTearDown(db)
107+
})
108+
109+
When("Request with depth exceeding limit", func() {
110+
It("returns an error", func() {
111+
client := graphql.NewClient(fmt.Sprintf("http://localhost:%s/query", cfg.Port))
112+
req := graphql.NewRequest(queryWithDepthExceeded)
113+
req.Header.Set("Cache-Control", "no-cache")
114+
_, err := e2e_common.ExecuteGqlQuery[struct {
115+
Services model.ServiceConnection `json:"Services"`
116+
}](client, req)
117+
118+
Expect(err).To(HaveOccurred())
119+
Expect(err.Error()).To(ContainSubstring("operation exceeds the depth limit"))
120+
})
121+
})
122+
123+
When("Request with allowed depth", func() {
124+
It("returns an error", func() {
125+
client := graphql.NewClient(fmt.Sprintf("http://localhost:%s/query", cfg.Port))
126+
req := graphql.NewRequest(queryWithNormalDepth)
127+
128+
req.Header.Set("Cache-Control", "no-cache")
129+
130+
_, err := e2e_common.ExecuteGqlQuery[struct {
131+
Services model.ServiceConnection `json:"Services"`
132+
}](client, req)
133+
134+
Expect(err).ToNot(HaveOccurred())
135+
})
136+
})
137+
})

0 commit comments

Comments
 (0)