权限整改:证书工作流补齐部署执行权限

证书申请任务和证书导入接口同时校验 certificate.manage 与 deployment.execute,补充权限拒绝回归测试并保持项目资源权限边界。
This commit is contained in:
xuans 2026-09-29 19:37:27 +08:00
parent 07f86f1413
commit e514391da7
6 changed files with 111 additions and 5 deletions

View File

@ -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 {

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}