feat(auth): integrate authentication with installation lifecycle

This commit is contained in:
2026-08-17 20:21:42 +02:00
parent 0a2c667231
commit 9558eaa508
30 changed files with 756 additions and 120 deletions
+83 -5
View File
@@ -21,7 +21,9 @@ import (
"github.com/aritmolab/thothii/tools/tht/internal/compose"
"github.com/aritmolab/thothii/tools/tht/internal/config"
"github.com/aritmolab/thothii/tools/tht/internal/lifecycle"
"github.com/aritmolab/thothii/tools/tht/internal/safeio"
"github.com/aritmolab/thothii/tools/tht/internal/service"
"gopkg.in/yaml.v3"
)
var (
@@ -130,6 +132,10 @@ func createWithDependencies(ctx context.Context, installation config.Installatio
if err != nil {
return Result{}, fmt.Errorf("installation external secret references could not be read: %w", err)
}
authenticationPaths, err := authenticationConfigFiles(installation)
if err != nil {
return Result{}, err
}
revision, err := dependencies.revision(ctx, installation.ProjectDirectory)
if err != nil {
return Result{}, err
@@ -208,12 +214,12 @@ func createWithDependencies(ctx context.Context, installation config.Installatio
InstallationID: installationID,
CreatedAt: dependencies.now().UTC(),
SourceRevision: revision,
IncludesSecrets: request.IncludeSecrets && len(secretPaths) > 0,
IncludesSecrets: request.IncludeSecrets && len(secretPaths)+len(authenticationPaths) > 0,
ComposeProject: installation.ProjectName(),
Images: images,
Volumes: volumes,
}
if err := writeArchive(ctx, output, reservation, installation, request, secretPaths, manifest, volumes, dependencies); err != nil {
if err := writeArchive(ctx, output, reservation, installation, request, secretPaths, authenticationPaths, manifest, volumes, dependencies); err != nil {
return Result{}, err
}
published = true
@@ -228,8 +234,8 @@ func createWithDependencies(ctx context.Context, installation config.Installatio
maintenanceActive = false
}
result = Result{Path: output}
if request.IncludeSecrets {
result.Warning = "The archive contains external secret files. Protect its custody and access."
if manifest.IncludesSecrets {
result.Warning = "The archive contains external secret files, including authentication configuration. Protect its custody and access."
}
return result, nil
}
@@ -631,7 +637,7 @@ func runCompose(ctx context.Context, installation config.Installation, runner ar
return nil
}
func writeArchive(ctx context.Context, output string, reservation *archiveReservation, installation config.Installation, request CreateRequest, secretPaths []string, manifest Manifest, volumes []VolumeMetadata, dependencies dependencies) (resultErr error) {
func writeArchive(ctx context.Context, output string, reservation *archiveReservation, installation config.Installation, request CreateRequest, secretPaths, authenticationPaths []string, manifest Manifest, volumes []VolumeMetadata, dependencies dependencies) (resultErr error) {
directory := filepath.Dir(output)
temporary, err := os.CreateTemp(directory, ".tht-backup-*.tmp")
if err != nil {
@@ -732,6 +738,23 @@ func writeArchive(ctx context.Context, output string, reservation *archiveReserv
SourcePath: source, SHA256: "sha256:" + hex.EncodeToString(hash.Sum(nil)), Size: size, Sensitive: true,
})
}
for index, source := range authenticationPaths {
name := fmt.Sprintf("authentication-secrets/%03d-%s", index, filepath.Base(source))
if request.IncludeSecrets {
if err := addFile(name, "authentication-configuration", true, source); err != nil {
return err
}
entry := &manifest.Entries[len(manifest.Entries)-1]
entry.Kind, entry.SourcePath, entry.Sensitive = EntryExternalSecret, source, true
continue
}
if filepath.Base(source) != "auth.yaml" {
continue
}
if err := addAuthenticationReference(&manifest, name, source); err != nil {
return err
}
}
for _, volume := range volumes {
headerName := "volumes/" + volume.LogicalName + ".tar"
header := &zip.FileHeader{Name: headerName, Method: zip.Deflate}
@@ -804,6 +827,51 @@ func configurationInputs(installation config.Installation) []configurationInput
return inputs
}
const maxAuthenticationConfigurationBytes = 1 << 20
func authenticationConfigFiles(installation config.Installation) ([]string, error) {
directory := installation.AuthenticationDirectory()
if directory == "" {
return nil, nil
}
if err := safeio.ValidatePrivateDirectory(directory); err != nil {
return nil, errors.New("authentication configuration directory is unavailable or unsafe")
}
authPath := filepath.Join(directory, "auth.yaml")
contents, err := safeio.ReadCanonicalRegular(authPath, maxAuthenticationConfigurationBytes)
if err != nil {
return nil, errors.New("authentication configuration is unavailable or unsafe")
}
var configuration struct {
Mode string `yaml:"mode"`
}
if err := yaml.Unmarshal(contents, &configuration); err != nil || (configuration.Mode != "local" && configuration.Mode != "oidc") {
return nil, errors.New("authentication configuration is unavailable or invalid")
}
paths := []string{authPath}
if configuration.Mode == "local" {
usersPath := filepath.Join(directory, "users.yaml")
if _, err := safeio.ReadCanonicalRegular(usersPath, maxAuthenticationConfigurationBytes); err != nil {
return nil, errors.New("authentication user registry is unavailable or unsafe")
}
paths = append(paths, usersPath)
}
return paths, nil
}
func addAuthenticationReference(manifest *Manifest, name, source string) error {
contents, err := safeio.ReadCanonicalRegular(source, maxAuthenticationConfigurationBytes)
if err != nil {
return errors.New("authentication configuration is unavailable or unsafe")
}
digest := sha256.Sum256(contents)
manifest.Entries = append(manifest.Entries, Entry{
Path: name, Kind: EntrySecretReference, Owner: "authentication-configuration", SourcePath: source,
SHA256: "sha256:" + hex.EncodeToString(digest[:]), Size: int64(len(contents)), Sensitive: true,
})
return nil
}
func volumeArchiveCommand(volume string) []string {
return []string{"run", "--rm", "--network", "none", "--mount", "type=volume,src=" + volume + ",dst=/source,readonly", helperImage, "tar", "--numeric-owner", "-C", "/source", "-cf", "-", "."}
}
@@ -837,6 +905,10 @@ func archivePreservationRoots(installation config.Installation, secretPaths []st
if err != nil || !info.IsDir() {
return errors.New("server preservation root is unavailable")
}
authStateRoot := ""
if variable == "THT_DATA_ROOT" {
authStateRoot = filepath.Join(root, "auth")
}
err = filepath.WalkDir(root, func(path string, entry os.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
@@ -847,6 +919,12 @@ func archivePreservationRoots(installation config.Installation, secretPaths []st
}
return nil
}
if authStateRoot != "" && path == authStateRoot {
if entry.IsDir() {
return filepath.SkipDir
}
return nil
}
if entry.Type()&os.ModeSymlink != 0 {
return errors.New("server preservation root contains a symlink")
}