Files
orbit/internal/storage/mysql/bootstrap.go

221 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package mysql
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"strings"
"time"
core "git.yuntuo.hk/k2safe/orbit/model"
"git.yuntuo.hk/k2safe/orbit/platform/auth"
"git.yuntuo.hk/k2safe/orbit/pulse/service"
"golang.org/x/crypto/bcrypt"
)
// BootstrapInput 仅供持有数据库管理权限的初始化命令使用,不暴露为公共 HTTP。
type BootstrapInput struct {
ValidFor time.Duration // ValidFor 初始令牌有效时长;0 为长期,定期范围 1 小时至 365 天。
WorkflowAuditAccess bool // WorkflowAuditAccess 显式授予流程操作审计读取;不授予其他管理操作。
WorkflowRunAccess bool // WorkflowRunAccess 显式授予流程启动和读取,不包含发布或管理。
WorkflowControlAccess bool // WorkflowControlAccess 显式授予流程暂停、继续和取消及读取。
WorkflowSignalAccess bool // WorkflowSignalAccess 显式授予流程信号受理及读取。
WorkflowRecoveryAccess bool // WorkflowRecoveryAccess 显式授予有证据的流程人工恢复及读取。
WorkflowAccess bool // WorkflowAccess 显式授予流程定义读取和发布权限,不含实例执行权限。
RecoveryAccess bool // RecoveryAccess 显式授予任务核查、人工重试及操作审计读取权限。
EventAccess bool // EventAccess 显式增加结果订阅查询、管理和消费权限。
Application string // Application 新应用名称,与 ExistingApplicationID 二选一。
Namespace string // Namespace 命名空间名称。
Environment string // Environment 环境名称,必须显式指定。
Queue string // Queue 初始队列名称。
WorkerAccess bool // WorkerAccess 显式增加开发用 Worker 接入及消费权限。
ScheduleAccess bool // ScheduleAccess 显式增加开发用计划查询及管理权限。
ResourceAccess bool // ResourceAccess 显式授予整个应用的额度查询及管理权限。
ExistingApplicationID core.ID // ExistingApplicationID 在指定现有应用下创建命名空间,与 Application 二选一。
ConsoleUsername string // ConsoleUsername 可选的首个控制台管理员用户名;与 ConsolePassword 成对使用。
ConsoleDisplayName string // ConsoleDisplayName 可选的控制台展示名称,空时使用用户名。
ConsolePassword string // ConsolePassword 仅在初始化事务内短暂使用,数据库只保存 bcrypt 哈希。
}
// BootstrapResult 包含一次性交付的原始凭据,禁止记录到日志或提交到版本库。
type BootstrapResult struct {
ApplicationID core.ID `json:"application_id"` // ApplicationID 创建或关联的应用标识。
NamespaceID core.ID `json:"namespace_id"` // NamespaceID 新建隔离范围。
QueueID core.ID `json:"queue_id"` // QueueID 初始队列标识。
CredentialID core.ID `json:"credential_id"` // CredentialID 凭据记录标识。
Token string `json:"token"` // Token 原始秘密,仅保存到受保护交付文件。
ExpiresAt *time.Time `json:"expires_at,omitempty"` // ExpiresAt 初始凭据到期时间,UTC;空为长期。
ConsoleUsername string `json:"console_username,omitempty"` // ConsoleUsername 可选的首个控制台账号,不包含密码。
}
func Bootstrap(ctx context.Context, db *sql.DB, in BootstrapInput, deliver func(BootstrapResult) error) error {
if in.ValidFor != 0 && (in.ValidFor < time.Hour || in.ValidFor > 365*24*time.Hour) {
return fmt.Errorf("validity must be zero or between one hour and one year")
}
for _, v := range []string{in.Namespace, in.Queue} {
if !service.ValidName(v, 128) {
return fmt.Errorf("invalid bootstrap resource name")
}
}
if (in.ExistingApplicationID == "" && !service.ValidName(in.Application, 128)) || (in.ExistingApplicationID != "" && (in.ExistingApplicationID.Validate() != nil || in.Application != "")) {
return fmt.Errorf("exactly one new application name or existing application id is required")
}
if !service.ValidName(in.Environment, 32) || deliver == nil {
return fmt.Errorf("environment and secure delivery are required")
}
if (in.ConsoleUsername == "") != (in.ConsolePassword == "") || (in.ConsoleUsername == "" && in.ConsoleDisplayName != "") {
return fmt.Errorf("console username and password must be provided together")
}
if err := CheckSchema(ctx, db); err != nil {
return err
}
var out BootstrapResult
var err error
out.ApplicationID = in.ExistingApplicationID
if out.ApplicationID == "" {
out.ApplicationID, err = core.NewID()
if err != nil {
return err
}
}
for _, dest := range []*core.ID{&out.NamespaceID, &out.QueueID, &out.CredentialID} {
v, err := core.NewID()
if err != nil {
return err
}
*dest = v
}
if in.ConsoleUsername != "" {
if err := validateConsoleUserInput(ConsoleUserInput{NamespaceID: out.NamespaceID, CredentialID: out.CredentialID, Username: in.ConsoleUsername, DisplayName: in.ConsoleDisplayName, Password: in.ConsolePassword}); err != nil {
return err
}
}
secret, digest, err := auth.Issue(out.NamespaceID, out.CredentialID)
if err != nil {
return err
}
out.Token = secret
permissions := auth.Permissions{Actions: []string{"tasks:submit", "tasks:read", "tasks:cancel", "queues:read", "queues:manage"}, Queues: []string{"*"}}
if in.WorkerAccess {
permissions.Actions = append(permissions.Actions, "workers:connect", "workers:consume")
}
if in.WorkflowAccess {
permissions.Actions = append(permissions.Actions, "workflows:definitions:read", "workflows:definitions:publish")
}
if in.WorkflowAuditAccess {
permissions.Actions = append(permissions.Actions, "workflows:audit:read")
}
if in.WorkflowRunAccess || in.WorkflowControlAccess || in.WorkflowSignalAccess || in.WorkflowRecoveryAccess {
permissions.Actions = append(permissions.Actions, "workflows:read")
}
if in.WorkflowRunAccess {
permissions.Actions = append(permissions.Actions, "workflows:start")
}
if in.WorkflowControlAccess {
permissions.Actions = append(permissions.Actions, "workflows:control")
}
if in.WorkflowSignalAccess {
permissions.Actions = append(permissions.Actions, "workflows:signal")
}
if in.WorkflowRecoveryAccess {
permissions.Actions = append(permissions.Actions, "workflows:recover")
}
if in.ScheduleAccess {
permissions.Actions = append(permissions.Actions, "schedules:read", "schedules:manage")
}
if in.ResourceAccess {
permissions.Actions = append(permissions.Actions, "application:resources:read", "application:resources:manage")
}
if in.EventAccess {
permissions.Actions = append(permissions.Actions, "events:read", "events:manage", "events:consume")
}
if in.RecoveryAccess {
permissions.Actions = append(permissions.Actions, "tasks:review", "tasks:retry", "tasks:audit:read")
}
if err := permissions.Validate(); err != nil {
return err
}
var consolePasswordHash []byte
var consoleUserID core.ID
if in.ConsoleUsername != "" {
consolePasswordHash, err = bcrypt.GenerateFromPassword([]byte(in.ConsolePassword), consolePasswordCost)
if err != nil {
return err
}
consoleUserID, err = core.NewID()
if err != nil {
return err
}
out.ConsoleUsername = in.ConsoleUsername
}
raw, err := json.Marshal(permissions)
if err != nil {
return err
}
return WithTx(ctx, db, func(tx *sql.Tx) error {
now, err := dbNow(ctx, tx)
if err != nil {
return err
}
if in.ValidFor > 0 {
out.ExpiresAt = timePtr(now.Add(in.ValidFor))
}
if in.ExistingApplicationID == "" {
if _, err = tx.ExecContext(ctx, "INSERT INTO orbit_applications(id,name) VALUES (?,?)", out.ApplicationID, in.Application); err != nil {
return err
}
if _, err = tx.ExecContext(ctx, "INSERT INTO orbit_application_resources(application_id) VALUES (?)", out.ApplicationID); err != nil {
return err
}
} else {
var id core.ID
if err := tx.QueryRowContext(ctx, `SELECT a.id FROM orbit_applications a JOIN orbit_application_resources r ON r.application_id=a.id WHERE a.id=? AND a.enabled=TRUE FOR SHARE`, out.ApplicationID).Scan(&id); err != nil {
return err
}
}
if _, err = tx.ExecContext(ctx, "INSERT INTO orbit_namespaces(id,application_id,name,environment) VALUES (?,?,?,?)", out.NamespaceID, out.ApplicationID, in.Namespace, in.Environment); err != nil {
return err
}
if _, err = tx.ExecContext(ctx, "INSERT INTO pulse_queues(namespace_id,id,name,max_concurrency,rate_per_second,policy) VALUES (?,?,?,4,10,'{}')", out.NamespaceID, out.QueueID, in.Queue); err != nil {
return err
}
if _, err = tx.ExecContext(ctx, "INSERT INTO orbit_credentials(namespace_id,id,name,secret_digest,permissions,expires_at) VALUES (?,?,'bootstrap',?,?,?)", out.NamespaceID, out.CredentialID, digest, string(raw), out.ExpiresAt); err != nil {
return err
}
actor, err := databaseActor(ctx, tx)
if err != nil {
return err
}
op, err := core.NewID()
if err != nil {
return err
}
if err := appendAuditActor(ctx, tx, out.NamespaceID, actor, op, "bootstrap.create", out.CredentialID, "database administrator bootstrap", nil, map[string]any{"permissions": permissions, "expires_at": out.ExpiresAt}); err != nil {
return err
}
if in.ConsoleUsername != "" {
name := strings.TrimSpace(in.ConsoleDisplayName)
if name == "" {
name = in.ConsoleUsername
}
if _, err := tx.ExecContext(ctx, `INSERT INTO orbit_console_users
(id,namespace_id,credential_id,username,display_name,password_hash)
VALUES (?,?,?,?,?,?)`, consoleUserID, out.NamespaceID, out.CredentialID, in.ConsoleUsername, name, string(consolePasswordHash)); err != nil {
return err
}
consoleOperationID, err := core.NewID()
if err != nil {
return err
}
if err := appendAuditActor(ctx, tx, out.NamespaceID, actor, consoleOperationID, "console_user.create", consoleUserID, "bootstrap console administrator", nil, map[string]any{
"username": in.ConsoleUsername, "display_name": name, "credential_id": out.CredentialID,
}); err != nil {
return err
}
}
// 交付文件先持久化再提交;提交失败时保留文件以便核对不确定结果。
return deliver(out)
})
}