|
2 | 2 | import os |
3 | 3 | import io |
4 | 4 | from hashlib import md5 |
| 5 | +from contextlib import contextmanager |
5 | 6 |
|
6 | 7 | import unittest |
| 8 | +import unittest.mock |
7 | 9 | import tarfile |
8 | 10 |
|
9 | 11 | from test import support, script_helper |
@@ -2264,6 +2266,136 @@ def test_partial_input_bz2(self): |
2264 | 2266 | self._test_partial_input("r:bz2") |
2265 | 2267 |
|
2266 | 2268 |
|
| 2269 | +def root_is_uid_gid_0(): |
| 2270 | + try: |
| 2271 | + import pwd, grp |
| 2272 | + except ImportError: |
| 2273 | + return False |
| 2274 | + if pwd.getpwuid(0)[0] != 'root': |
| 2275 | + return False |
| 2276 | + if grp.getgrgid(0)[0] != 'root': |
| 2277 | + return False |
| 2278 | + return True |
| 2279 | + |
| 2280 | + |
| 2281 | +class NumericOwnerTest(unittest.TestCase): |
| 2282 | + # mock the following: |
| 2283 | + # os.chown: so we can test what's being called |
| 2284 | + # os.chmod: so the modes are not actually changed. if they are, we can't |
| 2285 | + # delete the files/directories |
| 2286 | + # os.geteuid: so we can lie and say we're root (uid = 0) |
| 2287 | + |
| 2288 | + @staticmethod |
| 2289 | + def _make_test_archive(filename_1, dirname_1, filename_2): |
| 2290 | + # the file contents to write |
| 2291 | + fobj = io.BytesIO(b"content") |
| 2292 | + |
| 2293 | + # create a tar file with a file, a directory, and a file within that |
| 2294 | + # directory. Assign various .uid/.gid values to them |
| 2295 | + items = [(filename_1, 99, 98, tarfile.REGTYPE, fobj), |
| 2296 | + (dirname_1, 77, 76, tarfile.DIRTYPE, None), |
| 2297 | + (filename_2, 88, 87, tarfile.REGTYPE, fobj), |
| 2298 | + ] |
| 2299 | + with tarfile.open(tmpname, 'w') as tarfl: |
| 2300 | + for name, uid, gid, typ, contents in items: |
| 2301 | + t = tarfile.TarInfo(name) |
| 2302 | + t.uid = uid |
| 2303 | + t.gid = gid |
| 2304 | + t.uname = 'root' |
| 2305 | + t.gname = 'root' |
| 2306 | + t.type = typ |
| 2307 | + tarfl.addfile(t, contents) |
| 2308 | + |
| 2309 | + # return the full pathname to the tar file |
| 2310 | + return tmpname |
| 2311 | + |
| 2312 | + @staticmethod |
| 2313 | + @contextmanager |
| 2314 | + def _setup_test(mock_geteuid): |
| 2315 | + mock_geteuid.return_value = 0 # lie and say we're root |
| 2316 | + fname = 'numeric-owner-testfile' |
| 2317 | + dirname = 'dir' |
| 2318 | + |
| 2319 | + # the names we want stored in the tarfile |
| 2320 | + filename_1 = fname |
| 2321 | + dirname_1 = dirname |
| 2322 | + filename_2 = os.path.join(dirname, fname) |
| 2323 | + |
| 2324 | + # create the tarfile with the contents we're after |
| 2325 | + tar_filename = NumericOwnerTest._make_test_archive(filename_1, |
| 2326 | + dirname_1, |
| 2327 | + filename_2) |
| 2328 | + |
| 2329 | + # open the tarfile for reading. yield it and the names of the items |
| 2330 | + # we stored into the file |
| 2331 | + with tarfile.open(tar_filename) as tarfl: |
| 2332 | + yield tarfl, filename_1, dirname_1, filename_2 |
| 2333 | + |
| 2334 | + @unittest.mock.patch('os.chown') |
| 2335 | + @unittest.mock.patch('os.chmod') |
| 2336 | + @unittest.mock.patch('os.geteuid') |
| 2337 | + def test_extract_with_numeric_owner(self, mock_geteuid, mock_chmod, |
| 2338 | + mock_chown): |
| 2339 | + with self._setup_test(mock_geteuid) as (tarfl, filename_1, _, |
| 2340 | + filename_2): |
| 2341 | + tarfl.extract(filename_1, TEMPDIR, numeric_owner=True) |
| 2342 | + tarfl.extract(filename_2 , TEMPDIR, numeric_owner=True) |
| 2343 | + |
| 2344 | + # convert to filesystem paths |
| 2345 | + f_filename_1 = os.path.join(TEMPDIR, filename_1) |
| 2346 | + f_filename_2 = os.path.join(TEMPDIR, filename_2) |
| 2347 | + |
| 2348 | + mock_chown.assert_has_calls([unittest.mock.call(f_filename_1, 99, 98), |
| 2349 | + unittest.mock.call(f_filename_2, 88, 87), |
| 2350 | + ], |
| 2351 | + any_order=True) |
| 2352 | + |
| 2353 | + @unittest.mock.patch('os.chown') |
| 2354 | + @unittest.mock.patch('os.chmod') |
| 2355 | + @unittest.mock.patch('os.geteuid') |
| 2356 | + def test_extractall_with_numeric_owner(self, mock_geteuid, mock_chmod, |
| 2357 | + mock_chown): |
| 2358 | + with self._setup_test(mock_geteuid) as (tarfl, filename_1, dirname_1, |
| 2359 | + filename_2): |
| 2360 | + tarfl.extractall(TEMPDIR, numeric_owner=True) |
| 2361 | + |
| 2362 | + # convert to filesystem paths |
| 2363 | + f_filename_1 = os.path.join(TEMPDIR, filename_1) |
| 2364 | + f_dirname_1 = os.path.join(TEMPDIR, dirname_1) |
| 2365 | + f_filename_2 = os.path.join(TEMPDIR, filename_2) |
| 2366 | + |
| 2367 | + mock_chown.assert_has_calls([unittest.mock.call(f_filename_1, 99, 98), |
| 2368 | + unittest.mock.call(f_dirname_1, 77, 76), |
| 2369 | + unittest.mock.call(f_filename_2, 88, 87), |
| 2370 | + ], |
| 2371 | + any_order=True) |
| 2372 | + |
| 2373 | + # this test requires that uid=0 and gid=0 really be named 'root'. that's |
| 2374 | + # because the uname and gname in the test file are 'root', and extract() |
| 2375 | + # will look them up using pwd and grp to find their uid and gid, which we |
| 2376 | + # test here to be 0. |
| 2377 | + @unittest.skipUnless(root_is_uid_gid_0(), |
| 2378 | + 'uid=0,gid=0 must be named "root"') |
| 2379 | + @unittest.mock.patch('os.chown') |
| 2380 | + @unittest.mock.patch('os.chmod') |
| 2381 | + @unittest.mock.patch('os.geteuid') |
| 2382 | + def test_extract_without_numeric_owner(self, mock_geteuid, mock_chmod, |
| 2383 | + mock_chown): |
| 2384 | + with self._setup_test(mock_geteuid) as (tarfl, filename_1, _, _): |
| 2385 | + tarfl.extract(filename_1, TEMPDIR, numeric_owner=False) |
| 2386 | + |
| 2387 | + # convert to filesystem paths |
| 2388 | + f_filename_1 = os.path.join(TEMPDIR, filename_1) |
| 2389 | + |
| 2390 | + mock_chown.assert_called_with(f_filename_1, 0, 0) |
| 2391 | + |
| 2392 | + @unittest.mock.patch('os.geteuid') |
| 2393 | + def test_keyword_only(self, mock_geteuid): |
| 2394 | + with self._setup_test(mock_geteuid) as (tarfl, filename_1, _, _): |
| 2395 | + self.assertRaises(TypeError, |
| 2396 | + tarfl.extract, filename_1, TEMPDIR, False, True) |
| 2397 | + |
| 2398 | + |
2267 | 2399 | def setUpModule(): |
2268 | 2400 | support.unlink(TEMPDIR) |
2269 | 2401 | os.makedirs(TEMPDIR) |
|
0 commit comments