First attempt at an ORM-like interface to the PRDB.
authorEvan Broder <broder@mit.edu>
Mon, 27 Jul 2009 08:08:59 +0000 (01:08 -0700)
committerEvan Broder <broder@mit.edu>
Thu, 19 Nov 2009 07:29:05 +0000 (02:29 -0500)
Missing features that should eventually be added:
 - Create/delete users/groups

 - Expose afs._pts.PTS._AfsToKrb5 and afs._pts.PTS._Krb5ToAfs.

   _Krb5ToAfs could be exposed as a method to return a PTEntry from a
   Kerberos principal, while _AfsToKrb5 could be a method (or
   property) of a PTEntry that returns the equivalent Kerberos
   principal.

Signed-off-by: Evan Broder <broder@mit.edu>

afs/pts.py [new file with mode: 0644]

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)