mirror of
https://github.com/temporalio/temporal.git
synced 2026-08-31 11:01:53 -07:00
## 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.)
107 lines
3.4 KiB
Go
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
|
|
}
|