mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 12:30:44 +00:00
发布脚本:提前检查 EzHub 令牌写权限
Gitea 令牌可以只给读权限。只读令牌能读仓库、能列发行版, 看起来一切正常,但一推代码就 403、一建发行版也 403。 加了 check_ezhub_scope():开头就用一个**无副作用**的探针确认权限 (对不存在的发行版发 DELETE —— 403 说明缺 write:repository, 404 说明权限过了只是资源不存在),缺权限就立刻停下并给出 明确的修复指引,不让它走到一半才炸。 文档里也写清了这个坑。
This commit is contained in:
@@ -0,0 +1,96 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
r"""把 EzHub 的令牌存到仓库外,并支持用 HTTPS + 令牌推代码。
|
||||||
|
|
||||||
|
为什么需要这个:
|
||||||
|
用户加 SSH 公钥时可能加错了(服务端拒绝了本机的
|
||||||
|
SHA256:Acmu49YQcjw3m5WefOXSBzYNY2WPHLL2vvhnlYRAttA),
|
||||||
|
但令牌有 push 权限,走 HTTPS 就能推。
|
||||||
|
|
||||||
|
为什么不把令牌写进 remote URL:
|
||||||
|
`git remote -v` 会直接打印出来,任何时候截图/录屏都会泄。
|
||||||
|
所以令牌存在仓库外的 %USERPROFILE%\.ykxmz\ezhub_token,
|
||||||
|
推送时临时拼 URL,推完不留痕。
|
||||||
|
|
||||||
|
用法:
|
||||||
|
python _build/ezhub_push.py # 用存好的令牌推
|
||||||
|
python _build/ezhub_push.py <令牌> # 先存再用这个令牌推
|
||||||
|
python _build/ezhub_push.py --show # 只看存了什么(打码)
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
|
||||||
|
GIT = r"C:\Program Files\Git\bin\git.exe"
|
||||||
|
TOKEN_FILE = Path.home() / ".ykxmz" / "ezhub_token"
|
||||||
|
|
||||||
|
OWNER = "xmz"
|
||||||
|
REPO = "ykxmz"
|
||||||
|
HOST = "git.261826.xyz"
|
||||||
|
|
||||||
|
|
||||||
|
def save_token(token: str) -> None:
|
||||||
|
TOKEN_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
TOKEN_FILE.write_text(token.strip(), encoding="utf-8")
|
||||||
|
# 尽量收紧权限(Windows 上效果有限,但聊胜于无)
|
||||||
|
try:
|
||||||
|
import os
|
||||||
|
os.chmod(TOKEN_FILE, 0o600)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
print(" 令牌已存到 %s(仓库外,不会进版本库)" % TOKEN_FILE)
|
||||||
|
|
||||||
|
|
||||||
|
def load_token() -> str:
|
||||||
|
if TOKEN_FILE.is_file():
|
||||||
|
return TOKEN_FILE.read_text(encoding="utf-8").strip()
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
args = [a for a in sys.argv[1:] if a]
|
||||||
|
|
||||||
|
if args and args[0] == "--show":
|
||||||
|
token = load_token()
|
||||||
|
if not token:
|
||||||
|
print(" 还没存令牌")
|
||||||
|
else:
|
||||||
|
print(" 令牌: %s…%s(长度 %d)"
|
||||||
|
% (token[:8], token[-4:], len(token)))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if args and not args[0].startswith("--"):
|
||||||
|
save_token(args[0])
|
||||||
|
token = load_token()
|
||||||
|
if not token:
|
||||||
|
print(" 用法: python _build/ezhub_push.py <令牌>")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# 带令牌的 HTTPS 地址(只在这次进程里用,不写进 .git/config)
|
||||||
|
url = "https://%s:%s@%s/%s/%s.git" % (OWNER, token, HOST, OWNER, REPO)
|
||||||
|
|
||||||
|
print(" 推到 EzHub(HTTPS + 令牌)…")
|
||||||
|
result = subprocess.run([GIT, "push", url, "main"], cwd=str(ROOT),
|
||||||
|
capture_output=True, text=True,
|
||||||
|
encoding="utf-8", errors="replace")
|
||||||
|
err = (result.stderr or result.stdout or "").strip()
|
||||||
|
|
||||||
|
# 输出里可能带令牌,打码后再打印
|
||||||
|
safe = err.replace(token, "***")
|
||||||
|
for line in safe.splitlines()[-6:]:
|
||||||
|
# 远程 URL 里的令牌也打掉
|
||||||
|
line = line.replace(url, "https://%s:***@%s/%s/%s.git"
|
||||||
|
% (OWNER, HOST, OWNER, REPO))
|
||||||
|
print(" " + line)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
print(" [失败] exit=%s" % result.returncode)
|
||||||
|
return result.returncode
|
||||||
|
print(" ✓ 推完了")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
r'''判定 EzHub 令牌到底有没有写权限(无副作用)。
|
||||||
|
|
||||||
|
方法:对一个**不存在的**资源发写请求。
|
||||||
|
* 令牌没有写权限 -> 服务端先拦权限,返回 403
|
||||||
|
* 令牌有写权限 -> 通过权限检查,然后发现资源不存在,返回 404
|
||||||
|
所以 403 / 404 这个差别就能说明问题,而且不会真的改到任何东西。
|
||||||
|
'''
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import urllib.error
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
TOKEN = sys.argv[1] if len(sys.argv) > 1 else ""
|
||||||
|
if not TOKEN:
|
||||||
|
print(" 用法: python probe_token.py <令牌>")
|
||||||
|
raise SystemExit(1)
|
||||||
|
|
||||||
|
BASE = "https://git.261826.xyz/api/v1"
|
||||||
|
HEAD = {"Authorization": "token " + TOKEN, "User-Agent": "ykxmz-probe"}
|
||||||
|
|
||||||
|
|
||||||
|
def probe(method: str, path: str, payload: dict | None = None,
|
||||||
|
label: str = "") -> int:
|
||||||
|
data = json.dumps(payload).encode() if payload else None
|
||||||
|
headers = dict(HEAD)
|
||||||
|
if data:
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
req = urllib.request.Request(BASE + path, data=data, method=method,
|
||||||
|
headers=headers)
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||||
|
print(" %-10s %-46s -> %s" % (method, path, resp.status))
|
||||||
|
return resp.status
|
||||||
|
except urllib.error.HTTPError as exc:
|
||||||
|
body = exc.read().decode("utf-8", "replace")[:110]
|
||||||
|
print(" %-10s %-46s -> %s %s"
|
||||||
|
% (method, path, exc.code, body))
|
||||||
|
return exc.code
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
print(" %-10s %-46s -> 异常 %s" % (method, path, exc))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
print(" 一、读权限(应该 200)")
|
||||||
|
probe("GET", "/user")
|
||||||
|
probe("GET", "/repos/xmz/ykxmz")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(" 二、写权限判定(对不存在的资源写)")
|
||||||
|
print(" 403 = 没写权限;404 = 有写权限(只是资源不存在)")
|
||||||
|
del_code = probe("DELETE", "/repos/xmz/ykxmz/releases/99999999")
|
||||||
|
print()
|
||||||
|
patch_code = probe("PATCH", "/repos/xmz/ykxmz/releases/99999999",
|
||||||
|
{"body": "probe"})
|
||||||
|
print()
|
||||||
|
post_code = probe("POST", "/repos/xmz/ykxmz/releases",
|
||||||
|
{"tag_name": "", "name": "", "body": ""})
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(" 三、结论")
|
||||||
|
if del_code == 404 or patch_code == 404:
|
||||||
|
print(" ✓ 令牌**有写权限**(返回 404 说明权限过了,只是资源不存在)")
|
||||||
|
print(" -> HTTPS push 的 403 是另一个原因,需要再查")
|
||||||
|
elif del_code == 403 or patch_code == 403:
|
||||||
|
print(" ✗ 令牌**没有写权限**(403)")
|
||||||
|
print(" -> 需要重新生成令牌,勾上 write:repository")
|
||||||
|
else:
|
||||||
|
print(" ? 结果不明确:DELETE=%s PATCH=%s POST=%s"
|
||||||
|
% (del_code, patch_code, post_code))
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(" 四、顺带看一下令牌信息")
|
||||||
|
try:
|
||||||
|
req = urllib.request.Request(BASE + "/user", headers=HEAD)
|
||||||
|
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||||
|
for name in ("X-Oauth-Scopes", "X-Oauth-Client-Id"):
|
||||||
|
value = resp.headers.get(name)
|
||||||
|
if value:
|
||||||
|
print(" %s: %s" % (name, value))
|
||||||
|
me = json.loads(resp.read().decode())
|
||||||
|
print(" 用户: %s (id=%s, admin=%s)"
|
||||||
|
% (me.get("login"), me.get("id"), me.get("is_admin")))
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
print(" 读取失败: %s" % exc)
|
||||||
@@ -227,6 +227,43 @@ def gitea_release(token: str, version: str, body: str,
|
|||||||
return ok == len(files)
|
return ok == len(files)
|
||||||
|
|
||||||
|
|
||||||
|
def check_ezhub_scope(token: str) -> bool:
|
||||||
|
"""提前确认 EzHub 令牌有 write:repository。
|
||||||
|
|
||||||
|
为什么要单独查:
|
||||||
|
Gitea 的令牌可以只给读权限。只读令牌能读仓库、能列发行版,
|
||||||
|
看起来"正常",但一推代码就 403、一建发行版就 403,
|
||||||
|
而且报错信息藏在一层层输出里。所以开头就探一下。
|
||||||
|
|
||||||
|
探法(无副作用):删一个**不存在的**发行版。
|
||||||
|
403 -> 缺 write:repository
|
||||||
|
404 -> 有写权限(只是资源不存在)
|
||||||
|
"""
|
||||||
|
url = ("https://git.261826.xyz/api/v1/repos/xmz/ykxmz"
|
||||||
|
"/releases/99999999")
|
||||||
|
req = urllib.request.Request(
|
||||||
|
url, method="DELETE",
|
||||||
|
headers={"Authorization": "token " + token,
|
||||||
|
"User-Agent": "ykxmz-release"})
|
||||||
|
try:
|
||||||
|
urllib.request.urlopen(req, timeout=30)
|
||||||
|
return True
|
||||||
|
except urllib.error.HTTPError as exc:
|
||||||
|
if exc.code == 404:
|
||||||
|
return True
|
||||||
|
detail = exc.read().decode("utf-8", "replace")[:200]
|
||||||
|
if "write:repository" in detail:
|
||||||
|
print(" ✗ EzHub 令牌缺 write:repository 权限")
|
||||||
|
print(" -> 去 EzHub:设置 → 应用 → 生成令牌,"
|
||||||
|
"权限勾上 write:repository")
|
||||||
|
return False
|
||||||
|
print(" ✗ EzHub 令牌检查失败 HTTP %s: %s" % (exc.code, detail))
|
||||||
|
return False
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
print(" ✗ 连不上 EzHub: %s" % exc)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
args = [a for a in sys.argv[1:] if not a.startswith("--")]
|
args = [a for a in sys.argv[1:] if not a.startswith("--")]
|
||||||
dry = "--dry" in sys.argv
|
dry = "--dry" in sys.argv
|
||||||
@@ -269,6 +306,11 @@ def main() -> int:
|
|||||||
return 1
|
return 1
|
||||||
print(" ✓ 工作区干净")
|
print(" ✓ 工作区干净")
|
||||||
|
|
||||||
|
# 两个平台的令牌都得有写权限,否则走到一半才失败
|
||||||
|
if not check_ezhub_scope(ezhub_token):
|
||||||
|
return 1
|
||||||
|
print(" ✓ EzHub 令牌有写权限")
|
||||||
|
|
||||||
# 对外材料清洁度:绝不能让管理页字眼进发行版说明
|
# 对外材料清洁度:绝不能让管理页字眼进发行版说明
|
||||||
for word in ("zhongzhuanzhan", "/admin", "管理后台", "管理页"):
|
for word in ("zhongzhuanzhan", "/admin", "管理后台", "管理页"):
|
||||||
if word in body:
|
if word in body:
|
||||||
|
|||||||
Reference in New Issue
Block a user