Skip to content

Commit 0bda4f5

Browse files
fuse: add ACL support (Linux)
1 parent 7ab45c7 commit 0bda4f5

File tree

7 files changed

+107
-8
lines changed

7 files changed

+107
-8
lines changed

src/borg/fuse.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def async_wrapper(fn):
4444
from .helpers import msgpack
4545
from .helpers.lrucache import LRUCache
4646
from .item import Item
47-
from .platform import uid2user, gid2group
47+
from .platform import uid2user, gid2group, get_binary_acl
4848
from .platformflags import is_darwin
4949
from .repository import Repository
5050
from .remote import RemoteRepository
@@ -455,6 +455,8 @@ def _process_inner(self, name, parent_inode):
455455
class FuseOperations(llfuse.Operations, FuseBackend):
456456
"""Export archive as a FUSE filesystem"""
457457

458+
enable_acl = True
459+
458460
def __init__(self, manifest, args, decrypted_repository):
459461
llfuse.Operations.__init__(self)
460462
FuseBackend.__init__(self, manifest, args, decrypted_repository)
@@ -631,13 +633,23 @@ def getattr(self, inode, ctx=None):
631633
@async_wrapper
632634
def listxattr(self, inode, ctx=None):
633635
item = self.get_item(inode)
634-
return item.get("xattrs", {}).keys()
636+
xattrs = list(item.get("xattrs", {}).keys())
637+
if "acl_access" in item:
638+
xattrs.append(b"system.posix_acl_access")
639+
if "acl_default" in item:
640+
xattrs.append(b"system.posix_acl_default")
641+
return xattrs
635642

636643
@async_wrapper
637644
def getxattr(self, inode, name, ctx=None):
638645
item = self.get_item(inode)
639646
try:
640-
return item.get("xattrs", {})[name] or b""
647+
if name == b"system.posix_acl_access":
648+
return get_binary_acl(item, "acl_access", self.numeric_ids)
649+
elif name == b"system.posix_acl_default":
650+
return get_binary_acl(item, "acl_default", self.numeric_ids)
651+
else:
652+
return item.get("xattrs", {})[name] or b""
641653
except KeyError:
642654
raise llfuse.FUSEError(llfuse.ENOATTR) from None
643655

src/borg/platform/__init__.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
if is_linux: # pragma: linux only
1414
from .linux import API_VERSION as OS_API_VERSION
1515
from .linux import listxattr, getxattr, setxattr
16-
from .linux import acl_get, acl_set
16+
from .linux import acl_get, acl_set, get_binary_acl
1717
from .linux import set_flags, get_flags
1818
from .linux import SyncFile
1919
from .posix import process_alive, local_pid_alive
@@ -23,7 +23,7 @@
2323
elif is_freebsd: # pragma: freebsd only
2424
from .freebsd import API_VERSION as OS_API_VERSION
2525
from .freebsd import listxattr, getxattr, setxattr
26-
from .freebsd import acl_get, acl_set
26+
from .freebsd import acl_get, acl_set, get_binary_acl
2727
from .base import set_flags, get_flags
2828
from .base import SyncFile
2929
from .posix import process_alive, local_pid_alive
@@ -33,7 +33,7 @@
3333
elif is_darwin: # pragma: darwin only
3434
from .darwin import API_VERSION as OS_API_VERSION
3535
from .darwin import listxattr, getxattr, setxattr
36-
from .darwin import acl_get, acl_set
36+
from .darwin import acl_get, acl_set, get_binary_acl
3737
from .darwin import is_darwin_feature_64_bit_inode, _get_birthtime_ns
3838
from .base import set_flags, get_flags
3939
from .base import SyncFile
@@ -45,7 +45,7 @@
4545
# Generic code for all other POSIX OSes
4646
OS_API_VERSION = API_VERSION
4747
from .base import listxattr, getxattr, setxattr
48-
from .base import acl_get, acl_set
48+
from .base import acl_get, acl_set, get_binary_acl
4949
from .base import set_flags, get_flags
5050
from .base import SyncFile
5151
from .posix import process_alive, local_pid_alive

src/borg/platform/base.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,13 @@ def acl_set(path, item, numeric_ids=False, fd=None):
8080
"""
8181

8282

83+
def get_binary_acl(item, key, numeric_ids=False):
84+
"""
85+
Get binary ACL data for item and key (acl name).
86+
"""
87+
raise KeyError(key)
88+
89+
8390
try:
8491
from os import lchflags # type: ignore[attr-defined]
8592

src/borg/platform/darwin.pyx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ from libc cimport errno
55
from posix.time cimport timespec
66

77
from .posix import user2uid, group2gid
8+
from .base import get_binary_acl # dummy
89
from ..helpers import safe_decode, safe_encode
910
from .xattr import _listxattr_inner, _getxattr_inner, _setxattr_inner, split_string0
1011

src/borg/platform/freebsd.pyx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import stat
44
from libc cimport errno
55

66
from .posix import posix_acl_use_stored_uid_gid
7+
from .base import get_binary_acl # dummy
78
from ..helpers import safe_encode, safe_decode
89
from .xattr import _listxattr_inner, _getxattr_inner, _setxattr_inner, split_lstring
910

src/borg/platform/linux.pyx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from cpython.bytes cimport PyBytes_FromStringAndSize
2+
13
import os
24
import re
35
import stat
@@ -48,6 +50,7 @@ cdef extern from "sys/acl.h":
4850
int acl_set_fd(int fd, acl_t acl)
4951
acl_t acl_from_text(const char *buf)
5052
char *acl_to_text(acl_t acl, ssize_t *len)
53+
ssize_t acl_size(acl_t)
5154

5255
cdef extern from "acl/libacl.h":
5356
int acl_extended_file_nofollow(const char *path)
@@ -321,6 +324,31 @@ def acl_set(path, item, numeric_ids=False, fd=None):
321324
acl_free(default_acl)
322325

323326

327+
def get_binary_acl(item, key, numeric_ids=False):
328+
assert key in ("acl_access", "acl_default")
329+
cdef acl_t acl = NULL
330+
cdef ssize_t size
331+
332+
if numeric_ids:
333+
converter = posix_acl_use_stored_uid_gid
334+
else:
335+
converter = acl_use_local_uid_gid
336+
acl_text = item.get(key)
337+
if acl_text is None:
338+
raise KeyError(key)
339+
try:
340+
acl = acl_from_text(<bytes>converter(acl_text))
341+
if acl == NULL:
342+
raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(item.path))
343+
size = acl_size(acl)
344+
if size < 0:
345+
raise OSError(errno.errno, "Failed to get ACL size")
346+
# Create a bytes object from the ACL data structure:
347+
return PyBytes_FromStringAndSize(<char *> acl, size)
348+
finally:
349+
acl_free(acl)
350+
351+
324352
cdef _sync_file_range(fd, offset, length, flags):
325353
assert offset & PAGE_MASK == 0, "offset %d not page-aligned" % offset
326354
assert length & PAGE_MASK == 0, "length %d not page-aligned" % length

src/borg/testsuite/archiver/mount_cmds_test.py

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from .. import has_lchflags, llfuse
1313
from .. import changedir, no_selinux, same_ts_ns
1414
from .. import are_symlinks_supported, are_hardlinks_supported, are_fifos_supported
15-
from ..platform.platform_test import fakeroot_detected
15+
from ..platform.platform_test import fakeroot_detected, skipif_not_linux, skipif_acls_not_working
1616
from . import RK_ENCRYPTION, cmd, assert_dirs_equal, create_regular_file, create_src_archive, open_archive, src_file
1717
from . import requires_hardlinks, _extract_hardlinks_setup, fuse_mount, create_test_files, generate_archiver_tests
1818

@@ -271,6 +271,56 @@ def test_fuse_mount_options(archivers, request):
271271
assert sorted(os.listdir(os.path.join(mountpoint))) == []
272272

273273

274+
@pytest.mark.skipif(not llfuse, reason="llfuse not installed")
275+
@skipif_not_linux
276+
@skipif_acls_not_working
277+
def test_fuse_acl_support(archivers, request):
278+
"""Test that ACLs are correctly exposed through the FUSE interface."""
279+
archiver = request.getfixturevalue(archivers)
280+
cmd(archiver, "repo-create", RK_ENCRYPTION)
281+
282+
# Create a file with access ACL
283+
file_path = os.path.join(archiver.input_path, "aclfile")
284+
with open(file_path, "w") as f:
285+
f.write("test content")
286+
access_acl = b"user::rw-\ngroup::r--\nmask::rw-\nother::---\nuser:root:rw-:0\ngroup:root:r--:0\n"
287+
platform.acl_set(file_path, {"acl_access": access_acl})
288+
289+
# Create a directory with default ACL
290+
dir_path = os.path.join(archiver.input_path, "acldir")
291+
os.mkdir(dir_path)
292+
default_acl = b"user::rw-\ngroup::r--\nmask::rw-\nother::---\nuser:root:r--:0\ngroup:root:r--:0\n"
293+
platform.acl_set(dir_path, {"acl_access": access_acl, "acl_default": default_acl})
294+
295+
# Create an archive
296+
cmd(archiver, "create", "test", "input")
297+
298+
# Mount the archive
299+
mountpoint = os.path.join(archiver.tmpdir, "mountpoint")
300+
with fuse_mount(archiver, mountpoint, "-a", "test"):
301+
# Verify file ACLs are preserved
302+
mounted_file = os.path.join(mountpoint, "test", "input", "aclfile")
303+
assert os.path.exists(mounted_file)
304+
assert os.path.isfile(mounted_file)
305+
file_acl = {}
306+
platform.acl_get(mounted_file, file_acl, os.stat(mounted_file))
307+
assert "acl_access" in file_acl
308+
assert b"user::rw-" in file_acl["acl_access"]
309+
assert b"user:root:rw-" in file_acl["acl_access"]
310+
# Verify directory ACLs are preserved
311+
mounted_dir = os.path.join(mountpoint, "test", "input", "acldir")
312+
assert os.path.exists(mounted_dir)
313+
assert os.path.isdir(mounted_dir)
314+
dir_acl = {}
315+
platform.acl_get(mounted_dir, dir_acl, os.stat(mounted_dir))
316+
assert "acl_access" in dir_acl
317+
assert "acl_default" in dir_acl
318+
assert b"user::rw-" in dir_acl["acl_access"]
319+
assert b"user:root:rw-" in dir_acl["acl_access"]
320+
assert b"user::rw-" in dir_acl["acl_default"]
321+
assert b"user:root:r--" in dir_acl["acl_default"]
322+
323+
274324
@pytest.mark.skipif(not llfuse, reason="llfuse not installed")
275325
def test_migrate_lock_alive(archivers, request):
276326
"""Both old_id and new_id must not be stale during lock migration / daemonization."""

0 commit comments

Comments
 (0)