omni/internal/backend/grpc/validate.go
Utku Ozdemir 0e76483bab
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-omni-upgrade (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
chore: rekres, bump deps, Go, Talos and k8s versions, satisfy linters
- Bump some deps, namely cosi-runtime and Talos machinery.
- Update `auditState` to implement the new methods in COSI's `state.State`.
- Bump default Talos and Kubernetes versions to their latest.
- Rekres, which brings Go 1.24.5. Also update it in go.mod files.
- Fix linter errors coming from new linters.

Signed-off-by: Utku Ozdemir <utku.ozdemir@siderolabs.com>
2025-07-11 18:23:48 +02:00

158 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
}