Scert/backend/internal/repository/project_domain_repository.go

213 lines
8.5 KiB
Go
Raw Permalink 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 repository
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"time"
"git.pcux.cn/xuans/Scert/backend/internal/domain"
)
var ErrProjectDomainBusy = errors.New("域名存在正在执行的任务,暂不能删除")
var _ ProjectDomainRepository = (*SQLProjectMemberRepository)(nil)
func (r *SQLProjectMemberRepository) CreateProjectDomain(ctx context.Context, actorID string, item domain.Domain) error {
if err := r.ensureDB(); err != nil {
return err
}
item.ResourceScope = item.ResourceScope.Normalize()
challengeType, err := domain.NormalizeChallengeType(item.ChallengeType, append([]string{item.Name}, item.SANs...))
if err != nil {
return err
}
payload, err := json.Marshal(item.SANs)
if err != nil {
return fmt.Errorf("编码域名 SAN 失败:%w", err)
}
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("开始域名创建事务失败:%w", err)
}
defer tx.Rollback()
if err := r.authorizeProjectTransaction(ctx, tx, actorID, item.ProjectID, domain.PermissionCertificateManage); err != nil {
return err
}
if err := r.validateInitialDNSBinding(ctx, tx, actorID, item, challengeType); err != nil {
return err
}
_, err = tx.ExecContext(ctx, Rebind(r.driver, `INSERT INTO domains
(id, name, certificate_type, sans_json, auto_renew, acme_environment, dns_provider, status, expires_at, created_at, updated_at, project_id, owner_id, created_by, updated_by, challenge_type)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`), item.ID, item.Name, item.CertificateType, string(payload), item.AutoRenew,
item.ACMEEnvironment, item.DNSProvider, item.Status, nullableTime(item.ExpiresAt), item.CreatedAt, item.UpdatedAt,
item.ProjectID, item.OwnerID, item.CreatedBy, item.UpdatedBy, challengeType)
if err != nil {
return fmt.Errorf("保存域名失败:%w", err)
}
if err := writeDomainAudit(ctx, tx, r.driver, "domain.create", item.ID, item.ProjectID, actorID, "创建域名"); err != nil {
return err
}
if item.DNSAccountID != "" {
if _, err := tx.ExecContext(ctx, Rebind(r.driver, `INSERT INTO domain_dns_accounts (domain_id, id, project_id, dns_account_id, updated_at) VALUES (?, ?, ?, ?, ?)`),
item.ID, item.DNSBindingID, item.ProjectID, item.DNSAccountID, item.UpdatedAt); err != nil {
return err
}
if err := writeDomainAudit(ctx, tx, r.driver, "domain.dns_account", item.ID, item.ProjectID, actorID, "创建域名时绑定 DNS 账户"); err != nil {
return err
}
}
return commitProjectDomain(tx, "提交域名创建事务失败")
}
func (r *SQLProjectMemberRepository) UpdateProjectDomain(ctx context.Context, actorID string, item domain.Domain) error {
if err := r.ensureDB(); err != nil {
return err
}
payload, err := json.Marshal(item.SANs)
if err != nil {
return fmt.Errorf("编码域名 SAN 失败:%w", err)
}
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("开始域名更新事务失败:%w", err)
}
defer tx.Rollback()
var stored domain.Domain
var certificateType string
if err := tx.QueryRowContext(ctx, Rebind(r.driver, `SELECT id, name, certificate_type, project_id FROM domains WHERE id = ?`+r.resourceWriteLock()), item.ID).
Scan(&stored.ID, &stored.Name, &certificateType, &stored.ProjectID); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return sql.ErrNoRows
}
return fmt.Errorf("读取待更新域名失败:%w", err)
}
stored.CertificateType = domain.CertificateType(certificateType)
if err := r.authorizeProjectTransaction(ctx, tx, actorID, stored.ProjectID, domain.PermissionCertificateManage); err != nil {
return err
}
item.ProjectID = stored.ProjectID
if err := r.rejectActiveDNSLease(ctx, tx, "domain_id", item.ID); err != nil {
return err
}
if err := r.validateDomainPathsInTransaction(ctx, tx, item); err != nil {
return err
}
challengeType, err := domain.NormalizeChallengeType(item.ChallengeType, append([]string{item.Name}, item.SANs...))
if err != nil {
return err
}
if err := validateBoundDomainDNS(ctx, tx, r.driver, item, challengeType); err != nil {
return err
}
result, err := tx.ExecContext(ctx, Rebind(r.driver, `UPDATE domains SET
name = ?, certificate_type = ?, sans_json = ?, auto_renew = ?, acme_environment = ?, dns_provider = ?, updated_at = ?, updated_by = ?, challenge_type = ?
WHERE id = ? AND project_id = ?`), item.Name, item.CertificateType, string(payload), item.AutoRenew, item.ACMEEnvironment,
item.DNSProvider, item.UpdatedAt, item.UpdatedBy, challengeType, item.ID, stored.ProjectID)
if err != nil {
return fmt.Errorf("更新域名失败:%w", err)
}
if count, err := result.RowsAffected(); err != nil || count != 1 {
return sql.ErrNoRows
}
if err := writeDomainAudit(ctx, tx, r.driver, "domain.update", item.ID, stored.ProjectID, actorID, "更新域名"); err != nil {
return err
}
return commitProjectDomain(tx, "提交域名更新事务失败")
}
func (r *SQLProjectMemberRepository) DeleteProjectDomain(ctx context.Context, actorID, domainID string) error {
if err := r.ensureDB(); err != nil {
return err
}
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("开始域名删除事务失败:%w", err)
}
defer tx.Rollback()
var projectID string
if err := tx.QueryRowContext(ctx, Rebind(r.driver, `SELECT project_id FROM domains WHERE id = ?`+r.resourceWriteLock()), domainID).Scan(&projectID); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return sql.ErrNoRows
}
return fmt.Errorf("读取待删除域名失败:%w", err)
}
if err := r.authorizeProjectTransaction(ctx, tx, actorID, projectID, domain.PermissionCertificateManage); err != nil {
return err
}
rows, err := tx.QueryContext(ctx, Rebind(r.driver, `SELECT id FROM tasks
WHERE domain_id = ? AND task_type IN ('certificate_issue', 'certificate_validate', 'certificate_deploy', 'certificate_import', 'certificate_restore', 'renewal_run')
AND status IN ('queued', 'running', 'retry_wait', 'cancelling')`+r.resourceWriteLock()), domainID)
if err != nil {
return fmt.Errorf("检查域名任务失败:%w", err)
}
busy := rows.Next()
rows.Close()
if busy {
return ErrProjectDomainBusy
}
if err := r.rejectActiveDNSLease(ctx, tx, "domain_id", domainID); err != nil {
return err
}
if _, err := tx.ExecContext(ctx, Rebind(r.driver, `DELETE FROM domain_servers WHERE domain_id = ?`), domainID); err != nil {
return fmt.Errorf("删除域名服务器关联失败:%w", err)
}
if _, err := tx.ExecContext(ctx, Rebind(r.driver, `DELETE FROM domain_dns_accounts WHERE domain_id = ?`), domainID); err != nil {
return err
}
result, err := tx.ExecContext(ctx, Rebind(r.driver, `DELETE FROM domains WHERE id = ? AND project_id = ?`), domainID, projectID)
if err != nil {
return fmt.Errorf("删除域名失败:%w", err)
}
if count, err := result.RowsAffected(); err != nil || count != 1 {
return sql.ErrNoRows
}
if err := writeDomainAudit(ctx, tx, r.driver, "domain.delete", domainID, projectID, actorID, "删除域名"); err != nil {
return err
}
return commitProjectDomain(tx, "提交域名删除事务失败")
}
func (r *SQLProjectMemberRepository) validateDomainPathsInTransaction(ctx context.Context, tx *sql.Tx, item domain.Domain) error {
rows, err := tx.QueryContext(ctx, Rebind(r.driver, serverSelectQualified+` INNER JOIN domain_servers ds ON ds.server_id = servers.id WHERE ds.domain_id = ? ORDER BY servers.id`+r.resourceWriteLock()), item.ID)
if err != nil {
return fmt.Errorf("查询域名服务器路径失败:%w", err)
}
defer rows.Close()
for rows.Next() {
server, err := scanServer(rows)
if err != nil {
return err
}
if server.ProjectID != item.ProjectID {
return ErrProjectAccessDenied
}
if err := validateServerDomainPaths(ctx, tx, r.driver, server, &item); err != nil {
return err
}
}
return rows.Err()
}
func writeDomainAudit(ctx context.Context, tx *sql.Tx, driver, action, domainID, projectID, actorID, message string) error {
result, err := tx.ExecContext(ctx, Rebind(driver, `INSERT INTO audit_logs
(action, resource_type, resource_id, status, message, created_at, scope_type, project_id, task_id, operator_id)
VALUES (?, 'domain', ?, 'success', ?, ?, 'project', ?, '', ?)`), action, domainID, message, time.Now().UTC(), projectID, actorID)
if err != nil {
return fmt.Errorf("写入域名操作审计失败:%w", err)
}
if count, err := result.RowsAffected(); err != nil || count != 1 {
return errors.New("域名操作审计未写入,业务变更已回滚")
}
return nil
}
func commitProjectDomain(tx *sql.Tx, message string) error {
if err := tx.Commit(); err != nil {
return fmt.Errorf("%s:%w", message, err)
}
return nil
}