Files
temporal/common/callbacks/validator.go
Chris Smith 3c5aaed9a1 Move CHASM Link and Callback validators into common (#11697)
## What changed?

- Moves `activity.linkValidator` into `common/links`.
- Moves `callback.Validator` into `common/callbacks`

In addition, this PR performs some minor refactorings for consistency
and clarity.

- Moved some `links.Validator`-specific tests from
`chasm/lib/activity/validator_test.go` elsewhere.
- Introduced a `callbacks.ValidatorConfig` to bundle all of the specific
settings. (Since we'll need to wire 3+ more parameters when updating the
`callbacks.Validator` to support worker callbacks.)

> The singular package names `common/link` or `common/callback` would be
more consistent. But `common/links` already existed, there are other
pluralized ones like `common/enums` or `common/headers`. And IMHO, the
plural seems a little more applicable since the validations are only on
groupings of links or callbacks.

## Why?

The `activity.linkValidator` and `callback.Validator` types are great,
but they aren't able to be used as across other CHASM components as
easily. Moreover, `callback.Validator` uses types that are exposed from
the CHASM `callback` package, it will lead to circular dependencies in
the future. (I'm hitting this now in PRs for landing worker callbacks.)

Moving the `commonpb` protobuf validation into `common/` means we can
better separate the the distinction between validation logic and the
CHASM executions that rely on it.

## How did you test it?
- [x] built
- [x] run locally and tested manually
- [x] covered by existing tests
- [x] added new unit test(s)
- [ ] added new functional test(s)

## Potential risks

This should just be a standard refactoring. There should not be any new
validation checks enabled on codepaths where they weren't already
present. (Or in test cases, we initialize fields of
`callback.ValidatorConfig` that weren't used before.)
2026-08-24 14:47:21 -07:00

107 lines
3.4 KiB
Go

package callbacks
import (
"context"
"fmt"
"strings"
commonpb "go.temporal.io/api/common/v1"
"go.temporal.io/api/serviceerror"
"go.temporal.io/server/common/dynamicconfig"
"google.golang.org/grpc/status"
)
// Validator validates completion callbacks attached to executions (e.g. workflows and standalone activities).
type Validator interface {
Validate(ctx context.Context, namespaceName string, cbs []*commonpb.Callback) error
}
// ValidatorConfig holds the limits a [Validator] enforces.
type ValidatorConfig struct {
MaxCallbacksPerExecution dynamicconfig.IntPropertyFnWithNamespaceFilter
// Nexus-variant limits.
URLMaxLength dynamicconfig.IntPropertyFnWithNamespaceFilter
HeaderMaxSize dynamicconfig.IntPropertyFnWithNamespaceFilter
EndpointRules dynamicconfig.TypedPropertyFnWithNamespaceFilter[AddressMatchRules]
}
func (vc *ValidatorConfig) Validate() error {
var missingFields []string
if vc.MaxCallbacksPerExecution == nil {
missingFields = append(missingFields, "MaxCallbacksPerExecution")
}
if vc.URLMaxLength == nil {
missingFields = append(missingFields, "URLMaxLength")
}
if vc.HeaderMaxSize == nil {
missingFields = append(missingFields, "HeaderMaxSize")
}
if vc.EndpointRules == nil {
missingFields = append(missingFields, "EndpointRules")
}
if len(missingFields) != 0 {
return fmt.Errorf("missing required fields: %v", missingFields)
}
return nil
}
type validator struct {
config ValidatorConfig
}
// NewValidator returns a new Validator.
func NewValidator(config ValidatorConfig) (Validator, error) {
if err := config.Validate(); err != nil {
return nil, err
}
return &validator{config: config}, nil
}
// Validate validates completion callbacks: count, URL length, endpoint allowlist, header size, and normalizes header
// keys to lowercase.
func (v *validator) Validate(_ context.Context, namespaceName string, cbs []*commonpb.Callback) error {
if len(cbs) > v.config.MaxCallbacksPerExecution(namespaceName) {
return serviceerror.NewInvalidArgumentf(
"cannot attach more than %d callbacks to an execution", v.config.MaxCallbacksPerExecution(namespaceName),
)
}
for _, cb := range cbs {
switch variant := cb.GetVariant().(type) {
case *commonpb.Callback_Nexus_:
rawURL := variant.Nexus.GetUrl()
if len(rawURL) > v.config.URLMaxLength(namespaceName) {
return serviceerror.NewInvalidArgumentf(
"invalid url: url length longer than max length allowed of %d", v.config.URLMaxLength(namespaceName),
)
}
if err := v.config.EndpointRules(namespaceName).Validate(rawURL); err != nil {
if s, ok := status.FromError(err); ok {
return serviceerror.NewInvalidArgument(s.Message())
}
return serviceerror.NewInvalidArgument(err.Error())
}
headerSize := 0
lowerCaseHeaders := make(map[string]string, len(variant.Nexus.GetHeader()))
for k, val := range variant.Nexus.GetHeader() {
headerSize += len(k) + len(val)
lowerCaseHeaders[strings.ToLower(k)] = val
}
if headerSize > v.config.HeaderMaxSize(namespaceName) {
return serviceerror.NewInvalidArgumentf(
"invalid header: header size longer than max allowed size of %d", v.config.HeaderMaxSize(namespaceName),
)
}
variant.Nexus.Header = lowerCaseHeaders
case *commonpb.Callback_Internal_:
continue
default:
return serviceerror.NewUnimplemented(fmt.Sprintf("unknown callback variant: %T", variant))
}
}
return nil
}