Files
DDI/server/cmd/ddi-server/main.go
T
sir_melonchenandClaude Opus 5.5 df25705b6e feat(server): add license set-key command reading the key from stdin
Validates and activates the key like PUT /api/v1/license (same service
call, audited as the system actor) and prints the license state without
the key. Exit codes: 2 bad input, 3 key rejected, 4 license server
unreachable or untrusted (nothing stored).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 00:28:33 +02:00

192 lines
5.6 KiB
Go

// SPDX-License-Identifier: LicenseRef-DDI-Proprietary
// Command ddi-server is the management server of the DDI suite.
package main
import (
"context"
"errors"
"flag"
"fmt"
"io"
"log/slog"
"os"
"os/signal"
"path/filepath"
"syscall"
"git.beugsiud.de/beug.k/DDI/internal/brand"
"git.beugsiud.de/beug.k/DDI/internal/buildinfo"
"git.beugsiud.de/beug.k/DDI/internal/logging"
"git.beugsiud.de/beug.k/DDI/server/internal/app"
"git.beugsiud.de/beug.k/DDI/server/internal/config"
"git.beugsiud.de/beug.k/DDI/server/internal/secrets"
"git.beugsiud.de/beug.k/DDI/server/internal/service"
)
func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
code := run(ctx, os.Args[1:], os.LookupEnv, os.Stdin, os.Stdout, os.Stderr)
stop()
os.Exit(code)
}
func usage(w io.Writer) {
_, _ = fmt.Fprintf(w, `Usage: %[1]s <command> [options]
Commands:
serve run the management server (default)
migrate apply database migrations and exit
bootstrap-admin create the first administrator and print its one-time password
gen-master-key write a new master key file (refuses to overwrite)
license set-key read a license key from stdin and activate it (%[1]s license help)
version print version information
help show this help
Configuration: YAML file from $%[2]s (if set), overridden by
environment variables; see docs/ARCHITECTURE.md.
`, brand.ServerBinary, config.EnvName("CONFIG_FILE"))
}
func run(ctx context.Context, args []string, lookup config.LookupFunc, stdin io.Reader, stdout, stderr io.Writer) int {
cmd := "serve"
if len(args) > 0 {
cmd, args = args[0], args[1:]
}
var err error
switch cmd {
case "serve":
err = cmdServe(ctx, lookup, stderr)
case "migrate":
err = cmdMigrate(ctx, lookup, stderr)
case "bootstrap-admin":
err = cmdBootstrapAdmin(ctx, args, lookup, stdout, stderr)
case "gen-master-key":
err = cmdGenMasterKey(args, lookup, stdout)
case "license":
err = cmdLicense(ctx, args, lookup, stdin, stdout, stderr)
case "version", "--version":
err = buildinfo.Print(stdout, brand.ServerBinary)
case "help", "-h", "--help":
usage(stdout)
default:
_, _ = fmt.Fprintf(stderr, "%s: unknown command %q\n\n", brand.ServerBinary, cmd)
usage(stderr)
return 2
}
if err != nil {
_, _ = fmt.Fprintf(stderr, "%s: %v\n", brand.ServerBinary, err)
if ee, ok := errors.AsType[*exitError](err); ok {
return ee.code
}
return 1
}
return 0
}
// setup loads the configuration and creates the logger.
func setup(lookup config.LookupFunc, logOut io.Writer) (config.Config, *slog.Logger, error) {
path, _ := lookup(config.EnvName("CONFIG_FILE"))
cfg, err := config.Load(path, lookup)
if err != nil {
return config.Config{}, nil, fmt.Errorf("invalid configuration: %w", err)
}
log, err := logging.New(logOut, logging.Options{Level: cfg.Log.Level, Format: cfg.Log.Format})
if err != nil {
return config.Config{}, nil, fmt.Errorf("invalid configuration: %w", err)
}
return cfg, log, nil
}
func cmdServe(ctx context.Context, lookup config.LookupFunc, logOut io.Writer) error {
cfg, log, err := setup(lookup, logOut)
if err != nil {
return err
}
core, err := app.Open(ctx, cfg, log, cfg.AutoMigrate())
if err != nil {
return err
}
defer core.Close()
return app.Serve(ctx, core)
}
func cmdMigrate(ctx context.Context, lookup config.LookupFunc, logOut io.Writer) error {
cfg, log, err := setup(lookup, logOut)
if err != nil {
return err
}
core, err := app.Open(ctx, cfg, log, true)
if err != nil {
return err
}
core.Close()
log.Info("database is up to date")
return nil
}
func cmdBootstrapAdmin(ctx context.Context, args []string, lookup config.LookupFunc, stdout, logOut io.Writer) error {
fs := flag.NewFlagSet("bootstrap-admin", flag.ContinueOnError)
fs.SetOutput(logOut)
username := fs.String("username", "admin", "name of the first administrator")
if err := fs.Parse(args); err != nil {
return err
}
cfg, log, err := setup(lookup, logOut)
if err != nil {
return err
}
core, err := app.Open(ctx, cfg, log, true)
if err != nil {
return err
}
defer core.Close()
pw, err := core.Service.BootstrapAdmin(ctx, *username)
if errors.Is(err, service.ErrAlreadyInitialized) {
return errors.New("users already exist; use the web interface to manage them")
}
if err != nil {
return err
}
_, err = fmt.Fprintf(stdout, "username: %s\npassword: %s\n(one-time password; it must be changed at the first login)\n", *username, pw)
return err
}
func cmdGenMasterKey(args []string, lookup config.LookupFunc, stdout io.Writer) error {
fs := flag.NewFlagSet("gen-master-key", flag.ContinueOnError)
out := fs.String("out", "", "path of the key file (default: configured master_key_file)")
keyID := fs.String("id", "k1", "key ID")
if err := fs.Parse(args); err != nil {
return err
}
path := *out
if path == "" {
cfgPath, _ := lookup(config.EnvName("CONFIG_FILE"))
cfg, err := config.Load(cfgPath, lookup)
if err != nil {
return fmt.Errorf("invalid configuration: %w", err)
}
path = cfg.MasterKeyFile
}
data, err := secrets.GenerateKeyFile(*keyID)
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil {
return err
}
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o400) //nolint:gosec // operator-chosen path
if err != nil {
return fmt.Errorf("create master key file (it must not exist yet): %w", err)
}
if _, err := f.Write(data); err != nil {
_ = f.Close()
return err
}
if err := f.Close(); err != nil {
return err
}
_, err = fmt.Fprintf(stdout, "master key written to %s; back it up, without it encrypted data cannot be recovered\n", path)
return err
}