Commit 343b8424 authored by owsla's avatar owsla

Support for Windows ACLs. (Patch from Josh Nisly and Fred Gansevles)


git-svn-id: http://svn.savannah.nongnu.org/svn/rdiff-backup@904 2b77aa54-bcbc-44c9-a7ec-4f6cf2b41109
parent 9d58a56b
New in v1.1.17 (????/??/??)
---------------------------
Support for Windows ACLs. (Patch from Josh Nisly and Fred Gansevles)
Fix user_group.py to run on native Windows, which lacks grp and pwd Python
modules. (Patch from Fred Gansevles)
......
......@@ -118,7 +118,7 @@ def MakeTar(specfiles):
"Security.py", "selection.py",
"SetConnections.py", "static.py",
"statistics.py", "TempFile.py", "Time.py",
"user_group.py"]:
"user_group.py", "win_acls.py"]:
shutil.copyfile(os.path.join(SourceDir, filename),
os.path.join(tardir, "rdiff_backup", filename))
......
......@@ -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
......
......@@ -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
......
......@@ -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()
......
......@@ -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
......@@ -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
# Copyright 2008 Fred Gansevles <fred@betterbe.com>
#
# 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)
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment