ILIAS  trunk Revision v13.0_alpha-62-g3b72d83f2e5
OverlayDirectoryOwnershipTest.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
26use PHPUnit\Framework\TestCase;
27
33class OverlayDirectoryOwnershipTest extends TestCase
34{
36
37 private const string MODULE = 'stest';
38
39 private string $fixture_directory;
40 private string $client_data_dir;
43
44 protected function setUp(): void
45 {
46 // never the build of the installation, see MigratedPoFixture::resetRuntime()
49
50 if (!defined('ILIAS_ABSOLUTE_PATH')) {
51 define('ILIAS_ABSOLUTE_PATH', realpath(__DIR__ . '/../../../../../'));
52 }
53 if (!function_exists('posix_getpwuid') || !function_exists('posix_getgrgid')) {
54 $this->markTestSkipped('Requires the POSIX extension (posix_getpwuid()/posix_getgrgid()).');
55 }
56
57 $this->fixture_directory = __DIR__ . '/tmp-ownership-fixtures-' . bin2hex(random_bytes(4));
58 mkdir($this->fixture_directory, 0775, true);
59 $this->client_data_dir = sys_get_temp_dir() . '/ilias_mlfs_ownership_test_' . bin2hex(random_bytes(4));
60 mkdir($this->client_data_dir, 0775, true);
61
62 $this->directory = MigratedPoFixture::directory(
63 self::MODULE,
64 'components/ILIAS/Language/tests/ComponentTranslation/' . basename($this->fixture_directory) . '/'
65 );
66 $this->manager = new LanguageFileDirectoryManager(new CustomizingLanguageFileDirectory(), $this->directory);
68 $this->fixture_directory . '/' . self::MODULE . '_de.po',
69 MigratedPoFixture::catalog(self::MODULE, ['greeting' => 'Hallo'])
70 );
71 }
72
73 protected function tearDown(): void
74 {
75 if (is_dir($this->overlayDirectory())) {
76 chmod($this->overlayDirectory(), 0775);
77 }
78 MigratedPoFixture::removeDirectory($this->fixture_directory);
79 MigratedPoFixture::removeDirectory($this->client_data_dir);
80
82 }
83
84 private function overlayDirectory(): string
85 {
86 return dirname(MigratedPoFixture::overlayBase($this->client_data_dir, $this->directory, 'de'));
87 }
88
92 private function findUnwritable(?int $for_user_id): array
93 {
94 return MigratedLanguageFileSync::findUnwritableOverlayDirectories(
95 $this->manager,
96 ILIAS_ABSOLUTE_PATH,
97 ['de'],
98 $this->client_data_dir,
99 $for_user_id
100 );
101 }
102
107 private function seedOverlayDirectory(int $mode): void
108 {
109 $overlay_directory = $this->overlayDirectory();
110 if (!is_dir(dirname($overlay_directory))) {
111 mkdir(dirname($overlay_directory), 0775, true);
112 }
113 if (!is_dir($overlay_directory)) {
114 mkdir($overlay_directory, 0775);
115 }
116 chmod($overlay_directory, $mode); // avoid mkdir()'s own mode being masked by umask
117 clearstatcache(true, $overlay_directory);
118 }
119
120 private function assertReported(int $mode, int $for_user_id, string $message): void
121 {
122 $this->seedOverlayDirectory($mode);
123
124 $this->assertSame([$this->overlayDirectory()], $this->findUnwritable($for_user_id), $message);
125 }
126
127 private function assertNotReported(int $mode, int $for_user_id, string $message): void
128 {
129 $this->seedOverlayDirectory($mode);
130
131 $this->assertSame([], $this->findUnwritable($for_user_id), $message);
132 }
133
139 {
140 $this->assertNotReported(0000, 0, 'root (uid 0) must always be able to create/replace files');
141 }
142
149 {
150 if (posix_getuid() === 0) {
151 $this->markTestSkipped(
152 'The current process is root, so its own uid would only hit the for_user_id===0 short-circuit '
153 . '(see testRootMayAlwaysWriteRegardlessOfMode()) instead of the owner branch under test; '
154 . 'testAnArbitraryChownedOwnerNeedsTheOwnerWriteAndSearchBits() covers the owner branch as root.'
155 );
156 }
157 // the directory is created by this process, so its owner is the current user, as fileowner() would report
158 $owner = posix_getuid();
159
160 $this->assertReported(
161 0577, // owner: r-x (no write), group+other: rwx - irrelevant, the owner still can't write
162 $owner,
163 'the owner lacking the write bit must be reported even though group/other have it'
164 );
165 $this->assertNotReported(
166 0700, // owner: rwx, group+other: nothing
167 $owner,
168 'the owner having the write+search bits must be enough on its own'
169 );
170 }
171
177 {
178 if (!function_exists('posix_geteuid') || posix_geteuid() !== 0) {
179 $this->markTestSkipped('Requires root to chown() the overlay directory to an arbitrary owner.');
180 }
181 $foreign_owner = 33; // www-data, present on both the reference host and the ILIAS container
182 $this->seedOverlayDirectory(0555);
183 chown($this->overlayDirectory(), $foreign_owner);
184 clearstatcache(true, $this->overlayDirectory());
185
186 $this->assertSame(
187 [$this->overlayDirectory()],
188 $this->findUnwritable($foreign_owner),
189 'the chown()ed owner lacking the write bit must be reported'
190 );
191
192 chmod($this->overlayDirectory(), 0775);
193 clearstatcache(true, $this->overlayDirectory());
194
195 $this->assertSame(
196 [],
197 $this->findUnwritable($foreign_owner),
198 'the chown()ed owner having the write+search bits must not be reported'
199 );
200 }
201
208 {
209 $foreign_uid = 1; // "daemon", present on both the reference host and the ILIAS container
210
211 $this->assertReported(
212 0775, // owner rwx, group rwx, other r-x (no write) - a stranger still can't write
213 $foreign_uid,
214 'other lacking the write bit must be reported even though owner/group have it'
215 );
216 $this->assertNotReported(
217 0777, // other now has rwx as well
218 $foreign_uid,
219 'other having the write+search bits must be enough for a stranger'
220 );
221 }
222
228 {
229 if (!function_exists('posix_geteuid') || posix_geteuid() !== 0) {
230 $this->markTestSkipped('Requires root to chown()/chgrp() the overlay directory to an arbitrary owner/group.');
231 }
232 // www-data (33) does not exist everywhere (FreeBSD: www, 80)
233 $group_member_uid = 0;
234 $group_gid = 0;
235 foreach (array_merge([33], range(1, 1000)) as $uid) {
236 $user = posix_getpwuid($uid);
237 if ($user !== false && $user['gid'] !== 0) {
238 [$group_member_uid, $group_gid] = [$uid, $user['gid']];
239 break;
240 }
241 }
242 if ($group_member_uid === 0) {
243 $this->markTestSkipped('No unprivileged user with an own primary group found.');
244 }
245 $this->seedOverlayDirectory(0750); // owner: rwx, group: r-x (no write) - not enough
246 chown($this->overlayDirectory(), 0);
247 chgrp($this->overlayDirectory(), $group_gid);
248 clearstatcache(true, $this->overlayDirectory());
249
250 $this->assertSame(
251 [$this->overlayDirectory()],
252 $this->findUnwritable($group_member_uid),
253 'the group lacking the write bit must be reported even though the (different) owner has it'
254 );
255
256 chmod($this->overlayDirectory(), 0770); // group: rwx - now enough
257 clearstatcache(true, $this->overlayDirectory());
258
259 $this->assertSame(
260 [],
261 $this->findUnwritable($group_member_uid),
262 'the group having the write+search bits must be enough for a group member who is not the owner'
263 );
264 }
265}
trait RestoresProcessState
Whatever a test does to the process (locale, LANGUAGE/LC_*, LOCPATH, the static state of the runtime ...
The single place that knows where the files of a module migrated to PO/MO live:
Reads the shipped .po of a migrated module and maintains its per-installation overlay: the ....
static directory(string $prefix, string $path, bool $local=false)
static catalog(string $module, array $entries)
static writePo(string $file, TranslationCatalog $catalog)
static removeDirectory(string $directory)
static overlayBase(string $client_data_dir, LanguageFileDirectory $directory, string $lang_key)
The overlay of $directory's module for $lang_key without the .po extension.
static resetRuntime(?string $artifact_directory=null)
Forgets the static state of the runtime read path and points build and runtime at $artifact_directory...
MigratedLanguageFileSync::findUnwritableOverlayDirectories() with $for_user_id (private isWritableDir...
testAGroupMemberNeedsTheGroupWriteAndSearchBitsRegardlessOfOwnerOrOtherBits()
Group branch: a uid that is not the owner but shares the directory's group (chgrp()'d to $for_user_id...
testRootMayAlwaysWriteRegardlessOfMode()
Mutation: the $for_user_id === 0 short-circuit being removed - root must be considered able to write ...
testTheOwnerNeedsTheOwnerWriteAndSearchBitsRegardlessOfGroupOrOtherBits()
Mutation: the owner branch using the wrong mask (e.g.
seedOverlayDirectory(int $mode)
Only the leaf overlay directory gets $mode; parents stay 0775, since a restrictive intermediate mode ...
testAnArbitraryChownedOwnerNeedsTheOwnerWriteAndSearchBits()
As above, with a directory chown()ed to a non-current uid - proves $stat['uid'] is evaluated against ...
assertReported(int $mode, int $for_user_id, string $message)
assertNotReported(int $mode, int $for_user_id, string $message)
testAForeignUidNeedsTheOtherWriteAndSearchBitsEvenWhenOwnerAndGroupHaveThem()
Mutation: the "other" branch being skipped, or using the owner/group mask instead - a uid that is nei...
A directory of language files with a path and a prefix for them.