[CalendarServer-changes] [3721] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Mon Feb 23 20:22:52 PST 2009


Revision: 3721
          http://trac.macosforge.org/projects/calendarserver/changeset/3721
Author:   cdaboo at apple.com
Date:     2009-02-23 20:22:48 -0800 (Mon, 23 Feb 2009)
Log Message:
-----------
Better support for multiple realms with Kerberos auth - requires new attributes
in OD directory to help map Kerberos principals to directory records.

Modified Paths:
--------------
    CalendarServer/trunk/calendarserver/tools/util.py
    CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.test_acl.patch
    CalendarServer/trunk/twistedcaldav/authkerb.py
    CalendarServer/trunk/twistedcaldav/directory/aggregate.py
    CalendarServer/trunk/twistedcaldav/directory/apache.py
    CalendarServer/trunk/twistedcaldav/directory/appleopendirectory.py
    CalendarServer/trunk/twistedcaldav/directory/directory.py
    CalendarServer/trunk/twistedcaldav/directory/idirectory.py
    CalendarServer/trunk/twistedcaldav/directory/principal.py
    CalendarServer/trunk/twistedcaldav/directory/sqldb.py
    CalendarServer/trunk/twistedcaldav/directory/sudo.py
    CalendarServer/trunk/twistedcaldav/directory/test/test_opendirectory.py
    CalendarServer/trunk/twistedcaldav/directory/test/test_opendirectoryrecords.py
    CalendarServer/trunk/twistedcaldav/directory/test/test_principal.py
    CalendarServer/trunk/twistedcaldav/directory/wiki.py
    CalendarServer/trunk/twistedcaldav/extensions.py
    CalendarServer/trunk/twistedcaldav/test/test_kerberos.py

Modified: CalendarServer/trunk/calendarserver/tools/util.py
===================================================================
--- CalendarServer/trunk/calendarserver/tools/util.py	2009-02-24 04:09:21 UTC (rev 3720)
+++ CalendarServer/trunk/calendarserver/tools/util.py	2009-02-24 04:22:48 UTC (rev 3721)
@@ -80,6 +80,7 @@
     recordType = "dummy",
     guid = "8EF0892F-7CB6-4B8E-B294-7C5A5321136A",
     shortNames = ("dummy",),
+    authIDs = set(),
     fullName = "Dummy McDummerson",
     firstName = "Dummy",
     lastName = "McDummerson",

Modified: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.test_acl.patch
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.test_acl.patch	2009-02-24 04:09:21 UTC (rev 3720)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.test_acl.patch	2009-02-24 04:22:48 UTC (rev 3721)
@@ -10,7 +10,7 @@
  from twisted.web2.dav.util import davXMLFromStream
  from twisted.web2.dav.auth import TwistedPasswordProperty, IPrincipal, DavRealm, TwistedPropertyChecker, AuthenticationWrapper
  
-@@ -38,6 +39,22 @@
+@@ -38,6 +39,25 @@
  from twisted.web2.dav.test.util import Site, serialize
  from twisted.web2.dav.test.test_resource import TestResource, TestDAVPrincipalResource
  
@@ -22,6 +22,9 @@
 +    def principalForUser(self, user):
 +        return self.principalForShortName('users', user)
 +
++    def principalForAuthID(self, creds):
++        return self.principalForShortName('users', creds.username)
++
 +    def principalForShortName(self, type, shortName):
 +        typeResource = self.children.get(type, None)
 +        user = None
@@ -33,7 +36,7 @@
  class ACL(twisted.web2.dav.test.util.TestCase):
      """
      RFC 3744 (WebDAV ACL) tests.
-@@ -46,8 +63,18 @@
+@@ -46,8 +66,18 @@
          if not hasattr(self, "docroot"):
              self.docroot = self.mktemp()
              os.mkdir(self.docroot)
@@ -53,7 +56,7 @@
              portal = Portal(DavRealm())
              portal.registerChecker(TwistedPropertyChecker())
  
-@@ -56,26 +83,14 @@
+@@ -56,26 +86,14 @@
              loginInterfaces = (IPrincipal,)
  
              self.site = Site(AuthenticationWrapper(
@@ -82,7 +85,7 @@
          for name, acl in (
              ("none"       , self.grant()),
              ("read"       , self.grant(davxml.Read())),
-@@ -361,9 +376,7 @@
+@@ -361,9 +379,7 @@
                  if method == "GET":
                      ok = responsecode.OK
                  elif method == "REPORT":
@@ -93,7 +96,7 @@
                  else:
                      raise AssertionError("We shouldn't be here.  (method = %r)" % (method,))
  
-@@ -377,6 +390,9 @@
+@@ -377,6 +393,9 @@
                      path = os.path.join(self.docroot, name)
  
                      request = SimpleRequest(self.site, method, "/" + name)

Modified: CalendarServer/trunk/twistedcaldav/authkerb.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/authkerb.py	2009-02-24 04:09:21 UTC (rev 3720)
+++ CalendarServer/trunk/twistedcaldav/authkerb.py	2009-02-24 04:22:48 UTC (rev 3721)
@@ -1,6 +1,6 @@
 ##
 # Copyright (c) 2001-2004 Twisted Matrix Laboratories.
-# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+# Copyright (c) 2006-2009 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -183,8 +183,9 @@
 
     implements(credentials.ICredentials)
 
-    def __init__(self, username):
+    def __init__(self, principal, username):
         
+        self.principal = principal
         self.username = username
         
 class NegotiateCredentialFactory(KerberosCredentialFactoryBase):
@@ -233,7 +234,8 @@
             raise error.UnauthorizedLogin('Bad credentials: %s' % (ex[0],))
 
         response = kerberos.authGSSServerResponse(context)
-        username = kerberos.authGSSServerUserName(context)
+        principal = kerberos.authGSSServerUserName(context)
+        username = principal
         realmname = ""
         
         # Username may include realm suffix which we want to strip
@@ -242,14 +244,11 @@
             username = splits[0]
             realmname = splits[1]
         
-        # We currently do not support cross-realm authentciation, so we
+        # We currently do not support cross-realm authentication, so we
         # must verify that the realm we got exactly matches the one we expect.
         if realmname != self.realm:
-            self.log_error("authGSSServer Realms do not match: %s vs %s" % (realmname, self.realm,))
-            kerberos.authGSSServerClean(context)
-            raise error.UnauthorizedLogin('Bad credentials: mismatched realm')
+            username = principal
 
-
         # Close the context
         try:
             kerberos.authGSSServerClean(context);
@@ -270,7 +269,7 @@
 
         request.addResponseFilter(responseFilterAddWWWAuthenticate)
 
-        return succeed(NegotiateCredentials(username))
+        return succeed(NegotiateCredentials(principal, username))
 
 class NegotiateCredentialsChecker(object):
 

Modified: CalendarServer/trunk/twistedcaldav/directory/aggregate.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/aggregate.py	2009-02-24 04:09:21 UTC (rev 3720)
+++ CalendarServer/trunk/twistedcaldav/directory/aggregate.py	2009-02-24 04:22:48 UTC (rev 3721)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+# Copyright (c) 2006-2009 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -102,6 +102,9 @@
     def recordWithUID(self, uid):
         return self._queryAll("recordWithUID", uid)
 
+    def recordWithAuthID(self, authID):
+        return self._queryAll("recordWithAuthID", authID)
+
     def recordWithCalendarUserAddress(self, address):
         return self._queryAll("recordWithCalendarUserAddress", address)
 
@@ -151,17 +154,11 @@
     userRecordTypes = [DirectoryService.recordType_users]
 
     def requestAvatarId(self, credentials):
-        for type in self.userRecordTypes:
-            user = self.recordWithShortName(
-                type,
-                credentials.credentials.username)
-
-            if user:
-                return self.serviceForRecordType(
-                    type).requestAvatarId(credentials)
         
-        raise UnauthorizedLogin("No such user: %s" % (
-                credentials.credentials.username,))
+        if credentials.authnPrincipal:
+            return credentials.authnPrincipal.record.service.requestAvatarId(credentials)
+        
+        raise UnauthorizedLogin("No such user: %s" % (credentials.credentials.username,))
 
 class DuplicateRecordTypeError(DirectoryError):
     """

Modified: CalendarServer/trunk/twistedcaldav/directory/apache.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/apache.py	2009-02-24 04:09:21 UTC (rev 3720)
+++ CalendarServer/trunk/twistedcaldav/directory/apache.py	2009-02-24 04:22:48 UTC (rev 3721)
@@ -144,12 +144,6 @@
             recordType            = recordType,
             guid                  = None,
             shortNames            = (shortName,),
-            fullName              = None,
-            firstName             = None,
-            lastName              = None,
-            emailAddresses        = set(),
-            calendarUserAddresses = set(),
-            autoSchedule          = False,
         )
 
 class AbstractUserRecord(AbstractDirectoryRecord):

Modified: CalendarServer/trunk/twistedcaldav/directory/appleopendirectory.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/appleopendirectory.py	2009-02-24 04:09:21 UTC (rev 3720)
+++ CalendarServer/trunk/twistedcaldav/directory/appleopendirectory.py	2009-02-24 04:22:48 UTC (rev 3721)
@@ -288,6 +288,9 @@
 
     recordWithUID = recordWithGUID
 
+    def recordWithAuthID(self, authID):
+        return self._recordWithAttribute("authIDs", "disabled authIDs", "authID", authID)
+
     def _recordWithAttribute(self, cacheKey, disabledKey, lookupKey, value):
         def lookup():
             for recordType in self.recordTypes():
@@ -436,11 +439,13 @@
         if lookup is None:
             records = {}
             guids   = {}
+            authIDs = {}
             emails  = {}
 
-            disabledNames  = set()
-            disabledGUIDs  = set()
-            disabledEmails = set()
+            disabledNames   = set()
+            disabledGUIDs   = set()
+            disabledAuthIDs = set()
+            disabledEmails  = set()
             
             if recordType == self.recordType_groups:
                 groupsForGUID = {}
@@ -452,11 +457,13 @@
 
             records = storage["records"]
             guids   = storage["guids"]
+            authIDs = storage["authIDs"]
             emails  = storage["emails"]
 
-            disabledNames  = storage["disabled names"]
-            disabledGUIDs  = storage["disabled guids"]
-            disabledEmails = storage["disabled emails"]
+            disabledNames   = storage["disabled names"]
+            disabledGUIDs   = storage["disabled guids"]
+            disabledAuthIDs = storage["disabled authIDs"]
+            disabledEmails  = storage["disabled emails"]
             
             if recordType == self.recordType_groups:
                 groupsForGUID = storage["groupsForGUID"]
@@ -465,25 +472,38 @@
                 readOnlyProxiesForGUID = storage["readOnlyProxiesForGUID"]
 
         enabled_count = 0
-        for (recordShortName, value) in results:
-
-            # Now get useful record info.
-            recordGUID         = value.get(dsattributes.kDS1AttrGeneratedUID)
-            recordShortNames   = value.get(dsattributes.kDSNAttrRecordName)
-            if recordShortNames:
-                if isinstance(recordShortNames, str):
-                    recordShortNames = (recordShortNames,)
+        
+        def _uniqueTupleFromAttribute(attribute):
+            if attribute:
+                if isinstance(attribute, str):
+                    return (attribute,)
                 else:
                     s = set()
-                    recordShortNames = tuple([(s.add(x), x)[1] for x in recordShortNames if x not in s])
+                    return tuple([(s.add(x), x)[1] for x in attribute if x not in s])
             else:
-                recordShortNames = ()
-            recordFullName     = value.get(dsattributes.kDS1AttrDistinguishedName)
-            recordFirstName    = value.get(dsattributes.kDS1AttrFirstName)
-            recordLastName     = value.get(dsattributes.kDS1AttrLastName)
-            recordEmailAddress = value.get(dsattributes.kDSNAttrEMailAddress)
-            recordNodeName     = value.get(dsattributes.kDSNAttrMetaNodeLocation)
+                return ()
+            
+        def _setFromAttribute(attribute, lower=False):
+            if attribute:
+                if isinstance(attribute, str):
+                    return set((attribute.lower() if lower else attribute,))
+                else:
+                    return set([item.lower() if lower else item for item in attribute])
+            else:
+                return ()
+            
+        for (recordShortName, value) in results:
 
+            # Now get useful record info.
+            recordGUID           = value.get(dsattributes.kDS1AttrGeneratedUID)
+            recordShortNames     = _uniqueTupleFromAttribute(value.get(dsattributes.kDSNAttrRecordName))
+            recordAuthIDs        = _setFromAttribute(value.get(dsattributes.kDSNAttrAltSecurityIdentities))
+            recordFullName       = value.get(dsattributes.kDS1AttrDistinguishedName)
+            recordFirstName      = value.get(dsattributes.kDS1AttrFirstName)
+            recordLastName       = value.get(dsattributes.kDS1AttrLastName)
+            recordEmailAddresses = _setFromAttribute(value.get(dsattributes.kDSNAttrEMailAddress), lower=True)
+            recordNodeName       = value.get(dsattributes.kDSNAttrMetaNodeLocation)
+
             if not recordGUID:
                 self.log_debug("Record (%s)%s in node %s has no GUID; ignoring."
                                % (recordType, recordShortName, recordNodeName))
@@ -525,14 +545,6 @@
                 )
                 calendarUserAddresses = ()
 
-            # Get email address from directory record
-            recordEmailAddresses = set()
-            if isinstance(recordEmailAddress, str):
-                recordEmailAddresses.add(recordEmailAddress.lower())
-            elif isinstance(recordEmailAddress, list):
-                for addr in recordEmailAddresses:
-                    recordEmailAddresses.add(addr.lower())
-
             # Special case for groups, which have members.
             if recordType == self.recordType_groups:
                 memberGUIDs = value.get(dsattributes.kDSNAttrGroupMembers)
@@ -570,6 +582,7 @@
                 guid                  = recordGUID,
                 nodeName              = recordNodeName,
                 shortNames            = recordShortNames,
+                authIDs               = recordAuthIDs,
                 fullName              = recordFullName,
                 firstName             = recordFirstName,
                 lastName              = recordLastName,
@@ -592,6 +605,7 @@
 
                 disabledGUIDs.add(guid)
                 disabledNames.update(record.shortNames)
+                disabledAuthIDs.update(record.authIDs)
                 disabledEmails.update(record.emailAddresses)
 
                 if guid in guids:
@@ -604,6 +618,11 @@
                         del records[shortName]
                     except KeyError:
                         pass
+                for authID in record.authIDs:
+                    try:
+                        del authIDs[authID]
+                    except KeyError:
+                        pass
                 for email in record.emailAddresses:
                     try:
                         del emails[email]
@@ -659,6 +678,29 @@
                             else:
                                 records[shortName] = record
         
+                    # Index non-duplicate authIDs
+                    def disableAuthIDs(authID, record):
+                        self.log_warn("Auth ID %s disabled due to conflict for record: %s"
+                                      % (authID, record))
+        
+                        record.authIDs.remove(authID)
+                        disabledAuthIDs.add(authID)
+        
+                        if authID in authIDs:
+                            del authIDs[authID]
+        
+                    for authID in frozenset(recordAuthIDs):
+                        if authID in disabledAuthIDs:
+                            disableAuthIDs(authID, record)
+                        else:
+                            # Check for duplicates
+                            existing_record = authIDs.get(authID)
+                            if existing_record is not None:
+                                disableAuthIDs(authID, record)
+                                disableAuthIDs(authID, existing_record)
+                            else:
+                                authIDs[authID] = record
+        
                     # Index non-duplicate emails
                     def disableEmail(emailAddress, record):
                         self.log_warn("Email address %s disabled due to conflict for record: %s"
@@ -687,13 +729,15 @@
             # Replace the entire cache
             #
             storage = {
-                "status"         : "new",
-                "records"        : records,
-                "guids"          : guids,
-                "emails"         : emails,
-                "disabled names" : disabledNames,
-                "disabled guids" : disabledGUIDs,
-                "disabled emails": disabledEmails,
+                "status"           : "new",
+                "records"          : records,
+                "guids"            : guids,
+                "authIDs"          : authIDs,
+                "emails"           : emails,
+                "disabled names"   : disabledNames,
+                "disabled guids"   : disabledGUIDs,
+                "disabled authIDs" : disabledAuthIDs,
+                "disabled emails"  : disabledEmails,
             }
 
             # Add group indexing if needed
@@ -734,6 +778,7 @@
         attrs = [
             dsattributes.kDS1AttrGeneratedUID,
             dsattributes.kDSNAttrRecordName,
+            dsattributes.kDSNAttrAltSecurityIdentities,
             dsattributes.kDS1AttrDistinguishedName,
             dsattributes.kDS1AttrFirstName,
             dsattributes.kDS1AttrLastName,
@@ -803,6 +848,7 @@
             queryattr = {
                 "shortName" : dsattributes.kDSNAttrRecordName,
                 "guid"      : dsattributes.kDS1AttrGeneratedUID,
+                "authID"    : dsattributes.kDSNAttrAltSecurityIdentities,
                 "email"     : dsattributes.kDSNAttrEMailAddress,
             }.get(lookup[0])
             assert queryattr is not None, "Invalid type for record faulting query"
@@ -850,7 +896,7 @@
     Open Directory implementation of L{IDirectoryRecord}.
     """
     def __init__(
-        self, service, recordType, guid, nodeName, shortNames, fullName,
+        self, service, recordType, guid, nodeName, shortNames, authIDs, fullName,
         firstName, lastName, emailAddresses,
         calendarUserAddresses, autoSchedule, enabledForCalendaring,
         memberGUIDs, proxyGUIDs, readOnlyProxyGUIDs,
@@ -860,6 +906,7 @@
             recordType            = recordType,
             guid                  = guid,
             shortNames            = shortNames,
+            authIDs               = authIDs,
             fullName              = fullName,
             firstName             = firstName,
             lastName              = lastName,

Modified: CalendarServer/trunk/twistedcaldav/directory/directory.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/directory.py	2009-02-24 04:09:21 UTC (rev 3720)
+++ CalendarServer/trunk/twistedcaldav/directory/directory.py	2009-02-24 04:22:48 UTC (rev 3721)
@@ -90,9 +90,8 @@
         # implementation because you shouldn't have a principal object for a
         # disabled directory principal.
 
-        user = self.recordWithShortName(DirectoryService.recordType_users, credentials.credentials.username)
-        if user is None:
-            raise UnauthorizedLogin("No such user: %s" % (user,))
+        if credentials.authnPrincipal is None:
+            raise UnauthorizedLogin("No such user: %s" % (credentials.credentials.username,))
 
         # Handle Kerberos as a separate behavior
         try:
@@ -108,13 +107,13 @@
                 credentials.authzPrincipal.principalURL(),
             )
         else:
-            if user.verifyCredentials(credentials.credentials):
+            if credentials.authnPrincipal.record.verifyCredentials(credentials.credentials):
                 return (
                     credentials.authnPrincipal.principalURL(),
                     credentials.authzPrincipal.principalURL(),
                 )
             else:
-                raise UnauthorizedLogin("Incorrect credentials for %s" % (user,)) 
+                raise UnauthorizedLogin("Incorrect credentials for %s" % (credentials.credentials.username,)) 
 
     def recordTypes(self):
         raise NotImplementedError("Subclass must implement recordTypes()")
@@ -137,6 +136,12 @@
                 return record
         return None
 
+    def recordWithAuthID(self, authID):
+        for record in self.allRecords():
+            if authID in record.authIDs:
+                return record
+        return None
+
     def recordWithCalendarUserAddress(self, address):
         address = normalizeCUAddr(address)
         if address.startswith("urn:uuid:"):
@@ -267,9 +272,9 @@
         )
 
     def __init__(
-        self, service, recordType, guid, shortNames, fullName,
-        firstName, lastName, emailAddresses,
-        calendarUserAddresses, autoSchedule, enabledForCalendaring=None,
+        self, service, recordType, guid, shortNames=(), authIDs=set(), fullName=None,
+        firstName=None, lastName=None, emailAddresses=set(),
+        calendarUserAddresses=set(), autoSchedule=False, enabledForCalendaring=None,
         uid=None,
     ):
         assert service.realmName is not None
@@ -302,6 +307,7 @@
         self.guid                  = guid
         self.uid                   = uid
         self.shortNames            = shortNames
+        self.authIDs               = authIDs
         self.fullName              = fullName
         self.firstName             = firstName
         self.lastName              = lastName

Modified: CalendarServer/trunk/twistedcaldav/directory/idirectory.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/idirectory.py	2009-02-24 04:09:21 UTC (rev 3720)
+++ CalendarServer/trunk/twistedcaldav/directory/idirectory.py	2009-02-24 04:22:48 UTC (rev 3721)
@@ -95,6 +95,7 @@
     guid                  = Attribute("The GUID of this record.")
     uid                   = Attribute("The UID of this record.")
     shortNames            = Attribute("The names for this record.")
+    authIDs               = Attribute("Alternative security identities for this record.")
     fullName              = Attribute("The full name of this record.")
     firstName             = Attribute("The first name of this record.")
     lastName              = Attribute("The last name of this record.")

Modified: CalendarServer/trunk/twistedcaldav/directory/principal.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/principal.py	2009-02-24 04:09:21 UTC (rev 3720)
+++ CalendarServer/trunk/twistedcaldav/directory/principal.py	2009-02-24 04:22:48 UTC (rev 3721)
@@ -34,15 +34,18 @@
 from urllib import unquote
 from urlparse import urlparse
 
+from twisted.cred.credentials import UsernamePassword
 from twisted.python.failure import Failure
 from twisted.internet.defer import inlineCallbacks, returnValue
 from twisted.internet.defer import succeed
+from twisted.protocols.sip import DigestedCredentials
 from twisted.web2 import responsecode
 from twisted.web2.http import HTTPError
 from twisted.web2.dav import davxml
 from twisted.web2.dav.util import joinURL
 from twisted.web2.dav.noneprops import NonePropertyStore
 
+from twistedcaldav.authkerb import NegotiateCredentials
 from twistedcaldav.config import config
 from twistedcaldav.cache import DisabledCacheNotifier, PropfindCacheMixin
 
@@ -161,6 +164,20 @@
     def principalForUser(self, user):
         return self.principalForShortName(DirectoryService.recordType_users, user)
 
+    def principalForAuthID(self, user):
+        # Basic/Digest creds -> just lookup user name
+        if isinstance(user, UsernamePassword) or isinstance(user, DigestedCredentials):
+            return self.principalForUser(user.username)
+        elif isinstance(user, NegotiateCredentials):
+            authID = "Kerberos:%s" % (user.principal,)
+            principal = self.principalForRecord(self.directory.recordWithAuthID(authID))
+            if principal:
+                return principal
+            elif user.username:
+                return self.principalForUser(user.username)
+        
+        return None
+
     def principalForUID(self, uid):
         raise NotImplementedError("Subclass must implement principalForUID()")
 
@@ -519,8 +536,9 @@
     @inlineCallbacks
     def renderDirectoryBody(self, request):
 
+        extras = self.extraDirectoryBodyItems(request)
         output = (yield super(DirectoryPrincipalResource, self).renderDirectoryBody(request))
-        
+
         members = (yield self.groupMembers())
         
         memberships = (yield self.groupMemberships())
@@ -543,6 +561,7 @@
             """GUID: %s\n"""                   % (self.record.guid,),
             """Record type: %s\n"""            % (self.record.recordType,),
             """Short names: %s\n"""            % (",".join(self.record.shortNames),),
+            """Security Identities: %s\n"""    % (",".join(self.record.authIDs),),
             """Full name: %s\n"""              % (self.record.fullName,),
             """First name: %s\n"""             % (self.record.firstName,),
             """Last name: %s\n"""              % (self.record.lastName,),
@@ -554,10 +573,13 @@
             """\nGroup memberships:\n"""       , format_principals(memberships),
             """\nRead-write Proxy For:\n"""    , format_principals(proxyFor),
             """\nRead-only Proxy For:\n"""     , format_principals(readOnlyProxyFor),
-            """</pre></blockquote></div>""",
+            """%s</pre></blockquote></div>"""  % extras,
             output
         )))
 
+    def extraDirectoryBodyItems(self, request):
+        return ""
+
     ##
     # DAV
     ##
@@ -729,49 +751,12 @@
     """
     Directory calendar principal resource.
     """
-    @inlineCallbacks
-    def renderDirectoryBody(self, request):
 
-        output = (yield super(DirectoryPrincipalResource, self).renderDirectoryBody(request))
-        
-        members = (yield self.groupMembers())
-        
-        memberships = (yield self.groupMemberships())
-        
-        proxyFor = (yield self.proxyFor(True))
-        
-        readOnlyProxyFor = (yield self.proxyFor(False))
-        
-        returnValue("".join((
-            """<div class="directory-listing">"""
-            """<h1>Principal Details</h1>"""
-            """<pre><blockquote>"""
-            """Directory Information\n"""
-            """---------------------\n"""
-            """Directory GUID: %s\n"""         % (self.record.service.guid,),
-            """Realm: %s\n"""                  % (self.record.service.realmName,),
-            """\n"""
-            """Principal Information\n"""
-            """---------------------\n"""
-            """GUID: %s\n"""                   % (self.record.guid,),
-            """Record type: %s\n"""            % (self.record.recordType,),
-            """Short names: %s\n"""            % (",".join(self.record.shortNames),),
-            """Full name: %s\n"""              % (self.record.fullName,),
-            """First name: %s\n"""             % (self.record.firstName,),
-            """Last name: %s\n"""              % (self.record.lastName,),
-            """Email addresses:\n"""           , format_list(self.record.emailAddresses),
-            """Principal UID: %s\n"""          % (self.principalUID(),),
-            """Principal URL: %s\n"""          % (format_link(self.principalURL()),),
-            """\nAlternate URIs:\n"""          , format_list(format_link(u) for u in self.alternateURIs()),
-            """\nGroup members:\n"""           , format_principals(members),
-            """\nGroup memberships:\n"""       , format_principals(memberships),
-            """\nRead-write Proxy For:\n"""    , format_principals(proxyFor),
-            """\nRead-only Proxy For:\n"""     , format_principals(readOnlyProxyFor),
+    def extraDirectoryBodyItems(self, request):
+        return "".join((
             """\nCalendar homes:\n"""          , format_list(format_link(u) for u in self.calendarHomeURLs()),
             """\nCalendar user addresses:\n""" , format_list(format_link(a) for a in self.calendarUserAddresses()),
-            """</pre></blockquote></div>""",
-            output
-        )))
+        ))
 
     ##
     # CalDAV

Modified: CalendarServer/trunk/twistedcaldav/directory/sqldb.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/sqldb.py	2009-02-24 04:09:21 UTC (rev 3720)
+++ CalendarServer/trunk/twistedcaldav/directory/sqldb.py	2009-02-24 04:22:48 UTC (rev 3721)
@@ -350,9 +350,6 @@
             guid                  = guid,
             shortNames            = (shortName,),
             fullName              = name,
-            firstName             = None,
-            lastName              = None,
-            emailAddresses        = set(),
             calendarUserAddresses = calendarUserAddresses,
             autoSchedule          = autoSchedule,
         )

Modified: CalendarServer/trunk/twistedcaldav/directory/sudo.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/sudo.py	2009-02-24 04:09:21 UTC (rev 3720)
+++ CalendarServer/trunk/twistedcaldav/directory/sudo.py	2009-02-24 04:22:48 UTC (rev 3721)
@@ -100,14 +100,12 @@
         # We were checking if principal is enabled; seems unnecessary in current
         # implementation because you shouldn't have a principal object for a
         # disabled directory principal.
-        sudouser = self.recordWithShortName(
-            SudoDirectoryService.recordType_sudoers,
-            credentials.credentials.username)
 
-        if sudouser is None:
-            raise UnauthorizedLogin("No such user: %s" % (sudouser,))
+        if credentials.authnPrincipal is None or not hasattr(credentials.authnPrincipal, "record"):
+            raise UnauthorizedLogin("No such user: %s" % (credentials.credentials.username,))
+        sudouser = credentials.authnPrincipal.record
 
-        if sudouser.verifyCredentials(credentials.credentials):
+        if credentials.authnPrincipal.record.verifyCredentials(credentials.credentials):
             return (
                 credentials.authnPrincipal.principalURL(),
                 credentials.authzPrincipal.principalURL(),
@@ -129,12 +127,7 @@
             guid=None,
             shortNames=(shortName,),
             fullName=shortName,
-            firstName="",
-            lastName="",
-            emailAddresses=set(),
-            calendarUserAddresses=set(),
-            autoSchedule=False,
-            enabledForCalendaring=False)
+        )
 
         self.password = entry['password']
 

Modified: CalendarServer/trunk/twistedcaldav/directory/test/test_opendirectory.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/test_opendirectory.py	2009-02-24 04:09:21 UTC (rev 3720)
+++ CalendarServer/trunk/twistedcaldav/directory/test/test_opendirectory.py	2009-02-24 04:22:48 UTC (rev 3721)
@@ -66,6 +66,7 @@
                 guid                  = "B1F93EB1-DA93-4772-9141-81C250DA35B3",
                 nodeName              = "/LDAPv2/127.0.0.1",
                 shortNames            = ("user",),
+                authIDs               = set(),
                 fullName              = "Some user",
                 firstName             = "Some",
                 lastName              = "User",

Modified: CalendarServer/trunk/twistedcaldav/directory/test/test_opendirectoryrecords.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/test_opendirectoryrecords.py	2009-02-24 04:09:21 UTC (rev 3720)
+++ CalendarServer/trunk/twistedcaldav/directory/test/test_opendirectoryrecords.py	2009-02-24 04:22:48 UTC (rev 3721)
@@ -289,6 +289,22 @@
 
             self.verifyRecords(DirectoryService.recordType_users, ())
 
+        def test_duplicateAuthIDs(self):
+            self.loadRecords({
+                DirectoryService.recordType_users: [
+                    fakeODRecord("User 01"),
+                    fakeODRecord("User 02", email="shared at example.com"),
+                    fakeODRecord("User 03", email="shared at example.com"),
+                ],
+            })
+
+            self.verifyRecords(DirectoryService.recordType_users, ("user01", "user02", "user03"))
+            self.verifyDisabledRecords(DirectoryService.recordType_users, (), ())
+
+            self.assertTrue (self.service.recordWithShortName(DirectoryService.recordType_users, "user01").authIDs)
+            self.assertFalse(self.service.recordWithShortName(DirectoryService.recordType_users, "user02").authIDs)
+            self.assertFalse(self.service.recordWithShortName(DirectoryService.recordType_users, "user03").authIDs)
+
         def test_duplicateEmail(self):
             self.loadRecords({
                 DirectoryService.recordType_users: [
@@ -585,6 +601,50 @@
             self.verifyQuery(self.service.recordWithGUID, guidForShortName("location05"))
             self.verifyNoQuery(self.service.recordWithGUID, guidForShortName("location05"))
 
+        def test_negativeCacheAuthID(self):
+            self.loadRecords({
+                DirectoryService.recordType_users: [
+                    fakeODRecord("User 01"),
+                    fakeODRecord("User 02"),
+                    fakeODRecord("User 03"),
+                    fakeODRecord("User 04"),
+                ],
+                DirectoryService.recordType_groups: [
+                    fakeODRecord("Group 01"),
+                    fakeODRecord("Group 02"),
+                    fakeODRecord("Group 03"),
+                    fakeODRecord("Group 04"),
+                ],
+                DirectoryService.recordType_resources: [
+                    fakeODRecord("Resource 01"),
+                    fakeODRecord("Resource 02"),
+                    fakeODRecord("Resource 03"),
+                    fakeODRecord("Resource 04"),
+                ],
+                DirectoryService.recordType_locations: [
+                    fakeODRecord("Location 01"),
+                    fakeODRecord("Location 02"),
+                    fakeODRecord("Location 03"),
+                    fakeODRecord("Location 04"),
+                ],
+            })
+
+            self.assertTrue(self.service.recordWithAuthID("Kerberos:user01 at example.com"))
+            self.verifyQuery(self.service.recordWithAuthID, "Kerberos:user05 at example.com")
+            self.verifyNoQuery(self.service.recordWithAuthID, "Kerberos:user05 at example.com")
+
+            self.assertTrue(self.service.recordWithAuthID("Kerberos:group01 at example.com"))
+            self.verifyQuery(self.service.recordWithAuthID, "Kerberos:group05 at example.com")
+            self.verifyNoQuery(self.service.recordWithAuthID, "Kerberos:group05 at example.com")
+
+            self.assertTrue(self.service.recordWithAuthID("Kerberos:resource01 at example.com"))
+            self.verifyQuery(self.service.recordWithAuthID, "Kerberos:resource05 at example.com")
+            self.verifyNoQuery(self.service.recordWithAuthID, "Kerberos:resource05 at example.com")
+
+            self.assertTrue(self.service.recordWithAuthID("Kerberos:location01 at example.com"))
+            self.verifyQuery(self.service.recordWithAuthID, "Kerberos:location05 at example.com")
+            self.verifyNoQuery(self.service.recordWithAuthID, "Kerberos:location05 at example.com")
+
         def test_negativeCacheEmailAddress(self):
             self.loadRecords({
                 DirectoryService.recordType_users: [
@@ -697,6 +757,7 @@
         dsattributes.kDS1AttrDistinguishedName: fullName,
         dsattributes.kDS1AttrGeneratedUID: guid,
         dsattributes.kDSNAttrRecordName: shortName,
+        dsattributes.kDSNAttrAltSecurityIdentities: "Kerberos:%s" % (email,),
         dsattributes.kDSNAttrEMailAddress: email,
         dsattributes.kDSNAttrMetaNodeLocation: "/LDAPv3/127.0.0.1",
     }

Modified: CalendarServer/trunk/twistedcaldav/directory/test/test_principal.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/test_principal.py	2009-02-24 04:09:21 UTC (rev 3720)
+++ CalendarServer/trunk/twistedcaldav/directory/test/test_principal.py	2009-02-24 04:22:48 UTC (rev 3721)
@@ -16,6 +16,7 @@
 
 import os
 
+from twisted.cred.credentials import UsernamePassword
 from twisted.internet.defer import inlineCallbacks
 from twisted.web2.dav import davxml
 from twisted.web2.dav.fileop import rmdir
@@ -147,6 +148,19 @@
                 self.failIf(userResource is None)
                 self.assertEquals(user, userResource.record)
 
+    def test_principalForAuthID(self):
+        """
+        DirectoryPrincipalProvisioningResource.principalForAuthID()
+        """
+        for directory in directoryServices:
+            provisioningResource = self.principalRootResources[directory.__class__.__name__]
+
+            for user in directory.listRecords(DirectoryService.recordType_users):
+                creds = UsernamePassword(user.shortNames[0], "bogus")
+                userResource = provisioningResource.principalForAuthID(creds)
+                self.failIf(userResource is None)
+                self.assertEquals(user, userResource.record)
+
     def test_principalForUID(self):
         """
         DirectoryPrincipalProvisioningResource.principalForUID()

Modified: CalendarServer/trunk/twistedcaldav/directory/wiki.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/wiki.py	2009-02-24 04:09:21 UTC (rev 3720)
+++ CalendarServer/trunk/twistedcaldav/directory/wiki.py	2009-02-24 04:22:48 UTC (rev 3721)
@@ -119,15 +119,11 @@
             service=service,
             recordType=recordType,
             guid=None,
-            uid="%s%s" % (WikiDirectoryService.UIDPrefix, shortName),
             shortNames=(shortName,),
             fullName=shortName,
-            firstName="",
-            lastName="",
-            emailAddresses=set(),
-            calendarUserAddresses=set(),
-            autoSchedule=False,
-            enabledForCalendaring=True)
+            enabledForCalendaring=True,
+            uid="%s%s" % (WikiDirectoryService.UIDPrefix, shortName),
+        )
 
 
 

Modified: CalendarServer/trunk/twistedcaldav/extensions.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/extensions.py	2009-02-24 04:09:21 UTC (rev 3720)
+++ CalendarServer/trunk/twistedcaldav/extensions.py	2009-02-24 04:22:48 UTC (rev 3721)
@@ -37,14 +37,17 @@
 from twisted.internet.defer import succeed, DeferredList, inlineCallbacks, returnValue
 from twisted.internet.defer import maybeDeferred
 from twisted.web2 import responsecode
+from twisted.web2.auth.wrapper import UnauthorizedResponse
 from twisted.web2.http import HTTPError, Response, RedirectResponse
 from twisted.web2.http import StatusResponse
 from twisted.web2.http_headers import MimeType
 from twisted.web2.stream import FileStream
 from twisted.web2.static import MetaDataMixin
 from twisted.web2.dav import davxml
+from twisted.web2.dav.auth import PrincipalCredentials
 from twisted.web2.dav.davxml import dav_namespace
 from twisted.web2.dav.http import MultiStatusResponse
+from twisted.web2.dav.idav import IDAVPrincipalResource
 from twisted.web2.dav.static import DAVFile as SuperDAVFile
 from twisted.web2.dav.resource import DAVResource as SuperDAVResource
 from twisted.web2.dav.resource import DAVPrincipalResource as SuperDAVPrincipalResource
@@ -99,39 +102,118 @@
     Mixin class to let DAVResource, and DAVFile subclasses below know
     about sudoer principals and how to find their AuthID
     """
+
+    @inlineCallbacks
     def authenticate(self, request):
         # Bypass normal authentication if its already been done (by SACL check)
         if (hasattr(request, "authnUser") and
             hasattr(request, "authzUser") and
             request.authnUser is not None and
             request.authzUser is not None):
-            return (request.authnUser, request.authzUser)
+            returnValue((request.authnUser, request.authzUser))
+
+        # Copy of SuperDAVResource.authenticate except we pass the creds on as well
+        # as we will need to take different actions based on what the auth method was
+        if not (
+            hasattr(request, 'portal') and 
+            hasattr(request, 'credentialFactories') and
+            hasattr(request, 'loginInterfaces')
+        ):
+            request.authnUser = davxml.Principal(davxml.Unauthenticated())
+            request.authzUser = davxml.Principal(davxml.Unauthenticated())
+            returnValue((request.authnUser, request.authzUser,))
+
+        authHeader = request.headers.getHeader('authorization')
+
+        if authHeader is not None:
+            if authHeader[0] not in request.credentialFactories:
+                log.err("Client authentication scheme %s is not provided by server %s"
+                        % (authHeader[0], request.credentialFactories.keys()))
+
+                response = (yield UnauthorizedResponse.makeResponse(
+                    request.credentialFactories,
+                    request.remoteAddr
+                ))
+                raise HTTPError(response)
+            else:
+                factory = request.credentialFactories[authHeader[0]]
+
+                creds = (yield factory.decode(authHeader[1], request))
+
+                # Try to match principals in each principal collection on the resource
+                authnPrincipal, authzPrincipal = (yield self.principalsForAuthID(request, creds))
+                authnPrincipal = IDAVPrincipalResource(authnPrincipal)
+                authzPrincipal = IDAVPrincipalResource(authzPrincipal)
+
+                pcreds = PrincipalCredentials(authnPrincipal, authzPrincipal, creds)
+
+                result = (yield request.portal.login(pcreds, None, *request.loginInterfaces))
+                request.authnUser = result[1]
+                request.authzUser = result[2]
+                returnValue((request.authnUser, request.authzUser,))
         else:
-            return super(SudoSACLMixin, self).authenticate(request)
+            request.authnUser = davxml.Principal(davxml.Unauthenticated())
+            request.authzUser = davxml.Principal(davxml.Unauthenticated())
+            returnValue((request.authnUser, request.authzUser,))
 
-    def findPrincipalForAuthID(self, authid):
+
+    def principalsForAuthID(self, request, creds):
         """
+        Return authentication and authorization prinicipal identifiers for the
+        authentication identifer passed in. In this implementation authn and authz
+        principals are the same.
+
+        @param request: the L{IRequest} for the request in progress.
+        @param creds: L{Credentials} or the principal to lookup.
+        @return: a deferred tuple of two tuples. Each tuple is
+            C{(principal, principalURI)} where: C{principal} is the L{Principal}
+            that is found; {principalURI} is the C{str} URI of the principal.
+            The first tuple corresponds to authentication identifiers,
+            the second to authorization identifiers.
+            It will errback with an HTTPError(responsecode.FORBIDDEN) if
+            the principal isn't found.
+        """
+        authnPrincipal = self.findPrincipalForAuthID(creds)
+
+        if authnPrincipal is None:
+            log.msg("Could not find the principal resource for user id: %s" % (creds.username,))
+            raise HTTPError(responsecode.FORBIDDEN)
+
+        d = self.authorizationPrincipal(request, creds.username, authnPrincipal)
+        d.addCallback(lambda authzPrincipal: (authnPrincipal, authzPrincipal))
+        return d
+
+    def findPrincipalForAuthID(self, creds):
+        """
         Return an authentication and authorization principal identifiers for 
         the authentication identifier passed in.  Check for sudo users before
         regular users.
         """
+        
+        if type(creds) is str:
+            return super(SudoSACLMixin, self).findPrincipalForAuthID(creds)
+
         for collection in self.principalCollections():
             principal = collection.principalForShortName(
                 SudoDirectoryService.recordType_sudoers, 
-                authid)
+                creds.username)
             if principal is not None:
                 return principal
 
-        return super(SudoSACLMixin, self).findPrincipalForAuthID(authid)
+        for collection in self.principalCollections():
+            principal = collection.principalForAuthID(creds)
+            if principal is not None:
+                return principal
+        return None
 
     @inlineCallbacks
-    def authorizationPrincipal(self, request, authid, authnPrincipal):
+    def authorizationPrincipal(self, request, authID, authnPrincipal):
         """
         Determine the authorization principal for the given request and authentication principal.
         This implementation looks for an X-Authorize-As header value to use as the authorization principal.
         
         @param request: the L{IRequest} for the request in progress.
-        @param authid: a string containing the authentication/authorization identifier
+        @param authID: a string containing the authentication/authorization identifier
             for the principal to lookup.
         @param authnPrincipal: the L{IDAVPrincipal} for the authenticated principal
         @return: a deferred result C{tuple} of (L{IDAVPrincipal}, C{str}) containing the authorization principal
@@ -152,16 +234,15 @@
                 if principal:
                     return principal
 
-        def isSudoPrincipal(authid):
-            if getPrincipalForType(SudoDirectoryService.recordType_sudoers, 
-                                   authid):
+        def isSudoUser(authzID):
+            if getPrincipalForType(SudoDirectoryService.recordType_sudoers, authzID):
                 return True
             return False
 
-        if isSudoPrincipal(authid):
+        if hasattr(authnPrincipal, "record") and authnPrincipal.record.recordType == SudoDirectoryService.recordType_sudoers:
             if authz:
-                if isSudoPrincipal(authz):
-                    log.msg("Cannot proxy as another proxy: user '%s' as user '%s'" % (authid, authz))
+                if isSudoUser(authz):
+                    log.msg("Cannot proxy as another proxy: user '%s' as user '%s'" % (authID, authz))
                     raise HTTPError(responsecode.FORBIDDEN)
                 else:
                     authzPrincipal = getPrincipalForType(
@@ -171,21 +252,21 @@
                         authzPrincipal = self.findPrincipalForAuthID(authz)
 
                     if authzPrincipal is not None:
-                        log.msg("Allow proxy: user '%s' as '%s'" % (authid, authz,))
+                        log.msg("Allow proxy: user '%s' as '%s'" % (authID, authz,))
                         returnValue(authzPrincipal)
                     else:
                         log.msg("Could not find authorization user id: '%s'" % 
                                 (authz,))
                         raise HTTPError(responsecode.FORBIDDEN)
             else:
-                log.msg("Cannot authenticate proxy user '%s' without X-Authorize-As header" % (authid, ))
+                log.msg("Cannot authenticate proxy user '%s' without X-Authorize-As header" % (authID, ))
                 raise HTTPError(responsecode.BAD_REQUEST)
         elif authz:
-            log.msg("Cannot proxy: user '%s' as '%s'" % (authid, authz,))
+            log.msg("Cannot proxy: user '%s' as '%s'" % (authID, authz,))
             raise HTTPError(responsecode.FORBIDDEN)
         else:
             # No proxy - do default behavior
-            result = (yield super(SudoSACLMixin, self).authorizationPrincipal(request, authid, authnPrincipal))
+            result = (yield super(SudoSACLMixin, self).authorizationPrincipal(request, authID, authnPrincipal))
             returnValue(result)
 
 

Modified: CalendarServer/trunk/twistedcaldav/test/test_kerberos.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_kerberos.py	2009-02-24 04:09:21 UTC (rev 3720)
+++ CalendarServer/trunk/twistedcaldav/test/test_kerberos.py	2009-02-24 04:22:48 UTC (rev 3721)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2009 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -49,7 +49,7 @@
         )
 
     def test_NegotiateCredentials(self):
-        authkerb.NegotiateCredentials("test")
+        authkerb.NegotiateCredentials("test at EXAMPLE.COM", "test")
 
     @inlineCallbacks
     def test_NegotiateCredentialFactory(self):
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20090223/a39b3c56/attachment-0001.html>


More information about the calendarserver-changes mailing list