diff options
-rw-r--r-- | rdiff-backup/dist/rdiff-backup.spec | 51 | ||||
-rw-r--r-- | rdiff-backup/rdiff_backup/eas_acls.py | 395 | ||||
-rw-r--r-- | rdiff-backup/testing/eas_aclstest.py | 344 | ||||
-rw-r--r-- | rdiff-backup/testing/resourceforktest.py | 104 | ||||
-rw-r--r-- | rdiff-backup/testing/securitytest.py | 128 |
5 files changed, 1019 insertions, 3 deletions
diff --git a/rdiff-backup/dist/rdiff-backup.spec b/rdiff-backup/dist/rdiff-backup.spec new file mode 100644 index 0000000..2073b51 --- /dev/null +++ b/rdiff-backup/dist/rdiff-backup.spec @@ -0,0 +1,51 @@ +%define PYTHON_NAME %((rpm -q --quiet python2 && echo python2) || echo python) + +Version: $version +Summary: Convenient and transparent local/remote incremental mirror/backup +Name: rdiff-backup +Release: 2 +URL: http://www.stanford.edu/~bescoto/rdiff-backup/ +Source: %{name}-%{version}.tar.gz +Copyright: GPL +Group: Applications/Archiving +BuildRoot: %{_tmppath}/%{name}-root +requires: librsync >= 0.9.5.1, %{PYTHON_NAME} >= 2.2 +BuildPrereq: %{PYTHON_NAME}-devel >= 2.2, librsync-devel >= 0.9.5.1 + +%description +rdiff-backup is a script, written in Python, that backs up one +directory to another and is intended to be run periodically (nightly +from cron for instance). The target directory ends up a copy of the +source directory, but extra reverse diffs are stored in the target +directory, so you can still recover files lost some time ago. The idea +is to combine the best features of a mirror and an incremental +backup. rdiff-backup can also operate in a bandwidth efficient manner +over a pipe, like rsync. Thus you can use rdiff-backup and ssh to +securely back a hard drive up to a remote location, and only the +differences from the previous backup will be transmitted. + +%prep +%setup -q + +%build +%{PYTHON_NAME} setup.py build + +%install +%{PYTHON_NAME} setup.py install --prefix=$RPM_BUILD_ROOT/usr + +%clean +[ "$RPM_BUILD_ROOT" != "/" ] && rm -rf $RPM_BUILD_ROOT + +%files +%defattr(-,root,root) +/usr/bin/rdiff-backup +/usr/share/man/man1 +/usr/lib +%doc CHANGELOG COPYING FAQ.html README + +%changelog +* Sun Jan 19 2002 Troels Arvin <troels@arvin.dk> +- Builds, no matter if Python 2.2 is called python2-2.2 or python-2.2. + +* Sun Nov 4 2001 Ben Escoto <bescoto@stanford.edu> +- Initial RPM diff --git a/rdiff-backup/rdiff_backup/eas_acls.py b/rdiff-backup/rdiff_backup/eas_acls.py new file mode 100644 index 0000000..4b4d169 --- /dev/null +++ b/rdiff-backup/rdiff_backup/eas_acls.py @@ -0,0 +1,395 @@ +# Copyright 2003 Ben Escoto +# +# 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 + +"""Store and retrieve extended attributes and access control lists + +Not all file systems will have EAs and ACLs, but if they do, store +this information in separate files in the rdiff-backup-data directory, +called extended_attributes.<time>.snapshot and +access_control_lists.<time>.snapshot. + +""" + +from __future__ import generators +import base64, errno, re +try: import posix1e +except ImportError: pass +import static, Globals, metadata, connection, rorpiter, log + + +class ExtendedAttributes: + """Hold a file's extended attribute information""" + def __init__(self, index, attr_dict = None): + """Initialize EA object with no attributes""" + self.index = index + if attr_dict is None: self.attr_dict = {} + else: self.attr_dict = attr_dict + + def __eq__(self, ea): + """Equal if all attributes and index are equal""" + assert isinstance(ea, ExtendedAttributes) + return ea.index == self.index and ea.attr_dict == self.attr_dict + def __ne__(self, ea): return not self.__eq__(ea) + + def get_indexpath(self): return self.index and '/'.join(self.index) or '.' + + def read_from_rp(self, rp): + """Set the extended attributes from an rpath""" + try: attr_list = rp.conn.xattr.listxattr(rp.path) + except IOError, exc: + if exc[0] == errno.EOPNOTSUPP: return # if not sup, consider empty + raise + for attr in attr_list: + if not attr.startswith('user.'): + # Only preserve user extended attributes + continue + try: self.attr_dict[attr] = rp.conn.xattr.getxattr(rp.path, attr) + except IOError, exc: + # File probably modified while reading, just continue + if exc[0] == errno.ENODATA: continue + elif exc[0] == errno.ENOENT: break + else: raise + + def clear_rp(self, rp): + """Delete all the extended attributes in rpath""" + for name in rp.conn.xattr.listxattr(rp.path): + rp.conn.xattr.removexattr(rp.path, name) + + def write_to_rp(self, rp): + """Write extended attributes to rpath rp""" + self.clear_rp(rp) + for (name, value) in self.attr_dict.iteritems(): + rp.conn.xattr.setxattr(rp.path, name, value) + + def get(self, name): + """Return attribute attached to given name""" + return self.attr_dict[name] + + def set(self, name, value = ""): + """Set given name to given value. Does not write to disk""" + self.attr_dict[name] = value + + def delete(self, name): + """Delete value associated with given name""" + del self.attr_dict[name] + + def empty(self): + """Return true if no extended attributes are set""" + return not self.attr_dict + +def ea_compare_rps(rp1, rp2): + """Return true if rp1 and rp2 have same extended attributes""" + ea1 = ExtendedAttributes(rp1.index) + ea1.read_from_rp(rp1) + ea2 = ExtendedAttributes(rp2.index) + ea2.read_from_rp(rp2) + return ea1 == ea2 + + +def EA2Record(ea): + """Convert ExtendedAttributes object to text record""" + str_list = ['# file: %s' % ea.get_indexpath()] + for (name, val) in ea.attr_dict.iteritems(): + if not val: str_list.append(name) + else: + encoded_val = base64.encodestring(val).replace('\n', '') + str_list.append('%s=0s%s' % (name, encoded_val)) + return '\n'.join(str_list)+'\n' + +def Record2EA(record): + """Convert text record to ExtendedAttributes object""" + lines = record.split('\n') + first = lines.pop(0) + if not first[:8] == "# file: ": + raise metadata.ParsingError("Bad record beginning: " + first[:8]) + filename = first[8:] + if filename == '.': index = () + else: index = tuple(filename.split('/')) + ea = ExtendedAttributes(index) + + for line in lines: + line = line.strip() + if not line: continue + assert line[0] != '#', line + eq_pos = line.find('=') + if eq_pos == -1: ea.set(line) + else: + name = line[:eq_pos] + assert line[eq_pos+1:eq_pos+3] == '0s', \ + "Currently only base64 encoding supported" + encoded_val = line[eq_pos+3:] + ea.set(name, base64.decodestring(encoded_val)) + return ea + +def quote_path(path): + """Quote a path for use EA/ACL records. + + Right now no quoting!!! Change this to reflect the updated + quoting style of getfattr/setfattr when they are changed. + + """ + return path + + +class EAExtractor(metadata.FlatExtractor): + """Iterate ExtendedAttributes objects from the EA information file""" + record_boundary_regexp = re.compile("\\n# file:") + record_to_object = staticmethod(Record2EA) + def get_index_re(self, index): + """Find start of EA record with given index""" + if not index: indexpath = '.' + else: indexpath = '/'.join(index) + # Right now there is no quoting, due to a bug in + # getfacl/setfacl. Replace later when bug fixed. + return re.compile('(^|\\n)(# file: %s\\n)' % indexpath) + +class ExtendedAttributesFile(metadata.FlatFile): + """Store/retrieve EAs from extended_attributes file""" + _prefix = "extended_attributes" + _extractor = EAExtractor + _object_to_record = staticmethod(EA2Record) + + def join(cls, rorp_iter, rbdir, time, restrict_index): + """Add extended attribute information to existing rorp_iter""" + def helper(rorp_iter, ea_iter): + """Add EA information in ea_iter to rorp_iter""" + collated = rorpiter.CollateIterators(rorp_iter, ea_iter) + for rorp, ea in collated: + assert rorp, (rorp, (ea.index, ea.attr_dict), rest_time) + if not ea: ea = ExtendedAttributes(rorp.index) + rorp.set_ea(ea) + yield rorp + + ea_iter = cls.get_objects_at_time(rbdir, time, restrict_index) + if ea_iter: return helper(rorp_iter, ea_iter) + else: + log.Log("Warning: Extended attributes file not found",2) + return rorp_iter + +static.MakeClass(ExtendedAttributesFile) + + +class AccessControlList: + """Hold a file's access control list information + + Since ACL objects cannot be picked, store everything as text, in + self.acl_text and self.def_acl_text. + + """ + def __init__(self, index, acl_text = None, def_acl_text = None): + """Initialize object with index and possibly acl_text""" + self.index = index + if acl_text: # Check validity of ACL, reorder if necessary + ACL = posix1e.ACL(text=acl_text) + assert ACL.valid(), "Bad ACL: "+acl_text + self.acl_text = str(ACL) + else: self.acl_text = None + + if def_acl_text: + def_ACL = posix1e.ACL(text=def_acl_text) + assert def_ACL.valid(), "Bad default ACL: "+def_acl_text + self.def_acl_text = str(def_ACL) + else: self.def_acl_text = None + + def __str__(self): + """Return human-readable string""" + return ("acl_text: %s\ndef_acl_text: %s" % + (self.acl_text, self.def_acl_text)) + + def __eq__(self, acl): + """Compare self and other access control list + + Basic acl permissions are considered equal to an empty acl + object. + + """ + assert isinstance(acl, self.__class__) + if self.index != acl.index: return 0 + if self.is_basic(): return acl.is_basic() + if acl.is_basic(): return self.is_basic() + if self.acl_text != acl.acl_text: return 0 + if not self.def_acl_text and not acl.def_acl_text: return 1 + return self.def_acl_text == acl.def_acl_text + def __ne__(self, acl): return not self.__eq__(acl) + + def eq_verbose(self, acl): + """Returns same as __eq__ but print explanation if not equal""" + if self.index != acl.index: + print "index %s not equal to index %s" % (self.index, acl.index) + return 0 + if self.acl_text != acl.acl_text: + print "ACL texts not equal:" + print self.acl_text + print acl.acl_text + return 0 + if (self.def_acl_text != acl.def_acl_text and + (self.def_acl_text or acl.def_acl_text)): + print "Unequal default acl texts:" + print self.def_acl_text + print acl.def_acl_text + return 0 + return 1 + + def get_indexpath(self): return self.index and '/'.join(self.index) or '.' + + def is_basic(self): + """True if acl can be reduced to standard unix permissions + + Assume that if they are only three entries, they correspond to + user, group, and other, and thus don't use any special ACL + features. + + """ + if not self.acl_text and not self.def_acl_text: return 1 + lines = self.acl_text.strip().split('\n') + assert len(lines) >= 3, lines + return len(lines) == 3 and not self.def_acl_text + + def read_from_rp(self, rp): + """Set self.ACL from an rpath, or None if not supported""" + self.acl_text, self.def_acl_text = \ + rp.conn.eas_acls.get_acl_text_from_rp(rp) + + def write_to_rp(self, rp): + """Write current access control list to RPath rp""" + rp.conn.eas_acls.set_rp_acl(rp, self.acl_text, self.def_acl_text) + +def set_rp_acl(rp, acl_text = None, def_acl_text = None): + """Set given rp with ACL that acl_text defines. rp should be local""" + assert rp.conn is Globals.local_connection + if acl_text: + acl = posix1e.ACL(text=acl_text) + assert acl.valid() + else: acl = posix1e.ACL() + acl.applyto(rp.path) + if rp.isdir(): + if def_acl_text: + def_acl = posix1e.ACL(text=def_acl_text) + assert def_acl.valid() + else: def_acl = posix1e.ACL() + def_acl.applyto(rp.path, posix1e.ACL_TYPE_DEFAULT) + +def get_acl_text_from_rp(rp): + """Returns (acl_text, def_acl_text) from an rpath. Call locally""" + assert rp.conn is Globals.local_connection + try: acl_text = str(posix1e.ACL(file=rp.path)) + except IOError, exc: + if exc[0] == errno.EOPNOTSUPP: acl_text = None + else: raise + if rp.isdir(): + try: def_acl_text = str(posix1e.ACL(filedef=rp.path)) + except IOError, exc: + if exc[0] == errno.EOPNOTSUPP: def_acl_text = None + else: raise + else: def_acl_text = None + return (acl_text, def_acl_text) + +def acl_compare_rps(rp1, rp2): + """Return true if rp1 and rp2 have same acl information""" + acl1 = AccessControlList(rp1.index) + acl1.read_from_rp(rp1) + acl2 = AccessControlList(rp2.index) + acl2.read_from_rp(rp2) + return acl1 == acl2 + + +def ACL2Record(acl): + """Convert an AccessControlList object into a text record""" + start = "# file: %s\n%s" % (acl.get_indexpath(), acl.acl_text) + if not acl.def_acl_text: return start + default_lines = acl.def_acl_text.strip().split('\n') + default_text = '\ndefault:'.join(default_lines) + return "%sdefault:%s\n" % (start, default_text) + +def Record2ACL(record): + """Convert text record to an AccessControlList object""" + lines = record.split('\n') + first_line = lines.pop(0) + if not first_line.startswith('# file: '): + raise metadata.ParsingError("Bad record beginning: "+ first_line) + filename = first_line[8:] + if filename == '.': index = () + else: index = tuple(filename.split('/')) + + normal_entries = []; default_entries = [] + for line in lines: + if line.startswith('default:'): default_entries.append(line[8:]) + else: normal_entries.append(line) + return AccessControlList(index, acl_text='\n'.join(normal_entries), + def_acl_text='\n'.join(default_entries)) + + +class ACLExtractor(EAExtractor): + """Iterate AccessControlList objects from the ACL information file + + Except for the record_to_object method, we can reuse everything in + the EAExtractor class because the file formats are so similar. + + """ + record_to_object = staticmethod(Record2ACL) + +class AccessControlListFile(metadata.FlatFile): + """Store/retrieve ACLs from extended attributes file""" + _prefix = 'access_control_lists' + _extractor = ACLExtractor + _object_to_record = staticmethod(ACL2Record) + + def join(cls, rorp_iter, rbdir, time, restrict_index): + """Add access control list information to existing rorp_iter""" + def helper(rorp_iter, acl_iter): + """Add ACL information in acl_iter to rorp_iter""" + collated = rorpiter.CollateIterators(rorp_iter, acl_iter) + for rorp, acl in collated: + assert rorp, "Missing rorp for index %s" % (acl.index,) + if not acl: acl = AccessControlList(rorp.index) + rorp.set_acl(acl) + yield rorp + + acl_iter = cls.get_objects_at_time(rbdir, time, restrict_index) + if acl_iter: return helper(rorp_iter, acl_iter) + else: + log.Log("Warning: Access Control List file not found", 2) + return rorp_iter + +static.MakeClass(AccessControlListFile) + + +def GetCombinedMetadataIter(rbdir, time, restrict_index = None, + acls = None, eas = None): + """Return iterator of rorps from metadata and related files + + None will be returned if the metadata file is absent. If acls or + eas is true, access control list or extended attribute information + will be added. + + """ + metadata_iter = metadata.MetadataFile.get_objects_at_time( + rbdir, time, restrict_index) + if not metadata_iter: + log.Log("Warning, metadata file not found.\n" + "Metadata will be read from filesystem.", 2) + return None + if eas: + metadata_iter = ExtendedAttributesFile.join( + metadata_iter, rbdir, time, restrict_index) + if acls: + metadata_iter = AccessControlListFile.join( + metadata_iter, rbdir, time, restrict_index) + return metadata_iter + diff --git a/rdiff-backup/testing/eas_aclstest.py b/rdiff-backup/testing/eas_aclstest.py new file mode 100644 index 0000000..ed1a5b4 --- /dev/null +++ b/rdiff-backup/testing/eas_aclstest.py @@ -0,0 +1,344 @@ +import unittest, os, time +from commontest import * +from rdiff_backup.eas_acls import * +from rdiff_backup import Globals, rpath, Time + +tempdir = rpath.RPath(Globals.local_connection, "testfiles/output") + +class EATest(unittest.TestCase): + """Test extended attributes""" + sample_ea = ExtendedAttributes( + (), {'user.empty':'', 'user.not_empty':'foobar', 'user.third':'hello', + 'user.binary':chr(0)+chr(1)+chr(2)+chr(140)+'/="', + 'user.multiline':"""This is a fairly long extended attribute. + Encoding it will require several lines of + base64.""" + chr(177)*300}) + empty_ea = ExtendedAttributes(()) + ea1 = ExtendedAttributes(('1',), sample_ea.attr_dict.copy()) + ea1.delete('user.not_empty') + ea2 = ExtendedAttributes(('2',), sample_ea.attr_dict.copy()) + ea2.set('user.third', 'Another random attribute') + ea3 = ExtendedAttributes(('3',)) + ea4 = ExtendedAttributes(('4',), {'user.deleted': 'File to be deleted'}) + ea_testdir1 = rpath.RPath(Globals.local_connection, "testfiles/ea_test1") + ea_testdir2 = rpath.RPath(Globals.local_connection, "testfiles/ea_test2") + + def make_temp(self): + """Make temp directory testfiles/output""" + if tempdir.lstat(): tempdir.delete() + tempdir.mkdir() + + def testBasic(self): + """Test basic writing and reading of extended attributes""" + self.make_temp() + new_ea = ExtendedAttributes(()) + new_ea.read_from_rp(tempdir) + assert not new_ea.attr_dict + assert not new_ea == self.sample_ea + assert new_ea != self.sample_ea + assert new_ea == self.empty_ea + + self.sample_ea.write_to_rp(tempdir) + new_ea.read_from_rp(tempdir) + assert new_ea.attr_dict == self.sample_ea.attr_dict, \ + (new_ea.attr_dict, self.sample_ea.attr_dict) + assert new_ea == self.sample_ea + + def testRecord(self): + """Test writing a record and reading it back""" + record = EA2Record(self.sample_ea) + new_ea = Record2EA(record) + if not new_ea == self.sample_ea: + new_list = new_ea.attr_dict.keys() + sample_list = self.sample_ea.attr_dict.keys() + new_list.sort() + sample_list.sort() + assert new_list == sample_list, (new_list, sample_list) + for name in new_list: + assert self.sample_ea.get(name) == new_ea.get(name), \ + (self.sample_ea.get(name), new_ea.get(name)) + assert self.sample_ea.index == new_ea.index, \ + (self.sample_ea.index, new_ea.index) + assert 0, "We shouldn't have gotten this far" + + def make_backup_dirs(self): + """Create testfiles/ea_test[12] directories + + Goal is to set range of extended attributes, to give good test + to extended attribute code. + + """ + if self.ea_testdir1.lstat(): self.ea_testdir1.delete() + if self.ea_testdir2.lstat(): self.ea_testdir2.delete() + self.ea_testdir1.mkdir() + rp1_1 = self.ea_testdir1.append('1') + rp1_2 = self.ea_testdir1.append('2') + rp1_3 = self.ea_testdir1.append('3') + rp1_4 = self.ea_testdir1.append('4') + map(rpath.RPath.touch, [rp1_1, rp1_2, rp1_3, rp1_4]) + self.sample_ea.write_to_rp(self.ea_testdir1) + self.ea1.write_to_rp(rp1_1) + self.ea2.write_to_rp(rp1_2) + self.ea4.write_to_rp(rp1_4) + + self.ea_testdir2.mkdir() + rp2_1 = self.ea_testdir2.append('1') + rp2_2 = self.ea_testdir2.append('2') + rp2_3 = self.ea_testdir2.append('3') + map(rpath.RPath.touch, [rp2_1, rp2_2, rp2_3]) + self.ea3.write_to_rp(self.ea_testdir2) + self.sample_ea.write_to_rp(rp2_1) + self.ea1.write_to_rp(rp2_2) + self.ea2.write_to_rp(rp2_3) + + def testIterate(self): + """Test writing several records and then reading them back""" + self.make_backup_dirs() + rp1 = self.ea_testdir1.append('1') + rp2 = self.ea_testdir1.append('2') + rp3 = self.ea_testdir1.append('3') + + # Now write records corresponding to above rps into file + Globals.rbdir = tempdir + Time.setcurtime(10000) + ExtendedAttributesFile.open_file() + for rp in [self.ea_testdir1, rp1, rp2, rp3]: + ea = ExtendedAttributes(rp.index) + ea.read_from_rp(rp) + ExtendedAttributesFile.write_object(ea) + ExtendedAttributesFile.close_file() + + # Read back records and compare + ea_iter = ExtendedAttributesFile.get_objects_at_time(tempdir, 10000) + assert ea_iter, "No extended_attributes.<time> file found" + sample_ea_reread = ea_iter.next() + assert sample_ea_reread == self.sample_ea + ea1_reread = ea_iter.next() + assert ea1_reread == self.ea1 + ea2_reread = ea_iter.next() + assert ea2_reread == self.ea2 + ea3_reread = ea_iter.next() + assert ea3_reread == self.ea3 + try: ea_iter.next() + except StopIteration: pass + else: assert 0, "Expected end to iterator" + + def testSeriesLocal(self): + """Test backing up and restoring directories with EAs locally""" + self.make_backup_dirs() + dirlist = ['testfiles/ea_test1', 'testfiles/empty', + 'testfiles/ea_test2', 'testfiles/ea_test1'] + BackupRestoreSeries(1, 1, dirlist, compare_eas = 1) + + def testSeriesRemote(self): + """Test backing up, restoring directories with EA remotely""" + self.make_backup_dirs() + dirlist = ['testfiles/ea_test1', 'testfiles/ea_test2', + 'testfiles/empty', 'testfiles/ea_test1'] + BackupRestoreSeries(None, None, dirlist, compare_eas = 1) + + + +class ACLTest(unittest.TestCase): + """Test access control lists""" + sample_acl = AccessControlList((),"""user::rwx +user:root:rwx +group::r-x +group:root:r-x +mask::r-x +other::---""") + dir_acl = AccessControlList((), """user::rwx +user:root:rwx +group::r-x +group:root:r-x +mask::r-x +other::---""", + """user::rwx +user:root:--- +group::r-x +mask::r-x +other::---""") + acl1 = AccessControlList(('1',), """user::r-- +user:ben:--- +group::--- +group:root:--- +mask::--- +other::---""") + acl2 = AccessControlList(('2',), """user::rwx +group::r-x +group:ben:rwx +mask::--- +other::---""") + acl3 = AccessControlList(('3',), """user::rwx +user:root:--- +group::r-x +mask::--- +other::---""") + empty_acl = AccessControlList((), "user::rwx\ngroup::---\nother::---") + acl_testdir1 = rpath.RPath(Globals.local_connection, 'testfiles/acl_test1') + acl_testdir2 = rpath.RPath(Globals.local_connection, 'testfiles/acl_test2') + def make_temp(self): + """Make temp directory testfile/output""" + if tempdir.lstat(): tempdir.delete() + tempdir.mkdir() + + def testBasic(self): + """Test basic writing and reading of ACLs""" + self.make_temp() + new_acl = AccessControlList(()) + tempdir.chmod(0700) + new_acl.read_from_rp(tempdir) + assert new_acl.is_basic(), new_acl.acl_text + assert not new_acl == self.sample_acl + assert new_acl != self.sample_acl + assert new_acl == self.empty_acl, \ + (new_acl.acl_text, self.empty_acl.acl_text) + + self.sample_acl.write_to_rp(tempdir) + new_acl.read_from_rp(tempdir) + assert new_acl.acl_text == self.sample_acl.acl_text, \ + (new_acl.acl_text, self.sample_acl.acl_text) + assert new_acl == self.sample_acl + + def testBasicDir(self): + """Test reading and writing of ACL w/ defaults to directory""" + self.make_temp() + new_acl = AccessControlList(()) + new_acl.read_from_rp(tempdir) + assert new_acl.is_basic() + assert new_acl != self.dir_acl + + self.dir_acl.write_to_rp(tempdir) + new_acl.read_from_rp(tempdir) + assert not new_acl.is_basic() + if not new_acl == self.dir_acl: + assert new_acl.eq_verbose(self.dir_acl) + assert 0, "Shouldn't be here---eq != eq_verbose?" + + def testRecord(self): + """Test writing a record and reading it back""" + record = ACL2Record(self.sample_acl) + new_acl = Record2ACL(record) + assert new_acl == self.sample_acl + + record2 = ACL2Record(self.dir_acl) + new_acl2 = Record2ACL(record2) + if not new_acl2 == self.dir_acl: + assert new_acl2.eq_verbose(self.dir_acl) + assert 0 + + def make_backup_dirs(self): + """Create testfiles/acl_test[12] directories""" + if self.acl_testdir1.lstat(): self.acl_testdir1.delete() + if self.acl_testdir2.lstat(): self.acl_testdir2.delete() + self.acl_testdir1.mkdir() + rp1_1 = self.acl_testdir1.append('1') + rp1_2 = self.acl_testdir1.append('2') + rp1_3 = self.acl_testdir1.append('3') + map(rpath.RPath.touch, [rp1_1, rp1_2, rp1_3]) + self.dir_acl.write_to_rp(self.acl_testdir1) + self.acl1.write_to_rp(rp1_1) + self.acl2.write_to_rp(rp1_2) + self.acl3.write_to_rp(rp1_3) + + self.acl_testdir2.mkdir() + rp2_1, rp2_2, rp2_3 = map(self.acl_testdir2.append, ('1', '2', '3')) + map(rpath.RPath.touch, (rp2_1, rp2_2, rp2_3)) + self.sample_acl.write_to_rp(self.acl_testdir2) + self.acl3.write_to_rp(rp2_1) + self.acl1.write_to_rp(rp2_2) + self.acl2.write_to_rp(rp2_3) + + def testIterate(self): + """Test writing several records and then reading them back""" + self.make_backup_dirs() + rp1 = self.acl_testdir1.append('1') + rp2 = self.acl_testdir1.append('2') + rp3 = self.acl_testdir1.append('3') + + # Now write records corresponding to above rps into file + Globals.rbdir = tempdir + Time.setcurtime(10000) + AccessControlListFile.open_file() + for rp in [self.acl_testdir1, rp1, rp2, rp3]: + acl = AccessControlList(rp.index) + acl.read_from_rp(rp) + AccessControlListFile.write_object(acl) + AccessControlListFile.close_file() + + # Read back records and compare + acl_iter = AccessControlListFile.get_objects_at_time(tempdir, 10000) + assert acl_iter, "No acl file found" + dir_acl_reread = acl_iter.next() + assert dir_acl_reread == self.dir_acl + acl1_reread = acl_iter.next() + assert acl1_reread == self.acl1 + acl2_reread = acl_iter.next() + assert acl2_reread == self.acl2 + acl3_reread = acl_iter.next() + assert acl3_reread == self.acl3 + try: extra = acl_iter.next() + except StopIteration: pass + else: assert 0, "Got unexpected object: " + repr(extra) + + def testSeriesLocal(self): + """Test backing up and restoring directories with ACLs locally""" + self.make_backup_dirs() + dirlist = ['testfiles/acl_test1', 'testfiles/empty', + 'testfiles/acl_test2', 'testfiles/acl_test1'] + BackupRestoreSeries(1, 1, dirlist, compare_acls = 1) + + def testSeriesRemote(self): + """Test backing up, restoring directories with EA remotely""" + self.make_backup_dirs() + dirlist = ['testfiles/acl_test1', 'testfiles/acl_test2', + 'testfiles/empty', 'testfiles/acl_test1'] + BackupRestoreSeries(None, None, dirlist, compare_acls = 1) + + +class CombinedTest(unittest.TestCase): + """Test backing up and restoring directories with both EAs and ACLs""" + combo_testdir1 = rpath.RPath(Globals.local_connection, + 'testfiles/ea_acl_test1') + combo_testdir2 = rpath.RPath(Globals.local_connection, + 'testfiles/ea_acl_test2') + def make_backup_dirs(self): + """Create testfiles/ea_acl_test[12] directories""" + if self.combo_testdir1.lstat(): self.combo_testdir1.delete() + if self.combo_testdir2.lstat(): self.combo_testdir2.delete() + self.combo_testdir1.mkdir() + rp1_1, rp1_2, rp1_3 = map(self.combo_testdir1.append, ('1', '2', '3')) + map(rpath.RPath.touch, [rp1_1, rp1_2, rp1_3]) + ACLTest.dir_acl.write_to_rp(self.combo_testdir1) + EATest.sample_ea.write_to_rp(self.combo_testdir1) + ACLTest.acl1.write_to_rp(rp1_1) + EATest.ea2.write_to_rp(rp1_2) + ACLTest.acl3.write_to_rp(rp1_3) + EATest.ea3.write_to_rp(rp1_3) + + self.combo_testdir2.mkdir() + rp2_1, rp2_2, rp2_3 = map(self.combo_testdir2.append, ('1', '2', '3')) + map(rpath.RPath.touch, [rp2_1, rp2_2, rp2_3]) + ACLTest.sample_acl.write_to_rp(self.combo_testdir2) + EATest.ea1.write_to_rp(rp2_1) + EATest.ea3.write_to_rp(rp2_2) + ACLTest.acl2.write_to_rp(rp2_2) + + def testSeriesLocal(self): + """Test backing up and restoring EAs/ACLs locally""" + self.make_backup_dirs() + dirlist = ['testfiles/ea_acl_test1', 'testfiles/ea_acl_test2', + 'testfiles/empty', 'testfiles/ea_acl_test1'] + BackupRestoreSeries(1, 1, dirlist, + compare_eas = 1, compare_acls = 1) + + def testSeriesRemote(self): + """Test backing up and restoring EAs/ACLs locally""" + self.make_backup_dirs() + dirlist = ['testfiles/ea_acl_test1', 'testfiles/empty', + 'testfiles/ea_acl_test2', 'testfiles/ea_acl_test1'] + BackupRestoreSeries(None, None, dirlist, + compare_eas = 1, compare_acls = 1) + + +if __name__ == "__main__": unittest.main() diff --git a/rdiff-backup/testing/resourceforktest.py b/rdiff-backup/testing/resourceforktest.py new file mode 100644 index 0000000..4617b28 --- /dev/null +++ b/rdiff-backup/testing/resourceforktest.py @@ -0,0 +1,104 @@ +import unittest +from commontest import * +from rdiff_backup import rpath +from rdiff_backup import metadata + +"""***NOTE*** + +None of these tests should work unless your system supports resource +forks. So basically these tests should only be run on Mac OS X. + +""" + +Globals.read_resource_forks = Globals.write_resource_forks = 1 + +class ResourceForkTest(unittest.TestCase): + """Test dealing with Mac OS X style resource forks""" + tempdir = rpath.RPath(Globals.local_connection, 'testfiles/output') + rf_testdir1 = rpath.RPath(Globals.local_connection, + 'testfiles/resource_fork_test1') + rf_testdir2 = rpath.RPath(Globals.local_connection, + 'testfiles/resource_fork_test2') + def make_temp(self): + """Make temp directory testfiles/resource_fork_test""" + if self.tempdir.lstat(): self.tempdir.delete() + self.tempdir.mkdir() + + def testBasic(self): + """Test basic reading and writing of resource forks""" + self.make_temp() + rp = self.tempdir.append('test') + rp.touch() + assert rp.get_resource_fork() == '', rp.get_resource_fork() + + s = 'new resource fork data' + rp.write_resource_fork(s) + assert rp.get_resource_fork() == s, rp.get_resource_fork() + + rp2 = self.tempdir.append('test') + assert rp2.isreg() + assert rp2.get_resource_fork() == s, rp2.get_resource_fork() + + def testRecord(self): + """Test reading, writing, and comparing of records with rforks""" + self.make_temp() + rp = self.tempdir.append('test') + rp.touch() + rp.set_resource_fork('hello') + + record = metadata.RORP2Record(rp) + #print record + rorp_out = metadata.Record2RORP(record) + assert rorp_out == rp, (rorp_out, rp) + assert rorp_out.get_resource_fork() == 'hello' + + def make_backup_dirs(self): + """Create testfiles/resource_fork_test[12] dirs for testing""" + if self.rf_testdir1.lstat(): self.rf_testdir1.delete() + if self.rf_testdir2.lstat(): self.rf_testdir2.delete() + self.rf_testdir1.mkdir() + rp1_1 = self.rf_testdir1.append('1') + rp1_2 = self.rf_testdir1.append('2') + rp1_3 = self.rf_testdir1.append('3') + rp1_1.touch() + rp1_2.touch() + rp1_3.symlink('foo') + rp1_1.write_resource_fork('This should appear in resource fork') + rp1_1.chmod(0400) # test for bug changing resource forks after perms + rp1_2.write_resource_fork('Data for the resource fork 2') + + + self.rf_testdir2.mkdir() + rp2_1 = self.rf_testdir2.append('1') + rp2_2 = self.rf_testdir2.append('2') + rp2_3 = self.rf_testdir2.append('3') + rp2_1.touch() + rp2_2.touch() + rp2_3.touch() + rp2_1.write_resource_fork('New data for resource fork 1') + rp2_1.chmod(0400) + rp2_3.write_resource_fork('New fork') + + def testSeriesLocal(self): + """Test backing up and restoring directories with ACLs locally""" + Globals.read_resource_forks = Globals.write_resource_forks = 1 + self.make_backup_dirs() + dirlist = ['testfiles/resource_fork_test1', 'testfiles/empty', + 'testfiles/resource_fork_test2', + 'testfiles/resource_fork_test1'] + # BackupRestoreSeries(1, 1, dirlist, compare_resource_forks = 1) + BackupRestoreSeries(1, 1, dirlist) + + def testSeriesRemote(self): + """Test backing up and restoring directories with ACLs locally""" + Globals.read_resource_forks = Globals.write_resource_forks = 1 + self.make_backup_dirs() + dirlist = ['testfiles/resource_fork_test1', + 'testfiles/resource_fork_test2', 'testfiles/empty', + 'testfiles/resource_fork_test1'] + # BackupRestoreSeries(1, 1, dirlist, compare_resource_forks = 1) + BackupRestoreSeries(1, 1, dirlist) + + +if __name__ == "__main__": unittest.main() + diff --git a/rdiff-backup/testing/securitytest.py b/rdiff-backup/testing/securitytest.py index 863d36a..1c7bade 100644 --- a/rdiff-backup/testing/securitytest.py +++ b/rdiff-backup/testing/securitytest.py @@ -1,6 +1,6 @@ -import os, unittest +import os, unittest, time from commontest import * -import rdiff_backup.Security +import rdiff_backup.Security as Security #Log.setverbosity(5) @@ -12,7 +12,7 @@ class SecurityTest(unittest.TestCase): problem. """ - assert isinstance(exc, rdiff_backup.Security.Violation) + assert isinstance(exc, Security.Violation) #assert str(exc).find("Security") >= 0, "%s\n%s" % (exc, repr(exc)) def test_vet_request_ro(self): @@ -56,5 +56,127 @@ class SecurityTest(unittest.TestCase): SetConnections.CloseConnections() + def secure_rdiff_backup(self, in_dir, out_dir, in_local, restrict_args, + extra_args = "", success = 1, current_time = None): + """Run rdiff-backup locally, with given restrict settings""" + if not current_time: current_time = int(time.time()) + prefix = ('rdiff-backup --current-time %s ' % (current_time,) + + '--remote-schema %s ') + + if in_local: out_dir = ("'rdiff-backup %s --server'::%s" % + (restrict_args, out_dir)) + else: in_dir = ("'rdiff-backup %s --server'::%s" % + (restrict_args, in_dir)) + + cmdline = "%s %s %s %s" % (prefix, extra_args, in_dir, out_dir) + print "Executing:", cmdline + exit_val = os.system(cmdline) + if success: assert not exit_val + else: assert exit_val, "Success when wanted failure" + + def test_restrict_positive(self): + """Test that --restrict switch doesn't get in the way + + This makes sure that basic backups with the restrict operator + work, (initial backup, incremental, restore). + + """ + Myrm("testfiles/output") + self.secure_rdiff_backup('testfiles/various_file_types', + 'testfiles/output', 1, + '--restrict testfiles/output', + current_time = 10000) + self.secure_rdiff_backup('testfiles/various_file_types', + 'testfiles/output', 1, + '--restrict testfiles/output') + + Myrm("testfiles/restore_out") + self.secure_rdiff_backup('testfiles/output', + 'testfiles/restore_out', 1, + '--restrict testfiles/restore_out', + extra_args = '-r now') + + def test_restrict_negative(self): + """Test that --restrict switch denies certain operations""" + # Backup to wrong directory + Myrm("testfiles/output testfiles/output2") + self.secure_rdiff_backup('testfiles/various_file_types', + 'testfiles/output2', 1, + '--restrict testfiles/output', + success = 0) + + # Restore to wrong directory + Myrm("testfiles/output testfiles/restore_out") + rdiff_backup(1, 1, 'testfiles/various_file_types', + 'testfiles/output') + self.secure_rdiff_backup('testfiles/output', + 'testfiles/restore_out', 1, + '--restrict testfiles/output2', + extra_args = '-r now', + success = 0) + + # Backup from wrong directory + Myrm("testfiles/output") + self.secure_rdiff_backup('testfiles/various_file_types', + 'testfiles/output', 0, + '--restrict testfiles/foobar', + success = 0) + + def test_restrict_readonly_positive(self): + """Test that --restrict-read-only switch doesn't impair normal ops""" + Myrm("testfiles/output testfiles/restore_out") + self.secure_rdiff_backup('testfiles/various_file_types', + 'testfiles/output', 0, + '--restrict-read-only testfiles/various_file_types') + + self.secure_rdiff_backup('testfiles/output', + 'testfiles/restore_out', 0, + '--restrict-read-only testfiles/output', + extra_args = '-r now') + + def test_restrict_readonly_negative(self): + """Test that --restrict-read-only doesn't allow too much""" + # Backup to restricted directory + Myrm('testfiles/output') + self.secure_rdiff_backup('testfiles/various_file_types', + 'testfiles/output', 1, + '--restrict-read-only testfiles/output', + success = 0) + + # Restore to restricted directory + Myrm('testfiles/output testfiles/restore_out') + rdiff_backup(1, 1, 'testfiles/various_file_types', 'testfiles/output') + self.secure_rdiff_backup('testfiles/output', + 'testfiles/restore_out', 1, + '--restrict-read-only testfiles/restore_out', + extra_args = '-r now', + success = 0) + + def test_restrict_updateonly_positive(self): + """Test that --restrict-update-only allows intended use""" + Myrm('testfiles/output') + rdiff_backup(1, 1, 'testfiles/various_file_types', 'testfiles/output', + current_time = 10000) + self.secure_rdiff_backup('testfiles/various_file_types', + 'testfiles/output', 1, + '--restrict-update-only testfiles/output') + + def test_restrict_updateonly_negative(self): + """Test that --restrict-update-only impairs unintended""" + Myrm('testfiles/output') + self.secure_rdiff_backup('testfiles/various_file_types', + 'testfiles/output', 1, + '--restrict-update-only testfiles/output', + success = 0) + + Myrm('testfiles/output testfiles/restore_out') + rdiff_backup(1, 1, 'testfiles/various_file_types', 'testfiles/output') + self.secure_rdiff_backup('testfiles/output', + 'testfiles/restore_out', 1, + '--restrict-update-only testfiles/restore_out', + extra_args = '-r now', + success = 0) + + if __name__ == "__main__": unittest.main() |