159 lines
6.3 KiB
Go
159 lines
6.3 KiB
Go
package mysql
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
|
|
"git.yuntuo.hk/k2safe/orbit/internal/fault"
|
|
"git.yuntuo.hk/k2safe/orbit/internal/retention"
|
|
core "git.yuntuo.hk/k2safe/orbit/model"
|
|
"git.yuntuo.hk/k2safe/orbit/platform/auth"
|
|
driver "github.com/go-sql-driver/mysql"
|
|
)
|
|
|
|
const retentionPolicyOperation = "retention.policy.update"
|
|
const retentionPolicyColumns = `revision,enabled,idempotency_days,task_history_days,workflow_history_days,event_log_days,delivery_history_days,inbox_history_days,audit_log_days,updated_at`
|
|
|
|
// RetentionPolicyStore 保存命名空间级策略,数据库配置优先于部署默认值。
|
|
// 不缓存配置;节点重启和跨节点读取保持数据库中同一版本。
|
|
type RetentionPolicyStore struct {
|
|
db *sql.DB
|
|
defaults retention.Policy
|
|
}
|
|
|
|
func NewRetentionPolicyStore(db *sql.DB, defaults ...retention.Policy) *RetentionPolicyStore {
|
|
p := retention.Defaults("")
|
|
if len(defaults) > 0 {
|
|
p = defaults[0]
|
|
}
|
|
return &RetentionPolicyStore{db: db, defaults: p}
|
|
}
|
|
|
|
var _ retention.Store = (*RetentionPolicyStore)(nil)
|
|
|
|
func (s *RetentionPolicyStore) defaultPolicy(namespace core.ID) retention.Policy {
|
|
p := s.defaults
|
|
p.NamespaceID = namespace
|
|
p.Source = "default"
|
|
return p
|
|
}
|
|
|
|
type retentionQuery interface {
|
|
QueryRowContext(context.Context, string, ...any) *sql.Row
|
|
}
|
|
|
|
func (s *RetentionPolicyStore) readPolicy(ctx context.Context, db retentionQuery, namespace core.ID, lock bool) (retention.Policy, error) {
|
|
p := s.defaultPolicy(namespace)
|
|
q := `SELECT ` + retentionPolicyColumns + ` FROM orbit_retention_policies WHERE namespace_id=?`
|
|
if lock {
|
|
q += " FOR UPDATE"
|
|
}
|
|
err := db.QueryRowContext(ctx, q, namespace).Scan(&p.Revision, &p.Enabled, &p.IdempotencyDays, &p.TaskHistoryDays, &p.WorkflowHistoryDays, &p.EventLogDays, &p.DeliveryHistoryDays, &p.InboxHistoryDays, &p.AuditLogDays, &p.UpdatedAt)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return p, p.Validate()
|
|
}
|
|
if err != nil {
|
|
return retention.Policy{}, err
|
|
}
|
|
p.Source = "database"
|
|
return p, p.Validate()
|
|
}
|
|
|
|
// GetRetentionPolicy 供经过会话认证的控制台和数据库管理工具读取配置。
|
|
func (s *RetentionPolicyStore) GetRetentionPolicy(ctx context.Context, namespace core.ID) (retention.Policy, error) {
|
|
if namespace.Validate() != nil {
|
|
return retention.Policy{}, fault.Invalid("invalid namespace")
|
|
}
|
|
return s.readPolicy(ctx, s.db, namespace, false)
|
|
}
|
|
|
|
// retentionActor 在策略事务内验证控制台会话。普通 SDK 凭据不能修改保留策略。
|
|
// 管理员必须具备当前命名空间全部队列的管理范围;不添加角色体系或跨命名空间权限。
|
|
func retentionActor(ctx context.Context, tx *sql.Tx, token auth.Token) (core.ID, error) {
|
|
p, err := authenticate(ctx, tx, token)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
all := false
|
|
for _, q := range p.Queues {
|
|
if q == "*" {
|
|
all = true
|
|
}
|
|
}
|
|
if !all || !p.Allows("queues:manage") {
|
|
return "", fault.Forbidden
|
|
}
|
|
var user core.ID
|
|
err = tx.QueryRowContext(ctx, `SELECT u.id FROM orbit_console_sessions s
|
|
JOIN orbit_console_users u ON u.id=s.user_id AND u.namespace_id=s.namespace_id AND u.credential_id=s.credential_id
|
|
WHERE s.namespace_id=? AND s.credential_id=? AND s.token_digest=? AND s.revoked_at IS NULL
|
|
AND s.expires_at>UTC_TIMESTAMP(6) AND u.enabled=TRUE FOR SHARE`, token.NamespaceID, token.CredentialID, token.Digest).Scan(&user)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return "", fault.Unauthorized
|
|
}
|
|
return user, err
|
|
}
|
|
|
|
// UpdateRetentionPolicy 在同一事务中核验版本、保存策略、前后快照、账号身份和幂等回执。
|
|
func (s *RetentionPolicyStore) UpdateRetentionPolicy(ctx context.Context, token auth.Token, key string, in retention.UpdateInput) (retention.Policy, error) {
|
|
if strings.TrimSpace(key) == "" || len(key) > 128 {
|
|
return retention.Policy{}, fault.Invalid("idempotency key must be 1-128 bytes")
|
|
}
|
|
want, err := in.ToPolicy(token.NamespaceID)
|
|
if err != nil {
|
|
return retention.Policy{}, fault.Invalid(err.Error())
|
|
}
|
|
var out retention.Policy
|
|
err = WithTx(ctx, s.db, func(tx *sql.Tx) error {
|
|
user, err := retentionActor(ctx, tx, token)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
identity, cached, err := reserveIdentity(ctx, tx, token.NamespaceID, retentionPolicyOperation, "", key, "retention_policy", token.NamespaceID, in)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if cached != nil {
|
|
return json.Unmarshal(cached, &out)
|
|
}
|
|
current, err := s.readPolicy(ctx, tx, token.NamespaceID, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if current.Revision != in.ExpectedRevision {
|
|
return fault.Conflict
|
|
}
|
|
if current.Revision == 0 {
|
|
_, err = tx.ExecContext(ctx, `INSERT INTO orbit_retention_policies(namespace_id,enabled,idempotency_days,task_history_days,workflow_history_days,event_log_days,delivery_history_days,inbox_history_days,audit_log_days) VALUES (?,?,?,?,?,?,?,?,?)`, token.NamespaceID, want.Enabled, want.IdempotencyDays, want.TaskHistoryDays, want.WorkflowHistoryDays, want.EventLogDays, want.DeliveryHistoryDays, want.InboxHistoryDays, want.AuditLogDays)
|
|
var me *driver.MySQLError
|
|
if errors.As(err, &me) && me.Number == 1062 {
|
|
return fault.Conflict
|
|
}
|
|
} else {
|
|
_, err = tx.ExecContext(ctx, `UPDATE orbit_retention_policies SET revision=revision+1,enabled=?,idempotency_days=?,task_history_days=?,workflow_history_days=?,event_log_days=?,delivery_history_days=?,inbox_history_days=?,audit_log_days=?,updated_at=UTC_TIMESTAMP(6) WHERE namespace_id=? AND revision=?`, want.Enabled, want.IdempotencyDays, want.TaskHistoryDays, want.WorkflowHistoryDays, want.EventLogDays, want.DeliveryHistoryDays, want.InboxHistoryDays, want.AuditLogDays, token.NamespaceID, in.ExpectedRevision)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
out, err = s.readPolicy(ctx, tx, token.NamespaceID, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
auditID, err := core.NewID()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
old, _ := json.Marshal(current)
|
|
updated, _ := json.Marshal(out)
|
|
_, err = tx.ExecContext(ctx, "INSERT INTO orbit_audit_events(namespace_id,id,operation_id,actor_id,action,resource_kind,resource_id,reason,`before`,`after`) VALUES (?,?,?,?,?,'retention_policy',?,?,?,?)", token.NamespaceID, auditID, identity, "console:"+string(user), retentionPolicyOperation, token.NamespaceID, in.Reason, string(old), string(updated))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return saveIdentity(ctx, tx, token.NamespaceID, identity, out)
|
|
})
|
|
return out, err
|
|
}
|