mirror of
https://github.com/lukaszraczylo/graphql-monitoring-proxy.git
synced 2026-06-25 04:33:23 +00:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 19b3b3e596 | |||
| 5852a4c356 | |||
| e814345069 | |||
| 984e448ff0 | |||
| 5799f8ca7c | |||
| ac84c69812 | |||
| e54bbe8249 | |||
| ed3966e577 | |||
| 6a52a9f673 | |||
| 1ca05a7a2a | |||
| eb1b4b4eb7 | |||
| fc9bab47fb | |||
| cbe2afe539 | |||
| 2190744729 | |||
| 0a96d139b6 | |||
| 1c1ac06e11 | |||
| b2a67df3b6 | |||
| 3805e63f95 | |||
| 8abf731867 | |||
| 4e9db9a5c7 | |||
| 615836ab36 | |||
| a51f37c0a2 |
@@ -13,7 +13,7 @@ help: ## display this help
|
||||
|
||||
.PHONY: run
|
||||
run: build ## run application
|
||||
@LOG_LEVEL=debug PURGE_METRICS_ON_CRAWL=true BLOCK_SCHEMA_INTROSPECTION=false CACHE_TTL=10 JWT_ROLE_RATE_LIMIT=false JWT_ROLE_CLAIM_PATH="Hasura.x-hasura-default-role" JWT_USER_CLAIM_PATH="Hasura.x-hasura-user-id" HOST_GRAPHQL=https://hasura8.lan/ HEALTHCHECK_GRAPHQL_URL=https://hasura8.lan/v1/graphql ./graphql-proxy
|
||||
@LOG_LEVEL=debug PURGE_METRICS_ON_CRAWL=true BLOCK_SCHEMA_INTROSPECTION=true CACHE_TTL=10 JWT_ROLE_RATE_LIMIT=false JWT_ROLE_CLAIM_PATH="Hasura.x-hasura-default-role" JWT_USER_CLAIM_PATH="Hasura.x-hasura-user-id" HOST_GRAPHQL=https://hasura8.lan/ HEALTHCHECK_GRAPHQL_URL=https://hasura8.lan/v1/graphql PORT_GRAPHQL=8111 ./graphql-proxy
|
||||
|
||||
.PHONY: build
|
||||
build: ## build the binary
|
||||
|
||||
@@ -40,6 +40,8 @@ I wanted to monitor the queries and responses of our graphql endpoint. Still, we
|
||||
|
||||
You should always try to stick to the latest and greatest version of the graphql-proxy to ensure that it's as much bug-free as possible. Following list will be kept to the maximum of five "most important" bugs and enhancements included in the latest versions.
|
||||
|
||||
* **06/12/2024 - 0.25.12** - Fixes the bug where deeply nested introspection queries were blocked despite of being present on the whitelist. GraphQL proxy will now inspect the queries in depth to find any possible nested introspections.
|
||||
|
||||
* **20/08/2024 - 0.23.21+** - Fixes the bug when timeouts were not respected on proxy-graphql line. Affected versions before that were timeouting after 30 seconds which was set as default ( thanks to Jurica Železnjak for reporting ). It also provides a temporary fix for running within kubernetes deployment, when graphql server ( for example - hasura ) took more time to start than the proxy, causing avalanche of errors with "can't proxy the request".
|
||||
|
||||
* **19/08/2024 - 0.21.82+** - Fixed the issue when proxy failed to start if global cache was disabled, therefore not initialized and proxy tried to perform the cache operations during normal query operations.
|
||||
|
||||
@@ -71,13 +71,14 @@ func enableHasuraEventCleaner() {
|
||||
|
||||
func cleanEvents(pool *pgxpool.Pool) {
|
||||
ctx := context.Background()
|
||||
var errors []error
|
||||
var failedQueries []string
|
||||
|
||||
for _, query := range delQueries {
|
||||
_, err := pool.Exec(ctx, fmt.Sprintf(query, cfg.HasuraEventCleaner.ClearOlderThan))
|
||||
if err != nil {
|
||||
cfg.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Failed to execute query",
|
||||
Pairs: map[string]interface{}{"query": query, "error": err.Error()},
|
||||
})
|
||||
errors = append(errors, err)
|
||||
failedQueries = append(failedQueries, query)
|
||||
} else {
|
||||
cfg.Logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Successfully executed query",
|
||||
@@ -85,4 +86,18 @@ func cleanEvents(pool *pgxpool.Pool) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
var errMsgs []string
|
||||
for _, err := range errors {
|
||||
errMsgs = append(errMsgs, err.Error())
|
||||
}
|
||||
cfg.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Failed to execute some queries",
|
||||
Pairs: map[string]interface{}{
|
||||
"failed_queries": failedQueries,
|
||||
"errors": errMsgs,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,19 +6,19 @@ require (
|
||||
github.com/VictoriaMetrics/metrics v1.35.1
|
||||
github.com/alicebob/miniredis/v2 v2.33.0
|
||||
github.com/avast/retry-go/v4 v4.6.0
|
||||
github.com/goccy/go-json v0.10.3
|
||||
github.com/goccy/go-json v0.10.4
|
||||
github.com/gofiber/fiber/v2 v2.52.5
|
||||
github.com/gofrs/flock v0.12.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gookit/goutil v0.6.17
|
||||
github.com/gookit/goutil v0.6.18
|
||||
github.com/graphql-go/graphql v0.8.1
|
||||
github.com/jackc/pgx/v5 v5.7.1
|
||||
github.com/lukaszraczylo/ask v0.0.0-20240916204100-6e9ef53a62d9
|
||||
github.com/lukaszraczylo/go-ratecounter v0.1.12
|
||||
github.com/lukaszraczylo/go-simple-graphql v1.2.29
|
||||
github.com/redis/go-redis/v9 v9.6.1
|
||||
github.com/lukaszraczylo/go-simple-graphql v1.2.33
|
||||
github.com/redis/go-redis/v9 v9.7.0
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/valyala/fasthttp v1.56.0
|
||||
github.com/valyala/fasthttp v1.58.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -31,7 +31,7 @@ require (
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/klauspost/compress v1.17.10 // indirect
|
||||
github.com/klauspost/compress v1.17.11 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
@@ -45,11 +45,11 @@ require (
|
||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
github.com/yuin/gopher-lua v1.1.1 // indirect
|
||||
golang.org/x/crypto v0.28.0 // indirect
|
||||
golang.org/x/net v0.30.0 // indirect
|
||||
golang.org/x/sync v0.8.0 // indirect
|
||||
golang.org/x/sys v0.26.0 // indirect
|
||||
golang.org/x/term v0.25.0 // indirect
|
||||
golang.org/x/text v0.19.0 // indirect
|
||||
golang.org/x/crypto v0.31.0 // indirect
|
||||
golang.org/x/net v0.32.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/term v0.27.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -20,8 +20,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
|
||||
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
|
||||
github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-reflect v1.2.0 h1:O0T8rZCuNmGXewnATuKYnkL0xm6o8UNOJZd/gOkb9ms=
|
||||
github.com/goccy/go-reflect v1.2.0/go.mod h1:n0oYZn8VcV2CkWTxi8B9QjkCoq6GTtCEdfmR66YhFtE=
|
||||
github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo=
|
||||
@@ -32,8 +32,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
|
||||
github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
|
||||
github.com/gookit/goutil v0.6.17 h1:SxmbDz2sn2V+O+xJjJhJT/sq1/kQh6rCJ7vLBiRPZjI=
|
||||
github.com/gookit/goutil v0.6.17/go.mod h1:rSw1LchE1I3TDWITZvefoAC9tS09SFu3lHXLCV7EaEY=
|
||||
github.com/gookit/goutil v0.6.18 h1:MUVj0G16flubWT8zYVicIuisUiHdgirPAkmnfD2kKgw=
|
||||
github.com/gookit/goutil v0.6.18/go.mod h1:AY/5sAwKe7Xck+mEbuxj0n/bc3qwrGNe3Oeulln7zBA=
|
||||
github.com/graphql-go/graphql v0.8.1 h1:p7/Ou/WpmulocJeEx7wjQy611rtXGQaAcXGqanuMMgc=
|
||||
github.com/graphql-go/graphql v0.8.1/go.mod h1:nKiHzRM0qopJEwCITUuIsxk9PlVlwIiiI8pnJEhordQ=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
@@ -44,8 +44,8 @@ github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs=
|
||||
github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/klauspost/compress v1.17.10 h1:oXAz+Vh0PMUvJczoi+flxpnBEPxoER1IaAnU/NMPtT0=
|
||||
github.com/klauspost/compress v1.17.10/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
@@ -54,8 +54,8 @@ github.com/lukaszraczylo/ask v0.0.0-20240916204100-6e9ef53a62d9 h1:pL8B9mjv6RPUf
|
||||
github.com/lukaszraczylo/ask v0.0.0-20240916204100-6e9ef53a62d9/go.mod h1:M+UVdyqZs++xtEPrascaVmZdOMhCnxjZ2SgH+xHpR0c=
|
||||
github.com/lukaszraczylo/go-ratecounter v0.1.12 h1:VO6hHYGw/Jy9JUizXf/bS0AI2QX1ueWWAWckMFVJ/w4=
|
||||
github.com/lukaszraczylo/go-ratecounter v0.1.12/go.mod h1:TqXEOCtFJStk1i0tkipprv1kiDHGon1MVUisjSTBSKM=
|
||||
github.com/lukaszraczylo/go-simple-graphql v1.2.29 h1:Fo/3SN4vrST1pyX1UJ5Nd+pQCkurZNJSck4pyx5B/Fk=
|
||||
github.com/lukaszraczylo/go-simple-graphql v1.2.29/go.mod h1:kCvRu01tLxj0iKash5qwL7Em+SltQmZ82bs0yu2aOrk=
|
||||
github.com/lukaszraczylo/go-simple-graphql v1.2.33 h1:77znJSjPCCbJg8qBjGrx+uRLn/JncBKDDPOwO3I64DM=
|
||||
github.com/lukaszraczylo/go-simple-graphql v1.2.33/go.mod h1:UOhnDW0SGIQglxwCbHLzpCQZ755JAUdQUa6v/c+V3dU=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
@@ -65,8 +65,8 @@ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6T
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4=
|
||||
github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA=
|
||||
github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
|
||||
github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
@@ -79,8 +79,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.56.0 h1:bEZdJev/6LCBlpdORfrLu/WOZXXxvrUQSiyniuaoW8U=
|
||||
github.com/valyala/fasthttp v1.56.0/go.mod h1:sReBt3XZVnudxuLOx4J/fMrJVorWRiWY2koQKgABiVI=
|
||||
github.com/valyala/fasthttp v1.58.0 h1:GGB2dWxSbEprU9j0iMJHgdKYJVDyjrOwF9RE59PbRuE=
|
||||
github.com/valyala/fasthttp v1.58.0/go.mod h1:SYXvHHaFp7QZHGKSHmoMipInhrI5StHrhDTYVEjK/Kw=
|
||||
github.com/valyala/fastrand v1.1.0 h1:f+5HkLW4rsgzdNoleUOB69hyT9IlD2ZQh9GyDMfb5G8=
|
||||
github.com/valyala/fastrand v1.1.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ=
|
||||
github.com/valyala/histogram v1.2.0 h1:wyYGAZZt3CpwUiIb9AU/Zbllg1llXyrtApRS815OLoQ=
|
||||
@@ -93,22 +93,22 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
|
||||
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
||||
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
|
||||
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
|
||||
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
|
||||
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI=
|
||||
golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
|
||||
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
|
||||
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
|
||||
+66
-48
@@ -27,12 +27,18 @@ var (
|
||||
)
|
||||
|
||||
func prepareQueriesAndExemptions() {
|
||||
introspectionAllowedQueries = make(map[string]struct{})
|
||||
allowedUrls = make(map[string]struct{})
|
||||
|
||||
// Process allowed introspection queries
|
||||
for _, q := range cfg.Security.IntrospectionAllowed {
|
||||
introspectionAllowedQueries[strings.ToLower(q)] = struct{}{}
|
||||
cleanQuery := strings.Trim(strings.TrimSpace(q), `"`)
|
||||
introspectionAllowedQueries[strings.ToLower(cleanQuery)] = struct{}{}
|
||||
}
|
||||
|
||||
// Process allowed URLs
|
||||
for _, u := range cfg.Server.AllowURLs {
|
||||
allowedUrls[u] = struct{}{}
|
||||
allowedUrls[u] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,10 +86,6 @@ func parseGraphQLQuery(c *fiber.Ctx) *parseGraphQLQueryResult {
|
||||
if ifNotInTest() {
|
||||
cfg.Monitoring.Increment(libpack_monitoring.MetricsSkipped, nil)
|
||||
}
|
||||
if res.shouldBlock {
|
||||
resultPool.Put(res)
|
||||
return res
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
@@ -96,7 +98,6 @@ func parseGraphQLQuery(c *fiber.Ctx) *parseGraphQLQueryResult {
|
||||
if ifNotInTest() {
|
||||
cfg.Monitoring.Increment(libpack_monitoring.MetricsSkipped, nil)
|
||||
}
|
||||
resultPool.Put(res)
|
||||
return res
|
||||
}
|
||||
|
||||
@@ -109,7 +110,6 @@ func parseGraphQLQuery(c *fiber.Ctx) *parseGraphQLQueryResult {
|
||||
if ifNotInTest() {
|
||||
cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil)
|
||||
}
|
||||
resultPool.Put(res)
|
||||
return res
|
||||
}
|
||||
|
||||
@@ -126,21 +126,11 @@ func parseGraphQLQuery(c *fiber.Ctx) *parseGraphQLQueryResult {
|
||||
}
|
||||
|
||||
if cfg.Server.HostGraphQLReadOnly != "" {
|
||||
if res.operationType == "" {
|
||||
res.activeEndpoint = cfg.Server.HostGraphQLReadOnly
|
||||
} else if res.operationType != "mutation" {
|
||||
if res.operationType == "" || res.operationType != "mutation" {
|
||||
res.activeEndpoint = cfg.Server.HostGraphQLReadOnly
|
||||
}
|
||||
}
|
||||
|
||||
cfg.Logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Endpoint selection",
|
||||
Pairs: map[string]interface{}{
|
||||
"operationType": res.operationType,
|
||||
"selectedEndpoint": res.activeEndpoint,
|
||||
},
|
||||
})
|
||||
|
||||
if res.operationType == "mutation" && cfg.Server.ReadOnlyMode {
|
||||
cfg.Logger.Warning(&libpack_logger.LogMessage{
|
||||
Message: "Mutation blocked - server in read-only mode",
|
||||
@@ -174,8 +164,9 @@ func parseGraphQLQuery(c *fiber.Ctx) *parseGraphQLQueryResult {
|
||||
}
|
||||
|
||||
if cfg.Security.BlockIntrospection {
|
||||
res.shouldBlock = checkSelections(c, oper.GetSelectionSet().Selections)
|
||||
if res.shouldBlock {
|
||||
if checkSelections(c, oper.GetSelectionSet().Selections) {
|
||||
_ = c.Status(403).SendString("Introspection queries are not allowed")
|
||||
res.shouldBlock = true
|
||||
resultPool.Put(res)
|
||||
return res
|
||||
}
|
||||
@@ -186,43 +177,70 @@ func parseGraphQLQuery(c *fiber.Ctx) *parseGraphQLQueryResult {
|
||||
}
|
||||
|
||||
func checkSelections(c *fiber.Ctx, selections []ast.Selection) bool {
|
||||
stack := make([]ast.Selection, len(selections))
|
||||
copy(stack, selections)
|
||||
|
||||
for len(stack) > 0 {
|
||||
var s ast.Selection
|
||||
s, stack = stack[len(stack)-1], stack[:len(stack)-1]
|
||||
|
||||
if field, ok := s.(*ast.Field); ok {
|
||||
if checkIfContainsIntrospection(c, field.Name.Value) {
|
||||
return true
|
||||
for _, s := range selections {
|
||||
switch sel := s.(type) {
|
||||
case *ast.Field:
|
||||
fieldName := strings.ToLower(sel.Name.Value)
|
||||
if _, exists := introspectionQueries[fieldName]; exists {
|
||||
if len(cfg.Security.IntrospectionAllowed) > 0 {
|
||||
_, allowed := introspectionAllowedQueries[fieldName]
|
||||
if !allowed {
|
||||
return true // Block if this field isn't allowed
|
||||
}
|
||||
// Even if this field is allowed, we need to check its nested selections
|
||||
} else {
|
||||
return true // Block if no allowlist exists
|
||||
}
|
||||
}
|
||||
// Always check nested selections
|
||||
if sel.SelectionSet != nil {
|
||||
if checkSelections(c, sel.GetSelectionSet().Selections) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
case *ast.InlineFragment:
|
||||
if sel.SelectionSet != nil {
|
||||
if checkSelections(c, sel.GetSelectionSet().Selections) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
if field.SelectionSet != nil {
|
||||
stack = append(stack, field.GetSelectionSet().Selections...)
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func checkIfContainsIntrospection(c *fiber.Ctx, whatever string) bool {
|
||||
whateverLower := strings.ToLower(whatever)
|
||||
|
||||
if _, exists := introspectionQueries[whateverLower]; exists {
|
||||
if len(cfg.Security.IntrospectionAllowed) > 0 {
|
||||
if _, allowed := introspectionAllowedQueries[whateverLower]; allowed {
|
||||
cfg.Logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Introspection query allowed, passing through",
|
||||
Pairs: map[string]interface{}{"query": whatever},
|
||||
})
|
||||
return false
|
||||
func checkIfContainsIntrospection(c *fiber.Ctx, query string) bool {
|
||||
blocked := false
|
||||
// Try parsing as a complete query first
|
||||
p, err := parser.Parse(parser.ParseParams{Source: query})
|
||||
if err == nil {
|
||||
// It's a complete query, check all selections
|
||||
for _, def := range p.Definitions {
|
||||
if op, ok := def.(*ast.OperationDefinition); ok {
|
||||
if op.SelectionSet != nil {
|
||||
blocked = checkSelections(c, op.GetSelectionSet().Selections)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Not a complete query, check as a field name
|
||||
whateverLower := strings.ToLower(query)
|
||||
if _, exists := introspectionQueries[whateverLower]; exists {
|
||||
if len(cfg.Security.IntrospectionAllowed) > 0 {
|
||||
if _, allowed := introspectionAllowedQueries[whateverLower]; !allowed {
|
||||
blocked = true
|
||||
}
|
||||
} else {
|
||||
blocked = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if blocked {
|
||||
if ifNotInTest() {
|
||||
cfg.Monitoring.Increment(libpack_monitoring.MetricsSkipped, nil)
|
||||
}
|
||||
_ = c.Status(403).SendString("Introspection queries are not allowed")
|
||||
return true
|
||||
}
|
||||
return false
|
||||
return blocked
|
||||
}
|
||||
|
||||
+179
@@ -3,8 +3,12 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/goccy/go-json"
|
||||
fiber "github.com/gofiber/fiber/v2"
|
||||
"github.com/graphql-go/graphql/language/ast"
|
||||
"github.com/graphql-go/graphql/language/parser"
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
@@ -408,6 +412,8 @@ func (suite *Tests) Test_checkIfContainsIntrospection() {
|
||||
{"allowed introspection", "__schema", []string{"__schema"}, false},
|
||||
{"disallowed introspection", "__type", []string{"__schema"}, true},
|
||||
{"non-introspection query", "normalQuery", []string{}, false},
|
||||
{"allowed introspection with deep nesting of __typename", "{__schema {queryType {fields {name description __typename}}}}", []string{"__schema", "__typename"}, false},
|
||||
{"disallowed introspection with deep nesting of __typename", "{__type {queryType {fields {name description __typename}}}}", []string{"__type"}, true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -430,3 +436,176 @@ func createTestContext(body string) *fiber.Ctx {
|
||||
ctx.Request().SetBody([]byte(body))
|
||||
return ctx
|
||||
}
|
||||
|
||||
func (suite *Tests) Test_DeepIntrospectionQueries() {
|
||||
tests := []struct {
|
||||
name string
|
||||
query string
|
||||
allowed []string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "deeply nested single introspection",
|
||||
query: "query { users { profiles { settings { preferences { __typename } } } } }",
|
||||
allowed: []string{},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "multiple nested introspections",
|
||||
query: "query { users { __typename profiles { __schema settings { __type } } } }",
|
||||
allowed: []string{},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "nested with selective allowlist",
|
||||
query: "query { users { __typename profiles { __schema settings { __type } } } }",
|
||||
allowed: []string{"__typename"},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "deeply nested with full allowlist",
|
||||
query: "query { users { __typename profiles { __schema settings { __type } } } }",
|
||||
allowed: []string{"__typename", "__schema", "__type"},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "deeply nested with repeated item from allowlist",
|
||||
query: "query PreloadStaticData {\n scenario {\n id\n name\n __typename\n }\n impact {\n id\n description\n __typename\n }\n likelihood {\n id\n description\n __typename\n }\n consequence {\n name\n __typename\n }\n risk_categories {\n name\n abbreviation\n __typename\n }\n mitigation {\n name\n __typename\n }\n}",
|
||||
allowed: []string{"__type", "__typename"},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "deeply nested with repeated item denied",
|
||||
query: "query PreloadStaticData {\n scenario {\n id\n name\n __typename\n }\n impact {\n id\n description\n __typename\n }\n likelihood {\n id\n description\n __typename\n }\n consequence {\n name\n __typename\n }\n risk_categories {\n name\n abbreviation\n __typename\n }\n mitigation {\n name\n __typename\n }\n}",
|
||||
allowed: []string{},
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
suite.Run(tt.name, func() {
|
||||
cfg.Security.BlockIntrospection = true
|
||||
cfg.Security.IntrospectionAllowed = tt.allowed
|
||||
introspectionAllowedQueries = make(map[string]struct{})
|
||||
for _, q := range tt.allowed {
|
||||
introspectionAllowedQueries[strings.ToLower(q)] = struct{}{}
|
||||
}
|
||||
body := map[string]interface{}{
|
||||
"query": tt.query,
|
||||
}
|
||||
bodyBytes, _ := json.Marshal(body)
|
||||
ctx := fiber.New().AcquireCtx(&fasthttp.RequestCtx{})
|
||||
ctx.Request().SetBody(bodyBytes)
|
||||
parseGraphQLQuery(ctx)
|
||||
if tt.expected {
|
||||
suite.Equal(403, ctx.Response().StatusCode())
|
||||
} else {
|
||||
suite.Equal(200, ctx.Response().StatusCode())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntrospectionQueryHandling(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
blockIntrospection bool
|
||||
allowedQueries []string
|
||||
query string
|
||||
wantBlocked bool
|
||||
}{
|
||||
{
|
||||
name: "allows __typename when in allowed list",
|
||||
blockIntrospection: true,
|
||||
allowedQueries: []string{"__typename"},
|
||||
query: `{
|
||||
users {
|
||||
id
|
||||
name
|
||||
__typename
|
||||
}
|
||||
}`,
|
||||
wantBlocked: false,
|
||||
},
|
||||
{
|
||||
name: "case insensitive matching for allowed queries",
|
||||
blockIntrospection: true,
|
||||
allowedQueries: []string{"__TYPENAME"},
|
||||
query: `{
|
||||
users {
|
||||
__typename
|
||||
}
|
||||
}`,
|
||||
wantBlocked: false,
|
||||
},
|
||||
{
|
||||
name: "blocks other introspection queries",
|
||||
blockIntrospection: true,
|
||||
allowedQueries: []string{"__typename"},
|
||||
query: `{
|
||||
__schema {
|
||||
types {
|
||||
name
|
||||
}
|
||||
}
|
||||
}`,
|
||||
wantBlocked: true,
|
||||
},
|
||||
{
|
||||
name: "allows multiple __typename occurrences",
|
||||
blockIntrospection: true,
|
||||
allowedQueries: []string{"__typename"},
|
||||
query: `{
|
||||
users {
|
||||
__typename
|
||||
posts {
|
||||
__typename
|
||||
}
|
||||
}
|
||||
}`,
|
||||
wantBlocked: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Setup config
|
||||
cfg = &config{
|
||||
Security: struct {
|
||||
IntrospectionAllowed []string
|
||||
BlockIntrospection bool
|
||||
}{
|
||||
IntrospectionAllowed: tt.allowedQueries,
|
||||
BlockIntrospection: tt.blockIntrospection,
|
||||
},
|
||||
}
|
||||
|
||||
// Initialize allowed queries
|
||||
prepareQueriesAndExemptions()
|
||||
|
||||
// Parse query
|
||||
p, err := parser.Parse(parser.ParseParams{Source: tt.query})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse query: %v", err)
|
||||
}
|
||||
|
||||
// Create mock fiber context
|
||||
app := fiber.New()
|
||||
ctx := app.AcquireCtx(&fasthttp.RequestCtx{})
|
||||
defer app.ReleaseCtx(ctx)
|
||||
|
||||
// Check selections
|
||||
var blocked bool
|
||||
for _, def := range p.Definitions {
|
||||
if op, ok := def.(*ast.OperationDefinition); ok {
|
||||
blocked = checkSelections(ctx, op.GetSelectionSet().Selections)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if blocked != tt.wantBlocked {
|
||||
t.Errorf("checkSelections() blocked = %v, want %v", blocked, tt.wantBlocked)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ func parseConfig() {
|
||||
go enableApi()
|
||||
go enableHasuraEventCleaner()
|
||||
})
|
||||
prepareQueriesAndExemptions() // Ensure this function is defined elsewhere
|
||||
prepareQueriesAndExemptions()
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
+121
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -11,6 +12,7 @@ import (
|
||||
libpack_logging "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
||||
assertions "github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
type Tests struct {
|
||||
@@ -138,3 +140,122 @@ func (suite *Tests) Test_getDetailsFromEnv() {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *Tests) TestIntrospectionEnvironmentConfig() {
|
||||
// Save original env vars
|
||||
oldEnv := make(map[string]string)
|
||||
varsToSave := []string{
|
||||
"BLOCK_SCHEMA_INTROSPECTION",
|
||||
"ALLOWED_INTROSPECTION",
|
||||
"GMP_BLOCK_SCHEMA_INTROSPECTION",
|
||||
"GMP_ALLOWED_INTROSPECTION",
|
||||
}
|
||||
for _, env := range varsToSave {
|
||||
if val, exists := os.LookupEnv(env); exists {
|
||||
oldEnv[env] = val
|
||||
os.Unsetenv(env)
|
||||
}
|
||||
}
|
||||
defer func() {
|
||||
// Restore original env vars
|
||||
for k, v := range oldEnv {
|
||||
os.Setenv(k, v)
|
||||
}
|
||||
}()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
envVars map[string]string
|
||||
query string
|
||||
wantBlocked bool
|
||||
wantEndpoint string
|
||||
}{
|
||||
{
|
||||
name: "basic typename allowed",
|
||||
envVars: map[string]string{
|
||||
"BLOCK_SCHEMA_INTROSPECTION": "true",
|
||||
"ALLOWED_INTROSPECTION": "__typename",
|
||||
},
|
||||
query: `{
|
||||
users {
|
||||
id
|
||||
__typename
|
||||
}
|
||||
}`,
|
||||
wantBlocked: false,
|
||||
},
|
||||
{
|
||||
name: "GMP prefix takes precedence",
|
||||
envVars: map[string]string{
|
||||
"BLOCK_SCHEMA_INTROSPECTION": "false",
|
||||
"GMP_BLOCK_SCHEMA_INTROSPECTION": "true",
|
||||
"ALLOWED_INTROSPECTION": "__type",
|
||||
"GMP_ALLOWED_INTROSPECTION": "__typename",
|
||||
},
|
||||
query: `{
|
||||
users {
|
||||
__typename
|
||||
}
|
||||
}`,
|
||||
wantBlocked: false,
|
||||
},
|
||||
{
|
||||
name: "multiple allowed queries",
|
||||
envVars: map[string]string{
|
||||
"BLOCK_SCHEMA_INTROSPECTION": "true",
|
||||
"ALLOWED_INTROSPECTION": "__typename,__schema",
|
||||
},
|
||||
query: `{
|
||||
__schema {
|
||||
types {
|
||||
name
|
||||
__typename
|
||||
}
|
||||
}
|
||||
}`,
|
||||
wantBlocked: false,
|
||||
},
|
||||
{
|
||||
name: "multiple allowed queries with one of them blocked",
|
||||
envVars: map[string]string{
|
||||
"BLOCK_SCHEMA_INTROSPECTION": "true",
|
||||
"ALLOWED_INTROSPECTION": "__schema",
|
||||
},
|
||||
query: `{
|
||||
__schema {
|
||||
types {
|
||||
name
|
||||
__typename
|
||||
}
|
||||
}
|
||||
}`,
|
||||
wantBlocked: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
suite.Run(tt.name, func() {
|
||||
// Set test env vars
|
||||
for k, v := range tt.envVars {
|
||||
os.Setenv(k, v)
|
||||
}
|
||||
|
||||
// Reset global config
|
||||
cfg = nil
|
||||
parseConfig()
|
||||
|
||||
// Create test request
|
||||
app := fiber.New()
|
||||
ctx := app.AcquireCtx(&fasthttp.RequestCtx{})
|
||||
defer app.ReleaseCtx(ctx)
|
||||
ctx.Request().Header.SetMethod("POST")
|
||||
ctx.Request().SetBody([]byte(fmt.Sprintf(`{"query": %q}`, tt.query)))
|
||||
|
||||
result := parseGraphQLQuery(ctx)
|
||||
assert.Equal(tt.wantBlocked, result.shouldBlock)
|
||||
for k := range tt.envVars {
|
||||
os.Unsetenv(k)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user