Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions docs/server/docs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

69 changes: 69 additions & 0 deletions docs/server/swagger.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

56 changes: 56 additions & 0 deletions docs/server/swagger.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions pkg/runner/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (
"github.com/stacklok/toolhive/pkg/state"
"github.com/stacklok/toolhive/pkg/telemetry"
"github.com/stacklok/toolhive/pkg/transport/types"
"github.com/stacklok/toolhive/pkg/webhook"
workloadtypes "github.com/stacklok/toolhive/pkg/workloads/types"
)

Expand Down Expand Up @@ -191,6 +192,9 @@ type RunConfig struct {
// and the configuration for each middleware.
MiddlewareConfigs []types.MiddlewareConfig `json:"middleware_configs,omitempty" yaml:"middleware_configs,omitempty"`

// ValidatingWebhooks contains the configuration for validating webhook middleware.
ValidatingWebhooks []webhook.Config `json:"validating_webhooks,omitempty" yaml:"validating_webhooks,omitempty"`

// existingPort is the port from an existing workload being updated (not serialized)
// Used during port validation to allow reusing the same port
existingPort int
Expand Down
30 changes: 30 additions & 0 deletions pkg/runner/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
headerfwd "github.com/stacklok/toolhive/pkg/transport/middleware"
"github.com/stacklok/toolhive/pkg/transport/types"
"github.com/stacklok/toolhive/pkg/usagemetrics"
"github.com/stacklok/toolhive/pkg/webhook/validating"
)

// GetSupportedMiddlewareFactories returns a map of supported middleware types to their factory functions
Expand All @@ -37,6 +38,7 @@ func GetSupportedMiddlewareFactories() map[string]types.MiddlewareFactory {
audit.MiddlewareType: audit.CreateMiddleware,
recovery.MiddlewareType: recovery.CreateMiddleware,
headerfwd.HeaderForwardMiddlewareName: headerfwd.CreateMiddleware,
validating.MiddlewareType: validating.CreateMiddleware,
}
}

Expand Down Expand Up @@ -113,6 +115,12 @@ func PopulateMiddlewareConfigs(config *RunConfig) error {
}
middlewareConfigs = append(middlewareConfigs, *mcpParserConfig)

// Validating Webhooks middleware (if configured)
middlewareConfigs, err = addValidatingWebhookMiddleware(middlewareConfigs, config)
if err != nil {
return err
}

// Load application config for global settings
configProvider := cfg.NewDefaultProvider()
appConfig := configProvider.GetConfig()
Expand Down Expand Up @@ -197,6 +205,28 @@ func PopulateMiddlewareConfigs(config *RunConfig) error {
return nil
}

// addValidatingWebhookMiddleware configures the validating webhook middleware if any webhooks are defined
func addValidatingWebhookMiddleware(configs []types.MiddlewareConfig, runConfig *RunConfig) ([]types.MiddlewareConfig, error) {
if len(runConfig.ValidatingWebhooks) == 0 {
return configs, nil
}

params := validating.FactoryMiddlewareParams{
MiddlewareParams: validating.MiddlewareParams{
Webhooks: runConfig.ValidatingWebhooks,
},
ServerName: runConfig.Name,
Transport: runConfig.Transport.String(),
}

config, err := types.NewMiddlewareConfig(validating.MiddlewareType, params)
if err != nil {
return nil, fmt.Errorf("failed to create validating webhook middleware config: %w", err)
}

return append(configs, *config), nil
}

// addTokenExchangeMiddleware adds token exchange middleware if configured
func addTokenExchangeMiddleware(
middlewares []types.MiddlewareConfig,
Expand Down
12 changes: 6 additions & 6 deletions pkg/webhook/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,17 +62,17 @@ type TLSConfig struct {
// Config holds the configuration for a single webhook.
type Config struct {
// Name is a unique identifier for this webhook.
Name string `json:"name"`
Name string `json:"name" yaml:"name"`
// URL is the HTTPS endpoint to call.
URL string `json:"url"`
URL string `json:"url" yaml:"url"`
// Timeout is the maximum time to wait for a webhook response.
Timeout time.Duration `json:"timeout"`
Timeout time.Duration `json:"timeout" yaml:"timeout" swaggertype:"primitive,integer"`
// FailurePolicy determines behavior when the webhook call fails.
FailurePolicy FailurePolicy `json:"failure_policy"`
FailurePolicy FailurePolicy `json:"failure_policy" yaml:"failure_policy"`
// TLSConfig holds optional TLS configuration (CA bundles, client certs).
TLSConfig *TLSConfig `json:"tls_config,omitempty"`
TLSConfig *TLSConfig `json:"tls_config,omitempty" yaml:"tls_config,omitempty"`
// HMACSecretRef is an optional reference to an HMAC secret for payload signing.
HMACSecretRef string `json:"hmac_secret_ref,omitempty"`
HMACSecretRef string `json:"hmac_secret_ref,omitempty" yaml:"hmac_secret_ref,omitempty"`
}

// Validate checks that the WebhookConfig has valid required fields.
Expand Down
33 changes: 33 additions & 0 deletions pkg/webhook/validating/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc.
// SPDX-License-Identifier: Apache-2.0

// Package validating implements a validating webhook middleware for ToolHive.
// It calls external HTTP services to approve or deny MCP requests.
package validating

import (
"fmt"

"github.com/stacklok/toolhive/pkg/webhook"
)

// MiddlewareParams holds the configuration parameters for the validating webhook middleware.
type MiddlewareParams struct {
// Webhooks is the list of validating webhook configurations to call.
// Webhooks are called in configuration order; if any webhook denies the request,
// the request is rejected. All webhooks must allow the request for it to proceed.
Webhooks []webhook.Config `json:"webhooks"`
}

// Validate checks that the MiddlewareParams are valid.
func (p *MiddlewareParams) Validate() error {
if len(p.Webhooks) == 0 {
return fmt.Errorf("validating webhook middleware requires at least one webhook")
}
for i, wh := range p.Webhooks {
if err := wh.Validate(); err != nil {
return fmt.Errorf("webhook[%d] (%q): %w", i, wh.Name, err)
}
}
return nil
}
Loading
Loading