mirror of
https://github.com/certbot/certbot.git
synced 2025-08-08 04:02:10 +03:00
[Windows|Linux] Forbid os.stat and os.fstat (#7325)
Fixes #7212 This PR forbid os.stat and os.fstat, and fix or provide alternatives to avoid its usage in certbot outside of certbot.compat.filesystem. * Reimplement private key mode propagation * Remove other os.stat * Remove last call of os.stat in certbot package * Forbid stat and fstat * Implement mode comparison checks * Add unit tests * Update certbot/compat/filesystem.py Co-Authored-By: Brad Warren <bmw@users.noreply.github.com> * Update certbot/compat/filesystem.py Co-Authored-By: Brad Warren <bmw@users.noreply.github.com> * Handle case where multiple ace concerns a given SID in has_min_permissions * Add a new test scenario * Add a simple test for has_same_ownership * Fix name function * Add a comment explaining an ACE structure * Move a test in its dedicated class * Improve a message error * Calculate has_min_permission result using effective permission rights to be more generic. * Change an exception message * Add comments, avoid to skip a test. * Update certbot/compat/filesystem.py Co-Authored-By: Brad Warren <bmw@users.noreply.github.com>
This commit is contained in:
committed by
Brad Warren
parent
ada2f5c767
commit
ab76834100
@@ -6,13 +6,13 @@ coverage:
|
|||||||
flags: linux
|
flags: linux
|
||||||
# Fixed target instead of auto set by #7173, can
|
# Fixed target instead of auto set by #7173, can
|
||||||
# be removed when flags in Codecov are added back.
|
# be removed when flags in Codecov are added back.
|
||||||
target: 97.5
|
target: 97.4
|
||||||
threshold: 0.1
|
threshold: 0.1
|
||||||
base: auto
|
base: auto
|
||||||
windows:
|
windows:
|
||||||
flags: windows
|
flags: windows
|
||||||
# Fixed target instead of auto set by #7173, can
|
# Fixed target instead of auto set by #7173, can
|
||||||
# be removed when flags in Codecov are added back.
|
# be removed when flags in Codecov are added back.
|
||||||
target: 97.6
|
target: 97.7
|
||||||
threshold: 0.1
|
threshold: 0.1
|
||||||
base: auto
|
base: auto
|
||||||
|
@@ -7,6 +7,7 @@ from acme.magic_typing import List # pylint: disable=unused-import, no-name-in-
|
|||||||
|
|
||||||
from certbot import achallenges
|
from certbot import achallenges
|
||||||
from certbot import errors
|
from certbot import errors
|
||||||
|
from certbot.compat import filesystem
|
||||||
from certbot.compat import os
|
from certbot.compat import os
|
||||||
from certbot.tests import acme_util
|
from certbot.tests import acme_util
|
||||||
|
|
||||||
@@ -180,7 +181,7 @@ class ApacheHttp01Test(util.ApacheTest):
|
|||||||
self.assertEqual(self.http.perform(), expected_response)
|
self.assertEqual(self.http.perform(), expected_response)
|
||||||
|
|
||||||
self.assertTrue(os.path.isdir(self.http.challenge_dir))
|
self.assertTrue(os.path.isdir(self.http.challenge_dir))
|
||||||
self._has_min_permissions(self.http.challenge_dir, 0o755)
|
self.assertTrue(filesystem.has_min_permissions(self.http.challenge_dir, 0o755))
|
||||||
self._test_challenge_conf()
|
self._test_challenge_conf()
|
||||||
|
|
||||||
for achall in achalls:
|
for achall in achalls:
|
||||||
@@ -218,15 +219,10 @@ class ApacheHttp01Test(util.ApacheTest):
|
|||||||
name = os.path.join(self.http.challenge_dir, achall.chall.encode("token"))
|
name = os.path.join(self.http.challenge_dir, achall.chall.encode("token"))
|
||||||
validation = achall.validation(self.account_key)
|
validation = achall.validation(self.account_key)
|
||||||
|
|
||||||
self._has_min_permissions(name, 0o644)
|
self.assertTrue(filesystem.has_min_permissions(name, 0o644))
|
||||||
with open(name, 'rb') as f:
|
with open(name, 'rb') as f:
|
||||||
self.assertEqual(f.read(), validation.encode())
|
self.assertEqual(f.read(), validation.encode())
|
||||||
|
|
||||||
def _has_min_permissions(self, path, min_mode):
|
|
||||||
"""Tests the given file has at least the permissions in mode."""
|
|
||||||
st_mode = os.stat(path).st_mode
|
|
||||||
self.assertEqual(st_mode, st_mode | min_mode)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main() # pragma: no cover
|
unittest.main() # pragma: no cover
|
||||||
|
@@ -1,3 +1,3 @@
|
|||||||
# Remember to update setup.py to match the package versions below.
|
# Remember to update setup.py to match the package versions below.
|
||||||
acme[dev]==0.29.0
|
acme[dev]==0.29.0
|
||||||
certbot[dev]==0.37.0
|
-e .[dev]
|
||||||
|
@@ -10,7 +10,7 @@ version = '0.39.0.dev0'
|
|||||||
# acme/certbot version.
|
# acme/certbot version.
|
||||||
install_requires = [
|
install_requires = [
|
||||||
'acme>=0.29.0',
|
'acme>=0.29.0',
|
||||||
'certbot>=0.37.0',
|
'certbot>=0.39.0.dev0',
|
||||||
'mock',
|
'mock',
|
||||||
'python-augeas',
|
'python-augeas',
|
||||||
'setuptools',
|
'setuptools',
|
||||||
|
@@ -1,3 +1,3 @@
|
|||||||
# Remember to update setup.py to match the package versions below.
|
# Remember to update setup.py to match the package versions below.
|
||||||
acme[dev]==0.29.0
|
acme[dev]==0.29.0
|
||||||
certbot[dev]==0.34.0
|
-e .[dev]
|
||||||
|
@@ -8,7 +8,7 @@ version = '0.39.0.dev0'
|
|||||||
# acme/certbot version.
|
# acme/certbot version.
|
||||||
install_requires = [
|
install_requires = [
|
||||||
'acme>=0.29.0',
|
'acme>=0.29.0',
|
||||||
'certbot>=0.34.0',
|
'certbot>=0.39.0.dev0',
|
||||||
'cloudflare>=1.5.1',
|
'cloudflare>=1.5.1',
|
||||||
'mock',
|
'mock',
|
||||||
'setuptools',
|
'setuptools',
|
||||||
|
@@ -1,3 +1,3 @@
|
|||||||
# Remember to update setup.py to match the package versions below.
|
# Remember to update setup.py to match the package versions below.
|
||||||
acme[dev]==0.31.0
|
acme[dev]==0.31.0
|
||||||
certbot[dev]==0.34.0
|
-e .[dev]
|
||||||
|
@@ -8,7 +8,7 @@ version = '0.39.0.dev0'
|
|||||||
# acme/certbot version.
|
# acme/certbot version.
|
||||||
install_requires = [
|
install_requires = [
|
||||||
'acme>=0.31.0',
|
'acme>=0.31.0',
|
||||||
'certbot>=0.34.0',
|
'certbot>=0.39.0.dev0',
|
||||||
'dns-lexicon>=2.2.1', # Support for >1 TXT record per name
|
'dns-lexicon>=2.2.1', # Support for >1 TXT record per name
|
||||||
'mock',
|
'mock',
|
||||||
'setuptools',
|
'setuptools',
|
||||||
|
@@ -1,3 +1,3 @@
|
|||||||
# Remember to update setup.py to match the package versions below.
|
# Remember to update setup.py to match the package versions below.
|
||||||
acme[dev]==0.29.0
|
acme[dev]==0.29.0
|
||||||
certbot[dev]==0.34.0
|
-e .[dev]
|
||||||
|
@@ -8,7 +8,7 @@ version = '0.39.0.dev0'
|
|||||||
# acme/certbot version.
|
# acme/certbot version.
|
||||||
install_requires = [
|
install_requires = [
|
||||||
'acme>=0.29.0',
|
'acme>=0.29.0',
|
||||||
'certbot>=0.34.0',
|
'certbot>=0.39.0.dev0',
|
||||||
'mock',
|
'mock',
|
||||||
'python-digitalocean>=1.11',
|
'python-digitalocean>=1.11',
|
||||||
'setuptools',
|
'setuptools',
|
||||||
|
@@ -1,3 +1,3 @@
|
|||||||
# Remember to update setup.py to match the package versions below.
|
# Remember to update setup.py to match the package versions below.
|
||||||
acme[dev]==0.31.0
|
acme[dev]==0.31.0
|
||||||
certbot[dev]==0.34.0
|
-e .[dev]
|
||||||
|
@@ -9,7 +9,7 @@ version = '0.39.0.dev0'
|
|||||||
# acme/certbot version.
|
# acme/certbot version.
|
||||||
install_requires = [
|
install_requires = [
|
||||||
'acme>=0.31.0',
|
'acme>=0.31.0',
|
||||||
'certbot>=0.34.0',
|
'certbot>=0.39.0.dev0',
|
||||||
'mock',
|
'mock',
|
||||||
'setuptools',
|
'setuptools',
|
||||||
'zope.interface',
|
'zope.interface',
|
||||||
|
@@ -1,3 +1,3 @@
|
|||||||
# Remember to update setup.py to match the package versions below.
|
# Remember to update setup.py to match the package versions below.
|
||||||
acme[dev]==0.31.0
|
acme[dev]==0.31.0
|
||||||
certbot[dev]==0.34.0
|
-e .[dev]
|
||||||
|
@@ -8,7 +8,7 @@ version = '0.39.0.dev0'
|
|||||||
# acme/certbot version.
|
# acme/certbot version.
|
||||||
install_requires = [
|
install_requires = [
|
||||||
'acme>=0.31.0',
|
'acme>=0.31.0',
|
||||||
'certbot>=0.34.0',
|
'certbot>=0.39.0.dev0',
|
||||||
'dns-lexicon>=2.2.1', # Support for >1 TXT record per name
|
'dns-lexicon>=2.2.1', # Support for >1 TXT record per name
|
||||||
'mock',
|
'mock',
|
||||||
'setuptools',
|
'setuptools',
|
||||||
|
@@ -1,3 +1,3 @@
|
|||||||
# Remember to update setup.py to match the package versions below.
|
# Remember to update setup.py to match the package versions below.
|
||||||
acme[dev]==0.31.0
|
acme[dev]==0.31.0
|
||||||
certbot[dev]==0.34.0
|
-e .[dev]
|
||||||
|
@@ -7,7 +7,7 @@ version = '0.39.0.dev0'
|
|||||||
# Please update tox.ini when modifying dependency version requirements
|
# Please update tox.ini when modifying dependency version requirements
|
||||||
install_requires = [
|
install_requires = [
|
||||||
'acme>=0.31.0',
|
'acme>=0.31.0',
|
||||||
'certbot>=0.34.0',
|
'certbot>=0.39.0.dev0',
|
||||||
'dns-lexicon>=2.1.22',
|
'dns-lexicon>=2.1.22',
|
||||||
'mock',
|
'mock',
|
||||||
'setuptools',
|
'setuptools',
|
||||||
|
@@ -1,3 +1,3 @@
|
|||||||
# Remember to update setup.py to match the package versions below.
|
# Remember to update setup.py to match the package versions below.
|
||||||
acme[dev]==0.29.0
|
acme[dev]==0.29.0
|
||||||
certbot[dev]==0.34.0
|
-e .[dev]
|
||||||
|
@@ -8,7 +8,7 @@ version = '0.39.0.dev0'
|
|||||||
# acme/certbot version.
|
# acme/certbot version.
|
||||||
install_requires = [
|
install_requires = [
|
||||||
'acme>=0.29.0',
|
'acme>=0.29.0',
|
||||||
'certbot>=0.34.0',
|
'certbot>=0.39.0.dev0',
|
||||||
# 1.5 is the first version that supports oauth2client>=2.0
|
# 1.5 is the first version that supports oauth2client>=2.0
|
||||||
'google-api-python-client>=1.5',
|
'google-api-python-client>=1.5',
|
||||||
'mock',
|
'mock',
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
# Remember to update setup.py to match the package versions below.
|
# Remember to update setup.py to match the package versions below.
|
||||||
acme[dev]==0.31.0
|
acme[dev]==0.31.0
|
||||||
certbot[dev]==0.34.0
|
-e .[dev]
|
||||||
dns-lexicon==2.2.3
|
dns-lexicon==2.2.3
|
||||||
|
@@ -6,7 +6,7 @@ version = '0.39.0.dev0'
|
|||||||
# Please update tox.ini when modifying dependency version requirements
|
# Please update tox.ini when modifying dependency version requirements
|
||||||
install_requires = [
|
install_requires = [
|
||||||
'acme>=0.31.0',
|
'acme>=0.31.0',
|
||||||
'certbot>=0.34.0',
|
'certbot>=0.39.0.dev0',
|
||||||
'dns-lexicon>=2.2.3',
|
'dns-lexicon>=2.2.3',
|
||||||
'mock',
|
'mock',
|
||||||
'setuptools',
|
'setuptools',
|
||||||
|
@@ -1,3 +1,3 @@
|
|||||||
# Remember to update setup.py to match the package versions below.
|
# Remember to update setup.py to match the package versions below.
|
||||||
acme[dev]==0.31.0
|
acme[dev]==0.31.0
|
||||||
certbot[dev]==0.34.0
|
-e .[dev]
|
||||||
|
@@ -8,7 +8,7 @@ version = '0.39.0.dev0'
|
|||||||
# acme/certbot version.
|
# acme/certbot version.
|
||||||
install_requires = [
|
install_requires = [
|
||||||
'acme>=0.31.0',
|
'acme>=0.31.0',
|
||||||
'certbot>=0.34.0',
|
'certbot>=0.39.0.dev0',
|
||||||
'dns-lexicon>=2.2.1', # Support for >1 TXT record per name
|
'dns-lexicon>=2.2.1', # Support for >1 TXT record per name
|
||||||
'mock',
|
'mock',
|
||||||
'setuptools',
|
'setuptools',
|
||||||
|
@@ -1,3 +1,3 @@
|
|||||||
# Remember to update setup.py to match the package versions below.
|
# Remember to update setup.py to match the package versions below.
|
||||||
acme[dev]==0.31.0
|
acme[dev]==0.31.0
|
||||||
certbot[dev]==0.34.0
|
-e .[dev]
|
||||||
|
@@ -8,7 +8,7 @@ version = '0.39.0.dev0'
|
|||||||
# acme/certbot version.
|
# acme/certbot version.
|
||||||
install_requires = [
|
install_requires = [
|
||||||
'acme>=0.31.0',
|
'acme>=0.31.0',
|
||||||
'certbot>=0.34.0',
|
'certbot>=0.39.0.dev0',
|
||||||
'dns-lexicon>=2.2.1', # Support for >1 TXT record per name
|
'dns-lexicon>=2.2.1', # Support for >1 TXT record per name
|
||||||
'mock',
|
'mock',
|
||||||
'setuptools',
|
'setuptools',
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
# Remember to update setup.py to match the package versions below.
|
# Remember to update setup.py to match the package versions below.
|
||||||
acme[dev]==0.31.0
|
acme[dev]==0.31.0
|
||||||
certbot[dev]==0.34.0
|
-e .[dev]
|
||||||
dns-lexicon==2.7.14
|
dns-lexicon==2.7.14
|
||||||
|
@@ -8,7 +8,7 @@ version = '0.39.0.dev0'
|
|||||||
# acme/certbot version.
|
# acme/certbot version.
|
||||||
install_requires = [
|
install_requires = [
|
||||||
'acme>=0.31.0',
|
'acme>=0.31.0',
|
||||||
'certbot>=0.34.0',
|
'certbot>=0.39.0.dev0',
|
||||||
'dns-lexicon>=2.7.14', # Correct proxy use on OVH provider
|
'dns-lexicon>=2.7.14', # Correct proxy use on OVH provider
|
||||||
'mock',
|
'mock',
|
||||||
'setuptools',
|
'setuptools',
|
||||||
|
@@ -1,3 +1,3 @@
|
|||||||
# Remember to update setup.py to match the package versions below.
|
# Remember to update setup.py to match the package versions below.
|
||||||
acme[dev]==0.29.0
|
acme[dev]==0.29.0
|
||||||
certbot[dev]==0.34.0
|
-e .[dev]
|
||||||
|
@@ -8,7 +8,7 @@ version = '0.39.0.dev0'
|
|||||||
# acme/certbot version.
|
# acme/certbot version.
|
||||||
install_requires = [
|
install_requires = [
|
||||||
'acme>=0.29.0',
|
'acme>=0.29.0',
|
||||||
'certbot>=0.34.0',
|
'certbot>=0.39.0.dev0',
|
||||||
'dnspython',
|
'dnspython',
|
||||||
'mock',
|
'mock',
|
||||||
'setuptools',
|
'setuptools',
|
||||||
|
@@ -1,3 +1,3 @@
|
|||||||
# Remember to update setup.py to match the package versions below.
|
# Remember to update setup.py to match the package versions below.
|
||||||
acme[dev]==0.29.0
|
acme[dev]==0.29.0
|
||||||
certbot[dev]==0.34.0
|
-e .[dev]
|
||||||
|
@@ -7,7 +7,7 @@ version = '0.39.0.dev0'
|
|||||||
# acme/certbot version.
|
# acme/certbot version.
|
||||||
install_requires = [
|
install_requires = [
|
||||||
'acme>=0.29.0',
|
'acme>=0.29.0',
|
||||||
'certbot>=0.34.0',
|
'certbot>=0.39.0.dev0',
|
||||||
'boto3',
|
'boto3',
|
||||||
'mock',
|
'mock',
|
||||||
'setuptools',
|
'setuptools',
|
||||||
|
@@ -1,3 +1,3 @@
|
|||||||
# Remember to update setup.py to match the package versions below.
|
# Remember to update setup.py to match the package versions below.
|
||||||
acme[dev]==0.31.0
|
acme[dev]==0.31.0
|
||||||
certbot[dev]==0.34.0
|
-e .[dev]
|
||||||
|
@@ -7,7 +7,7 @@ version = '0.39.0.dev0'
|
|||||||
# Please update tox.ini when modifying dependency version requirements
|
# Please update tox.ini when modifying dependency version requirements
|
||||||
install_requires = [
|
install_requires = [
|
||||||
'acme>=0.31.0',
|
'acme>=0.31.0',
|
||||||
'certbot>=0.34.0',
|
'certbot>=0.39.0.dev0',
|
||||||
'dns-lexicon>=2.1.23',
|
'dns-lexicon>=2.1.23',
|
||||||
'mock',
|
'mock',
|
||||||
'setuptools',
|
'setuptools',
|
||||||
|
@@ -20,7 +20,7 @@ except ImportError:
|
|||||||
else:
|
else:
|
||||||
POSIX_MODE = False
|
POSIX_MODE = False
|
||||||
|
|
||||||
from acme.magic_typing import List # pylint: disable=unused-import, no-name-in-module
|
from acme.magic_typing import List, Union, Tuple # pylint: disable=unused-import, no-name-in-module
|
||||||
|
|
||||||
|
|
||||||
def chmod(file_path, mode):
|
def chmod(file_path, mode):
|
||||||
@@ -264,6 +264,7 @@ def replace(src, dst):
|
|||||||
|
|
||||||
|
|
||||||
def realpath(file_path):
|
def realpath(file_path):
|
||||||
|
# type: (str) -> str
|
||||||
"""
|
"""
|
||||||
Find the real path for the given path. This method resolves symlinks, including
|
Find the real path for the given path. This method resolves symlinks, including
|
||||||
recursive symlinks, and is protected against symlinks that creates an infinite loop.
|
recursive symlinks, and is protected against symlinks that creates an infinite loop.
|
||||||
@@ -300,10 +301,11 @@ def realpath(file_path):
|
|||||||
# requires to be run under a privileged shell, so the user will always benefit
|
# requires to be run under a privileged shell, so the user will always benefit
|
||||||
# from the highest (privileged one) set of permissions on a given file.
|
# from the highest (privileged one) set of permissions on a given file.
|
||||||
def is_executable(path):
|
def is_executable(path):
|
||||||
|
# type: (str) -> bool
|
||||||
"""
|
"""
|
||||||
Is path an executable file?
|
Is path an executable file?
|
||||||
:param str path: path to test
|
:param str path: path to test
|
||||||
:returns: True if path is an executable file
|
:return: True if path is an executable file
|
||||||
:rtype: bool
|
:rtype: bool
|
||||||
"""
|
"""
|
||||||
if POSIX_MODE:
|
if POSIX_MODE:
|
||||||
@@ -312,6 +314,118 @@ def is_executable(path):
|
|||||||
return _win_is_executable(path)
|
return _win_is_executable(path)
|
||||||
|
|
||||||
|
|
||||||
|
def has_world_permissions(path):
|
||||||
|
# type: (str) -> bool
|
||||||
|
"""
|
||||||
|
Check if everybody/world has any right (read/write/execute) on a file given its path
|
||||||
|
:param str path: path to test
|
||||||
|
:return: True if everybody/world has any right to the file
|
||||||
|
:rtype: bool
|
||||||
|
"""
|
||||||
|
if POSIX_MODE:
|
||||||
|
return bool(stat.S_IMODE(os.stat(path).st_mode) & stat.S_IRWXO)
|
||||||
|
|
||||||
|
security = win32security.GetFileSecurity(path, win32security.DACL_SECURITY_INFORMATION)
|
||||||
|
dacl = security.GetSecurityDescriptorDacl()
|
||||||
|
|
||||||
|
return bool(dacl.GetEffectiveRightsFromAcl({
|
||||||
|
'TrusteeForm': win32security.TRUSTEE_IS_SID,
|
||||||
|
'TrusteeType': win32security.TRUSTEE_IS_USER,
|
||||||
|
'Identifier': win32security.ConvertStringSidToSid('S-1-1-0'),
|
||||||
|
}))
|
||||||
|
|
||||||
|
|
||||||
|
def compute_private_key_mode(old_key, base_mode):
|
||||||
|
# type: (str, int) -> int
|
||||||
|
"""
|
||||||
|
Calculate the POSIX mode to apply to a private key given the previous private key
|
||||||
|
:param str old_key: path to the previous private key
|
||||||
|
:param int base_mode: the minimum modes to apply to a private key
|
||||||
|
:return: the POSIX mode to apply
|
||||||
|
:rtype: int
|
||||||
|
"""
|
||||||
|
if POSIX_MODE:
|
||||||
|
# On Linux, we keep read/write/execute permissions
|
||||||
|
# for group and read permissions for everybody.
|
||||||
|
old_mode = (stat.S_IMODE(os.stat(old_key).st_mode) &
|
||||||
|
(stat.S_IRGRP | stat.S_IWGRP | stat.S_IXGRP | stat.S_IROTH))
|
||||||
|
return base_mode | old_mode
|
||||||
|
|
||||||
|
# On Windows, the mode returned by os.stat is not reliable,
|
||||||
|
# so we do not keep any permission from the previous private key.
|
||||||
|
return base_mode
|
||||||
|
|
||||||
|
|
||||||
|
def has_same_ownership(path1, path2):
|
||||||
|
# type: (str, str) -> bool
|
||||||
|
"""
|
||||||
|
Return True if the ownership of two files given their respective path is the same.
|
||||||
|
On Windows, ownership is checked against owner only, since files do not have a group owner.
|
||||||
|
:param str path1: path to the first file
|
||||||
|
:param str path2: path to the second file
|
||||||
|
:return: True if both files have the same ownership, False otherwise
|
||||||
|
:rtype: bool
|
||||||
|
|
||||||
|
"""
|
||||||
|
if POSIX_MODE:
|
||||||
|
stats1 = os.stat(path1)
|
||||||
|
stats2 = os.stat(path2)
|
||||||
|
return (stats1.st_uid, stats1.st_gid) == (stats2.st_uid, stats2.st_gid)
|
||||||
|
|
||||||
|
security1 = win32security.GetFileSecurity(path1, win32security.OWNER_SECURITY_INFORMATION)
|
||||||
|
user1 = security1.GetSecurityDescriptorOwner()
|
||||||
|
|
||||||
|
security2 = win32security.GetFileSecurity(path2, win32security.OWNER_SECURITY_INFORMATION)
|
||||||
|
user2 = security2.GetSecurityDescriptorOwner()
|
||||||
|
|
||||||
|
return user1 == user2
|
||||||
|
|
||||||
|
|
||||||
|
def has_min_permissions(path, min_mode):
|
||||||
|
# type: (str, int) -> bool
|
||||||
|
"""
|
||||||
|
Check if a file given its path has at least the permissions defined by the given minimal mode.
|
||||||
|
On Windows, group permissions are ignored since files do not have a group owner.
|
||||||
|
:param str path: path to the file to check
|
||||||
|
:param int min_mode: the minimal permissions expected
|
||||||
|
:return: True if the file matches the minimal permissions expectations, False otherwise
|
||||||
|
:rtype: bool
|
||||||
|
"""
|
||||||
|
if POSIX_MODE:
|
||||||
|
st_mode = os.stat(path).st_mode
|
||||||
|
return st_mode == st_mode | min_mode
|
||||||
|
|
||||||
|
# Resolve symlinks, to get a consistent result with os.stat on Linux,
|
||||||
|
# that follows symlinks by default.
|
||||||
|
path = realpath(path)
|
||||||
|
|
||||||
|
# Get owner sid of the file
|
||||||
|
security = win32security.GetFileSecurity(
|
||||||
|
path, win32security.OWNER_SECURITY_INFORMATION | win32security.DACL_SECURITY_INFORMATION)
|
||||||
|
user = security.GetSecurityDescriptorOwner()
|
||||||
|
dacl = security.GetSecurityDescriptorDacl()
|
||||||
|
min_dacl = _generate_dacl(user, min_mode)
|
||||||
|
|
||||||
|
for index in range(min_dacl.GetAceCount()):
|
||||||
|
min_ace = min_dacl.GetAce(index)
|
||||||
|
|
||||||
|
# On a given ACE, index 0 is the ACE type, 1 is the permission mask, and 2 is the SID.
|
||||||
|
# See: http://timgolden.me.uk/pywin32-docs/PyACL__GetAce_meth.html
|
||||||
|
mask = min_ace[1]
|
||||||
|
user = min_ace[2]
|
||||||
|
|
||||||
|
effective_mask = dacl.GetEffectiveRightsFromAcl({
|
||||||
|
'TrusteeForm': win32security.TRUSTEE_IS_SID,
|
||||||
|
'TrusteeType': win32security.TRUSTEE_IS_USER,
|
||||||
|
'Identifier': user,
|
||||||
|
})
|
||||||
|
|
||||||
|
if effective_mask != effective_mask | mask:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def _win_is_executable(path):
|
def _win_is_executable(path):
|
||||||
if not os.path.isfile(path):
|
if not os.path.isfile(path):
|
||||||
return False
|
return False
|
||||||
@@ -472,8 +586,8 @@ def _compare_dacls(dacl1, dacl2):
|
|||||||
This method compare the two given DACLs to check if they are identical.
|
This method compare the two given DACLs to check if they are identical.
|
||||||
Identical means here that they contains the same set of ACEs in the same order.
|
Identical means here that they contains the same set of ACEs in the same order.
|
||||||
"""
|
"""
|
||||||
return ([dacl1.GetAce(index) for index in range(0, dacl1.GetAceCount())] ==
|
return ([dacl1.GetAce(index) for index in range(dacl1.GetAceCount())] ==
|
||||||
[dacl2.GetAce(index) for index in range(0, dacl2.GetAceCount())])
|
[dacl2.GetAce(index) for index in range(dacl2.GetAceCount())])
|
||||||
|
|
||||||
|
|
||||||
def _get_current_user():
|
def _get_current_user():
|
||||||
|
@@ -5,7 +5,6 @@ particular category.
|
|||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
|
|
||||||
import select
|
import select
|
||||||
import stat
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -18,18 +17,6 @@ from certbot import errors
|
|||||||
from certbot.compat import os
|
from certbot.compat import os
|
||||||
|
|
||||||
|
|
||||||
# MASK_FOR_PRIVATE_KEY_PERMISSIONS defines what are the permissions flags to keep
|
|
||||||
# when transferring the permissions from an old private key to a new one.
|
|
||||||
if POSIX_MODE:
|
|
||||||
# On Linux, we keep read/write/execute permissions
|
|
||||||
# for group and read permissions for everybody.
|
|
||||||
MASK_FOR_PRIVATE_KEY_PERMISSIONS = stat.S_IRGRP | stat.S_IWGRP | stat.S_IXGRP | stat.S_IROTH
|
|
||||||
else:
|
|
||||||
# On Windows, the mode returned by os.stat is not reliable,
|
|
||||||
# so we do not keep any permission from the previous private key.
|
|
||||||
MASK_FOR_PRIVATE_KEY_PERMISSIONS = 0
|
|
||||||
|
|
||||||
|
|
||||||
# For Linux: define OS specific standard binary directories
|
# For Linux: define OS specific standard binary directories
|
||||||
STANDARD_BINARY_DIRS = ["/usr/sbin", "/usr/local/bin", "/usr/local/sbin"] if POSIX_MODE else []
|
STANDARD_BINARY_DIRS = ["/usr/sbin", "/usr/local/bin", "/usr/local/sbin"] if POSIX_MODE else []
|
||||||
|
|
||||||
|
@@ -116,3 +116,21 @@ def access(*unused_args, **unused_kwargs):
|
|||||||
raise RuntimeError('Usage of os.access() is forbidden. '
|
raise RuntimeError('Usage of os.access() is forbidden. '
|
||||||
'Use certbot.compat.filesystem.check_mode() or '
|
'Use certbot.compat.filesystem.check_mode() or '
|
||||||
'certbot.compat.filesystem.is_executable() instead.')
|
'certbot.compat.filesystem.is_executable() instead.')
|
||||||
|
|
||||||
|
|
||||||
|
# On Windows os.stat call result is inconsistent, with a lot of flags that are not set or
|
||||||
|
# meaningless. We need to use specialized functions from the certbot.compat.filesystem module.
|
||||||
|
def stat(*unused_args, **unused_kwargs):
|
||||||
|
"""Method os.stat() is forbidden"""
|
||||||
|
raise RuntimeError('Usage of os.stat() is forbidden. '
|
||||||
|
'Use certbot.compat.filesystem functions instead '
|
||||||
|
'(eg. has_min_permissions, has_same_ownership).')
|
||||||
|
|
||||||
|
|
||||||
|
# Method os.fstat has the same problem than os.stat, since it is the same function,
|
||||||
|
# but accepting a file descriptor instead of a path.
|
||||||
|
def fstat(*unused_args, **unused_kwargs):
|
||||||
|
"""Method os.stat() is forbidden"""
|
||||||
|
raise RuntimeError('Usage of os.fstat() is forbidden. '
|
||||||
|
'Use certbot.compat.filesystem functions instead '
|
||||||
|
'(eg. has_min_permissions, has_same_ownership).')
|
||||||
|
@@ -167,14 +167,18 @@ class _UnixLockMechanism(_BaseLockMechanism):
|
|||||||
:returns: True if the lock was successfully acquired
|
:returns: True if the lock was successfully acquired
|
||||||
:rtype: bool
|
:rtype: bool
|
||||||
"""
|
"""
|
||||||
|
# Normally os module should not be imported in certbot codebase except in certbot.compat
|
||||||
|
# for the sake of compatibility over Windows and Linux.
|
||||||
|
# We make an exception here, since _lock_success is private and called only on Linux.
|
||||||
|
from os import stat, fstat # pylint: disable=os-module-forbidden
|
||||||
try:
|
try:
|
||||||
stat1 = os.stat(self._path)
|
stat1 = stat(self._path)
|
||||||
except OSError as err:
|
except OSError as err:
|
||||||
if err.errno == errno.ENOENT:
|
if err.errno == errno.ENOENT:
|
||||||
return False
|
return False
|
||||||
raise
|
raise
|
||||||
|
|
||||||
stat2 = os.fstat(fd)
|
stat2 = fstat(fd)
|
||||||
# If our locked file descriptor and the file on disk refer to
|
# If our locked file descriptor and the file on disk refer to
|
||||||
# the same device and inode, they're the same file.
|
# the same device and inode, they're the same file.
|
||||||
return stat1.st_dev == stat2.st_dev and stat1.st_ino == stat2.st_ino
|
return stat1.st_dev == stat2.st_dev and stat1.st_ino == stat2.st_ino
|
||||||
|
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import abc
|
import abc
|
||||||
import logging
|
import logging
|
||||||
import stat
|
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
import configobj
|
import configobj
|
||||||
@@ -12,6 +11,7 @@ from acme import challenges
|
|||||||
|
|
||||||
from certbot import errors
|
from certbot import errors
|
||||||
from certbot import interfaces
|
from certbot import interfaces
|
||||||
|
from certbot.compat import filesystem
|
||||||
from certbot.compat import os
|
from certbot.compat import os
|
||||||
from certbot.display import ops
|
from certbot.display import ops
|
||||||
from certbot.display import util as display_util
|
from certbot.display import util as display_util
|
||||||
@@ -312,8 +312,7 @@ def validate_file_permissions(filename):
|
|||||||
|
|
||||||
validate_file(filename)
|
validate_file(filename)
|
||||||
|
|
||||||
permissions = stat.S_IMODE(os.stat(filename).st_mode)
|
if filesystem.has_world_permissions(filename):
|
||||||
if permissions & stat.S_IRWXO:
|
|
||||||
logger.warning('Unsafe permissions on credentials configuration file: %s', filename)
|
logger.warning('Unsafe permissions on credentials configuration file: %s', filename)
|
||||||
|
|
||||||
|
|
||||||
|
@@ -7,14 +7,15 @@ import unittest
|
|||||||
import mock
|
import mock
|
||||||
|
|
||||||
from certbot import errors
|
from certbot import errors
|
||||||
|
from certbot import util
|
||||||
from certbot.compat import os
|
from certbot.compat import os
|
||||||
from certbot.display import util as display_util
|
from certbot.display import util as display_util
|
||||||
from certbot.plugins import dns_common
|
from certbot.plugins import dns_common
|
||||||
from certbot.plugins import dns_test_common
|
from certbot.plugins import dns_test_common
|
||||||
from certbot.tests import util
|
from certbot.tests import util as test_util
|
||||||
|
|
||||||
|
|
||||||
class DNSAuthenticatorTest(util.TempDirTestCase, dns_test_common.BaseAuthenticatorTest):
|
class DNSAuthenticatorTest(test_util.TempDirTestCase, dns_test_common.BaseAuthenticatorTest):
|
||||||
# pylint: disable=protected-access
|
# pylint: disable=protected-access
|
||||||
|
|
||||||
class _FakeDNSAuthenticator(dns_common.DNSAuthenticator):
|
class _FakeDNSAuthenticator(dns_common.DNSAuthenticator):
|
||||||
@@ -50,7 +51,7 @@ class DNSAuthenticatorTest(util.TempDirTestCase, dns_test_common.BaseAuthenticat
|
|||||||
|
|
||||||
self.auth._cleanup.assert_called_once_with(dns_test_common.DOMAIN, mock.ANY, mock.ANY)
|
self.auth._cleanup.assert_called_once_with(dns_test_common.DOMAIN, mock.ANY, mock.ANY)
|
||||||
|
|
||||||
@util.patch_get_utility()
|
@test_util.patch_get_utility()
|
||||||
def test_prompt(self, mock_get_utility):
|
def test_prompt(self, mock_get_utility):
|
||||||
mock_display = mock_get_utility()
|
mock_display = mock_get_utility()
|
||||||
mock_display.input.side_effect = ((display_util.OK, "",),
|
mock_display.input.side_effect = ((display_util.OK, "",),
|
||||||
@@ -59,14 +60,14 @@ class DNSAuthenticatorTest(util.TempDirTestCase, dns_test_common.BaseAuthenticat
|
|||||||
self.auth._configure("other_key", "")
|
self.auth._configure("other_key", "")
|
||||||
self.assertEqual(self.auth.config.fake_other_key, "value")
|
self.assertEqual(self.auth.config.fake_other_key, "value")
|
||||||
|
|
||||||
@util.patch_get_utility()
|
@test_util.patch_get_utility()
|
||||||
def test_prompt_canceled(self, mock_get_utility):
|
def test_prompt_canceled(self, mock_get_utility):
|
||||||
mock_display = mock_get_utility()
|
mock_display = mock_get_utility()
|
||||||
mock_display.input.side_effect = ((display_util.CANCEL, "c",),)
|
mock_display.input.side_effect = ((display_util.CANCEL, "c",),)
|
||||||
|
|
||||||
self.assertRaises(errors.PluginError, self.auth._configure, "other_key", "")
|
self.assertRaises(errors.PluginError, self.auth._configure, "other_key", "")
|
||||||
|
|
||||||
@util.patch_get_utility()
|
@test_util.patch_get_utility()
|
||||||
def test_prompt_file(self, mock_get_utility):
|
def test_prompt_file(self, mock_get_utility):
|
||||||
path = os.path.join(self.tempdir, 'file.ini')
|
path = os.path.join(self.tempdir, 'file.ini')
|
||||||
open(path, "wb").close()
|
open(path, "wb").close()
|
||||||
@@ -80,7 +81,7 @@ class DNSAuthenticatorTest(util.TempDirTestCase, dns_test_common.BaseAuthenticat
|
|||||||
self.auth._configure_file("file_path", "")
|
self.auth._configure_file("file_path", "")
|
||||||
self.assertEqual(self.auth.config.fake_file_path, path)
|
self.assertEqual(self.auth.config.fake_file_path, path)
|
||||||
|
|
||||||
@util.patch_get_utility()
|
@test_util.patch_get_utility()
|
||||||
def test_prompt_file_canceled(self, mock_get_utility):
|
def test_prompt_file_canceled(self, mock_get_utility):
|
||||||
mock_display = mock_get_utility()
|
mock_display = mock_get_utility()
|
||||||
mock_display.directory_select.side_effect = ((display_util.CANCEL, "c",),)
|
mock_display.directory_select.side_effect = ((display_util.CANCEL, "c",),)
|
||||||
@@ -96,7 +97,7 @@ class DNSAuthenticatorTest(util.TempDirTestCase, dns_test_common.BaseAuthenticat
|
|||||||
|
|
||||||
self.assertEqual(credentials.conf("test"), "value")
|
self.assertEqual(credentials.conf("test"), "value")
|
||||||
|
|
||||||
@util.patch_get_utility()
|
@test_util.patch_get_utility()
|
||||||
def test_prompt_credentials(self, mock_get_utility):
|
def test_prompt_credentials(self, mock_get_utility):
|
||||||
bad_path = os.path.join(self.tempdir, 'bad-file.ini')
|
bad_path = os.path.join(self.tempdir, 'bad-file.ini')
|
||||||
dns_test_common.write({"fake_other": "other_value"}, bad_path)
|
dns_test_common.write({"fake_other": "other_value"}, bad_path)
|
||||||
@@ -116,7 +117,7 @@ class DNSAuthenticatorTest(util.TempDirTestCase, dns_test_common.BaseAuthenticat
|
|||||||
self.assertEqual(credentials.conf("test"), "value")
|
self.assertEqual(credentials.conf("test"), "value")
|
||||||
|
|
||||||
|
|
||||||
class CredentialsConfigurationTest(util.TempDirTestCase):
|
class CredentialsConfigurationTest(test_util.TempDirTestCase):
|
||||||
class _MockLoggingHandler(logging.Handler):
|
class _MockLoggingHandler(logging.Handler):
|
||||||
messages = None
|
messages = None
|
||||||
|
|
||||||
@@ -150,14 +151,14 @@ class CredentialsConfigurationTest(util.TempDirTestCase):
|
|||||||
dns_common.logger.addHandler(log)
|
dns_common.logger.addHandler(log)
|
||||||
|
|
||||||
path = os.path.join(self.tempdir, 'too-permissive-file.ini')
|
path = os.path.join(self.tempdir, 'too-permissive-file.ini')
|
||||||
open(path, "wb").close()
|
util.safe_open(path, "wb", 0o744).close()
|
||||||
|
|
||||||
dns_common.CredentialsConfiguration(path)
|
dns_common.CredentialsConfiguration(path)
|
||||||
|
|
||||||
self.assertEqual(1, len([_ for _ in log.messages['warning'] if _.startswith("Unsafe")]))
|
self.assertEqual(1, len([_ for _ in log.messages['warning'] if _.startswith("Unsafe")]))
|
||||||
|
|
||||||
|
|
||||||
class CredentialsConfigurationRequireTest(util.TempDirTestCase):
|
class CredentialsConfigurationRequireTest(test_util.TempDirTestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(CredentialsConfigurationRequireTest, self).setUp()
|
super(CredentialsConfigurationRequireTest, self).setUp()
|
||||||
|
@@ -34,7 +34,13 @@ class AuthenticatorTest(unittest.TestCase):
|
|||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
from certbot.plugins.webroot import Authenticator
|
from certbot.plugins.webroot import Authenticator
|
||||||
self.path = tempfile.mkdtemp()
|
# On Linux directories created by tempfile.mkdtemp inherit their permissions from their
|
||||||
|
# parent directory. So the actual permissions are inconsistent over various tests env.
|
||||||
|
# To circumvent this, a dedicated sub-workspace is created under the workspace, using
|
||||||
|
# filesystem.mkdir to get consistent permissions.
|
||||||
|
self.workspace = tempfile.mkdtemp()
|
||||||
|
self.path = os.path.join(self.workspace, 'webroot')
|
||||||
|
filesystem.mkdir(self.path)
|
||||||
self.partial_root_challenge_path = os.path.join(
|
self.partial_root_challenge_path = os.path.join(
|
||||||
self.path, ".well-known")
|
self.path, ".well-known")
|
||||||
self.root_challenge_path = os.path.join(
|
self.root_challenge_path = os.path.join(
|
||||||
@@ -170,17 +176,12 @@ class AuthenticatorTest(unittest.TestCase):
|
|||||||
self.assertTrue(filesystem.check_mode(self.validation_path, 0o644))
|
self.assertTrue(filesystem.check_mode(self.validation_path, 0o644))
|
||||||
|
|
||||||
# Check permissions of the directories
|
# Check permissions of the directories
|
||||||
|
|
||||||
for dirpath, dirnames, _ in os.walk(self.path):
|
for dirpath, dirnames, _ in os.walk(self.path):
|
||||||
for directory in dirnames:
|
for directory in dirnames:
|
||||||
full_path = os.path.join(dirpath, directory)
|
full_path = os.path.join(dirpath, directory)
|
||||||
self.assertTrue(filesystem.check_mode(full_path, 0o755))
|
self.assertTrue(filesystem.check_mode(full_path, 0o755))
|
||||||
|
|
||||||
parent_gid = os.stat(self.path).st_gid
|
self.assertTrue(filesystem.has_same_ownership(self.validation_path, self.path))
|
||||||
parent_uid = os.stat(self.path).st_uid
|
|
||||||
|
|
||||||
self.assertEqual(os.stat(self.validation_path).st_gid, parent_gid)
|
|
||||||
self.assertEqual(os.stat(self.validation_path).st_uid, parent_uid)
|
|
||||||
|
|
||||||
def test_perform_cleanup(self):
|
def test_perform_cleanup(self):
|
||||||
self.auth.prepare()
|
self.auth.prepare()
|
||||||
|
@@ -18,7 +18,6 @@ from certbot import crypto_util
|
|||||||
from certbot import error_handler
|
from certbot import error_handler
|
||||||
from certbot import errors
|
from certbot import errors
|
||||||
from certbot import util
|
from certbot import util
|
||||||
from certbot.compat import misc
|
|
||||||
from certbot.compat import os
|
from certbot.compat import os
|
||||||
from certbot.compat import filesystem
|
from certbot.compat import filesystem
|
||||||
from certbot.plugins import common as plugins_common
|
from certbot.plugins import common as plugins_common
|
||||||
@@ -1107,9 +1106,7 @@ class RenewableCert(object):
|
|||||||
f.write(new_privkey)
|
f.write(new_privkey)
|
||||||
# Preserve gid and (mode & MASK_FOR_PRIVATE_KEY_PERMISSIONS)
|
# Preserve gid and (mode & MASK_FOR_PRIVATE_KEY_PERMISSIONS)
|
||||||
# from previous privkey in this lineage.
|
# from previous privkey in this lineage.
|
||||||
old_mode = (stat.S_IMODE(os.stat(old_privkey).st_mode) &
|
mode = filesystem.compute_private_key_mode(old_privkey, BASE_PRIVKEY_MODE)
|
||||||
misc.MASK_FOR_PRIVATE_KEY_PERMISSIONS)
|
|
||||||
mode = BASE_PRIVKEY_MODE | old_mode
|
|
||||||
filesystem.copy_ownership_and_apply_mode(
|
filesystem.copy_ownership_and_apply_mode(
|
||||||
old_privkey, target["privkey"], mode, copy_user=False, copy_group=True)
|
old_privkey, target["privkey"], mode, copy_user=False, copy_group=True)
|
||||||
|
|
||||||
|
@@ -150,6 +150,24 @@ class WindowsChmodTests(TempDirTestCase):
|
|||||||
self.assertEqual(security_dacl.GetSecurityDescriptorDacl().GetAceCount(), 2)
|
self.assertEqual(security_dacl.GetSecurityDescriptorDacl().GetAceCount(), 2)
|
||||||
|
|
||||||
|
|
||||||
|
class ComputePrivateKeyModeTest(TempDirTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(ComputePrivateKeyModeTest, self).setUp()
|
||||||
|
self.probe_path = _create_probe(self.tempdir)
|
||||||
|
|
||||||
|
def test_compute_private_key_mode(self):
|
||||||
|
filesystem.chmod(self.probe_path, 0o777)
|
||||||
|
new_mode = filesystem.compute_private_key_mode(self.probe_path, 0o600)
|
||||||
|
|
||||||
|
if POSIX_MODE:
|
||||||
|
# On Linux RWX permissions for group and R permission for world
|
||||||
|
# are persisted from the existing moe
|
||||||
|
self.assertEqual(new_mode, 0o674)
|
||||||
|
else:
|
||||||
|
# On Windows no permission is persisted
|
||||||
|
self.assertEqual(new_mode, 0o600)
|
||||||
|
|
||||||
|
|
||||||
@unittest.skipIf(POSIX_MODE, reason='Tests specific to Windows security')
|
@unittest.skipIf(POSIX_MODE, reason='Tests specific to Windows security')
|
||||||
class WindowsOpenTest(TempDirTestCase):
|
class WindowsOpenTest(TempDirTestCase):
|
||||||
def test_new_file_correct_permissions(self):
|
def test_new_file_correct_permissions(self):
|
||||||
@@ -262,14 +280,14 @@ class WindowsMkdirTests(test_util.TempDirTestCase):
|
|||||||
self.assertEqual(original_mkdir, std_os.mkdir)
|
self.assertEqual(original_mkdir, std_os.mkdir)
|
||||||
|
|
||||||
|
|
||||||
class CopyOwnershipTest(test_util.TempDirTestCase):
|
class OwnershipTest(test_util.TempDirTestCase):
|
||||||
"""Tests about replacement of chown: copy_ownership_and_apply_mode"""
|
"""Tests about copy_ownership_and_apply_mode and has_same_ownership"""
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(CopyOwnershipTest, self).setUp()
|
super(OwnershipTest, self).setUp()
|
||||||
self.probe_path = _create_probe(self.tempdir)
|
self.probe_path = _create_probe(self.tempdir)
|
||||||
|
|
||||||
@unittest.skipIf(POSIX_MODE, reason='Test specific to Windows security')
|
@unittest.skipIf(POSIX_MODE, reason='Test specific to Windows security')
|
||||||
def test_windows(self):
|
def test_copy_ownership_windows(self):
|
||||||
system = win32security.ConvertStringSidToSid(SYSTEM_SID)
|
system = win32security.ConvertStringSidToSid(SYSTEM_SID)
|
||||||
security = win32security.SECURITY_ATTRIBUTES().SECURITY_DESCRIPTOR
|
security = win32security.SECURITY_ATTRIBUTES().SECURITY_DESCRIPTOR
|
||||||
security.SetSecurityDescriptorOwner(system, False)
|
security.SetSecurityDescriptorOwner(system, False)
|
||||||
@@ -295,7 +313,7 @@ class CopyOwnershipTest(test_util.TempDirTestCase):
|
|||||||
if dacl.GetAce(index)[2] == everybody])
|
if dacl.GetAce(index)[2] == everybody])
|
||||||
|
|
||||||
@unittest.skipUnless(POSIX_MODE, reason='Test specific to Linux security')
|
@unittest.skipUnless(POSIX_MODE, reason='Test specific to Linux security')
|
||||||
def test_linux(self):
|
def test_copy_ownership_linux(self):
|
||||||
with mock.patch('os.chown') as mock_chown:
|
with mock.patch('os.chown') as mock_chown:
|
||||||
with mock.patch('os.chmod') as mock_chmod:
|
with mock.patch('os.chmod') as mock_chmod:
|
||||||
with mock.patch('os.stat') as mock_stat:
|
with mock.patch('os.stat') as mock_stat:
|
||||||
@@ -307,8 +325,18 @@ class CopyOwnershipTest(test_util.TempDirTestCase):
|
|||||||
mock_chown.assert_called_once_with(self.probe_path, 50, 51)
|
mock_chown.assert_called_once_with(self.probe_path, 50, 51)
|
||||||
mock_chmod.assert_called_once_with(self.probe_path, 0o700)
|
mock_chmod.assert_called_once_with(self.probe_path, 0o700)
|
||||||
|
|
||||||
|
def test_has_same_ownership(self):
|
||||||
|
path1 = os.path.join(self.tempdir, 'test1')
|
||||||
|
path2 = os.path.join(self.tempdir, 'test2')
|
||||||
|
|
||||||
|
util.safe_open(path1, 'w').close()
|
||||||
|
util.safe_open(path2, 'w').close()
|
||||||
|
|
||||||
|
self.assertTrue(filesystem.has_same_ownership(path1, path2))
|
||||||
|
|
||||||
|
|
||||||
class CheckPermissionsTest(test_util.TempDirTestCase):
|
class CheckPermissionsTest(test_util.TempDirTestCase):
|
||||||
|
"""Tests relative to functions that check modes."""
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(CheckPermissionsTest, self).setUp()
|
super(CheckPermissionsTest, self).setUp()
|
||||||
self.probe_path = _create_probe(self.tempdir)
|
self.probe_path = _create_probe(self.tempdir)
|
||||||
@@ -353,6 +381,23 @@ class CheckPermissionsTest(test_util.TempDirTestCase):
|
|||||||
mock_owner.return_value = False
|
mock_owner.return_value = False
|
||||||
self.assertFalse(filesystem.check_permissions(self.probe_path, 0o744))
|
self.assertFalse(filesystem.check_permissions(self.probe_path, 0o744))
|
||||||
|
|
||||||
|
def test_check_min_permissions(self):
|
||||||
|
filesystem.chmod(self.probe_path, 0o744)
|
||||||
|
self.assertTrue(filesystem.has_min_permissions(self.probe_path, 0o744))
|
||||||
|
|
||||||
|
filesystem.chmod(self.probe_path, 0o700)
|
||||||
|
self.assertFalse(filesystem.has_min_permissions(self.probe_path, 0o744))
|
||||||
|
|
||||||
|
filesystem.chmod(self.probe_path, 0o741)
|
||||||
|
self.assertFalse(filesystem.has_min_permissions(self.probe_path, 0o744))
|
||||||
|
|
||||||
|
def test_is_world_reachable(self):
|
||||||
|
filesystem.chmod(self.probe_path, 0o744)
|
||||||
|
self.assertTrue(filesystem.has_world_permissions(self.probe_path))
|
||||||
|
|
||||||
|
filesystem.chmod(self.probe_path, 0o700)
|
||||||
|
self.assertFalse(filesystem.has_world_permissions(self.probe_path))
|
||||||
|
|
||||||
|
|
||||||
class OsReplaceTest(test_util.TempDirTestCase):
|
class OsReplaceTest(test_util.TempDirTestCase):
|
||||||
"""Test to ensure consistent behavior of rename method"""
|
"""Test to ensure consistent behavior of rename method"""
|
||||||
|
@@ -8,8 +8,8 @@ class OsTest(unittest.TestCase):
|
|||||||
"""Unit tests for os module."""
|
"""Unit tests for os module."""
|
||||||
def test_forbidden_methods(self):
|
def test_forbidden_methods(self):
|
||||||
# Checks for os module
|
# Checks for os module
|
||||||
for method in ['chmod', 'chown', 'open', 'mkdir',
|
for method in ['chmod', 'chown', 'open', 'mkdir', 'makedirs', 'rename',
|
||||||
'makedirs', 'rename', 'replace', 'access']:
|
'replace', 'access', 'stat', 'fstat']:
|
||||||
self.assertRaises(RuntimeError, getattr(os, method))
|
self.assertRaises(RuntimeError, getattr(os, method))
|
||||||
# Checks for os.path module
|
# Checks for os.path module
|
||||||
for method in ['realpath']:
|
for method in ['realpath']:
|
||||||
|
@@ -82,7 +82,10 @@ class LockFileTest(test_util.TempDirTestCase):
|
|||||||
'Race conditions on lock are specific to the non-blocking file access approach on Linux.')
|
'Race conditions on lock are specific to the non-blocking file access approach on Linux.')
|
||||||
def test_race(self):
|
def test_race(self):
|
||||||
should_delete = [True, False]
|
should_delete = [True, False]
|
||||||
stat = os.stat
|
# Normally os module should not be imported in certbot codebase except in certbot.compat
|
||||||
|
# for the sake of compatibility over Windows and Linux.
|
||||||
|
# We make an exception here, since test_race is a test function called only on Linux.
|
||||||
|
from os import stat # pylint: disable=os-module-forbidden
|
||||||
|
|
||||||
def delete_and_stat(path):
|
def delete_and_stat(path):
|
||||||
"""Wrap os.stat and maybe delete the file first."""
|
"""Wrap os.stat and maybe delete the file first."""
|
||||||
@@ -90,7 +93,7 @@ class LockFileTest(test_util.TempDirTestCase):
|
|||||||
os.remove(path)
|
os.remove(path)
|
||||||
return stat(path)
|
return stat(path)
|
||||||
|
|
||||||
with mock.patch('certbot.lock.os.stat') as mock_stat:
|
with mock.patch('certbot.lock.filesystem.os.stat') as mock_stat:
|
||||||
mock_stat.side_effect = delete_and_stat
|
mock_stat.side_effect = delete_and_stat
|
||||||
self._call(self.lock_path)
|
self._call(self.lock_path)
|
||||||
self.assertFalse(should_delete)
|
self.assertFalse(should_delete)
|
||||||
@@ -117,7 +120,7 @@ class LockFileTest(test_util.TempDirTestCase):
|
|||||||
|
|
||||||
def test_unexpected_os_err(self):
|
def test_unexpected_os_err(self):
|
||||||
if POSIX_MODE:
|
if POSIX_MODE:
|
||||||
mock_function = 'certbot.lock.os.stat'
|
mock_function = 'certbot.lock.filesystem.os.stat'
|
||||||
else:
|
else:
|
||||||
mock_function = 'certbot.lock.msvcrt.locking'
|
mock_function = 'certbot.lock.msvcrt.locking'
|
||||||
# The only expected errno are ENOENT and EACCES in lock module.
|
# The only expected errno are ENOENT and EACCES in lock module.
|
||||||
|
2
tox.ini
2
tox.ini
@@ -228,7 +228,7 @@ commands =
|
|||||||
--acme-server={env:ACME_SERVER:pebble} \
|
--acme-server={env:ACME_SERVER:pebble} \
|
||||||
--cov=acme --cov=certbot --cov=certbot_nginx --cov-report= \
|
--cov=acme --cov=certbot --cov=certbot_nginx --cov-report= \
|
||||||
--cov-config=certbot-ci/certbot_integration_tests/.coveragerc
|
--cov-config=certbot-ci/certbot_integration_tests/.coveragerc
|
||||||
coverage report --include 'certbot/*' --show-missing --fail-under=66
|
coverage report --include 'certbot/*' --show-missing --fail-under=65
|
||||||
coverage report --include 'certbot-nginx/*' --show-missing --fail-under=74
|
coverage report --include 'certbot-nginx/*' --show-missing --fail-under=74
|
||||||
passenv = DOCKER_*
|
passenv = DOCKER_*
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user