Commit 47a30bcf authored by bescoto's avatar bescoto

Added mapping by name (not id) for ACLs.


git-svn-id: http://svn.savannah.nongnu.org/svn/rdiff-backup@440 2b77aa54-bcbc-44c9-a7ec-4f6cf2b41109
parent 322197c3
...@@ -30,7 +30,8 @@ from __future__ import generators ...@@ -30,7 +30,8 @@ from __future__ import generators
import base64, errno, re import base64, errno, re
try: import posix1e try: import posix1e
except ImportError: pass except ImportError: pass
import static, Globals, metadata, connection, rorpiter, log, C, rpath import static, Globals, metadata, connection, rorpiter, log, C, \
rpath, user_group
class ExtendedAttributes: class ExtendedAttributes:
"""Hold a file's extended attribute information""" """Hold a file's extended attribute information"""
...@@ -172,32 +173,111 @@ class ExtendedAttributesFile(metadata.FlatFile): ...@@ -172,32 +173,111 @@ class ExtendedAttributesFile(metadata.FlatFile):
static.MakeClass(ExtendedAttributesFile) static.MakeClass(ExtendedAttributesFile)
class AccessControlList: class AccessControlLists:
"""Hold a file's access control list information """Hold a file's access control list information
Since ACL objects cannot be picked, store everything as text, in Since posix1e.ACL objects cannot be picked, and because they lack
self.acl_text and self.def_acl_text. user/group name information, store everything in self.entry_list
and self.default_entry_list.
""" """
def __init__(self, index, acl_text = None, def_acl_text = None): def __init__(self, index, acl_text = None):
"""Initialize object with index and possibly acl_text""" """Initialize object with index and possibly acl_text"""
self.index = index self.index = index
if acl_text: # Check validity of ACL, reorder if necessary if acl_text: self.set_from_text(acl_text)
ACL = posix1e.ACL(text=acl_text) else: self.entry_list = self.default_entry_list = None
assert ACL.valid(), "Bad ACL: "+acl_text
self.acl_text = str(ACL) def set_from_text(self, text):
else: self.acl_text = None """Set self.entry_list and self.default_entry_list from text"""
self.entry_list, self.default_entry_list = [], []
if def_acl_text: for line in text.split('\n'):
def_ACL = posix1e.ACL(text=def_acl_text) comment_pos = text.find('#')
assert def_ACL.valid(), "Bad default ACL: "+def_acl_text if comment_pos >= 0: line = line[:comment_pos]
self.def_acl_text = str(def_ACL) line = line.strip()
else: self.def_acl_text = None if not line: continue
if line.startswith('default:'):
entrytuple = self.text_to_entrytuple(line[8:])
self.default_entry_list.append(entrytuple)
else: self.entry_list.append(self.text_to_entrytuple(line))
def __str__(self): def __str__(self):
"""Return human-readable string""" """Return text version of acls"""
return ("acl_text: %s\ndef_acl_text: %s" % slist = map(self.entrytuple_to_text, self.entry_list)
(self.acl_text, self.def_acl_text)) if self.default_entry_list:
slist.extend(map(lambda e: "default:" + self.entrytuple_to_text(e),
self.default_entry_list))
return "\n".join(slist)
def entrytuple_to_text(self, entrytuple):
"""Return text version of entrytuple, as in getfacl"""
type, name_pair, perms = entrytuple
if type == posix1e.ACL_USER_OBJ:
text = 'user::'
elif type == posix1e.ACL_USER:
uid, uname = name_pair
text = 'user:%s:' % (uname or uid)
elif type == posix1e.ACL_GROUP_OBJ:
text = 'group::'
elif type == posix1e.ACL_GROUP:
gid, gname = name_pair
text = 'group:%s:' % (gname or gid)
elif type == posix1e.ACL_MASK:
text = 'mask::'
else:
assert type == posix1e.ACL_OTHER, type
text = 'other::'
permstring = '%s%s%s' % (perms & 4 and 'r' or '-',
perms & 2 and 'w' or '-',
perms & 1 and 'x' or '-')
return text+permstring
def text_to_entrytuple(self, text):
"""Return entrytuple given text like 'user:foo:r--'"""
typetext, qualifier, permtext = text.split(':')
if qualifier:
try: id = int(qualifier)
except ValueError: namepair = (None, qualifier)
else: namepair = (uid, None)
if typetext == 'user': type = posix1e.ACL_USER
else:
assert typetext == 'group', (typetext, text)
type = posix1e.ACL_GROUP
else:
namepair = None
if typetext == 'user': type = posix1e.ACL_USER_OBJ
elif typetext == 'group': type = posix1e.ACL_GROUP_OBJ
elif typetext == 'mask': type = posix1e.ACL_MASK
else:
assert typetext == 'other', (typetext, text)
type = posix1e.ACL_OTHER
assert len(permtext) == 3, (permtext, text)
read, write, execute = permtext
perms = ((read == 'r') << 2 |
(write == 'w') << 1 |
(execute == 'x'))
return (type, namepair, perms)
def cmp_entry_list(self, l1, l2):
"""True if the lists have same entries. Assume preordered"""
if not l1: return l1 == l2
if not l2 or len(l1) != len(l2): return 0
for i in range(len(l1)):
type1, namepair1, perms1 = l1[i]
type2, namepair2, perms2 = l2[i]
if type1 != type2 or perms1 != perms2: return 0
if namepair1 == namepair2: continue
if not namepair1 or not namepair2: return 0
(id1, name1), (id2, name2) = namepair1, namepair2
if name1:
if name1 == name2: continue
else: return 0
if name2: return 0
if id1 != id2: return 0
return 1
def __eq__(self, acl): def __eq__(self, acl):
"""Compare self and other access control list """Compare self and other access control list
...@@ -208,11 +288,10 @@ class AccessControlList: ...@@ -208,11 +288,10 @@ class AccessControlList:
""" """
assert isinstance(acl, self.__class__) assert isinstance(acl, self.__class__)
if self.index != acl.index: return 0 if self.index != acl.index: return 0
if self.is_basic(): return acl.is_basic() return (self.cmp_entry_list(self.entry_list, acl.entry_list) and
if acl.is_basic(): return self.is_basic() self.cmp_entry_list(self.default_entry_list,
if self.acl_text != acl.acl_text: return 0 acl.default_entry_list))
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 __ne__(self, acl): return not self.__eq__(acl)
def eq_verbose(self, acl): def eq_verbose(self, acl):
...@@ -220,16 +299,12 @@ class AccessControlList: ...@@ -220,16 +299,12 @@ class AccessControlList:
if self.index != acl.index: if self.index != acl.index:
print "index %s not equal to index %s" % (self.index, acl.index) print "index %s not equal to index %s" % (self.index, acl.index)
return 0 return 0
if self.acl_text != acl.acl_text: if not self.cmp_entry_list(self.entry_list, acl.entry_list):
print "ACL texts not equal:" print "ACL entries for %s compare differently" % (self.index,)
print self.acl_text
print acl.acl_text
return 0 return 0
if (self.def_acl_text != acl.def_acl_text and if not self.cmp_entry_list(self.default_entry_list,
(self.def_acl_text or acl.def_acl_text)): acl.default_entry_list):
print "Unequal default acl texts:" print "Default ACL entries for %s do not compare" % (self.index,)
print self.def_acl_text
print acl.def_acl_text
return 0 return 0
return 1 return 1
...@@ -243,133 +318,119 @@ class AccessControlList: ...@@ -243,133 +318,119 @@ class AccessControlList:
features. features.
""" """
if not self.acl_text and not self.def_acl_text: return 1 if not self.entry_list and not self.default_entry_list: return 1
lines = self.acl_text.strip().split('\n') assert len(self.entry_list) >= 3, self.entry_list
assert len(lines) >= 3, lines return len(self.entry_list) == 3 and not self.default_entry_list
return len(lines) == 3 and not self.def_acl_text
def read_from_rp(self, rp): def read_from_rp(self, rp):
"""Set self.ACL from an rpath, or None if not supported""" """Set self.ACL from an rpath, or None if not supported"""
self.acl_text, self.def_acl_text = \ self.entry_list, self.default_entry_list = \
rp.conn.eas_acls.get_acl_text_from_rp(rp) rp.conn.eas_acls.get_acl_lists_from_rp(rp)
def write_to_rp(self, rp): def write_to_rp(self, rp):
"""Write current access control list to RPath rp""" """Write current access control list to RPath rp"""
rp.conn.eas_acls.set_rp_acl(rp, self.acl_text, self.def_acl_text) rp.conn.eas_acls.set_rp_acl(rp, self.entry_list,
self.default_entry_list)
def acl_to_list(self, acl):
"""Return list representation of posix1e.ACL object
ACL objects cannot be pickled, so this representation keeps
the structure while adding that option. Also we insert the
username along with the id, because that information will be
lost when moved to another system.
The result will be a list of tuples. Each tuple will have the def set_rp_acl(rp, entry_list = None, default_entry_list = None):
form (acltype, (uid or gid, uname or gname) or None,
permissions as an int).
"""
def entry_to_tuple(entry):
if entry.tag_type == posix1e.ACL_USER:
uid = entry.qualifier
owner_pair = (uid, user_group.uid2uname(uid))
elif entry.tag_type == posix1e.ACL_GROUP:
gid = entry.qualifier
owner_pair = (gid, user_group.gid2gname(gid))
else: owner_pair = None
perms = (entry.permset.read << 2 |
entry.permset.write << 1 |
entry.permset.execute)
return (entry.tag_type, owner_pair, perms)
return map(entry_to_tuple, acl)
def list_to_acl(self, listacl):
"""Return posix1e.ACL object from list representation"""
acl = posix1e.ACL()
for tag, owner_pair, perms in listacl:
entry = posix1e.Entry(acl)
entry.tag_type = tag
if owner_pair:
if tag == posix1e.ACL_USER:
entry.qualifier = user_group.UserMap.get_id(*owner_pair)
else:
assert tag == posix1e.ACL_GROUP
entry.qualifier = user_group.GroupMap.get_id(*owner_pair)
entry.read = perms >> 2
entry.write = perms >> 1 & 1
entry.execute = perms & 1
return acl
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""" """Set given rp with ACL that acl_text defines. rp should be local"""
assert rp.conn is Globals.local_connection assert rp.conn is Globals.local_connection
if acl_text: if entry_list: acl = list_to_acl(entry_list)
acl = posix1e.ACL(text=acl_text)
assert acl.valid()
else: acl = posix1e.ACL() else: acl = posix1e.ACL()
acl.applyto(rp.path) acl.applyto(rp.path)
if rp.isdir(): if rp.isdir():
if def_acl_text: if default_entry_list: def_acl = list_to_acl(default_entry_list)
def_acl = posix1e.ACL(text=def_acl_text)
assert def_acl.valid()
else: def_acl = posix1e.ACL() else: def_acl = posix1e.ACL()
def_acl.applyto(rp.path, posix1e.ACL_TYPE_DEFAULT) def_acl.applyto(rp.path, posix1e.ACL_TYPE_DEFAULT)
def get_acl_text_from_rp(rp): def get_acl_lists_from_rp(rp):
"""Returns (acl_text, def_acl_text) from an rpath. Call locally""" """Returns (acl_list, def_acl_list) from an rpath. Call locally"""
assert rp.conn is Globals.local_connection assert rp.conn is Globals.local_connection
try: acl_text = str(posix1e.ACL(file=rp.path)) try: acl = posix1e.ACL(file=rp.path)
except IOError, exc: except IOError, exc:
if exc[0] == errno.EOPNOTSUPP: acl_text = None if exc[0] == errno.EOPNOTSUPP: acl = None
else: raise else: raise
if rp.isdir(): if rp.isdir():
try: def_acl_text = str(posix1e.ACL(filedef=rp.path)) try: def_acl = posix1e.ACL(filedef=rp.path)
except IOError, exc: except IOError, exc:
if exc[0] == errno.EOPNOTSUPP: def_acl_text = None if exc[0] == errno.EOPNOTSUPP: def_acl = None
else: raise else: raise
else: def_acl_text = None else: def_acl = None
return (acl_text, def_acl_text) return (acl and acl_to_list(acl), def_acl and acl_to_list(def_acl))
def acl_to_list(acl):
"""Return list representation of posix1e.ACL object
ACL objects cannot be pickled, so this representation keeps
the structure while adding that option. Also we insert the
username along with the id, because that information will be
lost when moved to another system.
The result will be a list of tuples. Each tuple will have the
form (acltype, (uid or gid, uname or gname) or None,
permissions as an int).
"""
def entry_to_tuple(entry):
if entry.tag_type == posix1e.ACL_USER:
uid = entry.qualifier
owner_pair = (uid, user_group.uid2uname(uid))
elif entry.tag_type == posix1e.ACL_GROUP:
gid = entry.qualifier
owner_pair = (gid, user_group.gid2gname(gid))
else: owner_pair = None
perms = (entry.permset.read << 2 |
entry.permset.write << 1 |
entry.permset.execute)
return (entry.tag_type, owner_pair, perms)
return map(entry_to_tuple, acl)
def list_to_acl(entry_list):
"""Return posix1e.ACL object from list representation"""
acl = posix1e.ACL()
for tag, owner_pair, perms in entry_list:
entry = posix1e.Entry(acl)
entry.tag_type = tag
if owner_pair:
if tag == posix1e.ACL_USER:
entry.qualifier = user_group.UserMap.get_id(*owner_pair)
else:
assert tag == posix1e.ACL_GROUP, (tag, owner_pair, perms)
entry.qualifier = user_group.GroupMap.get_id(*owner_pair)
entry.permset.read = perms >> 2
entry.permset.write = perms >> 1 & 1
entry.permset.execute = perms & 1
return acl
def acl_compare_rps(rp1, rp2): def acl_compare_rps(rp1, rp2):
"""Return true if rp1 and rp2 have same acl information""" """Return true if rp1 and rp2 have same acl information"""
acl1 = AccessControlList(rp1.index) acl1 = AccessControlLists(rp1.index)
acl1.read_from_rp(rp1) acl1.read_from_rp(rp1)
acl2 = AccessControlList(rp2.index) acl2 = AccessControlLists(rp2.index)
acl2.read_from_rp(rp2) acl2.read_from_rp(rp2)
return acl1 == acl2 return acl1 == acl2
def ACL2Record(acl): def ACL2Record(acl):
"""Convert an AccessControlList object into a text record""" """Convert an AccessControlLists object into a text record"""
start = "# file: %s\n%s" % (C.acl_quote(acl.get_indexpath()), acl.acl_text) return '# file: %s\n%s\n' % (C.acl_quote(acl.get_indexpath()), str(acl))
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): def Record2ACL(record):
"""Convert text record to an AccessControlList object""" """Convert text record to an AccessControlLists object"""
lines = record.split('\n') newline_pos = record.find('\n')
first_line = lines.pop(0) first_line = record[:newline_pos]
if not first_line.startswith('# file: '): if not first_line.startswith('# file: '):
raise metadata.ParsingError("Bad record beginning: "+ first_line) raise metadata.ParsingError("Bad record beginning: "+ first_line)
filename = first_line[8:] filename = first_line[8:]
if filename == '.': index = () if filename == '.': index = ()
else: index = tuple(C.acl_unquote(filename).split('/')) else: index = tuple(C.acl_unquote(filename).split('/'))
return AccessControlLists(index, record[newline_pos:])
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): class ACLExtractor(EAExtractor):
"""Iterate AccessControlList objects from the ACL information file """Iterate AccessControlLists objects from the ACL information file
Except for the record_to_object method, we can reuse everything in Except for the record_to_object method, we can reuse everything in
the EAExtractor class because the file formats are so similar. the EAExtractor class because the file formats are so similar.
...@@ -390,7 +451,7 @@ class AccessControlListFile(metadata.FlatFile): ...@@ -390,7 +451,7 @@ class AccessControlListFile(metadata.FlatFile):
collated = rorpiter.CollateIterators(rorp_iter, acl_iter) collated = rorpiter.CollateIterators(rorp_iter, acl_iter)
for rorp, acl in collated: for rorp, acl in collated:
assert rorp, "Missing rorp for index %s" % (acl.index,) assert rorp, "Missing rorp for index %s" % (acl.index,)
if not acl: acl = AccessControlList(rorp.index) if not acl: acl = AccessControlLists(rorp.index)
rorp.set_acl(acl) rorp.set_acl(acl)
yield rorp yield rorp
...@@ -433,7 +494,7 @@ def rpath_acl_get(rp): ...@@ -433,7 +494,7 @@ def rpath_acl_get(rp):
This overrides a function in the rpath module. This overrides a function in the rpath module.
""" """
acl = AccessControlList(rp.index) acl = AccessControlLists(rp.index)
if not rp.issym(): acl.read_from_rp(rp) if not rp.issym(): acl.read_from_rp(rp)
return acl return acl
rpath.acl_get = rpath_acl_get rpath.acl_get = rpath_acl_get
......
...@@ -141,7 +141,7 @@ def init_user_mapping(mapping_string = None): ...@@ -141,7 +141,7 @@ def init_user_mapping(mapping_string = None):
if mapping_string: UserMap = DefinedMap(name2id_func, mapping_string) if mapping_string: UserMap = DefinedMap(name2id_func, mapping_string)
else: UserMap = Map(name2id_func) else: UserMap = Map(name2id_func)
def init_group_mapping(mapping_string): def init_group_mapping(mapping_string = None):
"""Initialize the group mapping dictionary with given mapping string""" """Initialize the group mapping dictionary with given mapping string"""
global GroupMap global GroupMap
name2id_func = lambda name: grp.getgrnam(name)[2] name2id_func = lambda name: grp.getgrnam(name)[2]
......
import unittest, os, time, cStringIO import unittest, os, time, cStringIO
from commontest import * from commontest import *
from rdiff_backup.eas_acls import * from rdiff_backup.eas_acls import *
from rdiff_backup import Globals, rpath, Time from rdiff_backup import Globals, rpath, Time, user_group
user_group.init_user_mapping()
user_group.init_group_mapping()
tempdir = rpath.RPath(Globals.local_connection, "testfiles/output") tempdir = rpath.RPath(Globals.local_connection, "testfiles/output")
restore_dir = rpath.RPath(Globals.local_connection, restore_dir = rpath.RPath(Globals.local_connection,
"testfiles/restore_out") "testfiles/restore_out")
...@@ -194,40 +196,40 @@ user.empty ...@@ -194,40 +196,40 @@ user.empty
class ACLTest(unittest.TestCase): class ACLTest(unittest.TestCase):
"""Test access control lists""" """Test access control lists"""
sample_acl = AccessControlList((),"""user::rwx sample_acl = AccessControlLists((),"""user::rwx
user:root:rwx user:root:rwx
group::r-x group::r-x
group:root:r-x group:root:r-x
mask::r-x mask::r-x
other::---""") other::---""")
dir_acl = AccessControlList((), """user::rwx dir_acl = AccessControlLists((), """user::rwx
user:root:rwx user:root:rwx
group::r-x group::r-x
group:root:r-x group:root:r-x
mask::r-x mask::r-x
other::---""", other::---
"""user::rwx default:user::rwx
user:root:--- default:user:root:---
group::r-x default:group::r-x
mask::r-x default:mask::r-x
other::---""") default:other::---""")
acl1 = AccessControlList(('1',), """user::r-- acl1 = AccessControlLists(('1',), """user::r--
user:ben:--- user:ben:---
group::--- group::---
group:root:--- group:root:---
mask::--- mask::---
other::---""") other::---""")
acl2 = AccessControlList(('2',), """user::rwx acl2 = AccessControlLists(('2',), """user::rwx
group::r-x group::r-x
group:ben:rwx group:ben:rwx
mask::--- mask::---
other::---""") other::---""")
acl3 = AccessControlList(('3',), """user::rwx acl3 = AccessControlLists(('3',), """user::rwx
user:root:--- user:root:---
group::r-x group::r-x
mask::--- mask::---
other::---""") other::---""")
empty_acl = AccessControlList((), "user::rwx\ngroup::---\nother::---") empty_acl = AccessControlLists((), "user::rwx\ngroup::---\nother::---")
acl_testdir1 = rpath.RPath(Globals.local_connection, 'testfiles/acl_test1') acl_testdir1 = rpath.RPath(Globals.local_connection, 'testfiles/acl_test1')
acl_testdir2 = rpath.RPath(Globals.local_connection, 'testfiles/acl_test2') acl_testdir2 = rpath.RPath(Globals.local_connection, 'testfiles/acl_test2')
def make_temp(self): def make_temp(self):
...@@ -239,25 +241,25 @@ other::---""") ...@@ -239,25 +241,25 @@ other::---""")
def testBasic(self): def testBasic(self):
"""Test basic writing and reading of ACLs""" """Test basic writing and reading of ACLs"""
self.make_temp() self.make_temp()
new_acl = AccessControlList(()) new_acl = AccessControlLists(())
tempdir.chmod(0700) tempdir.chmod(0700)
new_acl.read_from_rp(tempdir) new_acl.read_from_rp(tempdir)
assert new_acl.is_basic(), new_acl.acl_text assert new_acl.is_basic(), str(new_acl)
assert not new_acl == self.sample_acl assert not new_acl == self.sample_acl
assert new_acl != self.sample_acl assert new_acl != self.sample_acl
assert new_acl == self.empty_acl, \ assert new_acl == self.empty_acl, \
(new_acl.acl_text, self.empty_acl.acl_text) (str(new_acl), str(self.empty_acl))
self.sample_acl.write_to_rp(tempdir) self.sample_acl.write_to_rp(tempdir)
new_acl.read_from_rp(tempdir) new_acl.read_from_rp(tempdir)
assert new_acl.acl_text == self.sample_acl.acl_text, \ assert str(new_acl) == str(self.sample_acl), \
(new_acl.acl_text, self.sample_acl.acl_text) (str(new_acl), str(self.sample_acl))
assert new_acl == self.sample_acl assert new_acl == self.sample_acl
def testBasicDir(self): def testBasicDir(self):
"""Test reading and writing of ACL w/ defaults to directory""" """Test reading and writing of ACL w/ defaults to directory"""
self.make_temp() self.make_temp()
new_acl = AccessControlList(()) new_acl = AccessControlLists(())
new_acl.read_from_rp(tempdir) new_acl.read_from_rp(tempdir)
assert new_acl.is_basic() assert new_acl.is_basic()
assert new_acl != self.dir_acl assert new_acl != self.dir_acl
...@@ -273,7 +275,12 @@ other::---""") ...@@ -273,7 +275,12 @@ other::---""")
"""Test writing a record and reading it back""" """Test writing a record and reading it back"""
record = ACL2Record(self.sample_acl) record = ACL2Record(self.sample_acl)
new_acl = Record2ACL(record) new_acl = Record2ACL(record)
assert new_acl == self.sample_acl if new_acl != self.sample_acl:
print "New_acl", new_acl.entry_list
print "sample_acl", self.sample_acl.entry_list
print "New_acl text", str(new_acl)
print "sample acl text", str(self.sample_acl)
assert 0
record2 = ACL2Record(self.dir_acl) record2 = ACL2Record(self.dir_acl)
new_acl2 = Record2ACL(record2) new_acl2 = Record2ACL(record2)
...@@ -358,7 +365,7 @@ other::--- ...@@ -358,7 +365,7 @@ other::---
Time.setcurtime(10000) Time.setcurtime(10000)
AccessControlListFile.open_file() AccessControlListFile.open_file()
for rp in [self.acl_testdir1, rp1, rp2, rp3]: for rp in [self.acl_testdir1, rp1, rp2, rp3]:
acl = AccessControlList(rp.index) acl = AccessControlLists(rp.index)
acl.read_from_rp(rp) acl.read_from_rp(rp)
AccessControlListFile.write_object(acl) AccessControlListFile.write_object(acl)
AccessControlListFile.close_file() AccessControlListFile.close_file()
......
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