完成阿里云RAM权限双轨考证与域名操作前置拦截
This commit is contained in:
parent
0dbc20d0b6
commit
8fa1c82730
@ -114,7 +114,8 @@ func NewWithConfigPath(ctx context.Context, cfg config.Config, configPath string
|
||||
sshKeys := sshkey.NewService(secretStore)
|
||||
accounts := certificate.NewAccountRepository(secretStore)
|
||||
certificates := certificate.NewService(cfg, repo, secretStore, dnsprovider.NewLegoFactory(), accounts)
|
||||
certificates.SetDNSAuthorizationChecker(dnsprovider.NewAliyunAuthorizationChecker())
|
||||
aliyunAuthChecker := dnsprovider.NewAliyunAuthorizationChecker()
|
||||
certificates.SetDNSAuthorizationChecker(aliyunAuthChecker)
|
||||
deploymentService := deployment.NewService(deployment.SSHFactory{Secrets: secretStore})
|
||||
notifier := notification.New(cfg.Notification, secretStore)
|
||||
remoteValidator := certificate.RemoteValidator{}
|
||||
@ -140,7 +141,7 @@ func NewWithConfigPath(ctx context.Context, cfg config.Config, configPath string
|
||||
})
|
||||
server := httpapi.New(httpapi.Dependencies{
|
||||
Config: cfg, ConfigPath: configPath, Logger: logger, Repository: repo, Servers: serverRepo, Users: userRepo, SSHKeys: sshKeys,
|
||||
SetupDNS: dnsVerification, Secrets: secretStore, Firewall: firewall.NewManager(),
|
||||
SetupDNS: dnsVerification, Secrets: secretStore, Firewall: firewall.NewManager(), AliyunChecker: aliyunAuthChecker,
|
||||
IssueCertificate: workflow.IssueForHTTP,
|
||||
IssueSetupCertificate: func(ctx context.Context, request certificate.SetupCertificateRequest) (certificate.SavedResource, error) {
|
||||
return certificates.IssueForSetup(ctx, request)
|
||||
|
||||
@ -42,6 +42,7 @@ type AuthorizationReport struct {
|
||||
MatchedDomain string `json:"matched_domain,omitempty"`
|
||||
Domains []AuthorizationDomain `json:"domains"`
|
||||
Verified bool `json:"verified"`
|
||||
Diagnosis string `json:"diagnosis,omitempty"`
|
||||
CheckedAt time.Time `json:"checked_at"`
|
||||
}
|
||||
|
||||
@ -124,7 +125,8 @@ func (c *AliyunAuthorizationChecker) Verify(ctx context.Context, requestedDomain
|
||||
if lastErr == nil {
|
||||
lastErr = errors.New("未找到可验证的阿里云 DNS 托管根域名")
|
||||
}
|
||||
return report, fmt.Errorf("阿里云 RAM 账号无法写入域名 %s:%w", requestedDomain, lastErr)
|
||||
report.Diagnosis = DiagnoseAliyunError(lastErr, requestedDomain)
|
||||
return report, fmt.Errorf("阿里云 RAM 账号无法写入域名 %s:%w(建议:%s)", requestedDomain, lastErr, report.Diagnosis)
|
||||
}
|
||||
|
||||
// VerifyDomain 实现证书服务使用的最小授权检查接口。
|
||||
@ -373,3 +375,22 @@ func uniqueStrings(values []string) []string {
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// DiagnoseAliyunError 对阿里云 API 返回的错误进行分类并提供明确的修复指导。
|
||||
func DiagnoseAliyunError(err error, domainName string) string {
|
||||
if err == nil {
|
||||
return ""
|
||||
}
|
||||
msg := err.Error()
|
||||
lowered := strings.ToLower(msg)
|
||||
if strings.Contains(lowered, "forbidden.nopermission") || strings.Contains(lowered, "accessdenied") || strings.Contains(lowered, "not authorized") {
|
||||
return fmt.Sprintf("当前阿里云 RAM 账号未获得域名 %s 的 DNS 解析权限。请在阿里云 RAM 控制台授予该账号针对 acs:alidns:*:*:domain/%s 的操作权限(Action: alidns:*)", domainName, domainName)
|
||||
}
|
||||
if strings.Contains(lowered, "nosuchdomain") || strings.Contains(lowered, "invaliddomainname.noexist") {
|
||||
return fmt.Sprintf("域名 %s 未在当前阿里云账号下添加 DNS 托管,请先在阿里云云解析 DNS 控制台添加此域名", domainName)
|
||||
}
|
||||
if strings.Contains(lowered, "invalidaccesskeyid.notfound") || strings.Contains(lowered, "signaturedoesnotmatch") {
|
||||
return "阿里云 AccessKey ID 或 AccessKey Secret 无效,请检查系统 DNS 凭据配置"
|
||||
}
|
||||
return fmt.Sprintf("阿里云 RAM 权限验证未通过:%s", msg)
|
||||
}
|
||||
|
||||
@ -121,3 +121,39 @@ func TestAliyunAuthorizationCheckerCacheIsSafeForConcurrentUse(t *testing.T) {
|
||||
<-done
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiagnoseAliyunError(t *testing.T) {
|
||||
testCases := []struct {
|
||||
err error
|
||||
domain string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
err: errors.New("SDKError: Code: Forbidden.NoPermission, Message: Specified access key is not authorized."),
|
||||
domain: "solit.cn",
|
||||
expected: "当前阿里云 RAM 账号未获得域名 solit.cn 的 DNS 解析权限。请在阿里云 RAM 控制台授予该账号针对 acs:alidns:*:*:domain/solit.cn 的操作权限(Action: alidns:*)",
|
||||
},
|
||||
{
|
||||
err: errors.New("SDKError: Code: NoSuchDomain, Message: The specified domain name does not exist."),
|
||||
domain: "nonexistent.cn",
|
||||
expected: "域名 nonexistent.cn 未在当前阿里云账号下添加 DNS 托管,请先在阿里云云解析 DNS 控制台添加此域名",
|
||||
},
|
||||
{
|
||||
err: errors.New("SDKError: Code: InvalidAccessKeyId.NotFound, Message: Specified access key is not found."),
|
||||
domain: "solit.cn",
|
||||
expected: "阿里云 AccessKey ID 或 AccessKey Secret 无效,请检查系统 DNS 凭据配置",
|
||||
},
|
||||
{
|
||||
err: nil,
|
||||
domain: "solit.cn",
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
got := DiagnoseAliyunError(tc.err, tc.domain)
|
||||
if got != tc.expected {
|
||||
t.Errorf("DiagnoseAliyunError(%v, %s) = %q; want %q", tc.err, tc.domain, got, tc.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
189
backend/internal/httpapi/aliyun_scope_api.go
Normal file
189
backend/internal/httpapi/aliyun_scope_api.go
Normal file
@ -0,0 +1,189 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/dnsprovider"
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/domain"
|
||||
)
|
||||
|
||||
type aliyunScopeChecker interface {
|
||||
Discover(ctx context.Context, credentials map[string]string) (dnsprovider.AuthorizationReport, error)
|
||||
Verify(ctx context.Context, requestedDomain string, credentials map[string]string) (dnsprovider.AuthorizationReport, error)
|
||||
VerifyDomain(ctx context.Context, providerID, requestedDomain string, credentials map[string]string) error
|
||||
}
|
||||
|
||||
type aliyunDomainCheckRequest struct {
|
||||
Domain string `json:"domain"`
|
||||
}
|
||||
|
||||
type aliyunScopeResponse struct {
|
||||
Provider string `json:"provider"`
|
||||
Configured bool `json:"configured"`
|
||||
CredentialRef string `json:"credential_ref,omitempty"`
|
||||
PolicyMode string `json:"policy_mode"`
|
||||
GlobalDiscoveryAvailable bool `json:"global_discovery_available"`
|
||||
Message string `json:"message"`
|
||||
DiscoveredDomains []dnsprovider.AuthorizationDomain `json:"discovered_domains,omitempty"`
|
||||
ManagedDomains []string `json:"managed_domains"`
|
||||
CheckedAt string `json:"checked_at"`
|
||||
}
|
||||
|
||||
func (s *Server) getAliyunDNSScope(c *gin.Context) {
|
||||
s.configMu.RLock()
|
||||
provider := strings.ToLower(strings.TrimSpace(s.config.DNS.Provider))
|
||||
credentialRef := strings.TrimSpace(s.config.DNS.CredentialRef)
|
||||
s.configMu.RUnlock()
|
||||
|
||||
if provider != dnsprovider.Aliyun {
|
||||
c.JSON(http.StatusOK, aliyunScopeResponse{
|
||||
Provider: provider,
|
||||
Configured: false,
|
||||
PolicyMode: "not_aliyun",
|
||||
Message: "系统当前未将阿里云配置为主 DNS 服务商",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
credentials, err := s.readDNSCredentials(c.Request.Context(), credentialRef)
|
||||
if err != nil {
|
||||
s.writeError(c, http.StatusInternalServerError, "读取阿里云 DNS 凭据失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
response := aliyunScopeResponse{
|
||||
Provider: dnsprovider.Aliyun,
|
||||
Configured: true,
|
||||
CredentialRef: credentialRef,
|
||||
CheckedAt: s.now().UTC().Format("2006-01-02T15:04:05Z"),
|
||||
}
|
||||
|
||||
// 读取当前系统内已经纳管的域名列表
|
||||
if s.repository != nil {
|
||||
if items, listErr := s.repository.List(c.Request.Context()); listErr == nil {
|
||||
for _, item := range items {
|
||||
response.ManagedDomains = append(response.ManagedDomains, item.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if s.aliyunChecker == nil {
|
||||
response.PolicyMode = "uninitialized"
|
||||
response.Message = "阿里云权限探测器未装载"
|
||||
c.JSON(http.StatusOK, response)
|
||||
return
|
||||
}
|
||||
|
||||
// 尝试全局发现
|
||||
report, discoverErr := s.aliyunChecker.Discover(c.Request.Context(), credentials)
|
||||
if discoverErr == nil && len(report.Domains) > 0 {
|
||||
response.GlobalDiscoveryAvailable = true
|
||||
response.PolicyMode = "broad_or_read_all"
|
||||
response.Message = "当前 RAM 账号具备全局根域名读取权限"
|
||||
response.DiscoveredDomains = report.Domains
|
||||
} else {
|
||||
response.GlobalDiscoveryAvailable = false
|
||||
response.PolicyMode = "resource_scoped"
|
||||
response.Message = "当前 RAM 账号受特定域名资源策略限制(最小权限原则),不支持全局枚举,已启用单域名靶向权威考证"
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
func (s *Server) checkAliyunDNSDomain(c *gin.Context) {
|
||||
var request aliyunDomainCheckRequest
|
||||
if err := bindJSON(c, &request, jsonRequestBodyLimit); err != nil {
|
||||
s.writeError(c, http.StatusBadRequest, "请求参数无效", err)
|
||||
return
|
||||
}
|
||||
targetDomain := strings.ToLower(strings.TrimSuffix(strings.TrimSpace(request.Domain), "."))
|
||||
if !domain.IsValidName(targetDomain) {
|
||||
s.writeError(c, http.StatusBadRequest, fmt.Sprintf("域名格式无效:%s", targetDomain), nil)
|
||||
return
|
||||
}
|
||||
|
||||
s.configMu.RLock()
|
||||
provider := strings.ToLower(strings.TrimSpace(s.config.DNS.Provider))
|
||||
credentialRef := strings.TrimSpace(s.config.DNS.CredentialRef)
|
||||
s.configMu.RUnlock()
|
||||
|
||||
if provider != dnsprovider.Aliyun {
|
||||
s.writeError(c, http.StatusBadRequest, "当前系统 DNS 服务商不是阿里云,无法进行阿里云 RAM 权限考证", nil)
|
||||
return
|
||||
}
|
||||
|
||||
credentials, err := s.readDNSCredentials(c.Request.Context(), credentialRef)
|
||||
if err != nil {
|
||||
s.writeError(c, http.StatusInternalServerError, "读取阿里云 DNS 凭据失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
if s.aliyunChecker == nil {
|
||||
s.writeError(c, http.StatusNotImplemented, "阿里云权限探测器未装载", nil)
|
||||
return
|
||||
}
|
||||
|
||||
report, verifyErr := s.aliyunChecker.Verify(c.Request.Context(), targetDomain, credentials)
|
||||
if verifyErr != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"verified": false,
|
||||
"domain": targetDomain,
|
||||
"matched_domain": report.MatchedDomain,
|
||||
"diagnosis": report.Diagnosis,
|
||||
"error": verifyErr.Error(),
|
||||
"report": report,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"verified": true,
|
||||
"domain": targetDomain,
|
||||
"matched_domain": report.MatchedDomain,
|
||||
"message": "阿里云 RAM 账号对该域名的 DNS TXT 记录写入与清理权限验证通过",
|
||||
"report": report,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) validateDomainAliyunAuthorization(ctx context.Context, domainName string) error {
|
||||
if s == nil || s.aliyunChecker == nil {
|
||||
return nil
|
||||
}
|
||||
s.configMu.RLock()
|
||||
setupReady := s.config.SetupReady
|
||||
provider := strings.ToLower(strings.TrimSpace(s.config.DNS.Provider))
|
||||
credentialRef := strings.TrimSpace(s.config.DNS.CredentialRef)
|
||||
s.configMu.RUnlock()
|
||||
|
||||
if !setupReady || provider != dnsprovider.Aliyun || credentialRef == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
credentials, err := s.readDNSCredentials(ctx, credentialRef)
|
||||
if err != nil || len(credentials) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := s.aliyunChecker.VerifyDomain(ctx, dnsprovider.Aliyun, domainName, credentials); err != nil {
|
||||
diagnosis := dnsprovider.DiagnoseAliyunError(err, domainName)
|
||||
return fmt.Errorf("阿里云 RAM 账号无法操作域名 %s:%s", domainName, diagnosis)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) readDNSCredentials(ctx context.Context, credentialRef string) (map[string]string, error) {
|
||||
if s == nil || strings.TrimSpace(credentialRef) == "" {
|
||||
return nil, errors.New("DNS 凭据引用未配置")
|
||||
}
|
||||
reader, ok := s.secrets.(readinessSecretReader)
|
||||
if !ok || reader == nil {
|
||||
return nil, errors.New("凭据存储未就绪")
|
||||
}
|
||||
return reader.Get(ctx, credentialRef)
|
||||
}
|
||||
168
backend/internal/httpapi/aliyun_scope_api_test.go
Normal file
168
backend/internal/httpapi/aliyun_scope_api_test.go
Normal file
@ -0,0 +1,168 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/dnsprovider"
|
||||
)
|
||||
|
||||
type fakeAliyunScopeChecker struct {
|
||||
allowedZones []string
|
||||
}
|
||||
|
||||
func (f *fakeAliyunScopeChecker) Discover(ctx context.Context, credentials map[string]string) (dnsprovider.AuthorizationReport, error) {
|
||||
// 模拟 RAM 账号处于最小资源限制模式,无全局枚举权限
|
||||
return dnsprovider.AuthorizationReport{}, errors.New("SDKError: Code: Forbidden.NoPermission")
|
||||
}
|
||||
|
||||
func (f *fakeAliyunScopeChecker) Verify(ctx context.Context, requestedDomain string, credentials map[string]string) (dnsprovider.AuthorizationReport, error) {
|
||||
report := dnsprovider.AuthorizationReport{
|
||||
Provider: dnsprovider.Aliyun,
|
||||
RequestedDomain: requestedDomain,
|
||||
}
|
||||
for _, zone := range f.allowedZones {
|
||||
if requestedDomain == zone || strings.HasSuffix(requestedDomain, "."+zone) {
|
||||
report.Verified = true
|
||||
report.MatchedDomain = zone
|
||||
return report, nil
|
||||
}
|
||||
}
|
||||
err := errors.New("SDKError: Code: Forbidden.NoPermission, Message: Specified access key is not authorized.")
|
||||
report.Diagnosis = dnsprovider.DiagnoseAliyunError(err, requestedDomain)
|
||||
return report, err
|
||||
}
|
||||
|
||||
func (f *fakeAliyunScopeChecker) VerifyDomain(ctx context.Context, providerID, requestedDomain string, credentials map[string]string) error {
|
||||
for _, zone := range f.allowedZones {
|
||||
if requestedDomain == zone || strings.HasSuffix(requestedDomain, "."+zone) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return errors.New("SDKError: Code: Forbidden.NoPermission, Message: Specified access key is not authorized.")
|
||||
}
|
||||
|
||||
type fakeSecretReaderStore struct {
|
||||
data map[string]map[string]string
|
||||
}
|
||||
|
||||
func (s *fakeSecretReaderStore) Put(ctx context.Context, ref string, val map[string]string) error {
|
||||
if s.data == nil {
|
||||
s.data = make(map[string]map[string]string)
|
||||
}
|
||||
s.data[ref] = val
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *fakeSecretReaderStore) Delete(ctx context.Context, ref string) error {
|
||||
delete(s.data, ref)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *fakeSecretReaderStore) Get(ctx context.Context, ref string) (map[string]string, error) {
|
||||
if val, ok := s.data[ref]; ok {
|
||||
return val, nil
|
||||
}
|
||||
return nil, errors.New("secret not found")
|
||||
}
|
||||
|
||||
func TestAliyunScopeAPIsAndDomainValidation(t *testing.T) {
|
||||
cfg := readyTestConfig(t.TempDir())
|
||||
cfg.SetupReady = true
|
||||
cfg.DNS.Provider = "aliyun"
|
||||
cfg.DNS.CredentialRef = "secret://dns/aliyun/ref1"
|
||||
|
||||
store := &fakeSecretReaderStore{
|
||||
data: map[string]map[string]string{
|
||||
"secret://dns/aliyun/ref1": {
|
||||
"access_key_id": "LTAI_test",
|
||||
"access_key_secret": "test_secret",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
checker := &fakeAliyunScopeChecker{
|
||||
allowedZones: []string{"solit.cn", "odacm.cn", "apmmk.cn", "ifmj.uno", "vraq.uno"},
|
||||
}
|
||||
|
||||
repo := &stubDomainRepository{}
|
||||
server := New(Dependencies{
|
||||
Config: cfg,
|
||||
Repository: repo,
|
||||
Secrets: store,
|
||||
AliyunChecker: checker,
|
||||
})
|
||||
|
||||
// 1. 测试 GET /api/v1/dns/aliyun/scope(受限模式报告)
|
||||
{
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/dns/aliyun/scope", nil)
|
||||
serveAdminHTTP(server, w, req)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("GET /api/v1/dns/aliyun/scope status = %d; want 200, body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
if !strings.Contains(w.Body.String(), "resource_scoped") {
|
||||
t.Fatalf("expected resource_scoped policy mode, got: %s", w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 测试 POST /api/v1/dns/aliyun/check 验证授权域名(如 solit.cn)
|
||||
{
|
||||
w := httptest.NewRecorder()
|
||||
body := strings.NewReader(`{"domain":"solit.cn"}`)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/dns/aliyun/check", body)
|
||||
serveAdminHTTP(server, w, req)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("POST check solit.cn status = %d; body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
if !strings.Contains(w.Body.String(), `"verified":true`) {
|
||||
t.Fatalf("solit.cn 应验证通过: %s", w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 测试 POST /api/v1/dns/aliyun/check 验证未授权域名(如 unauth.com)
|
||||
{
|
||||
w := httptest.NewRecorder()
|
||||
body := strings.NewReader(`{"domain":"unauth.com"}`)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/dns/aliyun/check", body)
|
||||
serveAdminHTTP(server, w, req)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("POST check unauth.com status = %d; body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
if !strings.Contains(w.Body.String(), `"verified":false`) {
|
||||
t.Fatalf("unauth.com 应验证失败: %s", w.Body.String())
|
||||
}
|
||||
if !strings.Contains(w.Body.String(), "acs:alidns:*:*:domain/unauth.com") {
|
||||
t.Fatalf("应包含明确的 RAM 策略建议: %s", w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 测试 POST /api/v1/domains 添加授权域名(例如 *.solit.cn)
|
||||
{
|
||||
w := httptest.NewRecorder()
|
||||
body := strings.NewReader(`{"name":"test.solit.cn","certificate_type":"single"}`)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/domains", body)
|
||||
serveAdminHTTP(server, w, req)
|
||||
if w.Code != http.StatusCreated {
|
||||
t.Fatalf("创建授权域名应成功,got status = %d; body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 测试 POST /api/v1/domains 添加未授权域名(例如 forbidden.cn),应被前置阻断
|
||||
{
|
||||
w := httptest.NewRecorder()
|
||||
body := strings.NewReader(`{"name":"forbidden.cn","certificate_type":"single"}`)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/domains", body)
|
||||
serveAdminHTTP(server, w, req)
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Fatalf("添加未授权域名应被 400 阻断,got status = %d; body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
if !strings.Contains(w.Body.String(), "acs:alidns:*:*:domain/forbidden.cn") {
|
||||
t.Fatalf("阻断提示应包含针对该域名的 RAM 授权指导,got: %s", w.Body.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -64,6 +64,7 @@ type Dependencies struct {
|
||||
Audit repository.AuditRepository
|
||||
Tasks repository.TaskRepository
|
||||
ReadinessCheck func(context.Context) error
|
||||
AliyunChecker aliyunScopeChecker
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
@ -93,6 +94,7 @@ type Server struct {
|
||||
audit repository.AuditRepository
|
||||
tasks repository.TaskRepository
|
||||
readinessCheck func(context.Context) error
|
||||
aliyunChecker aliyunScopeChecker
|
||||
taskMu sync.Mutex
|
||||
taskCancels map[string]context.CancelFunc
|
||||
issueTaskMu sync.Mutex
|
||||
@ -135,6 +137,7 @@ func New(deps Dependencies) *Server {
|
||||
audit: deps.Audit,
|
||||
tasks: deps.Tasks,
|
||||
readinessCheck: deps.ReadinessCheck,
|
||||
aliyunChecker: deps.AliyunChecker,
|
||||
taskCancels: make(map[string]context.CancelFunc),
|
||||
resetLimiter: newResetRateLimiter(),
|
||||
authSessions: make(map[string]authSession),
|
||||
@ -180,6 +183,8 @@ func (s *Server) registerRoutes() {
|
||||
adminAPI.PUT("/settings", s.updateSettings)
|
||||
adminAPI.GET("/dns/providers", s.listAdminDNSProviders)
|
||||
adminAPI.POST("/dns/test", s.testAdminDNS)
|
||||
adminAPI.GET("/dns/aliyun/scope", s.getAliyunDNSScope)
|
||||
adminAPI.POST("/dns/aliyun/check", s.checkAliyunDNSDomain)
|
||||
adminAPI.POST("/renewal/run", s.runRenewalNow)
|
||||
adminAPI.GET("/certificates", s.listCertificates)
|
||||
adminAPI.GET("/certificates/:id", s.getCertificate)
|
||||
@ -453,6 +458,16 @@ func (s *Server) createDomain(c *gin.Context) {
|
||||
s.writeError(c, http.StatusBadRequest, "域名配置无效", err)
|
||||
return
|
||||
}
|
||||
if err := s.validateDomainAliyunAuthorization(c.Request.Context(), item.Name); err != nil {
|
||||
s.writeError(c, http.StatusBadRequest, err.Error(), err)
|
||||
return
|
||||
}
|
||||
for _, san := range item.SANs {
|
||||
if err := s.validateDomainAliyunAuthorization(c.Request.Context(), san); err != nil {
|
||||
s.writeError(c, http.StatusBadRequest, err.Error(), err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if err := s.repository.Create(c.Request.Context(), item); err != nil {
|
||||
if repository.IsConflict(err) {
|
||||
s.writeError(c, http.StatusConflict, "域名已经存在", err)
|
||||
@ -487,6 +502,16 @@ func (s *Server) updateDomain(c *gin.Context) {
|
||||
s.writeError(c, http.StatusBadRequest, "域名配置无效", err)
|
||||
return
|
||||
}
|
||||
if err := s.validateDomainAliyunAuthorization(c.Request.Context(), item.Name); err != nil {
|
||||
s.writeError(c, http.StatusBadRequest, err.Error(), err)
|
||||
return
|
||||
}
|
||||
for _, san := range item.SANs {
|
||||
if err := s.validateDomainAliyunAuthorization(c.Request.Context(), san); err != nil {
|
||||
s.writeError(c, http.StatusBadRequest, err.Error(), err)
|
||||
return
|
||||
}
|
||||
}
|
||||
item.ID = existing.ID
|
||||
item.CreatedAt = existing.CreatedAt
|
||||
item.Status = "pending"
|
||||
@ -764,4 +789,4 @@ func (s *Server) writeError(c *gin.Context, status int, message string, err erro
|
||||
s.logger.Error(message, "error", err)
|
||||
}
|
||||
c.JSON(status, gin.H{"error": message})
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user