P0-A: 修复鉴权体系

- check_endpoint_auth 路径归一化(无前导斜杠),v1/v2 端点级鉴权真正生效
- ADMIN_API_ENDPOINTS 重建:修复 files 单复数、补 mirrors/alerts/prewarm/api-docs/user-password
- handler check_auth 方法感知:公开端点仅 GET/HEAD,写操作默认要求认证(fail-closed)
- 补全受保护端点(monitor/activity/login-logs/mirrors/alerts/prewarm/file 元数据)
- _check_basic_auth/_check_token_auth 改用 hmac.compare_digest
- do_POST/do_HEAD 解析路径时剥离查询串;do_HEAD 增加 is_safe_path 检查
- is_safe_path 改用 realpath 防符号链接绕过
This commit is contained in:
HYC Fixer
2026-08-30 12:16:14 +08:00
parent a8e773839b
commit ffae028664
5 changed files with 164 additions and 66 deletions
+73 -29
View File
@@ -520,39 +520,53 @@ def require_auth(required_level: str = "admin", permission: str = None):
# === 需要认证的API端点定义 ===
# 规则格式: 'METHOD:/api/vN/path' 或 'METHOD:/api/vN/prefix/'(前缀规则,尾斜杠)
# 匹配时路径统一归一化为无前导斜杠形式,与 v1/v2 传入的 api_action 对齐
ADMIN_API_ENDPOINTS = {
# 同步管理
'POST:/api/v2/sync/*': 'sync:manage',
'POST:/api/v2/sync/*/start': 'sync:start',
'POST:/api/v2/sync/*/stop': 'sync:stop',
'DELETE:/api/v2/sync/*': 'sync:manage',
'POST:/api/v2/sync/': 'sync:manage',
'DELETE:/api/v2/sync/': 'sync:manage',
# 缓存管理
'POST:/api/v2/cache/clean': 'cache:manage',
'DELETE:/api/v2/cache/*': 'cache:manage',
'POST:/api/v2/cache/prewarm/': 'cache:manage',
'DELETE:/api/v2/cache/prewarm/': 'cache:manage',
# Webhook管理
'POST:/api/v2/webhooks': 'webhook:create',
'PUT:/api/v2/webhooks/*': 'webhook:update',
'DELETE:/api/v2/webhooks/*': 'webhook:delete',
'POST:/api/v2/webhooks/*/trigger': 'webhook:trigger',
'PUT:/api/v2/webhooks/': 'webhook:update',
'DELETE:/api/v2/webhooks/': 'webhook:delete',
'POST:/api/v2/webhooks/': 'webhook:trigger',
# 服务器配置
'PUT:/api/v2/config': 'config:manage',
'POST:/api/v2/server/reload': 'server:reload',
'POST:/api/v2/server/': 'server:manage',
# 文件管理(高危操作)
'DELETE:/api/v2/files/*': 'files:delete',
'PUT:/api/v2/files/*/rename': 'files:rename',
# 文件管理(高危操作: 删除/重命名/元数据/版本)
'DELETE:/api/v1/file/': 'files:delete',
'DELETE:/api/v2/file/': 'files:delete',
'PUT:/api/v2/file/': 'files:update',
'POST:/api/v2/file/': 'files:update',
# 用户管理
'POST:/api/v2/users': 'users:create',
'DELETE:/api/v2/users/*': 'users:delete',
'PUT:/api/v2/users/*': 'users:update',
'DELETE:/api/v2/users/': 'users:delete',
'PUT:/api/v2/users/': 'users:update',
'POST:/api/v2/user/password': 'users:update',
# 镜像管理(写 settings.json,必须鉴权)
'POST:/api/v2/mirrors': 'mirrors:manage',
'PUT:/api/v2/mirrors/': 'mirrors:manage',
'DELETE:/api/v2/mirrors/': 'mirrors:manage',
# 告警配置与API文档生成
'PUT:/api/v2/alerts': 'config:manage',
'POST:/api/v2/alerts': 'config:manage',
'POST:/api/v2/api-docs/generate': 'config:manage',
# === API v1 文件操作认证 ===
'DELETE:/api/v1/file/*': 'files:delete',
'PUT:/api/v1/mkdir': 'files:create',
'POST:/api/v1/upload': 'files:upload',
'POST:/api/v1/batch': 'files:batch',
@@ -560,26 +574,56 @@ ADMIN_API_ENDPOINTS = {
}
def _normalize_endpoint_patterns():
"""把 ADMIN_API_ENDPOINTS 归一化为 (精确表, 前缀表)
规则格式: 'METHOD:/api/vN/path'
- 无通配符 → 精确匹配 (method, path)
- 尾斜杠或尾* → 前缀匹配, 匹配 norm_path.startswith(prefix + '/')
"""
exact = {}
prefix = []
for pattern, permission in ADMIN_API_ENDPOINTS.items():
pat_method, pat_path = pattern.split(':', 1)
pat_path = pat_path.lstrip('/')
if '*' in pattern or pat_path.endswith('/'):
if pat_path.endswith('*'):
pat_path = pat_path.rstrip('*')
pat_path = pat_path.rstrip('/')
prefix.append((pat_method, pat_path, permission))
else:
exact[(pat_method, pat_path)] = permission
return exact, prefix
_EXACT_ENDPOINTS, _PREFIX_ENDPOINTS = _normalize_endpoint_patterns()
def check_endpoint_auth(method: str, path: str, auth_manager: APIAuthManager) -> dict:
"""检查端点是否需要认证"""
key = f"{method}:{path}"
if key in ADMIN_API_ENDPOINTS:
"""检查端点是否需要认证
路径统一归一化为无前导斜杠形式(如 'api/v2/sync/sources'),
与 v1/v2 传入的 api_action 保持一致。
"""
norm_path = path.lstrip('/')
# 精确匹配
key = (method, norm_path)
if key in _EXACT_ENDPOINTS:
return {
"required": True,
"permission": ADMIN_API_ENDPOINTS[key]
"permission": _EXACT_ENDPOINTS[key]
}
for pattern, permission in ADMIN_API_ENDPOINTS.items():
if '*' in pattern:
pat_method, pat_path = pattern.split(':', 1)
if method == pat_method or pat_method == '*':
if pat_path.endswith('*'):
prefix = pat_path.rstrip('*').rstrip('/')
if path.startswith(prefix):
return {
"required": True,
"permission": permission
}
# 前缀匹配
for pat_method, pat_path, permission in _PREFIX_ENDPOINTS:
if method != pat_method and pat_method != '*':
continue
if norm_path.startswith(pat_path + '/'):
return {
"required": True,
"permission": permission
}
return {
"required": False,