summaryrefslogtreecommitdiff
path: root/app-admin/foundry/files/foundry-0.6.2-export-access.patch
blob: 200e4f15adc50c9fabebd365637d0f5be842d738 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
--- a/scripts/repository-candidate-gate
+++ b/scripts/repository-candidate-gate
@@ -388,7 +388,7 @@
 
 def main() -> int:
     parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
-    parser.add_argument("--config", required=True)
+    parser.add_argument("--config", help="repository gate policy (required for the main process)")
     parser.add_argument("--candidate", default=".", help="candidate tree (default: working directory)")
     parser.add_argument("--baseline", help="baseline tree (default: config baseline, the published tree)")
     parser.add_argument("--worker", nargs="+", help=argparse.SUPPRESS)
@@ -397,6 +397,9 @@
         kind, root, out, *atoms = args.worker
         worker_packages(root, out) if kind == "packages" else worker_resolve(root, atoms, out)
         return 0
+
+    if not args.config:
+        parser.error("--config is required for the main gate process")
 
     # Portage sources ebuilds as the portage user: the work tree must be traversable.
     os.umask(0o022)
--- a/src/foundry/repository_activation.py
+++ b/src/foundry/repository_activation.py
@@ -2,6 +2,7 @@
 from __future__ import annotations
 
 import configparser
+import errno
 import grp
 import json
 import os
@@ -69,12 +70,29 @@
     """Give the generated tree ordinary checkout modes before it is frozen."""
     if not data.get("activation"):
         return
+
+    def clear_authoring_acl(path: Path) -> None:
+        # An inherited ACL mask can look group-readable in st_mode while its
+        # owning-group entry denies access after publication chowns to Portage.
+        # Strip workspace ACLs only on this generated public copy, before freeze.
+        if not hasattr(os, "removexattr"):
+            return
+        for attribute in ("system.posix_acl_access", "system.posix_acl_default"):
+            try:
+                os.removexattr(path, attribute)
+            except OSError as error:
+                if error.errno not in {getattr(errno, "ENODATA", -1),
+                                      getattr(errno, "ENOATTR", -1), errno.ENOTSUP}:
+                    raise
+
     for directory, dirs, files in os.walk(tree, followlinks=False):
+        clear_authoring_acl(Path(directory))
         os.chmod(directory, 0o755)
         for item in files:
             path = Path(directory) / item
             mode = path.lstat().st_mode
             if not stat.S_ISLNK(mode):
+                clear_authoring_acl(path)
                 os.chmod(path, 0o755 if mode & stat.S_IXUSR else 0o644)
 
 
--- a/tests/test_publish.py
+++ b/tests/test_publish.py
@@ -56,7 +56,8 @@
 
 def _config(root: Path) -> FoundryConfig:
     return FoundryConfig(
-        path=root / "foundry.toml", state_root=root / "state", portage_config_root=root
+        path=root / "foundry.toml", state_root=root / "state", portage_config_root=root,
+        publish_root=root / "public" / "binhost",
     )
 
 
--- /dev/null
+++ b/tests/test_candidate_gate.py
@@ -0,0 +1,21 @@
+from importlib.machinery import SourceFileLoader
+from importlib.util import module_from_spec, spec_from_loader
+from pathlib import Path
+from unittest.mock import patch
+import unittest
+
+
+class CandidateGateCommandTests(unittest.TestCase):
+    def test_worker_protocol_does_not_require_main_process_policy(self):
+        script = Path(__file__).resolve().parents[1] / 'scripts/repository-candidate-gate'
+        loader = SourceFileLoader('candidate_gate_test', str(script))
+        module = module_from_spec(spec_from_loader(loader.name, loader))
+        loader.exec_module(module)
+        with patch.object(module.sys, 'argv', [str(script), '--worker', 'packages', '/isolated/root', '/isolated/out']), \
+                patch.object(module, 'worker_packages') as worker:
+            self.assertEqual(module.main(), 0)
+            worker.assert_called_once_with('/isolated/root', '/isolated/out')
+
+
+if __name__ == '__main__':
+    unittest.main()
--- a/tests/test_repository_activation.py
+++ b/tests/test_repository_activation.py
@@ -4,6 +4,8 @@
 import grp
 import json
 import os
+import shutil
+import subprocess
 import unittest
 from unittest.mock import patch
 
@@ -16,6 +18,29 @@
 from foundry.transition import read_toml, tree_digest
 from test_frontends import runtime_at
 from test_merge import _write_plan_ledger, _write_repos, _write_build_env
+
+
+class PublicExportAclTests(unittest.TestCase):
+    @unittest.skipUnless(shutil.which('setfacl') and hasattr(os, 'listxattr'), 'POSIX ACL support required')
+    def test_private_workspace_acls_do_not_override_public_group_modes(self):
+        from foundry.repository_activation import normalize_composed_access
+        with TemporaryDirectory() as temporary:
+            root = Path(temporary)
+            source = root / 'private-source'
+            source.mkdir()
+            recipe = source / 'example.ebuild'
+            recipe.write_text('EAPI=8\n')
+            subprocess.run(['setfacl', '-m', 'g::---,u:65534:r--,m::r--', str(recipe)], check=True)
+            subprocess.run(['setfacl', '-m', 'd:g::---', str(source)], check=True)
+            self.assertIn('system.posix_acl_access', os.listxattr(recipe))
+            export = root / 'generated'
+            shutil.copytree(source, export)
+            normalize_composed_access({'activation': {'read_group': 'portage'}}, export)
+            self.assertNotIn('system.posix_acl_access', os.listxattr(export / recipe.name))
+            self.assertNotIn('system.posix_acl_default', os.listxattr(export))
+            self.assertEqual((export / recipe.name).stat().st_mode & 0o777, 0o644)
+            self.assertEqual(export.stat().st_mode & 0o777, 0o755)
+            self.assertIn('system.posix_acl_access', os.listxattr(recipe))
 
 
 class RepositoryActivationTests(unittest.TestCase):