Merge branch 'pts-orm'
authorEvan Broder <broder@mit.edu>
Thu, 19 Nov 2009 07:29:27 +0000 (02:29 -0500)
committerEvan Broder <broder@mit.edu>
Thu, 19 Nov 2009 07:29:27 +0000 (02:29 -0500)
afs/_pts.pyx
afs/pts.py [new file with mode: 0644]
setup.py

index bef7139..c347037 100644 (file)
@@ -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'^([^.].*?)(?<!\\)(?:\.(.*?))?(?<!\\)@([^@]*)$')
+
+cdef object kname_parse(fullname):
+    """Parse a krb4-style principal into a name, instance, and realm."""
+    cdef object re_match = kname_re.match(fullname)
+    if not re_match:
+        return None
+    else:
+        princ = re_match.groups()
+        return tuple([re.sub(r'\\(.)', r'\1', x) if x else x for x in princ])
+
+cdef object kname_unparse(name, inst, realm):
+    """Unparse a name, instance, and realm into a single krb4
+    principal string."""
+    name = re.sub('r([.\\@])', r'\\\1', name)
+    inst = re.sub('r([.\\@])', r'\\\1', inst)
+    realm = re.sub(r'([\\@])', r'\\\1', realm)
+    if inst:
+        return '%s.%s@%s' % (name, inst, realm)
+    else:
+        return '%s@%s' % (name, realm)
+
 cdef class PTS:
     """
     A PTS object is essentially a handle to talk to the server in a
@@ -131,14 +175,20 @@ cdef class PTS:
       - 2: fail if an authenticated connection can't be established
       - 3: same as 2, plus encrypt all traffic to the protection
         server
+
+    The realm attribute is the Kerberos realm against which this cell
+    authenticates.
     """
     cdef ubik_client * client
     cdef readonly object cell
+    cdef readonly object realm
 
     def __cinit__(self, cell=None, sec=1):
         cdef afs_int32 code
         cdef afsconf_dir *cdir
         cdef afsconf_cell info
+        cdef krb5_context context
+        cdef char ** hrealms = NULL
         cdef char * c_cell
         cdef ktc_principal prin
         cdef ktc_token token
@@ -167,6 +217,14 @@ cdef class PTS:
         code = afsconf_GetCellInfo(cdir, c_cell, "afsprot", &info)
         pyafs_error(code)
 
+        code = krb5_init_context(&context)
+        pyafs_error(code)
+        code = krb5_get_host_realm(context, info.hostName[0], &hrealms)
+        pyafs_error(code)
+        self.realm = hrealms[0]
+        krb5_free_host_realm(context, hrealms)
+        krb5_free_context(context)
+
         self.cell = info.name
 
         if sec > 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 = <char *>malloc(40)
+        k4_name[0] = '\0'
+        k4_inst = <char *>malloc(40)
+        k4_inst[0] = '\0'
+        k4_realm = <char *>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 (file)
index 0000000..cfbfb7d
--- /dev/null
@@ -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 '<PTEntry: %s>' % self.name
+        else:
+            return '<PTEntry: PTS ID %s>' % 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)
index 0a59340..3e593e3 100755 (executable)
--- 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}