[Top][All Lists]
[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)
+
- [rdiff-backup-users] [PATCH] Backing up Windows ACLs,
Josh Nisly <=