Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "chevah-compat"
version = "1.6.1"
version = "1.7.1"
maintainers = [
{ name = "Adi Roiban", email = "adi.roiban@proatria.com" },
]
Expand Down Expand Up @@ -40,8 +40,9 @@ Homepage = "https://github.com/chevah/compat"
# These are the deps required to develop.
# Try to pin them as much as possible.
dev = [
"ruff ~= 0.7",
"ruff ~= 0.16",
"bunch",
"psutil",
"Twisted==25.5.0",
"incremental",
"service-identity==24.2.0",
Expand Down
4 changes: 2 additions & 2 deletions pythia.conf
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
PYTHON_CONFIGURATION="default@3.13.2.6899196"
PYTHON_CONFIGURATION="default@3.12.13.20260408"
# This is defined as a Bash array of options to be passed to commands.
BASE_REQUIREMENTS=("chevah-brink==1.0.15" "paver==1.3.4" "wheel")
BASE_REQUIREMENTS=("chevah-brink==1.0.15" "paver==1.3.4" "wheel" "setuptools==78.1.1")
# Use our private PyPi server instead of the default one set in pythia.sh.
PIP_INDEX_URL="https://bin.chevah.com:20443/pypi/simple"
# Use our production server instead of the GitHub releases set by default.
Expand Down
12 changes: 12 additions & 0 deletions release-notes.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
Release notes for chevah.compat
===============================

1.7.1 - 2026-07-29
------------------

* For Unix, getRealPathFromSegments now returns the root folder instead
of an empty string.

1.7.0 - 2026-07-22
------------------

* Prevent the filesystem from modifying the root folder.
All operations that would modify the root folder will raise a CompatError.


1.6.1 - 2025-08-07
------------------
Expand Down
4 changes: 2 additions & 2 deletions src/chevah_compat/administration.py
Original file line number Diff line number Diff line change
Expand Up @@ -563,7 +563,7 @@ def _setUserPassword_freebsd(self, user):
)

def _setUserPassword_openbsd(self, user):
code, out = execute(
_, out = execute(
command=['encrypt'],
input_text=user.password.encode('utf-8'),
)
Expand Down Expand Up @@ -959,7 +959,7 @@ def deleteUser(self, user):
win32net.NetUserDel(user.pdc, user.name)
except win32net.error as error: # pragma: no cover
# Ignore user not found error.
(number, context, message) = error
(number, _context, _message) = error
# Ignore user not found error.
if number != ERROR_NONE_MAPPED:
raise
Expand Down
5 changes: 5 additions & 0 deletions src/chevah_compat/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,11 @@ def isLink(segments):
Return True if segments points to a link.
"""

def isRoot(segments):
"""
Return True if segments points to the root folder.
"""

def exists(segments):
"""
Return True if segments points to an existing path.
Expand Down
67 changes: 54 additions & 13 deletions src/chevah_compat/nt_filesystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,22 @@ def get_path(segments):
self._validateDrivePath(result)
return six.text_type(result)

def isRoot(self, segments):
"""
See `ILocalFilesystem`.
"""
normalized_segments = self.getSegments(self.getPath(segments))
if self._lock_in_home:
return super().isRoot(normalized_segments)

if normalized_segments in [[], ['.'], ['..']]:
return True

return (
len(normalized_segments) == 1
and normalized_segments[0].lower() in self._allowed_drive_letters
)

# Windows allows only 26 drive letters and is case insensitive.
_allowed_drive_letters = [
'a',
Expand Down Expand Up @@ -498,6 +514,10 @@ def makeLink(self, target_segments, link_segments):
if not self.process_capabilities.symbolic_link:
raise NotImplementedError('makeLink not implemented on this OS.')

self._rejectRoot(
link_segments, 'Creating a link as the root folder is not allowed.'
)

target_path = self.getRealPathFromSegments(
target_segments,
include_virtual=False,
Expand Down Expand Up @@ -562,14 +582,14 @@ def _getStatus(self, path, segments):
try:
file_info = win32file.GetFileInformationByHandle(file_handle)
(
attributes,
created_at,
accessed_at,
written_at,
_attributes,
_created_at,
_accessed_at,
_written_at,
volume_id,
file_high,
file_low,
n_links,
_file_high,
_file_low,
_n_links,
index_high,
index_low,
) = file_info
Expand Down Expand Up @@ -621,6 +641,10 @@ def setAttributes(self, segments, attributes):
"""
See `ILocalFilesystem`.
"""
self._rejectRoot(
segments,
'Setting attributes for the nt root folder is not allowed.',
)
with self._windowsToOSError(segments):
if 'uid' in attributes or 'gid' in attributes:
raise OSError(errno.EPERM, 'Operation not supported')
Expand Down Expand Up @@ -808,6 +832,11 @@ def deleteFolder(self, segments, recursive=True):

For symbolic links we always force non-recursive behaviour.
"""
self._rejectRoot(
segments,
'Deleting the nt root folder is not allowed.',
)

path = self.getRealPathFromSegments(segments, include_virtual=False)
path_encoded = self.getEncodedPath(path)
try:
Expand Down Expand Up @@ -854,6 +883,10 @@ def setOwner(self, segments, owner):
"""
See `ILocalFilesystem`.
"""
self._rejectRoot(
segments,
'Setting owner for the nt root folder is not allowed.',
)
path = self.getRealPathFromSegments(segments, include_virtual=False)
encoded_path = self.getEncodedPath(path)
try:
Expand All @@ -877,7 +910,7 @@ def _setOwner(self, path, owner):
)
d_acl = security_descriptor.GetSecurityDescriptorDacl()

user_sid, user_domain, user_type = (
user_sid, _user_domain, _user_type = (
win32security.LookupAccountName(None, owner)
)
flags = (
Expand Down Expand Up @@ -950,10 +983,14 @@ def addGroup(self, segments, group, permissions=None):
"""
See `ILocalFilesystem`.
"""
self._rejectRoot(
segments,
'Adding group for the nt root folder is not allowed.',
)
path = self.getRealPathFromSegments(segments, include_virtual=False)
encoded_path = self.getEncodedPath(path)
try:
group_sid, group_domain, group_type = (
group_sid, _group_domain, _group_type = (
win32security.LookupAccountName(None, group)
)
except win32net.error:
Expand Down Expand Up @@ -988,10 +1025,14 @@ def removeGroup(self, segments, group):
"""
See `ILocalFilesystem`.
"""
self._rejectRoot(
segments,
'Removing group for the nt root folder is not allowed.',
)
path = self.getRealPathFromSegments(segments, include_virtual=False)
encoded_path = self.getEncodedPath(path)
try:
group_sid, group_domain, group_type = (
group_sid, _group_domain, _group_type = (
win32security.LookupAccountName(None, group)
)
except win32net.error:
Expand All @@ -1018,7 +1059,7 @@ def removeGroup(self, segments, group):
return None
index_ace_to_remove = -1
for index in range(ace_count):
((ace_type, ace_flag), mask, sid) = dacl.GetAce(index)
((_ace_type, _ace_flag), _mask, sid) = dacl.GetAce(index)
if group_sid == sid:
index_ace_to_remove = index
break
Expand All @@ -1045,7 +1086,7 @@ def hasGroup(self, segments, group):
encoded_path = self.getEncodedPath(path)

try:
group_sid, group_domain, group_type = (
group_sid, _group_domain, _group_type = (
win32security.LookupAccountName(None, group)
)
except win32net.error:
Expand All @@ -1066,7 +1107,7 @@ def hasGroup(self, segments, group):
# Nothing in the list.
return False
for index in range(ace_count):
((ace_type, ace_flag), mask, sid) = dacl.GetAce(index)
((_ace_type, _ace_flag), _mask, sid) = dacl.GetAce(index)
if group_sid == sid:
return True
return False
Expand Down
4 changes: 2 additions & 2 deletions src/chevah_compat/nt_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,11 +206,11 @@ def getGroupForUser(self, username, groups, token):
if not groups:
raise ValueError("Groups for validation can't be empty.")

primary_domain_controller, name = self._parseUPN(username)
primary_domain_controller, _name = self._parseUPN(username)

for group in groups:
try:
group_sid, group_domain, group_type = (
group_sid, _group_domain, _group_type = (
win32security.LookupAccountName(
primary_domain_controller,
group,
Expand Down
42 changes: 39 additions & 3 deletions src/chevah_compat/posix_filesystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,25 @@ def isLink(self, segments):
"""
raise NotImplementedError('isLink')

def isRoot(self, segments):
"""
See `ILocalFilesystem`.
"""
normalized_segments = self.getSegments(self.getPath(segments))
path = self.getRealPathFromSegments(
normalized_segments,
include_virtual=False,
)
root = self.getRealPathFromSegments([], include_virtual=False)
return root.lower() == path.lower()
Comment thread
adiroiban marked this conversation as resolved.

def _rejectRoot(self, segments, message):
"""
Helper to raise an error on operations that modify the root folder.
"""
if self.isRoot(segments):
raise CompatError(1009, message)

def exists(self, segments):
"""See `ILocalFilesystem`."""

Expand All @@ -406,7 +425,10 @@ def exists(self, segments):
return os.path.lexists(path_encoded)

def createFolder(self, segments, recursive=False):
"""See `ILocalFilesystem`."""
"""
See `ILocalFilesystem`.
"""
self._rejectRoot(segments, 'Creating the root folder is not allowed.')
path = self.getRealPathFromSegments(segments, include_virtual=False)
path_encoded = self.getEncodedPath(path)
with self._impersonateUser():
Expand Down Expand Up @@ -488,7 +510,15 @@ def deleteFile(self, segments, ignore_errors=False):
raise

def rename(self, from_segments, to_segments):
"""See `ILocalFilesystem`."""
"""
See `ILocalFilesystem`.
"""
self._rejectRoot(
from_segments, 'Renaming from the root folder is not allowed.'
)
self._rejectRoot(
to_segments, 'Renaming to the root folder is not allowed.'
)
from_path = self.getRealPathFromSegments(
from_segments,
include_virtual=False,
Expand Down Expand Up @@ -857,7 +887,13 @@ def _getPlaceholderStatus(self):
return os.stat_result([0o40555, 0, 0, 0, 1, 1, 0, 1, modified, 0])

def setAttributes(self, segments, attributes):
"""See `ILocalFilesystem`."""
"""
See `ILocalFilesystem`.
"""
self._rejectRoot(
segments,
'Setting attributes on the posix root folder is not allowed.',
)
path = self.getRealPathFromSegments(segments, include_virtual=False)
path_encoded = self.getEncodedPath(path)
with self._impersonateUser():
Expand Down
2 changes: 1 addition & 1 deletion src/chevah_compat/testing/mockup.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def sanitizeName(cls, name):
os_version = ChevahTestCase.os_version
if os_name in ['aix', 'hpux', 'freebsd', 'openbsd']:
return _sanitize_name_legacy_unix(name)
if os_version in ['osx-10.16']:
if os_version == 'osx-10.16':
# It looks like macOS 11 can't handle full Unix group names.
macosname = _sanitize_name_legacy_unix(name)
return macosname.replace('_', 'Z')
Expand Down
4 changes: 2 additions & 2 deletions src/chevah_compat/tests/elevated/test_system_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ def test_checkPasswdFile_valid(self):
if self.os_name in ['aix', 'hpux']:
# On AIX and HPUX password is in the passwd file.
self.assertTrue(result)
elif self.os_version in ['rhel-5']:
elif self.os_version == 'rhel-5':
# Old RHEL/Centos contain has the password in passwd file.
self.assertIsTrue(result)
else:
Expand All @@ -277,7 +277,7 @@ def test_checkPasswdFile_invalid(self):
if self.os_name in ['aix', 'hpux']:
# On AIX and HPUX invalid passwords are not accepted.
self.assertFalse(result)
elif self.os_version in ['rhel-5']:
elif self.os_version == 'rhel-5':
# Old RHEL/Centos contain has the password in passwd file,
# and the provided password doesn't match what is in the file.
self.assertIsFalse(result)
Expand Down
2 changes: 1 addition & 1 deletion src/chevah_compat/tests/normal/test_capabilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ def test_getCurrentPrivilegesDescription(self):
self.assertNotContains('SeBackupPrivilege:0', text)
self.assertNotContains('SeRestorePrivilege', text)

if self.os_version in ['nt-5.1']:
if self.os_version == 'nt-5.1':
# Windows XP has SE_CREATE_GLOBAL enabled even when
# running without administrator privileges.
self.assertContains('SeCreateGlobalPrivilege:3', text)
Expand Down
Loading
Loading