158 lines
5.5 KiB
Go
158 lines
5.5 KiB
Go
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)
|
|
}
|