package preparation import ( "context" "crypto/rand" "encoding/hex" "errors" "fmt" "io" "os" "path/filepath" "strings" "github.com/aritmolab/thothii/tools/tht/internal/authconfig" "github.com/aritmolab/thothii/tools/tht/internal/config" "github.com/aritmolab/thothii/tools/tht/internal/safeio" ) // Credentials creates installation-owned secrets only. External credentials are supplied by // the operator. Existing files are checked and retained so interrupted preparation can resume. func Credentials(ctx context.Context, directory string) error { if err := safeio.ValidatePrivateDirectory(directory); err != nil { return fmt.Errorf("use an existing private preparation directory") } if _, err := safeio.ReadCanonicalPrivateRegular(filepath.Join(directory, "thothii-installation.yaml"), 1<<20); err != nil { return fmt.Errorf("prepare installation documents first") } secrets := filepath.Join(directory, "secrets") if err := safeio.EnsurePrivateDirectory(secrets); err != nil { return fmt.Errorf("secrets directory must be private and operator-owned") } for _, name := range []string{"catalog-runtime-password", "catalog-migrator-password", "admin-password"} { path := filepath.Join(secrets, name) if _, err := os.Lstat(path); err == nil { value, err := safeio.ReadCanonicalPrivateRegular(path, 64<<10) if err != nil || len(strings.TrimSpace(string(value))) < 32 { return fmt.Errorf("existing technical credentials must be private, readable and at least 32 characters; no file was replaced") } continue } else if !errors.Is(err, os.ErrNotExist) { return fmt.Errorf("cannot inspect technical credentials") } value := make([]byte, 32) if _, err := rand.Read(value); err != nil { return fmt.Errorf("cannot generate random credentials") } if err := safeio.WriteCanonicalNewPrivateFile(path, []byte(hex.EncodeToString(value)+"\n"), 0o600); err != nil { return fmt.Errorf("cannot create credential; existing files are retained") } } for name, contents := range map[string]string{"secrets.env": "# Supply the provider credential; never commit this file.\nOPENAI_API_KEY=CHANGE_ME\n", "pi-auth.json": "{}\n"} { path := filepath.Join(secrets, name) if _, err := os.Lstat(path); errors.Is(err, os.ErrNotExist) { if err := safeio.WriteCanonicalNewPrivateFile(path, []byte(contents), 0o600); err != nil { return fmt.Errorf("cannot create external credential template") } } else if _, err := safeio.ReadCanonicalPrivateRegular(path, 64<<10); err != nil { return fmt.Errorf("existing credential template is not private or readable") } } authDirectory := filepath.Join(directory, "auth") if _, err := os.Lstat(filepath.Join(authDirectory, "auth.yaml")); err == nil { if _, _, err := authconfig.Load(authDirectory); err != nil { return fmt.Errorf("existing authentication documents are invalid; repair them explicitly") } return nil } installation := config.Installation{Authentication: config.Authentication{ConfigDirectory: authDirectory}} if authconfig.Run(ctx, installation, []string{"configure", "--mode", "local", "--public-url", "http://localhost:8080", "--admin-user", "admin", "--password-file", filepath.Join(secrets, "admin-password")}, strings.NewReader(""), io.Discard, io.Discard) != 0 { return fmt.Errorf("cannot prepare local administrator; inspect protected auth files and retry without replacing existing credentials") } return nil }