From e514391da7d443e73a19985e4d39f11287065f10 Mon Sep 17 00:00:00 2001 From: xuans Date: Tue, 29 Sep 2026 19:37:27 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9D=83=E9=99=90=E6=95=B4=E6=94=B9=EF=BC=9A?= =?UTF-8?q?=E8=AF=81=E4=B9=A6=E5=B7=A5=E4=BD=9C=E6=B5=81=E8=A1=A5=E9=BD=90?= =?UTF-8?q?=E9=83=A8=E7=BD=B2=E6=89=A7=E8=A1=8C=E6=9D=83=E9=99=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 证书申请任务和证书导入接口同时校验 certificate.manage 与 deployment.execute,补充权限拒绝回归测试并保持项目资源权限边界。 --- backend/internal/httpapi/certificate_api.go | 2 +- .../internal/httpapi/certificate_api_test.go | 39 +++++++++++++++++++ .../internal/httpapi/project_resources_api.go | 24 ++++++++++-- backend/internal/httpapi/task_api.go | 8 ++++ backend/internal/httpapi/task_api_test.go | 37 ++++++++++++++++++ backend/internal/httpapi/test_helpers_test.go | 6 ++- 6 files changed, 111 insertions(+), 5 deletions(-) diff --git a/backend/internal/httpapi/certificate_api.go b/backend/internal/httpapi/certificate_api.go index 40757c1..622a45f 100644 --- a/backend/internal/httpapi/certificate_api.go +++ b/backend/internal/httpapi/certificate_api.go @@ -142,7 +142,7 @@ func (s *Server) deployCertificateCurrent(c *gin.Context) { } func (s *Server) importCertificateFiles(c *gin.Context) { - if !s.authorizeVisibleDomainPermission(c, domain.PermissionCertificateManage) { + if !s.authorizeVisibleDomainPermissions(c, domain.PermissionCertificateManage, domain.PermissionDeploymentExecute) { return } if s.importCertificate == nil { diff --git a/backend/internal/httpapi/certificate_api_test.go b/backend/internal/httpapi/certificate_api_test.go index db023ed..8a8e4f7 100644 --- a/backend/internal/httpapi/certificate_api_test.go +++ b/backend/internal/httpapi/certificate_api_test.go @@ -405,3 +405,42 @@ func TestCertificateMutationAPIsEnforceProjectPermissions(t *testing.T) { t.Fatalf("certificate workflow was called %d times after authorization failure", calls) } } + +func TestCertificateImportRequiresDeploymentPermission(t *testing.T) { + repo := &stubDomainRepository{ + loaded: domain.Domain{ + ID: "domain-1", + ResourceScope: domain.ResourceScope{ProjectID: domain.DefaultProjectID}, + }, + } + calls := 0 + server := New(Dependencies{ + Config: readyTestConfig(t.TempDir()), + Repository: repo, + ProjectResources: projectResourceReaderStub{domains: repo}, + ProjectDomains: projectDomainRepositoryStub{ + DomainRepository: repo, + authorize: func(_ context.Context, _ string, _ string, permission domain.ProjectPermission) error { + if permission == domain.PermissionDeploymentExecute { + return repository.ErrProjectAccessDenied + } + return nil + }, + }, + ImportCertificate: func(context.Context, certificate.ImportRequest) (certificate.IssueResult, deployment.Report, error) { + calls++ + return certificate.IssueResult{}, deployment.Report{}, nil + }, + }) + + request := httptest.NewRequest(http.MethodPost, "/api/v1/domains/domain-1/certificate/import", strings.NewReader(`{"certificate_path":"/ssl/cert.pem","private_key_path":"/ssl/key.pem"}`)) + request.Header.Set("Content-Type", "application/json") + response := httptest.NewRecorder() + serveAdminHTTP(server, response, request) + if response.Code != http.StatusForbidden { + t.Fatalf("缺少部署权限时证书导入未拒绝:%d %s", response.Code, response.Body.String()) + } + if calls != 0 { + t.Fatalf("权限拒绝后仍调用证书导入工作流:%d", calls) + } +} diff --git a/backend/internal/httpapi/project_resources_api.go b/backend/internal/httpapi/project_resources_api.go index ae655fc..bae968a 100644 --- a/backend/internal/httpapi/project_resources_api.go +++ b/backend/internal/httpapi/project_resources_api.go @@ -59,6 +59,22 @@ func (s *Server) authorizeVisibleDomainPermission(c *gin.Context, permission dom s.writeProjectResourceError(c, err) return false } + return s.authorizeProjectPermissions(c, item.ProjectID, permission) +} + +func (s *Server) authorizeVisibleDomainPermissions(c *gin.Context, permissions ...domain.ProjectPermission) bool { + if !s.requireProjectResourceReader(c) { + return false + } + item, err := s.projectResources.GetVisibleDomain(c.Request.Context(), c.GetString(authenticatedActorKey), c.Param("id")) + if err != nil { + s.writeProjectResourceError(c, err) + return false + } + return s.authorizeProjectPermissions(c, item.ProjectID, permissions...) +} + +func (s *Server) authorizeProjectPermissions(c *gin.Context, projectID string, permissions ...domain.ProjectPermission) bool { authorizer, ok := s.projectDomains.(repository.ProjectAuthorizer) if !ok { if fallback, fallbackOK := s.projectMembers.(repository.ProjectAuthorizer); fallbackOK { @@ -70,9 +86,11 @@ func (s *Server) authorizeVisibleDomainPermission(c *gin.Context, permission dom s.writeProjectResourceError(c, errors.New("project authorizer unavailable")) return false } - if err := authorizer.AuthorizeProject(c.Request.Context(), c.GetString(authenticatedActorKey), item.ProjectID, permission); err != nil { - s.writeProjectResourceError(c, err) - return false + for _, permission := range permissions { + if err := authorizer.AuthorizeProject(c.Request.Context(), c.GetString(authenticatedActorKey), projectID, permission); err != nil { + s.writeProjectResourceError(c, err) + return false + } } return true } diff --git a/backend/internal/httpapi/task_api.go b/backend/internal/httpapi/task_api.go index 3e1ec60..b2e7b20 100644 --- a/backend/internal/httpapi/task_api.go +++ b/backend/internal/httpapi/task_api.go @@ -52,6 +52,14 @@ func (s *Server) issueDomainAsTask(c *gin.Context) { s.writeError(c, http.StatusInternalServerError, "检查证书任务权限失败", err) return } + if err := authorizer.AuthorizeProject(c.Request.Context(), c.GetString(authenticatedActorKey), resource.ResourceScope.Normalize().ProjectID, domain.PermissionDeploymentExecute); err != nil { + if errors.Is(err, repository.ErrProjectAccessDenied) { + s.writeError(c, http.StatusForbidden, "无权执行此项目的证书部署任务", nil) + return + } + s.writeError(c, http.StatusInternalServerError, "检查证书部署权限失败", err) + return + } if !s.authorizeDNSCredentialUse(c, resource.ResourceScope.Normalize().ProjectID) { return } diff --git a/backend/internal/httpapi/task_api_test.go b/backend/internal/httpapi/task_api_test.go index 71c6399..0c9389d 100644 --- a/backend/internal/httpapi/task_api_test.go +++ b/backend/internal/httpapi/task_api_test.go @@ -120,6 +120,43 @@ func TestTaskAPIRecordsSuccessfulIssueAndAudit(t *testing.T) { } } +func TestIssueTaskRequiresDeploymentPermission(t *testing.T) { + calls := 0 + server, _, _, closeDB := newTaskAPITestServer(t, func(context.Context, string) (certificate.IssueResult, deployment.Report, error) { + calls++ + return certificate.IssueResult{}, deployment.Report{}, nil + }) + defer closeDB() + + server.projectMembers = permissionProjectMemberRepository{ + ProjectMemberRepository: server.projectMembers, + allow: func(permission domain.ProjectPermission) error { + if permission == domain.PermissionDeploymentExecute { + return repository.ErrProjectAccessDenied + } + return nil + }, + } + + response := httptest.NewRecorder() + serveAdminHTTP(server, response, httptest.NewRequest(http.MethodPost, "/api/v1/domains/domain-1/issue", nil)) + if response.Code != http.StatusForbidden { + t.Fatalf("缺少部署权限时证书任务未拒绝:%d %s", response.Code, response.Body.String()) + } + if calls != 0 { + t.Fatalf("权限拒绝后仍调用证书申请工作流:%d", calls) + } +} + +type permissionProjectMemberRepository struct { + repository.ProjectMemberRepository + allow func(domain.ProjectPermission) error +} + +func (p permissionProjectMemberRepository) AuthorizeProject(_ context.Context, _ string, _ string, permission domain.ProjectPermission) error { + return p.allow(permission) +} + type failingTaskStartRepository struct { repository.TaskRepository } diff --git a/backend/internal/httpapi/test_helpers_test.go b/backend/internal/httpapi/test_helpers_test.go index 4a740f5..30a3bdb 100644 --- a/backend/internal/httpapi/test_helpers_test.go +++ b/backend/internal/httpapi/test_helpers_test.go @@ -16,9 +16,13 @@ import ( type projectDomainRepositoryStub struct { repository.DomainRepository authorizeErr error + authorize func(context.Context, string, string, domain.ProjectPermission) error } -func (r projectDomainRepositoryStub) AuthorizeProject(context.Context, string, string, domain.ProjectPermission) error { +func (r projectDomainRepositoryStub) AuthorizeProject(ctx context.Context, actorID, projectID string, permission domain.ProjectPermission) error { + if r.authorize != nil { + return r.authorize(ctx, actorID, projectID, permission) + } return r.authorizeErr }