easyai-ai-gateway/apps/api/internal/securityevents/secret_store.go
chengcheng 9efeb16fd1 feat(ssf): 实现安全事件一键连接与凭据托管
新增数据库驱动的 SecurityEventConnectionManager,复用 RFC 7662 机器 Client,自动完成 Discovery、Push Bearer 生成、Stream 创建、Verification、首次启用、零停机轮换和安全退役。

增加文件与 Kubernetes SecretStore、最小权限 RBAC、动态 Receiver/撤销水位/内省降级,以及系统设置管理页面。已通过全量 Go 测试、go vet、关键包竞态测试、27 个 Web 测试、类型检查、生产构建、Compose 和 Kubernetes dry-run。
2026-07-15 17:25:00 +08:00

113 lines
3.0 KiB
Go

package securityevents
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"regexp"
)
var secretReferencePattern = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{0,127}$`)
var ErrSecretNotFound = errors.New("security event secret not found")
type SecretStore interface {
Put(context.Context, string, []byte) error
Get(context.Context, string) ([]byte, error)
Delete(context.Context, string) error
}
type FileSecretStore struct{ directory string }
func NewFileSecretStore(directory string) (*FileSecretStore, error) {
if directory == "" {
return nil, errors.New("security event secret directory is required")
}
if err := os.MkdirAll(directory, 0o700); err != nil {
return nil, fmt.Errorf("create security event secret directory: %w", err)
}
if err := os.Chmod(directory, 0o700); err != nil {
return nil, fmt.Errorf("protect security event secret directory: %w", err)
}
return &FileSecretStore{directory: directory}, nil
}
func (s *FileSecretStore) Put(_ context.Context, reference string, value []byte) error {
path, err := s.path(reference)
if err != nil {
return err
}
if len(value) < 32 || len(value) > 4096 {
return errors.New("security event secret length is invalid")
}
temporary, err := os.CreateTemp(s.directory, ".ssf-secret-*")
if err != nil {
return fmt.Errorf("create temporary security event secret: %w", err)
}
temporaryName := temporary.Name()
defer func() { _ = os.Remove(temporaryName) }()
if err := temporary.Chmod(0o600); err != nil {
_ = temporary.Close()
return err
}
if _, err := temporary.Write(value); err != nil {
_ = temporary.Close()
return fmt.Errorf("write security event secret: %w", err)
}
if err := temporary.Sync(); err != nil {
_ = temporary.Close()
return err
}
if err := temporary.Close(); err != nil {
return err
}
if err := os.Rename(temporaryName, path); err != nil {
return fmt.Errorf("publish security event secret: %w", err)
}
return os.Chmod(path, 0o600)
}
func (s *FileSecretStore) Get(_ context.Context, reference string) ([]byte, error) {
path, err := s.path(reference)
if err != nil {
return nil, err
}
info, err := os.Stat(path)
if errors.Is(err, os.ErrNotExist) {
return nil, ErrSecretNotFound
}
if err != nil || !info.Mode().IsRegular() || info.Mode().Perm()&0o077 != 0 {
return nil, errors.New("security event secret file is not protected")
}
value, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read security event secret: %w", err)
}
if len(value) < 32 || len(value) > 4096 {
clear(value)
return nil, errors.New("security event secret length is invalid")
}
return value, nil
}
func (s *FileSecretStore) Delete(_ context.Context, reference string) error {
path, err := s.path(reference)
if err != nil {
return err
}
err = os.Remove(path)
if errors.Is(err, os.ErrNotExist) {
return nil
}
return err
}
func (s *FileSecretStore) path(reference string) (string, error) {
if !secretReferencePattern.MatchString(reference) {
return "", errors.New("security event secret reference is invalid")
}
return filepath.Join(s.directory, reference), nil
}