mirror of
https://github.com/siderolabs/omni.git
synced 2026-05-05 06:36:12 +02:00
By default only allow to include files from the same directory where the template file lives. This is to prevent malicious cluster templates that include something like `/etc/passwd`. Fixes: https://github.com/siderolabs/omni/issues/2590 Signed-off-by: Artem Chernyshev <artem.chernyshev@talos-systems.com>
185 lines
4.9 KiB
Go
185 lines
4.9 KiB
Go
// 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 models provides cluster template models (for each sub-document of multi-doc YAML).
|
|
package models
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/cosi-project/runtime/pkg/resource"
|
|
"github.com/cosi-project/runtime/pkg/resource/kvutils"
|
|
"github.com/hashicorp/go-multierror"
|
|
"github.com/siderolabs/gen/pair"
|
|
|
|
"github.com/siderolabs/omni/client/pkg/omni/resources/omni"
|
|
)
|
|
|
|
// Meta is embedded into all template objects.
|
|
type Meta struct {
|
|
Kind string `yaml:"kind"`
|
|
}
|
|
|
|
// TranslateContext is a context for translation.
|
|
type TranslateContext struct {
|
|
// Root restricts file access to a single directory tree. When nil, no restriction is applied.
|
|
Root *os.Root
|
|
LockedMachines map[MachineID]struct{}
|
|
MachineDescriptors map[MachineID]Descriptors
|
|
MachineSetLevelKernelArgs map[MachineID]KernelArgs
|
|
ClusterLevelKernelArgs KernelArgs
|
|
ClusterName string
|
|
}
|
|
|
|
// SystemExtensions is embedded in Cluster, MachineSet and Machine objects.
|
|
type SystemExtensions struct {
|
|
SystemExtensions []string `yaml:"systemExtensions,omitempty"`
|
|
}
|
|
|
|
func (s *SystemExtensions) translate(ctx TranslateContext, nameSuffix string, labels ...pair.Pair[string, string]) []resource.Resource {
|
|
if len(s.SystemExtensions) == 0 {
|
|
return nil
|
|
}
|
|
|
|
configuration := omni.NewExtensionsConfiguration(fmt.Sprintf("schematic-%s", nameSuffix))
|
|
|
|
configuration.Metadata().Labels().Set(omni.LabelCluster, ctx.ClusterName)
|
|
|
|
configuration.Metadata().Labels().Do(func(temp kvutils.TempKV) {
|
|
for _, l := range labels {
|
|
temp.Set(l.F1, l.F2)
|
|
}
|
|
})
|
|
|
|
configuration.TypedSpec().Value.Extensions = s.SystemExtensions
|
|
|
|
return []resource.Resource{
|
|
configuration,
|
|
}
|
|
}
|
|
|
|
// Descriptors are the user descriptors (i.e. Labels, Annotations) to apply to the resource.
|
|
type Descriptors struct {
|
|
// Labels are the user labels to apply to the resource.
|
|
Labels map[string]string `yaml:"labels,omitempty"`
|
|
|
|
// Annotations are the user annotations to apply to the resource.
|
|
Annotations map[string]string `yaml:"annotations,omitempty"`
|
|
}
|
|
|
|
// Validate validates the descriptors.
|
|
func (d *Descriptors) Validate() error {
|
|
var multiErr error
|
|
|
|
for labelKey := range d.Labels {
|
|
if strings.HasPrefix(labelKey, omni.SystemLabelPrefix) {
|
|
multiErr = multierror.Append(multiErr, fmt.Errorf("label %q is invalid: prefix %q is reserved for internal use", labelKey, omni.SystemLabelPrefix))
|
|
}
|
|
}
|
|
|
|
for annotationKey := range d.Annotations {
|
|
if strings.HasPrefix(annotationKey, omni.SystemLabelPrefix) {
|
|
multiErr = multierror.Append(multiErr, fmt.Errorf("annotation %q is invalid: prefix %q is reserved for internal use", annotationKey, omni.SystemLabelPrefix))
|
|
}
|
|
}
|
|
|
|
return multiErr
|
|
}
|
|
|
|
// Apply applies the descriptors to the given resource.
|
|
func (d *Descriptors) Apply(res resource.Resource) {
|
|
for k, v := range d.Labels {
|
|
res.Metadata().Labels().Set(k, v)
|
|
}
|
|
|
|
for k, v := range d.Annotations {
|
|
res.Metadata().Annotations().Set(k, v)
|
|
}
|
|
}
|
|
|
|
// ValidateOptions contains options for model validation.
|
|
type ValidateOptions struct {
|
|
// Root restricts file access to a single directory tree. When nil, no restriction is applied.
|
|
Root *os.Root
|
|
}
|
|
|
|
// resolveForRoot translates a CWD-relative path into a path relative to the root directory.
|
|
func resolveForRoot(root *os.Root, path string) (string, error) {
|
|
absPath, err := filepath.Abs(path)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
rootAbs, err := filepath.Abs(root.Name())
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return filepath.Rel(rootAbs, absPath)
|
|
}
|
|
|
|
// ReadFile reads a file, using root to restrict access when non-nil.
|
|
func ReadFile(root *os.Root, path string) ([]byte, error) {
|
|
if root == nil {
|
|
return os.ReadFile(path)
|
|
}
|
|
|
|
rel, err := resolveForRoot(root, path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return root.ReadFile(rel)
|
|
}
|
|
|
|
// StatFile stats a file, using root to restrict access when non-nil.
|
|
func StatFile(root *os.Root, path string) (os.FileInfo, error) {
|
|
if root == nil {
|
|
return os.Stat(path)
|
|
}
|
|
|
|
rel, err := resolveForRoot(root, path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return root.Stat(rel)
|
|
}
|
|
|
|
// Model is a base interface for cluster templates.
|
|
type Model interface {
|
|
Validate(ValidateOptions) error
|
|
Translate(TranslateContext) ([]resource.Resource, error)
|
|
}
|
|
|
|
var registeredModels = map[string]func() Model{}
|
|
|
|
type model[T any] interface {
|
|
*T
|
|
Model
|
|
}
|
|
|
|
func register[T any, P model[T]](kind string) {
|
|
if _, ok := registeredModels[kind]; ok {
|
|
panic(fmt.Sprintf("model %s already registered", kind))
|
|
}
|
|
|
|
registeredModels[kind] = func() Model {
|
|
return P(new(T))
|
|
}
|
|
}
|
|
|
|
// New creates a model by kind.
|
|
func New(kind string) (Model, error) {
|
|
f, ok := registeredModels[kind]
|
|
if !ok {
|
|
return nil, fmt.Errorf("unknown model kind %q", kind)
|
|
}
|
|
|
|
return f(), nil
|
|
}
|