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>
192 lines
5.6 KiB
Go
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
|
|
}
|