[HLCC-ICE-000003] fix: recover code-channel owner login safely

This commit is contained in:
冰朔 2026-07-26 15:02:01 +08:00
commit 100848e9e4
12 changed files with 235 additions and 16 deletions

View file

@ -47,8 +47,119 @@ printf 'PASSWORD_AUTH=%s\nROOT_PASSWORD_AUTH=%s\nSSH=%s\nBACKUP=%s\n' \
"$backup_dir/90-guanghu-key-only.conf.before"
trap - EXIT`;
function run(file, args) {
return new Promise(resolve => execFile(file, args, { timeout: 30000, maxBuffer: 100000 }, (error, stdout, stderr) => resolve({
const RESTORE_CODE_CHANNEL_OWNER_LOGIN = String.raw`set -eu
test "$(id -u)" = "0"
/usr/bin/python3 - <<'PY'
import atexit
import datetime
import json
import os
import pathlib
import sqlite3
import subprocess
import time
import urllib.request
state = pathlib.Path("/var/lib/guanghu/personas/guanghu/hlcc-v16.0.1")
channel_db = state / "data/hlcc.db"
legacy_db = pathlib.Path("/var/lib/guanghu/forgejo/data/forgejo.db")
stamp = datetime.datetime.now(datetime.timezone.utc).strftime("%Y%m%dT%H%M%SZ")
backup = state / "data/backups" / f"owner-login-recovery-{stamp}.db"
service_started = False
def ensure_service_started():
if not service_started:
subprocess.run(["systemctl", "start", "hlcc-jd-candidate.service"])
atexit.register(ensure_service_started)
subprocess.run(["systemctl", "stop", "hlcc-jd-candidate.service"], check=True)
channel = sqlite3.connect(channel_db, timeout=30)
snapshot = sqlite3.connect(backup, timeout=30)
try:
channel.execute("pragma wal_checkpoint(truncate)")
channel.backup(snapshot)
finally:
snapshot.close()
os.chmod(backup, 0o600)
legacy = sqlite3.connect(f"file:{legacy_db}?mode=ro", uri=True, timeout=15)
try:
credential = legacy.execute(
"select passwd,salt,passwd_hash_algo from user where lower_name=?",
("bingshuo",),
).fetchone()
finally:
legacy.close()
if not credential or not all(credential):
raise RuntimeError("legacy owner credential unavailable")
try:
channel.execute("begin immediate")
changed = channel.execute(
"""update user
set passwd=?,salt=?,passwd_hash_algo=?,is_active=1,is_admin=1,
prohibit_login=0,must_change_password=0,updated_unix=?
where lower_name=?""",
(*credential, int(time.time()), "bingshuo"),
).rowcount
if changed != 1:
raise RuntimeError("channel owner identity mismatch")
channel.commit()
copied = channel.execute(
"""select passwd,salt,passwd_hash_algo,is_active,is_admin,
prohibit_login,must_change_password
from user where lower_name=?""",
("bingshuo",),
).fetchone()
if copied[:3] != credential or copied[3:] != (1, 1, 0, 0):
raise RuntimeError("owner login recovery verification failed")
if channel.execute("pragma integrity_check").fetchone()[0] != "ok":
raise RuntimeError("channel database integrity check failed")
channel.execute("pragma wal_checkpoint(truncate)")
finally:
channel.close()
subprocess.run(["systemctl", "start", "hlcc-jd-candidate.service"], check=True)
service_started = True
status = {}
for _ in range(90):
try:
with urllib.request.urlopen("http://127.0.0.1:3341/health", timeout=2) as response:
status = json.load(response)
with urllib.request.urlopen("http://127.0.0.1:3340/api/healthz", timeout=2) as response:
app = json.load(response)
if status.get("ready") is True and app.get("status") == "pass":
break
except Exception:
pass
time.sleep(1)
else:
subprocess.run(["systemctl", "stop", "hlcc-jd-candidate.service"])
service_started = False
restored = sqlite3.connect(channel_db, timeout=30)
saved = sqlite3.connect(f"file:{backup}?mode=ro", uri=True, timeout=30)
try:
saved.backup(restored)
finally:
saved.close()
restored.close()
subprocess.run(["systemctl", "start", "hlcc-jd-candidate.service"], check=True)
service_started = True
raise RuntimeError("channel health failed; database restored from backup")
print(json.dumps({
"ACCOUNT": "bingshuo",
"CREDENTIAL_SOURCE": "legacy-local-db",
"ACTIVE": True,
"ADMIN": True,
"PROHIBIT_LOGIN": False,
"SERVICE": "ready",
"BACKUP": str(backup),
}, ensure_ascii=False))
PY`;
function run(file, args, timeout = 30000) {
return new Promise(resolve => execFile(file, args, { timeout, maxBuffer: 100000 }, (error, stdout, stderr) => resolve({
ok: !error,
exit_code: error ? (Number.isInteger(error.code) ? error.code : 1) : 0,
stdout: String(stdout || "").slice(0, 100000),
@ -58,8 +169,14 @@ function run(file, args) {
async function execute(request) {
if (!request || request.cmd || request.command || request.shell || request.args) return { ok: false, error: "arbitrary_command_forbidden" };
if (request.target !== "JD-FD-PRIMARY" || request.action !== "restore-owner-password-login") return { ok: false, error: "action_not_registered" };
return run("/usr/bin/ssh", ["-F", SSH_CONFIG, "-o", "BatchMode=yes", "-o", "ConnectTimeout=10", "enterprise-lighthouse", RESTORE_OWNER_PASSWORD_LOGIN]);
if (request.target !== "JD-FD-PRIMARY") return { ok: false, error: "action_not_registered" };
const commands = {
"restore-owner-password-login": RESTORE_OWNER_PASSWORD_LOGIN,
"restore-code-channel-owner-login": RESTORE_CODE_CHANNEL_OWNER_LOGIN,
};
if (!commands[request.action]) return { ok: false, error: "action_not_registered" };
const timeout = request.action === "restore-code-channel-owner-login" ? 150000 : 30000;
return run("/usr/bin/ssh", ["-F", SSH_CONFIG, "-o", "BatchMode=yes", "-o", "ConnectTimeout=10", "enterprise-lighthouse", commands[request.action]], timeout);
}
function reply(socket, value) { socket.end(`${JSON.stringify(value)}\n`); }
@ -83,4 +200,9 @@ if (require.main === module) {
});
}
module.exports = { execute, RESTORE_OWNER_PASSWORD_LOGIN, SOCKET_PATH };
module.exports = {
execute,
RESTORE_OWNER_PASSWORD_LOGIN,
RESTORE_CODE_CHANNEL_OWNER_LOGIN,
SOCKET_PATH,
};