mirror of
https://github.com/TencentCloud/TencentDB-Agent-Memory
synced 2026-07-10 20:34:30 +00:00
241 lines
8.8 KiB
Python
241 lines
8.8 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
STS 临时密钥权限排查脚本
|
||
从 Shark 获取 COS 凭证,逐项测试各种 COS 操作权限。
|
||
|
||
用法: python3 check_sts_permissions.py
|
||
依赖: pip install cos-python-sdk-v5
|
||
"""
|
||
|
||
import json
|
||
import re
|
||
import sys
|
||
import urllib.request
|
||
import traceback
|
||
|
||
SHARK_URL = "http://tdai.gateway.cd.test.polaris:8000/meta/GetMemoryPlusCosConfig"
|
||
INSTANCE_ID = "mem-rkgqhd5z"
|
||
|
||
|
||
def fetch_cos_config():
|
||
"""从 Shark 获取 COS 配置"""
|
||
req = urllib.request.Request(
|
||
SHARK_URL,
|
||
data=b"{}",
|
||
headers={"Content-Type": "application/json"},
|
||
method="POST",
|
||
)
|
||
resp = urllib.request.urlopen(req, timeout=10)
|
||
payload = json.loads(resp.read())
|
||
print("=" * 60)
|
||
print("Shark 原始返回:")
|
||
print(json.dumps(payload, indent=2, ensure_ascii=False))
|
||
print("=" * 60)
|
||
|
||
if payload.get("code") != 0:
|
||
print(f"Shark 返回错误: code={payload.get('code')}, message={payload.get('message')}")
|
||
sys.exit(1)
|
||
|
||
return payload["data"]
|
||
|
||
|
||
def parse_cos_url(cos_url):
|
||
"""解析 COS URL 提取 bucket 和 region"""
|
||
host = cos_url.replace("https://", "").replace("http://", "").rstrip("/")
|
||
m = re.match(r"^(.+?)\.cos(?:-internal)?\.(.+?)\.(?:myqcloud\.com|tencentcos\.cn)$", host)
|
||
if not m:
|
||
raise ValueError(f"无法解析 COS URL: {cos_url}")
|
||
return m.group(1), m.group(2)
|
||
|
||
|
||
def test_permissions(data):
|
||
"""逐项测试 COS 操作权限"""
|
||
try:
|
||
from qcloud_cos import CosConfig, CosS3Client
|
||
except ImportError:
|
||
print("\n请先安装 COS SDK: pip install cos-python-sdk-v5")
|
||
sys.exit(1)
|
||
|
||
cos_url = data["CosUrl"]
|
||
secret_id = data["TmpSecretId"]
|
||
secret_key = data["TmpSecretKey"]
|
||
token = data["TmpToken"]
|
||
path_prefix = data["PathPrefix"]
|
||
expiration = data.get("ExpirationTime", "未知")
|
||
|
||
bucket, region = parse_cos_url(cos_url)
|
||
|
||
print(f"\n凭证信息:")
|
||
print(f" CosUrl: {cos_url}")
|
||
print(f" Bucket: {bucket}")
|
||
print(f" Region: {region}")
|
||
print(f" TmpSecretId: {secret_id[:10]}...{secret_id[-4:]}")
|
||
print(f" TmpSecretKey: {secret_key[:6]}...{secret_key[-4:]}")
|
||
print(f" TmpToken: {'有' if token else '无'} (长度={len(token) if token else 0})")
|
||
print(f" PathPrefix: {path_prefix}")
|
||
print(f" Expiration: {expiration}")
|
||
|
||
# 实际 prefix(按 memory service 逻辑拼接)
|
||
prefix = f"{path_prefix.rstrip('/')}/{INSTANCE_ID}/"
|
||
print(f" 实际 Key 前缀: {prefix}")
|
||
|
||
# 同时测试外网域名(开发机可能无法访问 cos-internal)
|
||
# 如果 CosUrl 是内网域名,先用外网试
|
||
use_public = "cos-internal" in cos_url or "tencentcos.cn" in cos_url
|
||
if use_public:
|
||
print(f"\n 注意: CosUrl 为内网域名,将同时用外网域名测试")
|
||
|
||
config = CosConfig(
|
||
Region=region,
|
||
SecretId=secret_id,
|
||
SecretKey=secret_key,
|
||
Token=token,
|
||
)
|
||
client = CosS3Client(config)
|
||
|
||
test_key = f"{prefix}__permission_test__.txt"
|
||
results = {}
|
||
|
||
# ── Test 1: GetService (列出所有 Bucket) ──
|
||
print(f"\n{'─' * 60}")
|
||
print("Test 1: GetService (列出所有 Bucket)")
|
||
try:
|
||
resp = client.list_buckets()
|
||
buckets = resp.get("Buckets", {}).get("Bucket", [])
|
||
results["GetService"] = f"OK ({len(buckets)} buckets)"
|
||
print(f" ✅ OK - 可见 {len(buckets)} 个 Bucket")
|
||
for b in buckets[:5]:
|
||
print(f" - {b.get('Name', '?')} ({b.get('Location', '?')})")
|
||
if len(buckets) > 5:
|
||
print(f" ... 及其他 {len(buckets) - 5} 个")
|
||
except Exception as e:
|
||
results["GetService"] = f"DENIED ({e})"
|
||
print(f" ❌ DENIED - {e}")
|
||
|
||
# ── Test 2: HeadBucket (检查 Bucket 是否存在/可访问) ──
|
||
print(f"\nTest 2: HeadBucket (Bucket={bucket})")
|
||
try:
|
||
client.head_bucket(Bucket=bucket)
|
||
results["HeadBucket"] = "OK"
|
||
print(f" ✅ OK - Bucket 存在且可访问")
|
||
except Exception as e:
|
||
results["HeadBucket"] = f"DENIED ({e})"
|
||
print(f" ❌ DENIED - {e}")
|
||
|
||
# ── Test 3: GetBucket / ListObjects (在 prefix 下列出对象) ──
|
||
print(f"\nTest 3: ListObjects (Prefix={prefix})")
|
||
try:
|
||
resp = client.list_objects(Bucket=bucket, Prefix=prefix, MaxKeys=10)
|
||
contents = resp.get("Contents", [])
|
||
results["ListObjects"] = f"OK ({len(contents)} objects)"
|
||
print(f" ✅ OK - 列出 {len(contents)} 个对象")
|
||
for c in contents[:5]:
|
||
print(f" - {c['Key']} ({c['Size']} bytes)")
|
||
except Exception as e:
|
||
results["ListObjects"] = f"DENIED ({e})"
|
||
print(f" ❌ DENIED - {e}")
|
||
|
||
# ── Test 3b: ListObjects 在 PathPrefix 下(不带 instanceId)──
|
||
raw_prefix = path_prefix.rstrip("/") + "/"
|
||
print(f"\nTest 3b: ListObjects (Prefix={raw_prefix}, 不带 instanceId)")
|
||
try:
|
||
resp = client.list_objects(Bucket=bucket, Prefix=raw_prefix, MaxKeys=10)
|
||
contents = resp.get("Contents", [])
|
||
results["ListObjects(raw)"] = f"OK ({len(contents)} objects)"
|
||
print(f" ✅ OK - 列出 {len(contents)} 个对象")
|
||
for c in contents[:5]:
|
||
print(f" - {c['Key']} ({c['Size']} bytes)")
|
||
except Exception as e:
|
||
results["ListObjects(raw)"] = f"DENIED ({e})"
|
||
print(f" ❌ DENIED - {e}")
|
||
|
||
# ── Test 3c: ListObjects 在 bucket 根目录 ──
|
||
print(f"\nTest 3c: ListObjects (Prefix='', Bucket 根目录)")
|
||
try:
|
||
resp = client.list_objects(Bucket=bucket, Prefix="", MaxKeys=10)
|
||
contents = resp.get("Contents", [])
|
||
results["ListObjects(root)"] = f"OK ({len(contents)} objects)"
|
||
print(f" ✅ OK - 列出 {len(contents)} 个对象")
|
||
for c in contents[:5]:
|
||
print(f" - {c['Key']} ({c['Size']} bytes)")
|
||
except Exception as e:
|
||
results["ListObjects(root)"] = f"DENIED ({e})"
|
||
print(f" ❌ DENIED - {e}")
|
||
|
||
# ── Test 4: PutObject (写入测试文件) ──
|
||
print(f"\nTest 4: PutObject (Key={test_key})")
|
||
try:
|
||
resp = client.put_object(
|
||
Bucket=bucket,
|
||
Body=b"permission test",
|
||
Key=test_key,
|
||
ContentType="text/plain",
|
||
)
|
||
results["PutObject"] = f"OK (ETag={resp.get('ETag', '?')})"
|
||
print(f" ✅ OK - ETag={resp.get('ETag', '?')}")
|
||
except Exception as e:
|
||
results["PutObject"] = f"DENIED ({e})"
|
||
print(f" ❌ DENIED - {e}")
|
||
|
||
# ── Test 5: GetObject (读取刚写入的文件) ──
|
||
print(f"\nTest 5: GetObject (Key={test_key})")
|
||
try:
|
||
resp = client.get_object(Bucket=bucket, Key=test_key)
|
||
body = resp["Body"].get_raw_stream().read()
|
||
results["GetObject"] = f"OK ({len(body)} bytes)"
|
||
print(f" ✅ OK - 读到 {len(body)} 字节: {body[:50]}")
|
||
except Exception as e:
|
||
results["GetObject"] = f"DENIED ({e})"
|
||
print(f" ❌ DENIED - {e}")
|
||
|
||
# ── Test 6: HeadObject (获取文件元数据) ──
|
||
print(f"\nTest 6: HeadObject (Key={test_key})")
|
||
try:
|
||
resp = client.head_object(Bucket=bucket, Key=test_key)
|
||
results["HeadObject"] = f"OK (size={resp.get('Content-Length', '?')})"
|
||
print(f" ✅ OK - Content-Length={resp.get('Content-Length', '?')}")
|
||
except Exception as e:
|
||
results["HeadObject"] = f"DENIED ({e})"
|
||
print(f" ❌ DENIED - {e}")
|
||
|
||
# ── Test 7: DeleteObject (删除测试文件) ──
|
||
print(f"\nTest 7: DeleteObject (Key={test_key})")
|
||
try:
|
||
client.delete_object(Bucket=bucket, Key=test_key)
|
||
results["DeleteObject"] = "OK"
|
||
print(f" ✅ OK - 删除成功")
|
||
except Exception as e:
|
||
results["DeleteObject"] = f"DENIED ({e})"
|
||
print(f" ❌ DENIED - {e}")
|
||
|
||
# ── 汇总 ──
|
||
print(f"\n{'=' * 60}")
|
||
print("权限汇总:")
|
||
print(f"{'=' * 60}")
|
||
for op, status in results.items():
|
||
icon = "✅" if status.startswith("OK") else "❌"
|
||
print(f" {icon} {op:25s} → {status}")
|
||
print(f"{'=' * 60}")
|
||
|
||
ok_count = sum(1 for s in results.values() if s.startswith("OK"))
|
||
total = len(results)
|
||
print(f"\n结论: {ok_count}/{total} 项操作有权限")
|
||
|
||
if ok_count == 0:
|
||
print("⚠️ STS 临时密钥无任何 COS 操作权限!需要 Shark 团队修复 STS Policy。")
|
||
print(" 所需权限: cos:GetObject, cos:PutObject, cos:DeleteObject, cos:GetBucket, cos:HeadObject")
|
||
elif ok_count < total:
|
||
denied = [op for op, s in results.items() if not s.startswith("OK")]
|
||
print(f"⚠️ 部分操作无权限: {', '.join(denied)}")
|
||
else:
|
||
print("✅ STS 临时密钥拥有完整的 COS 操作权限,write_scenario 应该可以正常工作。")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
print("STS 临时密钥权限排查")
|
||
print(f"Shark 地址: {SHARK_URL}")
|
||
print(f"实例 ID: {INSTANCE_ID}")
|
||
|
||
data = fetch_cos_config()
|
||
test_permissions(data)
|