feat: separate enterprise work and personal routes

This commit is contained in:
冰朔 2026-08-17 00:16:24 +08:00
commit 7414810862
9 changed files with 546 additions and 41 deletions

View file

@ -19,6 +19,8 @@ API 路由对客户端开放:
- 编号解析:把 TCS-GL 人类编号路由到工作域、企业账号和私有仓库;
- 关系确认:由人类确认自己与人格体的认领关系;
- 责任回执:独立记录对域责任的接受、拒绝、延期或修改后接受;
- 回执入仓:关系与责任签名回执使用提交者自己的 Forgejo 会话写入本人私有工作仓库的
`.guanghu/receipts/`,稳定路径与读回校验保证重试不重复;服务不持有管理员仓库令牌;
- 仓库验证:登录凭证只透传给同机 Forgejo 验证,不写入数据库或日志。
- 首次换密:使用用户自己的一次性凭证进入 Forgejo 强制换密会话,不持有长期管理员令牌;
换密回执只记录账号、时间和成功状态,不记录旧密码或新密码。

View file

@ -39,6 +39,9 @@ FORGEJO_USER_API = os.environ.get(
FORGEJO_WEB_BASE = os.environ.get(
"GH_ENTERPRISE_FORGEJO_WEB_BASE", "https://guanghu.chat/code"
).rstrip("/")
FORGEJO_API_BASE = os.environ.get(
"GH_ENTERPRISE_FORGEJO_API_BASE", "http://127.0.0.1:3341/api/v1"
).rstrip("/")
RECEIPT_KEY = os.environ.get("GH_ENTERPRISE_RECEIPT_KEY", "")
MAX_BODY = 16_384
USERNAME = re.compile(r"^[A-Za-z0-9_-]{1,40}$")
@ -210,6 +213,90 @@ def signed_receipt(payload: dict) -> dict:
return {**payload, "receipt_hash": receipt_hash, "receipt_signature": signature}
def stable_receipt_id(prefix: str, human_number: str, idempotency_key: str) -> str:
"""Keep one receipt path across safe client retries without exposing the key."""
material = f"{prefix}\n{human_number}\n{idempotency_key}".encode()
digest = hmac.new(RECEIPT_KEY.encode(), material, hashlib.sha256).hexdigest()[:32]
return f"{prefix}-{digest.upper()}"
def repository_receipt_path(kind: str, receipt_id: str) -> str:
if kind not in {"relationship", "responsibility"} or not re.fullmatch(
r"GH-(?:REL|RESP)-[A-F0-9]{32}", receipt_id
):
raise ValueError("repository receipt path input invalid")
return f".guanghu/receipts/{kind}/{receipt_id}.json"
def project_receipt_to_repository(
human: dict, username: str, password: str, kind: str, receipt: dict
) -> dict:
"""Commit a signed receipt with the human's own Forgejo authority.
No administrator token or server-side repository credential is held. A
retry that finds the deterministic path already present must read back the
exact bytes before treating the projection as idempotent.
"""
repository = str(human["repository"])
if repository.split("/", 1)[0] != username:
raise RuntimeError("repository owner does not match authenticated user")
path = repository_receipt_path(kind, str(receipt["receipt_id"]))
endpoint = (
f"{FORGEJO_API_BASE}/repos/{urllib.parse.quote(repository, safe='/')}"
f"/contents/{urllib.parse.quote(path, safe='/')}"
)
content = canonical(receipt) + b"\n"
authorization = "Basic " + base64.b64encode(f"{username}:{password}".encode()).decode()
create_body = canonical(
{
"branch": "main",
"content": base64.b64encode(content).decode(),
"message": f"receipt({kind}): {receipt['receipt_id']}",
}
)
request = urllib.request.Request(endpoint, data=create_body, method="POST")
request.add_header("Authorization", authorization)
request.add_header("Accept", "application/json")
request.add_header("Content-Type", "application/json")
try:
with urllib.request.urlopen(request, timeout=15) as response:
result = json.load(response)
if response.status != 201:
raise RuntimeError(f"repository projection returned {response.status}")
commit = str(result.get("commit", {}).get("sha", ""))
if not re.fullmatch(r"[0-9a-f]{40,64}", commit):
raise RuntimeError("repository projection commit missing")
return {
"state": "COMMITTED",
"repository": repository,
"path": path,
"commit": commit,
}
except urllib.error.HTTPError as error:
if error.code != 422:
raise RuntimeError(f"repository projection failed: {error.code}") from error
read_request = urllib.request.Request(endpoint + "?ref=main")
read_request.add_header("Authorization", authorization)
read_request.add_header("Accept", "application/json")
try:
with urllib.request.urlopen(read_request, timeout=15) as response:
existing = json.load(response)
except (urllib.error.HTTPError, urllib.error.URLError, TimeoutError, ValueError) as error:
raise RuntimeError("repository projection readback failed") from error
try:
existing_content = base64.b64decode(str(existing["content"]), validate=True)
except (KeyError, ValueError) as error:
raise RuntimeError("repository projection readback invalid") from error
if not hmac.compare_digest(existing_content, content):
raise RuntimeError("repository receipt path already contains different bytes")
return {
"state": "IDEMPOTENT_READBACK",
"repository": repository,
"path": path,
"commit": str(existing.get("sha", "")),
}
def public_projection(registry: dict, human: dict, db: sqlite3.Connection | None = None) -> dict:
projection = {
"status": "RESOLVED",
@ -273,7 +360,7 @@ class Handler(BaseHTTPRequestHandler):
raise ValueError("JSON object required")
return value
def authenticated_human(self, registry: dict, payload: dict) -> tuple[dict, str] | None:
def authenticated_human(self, registry: dict, payload: dict) -> tuple[dict, str, str] | None:
credentials = parse_basic(self.headers.get("Authorization", ""))
number = str(payload.get("human_number", ""))
human = find_human(registry, number)
@ -282,7 +369,7 @@ class Handler(BaseHTTPRequestHandler):
username, password = credentials
if not hmac.compare_digest(username, human["username"]) or not verify_forgejo(username, password):
return None
return human, username
return human, username, password
def do_GET(self) -> None:
try:
@ -349,7 +436,7 @@ class Handler(BaseHTTPRequestHandler):
authenticated = self.authenticated_human(registry, payload)
if not authenticated:
return self.respond(401, {"ok": False, "error": "enterprise account authentication failed"})
human, username = authenticated
human, username, password = authenticated
idempotency_key = str(payload.get("idempotency_key", ""))
if not re.fullmatch(r"[A-Za-z0-9_-]{16,96}", idempotency_key):
return self.respond(400, {"ok": False, "error": "valid idempotency_key required"})
@ -363,12 +450,16 @@ class Handler(BaseHTTPRequestHandler):
if existing:
return self.respond(200, {"ok": True, "idempotent": True, "receipt": dict(existing)})
observed = now()
receipt_id = "GH-REL-" + uuid.uuid4().hex.upper()
receipt_id = stable_receipt_id("GH-REL", human["human_number"], idempotency_key)
receipt = signed_receipt({"receipt_id":receipt_id,"human_number":human["human_number"],"username":username,"registry_version":registry["version"],"decision":decision,"observed_at":observed})
try:
projection = project_receipt_to_repository(human, username, password, "relationship", receipt)
except RuntimeError as error:
return self.respond(503, {"ok": False, "error": str(error)})
db.execute("INSERT INTO relationship_receipts VALUES (?,?,?,?,?,?,?,?,?)", (receipt_id,idempotency_key,human["human_number"],username,registry["version"],decision,observed,receipt["receipt_hash"],receipt["receipt_signature"]))
db.execute("INSERT INTO audit(observed_at,kind,human_number_hash,receipt_id) VALUES (?,?,?,?)", (observed,"RELATIONSHIP_CONFIRMATION",hashlib.sha256(human["human_number"].encode()).hexdigest(),receipt_id))
db.commit()
return self.respond(201, {"ok": True, "receipt": receipt})
return self.respond(201, {"ok": True, "receipt": receipt, "repository_projection": projection})
if self.path == "/v1/responsibility-receipts":
decision = str(payload.get("decision", ""))
note = str(payload.get("note", ""))[:1000]
@ -379,12 +470,16 @@ class Handler(BaseHTTPRequestHandler):
if existing:
return self.respond(200, {"ok": True, "idempotent": True, "receipt": dict(existing)})
observed = now()
receipt_id = "GH-RESP-" + uuid.uuid4().hex.upper()
receipt_id = stable_receipt_id("GH-RESP", human["human_number"], idempotency_key)
receipt = signed_receipt({"receipt_id":receipt_id,"human_number":human["human_number"],"username":username,"responsibility_domain":human["responsibility_domain"],"responsibility_version":version,"decision":decision,"note":note,"observed_at":observed})
try:
projection = project_receipt_to_repository(human, username, password, "responsibility", receipt)
except RuntimeError as error:
return self.respond(503, {"ok": False, "error": str(error)})
db.execute("INSERT INTO responsibility_receipts VALUES (?,?,?,?,?,?,?,?,?,?,?)", (receipt_id,idempotency_key,human["human_number"],username,human["responsibility_domain"],version,decision,note,observed,receipt["receipt_hash"],receipt["receipt_signature"]))
db.execute("INSERT INTO audit(observed_at,kind,human_number_hash,receipt_id) VALUES (?,?,?,?)", (observed,"RESPONSIBILITY_RECEIPT",hashlib.sha256(human["human_number"].encode()).hexdigest(),receipt_id))
db.commit()
return self.respond(201, {"ok": True, "receipt": receipt})
return self.respond(201, {"ok": True, "receipt": receipt, "repository_projection": projection})
if self.path == "/v1/me/entry":
return self.respond(200, {"ok": True, "entry": public_projection(registry, human, db)})
return self.respond(404, {"ok": False, "error": "not found"})

View file

@ -3,7 +3,9 @@ import json
import os
import tempfile
import unittest
import urllib.error
from pathlib import Path
from unittest import mock
ROOT = Path(__file__).parent
REGISTRY = ROOT / "registry" / "enterprise-identity-registry.json"
@ -60,6 +62,53 @@ class EnterpriseIdentityTests(unittest.TestCase):
self.assertIn("/user/settings/change_password", source)
self.assertNotIn("FORGEJO_ADMIN_TOKEN", source)
def test_receipt_id_and_repository_path_are_stable_without_exposing_idempotency_key(self):
first = service.stable_receipt_id("GH-RESP", "TCS-GL-0007∞", "responsibility-1234567890")
second = service.stable_receipt_id("GH-RESP", "TCS-GL-0007∞", "responsibility-1234567890")
self.assertEqual(first, second)
self.assertRegex(first, r"^GH-RESP-[A-F0-9]{32}$")
self.assertNotIn("1234567890", first)
self.assertEqual(
service.repository_receipt_path("responsibility", first),
f".guanghu/receipts/responsibility/{first}.json",
)
def test_repository_projection_uses_the_humans_own_forgejo_authority(self):
registry = service.load_registry()
human = service.find_human(registry, "TCS-GL-0007∞")
receipt_id = service.stable_receipt_id("GH-REL", human["human_number"], "relationship-1234567890")
receipt = service.signed_receipt(
{"receipt_id": receipt_id, "human_number": human["human_number"], "username": "feimao"}
)
class Response:
status = 201
def __enter__(self): return self
def __exit__(self, *_): return False
def read(self):
return json.dumps({"commit": {"sha": "a" * 40}}).encode()
with mock.patch.object(service.urllib.request, "urlopen", return_value=Response()) as opened:
projection = service.project_receipt_to_repository(
human, "feimao", "one-use-secret", "relationship", receipt
)
request = opened.call_args.args[0]
self.assertEqual(projection["repository"], "feimao/guanghu-zero-sense-work")
self.assertIn("/repos/feimao/guanghu-zero-sense-work/contents/", request.full_url)
self.assertTrue(request.headers["Authorization"].startswith("Basic "))
self.assertNotIn("one-use-secret", request.data.decode())
def test_repository_projection_refuses_cross_owner_repository(self):
human = {"repository": "juzi/guanghu-zero-sense-work"}
with self.assertRaisesRegex(RuntimeError, "owner"):
service.project_receipt_to_repository(
human,
"feimao",
"secret",
"relationship",
{"receipt_id": "GH-REL-" + "A" * 32},
)
if __name__ == "__main__":
unittest.main()