Mateusz Urbanek f1cceee8cd
feat: implement authentication support
This feature is Enterprise only (requires BUSL).

Any access to the schematic requires the user to be authenticated
before access.

Moreover, any schematic stores the owner in the schematic, so each
schematic becomes private (owned by the user which created it).

Authentication is configured using a set of usernames and keys
associates with each user (API key).

Co-authored-by: Andrey Smirnov <andrey.smirnov@siderolabs.com>
Signed-off-by: Mateusz Urbanek <mateusz.urbanek@siderolabs.com>
2026-04-20 15:54:31 +02:00

163 lines
4.4 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 artifacts
import (
"archive/tar"
"bytes"
"context"
"fmt"
"io"
"os"
"path/filepath"
"github.com/siderolabs/talos/pkg/machinery/extensions"
"github.com/siderolabs/talos/pkg/machinery/imager/quirks"
"go.yaml.in/yaml/v4"
"github.com/siderolabs/image-factory/pkg/constants"
"github.com/siderolabs/image-factory/pkg/schematic"
)
// GetSchematicExtension returns a path to the tarball with "virtual" extension matching a specified schematic.
func (m *Manager) GetSchematicExtension(ctx context.Context, versionTag string, schematic *schematic.Schematic) (string, error) {
schematicID, err := schematic.ID()
if err != nil {
return "", err
}
cacheID := fmt.Sprintf("%s-%v", schematicID, quirks.New(versionTag).SupportsOverlay())
extensionPath := filepath.Join(m.schematicsPath, cacheID+".tar")
if _, err = os.Stat(extensionPath); err == nil {
// already built
return extensionPath, nil
}
var schematicInfo []byte
if quirks.New(versionTag).SupportsOverlay() {
schematicInfo, err = schematic.Marshal()
if err != nil {
return "", fmt.Errorf("failed to marshal schematic overlay info: %w", err)
}
}
resultCh := m.sf.DoChan(cacheID, func() (any, error) {
return nil, m.buildSchematicExtension(schematicID, extensionPath, schematicInfo)
})
select {
case <-ctx.Done():
return "", ctx.Err()
case result := <-resultCh:
if result.Err != nil {
return "", result.Err
}
return extensionPath, nil
}
}
// schematicExtension builds a "virtual" extension matching a specified schematic.
func schematicExtension(schematicID string, schematicInfo []byte, baseURL string) (io.Reader, error) {
manifest := extensions.Manifest{
Version: "v1alpha1",
Metadata: extensions.Metadata{
Name: constants.SchematicIDExtensionName,
Version: schematicID,
Author: constants.ImageFactoryName + " (" + baseURL + ")",
Description: "Virtual extension which specifies the schematic of the image built with " + constants.SchematicIDExtensionName + ".",
Compatibility: extensions.Compatibility{
Talos: extensions.Constraint{
Version: ">= 1.0.0",
},
},
},
}
if len(schematicInfo) > 0 {
manifest.Metadata.ExtraInfo = string(schematicInfo)
}
manifestBytes, err := yaml.Marshal(manifest)
if err != nil {
return nil, fmt.Errorf("failed to marshal manifest: %w", err)
}
var buf bytes.Buffer
tw := tar.NewWriter(&buf)
if err = tw.WriteHeader(&tar.Header{
Name: "manifest.yaml",
Typeflag: tar.TypeReg,
Mode: 0o644,
Size: int64(len(manifestBytes)),
}); err != nil {
return nil, fmt.Errorf("failed to write manifest header: %w", err)
}
if _, err = tw.Write(manifestBytes); err != nil {
return nil, fmt.Errorf("failed to write manifest: %w", err)
}
for _, path := range []string{
"rootfs/",
"rootfs/usr/",
"rootfs/usr/local/",
"rootfs/usr/local/share/",
"rootfs/usr/local/share/schematic/",
} {
if err = tw.WriteHeader(&tar.Header{
Name: path,
Typeflag: tar.TypeDir,
Mode: 0o755,
}); err != nil {
return nil, fmt.Errorf("failed to write rootfs header: %w", err)
}
}
if err = tw.WriteHeader(&tar.Header{
Name: filepath.Join("rootfs/usr/local/share/schematic", schematicID), // empty file
Typeflag: tar.TypeReg,
Mode: 0o644,
}); err != nil {
return nil, fmt.Errorf("failed to write rootfs header: %w", err)
}
if err = tw.Close(); err != nil {
return nil, fmt.Errorf("failed to close tar writer: %w", err)
}
return &buf, nil
}
// buildSchematicExtension builds a schematic extension tarball.
func (m *Manager) buildSchematicExtension(schematicID, extensionPath string, schematicInfo []byte) error {
tarball, err := schematicExtension(schematicID, schematicInfo, m.options.ExternalURL)
if err != nil {
return fmt.Errorf("failed to build schematic layer: %w", err)
}
f, err := os.Create(extensionPath + ".tmp")
if err != nil {
return fmt.Errorf("failed to create extension tarball: %w", err)
}
defer f.Close() //nolint:errcheck
_, err = io.Copy(f, tarball)
if err != nil {
return fmt.Errorf("failed to write extension tarball: %w", err)
}
if err = os.Rename(extensionPath+".tmp", extensionPath); err != nil {
return fmt.Errorf("failed to rename extension tarball: %w", err)
}
return f.Close()
}