Seal catalog publication Git metadata
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -26,12 +27,65 @@ from govoplan_release.publisher import ( # noqa: E402
|
|||||||
verify_committed_publication,
|
verify_committed_publication,
|
||||||
verify_remote_branch_head,
|
verify_remote_branch_head,
|
||||||
verify_remote_publication,
|
verify_remote_publication,
|
||||||
|
_git_bytes,
|
||||||
_sanitized_git_environment,
|
_sanitized_git_environment,
|
||||||
|
_seal_git_metadata_file,
|
||||||
_trusted_npm_command,
|
_trusted_npm_command,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ReleaseCatalogPublicationTests(unittest.TestCase):
|
class ReleaseCatalogPublicationTests(unittest.TestCase):
|
||||||
|
def test_publication_git_writes_ignore_permissive_operator_umask(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
repository = Path(temp_dir) / "website"
|
||||||
|
repository.mkdir()
|
||||||
|
self._git(repository, "init", "--quiet")
|
||||||
|
payload_number = 0
|
||||||
|
while True:
|
||||||
|
payload = f"private publication object {payload_number}\n".encode()
|
||||||
|
header = f"blob {len(payload)}\0".encode()
|
||||||
|
expected_object = hashlib.sha1(
|
||||||
|
header + payload,
|
||||||
|
usedforsecurity=False,
|
||||||
|
).hexdigest()
|
||||||
|
object_parent = repository / ".git" / "objects" / expected_object[:2]
|
||||||
|
if not object_parent.exists():
|
||||||
|
break
|
||||||
|
payload_number += 1
|
||||||
|
|
||||||
|
previous_umask = os.umask(0o002)
|
||||||
|
try:
|
||||||
|
object_id = (
|
||||||
|
_git_bytes(
|
||||||
|
repository,
|
||||||
|
"hash-object",
|
||||||
|
"-w",
|
||||||
|
"--stdin",
|
||||||
|
input_bytes=payload,
|
||||||
|
)
|
||||||
|
.decode("ascii")
|
||||||
|
.strip()
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
os.umask(previous_umask)
|
||||||
|
|
||||||
|
object_path = object_parent / expected_object[2:]
|
||||||
|
self.assertEqual(expected_object, object_id)
|
||||||
|
self.assertEqual(os.geteuid(), object_parent.stat().st_uid)
|
||||||
|
self.assertEqual(0o700, object_parent.stat().st_mode & 0o777)
|
||||||
|
self.assertEqual(os.geteuid(), object_path.stat().st_uid)
|
||||||
|
self.assertEqual(0o400, object_path.stat().st_mode & 0o777)
|
||||||
|
|
||||||
|
def test_publication_git_metadata_is_sealed_after_git_writes(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
metadata = Path(temp_dir) / "index"
|
||||||
|
metadata.write_bytes(b"index")
|
||||||
|
metadata.chmod(0o664)
|
||||||
|
|
||||||
|
_seal_git_metadata_file(metadata, label="test index")
|
||||||
|
|
||||||
|
self.assertEqual(0o600, metadata.stat().st_mode & 0o777)
|
||||||
|
|
||||||
def test_publication_git_identity_is_fixed_and_non_personal(self) -> None:
|
def test_publication_git_identity_is_fixed_and_non_personal(self) -> None:
|
||||||
with patch.dict(
|
with patch.dict(
|
||||||
os.environ,
|
os.environ,
|
||||||
|
|||||||
@@ -614,6 +614,14 @@ def publish_catalog_candidate(
|
|||||||
effective_tag_name,
|
effective_tag_name,
|
||||||
publication_commit,
|
publication_commit,
|
||||||
)
|
)
|
||||||
|
_seal_git_metadata_file(
|
||||||
|
resolved_web_root
|
||||||
|
/ ".git"
|
||||||
|
/ "refs"
|
||||||
|
/ "tags"
|
||||||
|
/ effective_tag_name,
|
||||||
|
label="website publication tag reference",
|
||||||
|
)
|
||||||
publication_tag_object = git_text(
|
publication_tag_object = git_text(
|
||||||
resolved_web_root,
|
resolved_web_root,
|
||||||
"rev-parse",
|
"rev-parse",
|
||||||
@@ -1445,9 +1453,38 @@ def commit_publication_tree(
|
|||||||
frozen_head,
|
frozen_head,
|
||||||
)
|
)
|
||||||
_git_checked(web_root, "read-tree", "--reset", commit_sha)
|
_git_checked(web_root, "read-tree", "--reset", commit_sha)
|
||||||
|
_seal_git_metadata_file(
|
||||||
|
web_root / ".git" / "refs" / "heads" / branch,
|
||||||
|
label="website branch reference",
|
||||||
|
)
|
||||||
|
_seal_git_metadata_file(
|
||||||
|
web_root / ".git" / "index",
|
||||||
|
label="website Git index",
|
||||||
|
)
|
||||||
return commit_sha
|
return commit_sha
|
||||||
|
|
||||||
|
|
||||||
|
def _seal_git_metadata_file(path: Path, *, label: str) -> None:
|
||||||
|
try:
|
||||||
|
metadata = path.lstat()
|
||||||
|
if (
|
||||||
|
stat.S_ISLNK(metadata.st_mode)
|
||||||
|
or not stat.S_ISREG(metadata.st_mode)
|
||||||
|
or metadata.st_uid != os.geteuid()
|
||||||
|
):
|
||||||
|
raise RuntimeError(f"{label} is not an operator-owned regular file")
|
||||||
|
os.chmod(path, 0o600, follow_symlinks=False)
|
||||||
|
sealed = path.lstat()
|
||||||
|
except OSError as exc:
|
||||||
|
raise RuntimeError(f"{label} cannot be sealed") from exc
|
||||||
|
if (
|
||||||
|
not stat.S_ISREG(sealed.st_mode)
|
||||||
|
or sealed.st_uid != os.geteuid()
|
||||||
|
or stat.S_IMODE(sealed.st_mode) != 0o600
|
||||||
|
):
|
||||||
|
raise RuntimeError(f"{label} was not sealed to operator-only access")
|
||||||
|
|
||||||
|
|
||||||
def verify_committed_publication(
|
def verify_committed_publication(
|
||||||
*,
|
*,
|
||||||
web_root: Path,
|
web_root: Path,
|
||||||
@@ -1948,6 +1985,8 @@ def _git_bytes(
|
|||||||
"-C",
|
"-C",
|
||||||
str(path),
|
str(path),
|
||||||
"-c",
|
"-c",
|
||||||
|
"core.sharedRepository=0600",
|
||||||
|
"-c",
|
||||||
"core.fsmonitor=false",
|
"core.fsmonitor=false",
|
||||||
"-c",
|
"-c",
|
||||||
"core.hooksPath=/dev/null",
|
"core.hooksPath=/dev/null",
|
||||||
|
|||||||
Reference in New Issue
Block a user