Authored-By: OpenAI (GPT-5) <noreply@openai.com>
This commit is contained in:
@@ -136,8 +136,9 @@ provisional cache policy or a pointer to a missing immutable target.
|
|||||||
|
|
||||||
Artifact input directories must be pairwise disjoint after filesystem
|
Artifact input directories must be pairwise disjoint after filesystem
|
||||||
resolution. Publication stops before build or upload if one contains another or
|
resolution. Publication stops before build or upload if one contains another or
|
||||||
escapes the repository, preventing protected input from entering a public
|
escapes the repository. Descendant symlinks are also rejected, preventing
|
||||||
artifact. Split storage endpoints are pinned to Garage, and each website
|
protected input from entering a public artifact through dereference. Split
|
||||||
|
storage endpoints are pinned to Garage, and each website
|
||||||
authority is derived from its bucket; a site cannot expose an arbitrary backend.
|
authority is derived from its bucket; a site cannot expose an arbitrary backend.
|
||||||
|
|
||||||
Each split route gets a bucket-specific `<bucket>.web.sjc001.fritzlab.net`
|
Each split route gets a bucket-specific `<bucket>.web.sjc001.fritzlab.net`
|
||||||
|
|||||||
@@ -427,6 +427,8 @@ def normalize_site_config(raw, site_name):
|
|||||||
|
|
||||||
def validate_artifact_inputs(site_dir, cfg):
|
def validate_artifact_inputs(site_dir, cfg):
|
||||||
"""Reject source containment before a public or protected build starts."""
|
"""Reject source containment before a public or protected build starts."""
|
||||||
|
if cfg["compatibility"]:
|
||||||
|
return
|
||||||
root = Path(site_dir).resolve()
|
root = Path(site_dir).resolve()
|
||||||
sources = []
|
sources = []
|
||||||
for artifact in cfg["artifacts"]:
|
for artifact in cfg["artifacts"]:
|
||||||
@@ -435,6 +437,13 @@ def validate_artifact_inputs(site_dir, cfg):
|
|||||||
raise ConfigError(
|
raise ConfigError(
|
||||||
f"artifact {artifact['name']} content_dir resolves outside the repository"
|
f"artifact {artifact['name']} content_dir resolves outside the repository"
|
||||||
)
|
)
|
||||||
|
if source.exists():
|
||||||
|
symlink = next((path for path in source.rglob("*") if path.is_symlink()), None)
|
||||||
|
if symlink is not None:
|
||||||
|
raise ConfigError(
|
||||||
|
f"artifact {artifact['name']} build input contains symlink: "
|
||||||
|
f"{symlink.relative_to(root)}"
|
||||||
|
)
|
||||||
sources.append((artifact["name"], source))
|
sources.append((artifact["name"], source))
|
||||||
for index, (name, source) in enumerate(sources):
|
for index, (name, source) in enumerate(sources):
|
||||||
for other_name, other_source in sources[index + 1:]:
|
for other_name, other_source in sources[index + 1:]:
|
||||||
|
|||||||
@@ -219,6 +219,17 @@ class ConfigContractTests(unittest.TestCase):
|
|||||||
with self.assertRaisesRegex(ConfigError, "build inputs overlap after resolution"):
|
with self.assertRaisesRegex(ConfigError, "build inputs overlap after resolution"):
|
||||||
validate_artifact_inputs(root, cfg)
|
validate_artifact_inputs(root, cfg)
|
||||||
|
|
||||||
|
def test_descendant_symlink_cannot_cross_artifact_boundary(self):
|
||||||
|
cfg = normalize_site_config(self.raw, "baseline.fritzlab.net")
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
root = Path(tmp)
|
||||||
|
(root / "dist").mkdir()
|
||||||
|
(root / "portal" / "build").mkdir(parents=True)
|
||||||
|
(root / "portal" / "build" / "private.txt").write_text("private")
|
||||||
|
(root / "dist" / "portal-link").symlink_to(root / "portal" / "build")
|
||||||
|
with self.assertRaisesRegex(ConfigError, "build input contains symlink"):
|
||||||
|
validate_artifact_inputs(root, cfg)
|
||||||
|
|
||||||
|
|
||||||
class GenerationTests(unittest.TestCase):
|
class GenerationTests(unittest.TestCase):
|
||||||
def render(self, raw):
|
def render(self, raw):
|
||||||
|
|||||||
Reference in New Issue
Block a user