权限整改:完成证书资源项目权限隔离
将证书导入、校验、部署和历史版本恢复统一接入项目资源权限校验,并补充无权限阻断测试及清单进度记录。
This commit is contained in:
parent
85593bcde7
commit
f5ab9c1610
@ -13,6 +13,7 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/certificate"
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/domain"
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/repository"
|
||||
)
|
||||
|
||||
@ -92,6 +93,9 @@ func (s *Server) listCertificateVersions(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (s *Server) validateCertificate(c *gin.Context) {
|
||||
if !s.authorizeVisibleDomainPermission(c, domain.PermissionPrivateKeyRead) {
|
||||
return
|
||||
}
|
||||
if s.currentCertificate == nil {
|
||||
s.writeError(c, http.StatusNotImplemented, "证书校验接口尚未配置", errors.New("证书服务未配置"))
|
||||
return
|
||||
@ -116,6 +120,9 @@ func (s *Server) validateCertificate(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (s *Server) deployCertificateCurrent(c *gin.Context) {
|
||||
if !s.authorizeVisibleDomainPermission(c, domain.PermissionDeploymentExecute) {
|
||||
return
|
||||
}
|
||||
if s.deployCertificate == nil {
|
||||
s.writeError(c, http.StatusNotImplemented, "证书部署接口尚未配置", errors.New("证书部署流程未配置"))
|
||||
return
|
||||
@ -135,6 +142,9 @@ func (s *Server) deployCertificateCurrent(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (s *Server) importCertificateFiles(c *gin.Context) {
|
||||
if !s.authorizeVisibleDomainPermission(c, domain.PermissionCertificateManage) {
|
||||
return
|
||||
}
|
||||
if s.importCertificate == nil {
|
||||
s.writeError(c, http.StatusNotImplemented, "证书导入接口尚未配置", errors.New("证书导入流程未配置"))
|
||||
return
|
||||
|
||||
@ -17,6 +17,7 @@ import (
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/certificate"
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/deployment"
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/domain"
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/repository"
|
||||
)
|
||||
|
||||
type certificateReaderStub struct {
|
||||
@ -355,3 +356,52 @@ func TestCertificateImportAPISanitizesReportDetails(t *testing.T) {
|
||||
t.Fatalf("证书导入响应缺少安全的服务器信息:%s", responseBody)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCertificateMutationAPIsEnforceProjectPermissions(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,
|
||||
authorizeErr: repository.ErrProjectAccessDenied,
|
||||
},
|
||||
CurrentCertificate: certificateReaderStub{},
|
||||
DeployCertificate: func(context.Context, string) (certificate.IssueResult, deployment.Report, error) {
|
||||
calls++
|
||||
return certificate.IssueResult{}, deployment.Report{}, nil
|
||||
},
|
||||
ImportCertificate: func(context.Context, certificate.ImportRequest) (certificate.IssueResult, deployment.Report, error) {
|
||||
calls++
|
||||
return certificate.IssueResult{}, deployment.Report{}, nil
|
||||
},
|
||||
RestoreCertificateVersion: func(context.Context, string, string) (certificate.IssueResult, error) {
|
||||
calls++
|
||||
return certificate.IssueResult{}, nil
|
||||
},
|
||||
})
|
||||
|
||||
requests := []*http.Request{
|
||||
httptest.NewRequest(http.MethodPost, "/api/v1/domains/domain-1/deploy", nil),
|
||||
httptest.NewRequest(http.MethodPost, "/api/v1/domains/domain-1/certificate/validate", nil),
|
||||
httptest.NewRequest(http.MethodPost, "/api/v1/domains/domain-1/certificate/import", nil),
|
||||
httptest.NewRequest(http.MethodPost, "/api/v1/domains/domain-1/certificate/versions/v1/restore", nil),
|
||||
}
|
||||
for _, request := range requests {
|
||||
response := httptest.NewRecorder()
|
||||
serveAdminHTTP(server, response, request)
|
||||
if response.Code != http.StatusForbidden {
|
||||
t.Fatalf("unauthorized certificate mutation returned %d: %s", response.Code, response.Body.String())
|
||||
}
|
||||
}
|
||||
if calls != 0 {
|
||||
t.Fatalf("certificate workflow was called %d times after authorization failure", calls)
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,10 +7,14 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/domain"
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/repository"
|
||||
)
|
||||
|
||||
func (s *Server) restoreCertificateVersion(c *gin.Context) {
|
||||
if !s.authorizeVisibleDomainPermission(c, domain.PermissionDeploymentRollback) {
|
||||
return
|
||||
}
|
||||
if s.restoreVersion == nil {
|
||||
s.writeError(c, http.StatusNotImplemented, "证书恢复接口尚未配置", errors.New("证书恢复流程未配置"))
|
||||
return
|
||||
|
||||
@ -44,9 +44,12 @@ func newRestoreAPITestServer(t *testing.T, restore func(context.Context, string,
|
||||
cfg.Admin.Username = "admin"
|
||||
cfg.Admin.Email = "admin@example.test"
|
||||
cfg.Admin.PasswordHash = httpUserPasswordHash(t, "RestoreTest123")
|
||||
repo := &stubDomainRepository{loaded: domain.Domain{ID: "domain-1", ResourceScope: domain.ResourceScope{ProjectID: domain.DefaultProjectID}}}
|
||||
server := New(Dependencies{
|
||||
Config: cfg,
|
||||
Repository: &stubDomainRepository{},
|
||||
Repository: repo,
|
||||
ProjectResources: projectResourceReaderStub{domains: repo},
|
||||
ProjectDomains: projectDomainRepositoryStub{DomainRepository: repo},
|
||||
CurrentCertificate: restoreCertificateReader{},
|
||||
RestoreCertificateVersion: restore,
|
||||
Audit: audit,
|
||||
|
||||
@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/domain"
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/repository"
|
||||
)
|
||||
|
||||
@ -48,3 +49,30 @@ func (s *Server) requireVisibleCertificateDomain(c *gin.Context) bool {
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *Server) authorizeVisibleDomainPermission(c *gin.Context, permission 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
|
||||
}
|
||||
authorizer, ok := s.projectDomains.(repository.ProjectAuthorizer)
|
||||
if !ok {
|
||||
if fallback, fallbackOK := s.projectMembers.(repository.ProjectAuthorizer); fallbackOK {
|
||||
authorizer = fallback
|
||||
ok = true
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
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
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -226,10 +226,10 @@ func (s *Server) registerRoutes() {
|
||||
adminAPI.POST("/dns/test", s.dnsOperation, s.testAdminDNS)
|
||||
adminAPI.POST("/dns/aliyun/check", s.dnsOperation, s.checkAliyunDNSDomain)
|
||||
adminAPI.POST("/renewal/run", s.longOperation(certificateTaskTimeout), s.runRenewalNow)
|
||||
adminAPI.POST("/domains/:id/certificate/import", s.longOperation(certificateTaskTimeout), s.importCertificateFiles)
|
||||
adminAPI.POST("/domains/:id/certificate/validate", s.longOperation(certificateTaskTimeout), s.validateCertificate)
|
||||
adminAPI.POST("/domains/:id/deploy", s.longOperation(certificateTaskTimeout), s.deployCertificateCurrent)
|
||||
adminAPI.POST("/domains/:id/certificate/versions/:version/restore", s.longOperation(certificateTaskTimeout), s.restoreCertificateVersion)
|
||||
domainAPI.POST("/domains/:id/certificate/import", s.longOperation(certificateTaskTimeout), s.importCertificateFiles)
|
||||
domainAPI.POST("/domains/:id/certificate/validate", s.longOperation(certificateTaskTimeout), s.validateCertificate)
|
||||
domainAPI.POST("/domains/:id/deploy", s.longOperation(certificateTaskTimeout), s.deployCertificateCurrent)
|
||||
domainAPI.POST("/domains/:id/certificate/versions/:version/restore", s.longOperation(certificateTaskTimeout), s.restoreCertificateVersion)
|
||||
adminAPI.POST("/servers", s.createServer)
|
||||
adminAPI.PUT("/servers/:id", s.updateServer)
|
||||
adminAPI.DELETE("/servers/:id", s.deleteServer)
|
||||
|
||||
@ -75,6 +75,15 @@ func readyTestConfig(directory string) config.Config {
|
||||
}
|
||||
|
||||
func serveAdminHTTP(server *Server, recorder *httptest.ResponseRecorder, request *http.Request) {
|
||||
if server.projectResources == nil {
|
||||
if repo, ok := server.repository.(*stubDomainRepository); ok {
|
||||
if repo.loaded.ID == "" {
|
||||
repo.loaded = domain.Domain{ID: "domain-1", ResourceScope: domain.ResourceScope{ProjectID: domain.DefaultProjectID}}
|
||||
}
|
||||
server.projectResources = projectResourceReaderStub{domains: repo}
|
||||
server.projectDomains = projectDomainRepositoryStub{DomainRepository: repo}
|
||||
}
|
||||
}
|
||||
request.AddCookie(httpUserSessionCookie(server, "test-admin", repository.UserRoleAdmin))
|
||||
server.Handler().ServeHTTP(recorder, request)
|
||||
}
|
||||
|
||||
@ -3379,3 +3379,13 @@ Linux Ubuntu 22.04 上的 `Scert-admin` 手动启停、无开机自启动、无
|
||||
- [!] PostgreSQL/MySQL 矩阵、浏览器、Linux/Windows 服务、远端 SSH、HTTPS/DNS/ACME 和生产验收仍未执行。
|
||||
|
||||
下一步:继续补齐任务与证书/部署权限链,完成开发阶段后再统一运行三库、浏览器和远端生产流程测试。
|
||||
|
||||
|
||||
## ???????2026-09-29 18:38?
|
||||
|
||||
???????? **P0-03 ?????**?????????? P0-07?
|
||||
|
||||
- [x] ??????????????????????????????????
|
||||
- [x] ???????????????? HTTP ?????
|
||||
- [~] ????SSH?DNS????????????????????????????????????????????
|
||||
- [!] ????????????????????????????
|
||||
|
||||
@ -680,3 +680,13 @@ P0-05 从 `[!]` 调整为 `[~]`,并未标记完成;P0-04 剩余门禁和全
|
||||
- [!] PostgreSQL/MySQL 矩阵、浏览器、Linux/Windows 服务、远端 SSH、HTTPS/DNS/ACME 和生产验收仍未执行。
|
||||
|
||||
下一步:继续补齐任务与证书/部署权限链,完成开发阶段后再统一运行三库、浏览器和远端生产流程测试。
|
||||
|
||||
|
||||
## ???????2026-09-29 18:38?
|
||||
|
||||
???????????? **P0-03 ?????**??????? P0-07??????????????????????
|
||||
|
||||
- [x] ??????????????????????????????????????
|
||||
- [x] ?????? `certificate.manage`??????? `certificate.private_key.read`????????? `deployment.execute`????????? `deployment.rollback`?
|
||||
- [x] ????????????????? HTTP ?????`go test ./internal/httpapi` ????
|
||||
- [~] ????SSH ???DNS?????????????????????????????? SSH?HTTPS/DNS/ACME ????????????
|
||||
|
||||
@ -1318,3 +1318,11 @@ P0-11 生产验收门禁
|
||||
- [!] PostgreSQL/MySQL 矩阵、浏览器、Linux/Windows 服务、远端 SSH、HTTPS/DNS/ACME 和生产验收仍未执行。
|
||||
|
||||
下一步:继续补齐任务与证书/部署权限链,完成开发阶段后再统一运行三库、浏览器和远端生产流程测试。
|
||||
|
||||
|
||||
## ???????2026-09-29 18:38?
|
||||
|
||||
- **???????P0-03 ??????**
|
||||
- **P0-07?????** ?????????????????????????????????????????????????
|
||||
- P0-07 ????????SSH?DNS??????????????????????????????????????? HTTPS/DNS/ACME ???
|
||||
- ????????????????????????????????
|
||||
|
||||
@ -274,3 +274,6 @@ P0-06 与 P0-07 均维持 `[~]`,六阶段中的隔离、生产和最终验收
|
||||
- [!] PostgreSQL/MySQL 矩阵、浏览器、Linux/Windows 服务、远端 SSH、HTTPS/DNS/ACME 和生产验收仍未执行。
|
||||
|
||||
下一步:继续补齐任务与证书/部署权限链,完成开发阶段后再统一运行三库、浏览器和远端生产流程测试。
|
||||
|
||||
|
||||
> ???????2026-09-29 18:38???????? **P0-03 ?????**?P0-07 ?????????????????????????????????????? HTTP ?????????????SSH?DNS??????????????????????????????????
|
||||
|
||||
Loading…
Reference in New Issue
Block a user