diff --git a/deployment/requests/HLCC-NATIVE-SHARED-REPOSITORY-20260806.json b/deployment/requests/HLCC-NATIVE-SHARED-REPOSITORY-20260806.json new file mode 100644 index 0000000..7d6054e --- /dev/null +++ b/deployment/requests/HLCC-NATIVE-SHARED-REPOSITORY-20260806.json @@ -0,0 +1,63 @@ +{ + "schema": "guanghu.architecture-provision-request/v1", + "request_id": "HLCC-NATIVE-SHARED-REPOSITORY-20260806", + "target_node": "JD-FD-PRIMARY", + "architecture_id": "GLS-HLCC-009", + "module": { + "code": "HLCC-JD-CANDIDATE-01", + "name": "光湖代码频道原生共享仓库权限修复", + "bind": "loopback:3340,3341", + "owner": "systemd", + "unit": "hlcc-jd-candidate.service", + "run_user": "guanghu", + "writable_paths": [ + "/var/lib/guanghu/personas/guanghu/hlcc-v16.0.1" + ], + "read_only_paths": [] + }, + "source_ref": "REPO-012:refs/heads/main", + "deployed_commit_policy": "use the exact human-merged commit and record it in the server-owned deployment receipt", + "source_paths": [ + "server-tools/hololake-code-channel/jd-candidate/hlcc-bootstrap.py", + "server-tools/hololake-code-channel/jd-candidate/app.ini", + "server-tools/hololake-code-channel/jd-candidate/hlcc-jd-candidate.service" + ], + "initial_provision": { + "kind": "new-architecture-unit", + "not_an_existing_action_bridge_extension": true, + "requires": [ + "back up the currently installed hlcc-jd-candidate.service", + "install only the declared immutable source files", + "restart only hlcc-jd-candidate.service", + "set core.sharedRepository=group on the exact bingshuo/guanghu-ice-heart bare repository", + "remove only the identified obsolete guanghu-ice-heart sharing hook", + "preserve UMask=0077 for all other code-channel state", + "do not alter repository content, branches, owner identity or authentication" + ] + }, + "verification": [ + "the candidate health endpoint returns ready=true after restart", + "the exact repository reports core.sharedRepository as group", + "the obsolete post-receive chmod hook is absent", + "a later public push no longer emits recursive chmod permission warnings", + "both code-channel and authorized direct-receiver repository checks remain valid" + ], + "runtime_check": { + "url": "http://127.0.0.1:3341/health", + "expected": { + "ok": true, + "mode": "isolated-candidate", + "version": "16.0.1", + "code": "HLCC-JD-CANDIDATE-01", + "ready": true, + "stage": "ready" + } + }, + "rollback": [ + "restore the backed-up hlcc-jd-candidate.service", + "restart the previous immutable release", + "retain repository content and all authentication state", + "record any manual permission reconciliation separately" + ], + "status": "ARCHITECTURE_PACKAGE_READY · INITIAL_PROVISION_PENDING" +} diff --git a/server-tools/hololake-code-channel/jd-candidate/hlcc-bootstrap.py b/server-tools/hololake-code-channel/jd-candidate/hlcc-bootstrap.py index 35bcf9b..3a35229 100644 --- a/server-tools/hololake-code-channel/jd-candidate/hlcc-bootstrap.py +++ b/server-tools/hololake-code-channel/jd-candidate/hlcc-bootstrap.py @@ -26,6 +26,13 @@ STATE_ROOT = pathlib.Path("/var/lib/guanghu/personas/guanghu/hlcc-v16.0.1") LEGACY_DB = STATE_ROOT / "data" / "owner-identity-source.db" OWNER_NAME = "bingshuo" CHANNEL_REPOSITORY = "guanghu-ice-heart" +CHANNEL_REPOSITORY_PATH = ( + STATE_ROOT + / "data" + / "repositories" + / OWNER_NAME + / f"{CHANNEL_REPOSITORY}.git" +) LEGACY_REPOSITORY_URL = "https://guanghulab.com/fifth-domain/bingshuo/fifth-domain.git" SEED_COMMIT_NUMBER = "HLCC-ICE-000001" SEED_CONTRIBUTION_NUMBER = "ZY-CONTRIB-20260723-001" @@ -473,6 +480,59 @@ def seed_fifth_domain_channel(binary: pathlib.Path) -> str: delete_bootstrap_token(channel_db, token_name) +def configure_shared_channel_repository( + repository: pathlib.Path = CHANNEL_REPOSITORY_PATH, +) -> str: + """Use Git's native shared-repository mode instead of a post-receive chmod.""" + if not repository.is_dir(): + raise RuntimeError("channel repository path unavailable") + + subprocess.run( + [ + "git", + "--git-dir", + str(repository), + "config", + "core.sharedRepository", + "group", + ], + check=True, + capture_output=True, + text=True, + ) + configured = subprocess.run( + [ + "git", + "--git-dir", + str(repository), + "config", + "--get", + "core.sharedRepository", + ], + check=True, + capture_output=True, + text=True, + ).stdout.strip() + if configured not in {"1", "group"}: + raise RuntimeError("shared repository configuration verification failed") + + obsolete_hook = ( + repository + / "hooks" + / "post-receive.d" + / "guanghu-ice-heart-share" + ) + if obsolete_hook.exists(): + if obsolete_hook.is_symlink() or not obsolete_hook.is_file(): + raise RuntimeError("obsolete sharing hook path is unsafe") + hook_text = obsolete_hook.read_text(encoding="utf-8") + if "post_receive_permissions_reconciled" not in hook_text: + raise RuntimeError("obsolete sharing hook identity mismatch") + obsolete_hook.unlink() + + return "configured" + + def bootstrap() -> None: process: subprocess.Popen[bytes] | None = None try: @@ -514,6 +574,8 @@ def bootstrap() -> None: migrate_owner_identity() set_status(stage="seeding-fifth-domain-channel") seed_fifth_domain_channel(binary) + set_status(stage="configuring-shared-repository") + configure_shared_channel_repository() set_status(mode="isolated-candidate", ready=True, stage="ready") return_code = process.wait() raise RuntimeError(f"candidate stopped with code {return_code}") diff --git a/server-tools/hololake-code-channel/jd-candidate/native.test.js b/server-tools/hololake-code-channel/jd-candidate/native.test.js index 336722e..bd6438f 100644 --- a/server-tools/hololake-code-channel/jd-candidate/native.test.js +++ b/server-tools/hololake-code-channel/jd-candidate/native.test.js @@ -47,6 +47,10 @@ assert.match(bootstrap, /password_hash_preserved/); assert.match(bootstrap, /access_tokens_migrated": False/); assert.match(bootstrap, /repositories_migrated": False/); assert.match(bootstrap, /delete from access_token/); +assert.match(bootstrap, /core\.sharedRepository/); +assert.match(bootstrap, /"group"/); +assert.match(bootstrap, /obsolete sharing hook identity mismatch/); +assert.match(bootstrap, /configure_shared_channel_repository\(\)/); assert.doesNotMatch( bootstrap, /print\s*\([^)]*token|stderr\.write\s*\([^)]*token|stdout\.write\s*\([^)]*token/, diff --git a/server-tools/hololake-code-channel/jd-candidate/test_owner_migration.py b/server-tools/hololake-code-channel/jd-candidate/test_owner_migration.py index 80a24cb..75cdf80 100644 --- a/server-tools/hololake-code-channel/jd-candidate/test_owner_migration.py +++ b/server-tools/hololake-code-channel/jd-candidate/test_owner_migration.py @@ -6,6 +6,7 @@ import importlib.util import json import pathlib import sqlite3 +import subprocess import tempfile import unittest @@ -138,5 +139,50 @@ class OwnerMigrationTests(unittest.TestCase): self.assertEqual(MODULE.migrate_owner_identity(self.old, self.new, self.receipt), "already-present") +class SharedRepositoryTests(unittest.TestCase): + def test_configures_native_group_sharing_and_removes_only_known_hook(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + repository = pathlib.Path(temporary) / "channel.git" + subprocess.run(["git", "init", "--bare", str(repository)], check=True, capture_output=True) + hook = repository / "hooks" / "post-receive.d" / "guanghu-ice-heart-share" + hook.parent.mkdir(parents=True) + hook.write_text( + "#!/bin/sh\nprintf '%s\\n' post_receive_permissions_reconciled\n", + encoding="utf-8", + ) + + self.assertEqual( + MODULE.configure_shared_channel_repository(repository), + "configured", + ) + configured = subprocess.run( + [ + "git", + "--git-dir", + str(repository), + "config", + "--get", + "core.sharedRepository", + ], + check=True, + capture_output=True, + text=True, + ).stdout.strip() + self.assertIn(configured, {"1", "group"}) + self.assertFalse(hook.exists()) + + def test_refuses_unknown_hook_content(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + repository = pathlib.Path(temporary) / "channel.git" + subprocess.run(["git", "init", "--bare", str(repository)], check=True, capture_output=True) + hook = repository / "hooks" / "post-receive.d" / "guanghu-ice-heart-share" + hook.parent.mkdir(parents=True) + hook.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8") + + with self.assertRaisesRegex(RuntimeError, "identity mismatch"): + MODULE.configure_shared_channel_repository(repository) + self.assertTrue(hook.exists()) + + if __name__ == "__main__": unittest.main() diff --git a/server-tools/lake-lamp-authz/README.md b/server-tools/lake-lamp-authz/README.md index 8c08e32..49f3f1d 100644 --- a/server-tools/lake-lamp-authz/README.md +++ b/server-tools/lake-lamp-authz/README.md @@ -124,11 +124,10 @@ node server-tools/lake-lamp-authz/authorize-repo-push.js \ - 当裸仓库归属代码频道服务账户时,在系统 Git 配置中把该精确路径登记为 `safe.directory`,不得使用通配符。 -光湖代码频道本身保留 `UMask=0077`,以免放宽数据库和其他状态目录。仅在 -`guanghu-ice-heart.git/hooks/post-receive.d/guanghu-ice-heart-share` 安装仓库随附的 -`hooks/guanghu-ice-heart-post-receive`,让成功的公共 Git 推送完成后校正 -`objects` 与 `refs` 的共享组权限。不要为了共享一个裸仓库而修改整个代码频道服务的 -UMask。 +光湖代码频道本身保留 `UMask=0077`,以免放宽数据库和其他状态目录。对需要由直达 +接收器共同读写的精确裸仓库设置 Git 原生 `core.sharedRepository=group`,让新建的 +`objects` 与 `refs` 从写入时就继承专用共享组权限。不要安装推送后递归 `chmod` 的 +钩子,也不要为了共享一个裸仓库而修改整个代码频道服务的 UMask。 部署后用当前主分支生成无变化验收 bundle,经 `receiveBundle` 完整执行一次;验收前后 主分支 SHA 必须一致,并且代码频道账户与授权服务账户执行 `git fsck` 均通过。 diff --git a/server-tools/lake-lamp-authz/hooks/guanghu-ice-heart-post-receive b/server-tools/lake-lamp-authz/hooks/guanghu-ice-heart-post-receive deleted file mode 100755 index 9c0aeb8..0000000 --- a/server-tools/lake-lamp-authz/hooks/guanghu-ice-heart-post-receive +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/sh -set -eu - -# The code-channel service runs with UMask=0077. Keep that isolation for its -# database and other state, but make Git objects and refs readable/writable by -# the repository's dedicated shared group after an accepted public push. -cat >/dev/null -repo_dir=$(git rev-parse --absolute-git-dir) -expected_repo=/var/lib/guanghu/personas/guanghu/hlcc-v16.0.1/data/repositories/bingshuo/guanghu-ice-heart.git -[ "$repo_dir" = "$expected_repo" ] || exit 0 -cd "$repo_dir" -owner_uid=$(id -u) - -share_tree() { - # RestrictSUIDSGID forbids chmod calls that preserve a setgid directory. - # Existing shared directories are already traversable, so touch only new - # private directories and use a numeric mode that drops the special bit. - find "$1" -user "$owner_uid" -type d ! -perm -g=x -exec chmod 0770 {} + - find "$1" -user "$owner_uid" -type f ! -perm -g=r -exec chmod g+r {} + -} - -share_tree "$repo_dir/objects" -share_tree "$repo_dir/refs" - -# receive-pack may keep new objects in a quarantine directory until hooks have -# completed. Normalize that directory before Git moves the objects into place. -if [ -n "${GIT_OBJECT_DIRECTORY:-}" ] && [ -d "$GIT_OBJECT_DIRECTORY" ]; then - case "$GIT_OBJECT_DIRECTORY/" in - "$repo_dir/"*) share_tree "$GIT_OBJECT_DIRECTORY" ;; - esac -fi - -for shared_file in HEAD packed-refs; do - if [ -f "$repo_dir/$shared_file" ]; then - find "$repo_dir/$shared_file" -user "$owner_uid" -exec chmod g+rw {} + - fi -done - -printf '%s\n' "post_receive_permissions_reconciled" >"$repo_dir/hooks/post-receive-share.last"