From faf2b8f85c7d06021c5386c3aba0c3f16fb44a26 Mon Sep 17 00:00:00 2001 From: owsla Date: Wed, 2 Jul 2008 18:03:40 +0000 Subject: Support for Windows ACLs. (Patch from Josh Nisly and Fred Gansevles) git-svn-id: http://svn.savannah.nongnu.org/svn/rdiff-backup/trunk@904 2b77aa54-bcbc-44c9-a7ec-4f6cf2b41109 --- rdiff-backup/rdiff_backup/Globals.py | 6 + rdiff-backup/rdiff_backup/connection.py | 6 +- rdiff-backup/rdiff_backup/fs_abilities.py | 34 ++++- rdiff-backup/rdiff_backup/metadata.py | 37 +++++- rdiff-backup/rdiff_backup/rpath.py | 38 +++++- rdiff-backup/rdiff_backup/win_acls.py | 199 ++++++++++++++++++++++++++++++ 6 files changed, 310 insertions(+), 10 deletions(-) create mode 100644 rdiff-backup/rdiff_backup/win_acls.py (limited to 'rdiff-backup/rdiff_backup') diff --git a/rdiff-backup/rdiff_backup/Globals.py b/rdiff-backup/rdiff_backup/Globals.py index 740127c..ffccc27 100644 --- a/rdiff-backup/rdiff_backup/Globals.py +++ b/rdiff-backup/rdiff_backup/Globals.py @@ -85,6 +85,12 @@ acls_active = None acls_write = None acls_conn = None +# Like the above, but applies to support of Windows +# access control lists. +win_acls_active = None +win_acls_write = None +win_acls_conn = None + # Like above two setting groups, but applies to support of Mac OS X # style resource forks. resource_forks_active = None diff --git a/rdiff-backup/rdiff_backup/connection.py b/rdiff-backup/rdiff_backup/connection.py index 0ba1204..34aebae 100644 --- a/rdiff-backup/rdiff_backup/connection.py +++ b/rdiff-backup/rdiff_backup/connection.py @@ -27,7 +27,8 @@ try: import xattr except ImportError: pass try: import posix1e except ImportError: pass - +try: import win32security +except ImportError: pass class ConnectionError(Exception): pass class ConnectionReadError(ConnectionError): pass @@ -539,6 +540,9 @@ import Globals, Time, Rdiff, Hardlink, FilenameMapping, C, Security, \ TempFile, SetConnections, librsync, log, regress, fs_abilities, \ eas_acls, user_group, compare +try: import win_acls +except ImportError: pass + Globals.local_connection = LocalConnection() Globals.connections.append(Globals.local_connection) # Following changed by server in SetConnections diff --git a/rdiff-backup/rdiff_backup/fs_abilities.py b/rdiff-backup/rdiff_backup/fs_abilities.py index 9d125b1..286731e 100644 --- a/rdiff-backup/rdiff_backup/fs_abilities.py +++ b/rdiff-backup/rdiff_backup/fs_abilities.py @@ -29,7 +29,7 @@ FSAbilities object describing it. import errno, os import Globals, log, TempFile, selection, robust, SetConnections, \ - static, FilenameMapping + static, FilenameMapping, win_acls class FSAbilities: """Store capabilities of given file system""" @@ -39,6 +39,7 @@ class FSAbilities: ownership = None # True if chown works on this filesystem acls = None # True if access control lists supported eas = None # True if extended attributes supported + win_acls = None # True if windows access control lists supported hardlinks = None # True if hard linking supported fsync_dirs = None # True if directories can be fsync'd dir_inc_perms = None # True if regular files can have full permissions @@ -97,6 +98,7 @@ class FSAbilities: self.win_reserved_filenames)]) add_boolean_list([('Access control lists', self.acls), ('Extended attributes', self.eas), + ('Windows access control lists', self.win_acls), ('Case sensitivity', self.case_sensitive), ('Escape DOS devices', self.escape_dos_devices), ('Mac OS X style resource forks', @@ -120,6 +122,7 @@ class FSAbilities: self.read_only = 1 self.set_eas(rp, 0) self.set_acls(rp) + self.set_win_acls(rp) self.set_resource_fork_readonly(rp) self.set_carbonfile() self.set_case_sensitive_readonly(rp) @@ -151,6 +154,7 @@ class FSAbilities: self.set_fsync_dirs(subdir) self.set_eas(subdir, 1) self.set_acls(subdir) + self.set_win_acls(subdir) self.set_dir_inc_perms(subdir) self.set_resource_fork_readwrite(subdir) self.set_carbonfile() @@ -364,6 +368,24 @@ class FSAbilities: self.eas = 0 else: self.eas = 1 + def set_win_acls(self, dir_rp): + """Test if windows access control lists are supported""" + try: + import win32security + except ImportError: + log.Log("Unable to import win32security module. Windows ACLs\n" + "not supported by filesystem at %s" % dir_rp.path, 4) + self.win_acls = 0 + return + try: + win_acls.init_acls() + except OSError: + log.Log("Windows ACLs not supported by filesystem\n" + "at %s" % dir_rp.path, 4) + self.win_acls = 0 + return + self.win_acls = 1 + def set_dir_inc_perms(self, rp): """See if increments can have full permissions like a directory""" test_rp = rp.append('dir_inc_check') @@ -521,6 +543,10 @@ class SetGlobals: log.Log.FatalError("--never-drop-acls specified, but ACL support\n" "missing from destination filesystem") + def set_win_acls(self): + self.update_triple(self.src_fsa.win_acls, self.dest_fsa.win_acls, + ('win_acls_active', 'win_acls_write', 'win_acls_conn')) + def set_resource_forks(self): self.update_triple(self.src_fsa.resource_forks, self.dest_fsa.resource_forks, @@ -729,6 +755,10 @@ class SingleSetGlobals(RestoreSetGlobals): def set_acls(self): self.update_triple(self.dest_fsa.acls, ('acls_active', 'acls_write', 'acls_conn')) + def set_win_acls(self): + self.update_triple(self.src_fsa.win_acls, self.dest_fsa.win_acls, + ('win_acls_active', 'win_acls_write', 'win_acls_conn')) + def set_resource_forks(self): self.update_triple(self.dest_fsa.resource_forks, ('resource_forks_active', @@ -754,6 +784,7 @@ def backup_set_globals(rpin, force): bsg = BackupSetGlobals(rpin.conn, Globals.rbdir.conn, src_fsa, dest_fsa) bsg.set_eas() bsg.set_acls() + bsg.set_win_acls() bsg.set_resource_forks() bsg.set_carbonfile() bsg.set_hardlinks() @@ -781,6 +812,7 @@ def restore_set_globals(rpout): rsg = RestoreSetGlobals(Globals.rbdir.conn, rpout.conn, src_fsa, dest_fsa) rsg.set_eas() rsg.set_acls() + rsg.set_win_acls() rsg.set_resource_forks() rsg.set_carbonfile() rsg.set_hardlinks() diff --git a/rdiff-backup/rdiff_backup/metadata.py b/rdiff-backup/rdiff_backup/metadata.py index c20092b..1bbc5c6 100644 --- a/rdiff-backup/rdiff_backup/metadata.py +++ b/rdiff-backup/rdiff_backup/metadata.py @@ -433,9 +433,10 @@ class MetadataFile(FlatFile): class CombinedWriter: """Used for simultaneously writting metadata, eas, and acls""" - def __init__(self, metawriter, eawriter, aclwriter): + def __init__(self, metawriter, eawriter, aclwriter, winaclwriter): self.metawriter = metawriter - self.eawriter, self.aclwriter = eawriter, aclwriter # these can be None + self.eawriter, self.aclwriter, self.winaclwriter = \ + eawriter, aclwriter, winaclwriter # these can be None def write_object(self, rorp): """Write information in rorp to all the writers""" @@ -444,11 +445,14 @@ class CombinedWriter: self.eawriter.write_object(rorp.get_ea()) if self.aclwriter and not rorp.get_acl().is_basic(): self.aclwriter.write_object(rorp.get_acl()) + if self.winaclwriter: + self.winaclwriter.write_object(rorp.get_win_acl()) def close(self): self.metawriter.close() if self.eawriter: self.eawriter.close() if self.aclwriter: self.aclwriter.close() + if self.winaclwriter: self.winaclwriter.close() class Manager: @@ -456,6 +460,7 @@ class Manager: meta_prefix = 'mirror_metadata' acl_prefix = 'access_control_lists' ea_prefix = 'extended_attributes' + wacl_prefix = 'win_access_control_lists' def __init__(self): """Set listing of rdiff-backup-data dir""" @@ -501,6 +506,11 @@ class Manager: return self._iter_helper(self.acl_prefix, eas_acls.AccessControlListFile, time, restrict_index) + def get_win_acls_at_time(self, time, restrict_index): + """Return WACLs iter at given time from recordfile (or None)""" + return self._iter_helper(self.wacl_prefix, + win_acls.WinAccessControlListFile, time, restrict_index) + def GetAtTime(self, time, restrict_index = None): """Return combined metadata iter with ea/acl info if necessary""" cur_iter = self.get_meta_at_time(time, restrict_index) @@ -521,6 +531,14 @@ class Manager: log.Log("Warning: Extended Attributes file not found", 2) ea_iter = iter([]) cur_iter = eas_acls.join_ea_iter(cur_iter, ea_iter) + if Globals.win_acls_active: + wacl_iter = self.get_win_acls_at_time(time, restrict_index) + if not wacl_iter: + log.Log("Warning: Windows Access Control List file not" + " found.", 2) + wacl_iter = iter([]) + cur_iter = win_acls.join_wacl_iter(cur_iter, wacl_iter) + return cur_iter def _writer_helper(self, prefix, flatfileclass, typestr, time): @@ -548,17 +566,26 @@ class Manager: return self._writer_helper(self.acl_prefix, eas_acls.AccessControlListFile, typestr, time) + def get_win_acl_writer(self, typestr, time): + """Return WinAccessControlListFile opened for writing""" + return self._writer_helper(self.wacl_prefix, + win_acls.WinAccessControlListFile, typestr, time) + def GetWriter(self, typestr = 'snapshot', time = None): """Get a writer object that can write meta and possibly acls/eas""" metawriter = self.get_meta_writer(typestr, time) - if not Globals.eas_active and not Globals.acls_active: + if not Globals.eas_active and not Globals.acls_active and \ + not Globals.win_acls_active: return metawriter # no need for a CombinedWriter if Globals.eas_active: ea_writer = self.get_ea_writer(typestr, time) else: ea_writer = None if Globals.acls_active: acl_writer = self.get_acl_writer(typestr, time) else: acl_writer = None - return CombinedWriter(metawriter, ea_writer, acl_writer) + if Globals.win_acls_active: win_acl_writer = \ + self.get_win_acl_writer(typestr, time) + else: win_acl_writer = None + return CombinedWriter(metawriter, ea_writer, acl_writer, win_acl_writer) class PatchDiffMan(Manager): """Contains functions for patching and diffing metadata @@ -663,4 +690,4 @@ def SetManager(): return ManagerObj -import eas_acls # put at bottom to avoid python circularity bug +import eas_acls, win_acls # put at bottom to avoid python circularity bug diff --git a/rdiff-backup/rdiff_backup/rpath.py b/rdiff-backup/rdiff_backup/rpath.py index 7cdaa8d..2acfc32 100644 --- a/rdiff-backup/rdiff_backup/rpath.py +++ b/rdiff-backup/rdiff_backup/rpath.py @@ -185,6 +185,7 @@ def copy_attribs(rpin, rpout): rpout.chmod(rpin.getperms()) if Globals.acls_write: rpout.write_acl(rpin.get_acl()) if not rpin.isdev(): rpout.setmtime(rpin.getmtime()) + if Globals.win_acls_write: rpout.write_win_acl(rpin.get_win_acl()) def copy_attribs_inc(rpin, rpout): """Change file attributes of rpout to match rpin @@ -358,6 +359,7 @@ class RORPath: elif key == 'size' and not self.isreg(): pass elif key == 'ea' and not Globals.eas_active: pass elif key == 'acl' and not Globals.acls_active: pass + elif key == 'win_acl' and not Globals.win_acls_active: pass elif key == 'carbonfile' and not Globals.carbonfile_active: pass elif key == 'resourcefork' and not Globals.resource_forks_active: pass @@ -398,6 +400,7 @@ class RORPath: elif key == 'inode': pass elif key == 'ea' and not Globals.eas_write: pass elif key == 'acl' and not Globals.acls_write: pass + elif key == 'win_acl' and not Globals.win_acls_write: pass elif key == 'carbonfile' and not Globals.carbonfile_write: pass elif key == 'resourcefork' and not Globals.resource_forks_write: pass @@ -415,8 +418,8 @@ class RORPath: def equal_verbose(self, other, check_index = 1, compare_inodes = 0, compare_ownership = 0, - compare_acls = 0, compare_eas = 0, compare_size = 1, - compare_type = 1, verbosity = 2): + compare_acls = 0, compare_eas = 0, compare_win_acls = 0, + compare_size = 1, compare_type = 1, verbosity = 2): """Like __eq__, but log more information. Useful when testing""" if check_index and self.index != other.index: log.Log("Index %s != index %s" % (self.index, other.index), @@ -437,6 +440,7 @@ class RORPath: pass elif key == 'ea' and not compare_eas: pass elif key == 'acl' and not compare_acls: pass + elif key == 'win_acl' and not compare_win_acls: pass elif (not other.data.has_key(key) or self.data[key] != other.data[key]): if not other.data.has_key(key): @@ -454,7 +458,8 @@ class RORPath: return self.equal_verbose(other, compare_inodes = compare_inodes, compare_eas = Globals.eas_active, - compare_acls = Globals.acls_active) + compare_acls = Globals.acls_active, + compare_win_acls = Globals.win_acls_active) def __ne__(self, other): return not self.__eq__(other) @@ -702,6 +707,17 @@ class RORPath: """Record resource fork in dictionary. Does not write""" self.data['resourcefork'] = rfork + def set_win_acl(self, acl): + """Record Windows access control list in dictionary. Does not write""" + self.data['win_acl'] = acl + + def get_win_acl(self): + """Return access control list object from dictionary""" + try: return self.data['win_acl'] + except KeyError: + acl = self.data['win_acl'] = get_blank_win_acl(self.index) + return acl + def has_alt_mirror_name(self): """True if rorp has an alternate mirror name specified""" return self.data.has_key('mirrorname') @@ -1316,6 +1332,16 @@ class RPath(RORPath): assert not fp.close() self.set_resource_fork(rfork_data) + def get_win_acl(self): + """Return Windows access control list, setting if necessary""" + try: acl = self.data['win_acl'] + except KeyError: acl = self.data['win_acl'] = win_acl_get(self) + return acl + + def write_win_acl(self, acl): + """Change access control list of rp""" + write_win_acl(self, acl) + self.data['win_acl'] = acl class RPathFileHook: """Look like a file, but add closing hook""" @@ -1406,6 +1432,8 @@ def setdata_local(rpath): rpath.data['gname'] = user_group.gid2gname(rpath.data['gid']) if Globals.eas_conn: rpath.data['ea'] = ea_get(rpath) if Globals.acls_conn: rpath.data['acl'] = acl_get(rpath) + if Globals.win_acls_conn: + rpath.data['win_acl'] = win_acl_get(rpath) if Globals.resource_forks_conn and rpath.isreg(): rpath.get_resource_fork() if Globals.carbonfile_conn and rpath.isreg(): @@ -1439,3 +1467,7 @@ def acl_get(rp): assert 0 def get_blank_acl(index): assert 0 def ea_get(rp): assert 0 def get_blank_ea(index): assert 0 + +def win_acl_get(rp): assert 0 +def write_win_acl(rp): assert 0 +def get_blank_win_acl(): assert 0 diff --git a/rdiff-backup/rdiff_backup/win_acls.py b/rdiff-backup/rdiff_backup/win_acls.py new file mode 100644 index 0000000..1e95533 --- /dev/null +++ b/rdiff-backup/rdiff_backup/win_acls.py @@ -0,0 +1,199 @@ +# Copyright 2008 Fred Gansevles +# +# This file is part of rdiff-backup. +# +# rdiff-backup is free software; you can redistribute it and/or modify +# under the terms of the GNU General Public License as published by the +# Free Software Foundation; either version 2 of the License, or (at your +# option) any later version. +# +# rdiff-backup is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with rdiff-backup; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA + +import C, metadata, re, rorpiter, rpath + +try: + from win32security import * +except: + GROUP_SECURITY_INFORMATION = 0 + OWNER_SECURITY_INFORMATION = 0 + DACL_SECURITY_INFORMATION = 0 + +class ACL: + flags = (GROUP_SECURITY_INFORMATION| + OWNER_SECURITY_INFORMATION| + DACL_SECURITY_INFORMATION) + + def __init__(self, index=()): + self.__acl = "" + self.index = index + + def get_indexpath(self): return self.index and '/'.join(self.index) or '.' + + def load_from_rp(self, rp, skip_inherit_only = True): + self.index = rp.index + try: + sd = rp.conn.win32security.GetFileSecurity(rp.path, ACL.flags) + except: + return + + if skip_inherit_only: + # skip the inherit_only aces + acl = sd.GetSecurityDescriptorDacl() + if acl: + n = acl.GetAceCount() + # traverse the ACL in reverse, so the indices stay correct + while n: + n -= 1 + ace_flags = acl.GetAce(n)[0][1] + if ace_flags & INHERIT_ONLY_ACE: + acl.DeleteAce(n) + sd.SetSecurityDescriptorDacl(1, acl, 0) + + if ACL.flags & SACL_SECURITY_INFORMATION: + acl = sd.GetSecurityDescriptorSacl() + if acl: + n = acl.GetAceCount() + # traverse the ACL in reverse, so the indices stay correct + while n: + n -= 1 + ace_flags = acl.GetAce(n)[0][1] + if ace_flags & INHERIT_ONLY_ACE: + acl.DeleteAce(n) + sd.SetSecurityDescriptorSacl(1, acl, 0) + + self.__acl = \ + rp.conn.win32security.ConvertSecurityDescriptorToStringSecurityDescriptor(sd, + SDDL_REVISION_1, ACL.flags) + + def clear_rp(self, rp): + # not sure how to interpret this + # I'll jus clear all acl-s from rp.path + sd = rp.conn.win32security.GetFileSecurity(rp.path, ACL.flags) + + acl = sd.GetSecurityDescriptorDacl() + if acl: + n = acl.GetAceCount() + # traverse the ACL in reverse, so the indices stay correct + while n: + n -= 1 + acl.DeleteAce(n) + sd.SetSecurityDescriptorDacl(1, acl, 0) + + if ACL.flags & SACL_SECURITY_INFORMATION: + acl = sd.GetSecurityDescriptorSacl() + if acl: + n = acl.GetAceCount() + # traverse the ACL in reverse, so the indices stay correct + while n: + n -= 1 + acl.DeleteAce(n) + sd.SetSecurityDescriptorSacl(1, acl, 0) + + SetFileSecurity(rp.path, ACL.flags, sd) + + def write_to_rp(self, rp): + if self.__acl: + sd = rp.conn.win32security.ConvertStringSecurityDescriptorToSecurityDescriptor(self.__acl, + SDDL_REVISION_1) + rp.conn.win32security.SetFileSecurity(rp.path, ACL.flags, sd) + + def __str__(self): + return '# file: %s\n%s\n' % \ + (C.acl_quote(self.get_indexpath()), unicode(self.__acl)) + + def from_string(self, acl_str): + lines = acl_str.splitlines() + if len(lines) != 2 or not lines[0][:8] == "# file: ": + raise metadata.ParsingError("Bad record beginning: " + lines[0][:8]) + filename = lines[0][8:] + if filename == '.': self.index = () + else: self.index = tuple(C.acl_unquote(filename).split('/')) + self.__acl = lines[1] + +def Record2WACL(record): + acl = ACL() + acl.from_string(record) + return acl + +def WACL2Record(wacl): + return unicode(wacl) + +class WACLExtractor(metadata.FlatExtractor): + """Iterate ExtendedAttributes objects from the WACL information file""" + record_boundary_regexp = re.compile('(?:\\n|^)(# file: (.*?))\\n') + record_to_object = staticmethod(Record2WACL) + def filename_to_index(self, filename): + """Convert possibly quoted filename to index tuple""" + if filename == '.': return () + else: return tuple(C.acl_unquote(filename).split('/')) + +class WinAccessControlListFile(metadata.FlatFile): + """Store/retrieve ACLs from extended_attributes file""" + _prefix = "win_access_control_lists" + _extractor = WACLExtractor + _object_to_record = staticmethod(WACL2Record) + +def join_wacl_iter(rorp_iter, wacl_iter): + """Update a rorp iter by adding the information from acl_iter""" + for rorp, wacl in rorpiter.CollateIterators(rorp_iter, wacl_iter): + assert rorp, "Missing rorp for index %s" % (wacl.index,) + if not wacl: wacl = ACL(rorp.index) + rorp.set_win_acl(unicode(wacl)) + yield rorp + +def rpath_acl_win_get(rpath): + acl = ACL() + acl.load_from_rp(rpath) + return unicode(acl) +rpath.win_acl_get = rpath_acl_win_get + +def rpath_get_blank_win_acl(index): + acl = ACL(index) + return unicode(acl) +rpath.get_blank_win_acl = rpath_get_blank_win_acl + +def rpath_set_win_acl(rp, acl_str): + acl = ACL() + acl.from_string(acl_str) + acl.write_to_rp(rp) +rpath.write_win_acl = rpath_set_win_acl + +def init_acls(): + # A process that tries to read or write a SACL needs + # to have and enable the SE_SECURITY_NAME privilege. + # And inorder to backup/restore, the SE_BACKUP_NAME and + # SE_RESTORE_NAME privileges are needed. + import win32api + try: + hnd = OpenProcessToken(win32api.GetCurrentProcess(), + TOKEN_ADJUST_PRIVILEGES| TOKEN_QUERY) + except win32api.error: + return + try: + try: + lpv = lambda priv: LookupPrivilegeValue(None, priv) + # enable the SE_*_NAME priveleges + SecurityName = lpv(SE_SECURITY_NAME) + AdjustTokenPrivileges(hnd, False, [ + (SecurityName, SE_PRIVILEGE_ENABLED), + (lpv(SE_BACKUP_NAME), SE_PRIVILEGE_ENABLED), + (lpv(SE_RESTORE_NAME), SE_PRIVILEGE_ENABLED) + ]) + except win32api.error: + return + for name, enabled in GetTokenInformation(hnd, TokenPrivileges): + if name == SecurityName and enabled: + # now we *may* access the SACL (sigh) + ACL.flags |= SACL_SECURITY_INFORMATION + break + finally: + win32api.CloseHandle(hnd) + -- cgit v1.2.1