rdiff-backup-users
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[rdiff-backup-users] [PATCH] Backing up Windows ACLs


From: Josh Nisly
Subject: [rdiff-backup-users] [PATCH] Backing up Windows ACLs
Date: Sun, 22 Jun 2008 11:25:28 +0600
User-agent: Thunderbird 2.0.0.14 (X11/20080505)

Attached is a first-draft patch to add support for native Windows permissions, including owner and ACLs.

First, let me say that Fred Gansevles is responsible for the most interesting part of this - the code to load and save permissions information to files. Thanks, Fred!

There are a couple of places where I've "cheated" with initializing ACL loading to True instead of determining whether ACLs are supported, but I wanted to get the patch out so that other people could take a look at it and comment on it. At this point, it should work for both local<->local and local<->remote, as long as the remote end has these changes.

Probably the most controversial change is make Windows ACLs completely separate in the code from POSIX ACLs, including saving them to a separate file, etc. The reason that I did this is because they are conceptually and practically two completely different things. If you back up from Windows and restore to linux, you don't want to try to apply the Windows ACLs to the linux filesystem, and vice-versa. I see them as similar to OS X's carbon files, that is, they are a os-specific file information.

The one thing I don't like about this patch is that it is not backwards compatible with existing rdiff-backup servers. This is because an RPath object now contains a win_acls.ACL object, which causes the remote end to throw when unpickling the object. I don't know of an easy way around this. One way would be to serialize the object to string, but I'm not sure I like that.

I'd like any comments anyone has on this patch.

Thanks,
JoshN
--- rdiff_backup/Globals.py     13 Apr 2008 11:25:21 -0000      1.45
+++ rdiff_backup/Globals.py     17 Jun 2008 11:26:15 -0000
@@ -85,6 +85,9 @@
 acls_write = None
 acls_conn = None
 
+win_acls_active = True
+win_acls_write = True
+
 # Like above two setting groups, but applies to support of Mac OS X
 # style resource forks.
 resource_forks_active = None
--- rdiff_backup/connection.py  9 Jul 2007 03:53:40 -0000       1.29
+++ rdiff_backup/connection.py  20 Jun 2008 10:40:26 -0000
@@ -27,7 +27,8 @@
 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 @@
           TempFile, SetConnections, librsync, log, regress, fs_abilities, \
           eas_acls, user_group, compare
 
+try: import win_acls
+except: pass
+
 Globals.local_connection = LocalConnection()
 Globals.connections.append(Globals.local_connection)
 # Following changed by server in SetConnections
--- rdiff_backup/fs_abilities.py        13 Apr 2008 11:25:22 -0000      1.45
+++ rdiff_backup/fs_abilities.py        20 Jun 2008 11:02:30 -0000
@@ -29,7 +29,7 @@
 
 import errno, os
 import Globals, log, TempFile, selection, robust, SetConnections, \
-          static, FilenameMapping
+          static, FilenameMapping, win_acls
 
 class FSAbilities:
        """Store capabilities of given file system"""
@@ -50,6 +50,7 @@
        escape_dos_devices = None # True if dos device files can't be created 
(e.g.,
                                                          # aux, con, com1, etc)
        symlink_perms = None # True if symlink perms are affected by umask
+       win_acls = None # True if windows access control lists supported
 
        def __init__(self, name = None):
                """FSAbilities initializer.  name is only used in logging"""
@@ -101,7 +102,8 @@
                                                  ('Escape DOS devices', 
self.escape_dos_devices),
                                                  ('Mac OS X style resource 
forks',
                                                   self.resource_forks),
-                                                 ('Mac OS X Finder 
information', self.carbonfile)])
+                                                 ('Mac OS X Finder 
information', self.carbonfile),
+                                                 ('Windows access control 
lists', self.win_acls)])
                s.append(s[0])
                return '\n'.join(s)
 
@@ -124,6 +126,7 @@
                self.set_carbonfile()
                self.set_case_sensitive_readonly(rp)
                self.set_escape_dos_devices(rp)
+               self.set_win_acls(rp)
                return self
 
        def init_readwrite(self, rbdir):
@@ -157,6 +160,7 @@
                self.set_high_perms_readwrite(subdir)
                self.set_symlink_perms(subdir)
                self.set_escape_dos_devices(subdir)
+               self.set_win_acls(subdir)
 
                subdir.delete()
                return self
@@ -488,6 +492,24 @@
                        log.Log("escape_dos_devices required by filesystem at 
%s" \
                                        % (subdir.path), 4)
                        self.escape_dos_devices = 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 get_readonly_fsa(desc_string, rp):
        """Return an fsa with given description_string
@@ -531,6 +553,10 @@
                self.update_triple(self.src_fsa.carbonfile, 
self.dest_fsa.carbonfile,
                          ('carbonfile_active', 'carbonfile_write', 
'carbonfile_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_hardlinks(self):
                if Globals.preserve_hardlinks != 0:
                        SetConnections.UpdateGlobal('preserve_hardlinks',
@@ -756,6 +782,7 @@
        bsg.set_acls()
        bsg.set_resource_forks()
        bsg.set_carbonfile()
+       bsg.set_win_acls()
        bsg.set_hardlinks()
        bsg.set_fsync_directories()
        bsg.set_change_ownership()
--- rdiff_backup/metadata.py    10 Jul 2007 22:39:59 -0000      1.28
+++ rdiff_backup/metadata.py    21 Jun 2008 05:22:53 -0000
@@ -433,9 +433,10 @@
 
 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 @@
                        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 @@
        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 @@
                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 @@
                                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 @@
                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
@@ -664,3 +691,4 @@
 
 
 import eas_acls # put at bottom to avoid python circularity bug
+import win_acls
--- rdiff_backup/rpath.py       10 Jun 2008 13:14:52 -0000      1.120
+++ rdiff_backup/rpath.py       21 Jun 2008 02:32:23 -0000
@@ -185,6 +185,7 @@
        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
@@ -257,6 +258,8 @@
                                if error.errno != errno.EEXIST: raise
 
                                # On Windows, files can't be renamed on top of 
an existing file
+                               try: rp_source.conn.os.chmod(rp_dest.path, 
stat.S_IWRITE)
+                               except: pass
                                rp_source.conn.os.unlink(rp_dest.path)
                                rp_source.conn.os.rename(rp_source.path, 
rp_dest.path)
                            
@@ -341,6 +345,7 @@
                        elif key == 'carbonfile' and not 
Globals.carbonfile_active: pass
                        elif key == 'resourcefork' and not 
Globals.resource_forks_active:
                                pass
+                       elif key == 'win_acl' and not Globals.win_acls_active: 
pass
                        elif key == 'uname' or key == 'gname':
                                # here for legacy reasons - 0.12.x didn't store 
u/gnames
                                other_name = other.data.get(key, None)
@@ -378,6 +383,7 @@
                        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_active: 
pass
                        elif key == 'carbonfile' and not 
Globals.carbonfile_write: pass
                        elif key == 'resourcefork' and not 
Globals.resource_forks_write:
                                pass
@@ -396,7 +402,7 @@
        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_type = 1, compare_win_acls = 
0, 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),
@@ -417,6 +423,7 @@
                                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):
@@ -434,7 +441,8 @@
                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)
 
@@ -682,6 +690,17 @@
                """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')
@@ -994,7 +1013,12 @@
                        except os.error:
                                if Globals.fsync_directories: self.fsync()
                                self.conn.shutil.rmtree(self.path)
-               else: self.conn.os.unlink(self.path)
+               else:
+                       try:
+                               self.conn.os.unlink(self.path)
+                       except OSError, error:
+                               self.conn.os.chmod(self.path, stat.S_IWRITE)
+                               self.conn.os.unlink(self.path)
                self.setdata()
 
        def quote(self):
@@ -1304,6 +1328,16 @@
                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"""
@@ -1398,6 +1432,7 @@
                rpath.get_resource_fork()
        if Globals.carbonfile_conn and rpath.isreg():
                rpath.data['carbonfile'] = carbonfile_get(rpath)
+       if True: rpath.data['win_acl'] = win_acl_get(rpath)
 
 def carbonfile_get(rpath):
        """Return carbonfile value for local rpath"""
@@ -1427,3 +1462,7 @@
 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
--- rdiff_backup\win_acls.py    Sat Jun 21 10:03:54 2008
+++ rdiff_backup\win_acls.py    Sat Jun 21 09:33:07 2008
@@ -0,0 +1,201 @@
+# Copyright 2008 Fred Gansevles <address@hidden>
+#
+# 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
+
+__version__ = (0, 1, 0)
+
+import C, metadata, re, rorpiter, rpath
+
+from win32security import *
+
+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):
+               sd = rp.conn.win32security.GetFileSecurity(rp.path, ACL.flags)
+
+               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)
+               self.index = rp.index
+
+       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 self.__acl
+
+       def from_string(self, acl):
+               self.__acl = acl
+
+def Record2WACL(record):
+       lines = record.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 == '.': index = ()
+       else: index = tuple(C.acl_unquote(filename).split('/'))
+       acl = ACL(index)
+       acl.from_string(lines[1])
+       return acl
+
+def WACL2Record(wacl):
+       return '# file: %s\n%s\n' % \
+                       (C.acl_quote(wacl.get_indexpath()), 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(wacl)
+               yield rorp
+       
+def rpath_acl_win_get(rpath):
+       acl = ACL()
+       acl.load_from_rp(rpath)
+       return acl
+rpath.win_acl_get = rpath_acl_win_get
+
+def rpath_get_blank_win_acl(index):
+       acl = ACL(index)
+       return acl
+rpath.get_blank_win_acl = rpath_get_blank_win_acl
+
+def rpath_set_win_acl(rp, acl):
+       acl.write_to_rp(rp)
+rpath.write_win_acl = rpath_set_win_acl
+
+# in order to malipulate the SACL, special priveliges must be enabled
+# (if the process may enable them)
+# this code is therfore run (once) during module-import
+
+def init_acls():
+       # A process that tries to read or write a SACL needs
+       # to have and enable the SE_SECURITY_NAME privilege.
+       import win32api
+       try:
+               hnd = OpenThreadToken(win32api.GetCurrentThread(),
+                               TOKEN_ADJUST_PRIVILEGES|
+                               TOKEN_QUERY, False)
+       except win32api.error:
+               try:
+                       hnd = OpenProcessToken(win32api.GetCurrentProcess(),
+                               TOKEN_ADJUST_PRIVILEGES| TOKEN_QUERY)
+               except win32api.error:
+                       return
+       try:
+               try:
+                       SecurityName = LookupPrivilegeValue(None, 
SE_SECURITY_NAME)
+               except win32api.error:
+                       return
+               for nameValue, enabled in GetTokenInformation(hnd, 
TokenPrivileges):
+                       if nameValue == SecurityName:
+                               # we have the SE_SECURITY_NAME privelege
+                               break
+               else:
+                       return
+               try:
+                       # enable the SE_SECURITY_NAME privelege
+                       AdjustTokenPrivileges(hnd, False, [
+                               (SecurityName, SE_PRIVILEGE_ENABLED)
+                               ])
+               except win32api.error:
+                       return
+               # now we *may* access the SACL (sigh)
+               ACL.flags |= SACL_SECURITY_INFORMATION
+       finally:
+               win32api.CloseHandle(hnd)
+

reply via email to

[Prev in Thread] Current Thread [Next in Thread]