From bfd24043e3f728e960cf8c8829df36d799505106 Mon Sep 17 00:00:00 2001 From: bescoto Date: Thu, 18 Sep 2003 02:10:14 +0000 Subject: Added --never-drop-acls, ACL entry dropping. Final for 0.13.2 git-svn-id: http://svn.savannah.nongnu.org/svn/rdiff-backup/trunk@442 2b77aa54-bcbc-44c9-a7ec-4f6cf2b41109 --- rdiff-backup/CHANGELOG | 6 ++- rdiff-backup/TODO | 2 - rdiff-backup/rdiff-backup.1 | 6 +++ rdiff-backup/rdiff_backup/Globals.py | 3 ++ rdiff-backup/rdiff_backup/Main.py | 14 +++++-- rdiff-backup/rdiff_backup/eas_acls.py | 71 +++++++++++++++++++++++++++------ rdiff-backup/rdiff_backup/rpath.py | 16 +++++--- rdiff-backup/rdiff_backup/user_group.py | 28 +++++++++++++ rdiff-backup/testing/eas_aclstest.py | 71 ++++++++++++++++++++++++++++++++- 9 files changed, 191 insertions(+), 26 deletions(-) diff --git a/rdiff-backup/CHANGELOG b/rdiff-backup/CHANGELOG index 3b7546e..8daadba 100644 --- a/rdiff-backup/CHANGELOG +++ b/rdiff-backup/CHANGELOG @@ -1,9 +1,13 @@ -New in v0.13.2 (??????????) +New in v0.13.2 (2003/09/16) --------------------------- Change ownership policy and added --user-mapping-file and --group-mapping-file switches. See man page for more information. +Added option --never-drop-acls to cause fatal error instead of +dropping any acls or acl entries. Thanks to Greg Freemyer for +suggestion. + Specified socket type as SOCK_STREAM. (Error reported by Erik Forsberg.) diff --git a/rdiff-backup/TODO b/rdiff-backup/TODO index e958f5e..0ddc302 100644 --- a/rdiff-backup/TODO +++ b/rdiff-backup/TODO @@ -10,8 +10,6 @@ by Andrew Bressen. Profile 0.13.x -Look at EAs when unames change. - Examine regress handling of acls/eas/resource forks. Look into that strange regress error July 22nd on diff --git a/rdiff-backup/rdiff-backup.1 b/rdiff-backup/rdiff-backup.1 index 796d93a..44f0871 100644 --- a/rdiff-backup/rdiff-backup.1 +++ b/rdiff-backup/rdiff-backup.1 @@ -205,6 +205,12 @@ to --remove-older-than. Specifying a subdirectory is allowable; then only the sizes of the mirror and increments pertaining to that subdirectory will be listed. .TP +.B --never-drop-acls +Exit with error instead of dropping acls or acl entries. Normally +this may happen (with a warning) because the destination does not +support them or because the relevant user/group names do not exist on +the destination side. +.TP .B --no-compare-inode This relatively esoteric option prevents rdiff-backup from flagging a file as changed when its inode changes. This option may be useful if diff --git a/rdiff-backup/rdiff_backup/Globals.py b/rdiff-backup/rdiff_backup/Globals.py index e40fd0a..de334f7 100644 --- a/rdiff-backup/rdiff_backup/Globals.py +++ b/rdiff-backup/rdiff_backup/Globals.py @@ -207,6 +207,9 @@ compare_inode = 1 # guarantee that any changes have been committed to disk. fsync_directories = 1 +# If set, exit with error instead of dropping ACLs or ACL entries. +never_drop_acls = None + def get(name): """Return the value of something in this module""" diff --git a/rdiff-backup/rdiff_backup/Main.py b/rdiff-backup/rdiff_backup/Main.py index 68832d3..d1c7b97 100644 --- a/rdiff-backup/rdiff_backup/Main.py +++ b/rdiff-backup/rdiff_backup/Main.py @@ -57,9 +57,10 @@ def parse_cmdlineoptions(arglist): "include=", "include-filelist=", "include-filelist-stdin", "include-globbing-filelist=", "include-regexp=", "list-at-time=", "list-changed-since=", "list-increments", - "list-increment-sizes", "no-compare-inode", - "no-compression", "no-compression-regexp=", - "no-file-statistics", "no-hard-links", "null-separator", + "list-increment-sizes", "never-drop-acls", + "no-compare-inode", "no-compression", + "no-compression-regexp=", "no-file-statistics", + "no-hard-links", "null-separator", "override-chars-to-quote=", "parsable-output", "print-statistics", "remote-cmd=", "remote-schema=", "remove-older-than=", "restore-as-of=", "restrict=", @@ -109,6 +110,7 @@ def parse_cmdlineoptions(arglist): elif opt == "-l" or opt == "--list-increments": action = "list-increments" elif opt == '--list-increment-sizes': action = 'list-increment-sizes' + elif opt == "--never-drop-acls": Globals.set("never_drop_acls", 1) elif opt == "--no-compare-inode": Globals.set("compare_inode", 0) elif opt == "--no-compression": Globals.set("compression", None) elif opt == "--no-compression-regexp": @@ -368,6 +370,9 @@ def backup_set_fs_globals(rpin, rpout): dest_fsa = rpout.conn.fs_abilities.get_fsabilities_readwrite( 'destination', Globals.rbdir, 1, Globals.chars_to_quote) Log(str(dest_fsa), 3) + if Globals.never_drop_acls and not dest_fsa.acls: + Log.FatalError("--never-drop-acls specified, but ACL support\n" + "disabled on destination filesystem") update_bool_global('read_acls', src_fsa.acls) update_bool_global('read_eas', src_fsa.eas) @@ -462,6 +467,9 @@ def restore_set_fs_globals(target): mirror_fsa = Globals.rbdir.conn.fs_abilities.get_fsabilities_restoresource( Globals.rbdir) Log(str(mirror_fsa), 3) + if Globals.never_drop_acls and not target_fsa.acls: + Log.FatalError("--never-drop-acls specified, but ACL support\n" + "disabled on destination filesystem") update_bool_global('read_acls', target_fsa.acls) update_bool_global('write_acls', target_fsa.acls) diff --git a/rdiff-backup/rdiff_backup/eas_acls.py b/rdiff-backup/rdiff_backup/eas_acls.py index 442bdbd..e8258ad 100644 --- a/rdiff-backup/rdiff_backup/eas_acls.py +++ b/rdiff-backup/rdiff_backup/eas_acls.py @@ -33,6 +33,11 @@ except ImportError: pass import static, Globals, metadata, connection, rorpiter, log, C, \ rpath, user_group +# When an ACL gets dropped, put name in dropped_acl_names. This is +# only used so that only the first dropped ACL for any given name +# triggers a warning. +dropped_acl_names = {} + class ExtendedAttributes: """Hold a file's extended attribute information""" def __init__(self, index, attr_dict = None): @@ -288,6 +293,7 @@ class AccessControlLists: """ assert isinstance(acl, self.__class__) if self.index != acl.index: return 0 + if self.is_basic(): return acl.is_basic() return (self.cmp_entry_list(self.entry_list, acl.entry_list) and self.cmp_entry_list(self.default_entry_list, acl.default_entry_list)) @@ -327,21 +333,23 @@ class AccessControlLists: self.entry_list, self.default_entry_list = \ rp.conn.eas_acls.get_acl_lists_from_rp(rp) - def write_to_rp(self, rp): + def write_to_rp(self, rp, map_names = 1): """Write current access control list to RPath rp""" rp.conn.eas_acls.set_rp_acl(rp, self.entry_list, - self.default_entry_list) + self.default_entry_list, map_names) -def set_rp_acl(rp, entry_list = None, default_entry_list = None): +def set_rp_acl(rp, entry_list = None, default_entry_list = None, + map_names = 1): """Set given rp with ACL that acl_text defines. rp should be local""" assert rp.conn is Globals.local_connection - if entry_list: acl = list_to_acl(entry_list) + if entry_list: acl = list_to_acl(entry_list, map_names) else: acl = posix1e.ACL() acl.applyto(rp.path) if rp.isdir(): - if default_entry_list: def_acl = list_to_acl(default_entry_list) + if default_entry_list: + def_acl = list_to_acl(default_entry_list, map_names) else: def_acl = posix1e.ACL() def_acl.applyto(rp.path, posix1e.ACL_TYPE_DEFAULT) @@ -388,18 +396,55 @@ def acl_to_list(acl): return (entry.tag_type, owner_pair, perms) return map(entry_to_tuple, acl) -def list_to_acl(entry_list): - """Return posix1e.ACL object from list representation""" +def list_to_acl(entry_list, map_names = 1): + """Return posix1e.ACL object from list representation + + If map_names is true, use user_group to update the names for the + current system, and drop if not available. Otherwise just use the + same id. + + """ + def warn_drop(name): + """Warn about acl with name getting dropped""" + global dropped_acl_names + if Globals.never_drop_acls: + log.Log.FatalError( +"--never-drop-acls specified but cannot map name\n" +"%s occurring inside an ACL." % (name,)) + if dropped_acl_names.has_key(name): return + log.Log("Warning: name %s not found on system, dropping ACL entry.\n" + "Further ACL entries dropped with this name will not " + "trigger further warnings" % (name,), 2) + dropped_acl_names[name] = name + + def map_id_name(owner_pair, group = None): + """Return id of mapped id and user given original owner_pair""" + id, name = owner_pair + Map = group and user_group.GroupMap or user_group.UserMap + if name: return Map.get_id_from_name(name) + else: + assert id is not None + return Map.get_id_from_id(id) + acl = posix1e.ACL() for tag, owner_pair, perms in entry_list: - entry = posix1e.Entry(acl) - entry.tag_type = tag + id = None if owner_pair: - if tag == posix1e.ACL_USER: - entry.qualifier = user_group.UserMap.get_id(*owner_pair) + if map_names: + if tag == posix1e.ACL_USER: id = map_id_name(owner_pair, 0) + else: + assert tag == posix1e.ACL_GROUP, (tag, owner_pair, perms) + id = map_id_name(owner_pair, 1) + if id is None: + warn_drop(owner_pair[1]) + continue else: - assert tag == posix1e.ACL_GROUP, (tag, owner_pair, perms) - entry.qualifier = user_group.GroupMap.get_id(*owner_pair) + assert owner_pair[0] is not None, (tag, owner_pair, perms) + id = owner_pair[0] + + entry = posix1e.Entry(acl) + entry.tag_type = tag + if id is not None: entry.qualifier = id entry.permset.read = perms >> 2 entry.permset.write = perms >> 1 & 1 entry.permset.execute = perms & 1 diff --git a/rdiff-backup/rdiff_backup/rpath.py b/rdiff-backup/rdiff_backup/rpath.py index de2d2f7..a834c5c 100644 --- a/rdiff-backup/rdiff_backup/rpath.py +++ b/rdiff-backup/rdiff_backup/rpath.py @@ -179,8 +179,7 @@ def copy_attribs_inc(rpin, rpout): if rpin.isdir() and not rpout.isdir(): rpout.chmod(rpin.getperms() & 0777) else: rpout.chmod(rpin.getperms()) - if Globals.write_acls and not (rpin.isdir() and not rpout.isdir()): - rpout.write_acl(rpin.get_acl()) + if Globals.write_acls: rpout.write_acl(rpin.get_acl(), map_names = 0) if not rpin.isdev(): rpout.setmtime(rpin.getmtime()) def cmp_attribs(rp1, rp2): @@ -302,7 +301,8 @@ class RORPath: not Globals.compare_inode or not Globals.preserve_hardlinks)): pass elif (not other.data.has_key(key) or - self.data[key] != other.data[key]): return None + self.data[key] != other.data[key]): + return None return 1 def equal_loose(self, other): @@ -1035,9 +1035,13 @@ class RPath(RORPath): except KeyError: acl = self.data['acl'] = acl_get(self) return acl - def write_acl(self, acl): - """Change access control list of rp""" - acl.write_to_rp(self) + def write_acl(self, acl, map_names = 1): + """Change access control list of rp + + If map_names is true, map the ids in acl by user/group names. + + """ + acl.write_to_rp(self, map_names) self.data['acl'] = acl def get_ea(self): diff --git a/rdiff-backup/rdiff_backup/user_group.py b/rdiff-backup/rdiff_backup/user_group.py index 896e60e..b34f0cf 100644 --- a/rdiff-backup/rdiff_backup/user_group.py +++ b/rdiff-backup/rdiff_backup/user_group.py @@ -60,6 +60,16 @@ def gid2gname(gid): gid2gname_dict[gid] = gname return gname +def uname2uid(uname): + """Given uname, return uid or None if cannot find""" + try: uname = pwd.getpwnam(uname)[2] + except KeyError: return None + +def gname2gid(gname): + """Given gname, return gid or None if cannot find""" + try: gname = grp.getgrnam(gname)[2] + except KeyError: return None + class Map: """Used for mapping names and id on source side to dest side""" @@ -77,12 +87,25 @@ class Map: self.name2id_dict[name] = out_id return out_id + def get_id_from_name(self, name): + """Return mapped id from name only, or None if cannot""" + try: return self.name2id_dict[name] + except KeyError: + out_id = self.find_id_from_name(name) + self.name2id_dict[name] = out_id + return out_id + def get_id_from_id(self, id): return id def find_id(self, id, name): """Find the proper id to use with given id and name""" try: return self.name2id_func(name) except KeyError: return id + + def find_id_from_name(self, name): + """Look up proper id to use with name, or None""" + try: return self.name2id_func(name) + except KeyError: return None class DefinedMap(Map): """Map names and ids on source side to appropriate ids on dest side @@ -134,6 +157,11 @@ class DefinedMap(Map): try: return self.id_mapping_dict[id] except KeyError: return Map.find_id(self, id, name) + def find_id_from_name(self, name): + """Find id to map name to, or None if we can't""" + try: return self.name_mapping_dict[name] + except KeyError: return Map.find_id_from_name(name) + def init_user_mapping(mapping_string = None): """Initialize user mapping with given mapping string or None""" global UserMap diff --git a/rdiff-backup/testing/eas_aclstest.py b/rdiff-backup/testing/eas_aclstest.py index 2ae565e..91e9d75 100644 --- a/rdiff-backup/testing/eas_aclstest.py +++ b/rdiff-backup/testing/eas_aclstest.py @@ -1,4 +1,4 @@ -import unittest, os, time, cStringIO +import unittest, os, time, cStringIO, posix1e, pwd, grp from commontest import * from rdiff_backup.eas_acls import * from rdiff_backup import Globals, rpath, Time, user_group @@ -422,6 +422,75 @@ other::--- assert CompareRecursive(self.acl_testdir2, restore_dir, compare_acls = 1) + def test_acl_mapping(self): + """Test mapping ACL names""" + def make_dir(rootrp): + if rootrp.lstat(): rootrp.delete() + rootrp.mkdir() + rp = rootrp.append('1') + rp.touch() + acl = AccessControlLists(('1',), """user::rwx +user:root:rwx +user:ben:--- +user:bin:r-- +group::r-x +group:root:r-x +group:ben:-w- +mask::r-x +other::---""") + rp.write_acl(acl) + return rp + + def write_mapping_file(rootrp): + map_rp = rootrp.append('mapping_file') + map_rp.write_string("root:ben\nben:bin\nbin:root") + return map_rp + + def get_perms_of_user(acl, user): + """Return the permissions of ACL_USER in acl, or None""" + for type, owner_pair, perms in acl.entry_list: + if type == posix1e.ACL_USER and owner_pair[1] == user: + return perms + return None + + self.make_temp() + rootrp = rpath.RPath(Globals.local_connection, + 'testfiles/acl_map_test') + rp = make_dir(rootrp) + map_rp = write_mapping_file(rootrp) + + rdiff_backup(1, 1, rootrp.path, tempdir.path, + extra_options = "--user-mapping-file %s" % (map_rp.path,)) + + out_rp = tempdir.append('1') + assert out_rp.isreg() + out_acl = tempdir.append('1').get_acl() + assert get_perms_of_user(out_acl, 'root') == 4 + assert get_perms_of_user(out_acl, 'ben') == 7 + assert get_perms_of_user(out_acl, 'bin') == 0 + + def test_acl_dropping(self): + """Test dropping of ACL names""" + self.make_temp() + rp = tempdir.append('1') + rp.touch() + acl = AccessControlLists(('1',), """user::rwx +user:aoensutheu:r-- +group::r-x +group:aeuai:r-x +group:enutohnh:-w- +other::---""") + rp.write_acl(acl) + rp2 = tempdir.append('1') + acl2 = AccessControlLists(('1',)) + acl2.read_from_rp(rp2) + assert acl2.is_basic() + Globals.never_drop_acls = 1 + try: rp.write_acl(acl) + except SystemExit: pass + else: assert 0, "Above should have exited with fatal error" + Globals.never_drop_acls = None + class CombinedTest(unittest.TestCase): """Test backing up and restoring directories with both EAs and ACLs""" -- cgit v1.2.1