Skip to content

Commit a52132a

Browse files
authored
Webhook Middleware Phase 2: Validating webhook middleware (#4314)
Webhook middleware phase 2: validate webhook middleware Signed-off-by: Sanskarzz <sanskar.gur@gmail.com>
1 parent 3c7a3b0 commit a52132a

9 files changed

Lines changed: 979 additions & 6 deletions

File tree

docs/server/docs.go

Lines changed: 69 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/server/swagger.json

Lines changed: 69 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/server/swagger.yaml

Lines changed: 56 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/runner/config.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import (
3232
"github.com/stacklok/toolhive/pkg/state"
3333
"github.com/stacklok/toolhive/pkg/telemetry"
3434
"github.com/stacklok/toolhive/pkg/transport/types"
35+
"github.com/stacklok/toolhive/pkg/webhook"
3536
workloadtypes "github.com/stacklok/toolhive/pkg/workloads/types"
3637
)
3738

@@ -194,6 +195,9 @@ type RunConfig struct {
194195
// and the configuration for each middleware.
195196
MiddlewareConfigs []types.MiddlewareConfig `json:"middleware_configs,omitempty" yaml:"middleware_configs,omitempty"`
196197

198+
// ValidatingWebhooks contains the configuration for validating webhook middleware.
199+
ValidatingWebhooks []webhook.Config `json:"validating_webhooks,omitempty" yaml:"validating_webhooks,omitempty"`
200+
197201
// existingPort is the port from an existing workload being updated (not serialized)
198202
// Used during port validation to allow reusing the same port
199203
existingPort int

pkg/runner/middleware.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
headerfwd "github.com/stacklok/toolhive/pkg/transport/middleware"
2020
"github.com/stacklok/toolhive/pkg/transport/types"
2121
"github.com/stacklok/toolhive/pkg/usagemetrics"
22+
"github.com/stacklok/toolhive/pkg/webhook/validating"
2223
)
2324

2425
// GetSupportedMiddlewareFactories returns a map of supported middleware types to their factory functions
@@ -37,6 +38,7 @@ func GetSupportedMiddlewareFactories() map[string]types.MiddlewareFactory {
3738
audit.MiddlewareType: audit.CreateMiddleware,
3839
recovery.MiddlewareType: recovery.CreateMiddleware,
3940
headerfwd.HeaderForwardMiddlewareName: headerfwd.CreateMiddleware,
41+
validating.MiddlewareType: validating.CreateMiddleware,
4042
}
4143
}
4244

@@ -113,6 +115,12 @@ func PopulateMiddlewareConfigs(config *RunConfig) error {
113115
}
114116
middlewareConfigs = append(middlewareConfigs, *mcpParserConfig)
115117

118+
// Validating Webhooks middleware (if configured)
119+
middlewareConfigs, err = addValidatingWebhookMiddleware(middlewareConfigs, config)
120+
if err != nil {
121+
return err
122+
}
123+
116124
// Load application config for global settings
117125
configProvider := cfg.NewDefaultProvider()
118126
appConfig := configProvider.GetConfig()
@@ -197,6 +205,28 @@ func PopulateMiddlewareConfigs(config *RunConfig) error {
197205
return nil
198206
}
199207

208+
// addValidatingWebhookMiddleware configures the validating webhook middleware if any webhooks are defined
209+
func addValidatingWebhookMiddleware(configs []types.MiddlewareConfig, runConfig *RunConfig) ([]types.MiddlewareConfig, error) {
210+
if len(runConfig.ValidatingWebhooks) == 0 {
211+
return configs, nil
212+
}
213+
214+
params := validating.FactoryMiddlewareParams{
215+
MiddlewareParams: validating.MiddlewareParams{
216+
Webhooks: runConfig.ValidatingWebhooks,
217+
},
218+
ServerName: runConfig.Name,
219+
Transport: runConfig.Transport.String(),
220+
}
221+
222+
config, err := types.NewMiddlewareConfig(validating.MiddlewareType, params)
223+
if err != nil {
224+
return nil, fmt.Errorf("failed to create validating webhook middleware config: %w", err)
225+
}
226+
227+
return append(configs, *config), nil
228+
}
229+
200230
// addTokenExchangeMiddleware adds token exchange middleware if configured
201231
func addTokenExchangeMiddleware(
202232
middlewares []types.MiddlewareConfig,

pkg/webhook/types.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,17 +62,17 @@ type TLSConfig struct {
6262
// Config holds the configuration for a single webhook.
6363
type Config struct {
6464
// Name is a unique identifier for this webhook.
65-
Name string `json:"name"`
65+
Name string `json:"name" yaml:"name"`
6666
// URL is the HTTPS endpoint to call.
67-
URL string `json:"url"`
67+
URL string `json:"url" yaml:"url"`
6868
// Timeout is the maximum time to wait for a webhook response.
69-
Timeout time.Duration `json:"timeout"`
69+
Timeout time.Duration `json:"timeout" yaml:"timeout" swaggertype:"primitive,integer"`
7070
// FailurePolicy determines behavior when the webhook call fails.
71-
FailurePolicy FailurePolicy `json:"failure_policy"`
71+
FailurePolicy FailurePolicy `json:"failure_policy" yaml:"failure_policy"`
7272
// TLSConfig holds optional TLS configuration (CA bundles, client certs).
73-
TLSConfig *TLSConfig `json:"tls_config,omitempty"`
73+
TLSConfig *TLSConfig `json:"tls_config,omitempty" yaml:"tls_config,omitempty"`
7474
// HMACSecretRef is an optional reference to an HMAC secret for payload signing.
75-
HMACSecretRef string `json:"hmac_secret_ref,omitempty"`
75+
HMACSecretRef string `json:"hmac_secret_ref,omitempty" yaml:"hmac_secret_ref,omitempty"`
7676
}
7777

7878
// Validate checks that the WebhookConfig has valid required fields.

pkg/webhook/validating/config.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
// Package validating implements a validating webhook middleware for ToolHive.
5+
// It calls external HTTP services to approve or deny MCP requests.
6+
package validating
7+
8+
import (
9+
"fmt"
10+
11+
"github.com/stacklok/toolhive/pkg/webhook"
12+
)
13+
14+
// MiddlewareParams holds the configuration parameters for the validating webhook middleware.
15+
type MiddlewareParams struct {
16+
// Webhooks is the list of validating webhook configurations to call.
17+
// Webhooks are called in configuration order; if any webhook denies the request,
18+
// the request is rejected. All webhooks must allow the request for it to proceed.
19+
Webhooks []webhook.Config `json:"webhooks"`
20+
}
21+
22+
// Validate checks that the MiddlewareParams are valid.
23+
func (p *MiddlewareParams) Validate() error {
24+
if len(p.Webhooks) == 0 {
25+
return fmt.Errorf("validating webhook middleware requires at least one webhook")
26+
}
27+
for i, wh := range p.Webhooks {
28+
if err := wh.Validate(); err != nil {
29+
return fmt.Errorf("webhook[%d] (%q): %w", i, wh.Name, err)
30+
}
31+
}
32+
return nil
33+
}

0 commit comments

Comments
 (0)