package service import ( "context" "crypto/rand" "crypto/rsa" "crypto/x509" "encoding/pem" "errors" "fmt" "time" "oci-portal/internal/model" "oci-portal/internal/oci" ) // ErrCurrentApiKey 拒绝删除当前配置正在使用的签名 key。 var ErrCurrentApiKey = errors.New("当前配置正在使用的 API Key 不可删除,请先替换") // apiKeyVerifyAttempts / apiKeyVerifyDelay 控制新 key 生效验证的重试节奏; // OCI 上传公钥后生效有秒级延迟。测试将 delay 调小。 var ( apiKeyVerifyAttempts = 6 apiKeyVerifyDelay = 2 * time.Second ) // CreatedApiKey 是新建/轮换 API key 的一次性返回;私钥仅此一次,不落库。 type CreatedApiKey struct { Fingerprint string `json:"fingerprint"` PrivateKey string `json:"privateKey"` ConfigIni string `json:"configIni"` } // UserApiKeyInfo 是列表项:key 元数据加该 key 的 CLI config 模板(预览用)。 type UserApiKeyInfo struct { oci.TenantUserApiKey ConfigIni string `json:"configIni"` } // TenantUserApiKeys 列出用户 API 签名 key(oci 层已标注当前使用)。 func (s *OciConfigService) TenantUserApiKeys(ctx context.Context, id uint, userID string) ([]UserApiKeyInfo, error) { cred, homeRegion, err := s.credentialsAndHomeRegion(ctx, id) if err != nil { return nil, err } keys, err := s.client.ListTenantUserApiKeys(ctx, cred, homeRegion, userID) if err != nil { return nil, err } out := make([]UserApiKeyInfo, 0, len(keys)) for _, k := range keys { out = append(out, UserApiKeyInfo{TenantUserApiKey: k, ConfigIni: ociConfigIni(cred, userID, k.Fingerprint)}) } return out, nil } // AddTenantUserApiKey 生成 RSA-2048 密钥对并上传公钥;私钥仅本次返回。 func (s *OciConfigService) AddTenantUserApiKey(ctx context.Context, id uint, userID string) (CreatedApiKey, error) { cred, homeRegion, err := s.credentialsAndHomeRegion(ctx, id) if err != nil { return CreatedApiKey{}, err } privPEM, pubPEM, err := generateRsaKeyPair() if err != nil { return CreatedApiKey{}, err } fp, err := s.client.UploadTenantUserApiKey(ctx, cred, homeRegion, userID, pubPEM) if err != nil { return CreatedApiKey{}, err } return CreatedApiKey{Fingerprint: fp, PrivateKey: privPEM, ConfigIni: ociConfigIni(cred, userID, fp)}, nil } // DeleteTenantUserApiKey 删除用户单把 API key;当前签名 key 拒删以免面板失联。 func (s *OciConfigService) DeleteTenantUserApiKey(ctx context.Context, id uint, userID, fingerprint string) error { cred, homeRegion, err := s.credentialsAndHomeRegion(ctx, id) if err != nil { return err } if userID == cred.UserOCID && fingerprint == cred.Fingerprint { return ErrCurrentApiKey } return s.client.DeleteTenantUserApiKey(ctx, cred, homeRegion, userID, fingerprint) } // ActivateApiKey 把刚创建的 key 设为本配置的签名凭据:验证可用后落库,不删除旧 key。 // 私钥由前端回传(仅创建时下发过);归属无需显式校验——非本签名用户的 key 验证必失败。 func (s *OciConfigService) ActivateApiKey(ctx context.Context, id uint, fingerprint, privateKey string) error { cfg, err := s.Get(ctx, id) if err != nil { return err } cred, err := s.credentialsOf(cfg) if err != nil { return err } newCred := cred newCred.Fingerprint, newCred.PrivateKey, newCred.Passphrase = fingerprint, privateKey, "" if err := newCred.Validate(); err != nil { return err } if err := s.waitApiKeyUsable(ctx, newCred); err != nil { return err } return s.persistSigningKey(cfg, newCred) } // waitApiKeyUsable 用新凭据测活,等待上传的公钥在 OCI 侧生效。 func (s *OciConfigService) waitApiKeyUsable(ctx context.Context, cred oci.Credentials) error { var err error for i := 0; i < apiKeyVerifyAttempts; i++ { if _, err = s.client.ValidateKey(ctx, cred); err == nil { return nil } select { case <-ctx.Done(): return ctx.Err() case <-time.After(apiKeyVerifyDelay): } } return fmt.Errorf("new api key not usable: %w", err) } // persistSigningKey 加密新私钥,更新配置指纹并清空口令密文(面板生成的 key 无口令)。 func (s *OciConfigService) persistSigningKey(cfg *model.OciConfig, newCred oci.Credentials) error { enc, err := s.cipher.EncryptString(newCred.PrivateKey) if err != nil { return fmt.Errorf("encrypt private key: %w", err) } updates := map[string]any{"fingerprint": newCred.Fingerprint, "private_key_enc": enc, "passphrase_enc": ""} if err := s.db.Model(cfg).Updates(updates).Error; err != nil { return fmt.Errorf("persist rotated key: %w", err) } return nil } // generateRsaKeyPair 生成 RSA-2048 密钥对,返回 PKCS#1 私钥 PEM 与 PKIX 公钥 PEM。 func generateRsaKeyPair() (privatePEM, publicPEM string, err error) { key, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { return "", "", fmt.Errorf("generate rsa key: %w", err) } privBlock := &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)} pubDER, err := x509.MarshalPKIXPublicKey(&key.PublicKey) if err != nil { return "", "", fmt.Errorf("marshal public key: %w", err) } pubBlock := &pem.Block{Type: "PUBLIC KEY", Bytes: pubDER} return string(pem.EncodeToMemory(privBlock)), string(pem.EncodeToMemory(pubBlock)), nil } // ociConfigIni 拼装 OCI CLI config 文本;key_file 需用户保存私钥后自行填写。 func ociConfigIni(cred oci.Credentials, userID, fingerprint string) string { return fmt.Sprintf( "[DEFAULT]\nuser=%s\nfingerprint=%s\ntenancy=%s\nregion=%s\n# 保存私钥到本机后替换为实际路径\nkey_file=~/.oci/oci_api_key.pem\n", userID, fingerprint, cred.TenancyOCID, cred.Region) }