[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