Merge pull request #1875 from letsencrypt/webroot-permissions

Webroot permissions
This commit is contained in:
Peter Eckersley
2015-12-12 00:32:16 -08:00
2 changed files with 59 additions and 29 deletions
+29 -16
View File
@@ -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
+24 -7
View File
@@ -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))