// This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package extensions import ( "fmt" "io/fs" "os" "path/filepath" "strings" "github.com/blang/semver/v4" "github.com/siderolabs/talos/pkg/machinery/version" ) // ValidationOptions are used to configure the validation process. type ValidationOptions struct { // ValidateContstraints enables validation of the extension constraints. ValidateContstraints bool // ValidateContents enables validation of the extension contents. ValidateContents bool // TalosVersion is the version of Talos to validate against. TalosVersion *semver.Version } // WithValidateConstraints enables validation of the extension constraints. func WithValidateConstraints() ValidationOption { return func(o *ValidationOptions) error { o.ValidateContstraints = true return nil } } // WithValidateContents enables validation of the extension contents. func WithValidateContents() ValidationOption { return func(o *ValidationOptions) error { o.ValidateContents = true return nil } } // WithTalosVersion sets the Talos version to validate against. func WithTalosVersion(version semver.Version) ValidationOption { return func(o *ValidationOptions) error { o.TalosVersion = &version return nil } } // ValidationOption is a function that configures the validation options. type ValidationOption func(*ValidationOptions) error // Validate the extension: compatibility, contents, etc. func (ext *Extension) Validate(opts ...ValidationOption) error { validationOptions := &ValidationOptions{} for _, opt := range opts { if err := opt(validationOptions); err != nil { panic(err) } } if validationOptions.TalosVersion == nil { version, err := semver.ParseTolerant(version.Tag) if err != nil { return err } validationOptions.TalosVersion = &version } if validationOptions.ValidateContstraints { if err := ext.validateConstraints(*validationOptions.TalosVersion); err != nil { return err } } if validationOptions.ValidateContents { return ext.validateContents() } return nil } func (ext *Extension) validateConstraints(talosVersion semver.Version) error { constraint := ext.Manifest.Metadata.Compatibility.Talos.Version if constraint != "" { versionConstraint, err := semver.ParseRange(trim(constraint)) if err != nil { return fmt.Errorf("error parsing Talos version constraint: %w", err) } if !versionConstraint(coreVersion(talosVersion)) { return fmt.Errorf("version constraint %s can't be satisfied with Talos version %s", constraint, talosVersion) } } return nil } // trim removes 'v' symbol anywhere in string if it's located before the number. func trim(constraint string) string { for i := 0; i < len(constraint); i++ { if constraint[i] == 'v' && i+1 < len(constraint) && constraint[i+1] >= '0' && constraint[i+1] <= '9' { constraint = constraint[:i] + constraint[i+1:] } } return constraint } func coreVersion(talosVersion semver.Version) semver.Version { return semver.Version{ Major: talosVersion.Major, Minor: talosVersion.Minor, Patch: talosVersion.Patch, } } //nolint:gocyclo func (ext *Extension) validateContents() error { return filepath.WalkDir(ext.rootfsPath, func(path string, d fs.DirEntry, err error) error { if err != nil { return err } itemPath, err := filepath.Rel(ext.rootfsPath, path) if err != nil { return err } itemPath = filepath.Join("/", itemPath) // check for -------w- if d.Type().Perm()&0o002 > 0 { return fmt.Errorf("world-writeable files are not allowed: %q", itemPath) } // no special files if !d.IsDir() && !d.Type().IsRegular() && d.Type().Type() != os.ModeSymlink { return fmt.Errorf("special files are not allowed: %q", itemPath) } // regular file: check for file path being whitelisted if !d.IsDir() { allowed := false for _, allowedPath := range AllowedPaths { if strings.HasPrefix(itemPath, allowedPath) { _, err = filepath.Rel(allowedPath, itemPath) if err == nil { allowed = true break } } } if !allowed { return fmt.Errorf("path %q is not allowed in extensions", itemPath) } } return nil }) }