omni/internal/backend/grpc/validate.go
Artem Chernyshev e407b0ab43
Some checks failed
default / default (push) Has been cancelled
default / e2e-backups (push) Has been cancelled
default / e2e-forced-removal (push) Has been cancelled
default / e2e-scaling (push) Has been cancelled
default / e2e-short (push) Has been cancelled
default / e2e-short-secureboot (push) Has been cancelled
default / e2e-templates (push) Has been cancelled
default / e2e-upgrades (push) Has been cancelled
default / e2e-workload-proxy (push) Has been cancelled
fix: move JSON schema forms validation to backend
AJV library is using unsafe evals inside. When we enabled CSP for Omni,
it got broken.
The simplest way around that is to delegate JSON forms validation to the
backend.

Fixes: https://github.com/siderolabs/omni/issues/1099

Signed-off-by: Artem Chernyshev <artem.chernyshev@talos-systems.com>
2025-04-17 21:48:18 +03:00

159 lines
4.3 KiB
Go

// Copyright (c) 2025 Sidero Labs, Inc.
//
// Use of this software is governed by the Business Source License
// included in the LICENSE file.
package grpc
import (
"context"
"errors"
"fmt"
"path/filepath"
"strings"
"time"
pgpcrypto "github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/santhosh-tekuri/jsonschema/v6"
"github.com/santhosh-tekuri/jsonschema/v6/kind"
authpb "github.com/siderolabs/go-api-signature/api/auth"
"github.com/siderolabs/go-api-signature/pkg/pgp"
"golang.org/x/text/language"
"golang.org/x/text/message"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/siderolabs/omni/client/api/omni/management"
"github.com/siderolabs/omni/internal/pkg/auth"
omnijsonschema "github.com/siderolabs/omni/internal/pkg/jsonschema"
)
type publicKey struct {
expiration time.Time
id string
username string
data []byte
}
// validatePublicKey validates the public key in the request and returns a publicKey.
func validatePublicKey(keypb *authpb.PublicKey, opts ...pgp.ValidationOption) (publicKey, error) {
if keypb.GetPgpData() == nil && keypb.GetWebauthnData() == nil {
return publicKey{}, errors.New("no public key data provided")
}
if keypb.GetWebauthnData() != nil {
return publicKey{}, status.Error(codes.Unimplemented, "unimplemented") // todo: implement webauthn
}
return validatePGPPublicKey(keypb.GetPgpData(), opts...)
}
func validatePGPPublicKey(armored []byte, opts ...pgp.ValidationOption) (publicKey, error) {
pgpKey, err := pgpcrypto.NewKeyFromArmored(string(armored))
if err != nil {
return publicKey{}, err
}
key, err := pgp.NewKey(pgpKey)
if err != nil {
return publicKey{}, err
}
err = key.Validate(opts...)
if err != nil {
return publicKey{}, err
}
if key.IsPrivate() {
return publicKey{}, errors.New("PGP key contains private key")
}
lifetimeSecs := pgpKey.GetEntity().PrimaryIdentity().SelfSignature.KeyLifetimeSecs
if lifetimeSecs == nil {
return publicKey{}, errors.New("PGP key has no expiration")
}
expiration := pgpKey.GetEntity().PrimaryKey.CreationTime.Add(time.Duration(*lifetimeSecs) * time.Second)
return publicKey{
data: armored,
id: pgpKey.GetFingerprint(),
username: pgpKey.GetEntity().PrimaryIdentity().UserId.Name,
expiration: expiration,
}, nil
}
func (s *managementServer) ValidateJSONSchema(ctx context.Context, request *management.ValidateJsonSchemaRequest) (*management.ValidateJsonSchemaResponse, error) {
if _, err := auth.CheckGRPC(ctx, auth.WithValidSignature(true)); err != nil {
return nil, err
}
if len(request.Schema) > 1e6 {
return nil, fmt.Errorf("json schema can not be bigger than 1MB")
}
var err error
schema, err := omnijsonschema.Parse("untitled", request.Schema)
if err != nil {
return nil, err
}
err = omnijsonschema.Validate(request.Data, schema)
if err != nil {
var validationError *jsonschema.ValidationError
if !errors.As(err, &validationError) {
return nil, err
}
res := &management.ValidateJsonSchemaResponse{}
res.Errors = handleValidationErrors(validationError)
return res, nil
}
return &management.ValidateJsonSchemaResponse{}, nil
}
// handleValidationErrors processes the validation errors and appends them to the response.
func handleValidationErrors(validationError *jsonschema.ValidationError) []*management.ValidateJsonSchemaResponse_Error {
var res []*management.ValidateJsonSchemaResponse_Error
formatError := func(k jsonschema.ErrorKind) string {
p := message.NewPrinter(language.English)
return k.LocalizedString(p)
}
for _, nestedError := range validationError.Causes {
schemaPath := nestedError.SchemaURL
dataPath := "/" + strings.Join(nestedError.InstanceLocation, "/")
switch k := nestedError.ErrorKind.(type) {
case *kind.Required:
for _, path := range k.Missing {
res = append(res, &management.ValidateJsonSchemaResponse_Error{
SchemaPath: schemaPath,
Cause: "property is required",
DataPath: filepath.Join(dataPath, path),
})
}
default:
res = append(res, &management.ValidateJsonSchemaResponse_Error{
SchemaPath: schemaPath,
Cause: formatError(k),
DataPath: dataPath,
})
}
if len(nestedError.Causes) > 0 {
// Recursively process any nested errors (if any)
res = append(res, handleValidationErrors(nestedError)...)
}
}
return res
}