291 lines
11 KiB
Go
291 lines
11 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"database/sql"
|
|
"errors"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"git.pcux.cn/xuans/Scert/backend/internal/dnsprovider"
|
|
"git.pcux.cn/xuans/Scert/backend/internal/domain"
|
|
"git.pcux.cn/xuans/Scert/backend/internal/repository"
|
|
)
|
|
|
|
type dnsAccountView struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Provider string `json:"provider"`
|
|
ProviderName string `json:"provider_name"`
|
|
AccountIdentifier string `json:"account_identifier"`
|
|
ProjectID string `json:"project_id"`
|
|
OwnerUserID string `json:"owner_user_id,omitempty"`
|
|
OwnerUsername string `json:"owner_username"`
|
|
OwnerState string `json:"owner_state"`
|
|
VerificationState string `json:"verification_state"`
|
|
CredentialState string `json:"credential_state"`
|
|
Status string `json:"status"`
|
|
Revision int64 `json:"revision"`
|
|
Managed bool `json:"managed"`
|
|
}
|
|
|
|
// listDNSAccounts 合并独立账户和旧系统配置;不枚举秘密仓库中的临时或历史密钥。
|
|
func (s *Server) listDNSAccounts(c *gin.Context) {
|
|
s.configMu.RLock()
|
|
cfg := s.config
|
|
s.configMu.RUnlock()
|
|
items := make([]dnsAccountView, 0, 1)
|
|
hasLegacyAccount := false
|
|
var legacyAccount domain.DNSAccount
|
|
configuredLegacyProjectID := strings.TrimSpace(cfg.DNS.ProjectID)
|
|
if configuredLegacyProjectID == "" {
|
|
configuredLegacyProjectID = domain.DefaultProjectID
|
|
}
|
|
if repo, ok := s.projectMembers.(repository.DNSAccountRepository); ok {
|
|
accounts, err := repo.ListVisibleDNSAccounts(c.Request.Context(), c.GetString(authenticatedActorKey))
|
|
if err != nil {
|
|
s.writeDNSAccountError(c, err)
|
|
return
|
|
}
|
|
for _, account := range accounts {
|
|
if account.ID == "legacy-system-dns" {
|
|
if account.ProjectID != configuredLegacyProjectID {
|
|
continue
|
|
}
|
|
hasLegacyAccount = true
|
|
legacyAccount = account
|
|
continue
|
|
}
|
|
item, err := s.managedDNSAccountView(c, account)
|
|
if err != nil {
|
|
s.writeError(c, http.StatusServiceUnavailable, "读取 DNS 账户归属失败", nil)
|
|
return
|
|
}
|
|
items = append(items, item)
|
|
}
|
|
}
|
|
if strings.TrimSpace(cfg.DNS.CredentialRef) == "" {
|
|
c.JSON(http.StatusOK, gin.H{"items": items})
|
|
return
|
|
}
|
|
projectID := strings.TrimSpace(cfg.DNS.ProjectID)
|
|
if projectID == "" {
|
|
projectID = domain.DefaultProjectID
|
|
}
|
|
// 在查询账户归属或解密凭据之前,按配置中的真实项目实时判权。
|
|
// 全局管理员身份不能代替项目授权,撤权后的旧会话也不能继续读取。
|
|
authorizer, ok := s.projectMembers.(repository.ProjectAuthorizer)
|
|
if !ok {
|
|
authorizer, ok = s.projectDomains.(repository.ProjectAuthorizer)
|
|
}
|
|
if !ok {
|
|
s.writeError(c, http.StatusServiceUnavailable, "项目 DNS 权限服务尚未就绪", nil)
|
|
return
|
|
}
|
|
if err := authorizer.AuthorizeProject(c.Request.Context(), c.GetString(authenticatedActorKey), projectID, domain.PermissionDNSRead); err != nil {
|
|
if errors.Is(err, repository.ErrProjectAccessDenied) {
|
|
// 列表只返回可见账户,不泄露其他项目是否配置了凭据。
|
|
c.JSON(http.StatusOK, gin.H{"items": items})
|
|
return
|
|
}
|
|
s.writeError(c, http.StatusServiceUnavailable, "读取项目 DNS 权限失败,请稍后重试", nil)
|
|
return
|
|
}
|
|
if hasLegacyAccount {
|
|
item, viewErr := s.configuredLegacyDNSAccountView(c, legacyAccount, cfg.DNS.Provider, cfg.DNS.CredentialRef, cfg.DNS.OwnerUserID, cfg.Admin.Username)
|
|
if viewErr != nil {
|
|
s.writeError(c, http.StatusServiceUnavailable, "读取 DNS 账户归属失败", nil)
|
|
return
|
|
}
|
|
items = append(items, item)
|
|
c.JSON(http.StatusOK, gin.H{"items": items})
|
|
return
|
|
}
|
|
if importer, ok := s.projectMembers.(repository.DNSLegacyAccountImporter); ok {
|
|
if managedRepo, repoOK := s.projectMembers.(repository.DNSAccountRepository); repoOK && strings.TrimSpace(cfg.DNS.Provider) != "" {
|
|
const legacyID = "legacy-system-dns"
|
|
legacyAccount, err := managedRepo.GetVisibleDNSAccount(c.Request.Context(), c.GetString(authenticatedActorKey), legacyID)
|
|
if errors.Is(err, repository.ErrProjectAccessDenied) {
|
|
legacyProjectID := strings.TrimSpace(cfg.DNS.ProjectID)
|
|
if legacyProjectID == "" {
|
|
legacyProjectID = domain.DefaultProjectID
|
|
}
|
|
legacyOwnerID := strings.TrimSpace(cfg.DNS.OwnerUserID)
|
|
if legacyOwnerID == "" {
|
|
legacyOwnerID = "primary-super-admin"
|
|
}
|
|
if err := importer.ImportLegacyDNSAccount(c.Request.Context(), domain.DNSAccount{
|
|
ID: legacyID, Name: "旧系统 DNS 账户", ProjectID: legacyProjectID,
|
|
OwnerUserID: legacyOwnerID, Provider: cfg.DNS.Provider, CredentialRef: cfg.DNS.CredentialRef,
|
|
}); err != nil {
|
|
s.writeError(c, http.StatusServiceUnavailable, "登记已配置的 DNS 账户失败,请稍后重试", nil)
|
|
return
|
|
}
|
|
legacyAccount, err = managedRepo.GetVisibleDNSAccount(c.Request.Context(), c.GetString(authenticatedActorKey), legacyID)
|
|
}
|
|
if err == nil {
|
|
item, viewErr := s.configuredLegacyDNSAccountView(c, legacyAccount, cfg.DNS.Provider, cfg.DNS.CredentialRef, cfg.DNS.OwnerUserID, cfg.Admin.Username)
|
|
if viewErr != nil {
|
|
s.writeError(c, http.StatusServiceUnavailable, "读取 DNS 账户归属失败", nil)
|
|
return
|
|
}
|
|
items = append(items, item)
|
|
c.JSON(http.StatusOK, gin.H{"items": items})
|
|
return
|
|
}
|
|
if !errors.Is(err, repository.ErrProjectAccessDenied) {
|
|
s.writeError(c, http.StatusServiceUnavailable, "读取已配置的 DNS 账户失败,请稍后重试", nil)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
item := dnsAccountView{
|
|
ID: "system-dns", Name: "系统 DNS 账户", Provider: cfg.DNS.Provider,
|
|
ProjectID: projectID,
|
|
ProviderName: "未知服务商", AccountIdentifier: "未提供账号标识",
|
|
OwnerUserID: cfg.DNS.OwnerUserID, OwnerState: "unassigned", OwnerUsername: "未分配",
|
|
VerificationState: cfg.DNS.VerificationState, CredentialState: "available",
|
|
Status: domain.DNSAccountActive,
|
|
}
|
|
var owner repository.User
|
|
ownerID := strings.TrimSpace(cfg.DNS.OwnerUserID)
|
|
if ownerID == "" {
|
|
// 旧版本只有一个系统 DNS 配置,没有单独的归属字段;迁移时归属初始化超级管理员。
|
|
ownerID = "primary-super-admin"
|
|
item.OwnerUserID = ownerID
|
|
}
|
|
if ownerID == "primary-super-admin" {
|
|
owner = repository.User{ID: ownerID, Username: cfg.Admin.Username, Role: repository.UserRoleSuperAdmin}
|
|
} else {
|
|
if s.users == nil {
|
|
s.writeError(c, http.StatusServiceUnavailable, "用户归属查询暂不可用", nil)
|
|
return
|
|
}
|
|
var err error
|
|
owner, err = s.users.GetUserByID(c.Request.Context(), ownerID)
|
|
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
|
s.writeError(c, http.StatusServiceUnavailable, "用户归属查询暂不可用", nil)
|
|
return
|
|
}
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
item.OwnerState, item.OwnerUsername = "missing", "原用户已不存在"
|
|
}
|
|
}
|
|
if owner.ID != "" {
|
|
item.OwnerState, item.OwnerUsername = "assigned", owner.Username
|
|
}
|
|
if descriptor, err := dnsprovider.GetDescriptor(cfg.DNS.Provider); err == nil {
|
|
item.ProviderName = descriptor.Name
|
|
}
|
|
credentials, err := s.readDNSCredentials(c.Request.Context(), cfg.DNS.CredentialRef)
|
|
if err != nil {
|
|
item.CredentialState, item.AccountIdentifier = "unavailable", "暂不可读取"
|
|
} else {
|
|
item.AccountIdentifier = dnsAccountIdentifier(cfg.DNS.Provider, credentials)
|
|
if _, err := dnsprovider.ValidateCredentials(cfg.DNS.Provider, credentials); err != nil {
|
|
item.CredentialState = "invalid"
|
|
}
|
|
}
|
|
items = append(items, item)
|
|
c.JSON(http.StatusOK, gin.H{"items": items})
|
|
}
|
|
|
|
func (s *Server) configuredLegacyDNSAccountView(c *gin.Context, account domain.DNSAccount, provider, credentialRef, ownerUserID, adminUsername string) (dnsAccountView, error) {
|
|
ownerID := strings.TrimSpace(ownerUserID)
|
|
if ownerID == "" {
|
|
ownerID = account.OwnerUserID
|
|
}
|
|
if ownerID == "" {
|
|
ownerID = "primary-super-admin"
|
|
}
|
|
item := dnsAccountView{
|
|
ID: account.ID, Name: account.Name, Provider: account.Provider, ProjectID: account.ProjectID,
|
|
OwnerUserID: ownerID, OwnerState: "unassigned", OwnerUsername: "未分配",
|
|
VerificationState: account.VerificationState, CredentialState: "available",
|
|
Status: account.Status, Revision: account.Revision, Managed: true,
|
|
}
|
|
if descriptor, err := dnsprovider.GetDescriptor(provider); err == nil {
|
|
item.ProviderName = descriptor.Name
|
|
} else {
|
|
item.ProviderName = "未知服务商"
|
|
}
|
|
if ownerID == "primary-super-admin" {
|
|
item.OwnerState, item.OwnerUsername = "assigned", adminUsername
|
|
} else {
|
|
if s.users == nil {
|
|
return dnsAccountView{}, errors.New("用户归属查询暂不可用")
|
|
}
|
|
owner, err := s.users.GetUserByID(c.Request.Context(), ownerID)
|
|
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
|
return dnsAccountView{}, err
|
|
}
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
item.OwnerState, item.OwnerUsername = "missing", "原用户已不存在"
|
|
} else {
|
|
item.OwnerState, item.OwnerUsername = "assigned", owner.Username
|
|
}
|
|
}
|
|
credentials, err := s.readDNSCredentials(c.Request.Context(), credentialRef)
|
|
if err != nil {
|
|
item.CredentialState, item.AccountIdentifier = "unavailable", "暂不可读取"
|
|
} else {
|
|
item.AccountIdentifier = dnsAccountIdentifier(provider, credentials)
|
|
if _, err := dnsprovider.ValidateCredentials(provider, credentials); err != nil {
|
|
item.CredentialState = "invalid"
|
|
}
|
|
}
|
|
return item, nil
|
|
}
|
|
|
|
func (s *Server) managedDNSAccountView(c *gin.Context, account domain.DNSAccount) (dnsAccountView, error) {
|
|
item := dnsAccountView{
|
|
ID: account.ID, Name: account.Name, Provider: account.Provider, ProviderName: "未知服务商",
|
|
ProjectID: account.ProjectID, OwnerUserID: account.OwnerUserID,
|
|
OwnerUsername: "原用户已不存在", OwnerState: "missing",
|
|
VerificationState: account.VerificationState, CredentialState: "stored",
|
|
AccountIdentifier: "凭据已隐藏", Status: account.Status, Revision: account.Revision, Managed: true,
|
|
}
|
|
if descriptor, err := dnsprovider.GetDescriptor(account.Provider); err == nil {
|
|
item.ProviderName = descriptor.Name
|
|
}
|
|
if s.users == nil {
|
|
return dnsAccountView{}, errors.New("用户归属查询暂不可用")
|
|
}
|
|
owner, err := s.users.GetUserByID(c.Request.Context(), account.OwnerUserID)
|
|
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
|
return dnsAccountView{}, err
|
|
}
|
|
if err == nil {
|
|
item.OwnerUsername, item.OwnerState = owner.Username, "assigned"
|
|
}
|
|
return item, nil
|
|
}
|
|
|
|
// dnsAccountIdentifier 只允许输出公开标识的脱敏结果,绝不输出令牌或私钥片段。
|
|
func dnsAccountIdentifier(provider string, credentials map[string]string) string {
|
|
var value string
|
|
switch provider {
|
|
case dnsprovider.Aliyun, dnsprovider.HuaweiCloud, dnsprovider.Route53:
|
|
value = credentials["access_key_id"]
|
|
case dnsprovider.DNSPod, dnsprovider.TencentCloud:
|
|
value = credentials["secret_id"]
|
|
case dnsprovider.Cloudflare:
|
|
return "令牌账号(已隐藏)"
|
|
case dnsprovider.GoogleCloud:
|
|
return "服务账号(已隐藏)"
|
|
case dnsprovider.CNAME:
|
|
return "委派账号(已隐藏)"
|
|
default:
|
|
return "账号标识已隐藏"
|
|
}
|
|
identifier := []rune(strings.TrimSpace(value))
|
|
if len(identifier) == 0 {
|
|
return "未提供账号标识"
|
|
}
|
|
if len(identifier) <= 8 {
|
|
return "********"
|
|
}
|
|
return string(identifier[:4]) + "****" + string(identifier[len(identifier)-4:])
|
|
}
|