From 59f4796918235e70f0c6429c3f8e2e979f4ba0b4 Mon Sep 17 00:00:00 2001 From: Ad Timmering Date: Fri, 19 Nov 2021 23:15:24 +0900 Subject: [PATCH] Fixed #4282 -- Made startapp/startproject management commands honor umask. Co-authored-by: Christian Schmitt --- django/core/management/templates.py | 8 ++++++- tests/admin_scripts/tests.py | 33 ++++++++++++++++++++++++++--- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/django/core/management/templates.py b/django/core/management/templates.py index 976442021e..6449342ae1 100644 --- a/django/core/management/templates.py +++ b/django/core/management/templates.py @@ -181,7 +181,7 @@ class TemplateCommand(BaseCommand): if self.verbosity >= 2: self.stdout.write('Creating %s' % new_path) try: - shutil.copymode(old_path, new_path) + self.apply_umask(old_path, new_path) self.make_writeable(new_path) except OSError: self.stderr.write( @@ -345,6 +345,12 @@ class TemplateCommand(BaseCommand): scheme = template.split(':', 1)[0].lower() return scheme in self.url_schemes + def apply_umask(self, old_path, new_path): + current_umask = os.umask(0) + os.umask(current_umask) + current_mode = stat.S_IMODE(os.stat(old_path).st_mode) + os.chmod(new_path, current_mode & ~current_umask) + def make_writeable(self, filename): """ Make sure that the file is writeable. diff --git a/tests/admin_scripts/tests.py b/tests/admin_scripts/tests.py index 2c3f365c74..0f4bbc57d7 100644 --- a/tests/admin_scripts/tests.py +++ b/tests/admin_scripts/tests.py @@ -7,6 +7,7 @@ import os import re import shutil import socket +import stat import subprocess import sys import tempfile @@ -32,6 +33,7 @@ from django.test import ( LiveServerTestCase, SimpleTestCase, TestCase, override_settings, ) from django.test.utils import captured_stderr, captured_stdout +from django.utils.version import PY39 custom_templates_dir = os.path.join(os.path.dirname(__file__), 'custom_templates') @@ -95,7 +97,7 @@ class AdminScriptTestCase(SimpleTestCase): paths.append(os.path.dirname(backend_dir)) return paths - def run_test(self, args, settings_file=None, apps=None): + def run_test(self, args, settings_file=None, apps=None, umask=None): base_dir = os.path.dirname(self.test_dir) # The base dir for Django's tests is one level up. tests_dir = os.path.dirname(os.path.dirname(__file__)) @@ -124,11 +126,13 @@ class AdminScriptTestCase(SimpleTestCase): cwd=self.test_dir, env=test_environ, text=True, + # subprocess.run()'s umask was added in Python 3.9. + **({'umask': umask} if umask and PY39 else {}), ) return p.stdout, p.stderr - def run_django_admin(self, args, settings_file=None): - return self.run_test(['-m', 'django', *args], settings_file) + def run_django_admin(self, args, settings_file=None, umask=None): + return self.run_test(['-m', 'django', *args], settings_file, umask=umask) def run_manage(self, args, settings_file=None, manage_py=None): template_manage_py = ( @@ -2297,6 +2301,29 @@ class StartProject(LiveServerTestCase, AdminScriptTestCase): not_excluded = os.path.join(testproject_dir, project_name) self.assertIs(os.path.exists(not_excluded), True) + @unittest.skipIf( + sys.platform == 'win32', + 'Windows only partially supports umasks and chmod.', + ) + @unittest.skipUnless(PY39, "subprocess.run()'s umask was added in Python 3.9.") + def test_honor_umask(self): + _, err = self.run_django_admin(['startproject', 'testproject'], umask=0o077) + self.assertNoOutput(err) + testproject_dir = os.path.join(self.test_dir, 'testproject') + self.assertIs(os.path.isdir(testproject_dir), True) + tests = [ + (['manage.py'], 0o700), + (['testproject'], 0o700), + (['testproject', 'settings.py'], 0o600), + ] + for paths, expected_mode in tests: + file_path = os.path.join(testproject_dir, *paths) + with self.subTest(paths[-1]): + self.assertEqual( + stat.S_IMODE(os.stat(file_path).st_mode), + expected_mode, + ) + class StartApp(AdminScriptTestCase):