From bbccdbd81cb4a29760b8d5330bb292e54a803294 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 7 Mar 2021 00:27:07 +0100 Subject: [PATCH] mount: implement --numeric-owner (default: False!), fixes #2377 this is different default behaviour than in borg < 1.2: default (numeric_owner=False) is to use the user/group name from the archive, look up the local uid / gid and then use that for the FUSE fs. when --numeric-owner is given (numeric_owner=True), then the uid/gid from the archive is directly used (as it was the default behaviour in borg < 1.2). this was implemented like this (changing the default behaviour) to make borg mount and borg extract behave more similar considering usage of user/group numeric archived ids or archived names mapped to corresponding numeric local system ids. also, both now use the same function to get the uid/gid from the item. fuse: - add user and group name entries to default_dir - also: set internal_dict(!) of new Item with data from Item.as_dict() --- src/borg/archive.py | 25 +++++++++---- src/borg/archiver.py | 2 ++ src/borg/fuse.py | 18 +++++++--- src/borg/testsuite/archive.py | 68 ++++++++++++++++++++++++++++++++++- 4 files changed, 101 insertions(+), 12 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 202863c8f..1baa8fd6f 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -367,6 +367,24 @@ class CacheChunkBuffer(ChunkBuffer): return id_ +def get_item_uid_gid(item, *, numeric, uid_forced=None, gid_forced=None, uid_default=0, gid_default=0): + if uid_forced is not None: + uid = uid_forced + else: + uid = None if numeric else user2uid(item.user) + uid = item.uid if uid is None else uid + if uid < 0: + uid = uid_default + if gid_forced is not None: + gid = gid_forced + else: + gid = None if numeric else group2gid(item.group) + gid = item.gid if gid is None else gid + if gid < 0: + gid = gid_default + return uid, gid + + class Archive: class DoesNotExist(Error): @@ -809,12 +827,7 @@ Utilization of max. archive size: {csize_max:.0%} Does not access the repository. """ backup_io.op = 'attrs' - uid = gid = None - if not self.numeric_owner: - uid = user2uid(item.user) - gid = group2gid(item.group) - uid = item.uid if uid is None else uid - gid = item.gid if gid is None else gid + uid, gid = get_item_uid_gid(item, numeric=self.numeric_owner) # This code is a bit of a mess due to os specific differences if not is_win32: try: diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 8eb8bb28b..56ef1128d 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -2822,6 +2822,8 @@ class Archiver: help='stay in foreground, do not daemonize') parser.add_argument('-o', dest='options', type=str, help='Extra mount options') + parser.add_argument('--numeric-owner', dest='numeric_owner', action='store_true', + help='use numeric user and group identifiers from archive(s)') define_archive_filters_group(parser) parser.add_argument('paths', metavar='PATH', nargs='*', type=str, help='paths to extract; patterns are supported') diff --git a/src/borg/fuse.py b/src/borg/fuse.py index 841a72615..1c9840b4c 100644 --- a/src/borg/fuse.py +++ b/src/borg/fuse.py @@ -33,12 +33,13 @@ logger = create_logger() from .crypto.low_level import blake2b_128 from .archiver import Archiver -from .archive import Archive +from .archive import Archive, get_item_uid_gid from .hashindex import FuseVersionsIndex from .helpers import daemonize, daemonizing, hardlinkable, signal_handler, format_file_size from .helpers import msgpack from .item import Item from .lrucache import LRUCache +from .platform import uid2user, gid2group from .remote import RemoteRepository @@ -240,6 +241,7 @@ class FuseBackend(object): def __init__(self, key, manifest, repository, args, decrypted_repository): self.repository_uncached = repository self._args = args + self.numeric_owner = args.numeric_owner self._manifest = manifest self.key = key # Maps inode numbers to Item instances. This is used for synthetic inodes, i.e. file-system objects that are @@ -311,7 +313,7 @@ class FuseBackend(object): """ ino = self._allocate_inode() if mtime is not None: - self._items[ino] = Item(**self.default_dir.as_dict()) + self._items[ino] = Item(internal_dict=self.default_dir.as_dict()) self._items[ino].mtime = mtime else: self._items[ino] = self.default_dir @@ -530,8 +532,13 @@ class FuseOperations(llfuse.Operations, FuseBackend): self.umask = pop_option(options, 'umask', 0, 0, int, int_base=8) # umask is octal, e.g. 222 or 0222 dir_uid = self.uid_forced if self.uid_forced is not None else self.default_uid dir_gid = self.gid_forced if self.gid_forced is not None else self.default_gid + dir_user = uid2user(dir_uid) + dir_group = gid2group(dir_gid) + assert isinstance(dir_user, str) + assert isinstance(dir_group, str) dir_mode = 0o40755 & ~self.umask - self.default_dir = Item(mode=dir_mode, mtime=int(time.time() * 1e9), uid=dir_uid, gid=dir_gid) + self.default_dir = Item(mode=dir_mode, mtime=int(time.time() * 1e9), + user=dir_user, group=dir_group, uid=dir_uid, gid=dir_gid) self._create_filesystem() llfuse.init(self, mountpoint, options) if not foreground: @@ -581,8 +588,9 @@ class FuseOperations(llfuse.Operations, FuseBackend): entry.attr_timeout = 300 entry.st_mode = item.mode & ~self.umask entry.st_nlink = item.get('nlink', 1) - entry.st_uid = self.uid_forced if self.uid_forced is not None else item.uid if item.uid >= 0 else self.default_uid - entry.st_gid = self.gid_forced if self.gid_forced is not None else item.gid if item.gid >= 0 else self.default_gid + entry.st_uid, entry.st_gid = get_item_uid_gid(item, numeric=self.numeric_owner, + uid_default=self.default_uid, gid_default=self.default_gid, + uid_forced=self.uid_forced, gid_forced=self.gid_forced) entry.st_rdev = item.get('rdev', 0) entry.st_size = item.get_size() entry.st_blksize = 512 diff --git a/src/borg/testsuite/archive.py b/src/borg/testsuite/archive.py index 0decb9af9..f0480a362 100644 --- a/src/borg/testsuite/archive.py +++ b/src/borg/testsuite/archive.py @@ -8,10 +8,11 @@ import pytest from . import BaseTestCase from ..crypto.key import PlaintextKey from ..archive import Archive, CacheChunkBuffer, RobustUnpacker, valid_msgpacked_dict, ITEM_KEYS, Statistics -from ..archive import BackupOSError, backup_io, backup_io_iter +from ..archive import BackupOSError, backup_io, backup_io_iter, get_item_uid_gid from ..helpers import Manifest from ..helpers import msgpack from ..item import Item, ArchiveItem +from ..platform import uid2user, gid2group @pytest.fixture() @@ -249,3 +250,68 @@ def test_backup_io_iter(): normal_iterator = Iterator(StopIteration) for _ in backup_io_iter(normal_iterator): assert False, 'StopIteration handled incorrectly' + + +def test_get_item_uid_gid(): + # test requires that: + # - a name for user 0 and group 0 exists, usually root:root or root:wheel. + # - a system user/group udoesnotexist:gdoesnotexist does NOT exist. + + user0, group0 = uid2user(0), gid2group(0) + + # this is intentionally a "strange" item, with not matching ids/names. + item = Item(path='filename', uid=1, gid=2, user=user0, group=group0) + + uid, gid = get_item_uid_gid(item, numeric=False) + # these are found via a name-to-id lookup + assert uid == 0 + assert gid == 0 + + uid, gid = get_item_uid_gid(item, numeric=True) + # these are directly taken from the item.uid and .gid + assert uid == 1 + assert gid == 2 + + uid, gid = get_item_uid_gid(item, numeric=False, uid_forced=3, gid_forced=4) + # these are enforced (not from item metadata) + assert uid == 3 + assert gid == 4 + + # item metadata broken, has negative ids. + item = Item(path='filename', uid=-1, gid=-2, user=user0, group=group0) + + uid, gid = get_item_uid_gid(item, numeric=True) + # use the uid/gid defaults (which both default to 0). + assert uid == 0 + assert gid == 0 + + uid, gid = get_item_uid_gid(item, numeric=True, uid_default=5, gid_default=6) + # use the uid/gid defaults (as given). + assert uid == 5 + assert gid == 6 + + # item metadata broken, has negative ids and non-existing user/group names. + item = Item(path='filename', uid=-3, gid=-4, user='udoesnotexist', group='gdoesnotexist') + + uid, gid = get_item_uid_gid(item, numeric=False) + # use the uid/gid defaults (which both default to 0). + assert uid == 0 + assert gid == 0 + + uid, gid = get_item_uid_gid(item, numeric=True, uid_default=7, gid_default=8) + # use the uid/gid defaults (as given). + assert uid == 7 + assert gid == 8 + + # item metadata has valid uid/gid, but non-existing user/group names. + item = Item(path='filename', uid=9, gid=10, user='udoesnotexist', group='gdoesnotexist') + + uid, gid = get_item_uid_gid(item, numeric=False) + # because user/group name does not exist here, use valid numeric ids from item metadata. + assert uid == 9 + assert gid == 10 + + uid, gid = get_item_uid_gid(item, numeric=False, uid_default=11, gid_default=12) + # because item uid/gid seems valid, do not use the given uid/gid defaults + assert uid == 9 + assert gid == 10