From 56c88c3ec285fba7545c46890c87be6017f9c7e9 Mon Sep 17 00:00:00 2001 From: xuans Date: Tue, 29 Sep 2026 19:05:52 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9D=83=E9=99=90=E6=95=B4=E6=94=B9=EF=BC=9A?= =?UTF-8?q?=E5=AE=8C=E6=88=90=20SSH=20=E7=A7=81=E9=92=A5=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E5=BD=92=E5=B1=9E=E9=9A=94=E7=A6=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为 SSH 私钥记录项目归属,限制服务器引用及私钥列表、新增、删除操作的项目权限,兼容历史默认项目数据并补充专项测试。 --- backend/internal/httpapi/server.go | 8 ++-- backend/internal/httpapi/ssh_key.go | 30 ++++++++++++++- backend/internal/sshkey/service.go | 49 ++++++++++++++++++++++++- backend/internal/sshkey/service_test.go | 21 +++++++++++ 4 files changed, 100 insertions(+), 8 deletions(-) diff --git a/backend/internal/httpapi/server.go b/backend/internal/httpapi/server.go index 80cf39f..656582a 100644 --- a/backend/internal/httpapi/server.go +++ b/backend/internal/httpapi/server.go @@ -873,11 +873,11 @@ func (s *Server) updateServer(c *gin.Context) { c.JSON(http.StatusOK, item) } -func (s *Server) validateSSHKeyReference(ctx context.Context, reference string) error { +func (s *Server) validateSSHKeyReference(ctx context.Context, reference, projectID string) error { if s.sshKeys == nil { return errors.New("SSH 私钥管理服务未配置") } - _, err := s.sshKeys.ValidateReference(ctx, reference) + _, err := s.sshKeys.ValidateReferenceForProject(ctx, reference, projectID) return err } @@ -885,7 +885,7 @@ func (s *Server) prepareServerCredential(ctx context.Context, item deployment.Se item = item.Normalize() cleanup := func() {} if item.AuthMethod == deployment.SSHAuthPrivateKey { - if err := s.validateSSHKeyReference(ctx, item.PrivateKeyRef); err != nil { + if err := s.validateSSHKeyReference(ctx, item.PrivateKeyRef, item.ProjectID); err != nil { return deployment.Server{}, cleanup, err } if existing != nil && item.PasswordRef == "" { @@ -915,7 +915,7 @@ func (s *Server) prepareServerCredential(ctx context.Context, item deployment.Se } } if item.PrivateKeyRef == "" && s.sshKeys != nil && s.deploymentFactory != nil { - metadata, _, err := s.sshKeys.CreateGenerated(ctx, "服务器 "+item.Name+" SSH密钥") + metadata, _, err := s.sshKeys.CreateGeneratedForProject(ctx, "服务器 "+item.Name+" SSH密钥", item.ProjectID) if err != nil { return deployment.Server{}, cleanup, fmt.Errorf("生成 SSH 密钥失败:%w", err) } diff --git a/backend/internal/httpapi/ssh_key.go b/backend/internal/httpapi/ssh_key.go index c014b4f..4c51272 100644 --- a/backend/internal/httpapi/ssh_key.go +++ b/backend/internal/httpapi/ssh_key.go @@ -3,8 +3,11 @@ package httpapi import ( "errors" "net/http" + "strings" "github.com/gin-gonic/gin" + + "git.pcux.cn/xuans/Scert/backend/internal/domain" ) const maxSSHKeyRequestBytes = 2 * 1024 * 1024 @@ -13,6 +16,7 @@ type sshKeyRequest struct { Name string `json:"name" binding:"required"` PrivateKey string `json:"private_key" binding:"required"` Passphrase string `json:"passphrase"` + ProjectID string `json:"project_id"` } func (s *Server) listSSHKeys(c *gin.Context) { @@ -20,7 +24,14 @@ func (s *Server) listSSHKeys(c *gin.Context) { s.writeError(c, http.StatusNotImplemented, "SSH 私钥管理接口尚未配置", errors.New("SSH 私钥管理服务未配置")) return } - items, err := s.sshKeys.List(c.Request.Context()) + projectID := strings.TrimSpace(c.Query("project_id")) + if projectID == "" { + projectID = domain.DefaultProjectID + } + if !s.authorizeServerMutation(c, projectID) { + return + } + items, err := s.sshKeys.ListForProject(c.Request.Context(), projectID) if err != nil { s.writeError(c, http.StatusInternalServerError, "查询 SSH 私钥失败", err) return @@ -43,7 +54,14 @@ func (s *Server) createSSHKey(c *gin.Context) { s.writeError(c, http.StatusBadRequest, "请求参数无效", err) return } - item, err := s.sshKeys.Create(c.Request.Context(), request.Name, request.PrivateKey, request.Passphrase) + projectID := strings.TrimSpace(request.ProjectID) + if projectID == "" { + projectID = domain.DefaultProjectID + } + if !s.authorizeServerMutation(c, projectID) { + return + } + item, err := s.sshKeys.CreateForProject(c.Request.Context(), request.Name, request.PrivateKey, request.Passphrase, projectID) if err != nil { s.writeError(c, http.StatusBadRequest, "SSH 私钥无效", err) return @@ -61,6 +79,14 @@ func (s *Server) deleteSSHKey(c *gin.Context) { s.writeError(c, http.StatusBadRequest, "SSH 私钥 ID 无效", err) return } + metadata, err := s.sshKeys.ValidateReference(c.Request.Context(), reference) + if err != nil { + s.writeError(c, http.StatusBadRequest, "SSH 私钥引用无效", err) + return + } + if !s.authorizeServerMutation(c, metadata.ProjectID) { + return + } used, err := s.servers.IsPrivateKeyRefUsed(c.Request.Context(), reference) if err != nil { s.writeError(c, http.StatusInternalServerError, "检查 SSH 私钥占用状态失败", err) diff --git a/backend/internal/sshkey/service.go b/backend/internal/sshkey/service.go index a46cc64..f1b9d8a 100644 --- a/backend/internal/sshkey/service.go +++ b/backend/internal/sshkey/service.go @@ -39,6 +39,7 @@ type Metadata struct { Algorithm string `json:"algorithm"` Fingerprint string `json:"fingerprint"` UpdatedAt time.Time `json:"updated_at"` + ProjectID string `json:"project_id,omitempty"` } type Service struct { @@ -50,9 +51,14 @@ func NewService(store Store) *Service { } func (s *Service) Create(ctx context.Context, name, privateKey, passphrase string) (Metadata, error) { + return s.CreateForProject(ctx, name, privateKey, passphrase, domain.DefaultProjectID) +} + +func (s *Service) CreateForProject(ctx context.Context, name, privateKey, passphrase, projectID string) (Metadata, error) { if s == nil || s.store == nil { return Metadata{}, errors.New("SSH 私钥存储未初始化") } + projectID = normalizeProjectID(projectID) name = strings.TrimSpace(name) if name == "" { return Metadata{}, errors.New("SSH 私钥名称不能为空") @@ -83,17 +89,22 @@ func (s *Service) Create(ctx context.Context, name, privateKey, passphrase strin "passphrase": passphrase, "algorithm": signer.PublicKey().Type(), "fingerprint": ssh.FingerprintSHA256(signer.PublicKey()), + "project_id": projectID, } if err := s.store.Put(ctx, reference, payload); err != nil { return Metadata{}, fmt.Errorf("保存 SSH 私钥失败:%w", err) } return Metadata{ ID: id, Name: name, Reference: reference, Algorithm: payload["algorithm"], - Fingerprint: payload["fingerprint"], UpdatedAt: time.Now().UTC(), + Fingerprint: payload["fingerprint"], UpdatedAt: time.Now().UTC(), ProjectID: projectID, }, nil } func (s *Service) CreateGenerated(ctx context.Context, name string) (Metadata, string, error) { + return s.CreateGeneratedForProject(ctx, name, domain.DefaultProjectID) +} + +func (s *Service) CreateGeneratedForProject(ctx context.Context, name, projectID string) (Metadata, string, error) { if s == nil || s.store == nil { return Metadata{}, "", errors.New("SSH 私钥存储未初始化") } @@ -109,7 +120,7 @@ func (s *Service) CreateGenerated(ctx context.Context, name string) (Metadata, s if err != nil { return Metadata{}, "", fmt.Errorf("编码 SSH 私钥失败:%w", err) } - metadata, err := s.Create(ctx, name, string(pem.EncodeToMemory(privatePEM)), "") + metadata, err := s.CreateForProject(ctx, name, string(pem.EncodeToMemory(privatePEM)), "", projectID) if err != nil { return Metadata{}, "", err } @@ -164,6 +175,20 @@ func (s *Service) List(ctx context.Context) ([]Metadata, error) { return result, nil } +func (s *Service) ListForProject(ctx context.Context, projectID string) ([]Metadata, error) { + items, err := s.List(ctx) + if err != nil { + return nil, err + } + projectID = normalizeProjectID(projectID) + visible := make([]Metadata, 0, len(items)) + for _, item := range items { + if normalizeProjectID(item.ProjectID) == projectID { + visible = append(visible, item) + } + } + return visible, nil +} func (s *Service) ValidateReference(ctx context.Context, reference string) (Metadata, error) { if s == nil || s.store == nil { return Metadata{}, errors.New("SSH 私钥存储未初始化") @@ -171,6 +196,17 @@ func (s *Service) ValidateReference(ctx context.Context, reference string) (Meta return s.metadata(ctx, strings.TrimSpace(reference), time.Time{}, true) } +func (s *Service) ValidateReferenceForProject(ctx context.Context, reference, projectID string) (Metadata, error) { + metadata, err := s.ValidateReference(ctx, reference) + if err != nil { + return Metadata{}, err + } + if normalizeProjectID(metadata.ProjectID) != normalizeProjectID(projectID) { + return Metadata{}, errors.New("SSH 私钥不属于当前项目") + } + return metadata, nil +} + func (s *Service) Delete(ctx context.Context, id string) error { if s == nil || s.store == nil { return errors.New("SSH 私钥存储未初始化") @@ -220,9 +256,18 @@ func (s *Service) metadata(ctx context.Context, reference string, updatedAt time return Metadata{ ID: id, Name: payload["name"], Reference: reference, Algorithm: algorithm, Fingerprint: fingerprint, UpdatedAt: updatedAt, + ProjectID: normalizeProjectID(payload["project_id"]), }, nil } +func normalizeProjectID(value string) string { + value = strings.TrimSpace(value) + if value == "" { + return domain.DefaultProjectID + } + return value +} + func parsePrivateKey(privateKey, passphrase string) (ssh.Signer, error) { var ( signer ssh.Signer diff --git a/backend/internal/sshkey/service_test.go b/backend/internal/sshkey/service_test.go index 33d4941..7719c93 100644 --- a/backend/internal/sshkey/service_test.go +++ b/backend/internal/sshkey/service_test.go @@ -141,3 +141,24 @@ func testEncryptedPrivateKey(t *testing.T, passphrase string) string { } return string(pem.EncodeToMemory(block)) } + +func TestServiceScopesSSHKeyToProject(t *testing.T) { + store, err := secretstore.Open(t.TempDir()) + if err != nil { + t.Fatalf("打开敏感信息存储失败:%v", err) + } + service := NewService(store) + created, err := service.CreateForProject(t.Context(), "项目部署私钥", testPrivateKey(t), "", "project-a") + if err != nil { + t.Fatalf("创建项目 SSH 私钥失败:%v", err) + } + if created.ProjectID != "project-a" { + t.Fatalf("SSH 私钥项目归属错误:%+v", created) + } + if _, err := service.ValidateReferenceForProject(t.Context(), created.Reference, "project-a"); err != nil { + t.Fatalf("同项目 SSH 私钥校验失败:%v", err) + } + if _, err := service.ValidateReferenceForProject(t.Context(), created.Reference, "project-b"); err == nil { + t.Fatal("跨项目 SSH 私钥引用不应通过校验") + } +}