From: Evan Broder Date: Thu, 19 Nov 2009 07:29:27 +0000 (-0500) Subject: Merge branch 'pts-orm' X-Git-Tag: 0.1.0~11 X-Git-Url: http://xvm.mit.edu/gitweb/invirt/packages/python-afs.git/commitdiff_plain/31ac6fffbb28751cd390baab01c708e7fb607c26?hp=09b4d45674f9d37d44c68f52c9a965b14b7e336f Merge branch 'pts-orm' --- diff --git a/afs/_pts.pyx b/afs/_pts.pyx index bef7139..c347037 100644 --- a/afs/_pts.pyx +++ b/afs/_pts.pyx @@ -1,5 +1,6 @@ from afs cimport * from afs import pyafs_error +import re cdef import from "afs/ptuser.h": enum: @@ -71,6 +72,27 @@ cdef import from "afs/pterror.h": enum: PRNOENT +cdef import from "krb5/krb5.h": + struct _krb5_context: + pass + struct krb5_principal_data: + pass + + ctypedef _krb5_context * krb5_context + ctypedef krb5_principal_data * krb5_principal + + ctypedef long krb5_int32 + ctypedef krb5_int32 krb5_error_code + krb5_error_code krb5_init_context(krb5_context *) + krb5_error_code krb5_parse_name(krb5_context, char *, krb5_principal *) + krb5_error_code krb5_unparse_name(krb5_context, krb5_principal, char **) + krb5_error_code krb5_524_conv_principal(krb5_context, krb5_principal, char *, char *, char *) + krb5_error_code krb5_425_conv_principal(krb5_context, char *, char *, char *, krb5_principal *) + krb5_error_code krb5_get_host_realm(krb5_context, char *, char ***) + void krb5_free_host_realm(krb5_context, char **) + void krb5_free_principal(krb5_context, krb5_principal) + void krb5_free_context(krb5_context) + cdef class PTEntry: cdef public afs_int32 flags cdef public afs_int32 id @@ -117,6 +139,28 @@ cdef int _ptentry_to_c(prcheckentry * c_entry, PTEntry p_entry) except -1: strncpy(c_entry.name, p_entry.name, sizeof(c_entry.name)) return 0 +cdef object kname_re = re.compile(r'^([^.].*?)(? 0: @@ -549,3 +607,90 @@ cdef class PTS: code = ubik_PR_SetFieldsEntry(self.client, 0, id, mask, flags, ngroups, nusers, 0, 0) pyafs_error(code) + + def _AfsToKrb5(self, afs_name): + """Convert an AFS principal to a Kerberos v5 one.""" + cdef krb5_context ctx = NULL + cdef krb5_principal princ = NULL + cdef krb5_error_code code = 0 + cdef char * krb5_princ = NULL + cdef char *name = NULL, *inst = NULL, *realm = NULL + cdef object pname, pinst, prealm + + if '@' in afs_name: + pname, prealm = afs_name.rsplit('@', 1) + prealm = prealm.upper() + krb4_name = '%s@%s' % (pname, prealm) + else: + krb4_name = '%s@%s' % (afs_name, self.realm) + + pname, pinst, prealm = kname_parse(krb4_name) + if pname: + name = pname + if pinst: + inst = pinst + if prealm: + realm = prealm + + code = krb5_init_context(&ctx) + try: + pyafs_error(code) + + code = krb5_425_conv_principal(ctx, name, inst, realm, &princ) + try: + pyafs_error(code) + + code = krb5_unparse_name(ctx, princ, &krb5_princ) + try: + pyafs_error(code) + + return krb5_princ + finally: + if krb5_princ is not NULL: + free(krb5_princ) + finally: + if princ is not NULL: + krb5_free_principal(ctx, princ) + finally: + if ctx is not NULL: + krb5_free_context(ctx) + + def _Krb5ToAfs(self, krb5_name): + """Convert a Kerberos v5 principal to an AFS one.""" + cdef krb5_context ctx = NULL + cdef krb5_principal k5_princ = NULL + cdef char *k4_name, *k4_inst, *k4_realm + cdef object afs_princ + cdef object afs_name, afs_realm + + k4_name = malloc(40) + k4_name[0] = '\0' + k4_inst = malloc(40) + k4_inst[0] = '\0' + k4_realm = malloc(40) + k4_realm[0] = '\0' + + code = krb5_init_context(&ctx) + try: + pyafs_error(code) + + code = krb5_parse_name(ctx, krb5_name, &k5_princ) + try: + pyafs_error(code) + + code = krb5_524_conv_principal(ctx, k5_princ, k4_name, k4_inst, k4_realm) + pyafs_error(code) + + afs_princ = kname_unparse(k4_name, k4_inst, k4_realm) + afs_name, afs_realm = afs_princ.rsplit('@', 1) + + if k4_realm == self.realm: + return afs_name + else: + return '%s@%s' % (afs_name, afs_realm.lower()) + finally: + if k5_princ is not NULL: + krb5_free_principal(ctx, k5_princ) + finally: + if ctx is not NULL: + krb5_free_context(ctx) diff --git a/afs/pts.py b/afs/pts.py new file mode 100644 index 0000000..cfbfb7d --- /dev/null +++ b/afs/pts.py @@ -0,0 +1,377 @@ +import collections +import _pts + +class PTRelationSet(collections.MutableSet): + """Collection class for the groups/members of a PTEntry. + + This class, which acts like a set, is actually a view of the + groups or members associated with a PTS Entry. Changes to this + class are immediately reflected to the PRDB. + + Attributes: + _ent: The PTEntry whose groups/members this instance + represents + _set: If defined, the set of either groups or members for this + instance's PTEntry + """ + def __init__(self, ent): + """Initialize a PTRelationSet class. + + Args: + ent: The PTEntry this instance should be associated with. + """ + super(PTRelationSet, self).__init__() + + self._ent = ent + + def _loadSet(self): + """Load the membership/groups for this instance's PTEntry. + + If they have not previously been loaded, this method updates + self._set with the set of PTEntries that are either members of + this group, or the groups that this entry is a member of. + """ + if not hasattr(self, '_set'): + self._set = set(self._ent._pts.getEntry(m) for m in + self._ent._pts._ListMembers(self._ent.id)) + + def _add(self, elt): + """Add a new PTEntry to this instance's internal representation. + + This method adds a new entry to this instance's set of + members/groups, but unlike PTRelationSet.add, it doesn't add + itself to the other instance's set. + + Args: + elt: The element to add. + """ + self._set.add(self._ent._pts.getEntry(elt)) + + def _discard(self, elt): + """Remove a PTEntry to this instance's internal representation. + + This method removes an entry from this instance's set of + members/groups, but unlike PTRelationSet.discard, it doesn't + remove itself from the other instance's set. + + Args: + elt: The element to discard. + """ + self._set.discard(self._ent._pts.getEntry(elt)) + + def __len__(self): + """Count the members/groups in this set. + + Returns: + The number of entities in this instance. + """ + self._loadSet() + return len(self._set) + + def __iter__(self): + """Iterate over members/groups in this set + + Returns: + An iterator that loops over the members/groups of this + set. + """ + self._loadSet() + return iter(self._set) + + def __contains__(self, name): + """Test if a PTEntry is connected to this instance. + + If the membership of the group hasn't already been loaded, + this method takes advantage of the IsAMemberOf lookup to test + for membership. + + This has the convenient advantage of working even when the + user doens't have permission to enumerate the group's + membership. + + Args: + name: The element whose membership is being tested. + + Returns: + True, if name is a member of self (or if self is a member + of name); otherwise, False + """ + name = self._ent._pts.getEntry(name) + if hasattr(self, '_set'): + return name in self._set + else: + if self._ent.id < 0: + return self._ent._pts._IsAMemberOf(name.id, self._ent.id) + else: + return self._ent._pts._IsAMemberOf(self._ent.id, name.id) + + def __repr__(self): + self._loadSet() + return repr(self._set) + + def add(self, elt): + """Add one new entity to a group. + + This method will add a new user to a group, regardless of + whether this instance represents a group or a user. The change + is also immediately reflected to the PRDB. + + Raises: + TypeError: If you try to add a grop group to a group, or a + user to a user + """ + elt = self._ent._pts.getEntry(elt) + if elt in self: + return + + if self._ent.id < 0: + if elt.id < 0: + raise TypeError( + "Adding group '%s' to group '%s' is not supported." % + (elt, self._ent)) + + self._ent._pts._AddToGroup(elt.id, self._ent.id) + + elt.groups._add(self._ent) + else: + if elt.id > 0: + raise TypeError( + "Can't add user '%s' to user '%s'." % + (elt, self._ent)) + + self._ent._pts._AddToGroup(self._ent.id, elt.id) + + elt.members._add(self._ent) + + self._add(elt) + + def discard(self, elt): + """Remove one entity from a group. + + This method will remove a user from a group, regardless of + whether this instance represents a group or a user. The change + is also immediately reflected to the PRDB. + """ + elt = self._ent._pts.getEntry(elt) + if elt not in self: + return + + if self._ent.id < 0: + self._ent._pts._RemoveFromGroup(elt.id, self._ent.id) + elt.groups._discard(self._ent) + else: + self._ent._pts._RemoveFromGroup(self._ent.id, elt.id) + elt.members._discard(self._ent) + + self._discard(elt) + + +class PTEntry(object): + """An entry in the AFS protection database. + + PTEntry represents a user or group in the AFS protection + database. Each PTEntry is associated with a particular connection + to the protection database. + + PTEntry instances should not be created directly. Instead, use the + "getEntry" method of the PTS object. + + If a PTS connection is authenticated, it should be possible to + change most attributes on a PTEntry. These changes are immediately + propogated to the protection database. + + Attributes: + id: The PTS ID of the entry + name: The username or group name of the entry + count: For users, the number of groups they are a member of; for + groups, the number of users in that group + flags: An integer representation of the flags set on a given + entry + ngroups: The number of additional groups this entry is allowed + to create + nusers: Only meaningful for foreign-cell groups, where it + indicates the ID of the next entry to be created from that + cell. + owner: A PTEntry object representing the owner of a given entry. + creator: A PTEntry object representing the creator of a given + entry. This field is read-only. + + groups: For users, this contains a collection class representing + the set of groups the user is a member of. + users: For groups, this contains a collection class representing + the members of this group. + """ + _attrs = ('id', 'name', 'count', 'flags', 'ngroups', 'nusers') + _entry_attrs = ('owner', 'creator') + + def __new__(cls, pts, id=None, name=None): + if id is None: + if name is None: + raise TypeError('Must specify either a name or an id.') + else: + id = pts._NameToId(name) + + if id not in pts._cache: + if name is None: + name = pts._IdToName(id) + + inst = super(PTEntry, cls).__new__(cls) + inst._pts = pts + inst._id = id + inst._name = name + if id < 0: + inst.members = PTRelationSet(inst) + else: + inst.groups = PTRelationSet(inst) + pts._cache[id] = inst + return pts._cache[id] + + def __repr__(self): + if self.name != '': + return '' % self.name + else: + return '' % self.id + + def _get_id(self): + return self._id + def _set_id(self, val): + del self._pts._cache[self._id] + self._pts._ChangeEntry(self.id, newid=val) + self._id = val + self._pts._cache[val] = self + id = property(_get_id, _set_id) + + def _get_name(self): + return self._name + def _set_name(self, val): + self._pts._ChangeEntry(self.id, newname=val) + self._name = val + name = property(_get_name, _set_name) + + def _get_count(self): + self._loadEntry() + return self._count + count = property(_get_count) + + def _get_flags(self): + self._loadEntry() + return self._flags + def _set_flags(self, val): + self._pts._SetFields(self.id, access=val) + self._flags = val + flags = property(_get_flags, _set_flags) + + def _get_ngroups(self): + self._loadEntry() + return self._ngroups + def _set_ngroups(self, val): + self._pts._SetFields(self.id, groups=val) + self._ngroups = val + ngroups = property(_get_ngroups, _set_ngroups) + + def _get_nusers(self): + self._loadEntry() + return self._nusers + def _set_nusers(self, val): + self._pts._SetFields(self.id, users=val) + self._nusers = val + nusers = property(_get_nusers, _set_nusers) + + def _get_owner(self): + self._loadEntry() + return self._owner + def _set_owner(self, val): + self._pts._ChangeEntry(self.id, newoid=self._pts.getEntry(val).id) + self._owner = val + owner = property(_get_owner, _set_owner) + + def _get_creator(self): + self._loadEntry() + return self._creator + creator = property(_get_creator) + + def _loadEntry(self): + if not hasattr(self, '_flags'): + info = self._pts._ListEntry(self._id) + for field in self._attrs: + setattr(self, '_%s' % field, getattr(info, field)) + for field in self._entry_attrs: + setattr(self, '_%s' % field, self._pts.getEntry(getattr(info, field))) + +class PTS(_pts.PTS): + """A connection to an AFS protection database. + + This class represents a connection to the AFS protection database + for a particular cell. + + Both the umax and gmax attributes can be changed if the connection + was authenticated by a principal on system:administrators for the + cell. + + For sufficiently privileged and authenticated connections, + iterating over a PTS object will yield all entries in the + protection database, in no particular order. + + Args: + cell: The cell to connect to. If None (the default), PTS + connects to the workstations home cell. + sec: The security level to connect with, an integer from 0 to 3: + - 0: unauthenticated connection + - 1: try authenticated, then fall back to unauthenticated + - 2: fail if an authenticated connection can't be established + - 3: same as 2, plus encrypt all traffic to the protection + server + + Attributes: + realm: The Kerberos realm against which this cell authenticates + umax: The maximum user ID currently assigned (the next ID + assigned will be umax + 1) + gmax: The maximum (actually minimum) group ID currently assigned + (the next ID assigned will be gmax - 1, since group IDs are + negative) + """ + def __init__(self, *args, **kwargs): + self._cache = {} + + def __iter__(self): + for pte in self._ListEntries(): + yield self.getEntry(pte.id) + + def getEntry(self, ident): + """Retrieve a particular PTEntry from this cell. + + getEntry accepts either a name or PTS ID as an argument, and + returns a PTEntry object with that name or ID. + """ + if isinstance(ident, PTEntry): + if ident._pts is not self: + raise TypeError("Entry '%s' is from a different cell." % + elt) + return ident + + elif isinstance(ident, basestring): + return PTEntry(self, name=ident) + else: + return PTEntry(self, id=ident) + + def expire(self): + """Flush the cache of PTEntry objects. + + This method will disconnect all PTEntry objects from this PTS + object and flush the cache. + """ + for elt in self._cache.keys(): + del self._cache[elt]._pts + del self._cache[elt] + + def _get_umax(self): + return self._ListMax()[0] + def _set_umax(self, val): + self._SetMaxUserId(val) + umax = property(_get_umax, _set_umax) + + def _get_gmax(self): + return self._ListMax()[1] + def _set_gmax(self, val): + self._SetMaxGroupId(val) + gmax = property(_get_gmax, _set_gmax) diff --git a/setup.py b/setup.py index 0a59340..3e593e3 100755 --- a/setup.py +++ b/setup.py @@ -45,7 +45,7 @@ setup( packages=['afs', 'afs.tests'], ext_modules=[ PyAFSExtension("afs.afs"), - PyAFSExtension("afs._pts"), + PyAFSExtension("afs._pts", libraries=['krb5']), PyAFSExtension("afs._acl"), ], cmdclass= {"build_ext": build_ext}