feat: add database-backed console login

This commit is contained in:
2026-09-21 04:53:54 +08:00
parent 3e1db5e30d
commit a6f4aa229d
17 changed files with 781 additions and 84 deletions

View File

@@ -10,6 +10,7 @@ import (
"os"
"path/filepath"
"reflect"
"strings"
"time"
"git.yuntuo.hk/k2safe/orbit/internal/buildinfo"
@@ -26,13 +27,15 @@ func main() {
}
func run() error {
if len(os.Args) < 2 {
return errors.New("usage: orbit-admin release manifest|bootstrap|verify|observe snapshot|retention inventory|credential status|rotate|revoke|apply")
return errors.New("usage: orbit-admin release manifest|bootstrap|console-user create|verify|observe snapshot|retention inventory|credential status|rotate|revoke|apply")
}
switch os.Args[1] {
case "release":
return runRelease(os.Args[2:])
case "bootstrap":
return runBootstrap(os.Args[2:])
case "console-user":
return runConsoleUser(os.Args[2:])
case "verify":
return runVerify(os.Args[2:])
case "observe":
@@ -42,7 +45,7 @@ func run() error {
case "credential":
return runCredential(os.Args[2:])
}
return errors.New("usage: orbit-admin release manifest|bootstrap|verify|observe snapshot|retention inventory|credential status|rotate|revoke|apply")
return errors.New("usage: orbit-admin release manifest|bootstrap|console-user create|verify|observe snapshot|retention inventory|credential status|rotate|revoke|apply")
}
// releaseManifest 是发布物自身的静态身份清单,不读取数据库、不包含 DSN、凭据或业务数据。
@@ -324,3 +327,78 @@ func runBootstrap(args []string) error {
fmt.Println("Bootstrap committed. Credentials saved in the requested private file; valid for 30 days.")
return nil
}
// runConsoleUser 创建一个控制台登录账号。密码默认从标准输入读取,避免出现在
// shell 历史和进程列表;账号只绑定已有业务凭据,不会生成或输出业务 token。
func runConsoleUser(args []string) error {
if len(args) == 0 || args[0] != "create" {
return errors.New("usage: orbit-admin console-user create --credential-file path --username name [--display-name name] [--password-stdin]")
}
flags := flag.NewFlagSet("console-user create", flag.ContinueOnError)
var namespace, credential, credentialFile, username, displayName, password string
var passwordStdin bool
flags.StringVar(&namespace, "namespace-id", "", "namespace id; can be read from --credential-file")
flags.StringVar(&credential, "credential-id", "", "credential id; can be read from --credential-file")
flags.StringVar(&credentialFile, "credential-file", "", "private bootstrap credential JSON file")
flags.StringVar(&username, "username", "", "console login username")
flags.StringVar(&displayName, "display-name", "", "console display name")
flags.StringVar(&password, "password", "", "password; prefer --password-stdin")
flags.BoolVar(&passwordStdin, "password-stdin", false, "read password from stdin")
if err := flags.Parse(args[1:]); err != nil {
return err
}
if flags.NArg() != 0 || username == "" {
return errors.New("--username and principal information are required")
}
if credentialFile != "" {
raw, err := os.ReadFile(credentialFile)
if err != nil {
return errors.New("credential file could not be read")
}
var file struct {
NamespaceID core.ID `json:"namespace_id"`
CredentialID core.ID `json:"credential_id"`
}
if err := json.Unmarshal(raw, &file); err != nil {
return errors.New("credential file is invalid")
}
if namespace == "" {
namespace = string(file.NamespaceID)
}
if credential == "" {
credential = string(file.CredentialID)
}
}
if passwordStdin {
raw, err := io.ReadAll(io.LimitReader(os.Stdin, 2049))
if err != nil {
return errors.New("password could not be read")
}
password = strings.TrimRight(string(raw), "\r\n")
}
ns, cred := core.ID(namespace), core.ID(credential)
if ns.Validate() != nil || cred.Validate() != nil || password == "" {
return errors.New("valid namespace, credential and password are required")
}
cfg, err := config.Load()
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
db, err := mysql.Open(ctx, cfg.MySQLDSN, 2, 1)
if err != nil {
return errors.New("database unavailable")
}
defer db.Close()
user, err := mysql.NewConsoleAuthStore(db).CreateConsoleUser(ctx, mysql.ConsoleUserInput{NamespaceID: ns, CredentialID: cred, Username: username, DisplayName: displayName, Password: password})
if err != nil {
return err
}
encoded, err := json.Marshal(user)
if err != nil {
return err
}
fmt.Println(string(encoded))
return nil
}

View File

@@ -84,7 +84,8 @@ func run() error {
txStats := mysql.ReadTransactionStats()
metrics.RecordTransactionStats(observability.TransactionStats{Retries: txStats.Retries, Exhausted: txStats.Exhausted})
}
server := httpserver.New(cfg.HTTPAddr, readiness.Check, service.New(mysql.NewTaskStore(db)))
consoleAuth := mysql.NewConsoleAuthStore(db)
server := httpserver.NewWithConsoleAuth(cfg.HTTPAddr, readiness.Check, consoleAuth, service.New(mysql.NewTaskStore(db)))
httpserver.RegisterApplicationResources(server, resources.New(mysql.NewTaskStore(db)))
node, err := core.NewID()
if err != nil {
@@ -120,6 +121,7 @@ func run() error {
httpserver.RegisterSchedules(server, schedule.New(schedules))
httpserver.RegisterWorkflowDefinitions(server, definition.New(mysql.NewWorkflowDefinitionStore(db)))
httpserver.RegisterWorkflows(server, instance.New(mysql.NewWorkflowInstanceStore(db)), control.New(mysql.NewWorkflowControlStore(db)), weavesignal.New(mysql.NewWorkflowSignalStore(db)), recovery.New(mysql.NewWorkflowRecoveryStore(db)))
httpserver.WrapConsoleAuth(server, consoleAuth)
httpserver.WrapMetrics(server, metrics)
workflows, err := mysql.NewWorkflowRuntime(db, node)
if err != nil {