package mysql import ( "context" "database/sql" "encoding/json" "fmt" "time" core "git.yuntuo.hk/k2safe/orbit/model" "git.yuntuo.hk/k2safe/orbit/platform/auth" "git.yuntuo.hk/k2safe/orbit/pulse/service" ) // BootstrapInput 仅供持有数据库管理权限的初始化命令使用,不暴露为公共 HTTP。 type BootstrapInput struct { 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 二选一。 } // 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"` // ExpiresAt 初始凭据到期时间,UTC。 } func Bootstrap(ctx context.Context, db *sql.DB, in BootstrapInput, deliver func(BootstrapResult) error) error { 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 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 } 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 } 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 } out.ExpiresAt = now.Add(30 * 24 * time.Hour) 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 } op, err := core.NewID() if err != nil { return err } if err := appendAudit(ctx, tx, auth.Token{NamespaceID: out.NamespaceID, CredentialID: out.CredentialID}, op, out.CredentialID, "credential", "bootstrap.create", "database administrator bootstrap", nil, map[string]any{"permissions": permissions, "expires_at": out.ExpiresAt}); err != nil { return err } // 交付文件先持久化再提交;提交失败时保留文件以便核对不确定结果。 return deliver(out) }) }