From fcd4f26367aec5e3933c2dfca53a215a8523dfe1 Mon Sep 17 00:00:00 2001 From: Rob Browning Date: Sun, 27 Nov 2011 14:39:47 -0600 Subject: [PATCH] Restore any metadata during "bup restore"; add "bup meta --edit". Use "bup meta --edit" to test "bup restore" and the new tar/rsync-like restoration behaviors. Signed-off-by: Rob Browning Reviewed-by: Zoran Zaric Tested-by: Alexander Barton --- Documentation/bup-meta.md | 41 +++++- Documentation/bup-restore.md | 29 ++++ Documentation/bup-save.md | 3 +- cmd/meta-cmd.py | 63 ++++++++- cmd/restore-cmd.py | 91 +++++++++--- lib/bup/metadata.py | 67 ++++----- lib/bup/t/tmetadata.py | 51 ------- lib/bup/vfs.py | 10 +- t/some-owner | 22 +++ t/test-meta.sh | 262 +++++++++++++++++++++++++++++++++-- t/unknown-owner | 22 +++ 11 files changed, 537 insertions(+), 124 deletions(-) create mode 100755 t/some-owner create mode 100755 t/unknown-owner diff --git a/Documentation/bup-meta.md b/Documentation/bup-meta.md index a53cc4c..00096dd 100644 --- a/Documentation/bup-meta.md +++ b/Documentation/bup-meta.md @@ -23,11 +23,18 @@ bup meta \--start-extract bup meta \--finish-extract ~ [-v] [-q] [\--numeric-ids] [-f *file*] +bup meta \--edit + ~ [\--set-uid *uid* | \--set-gid *gid* | \--set-user *user* | \--set-group *group* | ...] \<*paths*...\> + # DESCRIPTION -`bup meta` either creates or extracts a metadata archive. A metadata -archive contains the metadata information (timestamps, ownership, -access permissions, etc.) for a set of filesystem paths. +`bup meta` creates, extracts, or otherwise manipulates metadata +archives. A metadata archive contains the metadata information +(timestamps, ownership, access permissions, etc.) for a set of +filesystem paths. + +See `bup-restore`(1) for a description of the way ownership metadata +is restored. # OPTIONS @@ -61,6 +68,10 @@ access permissions, etc.) for a set of filesystem paths. `--start-extract`. The archive will be read from standard input unless `--file` is specified. +\--edit +: Edit metadata archives. The result will be written to standard + output unless `--file` is specified. + -f, \--file=*filename* : Read the metadata archive from *filename* or write it to *filename* as appropriate. If *filename* is "-", then read from @@ -70,7 +81,7 @@ access permissions, etc.) for a set of filesystem paths. : Recursively descend into subdirectories during `--create`. \--numeric-ids -: Apply numeric user and group IDs (rather than text IDs) during +: Apply numeric IDs (user, group, etc.) rather than names during `--extract` or `--finish-extract`. \--symlinks @@ -83,6 +94,24 @@ access permissions, etc.) for a set of filesystem paths. : Record pathnames when creating an archive. This option is enabled by default. Specify `--no-paths` to disable it. +\--set-uid=*uid* +: Set the metadata uid to the integer *uid* during `--edit`. + +\--set-gid=*gid* +: Set the metadata gid to the integer *gid* during `--edit`. + +\--set-user=*user* +: Set the metadata user to *user* during `--edit`. + +\--unset-user +: Remove the metadata user during `--edit`. + +\--set-group=*group* +: Set the metadata user to *group* during `--edit`. + +\--unset-group +: Remove the metadata group during `--edit`. + -v, \--verbose : Be more verbose (can be used more than once). @@ -107,6 +136,10 @@ access permissions, etc.) for a set of filesystem paths. ...fill in all regular file contents using some other tool... $ bup meta --finish-extract -f ../etc.meta + # Change user/uid to root. + $ bup meta --edit --set-uid 0 --set-user root \ + src.meta > dest.meta + # BUGS Hard links are not handled yet. diff --git a/Documentation/bup-restore.md b/Documentation/bup-restore.md index f5a2ad4..5085a67 100644 --- a/Documentation/bup-restore.md +++ b/Documentation/bup-restore.md @@ -45,6 +45,21 @@ the children will be restored to a subdirectory of the current directory. See the EXAMPLES section to see how this works. +Whenever path metadata is available, `bup restore` will attempt to +restore it. When restoring ownership, bup implements tar/rsync-like +semantics. It will not try to restore the user unless running as +root, and it will fall back to the numeric uid or gid whenever the +metadata contains a user or group name that doesn't exist on the +current system. The use of user and group names can be disabled via +`--numeric-ids` (which can be important when restoring a chroot, for +example), and as a special case, a uid or gid of 0 will never be +remapped by name. + +Note that during the restoration process, access to data within the +restore tree may be more permissive than it was in the original +source. Unless security is irrelevant, you must restore to a private +subdirectory, and then move the resulting tree to its final position. +See the EXAMPLES section for a demonstration. # OPTIONS @@ -52,6 +67,9 @@ this works. : create and change to directory *outdir* before extracting the files. +\--numeric-ids +: restore numeric IDs (user, group, etc.) rather than names. + -v, \--verbose : increase log output. Given once, prints every directory as it is restored; given twice, prints every @@ -97,6 +115,17 @@ Restore the whole directory (trailing slash): test2 test2/passwd test2/profile + +Restore a tree without risk of unauthorized access: + + # mkdir --mode 0700 restore-tmp + + # bup restore -C restore-tmp /somebackup/latest/foo + Restoring: 42, done. + + # mv restore-tmp/foo somewhere + + # rmdir restore-tmp # SEE ALSO diff --git a/Documentation/bup-save.md b/Documentation/bup-save.md index a62eb08..0384ff2 100644 --- a/Documentation/bup-save.md +++ b/Documentation/bup-save.md @@ -23,7 +23,8 @@ for `bup-index`(1). By default, metadata will be saved for every path. However, if `--strip`, `--strip-path`, or `--graft` is specified, metadata will -not be saved for the root directory (*/*). +not be saved for the root directory (*/*). See `bup-restore`(1) for +more information about the handling of metadata. # OPTIONS diff --git a/cmd/meta-cmd.py b/cmd/meta-cmd.py index b557ba3..158df92 100755 --- a/cmd/meta-cmd.py +++ b/cmd/meta-cmd.py @@ -6,7 +6,6 @@ # Public License as described in the bup LICENSE file. # TODO: Add tar-like -C option. -# TODO: Add tar-like -v support to --list. import sys from bup import metadata @@ -31,18 +30,26 @@ bup meta --create [OPTION ...] bup meta --extract [OPTION ...] bup meta --start-extract [OPTION ...] bup meta --finish-extract [OPTION ...] +bup meta --edit [OPTION ...] -- c,create write metadata for PATHs to stdout (or --file) t,list display metadata x,extract perform --start-extract followed by --finish-extract start-extract build tree matching metadata provided on standard input (or --file) finish-extract finish applying standard input (or --file) metadata to filesystem +edit alter metadata; write to stdout (or --file) f,file= specify source or destination file R,recurse recurse into subdirectories xdev,one-file-system don't cross filesystem boundaries -numeric-ids apply numeric IDs (user, group, etc.), not names, during restore +numeric-ids apply numeric IDs (user, group, etc.) rather than names symlinks handle symbolic links (default is true) paths include paths in metadata (default is true) +set-uid= set metadata uid (via --edit) +set-gid= set metadata gid (via --edit) +set-user= set metadata user (via --edit) +unset-user remove metadata user (via --edit) +set-group= set metadata group (via --edit) +unset-group remove metadata group (via --edit) v,verbose increase log output (can be used more than once) q,quiet don't show progress meter """ @@ -57,9 +64,10 @@ opt.quiet = opt.quiet or 0 metadata.verbose = opt.verbose - opt.quiet action_count = sum([bool(x) for x in [opt.create, opt.list, opt.extract, - opt.start_extract, opt.finish_extract]]) + opt.start_extract, opt.finish_extract, + opt.edit]]) if action_count > 1: - o.fatal("bup: only one action permitted: --create --list --extract") + o.fatal("bup: only one action permitted: --create --list --extract --edit") if action_count == 0: o.fatal("bup: no action specified") @@ -95,6 +103,53 @@ elif opt.extract: metadata.extract(src, restore_numeric_ids=opt.numeric_ids, create_symlinks=opt.symlinks) +elif opt.edit: + if len(remainder) < 1: + o.fatal("no paths specified for edit") + output_file = open_output(opt.file) + + unset_user = False # True if --unset-user was the last relevant option. + unset_group = False # True if --unset-group was the last relevant option. + for flag in flags: + if flag[0] == '--set-user': + unset_user = False + elif flag[0] == '--unset-user': + unset_user = True + elif flag[0] == '--set-group': + unset_group = False + elif flag[0] == '--unset-group': + unset_group = True + + for path in remainder: + f = open(path, 'r') + try: + for m in metadata._ArchiveIterator(f): + if opt.set_uid is not None: + try: + m.uid = int(opt.set_uid) + except ValueError: + o.fatal("uid must be an integer") + + if opt.set_gid is not None: + try: + m.gid = int(opt.set_gid) + except ValueError: + o.fatal("gid must be an integer") + + if unset_user: + m.user = '' + elif opt.set_user is not None: + m.user = opt.set_user + + if unset_group: + m.group = '' + elif opt.set_group is not None: + m.group = opt.set_group + + m.write(output_file) + finally: + f.close() + if saved_errors: log('WARNING: %d errors encountered.\n' % len(saved_errors)) diff --git a/cmd/restore-cmd.py b/cmd/restore-cmd.py index 445d9d0..ff2d8b2 100755 --- a/cmd/restore-cmd.py +++ b/cmd/restore-cmd.py @@ -1,14 +1,15 @@ #!/usr/bin/env python import sys, stat -from bup import options, git, vfs +from bup import options, git, metadata, vfs from bup.helpers import * optspec = """ bup restore [-C outdir] -- -C,outdir= change to given outdir before extracting files -v,verbose increase log output (can be used more than once) -q,quiet don't show progress meter +C,outdir= change to given outdir before extracting files +numeric-ids restore numeric IDs (user, group, etc.) rather than names +v,verbose increase log output (can be used more than once) +q,quiet don't show progress meter """ total_restored = 0 @@ -30,30 +31,76 @@ def plog(s): qprogress(s) -def do_node(top, n): - global total_restored - fullname = n.fullname(stop_at=top) - unlink(fullname) +def print_info(n, fullname): if stat.S_ISDIR(n.mode): verbose1('%s/' % fullname) - mkdirp(fullname) elif stat.S_ISLNK(n.mode): verbose2('%s@ -> %s' % (fullname, n.readlink())) - os.symlink(n.readlink(), fullname) else: verbose2(fullname) - outf = open(fullname, 'wb') - try: - for b in chunkyreader(n.open()): - outf.write(b) - finally: - outf.close() - total_restored += 1 - plog('Restoring: %d\r' % total_restored) - for sub in n: - do_node(top, sub) - - + + +def create_path(n, fullname, meta): + if meta: + meta.create_path(fullname) + else: + # These fallbacks are important -- meta could be null if, for + # example, save created a "fake" item, i.e. a new strip/graft + # path element, etc. You can find cases like that by + # searching for "Metadata()". + unlink(fullname) + if stat.S_ISDIR(n.mode): + mkdirp(fullname) + elif stat.S_ISLNK(n.mode): + os.symlink(n.readlink(), fullname) + + +def do_node(top, n, meta=None): + # meta will be None for dirs, and when there is no .bupm (i.e. no metadata) + global total_restored, opt + meta_stream = None + try: + fullname = n.fullname(stop_at=top) + # If this is a directory, its metadata is the first entry in + # any .bupm file inside the directory. Get it. + if(stat.S_ISDIR(n.mode)): + mfile = n.metadata_file() # VFS file -- cannot close(). + if mfile: + meta_stream = mfile.open() + meta = metadata.Metadata.read(meta_stream) + print_info(n, fullname) + create_path(n, fullname, meta) + + # Write content if appropriate (only regular files have content). + plain_file = False + if meta: + plain_file = stat.S_ISREG(meta.mode) + else: + plain_file = stat.S_ISREG(n.mode) + + if plain_file: + outf = open(fullname, 'wb') + try: + for b in chunkyreader(n.open()): + outf.write(b) + finally: + outf.close() + + total_restored += 1 + plog('Restoring: %d\r' % total_restored) + for sub in n: + m = None + # Don't get metadata if this is a dir -- handled in sub do_node(). + if meta_stream and not stat.S_ISDIR(sub.mode): + m = metadata.Metadata.read(meta_stream) + do_node(top, sub, m) + if meta: + meta.apply_to_path(fullname, + restore_numeric_ids=opt.numeric_ids) + finally: + if meta_stream: + meta_stream.close() + handle_ctrl_c() o = options.Options(optspec) diff --git a/lib/bup/metadata.py b/lib/bup/metadata.py index 0e54d5c..87fd10b 100644 --- a/lib/bup/metadata.py +++ b/lib/bup/metadata.py @@ -196,14 +196,16 @@ class Metadata: self.mtime = st.st_mtime self.ctime = st.st_ctime self.user = self.group = '' + # FIXME: should we be caching id -> user/group name mappings? + # IIRC, tar uses some trick -- possibly caching the last pair. try: self.user = pwd.getpwuid(st.st_uid)[0] except KeyError, e: - add_error("no user name for id %s '%s'" % (st.st_gid, path)) + pass try: self.group = grp.getgrgid(st.st_gid)[0] except KeyError, e: - add_error("no group name for id %s '%s'" % (st.st_gid, path)) + pass self.mode = st.st_mode def _encode_common(self): @@ -336,43 +338,44 @@ class Metadata: else: raise - # Don't try to restore user unless we're root, and even - # if asked, don't try to restore the user or group if - # it doesn't exist in the system db. - uid = self.uid - gid = self.gid - if not restore_numeric_ids: - if not self.user: - uid = -1 - add_error('ignoring missing user for "%s"\n' % path) - else: - if not is_superuser(): - uid = -1 # Not root; assume we can't change user. - else: + # Implement tar/rsync-like semantics; see bup-restore(1). + # FIXME: should we consider caching user/group name <-> id + # mappings, getgroups(), etc.? + uid = gid = -1 # By default, do nothing. + if is_superuser(): + uid = self.uid + gid = self.gid + if not restore_numeric_ids: + if self.uid != 0 and self.user: try: uid = pwd.getpwnam(self.user)[2] except KeyError: - uid = -1 - fmt = 'ignoring unknown user %s for "%s"\n' - add_error(fmt % (self.user, path)) - if not self.group: - gid = -1 - add_error('ignoring missing group for "%s"\n' % path) - else: + pass # Fall back to self.uid. + if self.gid != 0 and self.group: + try: + gid = grp.getgrnam(self.group)[2] + except KeyError: + pass # Fall back to self.gid. + else: # not superuser - only consider changing the group/gid + user_gids = os.getgroups() + if self.gid in user_gids: + gid = self.gid + if not restore_numeric_ids and \ + self.gid != 0 and \ + self.group in [grp.getgrgid(x)[0] for x in user_gids]: try: gid = grp.getgrnam(self.group)[2] except KeyError: - gid = -1 - add_error('ignoring unknown group %s for "%s"\n' - % (self.group, path)) + pass # Fall back to gid. - try: - os.lchown(path, uid, gid) - except OSError, e: - if e.errno == errno.EPERM: - add_error('lchown: %s' % e) - else: - raise + if uid != -1 or gid != -1: + try: + os.lchown(path, uid, gid) + except OSError, e: + if e.errno == errno.EPERM: + add_error('lchown: %s' % e) + else: + raise if _have_lchmod: os.lchmod(path, stat.S_IMODE(self.mode)) diff --git a/lib/bup/t/tmetadata.py b/lib/bup/t/tmetadata.py index 89a440f..ca2e55c 100644 --- a/lib/bup/t/tmetadata.py +++ b/lib/bup/t/tmetadata.py @@ -156,57 +156,6 @@ def test_apply_to_path_restricted_access(): subprocess.call(['rm', '-rf', tmpdir]) -@wvtest -def test_restore_restricted_user_group(): - if is_superuser() or detect_fakeroot(): - return - tmpdir = tempfile.mkdtemp(prefix='bup-tmetadata-') - try: - path = tmpdir + '/foo' - os.mkdir(path) - m = metadata.from_path(path, archive_path=path, save_symlinks=True) - WVPASSEQ(m.path, path) - WVPASSEQ(m.apply_to_path(path), None) - orig_uid = m.uid - m.uid = 0; - m.apply_to_path(path, restore_numeric_ids=True) - WVPASS(len(helpers.saved_errors) == 1) - errmsg = _first_err() - WVPASS(errmsg.startswith('lchown: ')) - clear_errors() - m.uid = orig_uid - m.gid = 0; - m.apply_to_path(path, restore_numeric_ids=True) - WVPASS(len(helpers.saved_errors) == 1) - errmsg = _first_err() - WVPASS(errmsg.startswith('lchown: ') or os.stat(path).st_gid == m.gid) - clear_errors() - finally: - subprocess.call(['rm', '-rf', tmpdir]) - - -@wvtest -def test_restore_nonexistent_user_group(): - tmpdir = tempfile.mkdtemp(prefix='bup-tmetadata-') - try: - path = tmpdir + '/foo' - os.mkdir(path) - m = metadata.from_path(path, archive_path=path, save_symlinks=True) - WVPASSEQ(m.path, path) - junk,m.user = max([(len(x.pw_name), x.pw_name + 'x') - for x in pwd.getpwall()]) - junk,m.group = max([(len(x.gr_name), x.gr_name + 'x') - for x in grp.getgrall()]) - WVPASSEQ(m.apply_to_path(path, restore_numeric_ids=True), None) - WVPASSEQ(os.stat(path).st_uid, m.uid) - WVPASSEQ(os.stat(path).st_gid, m.gid) - WVPASSEQ(m.apply_to_path(path, restore_numeric_ids=False), None) - WVPASSEQ(os.stat(path).st_uid, m.uid) - WVPASSEQ(os.stat(path).st_gid, m.gid) - finally: - subprocess.call(['rm', '-rf', tmpdir]) - - @wvtest def test_restore_over_existing_target(): tmpdir = tempfile.mkdtemp(prefix='bup-tmetadata-') diff --git a/lib/bup/vfs.py b/lib/bup/vfs.py index ccedffc..f58dd5e 100644 --- a/lib/bup/vfs.py +++ b/lib/bup/vfs.py @@ -386,7 +386,7 @@ class Dir(Node): def __init__(self, *args): Node.__init__(self, *args) - self._metadata_sha = None + self._metadata = None def _mksubs(self): self._subs = {} @@ -399,7 +399,8 @@ class Dir(Node): assert(type == 'tree') for (mode,mangled_name,sha) in git.tree_decode(''.join(it)): if mangled_name == '.bupm': - self._metadata_sha = sha + self._metadata = \ + File(self, mangled_name, mode, sha, git.BUP_NORMAL) continue name = mangled_name (name,bupmode) = git.demangle_name(mangled_name) @@ -412,6 +413,11 @@ class Dir(Node): else: self._subs[name] = File(self, name, mode, sha, bupmode) + def metadata_file(self): + if self._subs == None: + self._mksubs() + return self._metadata + class CommitDir(Node): """A directory that contains all commits that are reachable by a ref. diff --git a/t/some-owner b/t/some-owner new file mode 100755 index 0000000..e200fae --- /dev/null +++ b/t/some-owner @@ -0,0 +1,22 @@ +#!/usr/bin/env python + +import grp +import pwd +import sys + +def usage(): + print >> sys.stderr, "Usage: some-owners (--user | --group)" + +if len(sys.argv) != 2: + usage() + sys.exit(1) + +if sys.argv[1] == '--user': + non_root_users = [x.pw_name for x in pwd.getpwall() if x.pw_name != 'root'] + print non_root_users[0] +elif sys.argv[1] == '--group': + non_root_groups = [x.gr_name for x in grp.getgrall() if x.gr_name != 'root'] + print non_root_groups[0] +else: + usage() + sys.exit(1) diff --git a/t/test-meta.sh b/t/test-meta.sh index e59c155..3ce1d55 100755 --- a/t/test-meta.sh +++ b/t/test-meta.sh @@ -44,6 +44,22 @@ force-delete() fi } +compare-trees() +{ + ( + set -e + set -o pipefail + tmpfile="$(mktemp)" + trap "rm -rf ${tmpfile}" EXIT + rsync -ni -aHAX "$1" "$2" > "${tmpfile}" + if test $(wc -l < "${tmpfile}") != 0; then + echo "ERROR: detected differences between $1 and $2" + cat "${tmpfile}" + false + fi + ) +} + test-src-create-extract() { # Test bup meta create/extract for ./src -> ./src-restore. @@ -69,17 +85,40 @@ test-src-create-extract() ) } +test-src-save-restore() +{ + # Test bup save/restore metadata for ./src -> ./src-restore. Also + # writes to ./src.bup. Note that for now this just tests the + # restore below src/, in order to avoid having to worry about + # operations that require root (like chown /home). + ( + set -x + rm -rf src.bup + mkdir src.bup + export BUP_DIR=$(pwd)/src.bup + WVPASS bup init + WVPASS bup index src + WVPASS bup save -t -n src src + # Test extract. + force-delete src-restore + mkdir src-restore + WVPASS bup restore -C src-restore "/src/latest$(pwd)/" + WVPASS test -d src-restore/src + WVPASS compare-trees src/ src-restore/src/ + rm -rf src.bup + set +x + ) +} + if actually-root; then umount "$TOP/bupmeta.tmp/testfs" || true fi -force-delete "$BUP_DIR" -force-delete "$TOP/bupmeta.tmp" - -# Create a test tree. +setup-test-tree() ( set -e - rm -rf "$TOP/bupmeta.tmp/src" + force-delete "$BUP_DIR" + force-delete "$TOP/bupmeta.tmp" mkdir -p "$TOP/bupmeta.tmp/src" cp -pPR Documentation cmd lib t "$TOP/bupmeta.tmp"/src @@ -94,8 +133,9 @@ force-delete "$TOP/bupmeta.tmp" ) || WVFAIL # Use the test tree to check bup meta. -WVSTART 'meta - general' +WVSTART 'meta --create/--extract' ( + setup-test-tree cd "$TOP/bupmeta.tmp" test-src-create-extract @@ -107,7 +147,211 @@ WVSTART 'meta - general' WVPASS bup meta -xf ../src-file.meta ) -# Root-only tests: ACLs, Linux attr, Linux xattr, etc. +# Use the test tree to check bup save/restore metadata. +WVSTART 'metadata save/restore (general)' +( + setup-test-tree + cd "$TOP/bupmeta.tmp" + test-src-save-restore +) + +WVSTART 'meta --edit' +( + force-delete "$TOP/bupmeta.tmp" + mkdir "$TOP/bupmeta.tmp" + cd "$TOP/bupmeta.tmp" + mkdir src + WVPASS bup meta -cf src.meta src + + WVPASS bup meta --edit --set-uid 0 src.meta | WVPASS bup meta -tvvf - \ + | WVPASS grep -qE '^uid: 0' + WVPASS bup meta --edit --set-uid 1000 src.meta | WVPASS bup meta -tvvf - \ + | WVPASS grep -qE '^uid: 1000' + + WVPASS bup meta --edit --set-gid 0 src.meta | WVPASS bup meta -tvvf - \ + | WVPASS grep -qE '^gid: 0' + WVPASS bup meta --edit --set-gid 1000 src.meta | WVPASS bup meta -tvvf - \ + | WVPASS grep -qE '^gid: 1000' + + WVPASS bup meta --edit --set-user foo src.meta | WVPASS bup meta -tvvf - \ + | WVPASS grep -qE '^user: foo' + WVPASS bup meta --edit --set-user bar src.meta | WVPASS bup meta -tvvf - \ + | WVPASS grep -qE '^user: bar' + WVPASS bup meta --edit --unset-user src.meta | WVPASS bup meta -tvvf - \ + | WVPASS grep -qE '^user:' + WVPASS bup meta --edit --set-user bar --unset-user src.meta \ + | WVPASS bup meta -tvvf - | WVPASS grep -qE '^user:' + WVPASS bup meta --edit --unset-user --set-user bar src.meta \ + | WVPASS bup meta -tvvf - | WVPASS grep -qE '^user: bar' + + WVPASS bup meta --edit --set-group foo src.meta | WVPASS bup meta -tvvf - \ + | WVPASS grep -qE '^group: foo' + WVPASS bup meta --edit --set-group bar src.meta | WVPASS bup meta -tvvf - \ + | WVPASS grep -qE '^group: bar' + WVPASS bup meta --edit --unset-group src.meta | WVPASS bup meta -tvvf - \ + | WVPASS grep -qE '^group:' + WVPASS bup meta --edit --set-group bar --unset-group src.meta \ + | WVPASS bup meta -tvvf - | WVPASS grep -qE '^group:' + WVPASS bup meta --edit --unset-group --set-group bar src.meta \ + | WVPASS bup meta -tvvf - | grep -qE '^group: bar' +) + +# Test ownership restoration (when not root or fakeroot). +( + if test "$(whoami)" == root; then + exit 0 + fi + + WVSTART 'metadata (restoration of ownership)' + force-delete "$TOP/bupmeta.tmp" + mkdir "$TOP/bupmeta.tmp" + cd "$TOP/bupmeta.tmp" + touch src + WVPASS bup meta -cf src.meta src + + mkdir dest + cd dest + # Make sure we don't change (or try to change) the user when not root. + WVPASS bup meta --edit --set-user root ../src.meta | WVPASS bup meta -x + WVPASS bup xstat src | WVPASS grep -qvE '^user: root' + rm -rf src + WVPASS bup meta --edit --unset-user --set-uid 0 ../src.meta \ + | WVPASS bup meta -x + WVPASS bup xstat src | grep -qvE '^user: root' + + # Make sure we can restore one of the user's groups. + user_groups="$(groups)" + last_group="$(echo ${user_groups/* /})" + rm -rf src + WVPASS bup meta --edit --set-group "$last_group" ../src.meta \ + | WVPASS bup meta -x + WVPASS bup xstat src | WVPASS grep -qE "^group: $last_group" + + # Make sure we can restore one of the user's gids. + user_gids="$(id -G)" + last_gid="$(echo ${user_gids/* /})" + rm -rf src + WVPASS bup meta --edit --unset-group --set-gid "$last_gid" ../src.meta \ + | WVPASS bup meta -x + WVPASS bup xstat src | WVPASS grep -qE "^gid: $last_gid" + + # Test --numeric-ids (gid). + rm -rf src + current_gidx=$(bup meta -tvvf ../src.meta | grep -e '^gid:') + WVPASS bup meta --edit --set-group "$last_group" ../src.meta \ + | WVPASS bup meta -x --numeric-ids + new_gidx=$(bup xstat src | grep -e '^gid:') + WVPASSEQ "$current_gidx" "$new_gidx" + + # Test that restoring an unknown user works. + unknown_user=$("$TOP"/t/unknown-owner --user) + rm -rf src + current_uidx=$(bup meta -tvvf ../src.meta | grep -e '^uid:') + WVPASS bup meta --edit --set-user "$unknown_user" ../src.meta \ + | WVPASS bup meta -x + new_uidx=$(bup xstat src | grep -e '^uid:') + WVPASSEQ "$current_uidx" "$new_uidx" + + # Test that restoring an unknown group works. + unknown_group=$("$TOP"/t/unknown-owner --group) + rm -rf src + current_gidx=$(bup meta -tvvf ../src.meta | grep -e '^gid:') + WVPASS bup meta --edit --set-group "$unknown_group" ../src.meta \ + | WVPASS bup meta -x + new_gidx=$(bup xstat src | grep -e '^gid:') + WVPASSEQ "$current_gidx" "$new_gidx" +) + +# Test ownership restoration (when root or fakeroot). +( + if test "$(whoami)" != root; then + exit 0 + fi + + WVSTART 'metadata (restoration of ownership as root)' + force-delete "$TOP/bupmeta.tmp" + mkdir "$TOP/bupmeta.tmp" + cd "$TOP/bupmeta.tmp" + touch src + WVPASS bup meta -cf src.meta src + + mkdir dest + chmod 700 dest # so we can't accidentally do something insecure + cd dest + + # Make sure we can restore a uid. + WVPASS bup meta --edit --unset-user --set-uid 42 ../src.meta \ + | WVPASS bup meta -x + WVPASS bup xstat src | WVPASS grep -qE '^uid: 42' + + # Make sure we can restore a gid. + WVPASS bup meta --edit --unset-group --set-gid 42 ../src.meta \ + | WVPASS bup meta -x + WVPASS bup xstat src | WVPASS grep -qE '^gid: 42' + + some_user=$("$TOP"/t/some-owner --user) + some_group=$("$TOP"/t/some-owner --group) + + # Try to restore a user (and see that user trumps uid when uid is not 0). + WVPASS bup meta --edit --set-uid 42 --set-user "$some_user" ../src.meta \ + | WVPASS bup meta -x + WVPASS bup xstat src | WVPASS grep -qE "^user: $some_user" + + # Try to restore a group (and see that group trumps gid when gid is not 0). + WVPASS bup meta --edit --set-gid 42 --set-group "$some_group" ../src.meta \ + | WVPASS bup meta -x + WVPASS bup xstat src | WVPASS grep -qE "^group: $some_user" + + # Make sure a uid of 0 trumps a non-root user. + WVPASS bup meta --edit --set-user "$some_user" ../src.meta \ + | WVPASS bup meta -x + WVPASS bup xstat src | WVPASS grep -qvE "^user: $some_user" + WVPASS bup xstat src | WVPASS grep -qE "^uid: 0" + + # Make sure a gid of 0 trumps a non-root group. + WVPASS bup meta --edit --set-group "$some_user" ../src.meta \ + | WVPASS bup meta -x + WVPASS bup xstat src | WVPASS grep -qvE "^group: $some_group" + WVPASS bup xstat src | WVPASS grep -qE "^gid: 0" + + # Test --numeric-ids (gid). Note the name 'root' is not handled + # specially, so we use that here as the test group name. We + # assume that the root group's gid is never 42. + rm -rf src + WVPASS bup meta --edit --set-group root --set-gid 42 ../src.meta \ + | WVPASS bup meta -x --numeric-ids + new_gidx=$(bup xstat src | grep -e '^gid:') + WVPASSEQ "$new_gidx" 'gid: 42' + + # Test --numeric-ids (uid). Note the name 'root' is not handled + # specially, so we use that here as the test user name. We assume + # that the root user's uid is never 42. + rm -rf src + WVPASS bup meta --edit --set-user root --set-uid 42 ../src.meta \ + | WVPASS bup meta -x --numeric-ids + new_uidx=$(bup xstat src | grep -e '^uid:') + WVPASSEQ "$new_uidx" 'uid: 42' + + # Test that restoring an unknown user works. + unknown_user=$("$TOP"/t/unknown-owners --user) + rm -rf src + WVPASS bup meta --edit --set-uid 42 --set-user "$unknown_user" ../src.meta \ + | WVPASS bup meta -x + new_uidx=$(bup xstat src | grep -e '^uid:') + WVPASSEQ "$new_uidx" 'uid: 42' + + # Test that restoring an unknown group works. + unknown_group=$("$TOP"/t/unknown-owners --group) + rm -rf src + WVPASS bup meta --edit \ + --set-gid 42 --set-group "$unknown_group" ../src.meta \ + | WVPASS bup meta -x + new_gidx=$(bup xstat src | grep -e '^gid:') + WVPASSEQ "$new_gidx" 'gid: 42' +) + +# Root-only tests that require an FS with all the trimmings: ACLs, +# Linux attr, Linux xattr, etc. if actually-root; then ( # These tests are only likely to work under Linux for now @@ -123,7 +367,9 @@ if actually-root; then trap cleanup_at_exit EXIT WVSTART 'meta - general (as root)' - WVPASS cd "$TOP/bupmeta.tmp" + setup-test-tree + cd "$TOP/bupmeta.tmp" + umount testfs || true dd if=/dev/zero of=testfs.img bs=1M count=32 mke2fs -F -j -m 0 testfs.img diff --git a/t/unknown-owner b/t/unknown-owner new file mode 100755 index 0000000..212d146 --- /dev/null +++ b/t/unknown-owner @@ -0,0 +1,22 @@ +#!/usr/bin/env python + +import grp +import pwd +import sys + +def usage(): + print >> sys.stderr, "Usage: unknown-owners (--user | --group)" + +if len(sys.argv) != 2: + usage() + sys.exit(1) + +if sys.argv[1] == '--user': + max_name_len = max([len(x.pw_name) for x in pwd.getpwall()]) +elif sys.argv[1] == '--group': + max_name_len = max([len(x.gr_name) for x in grp.getgrall()]) +else: + usage() + sys.exit(1) + +print 'x' * (max_name_len + 1) -- 2.39.2