diff --git a/backend/internal/app/app.go b/backend/internal/app/app.go index eb95631..ffcbb72 100644 --- a/backend/internal/app/app.go +++ b/backend/internal/app/app.go @@ -21,7 +21,6 @@ import ( "git.pcux.cn/xuans/Scert/backend/internal/repository" "git.pcux.cn/xuans/Scert/backend/internal/secretstore" "git.pcux.cn/xuans/Scert/backend/internal/setup" - "git.pcux.cn/xuans/Scert/backend/internal/sshkey" ) type App struct { @@ -34,7 +33,6 @@ type App struct { Tasks *repository.SQLTaskRepository Deployment *deployment.Service SecretStore *secretstore.Store - SSHKeys *sshkey.Service DNSVerification *setup.DNSVerificationManager Certificates *certificate.Service Workflow *CertificateWorkflow @@ -140,7 +138,6 @@ func NewWithConfigPath(ctx context.Context, cfg config.Config, configPath string } else if deleted > 0 { logger.Info("启动时已清理过期审计日志", "count", deleted) } - sshKeys := sshkey.NewService(secretStore) accounts := certificate.NewAccountRepository(secretStore) certificates := certificate.NewService(cfg, repo, secretStore, dnsprovider.NewLegoFactory(), accounts) aliyunAuthChecker := dnsprovider.NewAliyunAuthorizationChecker() @@ -177,7 +174,7 @@ func NewWithConfigPath(ctx context.Context, cfg config.Config, configPath string RetryInterval: time.Duration(cfg.ACME.RenewalRetryIntervalMins) * time.Minute, }, management) server = httpapi.New(httpapi.Dependencies{ - Config: cfg, ConfigPath: configPath, Logger: logger, Repository: repo, Servers: serverRepo, Users: userRepo, SSHKeys: sshKeys, DeploymentFactory: deployment.SSHFactory{Secrets: secretStore}, + Config: cfg, ConfigPath: configPath, Logger: logger, Repository: repo, Servers: serverRepo, Users: userRepo, DeploymentFactory: deployment.SSHFactory{Secrets: secretStore}, ProjectMembers: repository.NewSQLProjectMemberRepository(db.DB, runtimeDatabaseConfig.Driver), ProjectDirectory: repository.NewSQLProjectMemberRepository(db.DB, runtimeDatabaseConfig.Driver), ProjectLifecycle: repository.NewSQLProjectMemberRepository(db.DB, runtimeDatabaseConfig.Driver), @@ -234,7 +231,7 @@ func NewWithConfigPath(ctx context.Context, cfg config.Config, configPath string Users: userRepo, Tasks: taskRepo, Deployment: deploymentService, - SecretStore: secretStore, SSHKeys: sshKeys, DNSVerification: dnsVerification, HTTP: server, + SecretStore: secretStore, DNSVerification: dnsVerification, HTTP: server, Certificates: certificates, Workflow: workflow, Renewal: renewal, diff --git a/backend/internal/app/app_test.go b/backend/internal/app/app_test.go index d6964d2..9fcc4d2 100644 --- a/backend/internal/app/app_test.go +++ b/backend/internal/app/app_test.go @@ -27,8 +27,8 @@ func TestNewWiresApplicationServices(t *testing.T) { if application.Deployment == nil || application.Certificates == nil || application.Workflow == nil || application.Renewal == nil { t.Fatal("证书申请、部署或续期服务未完整注入应用实例") } - if application.SecretStore == nil || application.SSHKeys == nil || application.DNSVerification == nil || application.HTTP == nil { - t.Fatal("敏感信息、SSH、DNS 或 HTTP 服务未完整注入应用实例") + if application.SecretStore == nil || application.DNSVerification == nil || application.HTTP == nil { + t.Fatal("敏感信息、DNS 或 HTTP 服务未完整注入应用实例") } } func TestNewWithConfigPathRecoversInterruptedTasks(t *testing.T) { diff --git a/backend/internal/cli/server.go b/backend/internal/cli/server.go index 78f4897..fc085cd 100644 --- a/backend/internal/cli/server.go +++ b/backend/internal/cli/server.go @@ -65,9 +65,6 @@ func newServerAddCommand() *cobra.Command { item.CreatedAt = now item.UpdatedAt = now item.ID = domain.NewID() - if _, err := service.SSHKeys.ValidateReference(cmd.Context(), item.PrivateKeyRef); err != nil { - return fmt.Errorf("SSH 私钥引用无效:%w", err) - } if err := service.Servers.CreateServer(cmd.Context(), item); err != nil { return err } @@ -96,9 +93,6 @@ func newServerEditCommand() *cobra.Command { item.UpdatedBy = "" item.CreatedAt = existing.CreatedAt item.UpdatedAt = time.Now().UTC() - if _, err := service.SSHKeys.ValidateReference(cmd.Context(), item.PrivateKeyRef); err != nil { - return fmt.Errorf("SSH 私钥引用无效:%w", err) - } if err := service.Servers.UpdateServer(cmd.Context(), item); err != nil { return err } @@ -185,7 +179,7 @@ func newDomainServersSetCommand() *cobra.Command { } type serverFlags struct { - name, address, username, privateKeyRef, fingerprint string + name, address, username, fingerprint string certificatePath, privateKeyPath, chainPath, webServer string validationCommand, reloadCommand []string port int @@ -196,7 +190,6 @@ func (f *serverFlags) bind(command *cobra.Command) { command.Flags().StringVar(&f.address, "address", "", "服务器地址") command.Flags().IntVar(&f.port, "port", 22, "SSH 端口") command.Flags().StringVar(&f.username, "username", "", "SSH 用户") - command.Flags().StringVar(&f.privateKeyRef, "private-key-ref", "", "SSH 私钥的敏感信息引用") command.Flags().StringVar(&f.fingerprint, "host-key-fingerprint", "", "SSH 主机 SHA256 指纹") command.Flags().StringVar(&f.certificatePath, "certificate-path", deployment.DefaultCertificatePath, "远程证书文件路径模板") command.Flags().StringVar(&f.privateKeyPath, "private-key-path", deployment.DefaultPrivateKeyPath, "远程私钥文件路径模板") @@ -204,7 +197,7 @@ func (f *serverFlags) bind(command *cobra.Command) { command.Flags().StringVar(&f.webServer, "web-server", string(deployment.WebServerNginx), "Web 服务类型:nginx、caddy、apache、custom") command.Flags().StringSliceVar(&f.validationCommand, "validation-command", nil, "自定义配置校验命令及参数") command.Flags().StringSliceVar(&f.reloadCommand, "reload-command", nil, "自定义 reload 命令及参数") - for _, name := range []string{"name", "address", "username", "private-key-ref", "host-key-fingerprint"} { + for _, name := range []string{"name", "address", "username", "host-key-fingerprint"} { _ = command.MarkFlagRequired(name) } } @@ -212,7 +205,7 @@ func (f *serverFlags) bind(command *cobra.Command) { func (f serverFlags) server() deployment.Server { return deployment.Server{ Name: f.name, Address: f.address, Port: f.port, Username: f.username, - PrivateKeyRef: f.privateKeyRef, HostKeyFingerprint: f.fingerprint, + AuthMethod: deployment.SSHAuthPassword, HostKeyFingerprint: f.fingerprint, CertificatePath: f.certificatePath, PrivateKeyPath: f.privateKeyPath, CertificateChainPath: f.chainPath, WebServer: deployment.WebServer(f.webServer), ValidationCommand: normalizeCommand(f.validationCommand), ReloadCommand: normalizeCommand(f.reloadCommand), diff --git a/backend/internal/cli/ssh_key.go b/backend/internal/cli/ssh_key.go index e6d4b82..b3bedb5 100644 --- a/backend/internal/cli/ssh_key.go +++ b/backend/internal/cli/ssh_key.go @@ -2,117 +2,28 @@ package cli import ( "errors" - "fmt" - "io" - "os" - "strings" "github.com/spf13/cobra" - - "git.pcux.cn/xuans/Scert/backend/internal/sshkey" ) func newSSHKeyCommand() *cobra.Command { - command := &cobra.Command{Use: "ssh-key", Short: "管理 SSH 部署私钥"} - command.AddCommand(newSSHKeyListCommand(), newSSHKeyAddCommand(), newSSHKeyDeleteCommand()) - return command + return &cobra.Command{ + Use: "ssh-key", + Short: "SSH 密钥登录功能已停用", + RunE: func(cmd *cobra.Command, args []string) error { + return errors.New("SSH 密钥登录功能已永久停用,仅支持 SSH 密码认证") + }, + } } func newSSHKeyListCommand() *cobra.Command { - return &cobra.Command{Use: "list", Short: "查看 SSH 私钥元数据", RunE: func(cmd *cobra.Command, args []string) error { - service, err := openApplication(cmd) - if err != nil { - return err - } - defer service.Close() - items, err := service.SSHKeys.List(cmd.Context()) - if err != nil { - return err - } - return printJSON(cmd, items) - }} + return newSSHKeyCommand() } func newSSHKeyAddCommand() *cobra.Command { - var name, filePath, passphraseFile string - command := &cobra.Command{Use: "add", Short: "从文件安全录入 SSH 私钥", RunE: func(cmd *cobra.Command, args []string) error { - privateKey, err := readLimitedFile(filePath, sshkey.MaxPrivateKeyBytes) - if err != nil { - return fmt.Errorf("读取 SSH 私钥文件失败:%w", err) - } - passphrase := "" - if strings.TrimSpace(passphraseFile) != "" { - content, err := readLimitedFile(passphraseFile, 4096) - if err != nil { - return fmt.Errorf("读取 SSH 私钥口令文件失败:%w", err) - } - passphrase = strings.TrimRight(content, "\r\n") - } - service, err := openApplication(cmd) - if err != nil { - return err - } - defer service.Close() - item, err := service.SSHKeys.Create(cmd.Context(), name, privateKey, passphrase) - if err != nil { - return err - } - return printJSON(cmd, item) - }} - command.Flags().StringVar(&name, "name", "", "SSH 私钥名称") - command.Flags().StringVar(&filePath, "file", "", "SSH 私钥文件路径") - command.Flags().StringVar(&passphraseFile, "passphrase-file", "", "可选的私钥口令文件路径") - _ = command.MarkFlagRequired("name") - _ = command.MarkFlagRequired("file") - return command + return newSSHKeyCommand() } func newSSHKeyDeleteCommand() *cobra.Command { - var id string - command := &cobra.Command{Use: "delete", Short: "删除未被服务器使用的 SSH 私钥", RunE: func(cmd *cobra.Command, args []string) error { - service, err := openApplication(cmd) - if err != nil { - return err - } - defer service.Close() - reference, err := service.SSHKeys.Reference(id) - if err != nil { - return err - } - used, err := service.Servers.IsPrivateKeyRefUsed(cmd.Context(), reference) - if err != nil { - return err - } - if used { - return errors.New("SSH 私钥仍被部署服务器使用,不能删除") - } - if err := service.SSHKeys.Delete(cmd.Context(), id); err != nil { - return err - } - fmt.Fprintf(cmd.OutOrStdout(), "SSH 私钥已删除:%s\n", id) - return nil - }} - command.Flags().StringVar(&id, "id", "", "SSH 私钥 ID") - _ = command.MarkFlagRequired("id") - return command -} - -func readLimitedFile(path string, limit int) (string, error) { - path = strings.TrimSpace(path) - if path == "" { - return "", errors.New("文件路径不能为空") - } - file, err := os.Open(path) - if err != nil { - return "", err - } - defer file.Close() - content, err := io.ReadAll(io.LimitReader(file, int64(limit)+1)) - if err != nil { - return "", err - } - if len(content) > limit { - return "", fmt.Errorf("文件不能超过 %d 字节", limit) - } - return string(content), nil + return newSSHKeyCommand() } diff --git a/backend/internal/httpapi/server.go b/backend/internal/httpapi/server.go index 879acb0..5acea07 100644 --- a/backend/internal/httpapi/server.go +++ b/backend/internal/httpapi/server.go @@ -27,7 +27,6 @@ import ( "git.pcux.cn/xuans/Scert/backend/internal/firewall" "git.pcux.cn/xuans/Scert/backend/internal/logging" "git.pcux.cn/xuans/Scert/backend/internal/repository" - "git.pcux.cn/xuans/Scert/backend/internal/sshkey" ) type setupSecretWriter interface { @@ -53,7 +52,6 @@ type Dependencies struct { ProjectResources repository.ProjectResourceReader ProjectBindings repository.ProjectBindingRepository ProjectDomains repository.ProjectDomainRepository - SSHKeys *sshkey.Service DeploymentFactory deployment.ClientFactory SetupDNS setupDNSVerifier Secrets setupSecretWriter @@ -93,7 +91,6 @@ type Server struct { projectResources repository.ProjectResourceReader projectBindings repository.ProjectBindingRepository projectDomains repository.ProjectDomainRepository - sshKeys *sshkey.Service deploymentFactory deployment.ClientFactory setupDNS setupDNSVerifier secrets setupSecretWriter @@ -161,7 +158,6 @@ func New(deps Dependencies) *Server { projectResources: deps.ProjectResources, projectBindings: deps.ProjectBindings, projectDomains: deps.ProjectDomains, - sshKeys: deps.SSHKeys, deploymentFactory: deps.DeploymentFactory, issueCertificate: deps.IssueCertificate, issueSetupCertificate: deps.IssueSetupCertificate, diff --git a/backend/internal/httpapi/ssh_key.go b/backend/internal/httpapi/ssh_key.go index 4c51272..2fda3d3 100644 --- a/backend/internal/httpapi/ssh_key.go +++ b/backend/internal/httpapi/ssh_key.go @@ -1,104 +1,15 @@ 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 - -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"` -} +import "github.com/gin-gonic/gin" func (s *Server) listSSHKeys(c *gin.Context) { - if s.sshKeys == nil { - s.writeError(c, http.StatusNotImplemented, "SSH 私钥管理接口尚未配置", errors.New("SSH 私钥管理服务未配置")) - return - } - 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 - } - c.JSON(http.StatusOK, gin.H{"items": items}) + c.Status(404) } func (s *Server) createSSHKey(c *gin.Context) { - if s.sshKeys == nil { - s.writeError(c, http.StatusNotImplemented, "SSH 私钥管理接口尚未配置", errors.New("SSH 私钥管理服务未配置")) - return - } - var request sshKeyRequest - if err := bindJSON(c, &request, maxSSHKeyRequestBytes); err != nil { - var tooLarge *http.MaxBytesError - if errors.As(err, &tooLarge) { - s.writeError(c, http.StatusRequestEntityTooLarge, "SSH 私钥请求内容过大", err) - return - } - s.writeError(c, http.StatusBadRequest, "请求参数无效", err) - return - } - 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 - } - c.JSON(http.StatusCreated, item) + c.Status(404) } func (s *Server) deleteSSHKey(c *gin.Context) { - if s.sshKeys == nil || s.servers == nil { - s.writeError(c, http.StatusNotImplemented, "SSH 私钥管理接口尚未配置", errors.New("SSH 私钥或服务器管理服务未配置")) - return - } - reference, err := s.sshKeys.Reference(c.Param("id")) - if err != nil { - 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) - return - } - if used { - s.writeError(c, http.StatusConflict, "SSH 私钥仍被部署服务器使用", errors.New("SSH 私钥引用正在使用")) - return - } - if err := s.sshKeys.Delete(c.Request.Context(), c.Param("id")); err != nil { - s.writeError(c, http.StatusNotFound, "SSH 私钥不存在", err) - return - } - c.Status(http.StatusNoContent) + c.Status(404) } diff --git a/backend/internal/sshkey/service.go b/backend/internal/sshkey/service.go index 18e5783..f7e8344 100644 --- a/backend/internal/sshkey/service.go +++ b/backend/internal/sshkey/service.go @@ -2,27 +2,13 @@ package sshkey import ( "context" - "crypto/ed25519" - "crypto/rand" - "encoding/pem" "errors" - "fmt" - "strings" "time" - "golang.org/x/crypto/ssh" - - "git.pcux.cn/xuans/Scert/backend/internal/domain" "git.pcux.cn/xuans/Scert/backend/internal/secretstore" ) -const ( - ReferencePrefix = "secret://ssh/" - secretKind = "ssh_private_key" - MaxPrivateKeyBytes = 1024 * 1024 - maxNameLength = 128 - maxPassphraseBytes = 4096 -) +var ErrDisabled = errors.New("SSH 密钥登录功能已永久停用,仅支持 SSH 密码认证") type Store interface { Put(ctx context.Context, reference string, value map[string]string) error @@ -43,251 +29,49 @@ type Metadata struct { } type Service struct { - store Store } -func NewService(store Store) *Service { - return &Service{store: store} +func NewService(_ ...Store) *Service { + return &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) Create(context.Context, string, string, string) (Metadata, error) { + return Metadata{}, ErrDisabled } -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 私钥名称不能为空") - } - if len([]rune(name)) > maxNameLength { - return Metadata{}, fmt.Errorf("SSH 私钥名称不能超过 %d 个字符", maxNameLength) - } - privateKey = strings.TrimSpace(privateKey) - if privateKey == "" { - return Metadata{}, errors.New("SSH 私钥内容不能为空") - } - if len(privateKey) > MaxPrivateKeyBytes { - return Metadata{}, fmt.Errorf("SSH 私钥内容不能超过 %d 字节", MaxPrivateKeyBytes) - } - if len(passphrase) > maxPassphraseBytes { - return Metadata{}, fmt.Errorf("SSH 私钥口令不能超过 %d 字节", maxPassphraseBytes) - } - signer, err := parsePrivateKey(privateKey, passphrase) - if err != nil { - return Metadata{}, err - } - id := domain.NewID() - reference := ReferencePrefix + id - payload := map[string]string{ - "kind": secretKind, - "name": name, - "private_key": privateKey + "\n", - "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(), ProjectID: projectID, - }, nil +func (s *Service) CreateForProject(context.Context, string, string, string, string) (Metadata, error) { + return Metadata{}, ErrDisabled } -func (s *Service) CreateGenerated(ctx context.Context, name string) (Metadata, string, error) { - return s.CreateGeneratedForProject(ctx, name, domain.DefaultProjectID) +func (s *Service) CreateGenerated(context.Context, string) (Metadata, string, error) { + return Metadata{}, "", ErrDisabled } -func (s *Service) CreateGeneratedForProject(ctx context.Context, name, projectID string) (Metadata, string, error) { - if s == nil || s.store == nil { - return Metadata{}, "", errors.New("SSH 私钥存储未初始化") - } - name = strings.TrimSpace(name) - if name == "" { - return Metadata{}, "", errors.New("SSH 私钥名称不能为空") - } - publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader) - if err != nil { - return Metadata{}, "", fmt.Errorf("生成 SSH 密钥失败:%w", err) - } - privatePEM, err := ssh.MarshalPrivateKey(privateKey, "scert-deploy") - if err != nil { - return Metadata{}, "", fmt.Errorf("编码 SSH 私钥失败:%w", err) - } - metadata, err := s.CreateForProject(ctx, name, string(pem.EncodeToMemory(privatePEM)), "", projectID) - if err != nil { - return Metadata{}, "", err - } - sshPublicKey, err := ssh.NewPublicKey(publicKey) - if err != nil { - return Metadata{}, "", fmt.Errorf("编码 SSH 公钥失败:%w", err) - } - return metadata, strings.TrimSpace(string(ssh.MarshalAuthorizedKey(sshPublicKey))), nil +func (s *Service) CreateGeneratedForProject(context.Context, string, string) (Metadata, string, error) { + return Metadata{}, "", ErrDisabled } -func (s *Service) AuthorizedPublicKey(ctx context.Context, reference string) (string, error) { - if s == nil || s.store == nil { - return "", errors.New("SSH 私钥存储未初始化") - } - metadata, err := s.metadata(ctx, strings.TrimSpace(reference), time.Time{}, true) - if err != nil { - return "", err - } - payload, err := s.store.Get(ctx, metadata.Reference) - if err != nil { - return "", fmt.Errorf("读取 SSH 私钥失败:%w", err) - } - signer, err := parsePrivateKey(payload["private_key"], payload["passphrase"]) - if err != nil { - return "", err - } - return strings.TrimSpace(string(ssh.MarshalAuthorizedKey(signer.PublicKey()))), nil +func (s *Service) AuthorizedPublicKey(context.Context, string) (string, error) { + return "", ErrDisabled } -func (s *Service) List(ctx context.Context) ([]Metadata, error) { - if s == nil || s.store == nil { - return nil, errors.New("SSH 私钥存储未初始化") - } - items, err := s.store.List(ctx, ReferencePrefix) - if err != nil { - return nil, fmt.Errorf("列出 SSH 私钥失败:%w", err) - } - result := make([]Metadata, 0, len(items)) - for _, item := range items { - referenceID := strings.TrimPrefix(item.Reference, ReferencePrefix) - if strings.Contains(referenceID, "/") { - continue - } - metadata, err := s.metadata(ctx, item.Reference, item.UpdatedAt, false) - if err != nil { - return nil, err - } - result = append(result, metadata) - } - return result, nil +func (s *Service) List(context.Context) ([]Metadata, error) { + return nil, ErrDisabled } -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) ListForProject(context.Context, string) ([]Metadata, error) { + return nil, ErrDisabled } -func (s *Service) ValidateReference(ctx context.Context, reference string) (Metadata, error) { - if s == nil || s.store == nil { - return Metadata{}, errors.New("SSH 私钥存储未初始化") - } - return s.metadata(ctx, strings.TrimSpace(reference), time.Time{}, true) +func (s *Service) ValidateReference(context.Context, string) (Metadata, error) { + return Metadata{}, ErrDisabled } -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) ValidateReferenceForProject(context.Context, string, string) (Metadata, error) { + return Metadata{}, ErrDisabled } -func (s *Service) Delete(ctx context.Context, id string) error { - if s == nil || s.store == nil { - return errors.New("SSH 私钥存储未初始化") - } - reference, err := referenceFromID(id) - if err != nil { - return err - } - if _, err := s.metadata(ctx, reference, time.Time{}, false); err != nil { - return err - } - if err := s.store.Delete(ctx, reference); err != nil { - return fmt.Errorf("删除 SSH 私钥失败:%w", err) - } - return nil +func (s *Service) Delete(context.Context, string) error { + return ErrDisabled } -func (s *Service) Reference(id string) (string, error) { - return referenceFromID(id) -} - -func (s *Service) metadata(ctx context.Context, reference string, updatedAt time.Time, validateKey bool) (Metadata, error) { - if !strings.HasPrefix(reference, ReferencePrefix) { - return Metadata{}, errors.New("SSH 私钥引用格式无效") - } - id := strings.TrimPrefix(reference, ReferencePrefix) - if _, err := referenceFromID(id); err != nil { - return Metadata{}, err - } - payload, err := s.store.Get(ctx, reference) - if err != nil { - return Metadata{}, fmt.Errorf("读取 SSH 私钥失败:%w", err) - } - if payload["kind"] != secretKind || strings.TrimSpace(payload["private_key"]) == "" { - return Metadata{}, errors.New("敏感信息引用不是有效的 SSH 私钥") - } - algorithm := payload["algorithm"] - fingerprint := payload["fingerprint"] - if validateKey || algorithm == "" || fingerprint == "" { - signer, parseErr := parsePrivateKey(payload["private_key"], payload["passphrase"]) - if parseErr != nil { - return Metadata{}, parseErr - } - algorithm = signer.PublicKey().Type() - fingerprint = ssh.FingerprintSHA256(signer.PublicKey()) - } - 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 - err error - ) - if passphrase != "" { - signer, err = ssh.ParsePrivateKeyWithPassphrase([]byte(privateKey), []byte(passphrase)) - } else { - signer, err = ssh.ParsePrivateKey([]byte(privateKey)) - } - if err != nil { - var missing *ssh.PassphraseMissingError - if errors.As(err, &missing) { - return nil, errors.New("SSH 私钥已加密,请提供私钥口令") - } - return nil, errors.New("SSH 私钥格式或口令无效") - } - return signer, nil -} - -func referenceFromID(id string) (string, error) { - id = strings.TrimSpace(id) - if id == "" || strings.ContainsAny(id, "/\\\x00\r\n") { - return "", errors.New("SSH 私钥 ID 无效") - } - return ReferencePrefix + id, nil +func (s *Service) Reference(string) (string, error) { + return "", ErrDisabled } diff --git a/backend/internal/sshkey/service_disabled_test.go b/backend/internal/sshkey/service_disabled_test.go new file mode 100644 index 0000000..b073afd --- /dev/null +++ b/backend/internal/sshkey/service_disabled_test.go @@ -0,0 +1,32 @@ +package sshkey + +import ( + "errors" + "testing" +) + +func TestServiceIsPermanentlyDisabled(t *testing.T) { + service := NewService() + if service == nil { + t.Fatal("停用兼容服务不能为空") + } + + if _, err := service.Create(t.Context(), "test", "ignored", "ignored"); !errors.Is(err, ErrDisabled) { + t.Fatalf("创建操作未被永久停用:%v", err) + } + if _, _, err := service.CreateGenerated(t.Context(), "test"); !errors.Is(err, ErrDisabled) { + t.Fatalf("生成操作未被永久停用:%v", err) + } + if _, err := service.List(t.Context()); !errors.Is(err, ErrDisabled) { + t.Fatalf("列表操作未被永久停用:%v", err) + } + if _, err := service.ValidateReference(t.Context(), "ignored"); !errors.Is(err, ErrDisabled) { + t.Fatalf("引用校验操作未被永久停用:%v", err) + } + if err := service.Delete(t.Context(), "ignored"); !errors.Is(err, ErrDisabled) { + t.Fatalf("删除操作未被永久停用:%v", err) + } + if _, err := service.Reference("ignored"); !errors.Is(err, ErrDisabled) { + t.Fatalf("引用生成操作未被永久停用:%v", err) + } +} diff --git a/backend/internal/sshkey/service_test.go b/backend/internal/sshkey/service_test.go index 8e6a091..33006a6 100644 --- a/backend/internal/sshkey/service_test.go +++ b/backend/internal/sshkey/service_test.go @@ -1,3 +1,5 @@ +//go:build legacy_ssh_key_tests + package sshkey import ( diff --git a/frontend/src/views/Dashboard/ServerModal.css b/frontend/src/views/Dashboard/ServerModal.css index f0661b6..c921bc4 100644 --- a/frontend/src/views/Dashboard/ServerModal.css +++ b/frontend/src/views/Dashboard/ServerModal.css @@ -112,145 +112,6 @@ border-top: 1px solid #dce6f1; } -.server-modal .server-ssh-key-guide { - margin: 0; - padding: 14px; - border: 1px solid #c7daf1; - border-radius: 14px; - background: #f5f9ff; -} - -.server-modal .server-ssh-key-guide .ssh-key-steps { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 8px; - margin-bottom: 10px; -} - -.server-modal .server-ssh-key-guide .ssh-key-steps article { - display: flex; - align-items: center; - gap: 8px; - min-width: 0; - padding: 9px 10px; - border: 1px solid #d1e0f1; - border-radius: 10px; - background: #fff; - color: #294a73; -} - -.server-modal .server-ssh-key-guide .ssh-key-steps b { - display: inline-grid; - place-items: center; - flex: 0 0 24px; - width: 24px; - height: 24px; - border-radius: 50%; - background: #5b91dc; - color: #fff; -} - -.server-modal .server-ssh-key-guide .ssh-key-platforms { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 4px; - margin-bottom: 8px; - padding: 4px; - border-radius: 10px; - background: #e7effa; -} - -.server-modal .server-ssh-key-guide .platform-tab { - min-height: 34px; - border: 0; - border-radius: 8px; - background: transparent; - color: #5d7390; - cursor: pointer; - font: inherit; -} - -.server-modal .server-ssh-key-guide .platform-tab.active { - background: #fff; - color: #24579a; - box-shadow: 0 2px 8px rgb(42 80 125 / 12%); -} - -.server-modal .server-ssh-key-guide .ssh-key-path-line, -.server-modal .server-ssh-key-guide .ssh-key-path-summary { - display: grid; - gap: 8px; - margin-bottom: 8px; - padding: 8px 10px; - border: 1px solid #d1e0f1; - border-radius: 10px; - background: #fff; -} - -.server-modal .server-ssh-key-guide .ssh-key-path-line { - grid-template-columns: auto minmax(0, 1fr) auto; - align-items: center; -} - -.server-modal .server-ssh-key-guide .ssh-key-path-summary { - grid-template-columns: repeat(3, minmax(0, 1fr)); -} - -.server-modal .server-ssh-key-guide .ssh-key-path-summary > div { - display: grid; - gap: 4px; - min-width: 0; -} - -.server-modal .server-ssh-key-guide code { - overflow-wrap: anywhere; - color: #294a73; - font-family: ui-monospace, SFMono-Regular, Consolas, monospace; - font-size: 12px; -} - -.server-modal .server-ssh-key-guide .ssh-command-card { - margin-bottom: 8px; - padding: 9px 10px; - border: 1px solid #d1e0f1; - border-radius: 10px; - background: #fff; -} - -.server-modal .server-ssh-key-guide .ssh-command-heading, -.server-modal .server-ssh-key-actions { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; -} - -.server-modal .server-ssh-key-guide .ssh-command-heading { - margin-bottom: 5px; - color: #294a73; - font-weight: 700; -} - -.server-modal .server-ssh-key-fields { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 0 14px; - padding-top: 2px; -} - -.server-modal .server-ssh-key-fields .field:nth-child(3) { - grid-column: 1 / -1; -} - -.server-modal .server-ssh-key-fields textarea { - min-height: 96px; -} - -.server-modal .server-ssh-key-actions { - padding-top: 8px; - border-top: 1px solid #d1e0f1; -} - .server-modal .server-command-section textarea { min-height: 82px; } @@ -268,9 +129,6 @@ grid-template-columns: minmax(0, 1fr); } - .server-modal .server-ssh-key-guide .ssh-key-path-summary { - grid-template-columns: minmax(0, 1fr); - } } @media (max-width: 640px) { @@ -291,22 +149,4 @@ grid-column: 1 / -1; } - .server-modal .server-ssh-key-guide .ssh-key-steps, - .server-modal .server-ssh-key-fields { - grid-template-columns: minmax(0, 1fr); - } - - .server-modal .server-ssh-key-fields .field:nth-child(3) { - grid-column: auto; - } - - .server-modal .server-ssh-key-guide .ssh-key-path-line { - grid-template-columns: minmax(0, 1fr); - } - - .server-modal .server-ssh-key-heading, - .server-modal .server-ssh-key-actions { - align-items: stretch; - flex-direction: column; - } }