Files
LokiStack/backend/models/storage.go
T
opsx cfe1ccd154 fix(storage): 按 endpoint 形态解析响应 + 凭据解密返回 + 无建桶权限友好提示
【真实根因】COS 桶明明存在却报"桶不存在/创建失败"

用户配置:endpoint=loki-log-1394668510.cos.ap-hongkong.myqcloud.com(虚拟主机式,已含桶名)
旧代码:始终 GET / 并按 ListBuckets 的 <Buckets><Bucket><Name> 解析。
但在虚拟主机式 endpoint 下,GET / 返回的是 ListObjects(<ListBucketResult>)——
桶内对象列表,不是桶列表。解析必然找不到桶名 → 误判"桶不存在"
→ 触发建桶 PUT → 用户的 AK 无 cos:PutBucket 权限 → 403
→ 最终报"创建后校验失败"。

【实测确认(用该 AK 直连 COS)】
  GET / (vhost)         → 200  <ListBucketResult>         桶存在,能列对象
  GET / (区域端点)       → 200  <ListAllMyBucketsResult>   AK 有 cos:GetService
  HEAD /                → 403  AK 无 cos:HeadBucket
  PUT /  (CreateBucket) → 403  AK 无 cos:PutBucket
即 QcloudCOSDataFullControl = 数据面全控(读写/列桶),不含管控面(建桶/HeadBucket)。

【修复】
1. 探测统一用 GET /,再按 endpoint 形态解释响应:
   - 虚拟主机式 → 按 ListObjects(<ListBucketResult>)判断,200 即桶存在
   - 区域端点   → 按 ListBuckets(<Buckets><Bucket><Name>)匹配桶名
   桶存在就直接返回"桶已存在",不再走建桶流程。
2. 建桶二次校验同样改用 GET /(不再用 HEAD,避免 HeadBucket 403 误判)。
3. 建桶返回 403 时给出明确友好提示:
   "当前 Access Key 无建桶权限。腾讯云 QcloudCOSDataFullControl 只授予数据读写/列出,
    不含 CreateBucket。请到 COS 控制台手动创建桶,或在 CAM 追加 cos:PutBucket"
4. models/storage.go Sanitized() 用 crypto.DecryptField 解密后再返回 AK/SK
   (原来直接返回 ENC:xxx 密文,前端编辑框显示密文)。
5. 前端访问地址输入框补充说明:推荐填区域端点(不带桶名)。

镜像:lghost/lokistack:latest + v0.1 = sha256:350478003870
2026-10-26 18:38:00 +00:00

50 lines
1.5 KiB
Go

package models
import (
"time"
"opsxloki/pkg/crypto"
)
type StorageConfig struct {
ID uint `json:"id"`
Name string `json:"name"`
StorageType string `json:"storageType"`
Endpoint string `json:"endpoint"`
Bucket string `json:"bucket"`
Region string `json:"region"`
AccessKey string `json:"accessKey"`
SecretKey string `json:"secretKey"`
PathPrefix string `json:"pathPrefix"`
Insecure bool `json:"insecure"`
IsDefault bool `json:"isDefault"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
func (s *StorageConfig) Sanitized() map[string]interface{} {
// AccessKey / SecretKey 在库里是 ENC:<ciphertext> 加密存储,
// 直接返回会让前端编辑框显示 ENC:xxx 密文。这里统一解密后返回明文,
// 前端 Secret Key 仍用密码框(默认掩码,点「显示」才展开),
// 便于管理员核对凭据并「测试连接」。
plainAK := crypto.DecryptField(s.AccessKey)
plainSK := crypto.DecryptField(s.SecretKey)
return map[string]interface{}{
"id": s.ID,
"name": s.Name,
"storageType": s.StorageType,
"endpoint": s.Endpoint,
"bucket": s.Bucket,
"region": s.Region,
"accessKey": plainAK,
"secretKey": plainSK,
"pathPrefix": s.PathPrefix,
"insecure": s.Insecure,
"isDefault": s.IsDefault,
"hasSecretKey": plainSK != "",
"createdAt": s.CreatedAt,
"updatedAt": s.UpdatedAt,
}
}