Bound GraphQL request cost to prevent alias-flooding DoS
The GraphQL endpoint built its gqlgen server with bare handler.New and no limits, so a single request with thousands of aliased resolver calls was parsed, validated, executed, and marshalled in full. Under load this let an unauthenticated client drive excessive CPU and memory use against POST /api/connect/v1/graphql and the console and trust endpoints, which share the same constructor (GHSA-prh2-g8pv-m7p9). Add configurable guards in the shared gqlutils.NewHandler: a parser token limit rejects oversized queries at lex time before any execution, a fixed complexity limit caps field-selection count, an LRU query cache avoids repeated parsing, and field suggestions are disabled. The limits flow from a new APIConfig.GraphQL section through server and api config into all three GraphQL handlers, with PROBOD_API_GRAPHQL_* env vars and Helm values exposed for per-environment tuning. Defaults are sized with generous headroom over real traffic: the parser token limit (15000) and complexity limit (2000) sit far above the largest legitimate frontend query yet well below the proof-of-concept flood, so normal usage is unaffected while floods are rejected cheaply. Signed-off-by: Bryan Frimin <bryan@probo.com>
This commit is contained in:
@@ -75,6 +75,14 @@ spec:
|
|||||||
value: ":{{ .Values.service.port }}"
|
value: ":{{ .Values.service.port }}"
|
||||||
- name: PROBOD_API_CORS_ALLOWED_ORIGINS
|
- name: PROBOD_API_CORS_ALLOWED_ORIGINS
|
||||||
value: {{ join "," .Values.probo.cors.allowedOrigins | quote }}
|
value: {{ join "," .Values.probo.cors.allowedOrigins | quote }}
|
||||||
|
- name: PROBOD_API_GRAPHQL_PARSER_TOKEN_LIMIT
|
||||||
|
value: {{ .Values.probo.graphql.parserTokenLimit | quote }}
|
||||||
|
- name: PROBOD_API_GRAPHQL_COMPLEXITY_LIMIT
|
||||||
|
value: {{ .Values.probo.graphql.complexityLimit | quote }}
|
||||||
|
- name: PROBOD_API_GRAPHQL_QUERY_CACHE_SIZE
|
||||||
|
value: {{ .Values.probo.graphql.queryCacheSize | quote }}
|
||||||
|
- name: PROBOD_API_GRAPHQL_DISABLE_SUGGESTION
|
||||||
|
value: {{ .Values.probo.graphql.disableSuggestion | quote }}
|
||||||
# PostgreSQL Database
|
# PostgreSQL Database
|
||||||
- name: PROBOD_PG_ADDR
|
- name: PROBOD_PG_ADDR
|
||||||
value: {{ printf "%s:%v" (include "probo.postgresql.host" .) (include "probo.postgresql.port" .) | quote }}
|
value: {{ printf "%s:%v" (include "probo.postgresql.host" .) (include "probo.postgresql.port" .) | quote }}
|
||||||
|
|||||||
@@ -111,6 +111,15 @@ probo:
|
|||||||
allowedOrigins:
|
allowedOrigins:
|
||||||
- "https://probo.example.com"
|
- "https://probo.example.com"
|
||||||
|
|
||||||
|
# GraphQL request-cost guards (application-layer DoS protection).
|
||||||
|
# Defaults are production-safe; tune only if a legitimate query is rejected
|
||||||
|
# or to enable complexity analysis. A value of 0 disables the guard.
|
||||||
|
graphql:
|
||||||
|
parserTokenLimit: 15000
|
||||||
|
complexityLimit: 2000
|
||||||
|
queryCacheSize: 1000
|
||||||
|
disableSuggestion: true
|
||||||
|
|
||||||
# Extra HTTP headers to add to responses
|
# Extra HTTP headers to add to responses
|
||||||
extraHeaderFields: {}
|
extraHeaderFields: {}
|
||||||
# X-Custom-Header: "custom-value"
|
# X-Custom-Header: "custom-value"
|
||||||
|
|||||||
@@ -201,6 +201,20 @@ probo:
|
|||||||
- "https://probo.example.com"
|
- "https://probo.example.com"
|
||||||
- "http://probo.example.com"
|
- "http://probo.example.com"
|
||||||
|
|
||||||
|
# GraphQL request-cost guards (application-layer DoS protection).
|
||||||
|
# A value of 0 disables the corresponding guard.
|
||||||
|
graphql:
|
||||||
|
# Maximum number of lexer tokens accepted per query. Oversized queries
|
||||||
|
# (e.g. alias flooding) are rejected at parse time before execution.
|
||||||
|
parserTokenLimit: 15000
|
||||||
|
# Maximum query complexity (field-selection count). 0 disables complexity
|
||||||
|
# analysis.
|
||||||
|
complexityLimit: 2000
|
||||||
|
# Size of the LRU cache of parsed query documents.
|
||||||
|
queryCacheSize: 1000
|
||||||
|
# Disable field suggestions on invalid queries.
|
||||||
|
disableSuggestion: true
|
||||||
|
|
||||||
# Show Probo branding
|
# Show Probo branding
|
||||||
branding: true
|
branding: true
|
||||||
|
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -12,6 +12,7 @@ require (
|
|||||||
github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.53.4
|
github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.53.4
|
||||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.103.2
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.103.2
|
||||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.42.3
|
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.42.3
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/ssm v1.69.3
|
||||||
github.com/brianvoe/gofakeit/v7 v7.15.0
|
github.com/brianvoe/gofakeit/v7 v7.15.0
|
||||||
github.com/charmbracelet/huh v1.0.0
|
github.com/charmbracelet/huh v1.0.0
|
||||||
github.com/charmbracelet/lipgloss v1.1.0
|
github.com/charmbracelet/lipgloss v1.1.0
|
||||||
@@ -60,7 +61,6 @@ require (
|
|||||||
github.com/atotto/clipboard v0.1.4 // indirect
|
github.com/atotto/clipboard v0.1.4 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.28 // indirect
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.28 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/signin v1.1.4 // indirect
|
github.com/aws/aws-sdk-go-v2/service/signin v1.1.4 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.69.3 // indirect
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.31.2 // indirect
|
github.com/aws/aws-sdk-go-v2/service/sso v1.31.2 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.5 // indirect
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.5 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.43.2 // indirect
|
github.com/aws/aws-sdk-go-v2/service/sts v1.43.2 // indirect
|
||||||
|
|||||||
@@ -79,6 +79,12 @@ func (b *Builder) Build() (*probodconfig.FullConfig, error) {
|
|||||||
AllowedOrigins: b.parseOriginsList(b.resolver.getEnvOrDefault("PROBOD_API_CORS_ALLOWED_ORIGINS", "http://localhost:8080")),
|
AllowedOrigins: b.parseOriginsList(b.resolver.getEnvOrDefault("PROBOD_API_CORS_ALLOWED_ORIGINS", "http://localhost:8080")),
|
||||||
},
|
},
|
||||||
ExtraHeaderFields: make(map[string]string),
|
ExtraHeaderFields: make(map[string]string),
|
||||||
|
GraphQL: probodconfig.GraphQLConfig{
|
||||||
|
ParserTokenLimit: b.resolver.getEnvIntOrDefault("PROBOD_API_GRAPHQL_PARSER_TOKEN_LIMIT", 15000),
|
||||||
|
ComplexityLimit: b.resolver.getEnvIntOrDefault("PROBOD_API_GRAPHQL_COMPLEXITY_LIMIT", 2000),
|
||||||
|
QueryCacheSize: b.resolver.getEnvIntOrDefault("PROBOD_API_GRAPHQL_QUERY_CACHE_SIZE", 1000),
|
||||||
|
DisableSuggestion: b.resolver.getEnvBoolOrDefault("PROBOD_API_GRAPHQL_DISABLE_SUGGESTION", true),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Pg: probodconfig.PgConfig{
|
Pg: probodconfig.PgConfig{
|
||||||
Addr: b.resolver.getEnvOrDefault("PROBOD_PG_ADDR", "localhost:5432"),
|
Addr: b.resolver.getEnvOrDefault("PROBOD_PG_ADDR", "localhost:5432"),
|
||||||
|
|||||||
@@ -146,6 +146,10 @@ func TestBuilder_Build_Defaults(t *testing.T) {
|
|||||||
assert.Equal(t, ":8080", cfg.Probod.Api.Addr)
|
assert.Equal(t, ":8080", cfg.Probod.Api.Addr)
|
||||||
assert.Nil(t, cfg.Probod.Api.ProxyProtocol.TrustedProxies)
|
assert.Nil(t, cfg.Probod.Api.ProxyProtocol.TrustedProxies)
|
||||||
assert.Equal(t, []string{"http://localhost:8080"}, cfg.Probod.Api.Cors.AllowedOrigins)
|
assert.Equal(t, []string{"http://localhost:8080"}, cfg.Probod.Api.Cors.AllowedOrigins)
|
||||||
|
assert.Equal(t, 15000, cfg.Probod.Api.GraphQL.ParserTokenLimit)
|
||||||
|
assert.Equal(t, 2000, cfg.Probod.Api.GraphQL.ComplexityLimit)
|
||||||
|
assert.Equal(t, 1000, cfg.Probod.Api.GraphQL.QueryCacheSize)
|
||||||
|
assert.True(t, cfg.Probod.Api.GraphQL.DisableSuggestion)
|
||||||
|
|
||||||
// PG config
|
// PG config
|
||||||
assert.Equal(t, "localhost:5432", cfg.Probod.Pg.Addr)
|
assert.Equal(t, "localhost:5432", cfg.Probod.Pg.Addr)
|
||||||
@@ -295,6 +299,10 @@ func TestBuilder_Build_CustomValues(t *testing.T) {
|
|||||||
env["PROBOD_API_ADDR"] = "0.0.0.0:8080"
|
env["PROBOD_API_ADDR"] = "0.0.0.0:8080"
|
||||||
env["PROBOD_API_CORS_ALLOWED_ORIGINS"] = "https://app.example.com,https://admin.example.com"
|
env["PROBOD_API_CORS_ALLOWED_ORIGINS"] = "https://app.example.com,https://admin.example.com"
|
||||||
env["PROBOD_API_PROXY_PROTOCOL_TRUSTED_PROXIES"] = "10.0.0.1,10.0.0.2"
|
env["PROBOD_API_PROXY_PROTOCOL_TRUSTED_PROXIES"] = "10.0.0.1,10.0.0.2"
|
||||||
|
env["PROBOD_API_GRAPHQL_PARSER_TOKEN_LIMIT"] = "20000"
|
||||||
|
env["PROBOD_API_GRAPHQL_COMPLEXITY_LIMIT"] = "5000"
|
||||||
|
env["PROBOD_API_GRAPHQL_QUERY_CACHE_SIZE"] = "2000"
|
||||||
|
env["PROBOD_API_GRAPHQL_DISABLE_SUGGESTION"] = "false"
|
||||||
// PG
|
// PG
|
||||||
env["PROBOD_PG_ADDR"] = "postgres.example.com:5432"
|
env["PROBOD_PG_ADDR"] = "postgres.example.com:5432"
|
||||||
env["PROBOD_PG_USERNAME"] = "probo"
|
env["PROBOD_PG_USERNAME"] = "probo"
|
||||||
@@ -425,6 +433,10 @@ func TestBuilder_Build_CustomValues(t *testing.T) {
|
|||||||
assert.Equal(t, "0.0.0.0:8080", cfg.Probod.Api.Addr)
|
assert.Equal(t, "0.0.0.0:8080", cfg.Probod.Api.Addr)
|
||||||
assert.Equal(t, []string{"10.0.0.1", "10.0.0.2"}, cfg.Probod.Api.ProxyProtocol.TrustedProxies)
|
assert.Equal(t, []string{"10.0.0.1", "10.0.0.2"}, cfg.Probod.Api.ProxyProtocol.TrustedProxies)
|
||||||
assert.Equal(t, []string{"https://app.example.com", "https://admin.example.com"}, cfg.Probod.Api.Cors.AllowedOrigins)
|
assert.Equal(t, []string{"https://app.example.com", "https://admin.example.com"}, cfg.Probod.Api.Cors.AllowedOrigins)
|
||||||
|
assert.Equal(t, 20000, cfg.Probod.Api.GraphQL.ParserTokenLimit)
|
||||||
|
assert.Equal(t, 5000, cfg.Probod.Api.GraphQL.ComplexityLimit)
|
||||||
|
assert.Equal(t, 2000, cfg.Probod.Api.GraphQL.QueryCacheSize)
|
||||||
|
assert.False(t, cfg.Probod.Api.GraphQL.DisableSuggestion)
|
||||||
// PG
|
// PG
|
||||||
assert.Equal(t, "postgres.example.com:5432", cfg.Probod.Pg.Addr)
|
assert.Equal(t, "postgres.example.com:5432", cfg.Probod.Pg.Addr)
|
||||||
assert.Equal(t, "probo", cfg.Probod.Pg.Username)
|
assert.Equal(t, "probo", cfg.Probod.Pg.Username)
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ type (
|
|||||||
TrustCenterConfig = probodconfig.TrustCenterConfig
|
TrustCenterConfig = probodconfig.TrustCenterConfig
|
||||||
APIConfig = probodconfig.APIConfig
|
APIConfig = probodconfig.APIConfig
|
||||||
CorsConfig = probodconfig.CorsConfig
|
CorsConfig = probodconfig.CorsConfig
|
||||||
|
GraphQLConfig = probodconfig.GraphQLConfig
|
||||||
ProxyProtocolConfig = probodconfig.ProxyProtocolConfig
|
ProxyProtocolConfig = probodconfig.ProxyProtocolConfig
|
||||||
AuthConfig = probodconfig.AuthConfig
|
AuthConfig = probodconfig.AuthConfig
|
||||||
OAuth2ServerConfig = probodconfig.OAuth2ServerConfig
|
OAuth2ServerConfig = probodconfig.OAuth2ServerConfig
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ import (
|
|||||||
"go.probo.inc/probo/pkg/riskmanagement"
|
"go.probo.inc/probo/pkg/riskmanagement"
|
||||||
"go.probo.inc/probo/pkg/securecookie"
|
"go.probo.inc/probo/pkg/securecookie"
|
||||||
"go.probo.inc/probo/pkg/server"
|
"go.probo.inc/probo/pkg/server"
|
||||||
|
"go.probo.inc/probo/pkg/server/gqlutils"
|
||||||
"go.probo.inc/probo/pkg/server/trustedproxy"
|
"go.probo.inc/probo/pkg/server/trustedproxy"
|
||||||
"go.probo.inc/probo/pkg/slack"
|
"go.probo.inc/probo/pkg/slack"
|
||||||
"go.probo.inc/probo/pkg/thirdparty"
|
"go.probo.inc/probo/pkg/thirdparty"
|
||||||
@@ -93,6 +94,12 @@ func New() *Implm {
|
|||||||
BaseURL: "http://localhost:8080",
|
BaseURL: "http://localhost:8080",
|
||||||
Api: APIConfig{
|
Api: APIConfig{
|
||||||
Addr: "localhost:8080",
|
Addr: "localhost:8080",
|
||||||
|
GraphQL: GraphQLConfig{
|
||||||
|
ParserTokenLimit: 15000,
|
||||||
|
ComplexityLimit: 2000,
|
||||||
|
QueryCacheSize: 1000,
|
||||||
|
DisableSuggestion: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Pg: PgConfig{
|
Pg: PgConfig{
|
||||||
Addr: "localhost:5432",
|
Addr: "localhost:5432",
|
||||||
@@ -640,6 +647,12 @@ func (impl *Implm) Run(
|
|||||||
ConnectorRegistry: defaultConnectorRegistry,
|
ConnectorRegistry: defaultConnectorRegistry,
|
||||||
ProviderRegistry: providerRegistry,
|
ProviderRegistry: providerRegistry,
|
||||||
BaseURL: baseURL,
|
BaseURL: baseURL,
|
||||||
|
GraphQLLimits: gqlutils.Limits{
|
||||||
|
ParserTokenLimit: impl.cfg.Api.GraphQL.ParserTokenLimit,
|
||||||
|
ComplexityLimit: impl.cfg.Api.GraphQL.ComplexityLimit,
|
||||||
|
QueryCacheSize: impl.cfg.Api.GraphQL.QueryCacheSize,
|
||||||
|
DisableSuggestion: impl.cfg.Api.GraphQL.DisableSuggestion,
|
||||||
|
},
|
||||||
|
|
||||||
CustomDomainCname: impl.cfg.CustomDomains.CnameTarget,
|
CustomDomainCname: impl.cfg.CustomDomains.CnameTarget,
|
||||||
TokenSecret: impl.cfg.Auth.Cookie.Secret,
|
TokenSecret: impl.cfg.Auth.Cookie.Secret,
|
||||||
|
|||||||
@@ -22,9 +22,17 @@ type ProxyProtocolConfig struct {
|
|||||||
TrustedProxies []string `json:"trusted-proxies"`
|
TrustedProxies []string `json:"trusted-proxies"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GraphQLConfig struct {
|
||||||
|
ParserTokenLimit int `json:"parser-token-limit"`
|
||||||
|
ComplexityLimit int `json:"complexity-limit"`
|
||||||
|
QueryCacheSize int `json:"query-cache-size"`
|
||||||
|
DisableSuggestion bool `json:"disable-suggestion"`
|
||||||
|
}
|
||||||
|
|
||||||
type APIConfig struct {
|
type APIConfig struct {
|
||||||
Addr string `json:"addr"`
|
Addr string `json:"addr"`
|
||||||
ProxyProtocol ProxyProtocolConfig `json:"proxy-protocol"`
|
ProxyProtocol ProxyProtocolConfig `json:"proxy-protocol"`
|
||||||
Cors CorsConfig `json:"cors"`
|
Cors CorsConfig `json:"cors"`
|
||||||
ExtraHeaderFields map[string]string `json:"extra-header-fields"`
|
ExtraHeaderFields map[string]string `json:"extra-header-fields"`
|
||||||
|
GraphQL GraphQLConfig `json:"graphql"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ import (
|
|||||||
mcp_v1 "go.probo.inc/probo/pkg/server/api/mcp/v1"
|
mcp_v1 "go.probo.inc/probo/pkg/server/api/mcp/v1"
|
||||||
slack_v1 "go.probo.inc/probo/pkg/server/api/slack/v1"
|
slack_v1 "go.probo.inc/probo/pkg/server/api/slack/v1"
|
||||||
trust_v1 "go.probo.inc/probo/pkg/server/api/trust/v1"
|
trust_v1 "go.probo.inc/probo/pkg/server/api/trust/v1"
|
||||||
|
"go.probo.inc/probo/pkg/server/gqlutils"
|
||||||
"go.probo.inc/probo/pkg/slack"
|
"go.probo.inc/probo/pkg/slack"
|
||||||
"go.probo.inc/probo/pkg/thirdparty"
|
"go.probo.inc/probo/pkg/thirdparty"
|
||||||
"go.probo.inc/probo/pkg/trust"
|
"go.probo.inc/probo/pkg/trust"
|
||||||
@@ -75,6 +76,7 @@ type (
|
|||||||
ConnectorRegistry *connector.ConnectorRegistry
|
ConnectorRegistry *connector.ConnectorRegistry
|
||||||
ProviderRegistry *provider.Registry
|
ProviderRegistry *provider.Registry
|
||||||
CustomDomainCname string
|
CustomDomainCname string
|
||||||
|
GraphQLLimits gqlutils.Limits
|
||||||
Logger *log.Logger
|
Logger *log.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,6 +190,7 @@ func NewServer(cfg Config) (*Server, error) {
|
|||||||
cfg.Cookie,
|
cfg.Cookie,
|
||||||
cfg.TokenSecret,
|
cfg.TokenSecret,
|
||||||
cfg.BaseURL,
|
cfg.BaseURL,
|
||||||
|
cfg.GraphQLLimits,
|
||||||
),
|
),
|
||||||
consoleHandler: console_v1.NewMux(
|
consoleHandler: console_v1.NewMux(
|
||||||
cfg.Logger.Named("console.v1"),
|
cfg.Logger.Named("console.v1"),
|
||||||
@@ -208,6 +211,7 @@ func NewServer(cfg Config) (*Server, error) {
|
|||||||
cfg.CustomDomainCname,
|
cfg.CustomDomainCname,
|
||||||
cfg.ThirdParty,
|
cfg.ThirdParty,
|
||||||
cfg.RiskManagement,
|
cfg.RiskManagement,
|
||||||
|
cfg.GraphQLLimits,
|
||||||
),
|
),
|
||||||
cookieBannerHandler: cookiebanner_v1.NewMux(
|
cookieBannerHandler: cookiebanner_v1.NewMux(
|
||||||
cfg.Logger.Named("cookiebanner.v1"),
|
cfg.Logger.Named("cookiebanner.v1"),
|
||||||
@@ -261,6 +265,7 @@ func NewServer(cfg Config) (*Server, error) {
|
|||||||
_, err := cfg.Trust.GetByDomainName(ctx, host)
|
_, err := cfg.Trust.GetByDomainName(ctx, host)
|
||||||
return err == nil
|
return err == nil
|
||||||
},
|
},
|
||||||
|
cfg.GraphQLLimits,
|
||||||
),
|
),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import (
|
|||||||
"go.probo.inc/probo/pkg/server/gqlutils/directives/session"
|
"go.probo.inc/probo/pkg/server/gqlutils/directives/session"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewGraphQLHandler(svc *iam.Service, logger *log.Logger, fileManagerSvc *filemanager.Service, baseURL *baseurl.BaseURL, cookieConfig securecookie.Config) http.Handler {
|
func NewGraphQLHandler(svc *iam.Service, logger *log.Logger, fileManagerSvc *filemanager.Service, baseURL *baseurl.BaseURL, cookieConfig securecookie.Config, limits gqlutils.Limits) http.Handler {
|
||||||
config := schema.Config{
|
config := schema.Config{
|
||||||
Resolvers: &Resolver{
|
Resolvers: &Resolver{
|
||||||
authorize: authz.NewAuthorizeFunc(svc, logger),
|
authorize: authz.NewAuthorizeFunc(svc, logger),
|
||||||
@@ -49,7 +49,7 @@ func NewGraphQLHandler(svc *iam.Service, logger *log.Logger, fileManagerSvc *fil
|
|||||||
}
|
}
|
||||||
|
|
||||||
es := schema.NewExecutableSchema(config)
|
es := schema.NewExecutableSchema(config)
|
||||||
gqlh := gqlutils.NewHandler(es, logger)
|
gqlh := gqlutils.NewHandler(es, logger, limits)
|
||||||
|
|
||||||
return gqlh
|
return gqlh
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ import (
|
|||||||
"go.probo.inc/probo/pkg/server/api/authn"
|
"go.probo.inc/probo/pkg/server/api/authn"
|
||||||
"go.probo.inc/probo/pkg/server/api/authz"
|
"go.probo.inc/probo/pkg/server/api/authz"
|
||||||
"go.probo.inc/probo/pkg/server/api/connect/v1/types"
|
"go.probo.inc/probo/pkg/server/api/connect/v1/types"
|
||||||
|
"go.probo.inc/probo/pkg/server/gqlutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
@@ -70,6 +71,7 @@ func NewMux(
|
|||||||
baseURL *baseurl.BaseURL,
|
baseURL *baseurl.BaseURL,
|
||||||
allowedRedirectHost saferedirect.AllowedHostFunc,
|
allowedRedirectHost saferedirect.AllowedHostFunc,
|
||||||
isTrustCenterDomain IsTrustCenterDomainFunc,
|
isTrustCenterDomain IsTrustCenterDomainFunc,
|
||||||
|
graphqlLimits gqlutils.Limits,
|
||||||
) *chi.Mux {
|
) *chi.Mux {
|
||||||
r := chi.NewMux()
|
r := chi.NewMux()
|
||||||
|
|
||||||
@@ -77,7 +79,7 @@ func NewMux(
|
|||||||
apiKeyMiddleware := authn.NewAPIKeyMiddleware(svc, tokenSecret)
|
apiKeyMiddleware := authn.NewAPIKeyMiddleware(svc, tokenSecret)
|
||||||
oauth2Middleware := authn.NewOAuth2AccessTokenMiddleware(svc)
|
oauth2Middleware := authn.NewOAuth2AccessTokenMiddleware(svc)
|
||||||
identityPresenceMiddleware := authn.NewIdentityPresenceMiddleware(baseURL)
|
identityPresenceMiddleware := authn.NewIdentityPresenceMiddleware(baseURL)
|
||||||
graphqlHandler := NewGraphQLHandler(svc, logger, fileManagerSvc, baseURL, cookieConfig)
|
graphqlHandler := NewGraphQLHandler(svc, logger, fileManagerSvc, baseURL, cookieConfig, graphqlLimits)
|
||||||
samlHandler := NewSAMLHandler(svc, cookieConfig, baseURL, logger)
|
samlHandler := NewSAMLHandler(svc, cookieConfig, baseURL, logger)
|
||||||
scimHandler := NewSCIMHandler(svc, logger.Named("scim"))
|
scimHandler := NewSCIMHandler(svc, logger.Named("scim"))
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ func NewGraphQLHandler(
|
|||||||
riskManagementSvc *riskmanagement.Service,
|
riskManagementSvc *riskmanagement.Service,
|
||||||
fileManagerSvc *filemanager.Service,
|
fileManagerSvc *filemanager.Service,
|
||||||
baseURL *baseurl.BaseURL,
|
baseURL *baseurl.BaseURL,
|
||||||
|
limits gqlutils.Limits,
|
||||||
) http.Handler {
|
) http.Handler {
|
||||||
config := schema.Config{
|
config := schema.Config{
|
||||||
Resolvers: &Resolver{
|
Resolvers: &Resolver{
|
||||||
@@ -80,7 +81,7 @@ func NewGraphQLHandler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
es := schema.NewExecutableSchema(config)
|
es := schema.NewExecutableSchema(config)
|
||||||
gqlh := gqlutils.NewHandler(es, logger)
|
gqlh := gqlutils.NewHandler(es, logger, limits)
|
||||||
|
|
||||||
return gqlh
|
return gqlh
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ import (
|
|||||||
"go.probo.inc/probo/pkg/server/api/authz"
|
"go.probo.inc/probo/pkg/server/api/authz"
|
||||||
"go.probo.inc/probo/pkg/server/api/console/v1/dataloader"
|
"go.probo.inc/probo/pkg/server/api/console/v1/dataloader"
|
||||||
"go.probo.inc/probo/pkg/server/api/console/v1/types"
|
"go.probo.inc/probo/pkg/server/api/console/v1/types"
|
||||||
|
"go.probo.inc/probo/pkg/server/gqlutils"
|
||||||
"go.probo.inc/probo/pkg/thirdparty"
|
"go.probo.inc/probo/pkg/thirdparty"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -92,6 +93,7 @@ func NewMux(
|
|||||||
customDomainCname string,
|
customDomainCname string,
|
||||||
thirdPartySvc *thirdparty.Service,
|
thirdPartySvc *thirdparty.Service,
|
||||||
riskManagementSvc *riskmanagement.Service,
|
riskManagementSvc *riskmanagement.Service,
|
||||||
|
graphqlLimits gqlutils.Limits,
|
||||||
) *chi.Mux {
|
) *chi.Mux {
|
||||||
r := chi.NewMux()
|
r := chi.NewMux()
|
||||||
|
|
||||||
@@ -114,6 +116,7 @@ func NewMux(
|
|||||||
riskManagementSvc,
|
riskManagementSvc,
|
||||||
fileManagerSvc,
|
fileManagerSvc,
|
||||||
baseURL,
|
baseURL,
|
||||||
|
graphqlLimits,
|
||||||
)
|
)
|
||||||
|
|
||||||
r.Group(func(r chi.Router) {
|
r.Group(func(r chi.Router) {
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ func NewGraphQLHandler(
|
|||||||
baseURL *baseurl.BaseURL,
|
baseURL *baseurl.BaseURL,
|
||||||
cookieConfig securecookie.Config,
|
cookieConfig securecookie.Config,
|
||||||
tokenSecret string,
|
tokenSecret string,
|
||||||
|
limits gqlutils.Limits,
|
||||||
) http.Handler {
|
) http.Handler {
|
||||||
config := schema.Config{
|
config := schema.Config{
|
||||||
Resolvers: &Resolver{
|
Resolvers: &Resolver{
|
||||||
@@ -65,7 +66,7 @@ func NewGraphQLHandler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
es := schema.NewExecutableSchema(config)
|
es := schema.NewExecutableSchema(config)
|
||||||
gqlh := gqlutils.NewHandler(es, logger)
|
gqlh := gqlutils.NewHandler(es, logger, limits)
|
||||||
|
|
||||||
return gqlh
|
return gqlh
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ import (
|
|||||||
"go.probo.inc/probo/pkg/securecookie"
|
"go.probo.inc/probo/pkg/securecookie"
|
||||||
"go.probo.inc/probo/pkg/server/api/authn"
|
"go.probo.inc/probo/pkg/server/api/authn"
|
||||||
"go.probo.inc/probo/pkg/server/api/compliancepage"
|
"go.probo.inc/probo/pkg/server/api/compliancepage"
|
||||||
|
"go.probo.inc/probo/pkg/server/gqlutils"
|
||||||
"go.probo.inc/probo/pkg/trust"
|
"go.probo.inc/probo/pkg/trust"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -85,6 +86,7 @@ func NewMux(
|
|||||||
cookieConfig securecookie.Config,
|
cookieConfig securecookie.Config,
|
||||||
tokenSecret string,
|
tokenSecret string,
|
||||||
baseURL *baseurl.BaseURL,
|
baseURL *baseurl.BaseURL,
|
||||||
|
graphqlLimits gqlutils.Limits,
|
||||||
) *chi.Mux {
|
) *chi.Mux {
|
||||||
r := chi.NewMux()
|
r := chi.NewMux()
|
||||||
|
|
||||||
@@ -112,6 +114,7 @@ func NewMux(
|
|||||||
baseURL,
|
baseURL,
|
||||||
cookieConfig,
|
cookieConfig,
|
||||||
tokenSecret,
|
tokenSecret,
|
||||||
|
graphqlLimits,
|
||||||
)
|
)
|
||||||
|
|
||||||
r.Group(
|
r.Group(
|
||||||
|
|||||||
@@ -21,13 +21,27 @@ import (
|
|||||||
"github.com/99designs/gqlgen/graphql"
|
"github.com/99designs/gqlgen/graphql"
|
||||||
"github.com/99designs/gqlgen/graphql/handler"
|
"github.com/99designs/gqlgen/graphql/handler"
|
||||||
"github.com/99designs/gqlgen/graphql/handler/extension"
|
"github.com/99designs/gqlgen/graphql/handler/extension"
|
||||||
|
"github.com/99designs/gqlgen/graphql/handler/lru"
|
||||||
"github.com/99designs/gqlgen/graphql/handler/transport"
|
"github.com/99designs/gqlgen/graphql/handler/transport"
|
||||||
|
"github.com/vektah/gqlparser/v2/ast"
|
||||||
"go.gearno.de/kit/log"
|
"go.gearno.de/kit/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Handler struct {
|
type (
|
||||||
gqlhandler *handler.Server
|
Handler struct {
|
||||||
}
|
gqlhandler *handler.Server
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limits bounds the per-request cost of a GraphQL operation to protect
|
||||||
|
// against alias-flooding and other application-layer denial-of-service
|
||||||
|
// vectors. A zero value disables the corresponding guard.
|
||||||
|
Limits struct {
|
||||||
|
ParserTokenLimit int
|
||||||
|
ComplexityLimit int
|
||||||
|
QueryCacheSize int
|
||||||
|
DisableSuggestion bool
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
mb int64 = 1024 * 1024
|
mb int64 = 1024 * 1024
|
||||||
@@ -42,13 +56,27 @@ var (
|
|||||||
introspectionExtension = extension.Introspection{}
|
introspectionExtension = extension.Introspection{}
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewHandler[S graphql.ExecutableSchema](executableSchema S, logger *log.Logger) *Handler {
|
func NewHandler[S graphql.ExecutableSchema](executableSchema S, logger *log.Logger, limits Limits) *Handler {
|
||||||
handler := handler.New(executableSchema)
|
handler := handler.New(executableSchema)
|
||||||
|
|
||||||
handler.AddTransport(postTransport)
|
handler.AddTransport(postTransport)
|
||||||
handler.AddTransport(optionsTransport)
|
handler.AddTransport(optionsTransport)
|
||||||
handler.AddTransport(multipartTransport)
|
handler.AddTransport(multipartTransport)
|
||||||
|
|
||||||
|
if limits.QueryCacheSize > 0 {
|
||||||
|
handler.SetQueryCache(lru.New[*ast.QueryDocument](limits.QueryCacheSize))
|
||||||
|
}
|
||||||
|
|
||||||
|
if limits.ParserTokenLimit > 0 {
|
||||||
|
handler.SetParserTokenLimit(limits.ParserTokenLimit)
|
||||||
|
}
|
||||||
|
|
||||||
|
if limits.ComplexityLimit > 0 {
|
||||||
|
handler.Use(extension.FixedComplexityLimit(limits.ComplexityLimit))
|
||||||
|
}
|
||||||
|
|
||||||
|
handler.SetDisableSuggestion(limits.DisableSuggestion)
|
||||||
|
|
||||||
handler.Use(introspectionExtension)
|
handler.Use(introspectionExtension)
|
||||||
handler.Use(NewTracingExtension(logger))
|
handler.Use(NewTracingExtension(logger))
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import (
|
|||||||
"go.probo.inc/probo/pkg/securecookie"
|
"go.probo.inc/probo/pkg/securecookie"
|
||||||
"go.probo.inc/probo/pkg/server/api"
|
"go.probo.inc/probo/pkg/server/api"
|
||||||
"go.probo.inc/probo/pkg/server/api/compliancepage"
|
"go.probo.inc/probo/pkg/server/api/compliancepage"
|
||||||
|
"go.probo.inc/probo/pkg/server/gqlutils"
|
||||||
"go.probo.inc/probo/pkg/server/mailactions"
|
"go.probo.inc/probo/pkg/server/mailactions"
|
||||||
trust_web "go.probo.inc/probo/pkg/server/trust"
|
trust_web "go.probo.inc/probo/pkg/server/trust"
|
||||||
console_web "go.probo.inc/probo/pkg/server/web"
|
console_web "go.probo.inc/probo/pkg/server/web"
|
||||||
@@ -74,6 +75,7 @@ type Config struct {
|
|||||||
ConnectorRegistry *connector.ConnectorRegistry
|
ConnectorRegistry *connector.ConnectorRegistry
|
||||||
ProviderRegistry *provider.Registry
|
ProviderRegistry *provider.Registry
|
||||||
CustomDomainCname string
|
CustomDomainCname string
|
||||||
|
GraphQLLimits gqlutils.Limits
|
||||||
Logger *log.Logger
|
Logger *log.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,6 +116,7 @@ func NewServer(cfg Config) (*Server, error) {
|
|||||||
ConnectorRegistry: cfg.ConnectorRegistry,
|
ConnectorRegistry: cfg.ConnectorRegistry,
|
||||||
ProviderRegistry: cfg.ProviderRegistry,
|
ProviderRegistry: cfg.ProviderRegistry,
|
||||||
CustomDomainCname: cfg.CustomDomainCname,
|
CustomDomainCname: cfg.CustomDomainCname,
|
||||||
|
GraphQLLimits: cfg.GraphQLLimits,
|
||||||
Logger: cfg.Logger.Named("api"),
|
Logger: cfg.Logger.Named("api"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user