mirror of
https://github.com/certbot/certbot.git
synced 2026-08-03 03:41:53 +02:00
Merge pull request #1875 from letsencrypt/webroot-permissions
Webroot permissions
This commit is contained in:
@@ -2,7 +2,6 @@
|
|||||||
import errno
|
import errno
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import stat
|
|
||||||
|
|
||||||
import zope.interface
|
import zope.interface
|
||||||
|
|
||||||
@@ -59,24 +58,38 @@ to serve all files under specified web root ({0})."""
|
|||||||
|
|
||||||
logger.debug("Creating root challenges validation dir at %s",
|
logger.debug("Creating root challenges validation dir at %s",
|
||||||
self.full_roots[name])
|
self.full_roots[name])
|
||||||
|
|
||||||
|
# Change the permissions to be writable (GH #1389)
|
||||||
|
# Umask is used instead of chmod to ensure the client can also
|
||||||
|
# run as non-root (GH #1795)
|
||||||
|
old_umask = os.umask(0o022)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
os.makedirs(self.full_roots[name])
|
# This is coupled with the "umask" call above because
|
||||||
# Set permissions as parent directory (GH #1389)
|
# os.makedirs's "mode" parameter may not always work:
|
||||||
# We don't use the parameters in makedirs because it
|
|
||||||
# may not always work
|
|
||||||
# https://stackoverflow.com/questions/5231901/permission-problems-when-creating-a-dir-with-os-makedirs-python
|
# https://stackoverflow.com/questions/5231901/permission-problems-when-creating-a-dir-with-os-makedirs-python
|
||||||
|
os.makedirs(self.full_roots[name], 0o0755)
|
||||||
|
|
||||||
|
# Set owner as parent directory if possible
|
||||||
|
try:
|
||||||
stat_path = os.stat(path)
|
stat_path = os.stat(path)
|
||||||
filemode = stat.S_IMODE(stat_path.st_mode)
|
|
||||||
os.chmod(self.full_roots[name], filemode)
|
|
||||||
# Set owner and group, too
|
|
||||||
os.chown(self.full_roots[name], stat_path.st_uid,
|
os.chown(self.full_roots[name], stat_path.st_uid,
|
||||||
stat_path.st_gid)
|
stat_path.st_gid)
|
||||||
|
except OSError as exception:
|
||||||
|
if exception.errno == errno.EACCES:
|
||||||
|
logger.debug("Insufficient permissions to change owner and uid - ignoring")
|
||||||
|
else:
|
||||||
|
raise errors.PluginError(
|
||||||
|
"Couldn't create root for {0} http-01 "
|
||||||
|
"challenge responses: {1}", name, exception)
|
||||||
|
|
||||||
except OSError as exception:
|
except OSError as exception:
|
||||||
if exception.errno != errno.EEXIST:
|
if exception.errno != errno.EEXIST:
|
||||||
raise errors.PluginError(
|
raise errors.PluginError(
|
||||||
"Couldn't create root for {0} http-01 "
|
"Couldn't create root for {0} http-01 "
|
||||||
"challenge responses: {1}", name, exception)
|
"challenge responses: {1}", name, exception)
|
||||||
|
finally:
|
||||||
|
os.umask(old_umask)
|
||||||
|
|
||||||
def perform(self, achalls): # pylint: disable=missing-docstring
|
def perform(self, achalls): # pylint: disable=missing-docstring
|
||||||
assert self.full_roots, "Webroot plugin appears to be missing webroot map"
|
assert self.full_roots, "Webroot plugin appears to be missing webroot map"
|
||||||
@@ -95,18 +108,18 @@ to serve all files under specified web root ({0})."""
|
|||||||
|
|
||||||
def _perform_single(self, achall):
|
def _perform_single(self, achall):
|
||||||
response, validation = achall.response_and_validation()
|
response, validation = achall.response_and_validation()
|
||||||
|
|
||||||
path = self._path_for_achall(achall)
|
path = self._path_for_achall(achall)
|
||||||
logger.debug("Attempting to save validation to %s", path)
|
logger.debug("Attempting to save validation to %s", path)
|
||||||
|
|
||||||
|
# Change permissions to be world-readable, owner-writable (GH #1795)
|
||||||
|
old_umask = os.umask(0o022)
|
||||||
|
|
||||||
|
try:
|
||||||
with open(path, "w") as validation_file:
|
with open(path, "w") as validation_file:
|
||||||
validation_file.write(validation.encode())
|
validation_file.write(validation.encode())
|
||||||
|
finally:
|
||||||
# Set permissions as parent directory (GH #1389)
|
os.umask(old_umask)
|
||||||
parent_path = self.full_roots[achall.domain]
|
|
||||||
stat_parent_path = os.stat(parent_path)
|
|
||||||
filemode = stat.S_IMODE(stat_parent_path.st_mode)
|
|
||||||
# Remove execution bit (not needed for this file)
|
|
||||||
os.chmod(path, filemode & ~stat.S_IEXEC)
|
|
||||||
os.chown(path, stat_parent_path.st_uid, stat_parent_path.st_gid)
|
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
"""Tests for letsencrypt.plugins.webroot."""
|
"""Tests for letsencrypt.plugins.webroot."""
|
||||||
|
import errno
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
import stat
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
import stat
|
|
||||||
|
|
||||||
import mock
|
import mock
|
||||||
|
|
||||||
@@ -35,7 +36,6 @@ class AuthenticatorTest(unittest.TestCase):
|
|||||||
self.config = mock.MagicMock(webroot_path=self.path,
|
self.config = mock.MagicMock(webroot_path=self.path,
|
||||||
webroot_map={"thing.com": self.path})
|
webroot_map={"thing.com": self.path})
|
||||||
self.auth = Authenticator(self.config, "webroot")
|
self.auth = Authenticator(self.config, "webroot")
|
||||||
self.auth.prepare()
|
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
shutil.rmtree(self.path)
|
shutil.rmtree(self.path)
|
||||||
@@ -48,7 +48,7 @@ class AuthenticatorTest(unittest.TestCase):
|
|||||||
def test_add_parser_arguments(self):
|
def test_add_parser_arguments(self):
|
||||||
add = mock.MagicMock()
|
add = mock.MagicMock()
|
||||||
self.auth.add_parser_arguments(add)
|
self.auth.add_parser_arguments(add)
|
||||||
self.assertEqual(0, add.call_count) # became 0 when we moved the args to cli.py!
|
self.assertEqual(0, add.call_count) # args moved to cli.py!
|
||||||
|
|
||||||
def test_prepare_bad_root(self):
|
def test_prepare_bad_root(self):
|
||||||
self.config.webroot_path = os.path.join(self.path, "null")
|
self.config.webroot_path = os.path.join(self.path, "null")
|
||||||
@@ -70,17 +70,33 @@ class AuthenticatorTest(unittest.TestCase):
|
|||||||
self.assertRaises(errors.PluginError, self.auth.prepare)
|
self.assertRaises(errors.PluginError, self.auth.prepare)
|
||||||
os.chmod(self.path, 0o700)
|
os.chmod(self.path, 0o700)
|
||||||
|
|
||||||
|
@mock.patch("letsencrypt.plugins.webroot.os.chown")
|
||||||
|
def test_failed_chown_eacces(self, mock_chown):
|
||||||
|
mock_chown.side_effect = OSError(errno.EACCES, "msg")
|
||||||
|
self.auth.prepare() # exception caught and logged
|
||||||
|
|
||||||
|
@mock.patch("letsencrypt.plugins.webroot.os.chown")
|
||||||
|
def test_failed_chown_not_eacces(self, mock_chown):
|
||||||
|
mock_chown.side_effect = OSError()
|
||||||
|
self.assertRaises(errors.PluginError, self.auth.prepare)
|
||||||
|
|
||||||
def test_prepare_permissions(self):
|
def test_prepare_permissions(self):
|
||||||
|
self.auth.prepare()
|
||||||
|
|
||||||
# Remove exec bit from permission check, so that it
|
# Remove exec bit from permission check, so that it
|
||||||
# matches the file
|
# matches the file
|
||||||
self.auth.perform([self.achall])
|
self.auth.perform([self.achall])
|
||||||
parent_permissions = (stat.S_IMODE(os.stat(self.path).st_mode) &
|
path_permissions = stat.S_IMODE(os.stat(self.validation_path).st_mode)
|
||||||
~stat.S_IEXEC)
|
self.assertEqual(path_permissions, 0o644)
|
||||||
|
|
||||||
actual_permissions = stat.S_IMODE(os.stat(self.validation_path).st_mode)
|
# Check permissions of the directories
|
||||||
|
|
||||||
|
for dirpath, dirnames, _ in os.walk(self.path):
|
||||||
|
for directory in dirnames:
|
||||||
|
full_path = os.path.join(dirpath, directory)
|
||||||
|
dir_permissions = stat.S_IMODE(os.stat(full_path).st_mode)
|
||||||
|
self.assertEqual(dir_permissions, 0o755)
|
||||||
|
|
||||||
self.assertEqual(parent_permissions, actual_permissions)
|
|
||||||
parent_gid = os.stat(self.path).st_gid
|
parent_gid = os.stat(self.path).st_gid
|
||||||
parent_uid = os.stat(self.path).st_uid
|
parent_uid = os.stat(self.path).st_uid
|
||||||
|
|
||||||
@@ -88,6 +104,7 @@ class AuthenticatorTest(unittest.TestCase):
|
|||||||
self.assertEqual(os.stat(self.validation_path).st_uid, parent_uid)
|
self.assertEqual(os.stat(self.validation_path).st_uid, parent_uid)
|
||||||
|
|
||||||
def test_perform_cleanup(self):
|
def test_perform_cleanup(self):
|
||||||
|
self.auth.prepare()
|
||||||
responses = self.auth.perform([self.achall])
|
responses = self.auth.perform([self.achall])
|
||||||
self.assertEqual(1, len(responses))
|
self.assertEqual(1, len(responses))
|
||||||
self.assertTrue(os.path.exists(self.validation_path))
|
self.assertTrue(os.path.exists(self.validation_path))
|
||||||
|
|||||||
Reference in New Issue
Block a user